Add web-based save editor for "Reksio i Czarodzieje"
- Implemented a local web server using http.server to serve the save editor. - Created a user interface with HTML, CSS, and JavaScript for managing game slots and editing save files. - Added API endpoints for retrieving slot information, overview, and raw data for editing. - Implemented functionality for saving changes back to the game files. - Added tests for ARR and DTA formats to ensure data integrity during round-trip serialization. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
"""Manifest slotu zapisu — co, gdzie i pod jaką nazwą.
|
||||
|
||||
Wszystkie listy pochodzą wprost ze skryptów gry:
|
||||
|
||||
SCENES Game.cnv → G_ARRDATAS (BFITMP125)
|
||||
CHARACTERS MAINMENU.class:652 → M_ARRCHARACTERS (CONSTRUCTOR)
|
||||
MINIGAMES MAINMENU.class:652 → M_ARRPIOTR (CONSTRUCTOR)
|
||||
układ plików slotu → SAVEGAME (:667) i LOADGAME (:672)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
#: Lokacje mające własny plik <NAZWA><slot>.DTA. Kolejność jak w G_ARRDATAS.
|
||||
SCENES: tuple[str, ...] = (
|
||||
"PODWIECZOREK1", "PODWIECZOREK2", "PODWIECZOREK3", "TRZYWEJSCIA",
|
||||
"DOMALCHOMIKA", "GABINETDYR", "HOLGLOWNY", "UNIVFRONT",
|
||||
"BIALO", "MIASTO", "LUSTRO", "TELEP_PODWORKO", "TELEP_WARSZTAT", "TELEP_KURNIKI",
|
||||
"PIWNICA", "ALCHEMY", "RATUSZ", "LABIRYNTH", "PIKLIBIA", "DOMSPIEL", "PRZEPASC",
|
||||
"MIOTLISKO", "KAPTUREK", "CALINECZKA", "DRATEWKA", "SNIEZKA", "KROLEWNA", "JAGA",
|
||||
"OLBRZYM", "SZREKSIO", "TROL", "NORASZ", "SEZAM", "PRZEDSEZAMEM", "KROLOWA", "SMOK",
|
||||
"RATUSZIN", "GULDRYK", "KORYTARZ", "SALA", "UNIVBACK", "UNIVPIWNICA", "MIOTSHOP",
|
||||
"PRZYSTAN", "GABINETSNEJKA", "BARANMIOT", "WMWEJSCIE", "WMWNETRZE", "PIKMIOT",
|
||||
"SKLEPALCH", "ARRAS", "POMPA",
|
||||
)
|
||||
|
||||
#: Sceny samodzielne z Application.def (PRZYGODA:SCENES) — intro, outro, minigry,
|
||||
#: dialogi. Nie mają własnego .DTA. G_SARCADEOBJECTS może wskazywać i na nie:
|
||||
#: _LOADGAME_ sprawdza G_ARRDATAS^FIND() i albo podmienia .DTA w scenie ARCADE,
|
||||
#: albo robi PRZYGODA^GOTO() do sceny o tej nazwie (Arcade.cnv:1533).
|
||||
STANDALONE_SCENES: tuple[str, ...] = (
|
||||
"ARCADE", "INTRO_1", "INTRO_2", "INTRO_3", "INTRO_4", "MAGIC", "HILLS", "LIMBO",
|
||||
"DIALOGS", "CHOICE", "START", "LEBIODKA", "C_UCIECZKA", "CREDITS",
|
||||
"OUTRO_1", "OUTRO_2", "OUTRO_3", "OUTRO_4", "OUTRO_5", "OUTRO_6", "OUTRO_7",
|
||||
"OFERTA", "PRZYLOT", "PRZYLOT_2", "TELE_1", "TELE_2", "KONTROLA",
|
||||
"BARANDALF", "CAT", "FRED", "KUBKI", "MIOTLY", "LABIRYNT", "DRAGON", "SHOOTER",
|
||||
)
|
||||
|
||||
#: Wszystko, co może legalnie stać w G_SARCADEOBJECTS.
|
||||
VALID_LOCATIONS: frozenset[str] = frozenset(SCENES) | frozenset(STANDALONE_SCENES)
|
||||
|
||||
#: Postacie z historią dialogową <POSTAĆ>_HIST<slot>.ARR.
|
||||
CHARACTERS: tuple[str, ...] = (
|
||||
"BUREKTOR", "GULDRYK", "SNEJK", "WALDIMORS", "SPIELMAUSTER", "CHRUMBURAK",
|
||||
"BARANDALF", "KROLOWA", "KAMIEN", "SMOK", "GASIENICA",
|
||||
)
|
||||
|
||||
#: Minigry z własnym <MINIGRA><slot>.ARR.
|
||||
MINIGAMES: tuple[str, ...] = (
|
||||
"LABIRYNT", "MIOTLY", "CAT", "BARANDALF", "DRAGON", "SHOOTER",
|
||||
)
|
||||
|
||||
#: Sloty widoczne w menu gry. Slot 0 to żywy stan rozgrywki, nie pozycja w menu.
|
||||
MENU_SLOTS: tuple[int, ...] = (1, 2, 3, 4)
|
||||
LIVE_SLOT = 0
|
||||
|
||||
#: Stała liczba slotów ekwipunku — pętle w Arcade.cnv są zahardkodowane na 0..10.
|
||||
ITEM_SLOTS = 10
|
||||
|
||||
#: Wymagana długość SPELLS<slot>.ARR; krótsza → gra po cichu resetuje.
|
||||
SPELLS_LEN = 17
|
||||
|
||||
#: Wymagana długość GAME<slot>.ARR; krótsza → DEFSETS, czyli utrata pozycji.
|
||||
GAMESETS_LEN = 6
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Entry:
|
||||
"""Jeden plik należący do slotu."""
|
||||
|
||||
key: str # stabilny identyfikator, np. "scene:GULDRYK"
|
||||
kind: str # "dta" | "arr"
|
||||
template: str # nazwa pliku z {slot} do podstawienia
|
||||
label: str
|
||||
|
||||
def filename(self, slot: int) -> str:
|
||||
return self.template.format(slot=slot)
|
||||
|
||||
|
||||
def manifest() -> list[Entry]:
|
||||
"""Pełna lista plików tworzących jeden slot (bez miniaturki m_shot)."""
|
||||
entries: list[Entry] = [
|
||||
Entry("game", "arr", "GAME{slot}.ARR", "Zmienne globalne rozgrywki"),
|
||||
Entry("items", "dta", "ITEMS{slot}.DTA", "Ekwipunek"),
|
||||
Entry("spells", "arr", "SPELLS{slot}.ARR", "Znane czary"),
|
||||
Entry("cutscenes", "arr", "CUTSCENES{slot}.ARR", "Obejrzane przerywniki"),
|
||||
Entry("invest", "arr", "INVEST{slot}.ARR", "Karta śledztwa"),
|
||||
Entry("questions", "arr", "QUESTIONS{slot}.ARR", "Dostępne pytania"),
|
||||
Entry("questions_asked", "arr", "QUESTIONS_ASKED{slot}.ARR", "Zadane pytania"),
|
||||
]
|
||||
entries += [
|
||||
Entry(f"scene:{name}", "dta", name + "{slot}.DTA", f"Lokacja {name}")
|
||||
for name in SCENES
|
||||
]
|
||||
entries += [
|
||||
Entry(f"hist:{name}", "arr", name + "_HIST{slot}.ARR", f"Rozmowy: {name}")
|
||||
for name in CHARACTERS
|
||||
]
|
||||
entries += [
|
||||
Entry(f"minigame:{name}", "arr", name + "{slot}.ARR", f"Minigra {name}")
|
||||
for name in MINIGAMES
|
||||
]
|
||||
return entries
|
||||
|
||||
|
||||
MANIFEST: tuple[Entry, ...] = tuple(manifest())
|
||||
BY_KEY: dict[str, Entry] = {e.key: e for e in MANIFEST}
|
||||
|
||||
|
||||
def screenshot_filename(slot: int) -> str:
|
||||
"""Miniaturka zapisu pokazywana w menu — istnieje tylko dla slotów 1..4."""
|
||||
return f"m_shot{slot}.img"
|
||||
|
||||
|
||||
#: Pliki stanu leżące w common/, ale POZA manifestem slotu — nie są kopiowane przy
|
||||
#: zapisie/wczytaniu, więc przeciekają między rozgrywkami. Głównie alchemia.
|
||||
UNSLOTTED_GLOBS: tuple[str, ...] = (
|
||||
"SETTINGS.ARR", # globalne ustawienia: [?, głośność 0..800]
|
||||
"SLOT_*.SAV", # utrwalone mikstury (Alchemy.cnv:BEHSAVESLOTS)
|
||||
"DODOS_*.SAV", # stan dodo (Alchemy.cnv:BEHSAVEDODOS)
|
||||
)
|
||||
Reference in New Issue
Block a user