Fix unpacking

This commit is contained in:
DazedAnon 2026-07-05 14:52:12 -05:00
parent f85ea1af35
commit 9708b7a31f
3 changed files with 230 additions and 32 deletions

View file

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

View file

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

View file

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