Files
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

139 lines
4.2 KiB
Python

"""Testy formatów. Najważniejszy to round-trip po prawdziwych plikach gry:
jeśli parser + writer nie odtwarzają bajtów, edytor po cichu psuje zapis.
Ustaw RIC_GAME żeby uruchomić testy korpusowe:
RIC_GAME="$HOME/Reksio/Reksio i Czarodzieje" pytest
"""
from __future__ import annotations
import os
from pathlib import Path
import pytest
from ricsave import arr, catalog, dta
from ricsave.slot import Game
GAME_ROOT = os.environ.get("RIC_GAME")
needs_game = pytest.mark.skipif(not GAME_ROOT, reason="brak RIC_GAME")
# ---- format ARR ----------------------------------------------------------
def test_arr_empty():
assert arr.dumps([]) == b"\x00\x00\x00\x00"
assert arr.loads(b"\x00\x00\x00\x00") == []
def test_arr_mixed_roundtrip():
values = [0, -1, 2**31 - 1, "GULDRYK", "", "11A"]
assert arr.loads(arr.dumps(values)) == values
def test_arr_rejects_trailing_garbage():
with pytest.raises(arr.ArrError, match="śmieci"):
arr.loads(b"\x00\x00\x00\x00\xff")
def test_arr_rejects_unknown_type():
with pytest.raises(arr.ArrError, match="nieznany typ"):
arr.loads(b"\x01\x00\x00\x00" + b"\x09\x00\x00\x00")
def test_arr_rejects_bool():
# bool jest podklasą int — bez jawnej kontroli zapisałby się jako 0/1 po cichu
with pytest.raises(arr.ArrError, match="bool"):
arr.dumps([True])
# ---- format DTA ----------------------------------------------------------
def test_dta_roundtrip_with_trailing_newline():
raw = b"0|NULL|0|NULL|0\r\n1|BUTY|3|CAT|1\r\n"
table = dta.loads(raw)
assert len(table) == 2
assert table[1] == ["1", "BUTY", "3", "CAT", "1"]
assert dta.dumps(table) == raw
def test_dta_roundtrip_without_trailing_newline():
"""Pliki *_def.dta nie mają końcowego CRLF — writer musi to uszanować."""
raw = b"A|B\r\nC|D"
table = dta.loads(raw)
assert table.trailing_newline is False
assert dta.dumps(table) == raw
def test_dta_tolerates_ragged_rows_but_check_width_catches_them():
"""fighters.dta ma wiersze 4- i 5-kolumnowe, więc parser nie może być surowy.
Kontrola szerokości należy do warstwy schematu."""
table = dta.loads(b"A|B|C\r\nD|E\r\n")
assert table.widths == {3, 2}
with pytest.raises(dta.DtaError, match="oczekiwano 3"):
table.check_width(3, "test")
def test_dta_rejects_unix_newlines():
with pytest.raises(dta.DtaError, match="LF"):
dta.loads(b"A|B\nC|D\n")
# ---- korpus: prawdziwe pliki gry ----------------------------------------
@needs_game
def test_all_arr_files_roundtrip_byte_for_byte():
common = Game(GAME_ROOT).common
checked = 0
for path in common.iterdir():
if path.suffix.lower() not in (".arr", ".sav"):
continue
raw = path.read_bytes()
assert arr.dumps(arr.loads(raw)) == raw, f"round-trip nie wyszedł: {path.name}"
checked += 1
assert checked > 50, f"sprawdzono tylko {checked} plików .ARR — coś nie tak ze ścieżką"
@needs_game
def test_all_dta_files_roundtrip_byte_for_byte():
common = Game(GAME_ROOT).common
checked = 0
for path in common.iterdir():
if path.suffix.lower() != ".dta":
continue
raw = path.read_bytes()
assert dta.dumps(dta.loads(raw)) == raw, f"round-trip nie wyszedł: {path.name}"
checked += 1
assert checked > 100, f"sprawdzono tylko {checked} plików .DTA"
@needs_game
def test_manifest_matches_files_on_disk():
game = Game(GAME_ROOT)
missing = [
e.filename(0) for e in catalog.MANIFEST
if not game.path(e, 0).exists()
]
assert not missing, f"manifest wymienia pliki, których nie ma: {missing}"
@needs_game
def test_scene_tables_have_sobject_width():
slot = Game(GAME_ROOT).slot(0)
for name in catalog.SCENES:
table = slot.raw(f"scene:{name}")
assert table.width == 9, f"{name}: {table.width} kolumn zamiast 9 (SOBJECT)"
@needs_game
def test_items_table_shape():
items = Game(GAME_ROOT).slot(0).items
assert len(items.table) == catalog.ITEM_SLOTS
assert items.table.width == 5
@needs_game
def test_live_slot_validates():
problems = Game(GAME_ROOT).slot(0).validate()
assert problems == [], problems