From f1a7bb93e669de5a5f2aadf68b926026f43c56d8 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Tue, 7 Jul 2026 09:18:01 -0500 Subject: [PATCH] Correct root level for Data/Data extractions --- gui/wolf_workflow_tab.py | 42 +++++--- tests/test_wolf_project_scanner.py | 149 ++++++++++++++++++++--------- util/project_scanner.py | 64 +++++++++++-- 3 files changed, 192 insertions(+), 63 deletions(-) diff --git a/gui/wolf_workflow_tab.py b/gui/wolf_workflow_tab.py index 04eb4ea..d3475ca 100644 --- a/gui/wolf_workflow_tab.py +++ b/gui/wolf_workflow_tab.py @@ -94,6 +94,8 @@ from util.project_scanner import ( wolf_has_maps, wolf_maps_dir, wolf_maps_packed, + wolf_repair_nested_data_dir, + wolf_unpack_out_dir, ) from util.vocab import read_game_vocab, write_game_vocab @@ -1406,20 +1408,38 @@ class WolfWorkflowTab(QWidget): if interactive: QMessageBox.information(self, "Unpack", "No .wolf archives found to unpack.") return - out_dir = root / "Data" + root = Path(self._game_root) def task(log, progress=None): + from collections import defaultdict + from util import wolfdawn - log(f"Unpacking {len(archives)} archive(s) into {out_dir} …") - res = wolfdawn.unpack_all( - [str(a) for a in archives], - str(out_dir), - log_fn=log, - progress_fn=progress, - progress_total=len(archives), - ) - if not res.ok: - return False, f"unpack-all exited {res.returncode}" + + groups: dict[Path, list[Path]] = defaultdict(list) + for arc in archives: + groups[wolf_unpack_out_dir(root, arc)].append(arc) + + total = len(archives) + done = 0 + for target, group in groups.items(): + log(f"Unpacking {len(group)} archive(s) into {target} …") + res = wolfdawn.unpack_all( + [str(a) for a in group], + str(target), + log_fn=log, + progress_fn=( + (lambda c, _t, label, base=done: progress(base + min(c, 1), total, label)) + if progress + else None + ), + progress_total=len(group), + ) + done += len(group) + if progress: + progress(done, total, f"Unpacked {len(group)} archive(s)") + if not res.ok: + return False, f"unpack-all exited {res.returncode}" + wolf_repair_nested_data_dir(root) return True, "Unpacked archives into Data/." self._run_task(task, on_done=on_done) diff --git a/tests/test_wolf_project_scanner.py b/tests/test_wolf_project_scanner.py index d9ae377..e68bdb6 100644 --- a/tests/test_wolf_project_scanner.py +++ b/tests/test_wolf_project_scanner.py @@ -2,59 +2,118 @@ from __future__ import annotations +import os +import sys +import tempfile +import unittest from pathlib import Path -from util.project_scanner import ( +ROOT = Path(__file__).resolve().parents[1] +os.chdir(ROOT) +sys.path.insert(0, str(ROOT)) + +from util.project_scanner import ( # noqa: E402 + detect_wolf_layout, find_wolf_text_archives, wolf_has_maps, wolf_maps_dir, wolf_maps_packed, + wolf_repair_nested_data_dir, + wolf_unpack_out_dir, ) -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" +class WolfProjectScannerTests(unittest.TestCase): + def test_wolf_maps_dir_prefers_mapdata_subfolder(self): + with tempfile.TemporaryDirectory() as raw: + data = Path(raw) / "Data" + (data / "MapData").mkdir(parents=True) + self.assertEqual(wolf_maps_dir(data), data / "MapData") + + def test_wolf_maps_dir_falls_back_to_data_root(self): + with tempfile.TemporaryDirectory() as raw: + data = Path(raw) / "Data" + data.mkdir() + self.assertEqual(wolf_maps_dir(data), data) + + def test_wolf_has_maps(self): + with tempfile.TemporaryDirectory() as raw: + data = Path(raw) / "Data" + maps = data / "MapData" + maps.mkdir(parents=True) + self.assertFalse(wolf_has_maps(data)) + (maps / "town.mps").write_bytes(b"") + self.assertTrue(wolf_has_maps(data)) + + def test_find_wolf_text_archives_in_data_dir(self): + with tempfile.TemporaryDirectory() as raw: + base = Path(raw) + data = base / "Data" + data.mkdir() + (data / "MapData.wolf").write_bytes(b"") + (data / "BasicData.wolf").write_bytes(b"") + found = find_wolf_text_archives(base, data) + self.assertEqual(found["MapData"], data / "MapData.wolf") + self.assertEqual(found["BasicData"], data / "BasicData.wolf") + + def test_wolf_maps_packed_when_archive_present(self): + with tempfile.TemporaryDirectory() as raw: + base = Path(raw) + data = base / "Data" + (data / "BasicData" / "CommonEvent.dat").mkdir(parents=True) + (data / "MapData.wolf").write_bytes(b"") + self.assertTrue(wolf_maps_packed(base, data)) + + def test_wolf_maps_packed_false_when_loose_maps_exist(self): + with tempfile.TemporaryDirectory() as raw: + base = Path(raw) + data = base / "Data" + maps = data / "MapData" + maps.mkdir(parents=True) + (maps / "field.mps").write_bytes(b"") + (data / "MapData.wolf").write_bytes(b"") + self.assertFalse(wolf_maps_packed(base, data)) + + def test_wolf_unpack_out_dir_root_data_wolf(self): + with tempfile.TemporaryDirectory() as raw: + base = Path(raw) + (base / "Data.wolf").write_bytes(b"") + self.assertEqual(wolf_unpack_out_dir(base, base / "Data.wolf"), base) + + def test_wolf_unpack_out_dir_nested_text_archives(self): + with tempfile.TemporaryDirectory() as raw: + base = Path(raw) + data = base / "Data" + data.mkdir() + arc = data / "MapData.wolf" + arc.write_bytes(b"") + self.assertEqual(wolf_unpack_out_dir(base, arc), data) + + def test_wolf_repair_nested_data_dir(self): + with tempfile.TemporaryDirectory() as raw: + base = Path(raw) + outer = base / "Data" + inner = outer / "Data" + basic = inner / "BasicData" + basic.mkdir(parents=True) + (basic / "CommonEvent.dat").write_bytes(b"") + self.assertTrue(wolf_repair_nested_data_dir(base)) + self.assertTrue((outer / "BasicData" / "CommonEvent.dat").is_file()) + self.assertFalse(inner.exists()) + + def test_detect_wolf_layout_after_nested_repair(self): + with tempfile.TemporaryDirectory() as raw: + base = Path(raw) + outer = base / "Data" + inner = outer / "Data" + basic = inner / "BasicData" + basic.mkdir(parents=True) + (basic / "CommonEvent.dat").write_bytes(b"") + (base / "Data.wolf").write_bytes(b"") + info = detect_wolf_layout(base) + self.assertTrue(info["unpacked"]) + self.assertEqual(info["data_dir"], outer) -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) +if __name__ == "__main__": + unittest.main() diff --git a/util/project_scanner.py b/util/project_scanner.py index 3d71b34..6522d83 100644 --- a/util/project_scanner.py +++ b/util/project_scanner.py @@ -172,16 +172,66 @@ def find_wolf_archives(game_root: str | Path) -> list[Path]: return unique +def _wolf_dir_has_loose_data(data_dir: Path) -> bool: + """True when *data_dir* looks like an unpacked WOLF Data folder.""" + if not data_dir.is_dir(): + return False + for marker in _WOLF_LOOSE_MARKERS: + if (data_dir / marker).is_file(): + return True + if list(data_dir.glob("*.mps")) or list((data_dir / "MapData").glob("*.mps")): + return True + return False + + +def wolf_unpack_out_dir(game_root: str | Path, archive: str | Path) -> Path: + """Return the ``-o`` directory for ``wolf unpack-all`` on *archive*. + + ``wolf`` unpacks ``Name.wolf`` to ``/Name/``. A root-level ``Data.wolf`` + must therefore unpack to the game root (yielding ``Data/``), not into an + existing ``Data/`` folder (which would incorrectly create ``Data/Data/``). + """ + game_root = Path(game_root).resolve() + archive = Path(archive).resolve() + if archive.stem == "Data" and archive.parent == game_root: + return game_root + data_dir = game_root / "Data" + if data_dir.is_dir() and archive.parent == data_dir: + return data_dir + return archive.parent + + +def wolf_repair_nested_data_dir(game_root: str | Path) -> bool: + """Hoist ``Data/Data/`` to ``Data/`` after a mistaken ``Data.wolf`` unpack. + + Returns True when the nested layout was repaired. + """ + outer = Path(game_root) / "Data" + inner = outer / "Data" + if not inner.is_dir() or not _wolf_dir_has_loose_data(inner): + return False + if any(p.name != "Data" for p in outer.iterdir()): + return False + for child in list(inner.iterdir()): + dest = outer / child.name + if dest.exists(): + if dest.is_dir(): + shutil.rmtree(dest) + else: + dest.unlink() + shutil.move(str(child), str(dest)) + try: + inner.rmdir() + except OSError: + return False + return True + + def _wolf_loose_data_dir(root: Path) -> Optional[Path]: """Return the loose (unpacked) WOLF Data/ folder if it looks unpacked.""" + wolf_repair_nested_data_dir(root) for data_dir in (root / "Data", root): - if not data_dir.is_dir(): - continue - for marker in _WOLF_LOOSE_MARKERS: - if (data_dir / marker).is_file(): - return data_dir - # A folder with .mps maps (directly or under MapData/) is unpacked WOLF. - if list(data_dir.glob("*.mps")) or list((data_dir / "MapData").glob("*.mps")): + if _wolf_dir_has_loose_data(data_dir): return data_dir return None