Files
Patryk GenschandCodex edd8ceda9e 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>
2026-08-03 01:18:08 +02:00

123 lines
5.0 KiB
Python

"""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)
)