Correct root level for Data/Data extractions

This commit is contained in:
DazedAnon 2026-07-07 09:18:01 -05:00
parent 519093f797
commit f1a7bb93e6
3 changed files with 192 additions and 63 deletions

View file

@ -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)

View file

@ -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()

View file

@ -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 ``<out>/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