Files
rex-wizards-save-editor/ricsave/schema.py
T
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

125 lines
4.6 KiB
Python

"""Schematy: nazwy kolumn .DTA i znaczenie indeksów w .ARR.
Kolumny .DTA nie są zapisane w pliku — pochodzą z definicji STRUCT w Arcade.cnv
(linie 1455-1489). Znaczenia pól ARR z MAINMENU.class (DEFSETS, SAVEGAME).
"""
from __future__ import annotations
from dataclasses import dataclass
INT = "INTEGER"
STR = "STRING"
@dataclass(frozen=True)
class Column:
name: str
type: str
note: str = ""
#: SOBJECT — model bazy obiektów sceny (<SCENA><slot>.DTA).
SOBJECT: tuple[Column, ...] = (
Column("NAME", STR, "ścieżka do .ANN/.IMG/.SEK względem katalogu sceny"),
Column("IDNAME", STR, "identyfikator logiczny, używany przez skrypty sceny"),
Column("TYPE", INT, "rola wiersza — patrz OBJECT_TYPES"),
Column("SPARAM0", STR),
Column("SPARAM1", STR),
Column("SPARAM2", STR),
Column("IPARAM0", INT, "dla TYPE==2: 1 = widoczny, 0 = zabrany"),
Column("IPARAM1", INT),
Column("IPARAM2", INT),
)
#: SITEM — model ekwipunku (ITEMS<slot>.DTA).
SITEM: tuple[Column, ...] = (
Column("GUID", INT, "indeks slotu 0..9, równy numerowi wiersza"),
Column("NAME", STR, "identyfikator przedmiotu, NULL gdy pusto"),
Column("PARENT", INT, "indeks wiersza w .DTA lokacji, z której podniesiono (-1 = brak)"),
Column("BASE", STR, "lokacja pochodzenia"),
Column("EMPTY", INT, "MYLĄCA NAZWA: 1 = slot ZAJĘTY, 0 = wolny"),
)
DTA_SCHEMAS: dict[str, tuple[Column, ...]] = {
"scene": SOBJECT,
"items": SITEM,
}
#: Kolumna TYPE w SOBJECT — dekoder z Arcade.cnv:__LOAD_DB__.
OBJECT_TYPES: dict[int, str] = {
101: "tło statyczne (.IMG)",
102: "maska ścieżek chodzenia (.SEK)",
103: "aktywacja przejścia: WPATH^SETACTIVE(IPARAM0, IPARAM1, IPARAM2)",
104: "Reksio; IPARAM0 = prędkość maks.",
105: "punkt startowy, zestaw 0; IDNAME = scena skąd, IPARAM0/1 = X/Y",
106: "punkt docelowy, zestaw 0; IPARAM0/1 = X/Y",
107: "tło animowane; IPARAM0/1 = szerokość/wysokość",
108: "muzyka lokacji; IDNAME = plik .WAV",
140: "Kret",
145: "punkt startowy, zestaw 1",
146: "punkt docelowy, zestaw 1",
}
def describe_object_type(value: int) -> str:
if value in OBJECT_TYPES:
return OBJECT_TYPES[value]
if value < 100:
extra = " — IPARAM0 to flaga widoczności" if value == 2 else ""
return f"obiekt sceny (ANNOBJECT), podtyp {value}{extra}"
return f"nieznany typ {value}"
#: GAME<slot>.ARR — 6 elementów, wszystkie zapisane jako STRING.
GAMESETS: tuple[Column, ...] = (
Column("G_SARCADEOBJECTS", STR, "bieżąca lokacja"),
Column("G_SLASTOBJECTS", STR, "poprzednia lokacja — wyznacza punkt wejścia"),
Column("G_SDIALOGCHARACTER", STR, "rozmówca"),
Column("G_SDIALOGRETURN", STR, "dokąd wrócić po rozmowie"),
Column("G_ITKTPM", INT, "losowe 0..7, ustalane raz przy nowej grze"),
Column("G_IKRETACTIVE", INT, "czy Kret towarzyszy Reksiowi (0/1)"),
)
#: Stan fabryczny GAME0.ARR z DEFSETS (indeks 4 jest losowany 0..7).
GAMESETS_DEFAULT: list[str] = ["INTRO_1", "NULL", "BUREKTOR", "NULL", "0", "0"]
#: SPELLS<slot>.ARR — 17 intów. Stan fabryczny z NEWGAME.
SPELLS_DEFAULT: list[int] = [0] * 12 + [64] + [0] * 4
#: Próg doświadczenia na jeden poziom (MAINMENU.class: LEVELUP, BFITMP57/59/61).
EXP_PER_LEVEL = 150
#: Ile klatek animacji klepsydry — klatka = exp // 10 + 1 (BFITMP55).
EXP_PER_HOURGLASS_FRAME = 10
#: Indeksy 0..11 to flagi „czar znany" (0/1). Sześć czarów w wersji zwykłej (0-5)
#: i obronnej (6-11, sufiks _OBR); czar N ma obronę N+6. Nazwy z uchwytów klikania
#: kul w gabinetach — Guldryk.cnv, Gabinetsnejka.cnv, Wmwnetrze.cnv.
SPELL_NAMES: tuple[str, ...] = (
"Sen", "Zamiana w żabę", "Plaga much", "Kula budyniu", "Koła i wiry", "Ciemność",
"Obrona przed Snem", "Obrona przed Zamianą w żabę", "Obrona przed Plagą much", "Obrona przed Kulą budyniu",
"Obrona przed Kołami i wirami", "Obrona przed Ciemnością",
)
#: Indeksy 12..16 — liczniki i flagi stanu klepsydry.
SPELLS_NOTES: dict[int, str] = {
12: "startuje na 64, SUBAT(12,3) przy każdym awansie — nikt tego nie czyta",
13: f"DOŚWIADCZENIE, 0..{EXP_PER_LEVEL - 1}; po przekroczeniu progu reszta przechodzi dalej",
14: "licznik awansów (ile razy klepsydra się przesypała)",
15: "1 → klepsydra/różdżka widoczna w menu (SHOWEXP, M_WAND)",
16: "1 → klepsydra miga, można nauczyć się czaru",
}
#: Indeks doświadczenia w SPELLS<slot>.ARR.
EXP_INDEX = 13
LEVEL_INDEX = 14
HOURGLASS_VISIBLE_INDEX = 15
HOURGLASS_BLINKING_INDEX = 16
def describe_spell_index(index: int) -> str:
if index < len(SPELL_NAMES):
return f"{SPELL_NAMES[index]} (1 = znany)"
return SPELLS_NOTES.get(index, "nieznane")