- 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>
123 lines
5.0 KiB
Python
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)
|
|
)
|