- 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>
94 lines
3.1 KiB
Python
94 lines
3.1 KiB
Python
"""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))
|