From 9708b7a31f22bcc50fba489d4923eb3b7587e728 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Sun, 5 Jul 2026 14:52:12 -0500 Subject: [PATCH] Fix unpacking --- gui/wolf_workflow_tab.py | 151 +++++++++++++++++++++++------ tests/test_wolf_project_scanner.py | 60 ++++++++++++ util/project_scanner.py | 51 +++++++++- 3 files changed, 230 insertions(+), 32 deletions(-) create mode 100644 tests/test_wolf_project_scanner.py diff --git a/gui/wolf_workflow_tab.py b/gui/wolf_workflow_tab.py index 084b244..f4ac291 100644 --- a/gui/wolf_workflow_tab.py +++ b/gui/wolf_workflow_tab.py @@ -86,7 +86,14 @@ from gui.workflow_tab import ( ) from util import wolf_names from util.paths import PROJECT_ROOT -from util.project_scanner import detect_wolf_layout, list_wolf_json_files +from util.project_scanner import ( + detect_wolf_layout, + find_wolf_text_archives, + list_wolf_json_files, + wolf_has_maps, + wolf_maps_dir, + wolf_maps_packed, +) from util.vocab import read_game_vocab, write_game_vocab # Workflow-level label for the non-batch (live) translation path. The Translation @@ -1007,6 +1014,26 @@ class WolfWorkflowTab(QWidget): manifest = self._read_manifest() if manifest and manifest.get("entries"): + info = self._layout or detect_wolf_layout(self._game_root) + data_dir = info.get("data_dir") + has_map_entries = any(e.get("kind") == "map" for e in manifest["entries"]) + if ( + not has_map_entries + and data_dir + and ( + wolf_maps_packed(self._game_root, data_dir) + or wolf_has_maps(data_dir) + ) + ): + self._log( + "Auto-setup: maps missing from wolf_json/ — unpacking MapData if needed " + "and extracting map files …" + ) + self._extract_to_work_dir( + supplement_maps=True, + on_done=lambda ok, _m: self._scan_wolf_files() if ok else None, + ) + return self._scan_wolf_files() return @@ -1330,7 +1357,30 @@ class WolfWorkflowTab(QWidget): self._run_task(task, on_done=on_done) - def _extract_to_work_dir(self, *, interactive: bool = True, on_done=None): + def _ensure_text_archives_unpacked(self, data_dir: Path, log) -> bool: + """Unpack BasicData/MapData archives when their binaries are not loose yet.""" + from util import wolfdawn + + archives = find_wolf_text_archives(self._game_root, data_dir) + ok = True + basic = data_dir / "BasicData" + if "BasicData" in archives and not (basic / "CommonEvent.dat").is_file(): + log(f"Unpacking {archives['BasicData'].name} …") + res = wolfdawn.unpack_all([str(archives["BasicData"])], str(data_dir), log_fn=log) + if not res.ok: + log(f" ⚠ unpack failed (exit {res.returncode})") + ok = False + if "MapData" in archives and not wolf_has_maps(data_dir): + log(f"Unpacking {archives['MapData'].name} …") + res = wolfdawn.unpack_all([str(archives["MapData"])], str(data_dir), log_fn=log) + if not res.ok: + log(f" ⚠ unpack failed (exit {res.returncode})") + ok = False + return ok + + def _extract_to_work_dir( + self, *, interactive: bool = True, on_done=None, supplement_maps: bool = False + ): if not self._require_root(): return info = detect_wolf_layout(self._game_root) @@ -1352,13 +1402,26 @@ class WolfWorkflowTab(QWidget): def task(log): from util import wolfdawn - basic = Path(info["basic_data"]) if info.get("basic_data") else Path(data_dir) - maps_dir = Path(info["map_data"]) if info.get("map_data") else Path(data_dir) + if not self._ensure_text_archives_unpacked(Path(data_dir), log): + return False, "Could not unpack text archives (see log)." + + layout = detect_wolf_layout(self._game_root) + self._layout = layout + basic = Path(layout["basic_data"]) if layout.get("basic_data") else Path(data_dir) + maps_dir = wolf_maps_dir(data_dir) work_dir.mkdir(parents=True, exist_ok=True) - manifest_entries: list[dict] = [] + existing_manifest = self._read_manifest() if supplement_maps else None + existing_entries = list(existing_manifest.get("entries", [])) if existing_manifest else [] + existing_json = {e["json"] for e in existing_entries if e.get("json")} + + manifest_entries: list[dict] = ( + existing_entries.copy() if supplement_maps else [] + ) def _extract_one(base: Path, out_name: str, kind: str): + if supplement_maps and out_name in existing_json: + return out = work_dir / out_name log(f"Extracting {base.name} …") res = wolfdawn.strings_extract(str(base), str(out), log_fn=log) @@ -1367,34 +1430,54 @@ class WolfWorkflowTab(QWidget): else: log(f" ⚠ skipped {base.name} (exit {res.returncode})") - ce = basic / "CommonEvent.dat" - if ce.is_file(): - _extract_one(ce, "CommonEvent.dat.json", "common") - for stem in ("DataBase", "CDataBase", "SysDatabase"): - proj = basic / f"{stem}.project" - if proj.is_file(): - _extract_one(proj, f"{stem}.project.json", "db") - gd = basic / "Game.dat" - if gd.is_file(): - _extract_one(gd, "Game.dat.json", "gamedat") + if not supplement_maps: + ce = basic / "CommonEvent.dat" + if ce.is_file(): + _extract_one(ce, "CommonEvent.dat.json", "common") + for stem in ("DataBase", "CDataBase", "SysDatabase"): + proj = basic / f"{stem}.project" + if proj.is_file(): + _extract_one(proj, f"{stem}.project.json", "db") + gd = basic / "Game.dat" + if gd.is_file(): + _extract_one(gd, "Game.dat.json", "gamedat") + for mps in sorted(maps_dir.glob("*.mps")): _extract_one(mps, f"{mps.name}.json", "map") - evtext = Path(data_dir) / "Evtext" - if evtext.is_dir() and any(evtext.glob("*.txt")): - out = work_dir / "Evtext.json" - log("Extracting Evtext/ …") - res = wolfdawn.strings_extract(str(evtext), str(out), log_fn=log) - if res.ok and out.is_file(): - manifest_entries.append({"json": "Evtext.json", "base": str(evtext), "kind": "txt-dir"}) - log("Extracting name glossary …") - names_out = work_dir / NAMES_JSON - res = wolfdawn.names_extract(str(data_dir), str(names_out), log_fn=log) - if res.ok and names_out.is_file(): - manifest_entries.append({"json": NAMES_JSON, "base": str(data_dir), "kind": "names"}) + if not supplement_maps: + evtext = Path(data_dir) / "Evtext" + if evtext.is_dir() and any(evtext.glob("*.txt")): + out = work_dir / "Evtext.json" + log("Extracting Evtext/ …") + res = wolfdawn.strings_extract(str(evtext), str(out), log_fn=log) + if res.ok and out.is_file(): + manifest_entries.append({"json": "Evtext.json", "base": str(evtext), "kind": "txt-dir"}) + + log("Extracting name glossary …") + names_out = work_dir / NAMES_JSON + res = wolfdawn.names_extract(str(data_dir), str(names_out), log_fn=log) + if res.ok and names_out.is_file(): + manifest_entries.append({"json": NAMES_JSON, "base": str(data_dir), "kind": "names"}) + elif any(e.get("kind") == "map" for e in manifest_entries): + log("Refreshing name glossary (maps were added) …") + names_out = work_dir / NAMES_JSON + res = wolfdawn.names_extract(str(data_dir), str(names_out), log_fn=log) + if res.ok and names_out.is_file(): + manifest_entries = [ + e for e in manifest_entries if e.get("json") != NAMES_JSON + ] + manifest_entries.append({"json": NAMES_JSON, "base": str(data_dir), "kind": "names"}) + + new_entries = [ + e for e in manifest_entries + if supplement_maps and e.get("json") not in existing_json + ] if supplement_maps else manifest_entries if not manifest_entries: return False, "Nothing was extracted. Check the Data/ folder layout." + if supplement_maps and not new_entries: + return True, f"No new map files to extract in {WORK_DIR_NAME}/." manifest = { "root": game_root, @@ -1404,10 +1487,18 @@ class WolfWorkflowTab(QWidget): (work_dir / MANIFEST_NAME).write_text( json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8" ) - self._snapshot_originals(manifest_entries, Path(data_dir), log) + snapshot_entries = new_entries if supplement_maps else manifest_entries + self._snapshot_originals(snapshot_entries, Path(data_dir), log) + map_count = sum(1 for e in manifest_entries if e.get("kind") == "map") + if supplement_maps: + added = sum(1 for e in new_entries if e.get("kind") == "map") + return True, ( + f"Added {added} map file(s) ({map_count} total in {WORK_DIR_NAME}/). " + "Import checked files into files/ from Step 0." + ) return True, ( - f"Extracted {len(manifest_entries)} document(s) into {WORK_DIR_NAME}/. " - "Import checked files into files/ from Step 0." + f"Extracted {len(manifest_entries)} document(s) into {WORK_DIR_NAME}/ " + f"({map_count} map(s)). Import checked files into files/ from Step 0." ) self._run_task(task, on_done=on_done) diff --git a/tests/test_wolf_project_scanner.py b/tests/test_wolf_project_scanner.py new file mode 100644 index 0000000..d9ae377 --- /dev/null +++ b/tests/test_wolf_project_scanner.py @@ -0,0 +1,60 @@ +"""Tests for WOLF project layout helpers in util.project_scanner.""" + +from __future__ import annotations + +from pathlib import Path + +from util.project_scanner import ( + find_wolf_text_archives, + wolf_has_maps, + wolf_maps_dir, + wolf_maps_packed, +) + + +def test_wolf_maps_dir_prefers_mapdata_subfolder(tmp_path: Path): + data = tmp_path / "Data" + (data / "MapData").mkdir(parents=True) + assert wolf_maps_dir(data) == data / "MapData" + + +def test_wolf_maps_dir_falls_back_to_data_root(tmp_path: Path): + data = tmp_path / "Data" + data.mkdir() + assert wolf_maps_dir(data) == data + + +def test_wolf_has_maps(tmp_path: Path): + data = tmp_path / "Data" + maps = data / "MapData" + maps.mkdir(parents=True) + assert not wolf_has_maps(data) + (maps / "town.mps").write_bytes(b"") + assert wolf_has_maps(data) + + +def test_find_wolf_text_archives_in_data_dir(tmp_path: Path): + data = tmp_path / "Data" + data.mkdir() + (data / "MapData.wolf").write_bytes(b"") + (data / "BasicData.wolf").write_bytes(b"") + found = find_wolf_text_archives(tmp_path, data) + assert found["MapData"] == data / "MapData.wolf" + assert found["BasicData"] == data / "BasicData.wolf" + + +def test_wolf_maps_packed_when_archive_present(tmp_path: Path): + data = tmp_path / "Data" + data.mkdir() + (data / "BasicData" / "CommonEvent.dat").mkdir(parents=True) + (data / "MapData.wolf").write_bytes(b"") + assert wolf_maps_packed(tmp_path, data) + + +def test_wolf_maps_packed_false_when_loose_maps_exist(tmp_path: Path): + data = tmp_path / "Data" + maps = data / "MapData" + maps.mkdir(parents=True) + (maps / "field.mps").write_bytes(b"") + (data / "MapData.wolf").write_bytes(b"") + assert not wolf_maps_packed(tmp_path, data) diff --git a/util/project_scanner.py b/util/project_scanner.py index c38584f..3d71b34 100644 --- a/util/project_scanner.py +++ b/util/project_scanner.py @@ -57,6 +57,54 @@ _ACE_DATA_SCRIPTS = {".rvdata2", ".rvdata"} # the unpacked binaries (CommonEvent.dat, *.mps maps, *.project databases). _WOLF_LOOSE_MARKERS = ("BasicData/CommonEvent.dat", "CommonEvent.dat") +_WOLF_TEXT_ARCHIVE_STEMS = ("BasicData", "MapData") + + +def wolf_maps_dir(data_dir: str | Path) -> Path: + """Return the folder that holds map ``.mps`` files (``MapData/`` or flat ``Data/``).""" + data_dir = Path(data_dir) + nested = data_dir / "MapData" + return nested if nested.is_dir() else data_dir + + +def wolf_has_maps(data_dir: str | Path) -> bool: + """True when loose ``.mps`` map files are present under *data_dir*.""" + return any(wolf_maps_dir(data_dir).glob("*.mps")) + + +def find_wolf_text_archives( + game_root: str | Path, + data_dir: str | Path | None = None, +) -> dict[str, Path]: + """Return ``{BasicData|MapData: archive_path}`` for text-relevant ``.wolf`` files.""" + game_root = Path(game_root) + data_dir = Path(data_dir) if data_dir else None + found: dict[str, Path] = {} + bases: list[Path] = [game_root] + if data_dir is not None: + bases.append(data_dir) + if (game_root / "Data").is_dir(): + bases.append(game_root / "Data") + for base in bases: + if not base.is_dir(): + continue + for stem in _WOLF_TEXT_ARCHIVE_STEMS: + if stem in found: + continue + for name in (f"{stem}.wolf", f"{stem}.wolf.bak"): + arc = base / name + if arc.is_file(): + found[stem] = arc + break + return found + + +def wolf_maps_packed(game_root: str | Path, data_dir: str | Path) -> bool: + """True when map ``.mps`` are not loose but a ``MapData`` archive exists.""" + if wolf_has_maps(data_dir): + return False + return "MapData" in find_wolf_text_archives(game_root, data_dir) + # --------------------------------------------------------------------------- # Public API @@ -176,8 +224,7 @@ def detect_wolf_layout(game_root: str | Path) -> dict: result["unpacked"] = True basic = loose / "BasicData" result["basic_data"] = basic if basic.is_dir() else loose - maps = loose / "MapData" - result["map_data"] = maps if maps.is_dir() else loose + result["map_data"] = wolf_maps_dir(loose) return result