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:
Patryk Gensch
2026-08-03 01:18:08 +02:00
co-authored by Codex
commit edd8ceda9e
17 changed files with 2572 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
"""Obiekt DATABASE silnika PIK — pliki .DTA.
Wiersze rozdzielone CRLF, kolumny pionową kreską, brak nagłówka. Nazwy i typy kolumn
żyją w definicji STRUCT w pliku .cnv — patrz ricsave.schema.
Puste pole tekstowe zapisywane jest jako literalne NULL.
"""
from __future__ import annotations
from pathlib import Path
ENCODING = "cp1250"
ROW_SEP = "\r\n"
COL_SEP = "|"
NULL = "NULL"
class DtaError(ValueError):
"""Plik .DTA nie trzyma się formatu."""
class Table:
"""Tabela .DTA: lista wierszy, każdy to lista surowych stringów.
Świadomie trzymamy surowe stringi — konwersję na int robi dopiero warstwa schematu,
dzięki czemu zapis jest bajt-w-bajt identyczny gdy nic nie zmieniasz.
"""
def __init__(self, rows: list[list[str]], *, trailing_newline: bool = True) -> None:
self.rows = rows
# Pliki *_def.dta nie mają końcowego CRLF, te zapisane przez grę mają.
self.trailing_newline = trailing_newline
@property
def width(self) -> int:
return len(self.rows[0]) if self.rows else 0
@property
def widths(self) -> set[int]:
return {len(row) for row in self.rows}
def check_width(self, expected: int, label: str = "tabela") -> None:
"""Waliduje szerokość wobec znanego schematu STRUCT."""
found = self.widths
if found - {expected}:
raise DtaError(
f"{label}: oczekiwano {expected} kolumn, znaleziono {sorted(found)}"
)
def __len__(self) -> int:
return len(self.rows)
def __getitem__(self, index: int) -> list[str]:
return self.rows[index]
def __repr__(self) -> str:
return f"<Table {len(self.rows)}×{self.width}>"
def loads(data: bytes) -> Table:
text = data.decode(ENCODING)
trailing = text.endswith(ROW_SEP)
body = text[: -len(ROW_SEP)] if trailing else text
if not body:
return Table([], trailing_newline=trailing)
if "\r\n" not in body and "\n" in body:
raise DtaError("wiersze rozdzielone samym LF — to nie jest plik .DTA z gry")
# Bez walidacji szerokości: część statycznych tabel gry (np. fighters.dta) ma
# wiersze różnej długości. Szerokość sprawdzamy tam, gdzie znamy STRUCT —
# przez Table.check_width().
rows = [line.split(COL_SEP) for line in body.split(ROW_SEP)]
return Table(rows, trailing_newline=trailing)
def dumps(table: Table) -> bytes:
text = ROW_SEP.join(COL_SEP.join(row) for row in table.rows)
if table.trailing_newline and table.rows:
text += ROW_SEP
return text.encode(ENCODING)
def read(path: str | Path) -> Table:
return loads(Path(path).read_bytes())
def write(path: str | Path, table: Table) -> None:
Path(path).write_bytes(dumps(table))