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:
@@ -0,0 +1,138 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user