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
+93
View File
@@ -0,0 +1,93 @@
"""Obiekt ARRAY silnika PIK — pliki .ARR i .SAV.
Format (little-endian, bez nagłówka):
uint32 count
count × { uint32 type; type==1 ? int32 : (uint32 len; bytes[len]) }
type 1 = INTEGER, type 2 = STRING (cp1250, bez terminatora).
"""
from __future__ import annotations
import struct
from pathlib import Path
ENCODING = "cp1250"
TYPE_INT = 1
TYPE_STR = 2
TYPE_BOOL = 3
TYPE_FLOAT = 4
Value = int | str
class ArrError(ValueError):
"""Plik .ARR nie trzyma się formatu."""
def loads(data: bytes) -> list[Value]:
if len(data) < 4:
raise ArrError(f"plik ma {len(data)} B, minimum to 4 B (samo count)")
(count,) = struct.unpack_from("<I", data, 0)
off = 4
out: list[Value] = []
for i in range(count):
try:
(kind,) = struct.unpack_from("<I", data, off)
off += 4
if kind == TYPE_INT:
(value,) = struct.unpack_from("<i", data, off)
off += 4
out.append(value)
elif kind == TYPE_STR:
(length,) = struct.unpack_from("<I", data, off)
off += 4
raw = data[off : off + length]
if len(raw) != length:
raise ArrError(
f"element {i}: deklarowana długość {length} B, dostępne {len(raw)} B"
)
off += length
out.append(raw.decode(ENCODING))
elif kind == TYPE_BOOL:
(value,) = struct.unpack_from("<i", data, off)
off += 4
out.append(value != 0)
elif kind == TYPE_FLOAT:
(value,) = struct.unpack_from("<i", data, off)
off += 4
out.append(value / 1000) # w Reksio i Czarodzieje dzielone jest przez 1000
else:
raise ArrError(f"element {i} @0x{off - 4:x}: nieznany typ {kind}")
except struct.error as exc:
raise ArrError(f"element {i}: plik urwany @0x{off:x}") from exc
if off != len(data):
raise ArrError(f"{len(data) - off} B śmieci na końcu (po {count} elementach)")
return out
def dumps(values: list[Value]) -> bytes:
parts = [struct.pack("<I", len(values))]
for i, value in enumerate(values):
if isinstance(value, bool):
parts.append(struct.pack("<Ii", TYPE_BOOL, 1 if value else 0))
elif isinstance(value, int):
parts.append(struct.pack("<Ii", TYPE_INT, value))
elif isinstance(value, str):
raw = value.encode(ENCODING)
parts.append(struct.pack("<II", TYPE_STR, len(raw)) + raw)
elif isinstance(value, float):
parts.append(struct.pack("<Ii", TYPE_FLOAT, int(value * 1000))) # w Reksio i Czarodzieje mnożone jest przez 1000
else:
raise ArrError(f"element {i}: typ {type(value).__name__} nieobsługiwany")
return b"".join(parts)
def read(path: str | Path) -> list[Value]:
return loads(Path(path).read_bytes())
def write(path: str | Path, values: list[Value]) -> None:
Path(path).write_bytes(dumps(values))