mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
pqcli: add --import-save and --export-save
--import-save FILE adds the character in a .pqw file to the roster; it can be given more than once, and if any file fails, nothing is imported. --export-save NUM FILE writes a character out. Export never overwrites an existing file. The file handling lives in pqw.read_file and pqw.write_file, so the interfaces can share it.
This commit is contained in:
4 files changed
+178
-1
No files matched your search
@@ -5,6 +5,7 @@ from typing import TextIO
|
|||||||
|
|
||||||
from xdg_base_dirs import xdg_config_home
|
from xdg_base_dirs import xdg_config_home
|
||||||
|
|
||||||
|
from pqcli import pqw
|
||||||
from pqcli.game_data import (
|
from pqcli.game_data import (
|
||||||
DataError,
|
DataError,
|
||||||
GameData,
|
GameData,
|
||||||
@@ -81,6 +82,25 @@ def parse_args() -> argparse.Namespace:
|
|||||||
metavar="DIR",
|
metavar="DIR",
|
||||||
help="copy the built-in word lists to DIR for editing and exit",
|
help="copy the built-in word lists to DIR for editing and exit",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--import-save",
|
||||||
|
type=Path,
|
||||||
|
action="append",
|
||||||
|
metavar="FILE",
|
||||||
|
help=(
|
||||||
|
"add the character in a Progress Quest web save (.pqw) to the "
|
||||||
|
"roster and exit; can be given more than once"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--export-save",
|
||||||
|
nargs=2,
|
||||||
|
metavar=("NUM", "FILE"),
|
||||||
|
help=(
|
||||||
|
"write the chosen character to a Progress Quest web save "
|
||||||
|
"(.pqw) and exit"
|
||||||
|
),
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -106,6 +126,40 @@ def bootstrap_first_run(
|
|||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def import_saves(
|
||||||
|
roster: Roster, data: GameData, paths: list[Path], use_saves: bool
|
||||||
|
) -> None:
|
||||||
|
imported = []
|
||||||
|
for path in paths:
|
||||||
|
try:
|
||||||
|
player, warnings = pqw.read_file(path, data)
|
||||||
|
except pqw.PqwError as ex:
|
||||||
|
print(f"pqcli: {ex}", file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
for warning in warnings:
|
||||||
|
print(f"pqcli: {path}: {warning}", file=sys.stderr)
|
||||||
|
imported.append(player)
|
||||||
|
roster.players.extend(imported)
|
||||||
|
if use_saves:
|
||||||
|
roster.save()
|
||||||
|
for player in imported:
|
||||||
|
print(f"Imported {player.name}")
|
||||||
|
|
||||||
|
|
||||||
|
def export_save(roster: Roster, data: GameData, num: str, file: str) -> None:
|
||||||
|
if not num.isdigit() or not 1 <= int(num) <= len(roster.players):
|
||||||
|
print("Invalid player. Available players:", file=sys.stderr)
|
||||||
|
list_players(roster, file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
player = roster.players[int(num) - 1]
|
||||||
|
try:
|
||||||
|
pqw.write_file(player, Path(file), data)
|
||||||
|
except pqw.PqwError as ex:
|
||||||
|
print(f"pqcli: {ex}", file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
print(f"Exported {player.name} to {file}")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
|
||||||
@@ -130,6 +184,14 @@ def main() -> None:
|
|||||||
list_players(roster)
|
list_players(roster)
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
|
if args.import_save:
|
||||||
|
import_saves(roster, data, args.import_save, args.use_saves)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
if args.export_save:
|
||||||
|
export_save(roster, data, *args.export_save)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
player: Player | None = None
|
player: Player | None = None
|
||||||
if args.load_save:
|
if args.load_save:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import binascii
|
|||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from pqcli.config import Class, EquipmentType, Monster, Race, StatType
|
from pqcli.config import Class, EquipmentType, Monster, Race, StatType
|
||||||
@@ -351,3 +352,29 @@ def export_game(player: Player, data: GameData) -> dict[str, Any]:
|
|||||||
"TaskBar": bar(player.task_bar.position, player.task_bar.max_),
|
"TaskBar": bar(player.task_bar.position, player.task_bar.max_),
|
||||||
"queue": [_encode_queue_entry(task) for task in player.queue],
|
"queue": [_encode_queue_entry(task) for task in player.queue],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(path: Path, data: GameData) -> tuple[Player, list[str]]:
|
||||||
|
"""Import the character in a .pqw file, plus any warnings."""
|
||||||
|
try:
|
||||||
|
text = path.read_text(encoding="ascii", errors="replace")
|
||||||
|
except OSError as ex:
|
||||||
|
raise PqwError(f"cannot read {path}: {ex.strerror}") from ex
|
||||||
|
return import_game(decode(text), data)
|
||||||
|
|
||||||
|
|
||||||
|
def write_file(player: Player, path: Path, data: GameData) -> None:
|
||||||
|
"""Export a character to a .pqw file, never overwriting one."""
|
||||||
|
text = encode(export_game(player, data))
|
||||||
|
try:
|
||||||
|
with path.open("x", encoding="ascii") as handle:
|
||||||
|
handle.write(text)
|
||||||
|
except FileExistsError as ex:
|
||||||
|
raise PqwError(f"{path} already exists") from ex
|
||||||
|
except OSError as ex:
|
||||||
|
raise PqwError(f"cannot write {path}: {ex.strerror}") from ex
|
||||||
|
|
||||||
|
|
||||||
|
def default_file_name(player: Player) -> str:
|
||||||
|
# the web version names its downloads the same way
|
||||||
|
return f"{player.name}.pqw"
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from test_pqw import played
|
||||||
|
|
||||||
|
from pqcli.game_data import load_game_data
|
||||||
|
from pqcli.pqw import read_file, write_file
|
||||||
|
|
||||||
|
|
||||||
|
def pqcli(
|
||||||
|
config: Path, *args: str, cwd: Path | None = None
|
||||||
|
) -> subprocess.CompletedProcess[str]:
|
||||||
|
env = {**os.environ, "XDG_CONFIG_HOME": str(config)}
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, "-m", "pqcli", *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
cwd=cwd,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_then_export(tmp_path: Path) -> None:
|
||||||
|
data = load_game_data()
|
||||||
|
source = tmp_path / "in.pqw"
|
||||||
|
write_file(played(3000).player, source, data)
|
||||||
|
|
||||||
|
result = pqcli(tmp_path, "--import-save", str(source))
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert result.stdout == "Imported Exportée\n"
|
||||||
|
assert (tmp_path / "pqcli" / "save.dat").exists()
|
||||||
|
|
||||||
|
listed = pqcli(tmp_path, "--list-saves")
|
||||||
|
assert listed.stdout == "1. Exportée\n"
|
||||||
|
|
||||||
|
target = tmp_path / "out.pqw"
|
||||||
|
result = pqcli(tmp_path, "--export-save", "1", str(target))
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert read_file(target, data)[0].name == "Exportée"
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_bad_file_changes_nothing(tmp_path: Path) -> None:
|
||||||
|
bad = tmp_path / "bad.pqw"
|
||||||
|
bad.write_text("garbage!")
|
||||||
|
result = pqcli(tmp_path, "--import-save", str(bad))
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "not a .pqw file" in result.stderr
|
||||||
|
assert not (tmp_path / "pqcli" / "save.dat").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_rejects_unknown_character(tmp_path: Path) -> None:
|
||||||
|
result = pqcli(tmp_path, "--export-save", "3", str(tmp_path / "x.pqw"))
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "Invalid player" in result.stderr
|
||||||
|
assert not (tmp_path / "x.pqw").exists()
|
||||||
+32
-1
@@ -1,6 +1,7 @@
|
|||||||
import base64
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from upstream import bridge
|
from upstream import bridge
|
||||||
@@ -8,7 +9,16 @@ from upstream import bridge
|
|||||||
from pqcli.config import StatType
|
from pqcli.config import StatType
|
||||||
from pqcli.game_data import load_game_data
|
from pqcli.game_data import load_game_data
|
||||||
from pqcli.mechanic import Player, Simulation, Stats
|
from pqcli.mechanic import Player, Simulation, Stats
|
||||||
from pqcli.pqw import PqwError, decode, encode, export_game, import_game
|
from pqcli.pqw import (
|
||||||
|
PqwError,
|
||||||
|
decode,
|
||||||
|
default_file_name,
|
||||||
|
encode,
|
||||||
|
export_game,
|
||||||
|
import_game,
|
||||||
|
read_file,
|
||||||
|
write_file,
|
||||||
|
)
|
||||||
from pqcli.random import Alea
|
from pqcli.random import Alea
|
||||||
|
|
||||||
DATA = load_game_data()
|
DATA = load_game_data()
|
||||||
@@ -131,3 +141,24 @@ def test_round_trip_keeps_playing_identically(ticks: int) -> None:
|
|||||||
assert bridge.run(resumed, 10**8, 2000) == bridge.run(
|
assert bridge.run(resumed, 10**8, 2000) == bridge.run(
|
||||||
original, 10**8, 2000
|
original, 10**8, 2000
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_files_round_trip(tmp_path: Path) -> None:
|
||||||
|
original = played(3000).player
|
||||||
|
path = tmp_path / default_file_name(original)
|
||||||
|
write_file(original, path, DATA)
|
||||||
|
copy, _warnings = read_file(path, DATA)
|
||||||
|
assert bridge.snapshot(copy) == bridge.snapshot(original)
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_file_never_overwrites(tmp_path: Path) -> None:
|
||||||
|
path = tmp_path / "taken.pqw"
|
||||||
|
path.write_text("mine")
|
||||||
|
with pytest.raises(PqwError, match="already exists"):
|
||||||
|
write_file(played(10).player, path, DATA)
|
||||||
|
assert path.read_text() == "mine"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_file_reports_missing_file(tmp_path: Path) -> None:
|
||||||
|
with pytest.raises(PqwError, match="cannot read"):
|
||||||
|
read_file(tmp_path / "nope.pqw", DATA)
|
||||||
Reference in new issue
Block a user