diff --git a/gui/wolf_workflow_tab.py b/gui/wolf_workflow_tab.py index 93efcfa..1b9883e 100644 --- a/gui/wolf_workflow_tab.py +++ b/gui/wolf_workflow_tab.py @@ -383,6 +383,39 @@ class WolfWorkflowTab(QWidget): rel = Path(base.name) return self._originals_dir() / rel + def _snapshot_db_dat_sibling( + self, entry: dict, data_dir: Path, log=None + ) -> None: + """Snapshot the ``.dat`` sibling for a database ``.project`` entry. + + WolfDawn ``strings-inject`` on ``kind == "db"`` needs both files in the + ``--base`` directory. The manifest only lists the ``.project`` path. + """ + from util.wolfdawn import db_dat_sibling + + if entry.get("kind") != "db": + return + proj_orig = self._orig_base_for(entry, data_dir) + dat_orig = db_dat_sibling(proj_orig) + if dat_orig.exists(): + return + dat_live = db_dat_sibling(Path(entry["base"])) + if not dat_live.is_file(): + return + try: + dat_orig.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(dat_live, dat_orig) + except Exception as exc: + if log: + log(f" ⚠ could not snapshot {dat_orig.name}: {exc}") + + def _ensure_db_dat_snapshots( + self, entries: list[dict], data_dir: Path, log=None + ) -> None: + """Backfill missing database ``.dat`` files in originals/.""" + for entry in entries: + self._snapshot_db_dat_sibling(entry, data_dir, log) + def _snapshot_originals(self, entries: list[dict], data_dir: Path, log) -> None: """Copy the pristine base binaries into originals/ (only when missing, so a good snapshot is never clobbered by a later extract of injected data).""" @@ -392,6 +425,7 @@ class WolfWorkflowTab(QWidget): src = Path(entry["base"]) dst = self._orig_base_for(entry, data_dir) if dst.exists(): + self._snapshot_db_dat_sibling(entry, data_dir, log) continue try: if src.is_dir(): @@ -401,6 +435,8 @@ class WolfWorkflowTab(QWidget): shutil.copy2(src, dst) except Exception as exc: log(f" ⚠ could not snapshot original {src.name}: {exc}") + continue + self._snapshot_db_dat_sibling(entry, data_dir, log) def _ensure_originals(self, manifest: dict, log, progress=None, *, quiet: bool = False) -> None: """Make sure a pristine snapshot exists; if entries are missing it and the @@ -458,6 +494,7 @@ class WolfWorkflowTab(QWidget): ) if not res.ok: emit(f" ⚠ could not rebuild originals (unpack exit {res.returncode}).") + self._ensure_db_dat_snapshots(entries, data_dir, None if quiet else log) def _restore_live_from_originals( self, entries: list[dict], data_dir: Path, log, *, quiet: bool = False @@ -467,6 +504,8 @@ class WolfWorkflowTab(QWidget): ``names-inject`` rewrites every DB pair, CommonEvent.dat, and every map. Resetting from ``wolf_json/originals/`` first keeps them on one baseline. """ + from util.wolfdawn import db_dat_sibling + emit = (lambda _msg: None) if quiet else log restored = 0 for entry in entries: @@ -485,6 +524,11 @@ class WolfWorkflowTab(QWidget): else: live.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(orig, live) + if entry.get("kind") == "db": + dat_orig = db_dat_sibling(orig) + dat_live = db_dat_sibling(live) + if dat_orig.is_file(): + shutil.copy2(dat_orig, dat_live) restored += 1 except Exception as exc: emit(f" ⚠ could not restore {live.name} from originals: {exc}") @@ -2502,6 +2546,23 @@ class WolfWorkflowTab(QWidget): return f"wolf exited {exit_code}" return "inject failed" + def _strings_inject_failure_reason(self, res) -> str: + from util import wolfdawn + + cli_err = wolfdawn.parse_inject_cli_error(res.stdout, res.stderr) + if cli_err: + return cli_err + a, d = wolfdawn.parse_strings_inject_counts(res.stdout, res.stderr) + untranslated = wolfdawn.parse_strings_inject_untranslated(res.stdout, res.stderr) + mismatches = wolfdawn.parse_strings_inject_mismatches(res.stdout, res.stderr) + if res.ok: + return self._inject_failure_reason(drift=d or 0) + return self._inject_failure_reason( + exit_code=res.returncode, + untranslated=untranslated, + mismatches=len(mismatches), + ) + def _on_step_changed(self, idx: int): previous = self._current_step_index self._current_step_index = idx @@ -2625,6 +2686,7 @@ class WolfWorkflowTab(QWidget): entries = manifest["entries"] data_dir = manifest["data_dir"] data_dir_path = Path(data_dir) + self._ensure_db_dat_snapshots(entries, data_dir_path) names_entry = next((e for e in entries if e["kind"] == "names"), None) names_json = names_entry["json"] if names_entry else NAMES_JSON @@ -2678,6 +2740,16 @@ class WolfWorkflowTab(QWidget): (json_name, self._inject_failure_reason(no_original=True)) ) continue + if entry.get("kind") == "db": + from util.wolfdawn import db_dat_sibling + + if not db_dat_sibling(orig).is_file(): + failures.append(( + json_name, + "missing database .dat pair in originals " + f"(re-run Step 0 extract on the untranslated game)", + )) + continue res = wolfdawn.strings_inject( str(inject_src), base, out, allow_code_drift=allow_drift, en_punct=en_punct, log_fn=None, @@ -2701,16 +2773,12 @@ class WolfWorkflowTab(QWidget): elif res.ok: failures.append(( json_name, - self._inject_failure_reason(drift=d or 0), + self._strings_inject_failure_reason(res), )) else: failures.append(( json_name, - self._inject_failure_reason( - exit_code=res.returncode, - untranslated=untranslated, - mismatches=len(mismatches), - ), + self._strings_inject_failure_reason(res), )) if will_names and names_entry and names_src is not None: diff --git a/tests/test_wolf_inject_counts.py b/tests/test_wolf_inject_counts.py index 2c6e040..ea228bc 100644 --- a/tests/test_wolf_inject_counts.py +++ b/tests/test_wolf_inject_counts.py @@ -51,6 +51,18 @@ class WolfInjectCountsTests(unittest.TestCase): self.assertFalse(wolfdawn.inject_had_applied(0)) self.assertFalse(wolfdawn.inject_had_applied(None)) + def test_parse_inject_cli_error(self): + err = wolfdawn.parse_inject_cli_error( + "", "strings-inject: cannot read input or DB pair\n" + ) + self.assertEqual(err, "cannot read input or DB pair") + + def test_db_dat_sibling(self): + self.assertEqual( + wolfdawn.db_dat_sibling("BasicData/DataBase.project").name, + "DataBase.dat", + ) + if __name__ == "__main__": unittest.main() diff --git a/util/wolfdawn/__init__.py b/util/wolfdawn/__init__.py index 7d6d47f..56a0e79 100644 --- a/util/wolfdawn/__init__.py +++ b/util/wolfdawn/__init__.py @@ -138,6 +138,18 @@ def inject_had_applied(applied: int | None) -> bool: """ return applied is not None and applied > 0 + +def parse_inject_cli_error(stdout: str, stderr: str = "") -> str | None: + """Return the first wolf inject CLI error line, if any.""" + for line in _inject_output_text(stdout, stderr).splitlines(): + line = line.strip() + if not line: + continue + for prefix in ("strings-inject:", "names-inject:"): + if prefix in line: + return line.split(prefix, 1)[-1].strip() + return None + # Committed, prebuilt binaries live here (per-platform) so end users don't need a # Rust toolchain or a live upstream fetch. _PACKAGE_DIR = Path(__file__).resolve().parent @@ -165,6 +177,11 @@ _DOWNLOAD_UA = ( PathLike = Union[str, Path] +def db_dat_sibling(project_path: PathLike) -> Path: + """Sibling ``.dat`` for a WolfDawn ``*.project`` database pair.""" + return Path(project_path).with_suffix(".dat") + + class WolfDawnError(RuntimeError): """Raised when the WolfDawn binary can't be located/downloaded or a command fails hard."""