relayout for db and tls
This commit is contained in:
parent
3fdaaad0d0
commit
b9ab810c26
6 changed files with 457 additions and 72 deletions
|
|
@ -70,8 +70,9 @@ noteWidth="75"
|
|||
#wolfNameWrapWidth="60"
|
||||
#wolfNameWrapNotes='["├■プロフィール","説明"]'
|
||||
|
||||
# WolfDawn dialogue (Step 4 Phase 2): re-wrap translated event text
|
||||
#wolfWrap="true"
|
||||
# WolfDawn dialogue wrap at translate time is optional / advanced; normal workflow
|
||||
# leaves line breaks alone and runs `wolf relayout` after inject (Step 5).
|
||||
#wolfWrap="false"
|
||||
#wolfWidth="55"
|
||||
|
||||
#CSV delimiter character (comma, semicolon, or tab)
|
||||
|
|
|
|||
|
|
@ -15,12 +15,13 @@ Mirrors the RPGMaker WorkflowTab, driven by the vendored WolfDawn ``wolf`` CLI
|
|||
Phase 1 Database - item/skill/state descriptions and system
|
||||
messages (DataBase/CDataBase.project)
|
||||
Phase 2 Maps / events - .mps maps, CommonEvent, Game.dat,
|
||||
Evtext; speaker handling and text-wrap
|
||||
settings live under this phase
|
||||
Evtext; speaker handling lives under this phase
|
||||
Step 5 Inject - inject translations + translated names back into the Data/ binaries
|
||||
and refresh the git-tracked wolf_json/ with the translated
|
||||
JSON; quick-inject picks just a few translated files for
|
||||
fast in-game / git iteration, or inject everything at once
|
||||
fast in-game / git iteration, or inject everything at once.
|
||||
After a successful inject, optional WolfDawn relayout reflows
|
||||
Message text (and optionally DB descriptions) to the message box.
|
||||
Step 6 Package - run from a loose Data/ folder, or repack Data.wolf
|
||||
Step 7 Saves - fix baked strings in existing .sav files (optional)
|
||||
|
||||
|
|
@ -1720,11 +1721,11 @@ class WolfWorkflowTab(QWidget):
|
|||
layout.addWidget(self._desc(
|
||||
"Map scripts (.mps), common events (CommonEvent.dat), Game.dat, and Evtext - story "
|
||||
"dialogue, UI/objective strings, and other event text. Run after Phase 1 so it "
|
||||
"benefits from the names and terms already in vocab.txt. Speaker handling and text "
|
||||
"wrap below apply to the dialogue-like lines in this phase."
|
||||
"benefits from the names and terms already in vocab.txt. Speaker handling below "
|
||||
"applies to the dialogue-like lines in this phase. Message box layout is handled "
|
||||
"after inject (Step 5 Relayout), not here."
|
||||
))
|
||||
self._add_speaker_options(layout)
|
||||
self._add_wrap_options(layout)
|
||||
p2 = self._register(_make_btn("▶ Phase 2 · Translate maps / events", "#00a86b"))
|
||||
p2.clicked.connect(
|
||||
lambda: self._navigate_to_translation(kinds=PHASE_MAPS_EVENTS_KINDS, auto_start=True)
|
||||
|
|
@ -1789,53 +1790,6 @@ class WolfWorkflowTab(QWidget):
|
|||
except Exception as exc:
|
||||
self._log(f"❌ Could not save speaker options: {exc}")
|
||||
|
||||
def _add_wrap_options(self, layout: QVBoxLayout):
|
||||
"""Dialogue text-wrap width (like the RPGMaker workflow, via dazedwrap)."""
|
||||
layout.addWidget(_make_hr())
|
||||
layout.addWidget(self._subheading("Text wrap width"))
|
||||
layout.addWidget(self._desc(
|
||||
"Re-wraps the translated English dialogue so it fits WOLF's message box. "
|
||||
"Only the dialogue body is wrapped - a speaker's name line (the line break "
|
||||
"right after a nameplate) is always kept separate. Lower the width if lines "
|
||||
"overflow in-game; raise it if text wraps too early. Applies on the next run."
|
||||
))
|
||||
|
||||
wrap_env = self._read_env_values(["wolfWrap", "wolfWidth"])
|
||||
|
||||
self._wrap_enable_cb = QCheckBox("Wrap translated dialogue")
|
||||
enabled = str(wrap_env.get("wolfWrap", "true")).strip().lower() != "false"
|
||||
self._wrap_enable_cb.setChecked(enabled)
|
||||
self._wrap_enable_cb.stateChanged.connect(self._apply_wrap_config)
|
||||
layout.addWidget(self._wrap_enable_cb)
|
||||
|
||||
row = QHBoxLayout()
|
||||
width_lbl = QLabel("Width (characters):")
|
||||
width_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
||||
self._wrap_width_spin = QSpinBox()
|
||||
self._wrap_width_spin.setRange(20, 300)
|
||||
try:
|
||||
self._wrap_width_spin.setValue(int(wrap_env.get("wolfWidth") or 55))
|
||||
except ValueError:
|
||||
self._wrap_width_spin.setValue(55)
|
||||
self._wrap_width_spin.setFixedWidth(70)
|
||||
self._wrap_width_spin.valueChanged.connect(self._apply_wrap_config)
|
||||
row.addWidget(width_lbl)
|
||||
row.addWidget(self._wrap_width_spin)
|
||||
row.addStretch()
|
||||
layout.addLayout(row)
|
||||
|
||||
def _apply_wrap_config(self, *args):
|
||||
"""Persist wolfWrap / wolfWidth to .env so the next translation run uses them."""
|
||||
updates = {
|
||||
"wolfWrap": "true" if self._wrap_enable_cb.isChecked() else "false",
|
||||
"wolfWidth": str(self._wrap_width_spin.value()),
|
||||
}
|
||||
if self._write_env_values(updates):
|
||||
self._log(
|
||||
"Text wrap updated: "
|
||||
+ ", ".join(f"{k}={v}" for k, v in updates.items())
|
||||
)
|
||||
|
||||
def _read_env_values(self, keys) -> dict:
|
||||
"""Read a few `key='value'` pairs from .env (best effort)."""
|
||||
import re as _re
|
||||
|
|
@ -2283,6 +2237,115 @@ class WolfWorkflowTab(QWidget):
|
|||
inject_btn.clicked.connect(lambda: self._inject())
|
||||
layout.addWidget(inject_btn)
|
||||
|
||||
self._add_relayout_options(layout)
|
||||
|
||||
def _add_relayout_options(self, layout: QVBoxLayout):
|
||||
"""Post-inject WolfDawn message-box / DB-description reflow."""
|
||||
layout.addWidget(_make_hr())
|
||||
layout.addWidget(self._subheading("Relayout (after inject)"))
|
||||
layout.addWidget(self._desc(
|
||||
"Runs WolfDawn on the live Data/ binaries after a successful inject: reflow Message "
|
||||
"text to the message-box width (page-split overflow), and optionally refit DB "
|
||||
"description fields. Layout is no longer done at translate time - leave dialogue "
|
||||
"unwrapped in JSON and let this step fix the box."
|
||||
))
|
||||
|
||||
self._relayout_after_cb = QCheckBox("Relayout after inject")
|
||||
self._relayout_after_cb.setChecked(
|
||||
str(self._setting("relayout_after_inject", "true")).lower() != "false"
|
||||
)
|
||||
self._relayout_after_cb.stateChanged.connect(
|
||||
lambda: self._save_setting(
|
||||
"relayout_after_inject",
|
||||
"true" if self._relayout_after_cb.isChecked() else "false",
|
||||
)
|
||||
)
|
||||
layout.addWidget(self._relayout_after_cb)
|
||||
|
||||
self._relayout_desc_cb = QCheckBox("Also refit DB descriptions (desc-relayout)")
|
||||
self._relayout_desc_cb.setChecked(
|
||||
str(self._setting("relayout_desc", "true")).lower() != "false"
|
||||
)
|
||||
self._relayout_desc_cb.stateChanged.connect(
|
||||
lambda: self._save_setting(
|
||||
"relayout_desc",
|
||||
"true" if self._relayout_desc_cb.isChecked() else "false",
|
||||
)
|
||||
)
|
||||
layout.addWidget(self._relayout_desc_cb)
|
||||
|
||||
msg_row = QHBoxLayout()
|
||||
width_lbl = QLabel("Message width (cells):")
|
||||
width_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
||||
self._relayout_width_spin = QSpinBox()
|
||||
self._relayout_width_spin.setRange(20, 300)
|
||||
try:
|
||||
self._relayout_width_spin.setValue(int(self._setting("relayout_width", 55) or 55))
|
||||
except (TypeError, ValueError):
|
||||
self._relayout_width_spin.setValue(55)
|
||||
self._relayout_width_spin.setFixedWidth(70)
|
||||
self._relayout_width_spin.valueChanged.connect(
|
||||
lambda v: self._save_setting("relayout_width", v)
|
||||
)
|
||||
max_rows_lbl = QLabel("Max rows (0=default):")
|
||||
max_rows_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
||||
self._relayout_max_rows_spin = QSpinBox()
|
||||
self._relayout_max_rows_spin.setRange(0, 20)
|
||||
try:
|
||||
self._relayout_max_rows_spin.setValue(int(self._setting("relayout_max_rows", 0) or 0))
|
||||
except (TypeError, ValueError):
|
||||
self._relayout_max_rows_spin.setValue(0)
|
||||
self._relayout_max_rows_spin.setFixedWidth(70)
|
||||
self._relayout_max_rows_spin.valueChanged.connect(
|
||||
lambda v: self._save_setting("relayout_max_rows", v)
|
||||
)
|
||||
msg_row.addWidget(width_lbl)
|
||||
msg_row.addWidget(self._relayout_width_spin)
|
||||
msg_row.addWidget(max_rows_lbl)
|
||||
msg_row.addWidget(self._relayout_max_rows_spin)
|
||||
msg_row.addStretch()
|
||||
layout.addLayout(msg_row)
|
||||
|
||||
desc_row = QHBoxLayout()
|
||||
desc_width_lbl = QLabel("Desc width (cells):")
|
||||
desc_width_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
||||
self._relayout_desc_width_spin = QSpinBox()
|
||||
self._relayout_desc_width_spin.setRange(20, 300)
|
||||
try:
|
||||
self._relayout_desc_width_spin.setValue(
|
||||
int(self._setting("relayout_desc_width", 75) or 75)
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
self._relayout_desc_width_spin.setValue(75)
|
||||
self._relayout_desc_width_spin.setFixedWidth(70)
|
||||
self._relayout_desc_width_spin.valueChanged.connect(
|
||||
lambda v: self._save_setting("relayout_desc_width", v)
|
||||
)
|
||||
desc_lines_lbl = QLabel("Desc max lines (0=auto):")
|
||||
desc_lines_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
||||
self._relayout_desc_lines_spin = QSpinBox()
|
||||
self._relayout_desc_lines_spin.setRange(0, 20)
|
||||
try:
|
||||
self._relayout_desc_lines_spin.setValue(
|
||||
int(self._setting("relayout_desc_max_lines", 0) or 0)
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
self._relayout_desc_lines_spin.setValue(0)
|
||||
self._relayout_desc_lines_spin.setFixedWidth(70)
|
||||
self._relayout_desc_lines_spin.valueChanged.connect(
|
||||
lambda v: self._save_setting("relayout_desc_max_lines", v)
|
||||
)
|
||||
desc_row.addWidget(desc_width_lbl)
|
||||
desc_row.addWidget(self._relayout_desc_width_spin)
|
||||
desc_row.addWidget(desc_lines_lbl)
|
||||
desc_row.addWidget(self._relayout_desc_lines_spin)
|
||||
desc_row.addStretch()
|
||||
layout.addLayout(desc_row)
|
||||
|
||||
now_btn = self._register(_make_btn("Relayout Data/ now", "#007acc"))
|
||||
now_btn.clicked.connect(lambda: self._run_relayout(manual=True))
|
||||
layout.addWidget(now_btn)
|
||||
|
||||
def _translated_or_source(self, json_name: str) -> Path | None:
|
||||
"""Prefer translated/<name>; fall back to files/<name>."""
|
||||
for base in ("translated", "files"):
|
||||
|
|
@ -2564,8 +2627,150 @@ class WolfWorkflowTab(QWidget):
|
|||
msg += ". Review the git diff in the game project and test in-game."
|
||||
else:
|
||||
msg += ". Continue to Step 6 to package."
|
||||
inject_state["applied"] = applied
|
||||
return failed == 0, msg
|
||||
|
||||
def _after_inject(ok: bool, msg: str):
|
||||
if inject_state.get("applied", 0) > 0 and self._relayout_after_enabled():
|
||||
# Defer until the inject worker has fully finished (busy flag clear).
|
||||
QTimer.singleShot(0, lambda: self._run_relayout(manual=False))
|
||||
|
||||
inject_state: dict = {"applied": 0}
|
||||
self._run_task(task, on_done=_after_inject)
|
||||
|
||||
def _relayout_after_enabled(self) -> bool:
|
||||
cb = getattr(self, "_relayout_after_cb", None)
|
||||
if cb is not None:
|
||||
return cb.isChecked()
|
||||
return str(self._setting("relayout_after_inject", "true")).lower() != "false"
|
||||
|
||||
def _relayout_desc_enabled(self) -> bool:
|
||||
cb = getattr(self, "_relayout_desc_cb", None)
|
||||
if cb is not None:
|
||||
return cb.isChecked()
|
||||
return str(self._setting("relayout_desc", "true")).lower() != "false"
|
||||
|
||||
def _relayout_settings(self) -> dict:
|
||||
def _spin(attr: str, key: str, default: int) -> int:
|
||||
w = getattr(self, attr, None)
|
||||
if w is not None:
|
||||
return int(w.value())
|
||||
try:
|
||||
return int(self._setting(key, default) or default)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
return {
|
||||
"width": _spin("_relayout_width_spin", "relayout_width", 55),
|
||||
"max_rows": _spin("_relayout_max_rows_spin", "relayout_max_rows", 0),
|
||||
"desc_width": _spin("_relayout_desc_width_spin", "relayout_desc_width", 75),
|
||||
"desc_max_lines": _spin(
|
||||
"_relayout_desc_lines_spin", "relayout_desc_max_lines", 0
|
||||
),
|
||||
}
|
||||
|
||||
def _find_db_projects(self, data_dir: Path) -> list[Path]:
|
||||
"""DataBase.project / CDataBase.project under BasicData or flat Data/."""
|
||||
found: list[Path] = []
|
||||
for base in (data_dir / "BasicData", data_dir):
|
||||
if not base.is_dir():
|
||||
continue
|
||||
for name in ("DataBase.project", "CDataBase.project"):
|
||||
p = base / name
|
||||
if p.is_file() and p not in found:
|
||||
found.append(p)
|
||||
return found
|
||||
|
||||
def _sync_relayout_tree(self, src_root: Path, dest_root: Path, log) -> int:
|
||||
"""Copy every file under *src_root* onto *dest_root*, preserving relative paths."""
|
||||
copied = 0
|
||||
for src in src_root.rglob("*"):
|
||||
if not src.is_file():
|
||||
continue
|
||||
rel = src.relative_to(src_root)
|
||||
dest = dest_root / rel
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dest)
|
||||
copied += 1
|
||||
log(f" ← {rel}")
|
||||
return copied
|
||||
|
||||
def _run_relayout(self, *, manual: bool = False):
|
||||
"""Reflow Message text (and optionally DB descriptions) on live Data/."""
|
||||
if not self._require_manifest():
|
||||
return
|
||||
manifest = self._read_manifest()
|
||||
data_dir = Path(manifest["data_dir"])
|
||||
if not data_dir.is_dir():
|
||||
QMessageBox.warning(self, "Relayout", f"Data/ folder not found:\n{data_dir}")
|
||||
return
|
||||
opts = self._relayout_settings()
|
||||
do_desc = self._relayout_desc_enabled()
|
||||
|
||||
def task(log, progress=None):
|
||||
from util import wolfdawn
|
||||
|
||||
log(
|
||||
f"Relayout Message text in {data_dir} "
|
||||
f"(width={opts['width']}"
|
||||
+ (f", max-rows={opts['max_rows']}" if opts["max_rows"] else "")
|
||||
+ ") …"
|
||||
)
|
||||
with tempfile.TemporaryDirectory(prefix="wolfdawn-relayout-") as tmp:
|
||||
out_dir = Path(tmp) / "out"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
res = wolfdawn.relayout(
|
||||
data_dir,
|
||||
out_dir=out_dir,
|
||||
width=opts["width"],
|
||||
max_rows=opts["max_rows"] or None,
|
||||
log_fn=log,
|
||||
)
|
||||
if res.stdout:
|
||||
for line in res.stdout.splitlines():
|
||||
log(line)
|
||||
if not res.ok and res.returncode not in (0, 2):
|
||||
return False, f"relayout exited {res.returncode}"
|
||||
n = self._sync_relayout_tree(out_dir, data_dir, log)
|
||||
log(f"Applied {n} reflowed file(s) onto live Data/.")
|
||||
|
||||
if do_desc:
|
||||
projects = self._find_db_projects(data_dir)
|
||||
if not projects:
|
||||
log("No DataBase.project / CDataBase.project found — skip desc-relayout.")
|
||||
for proj in projects:
|
||||
with tempfile.TemporaryDirectory(prefix="wolfdawn-desc-") as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
out_proj = tmp_path / proj.name
|
||||
log(
|
||||
f"desc-relayout {proj.name} "
|
||||
f"(width={opts['desc_width']}, max-lines={opts['desc_max_lines']}) …"
|
||||
)
|
||||
dres = wolfdawn.desc_relayout(
|
||||
proj,
|
||||
out_proj,
|
||||
width=opts["desc_width"],
|
||||
max_lines=opts["desc_max_lines"],
|
||||
log_fn=log,
|
||||
)
|
||||
if dres.stdout:
|
||||
for line in dres.stdout.splitlines():
|
||||
log(line)
|
||||
if not dres.ok and dres.returncode not in (0, 2):
|
||||
log(f" ⚠ desc-relayout exit {dres.returncode} for {proj.name}")
|
||||
continue
|
||||
# WolfDawn may also write a sibling .dat next to the out .project
|
||||
for produced in tmp_path.iterdir():
|
||||
if not produced.is_file():
|
||||
continue
|
||||
dest = proj.parent / produced.name
|
||||
shutil.copy2(produced, dest)
|
||||
log(f" ← {dest.relative_to(data_dir)}")
|
||||
|
||||
return True, "Relayout complete." + (
|
||||
" Continue to Step 6 to package." if not manual else ""
|
||||
)
|
||||
|
||||
self._run_task(task)
|
||||
|
||||
# ── Step 5: Package ────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -25,12 +25,10 @@ names.json, translatable entries are harvested into ``vocab.txt`` (grouped by
|
|||
``note``, with bilingual headers) so later phases keep item / skill / term names
|
||||
consistent.
|
||||
|
||||
Text wrapping: translated dialogue is re-wrapped to a character width (like the
|
||||
RPGMaker module, via :mod:`util.dazedwrap`) so English fits WOLF's message box.
|
||||
Wrapping is applied to the dialogue *body* only - a speaker's nameplate line (the
|
||||
``\n`` right after a name) is kept on its own line and never folded into the body.
|
||||
Configurable from the workflow (``.env``: ``wolfWrap`` / ``wolfWidth``); a width of
|
||||
0 (or ``wolfWrap=false``) writes the model output back verbatim.
|
||||
Text wrapping: optional advanced ``wolfWrap`` / ``wolfWidth`` (via :mod:`util.dazedwrap`)
|
||||
can still re-wrap dialogue bodies at translate time, but the normal Wolf workflow
|
||||
leaves JSON line breaks alone and runs WolfDawn ``relayout`` after inject (Step 5)
|
||||
to fit the message box. Defaults to off (``wolfWrap=false``).
|
||||
|
||||
names.json category wrap (Step 3): selected ``note`` categories can be re-wrapped
|
||||
with dazedwrap at a dedicated width (``.env``: ``wolfNameWrap``,
|
||||
|
|
@ -94,12 +92,9 @@ SPEAKER_CONFIG = wolf_speakers.load_config()
|
|||
|
||||
# names.json: translate per-entry safe/refs badges only (see util.wolf_names).
|
||||
|
||||
# Text wrapping: rewrap translated dialogue to a character width the same way the
|
||||
# RPGMaker module does (util.dazedwrap), so English lines fit WOLF's message box.
|
||||
# Only the dialogue *body* is wrapped - the speaker's name line (the "\n" after a
|
||||
# nameplate) is kept on its own line and never merged into the body. Configurable
|
||||
# from the workflow (.env: wolfWrap / wolfWidth). Width <= 0 disables wrapping.
|
||||
WRAP = (os.getenv("wolfWrap", "true").strip().lower() == "true")
|
||||
# Text wrapping: optional advanced rewrap via dazedwrap (wolfWrap/wolfWidth). Defaults
|
||||
# off - the workflow uses WolfDawn relayout after inject instead.
|
||||
WRAP = (os.getenv("wolfWrap", "false").strip().lower() == "true")
|
||||
try:
|
||||
WRAPWIDTH = int(os.getenv("wolfWidth") or 0)
|
||||
except ValueError:
|
||||
|
|
|
|||
|
|
@ -418,26 +418,47 @@ class TestNameCategoryWrap(unittest.TestCase):
|
|||
self.assertEqual(wd._wrap_names_entry(long_text, entry), long_text)
|
||||
|
||||
def test_names_translation_wraps_selected_category(self):
|
||||
# Narrow JP source (8 cells of CJK) so LANGREGEX matches; mock returns long EN.
|
||||
profile_src = "あいうえ" # 4 CJK → matches LANGREGEX
|
||||
doc = {
|
||||
"kind": "names",
|
||||
"names": [
|
||||
{"source": "剣", "text": "剣", "note": "武器", "safety": "safe"},
|
||||
{
|
||||
"source": "alpha beta gamma delta epsilon zeta",
|
||||
"text": "alpha beta gamma delta epsilon zeta",
|
||||
"source": profile_src,
|
||||
"text": profile_src,
|
||||
"note": "├■プロフィール",
|
||||
"safety": "safe",
|
||||
},
|
||||
],
|
||||
}
|
||||
self._set(True, 8, {"├■プロフィール"})
|
||||
long_en = "alpha beta gamma delta epsilon zeta eta theta"
|
||||
orig_t = wd.translateAI
|
||||
orig_estimate = wd.ESTIMATE
|
||||
orig_wrap = wd.WRAP
|
||||
wd.WRAP = True
|
||||
wd.WRAPWIDTH = 4
|
||||
orig_update = wd.wolf_vocab.update_vocab_section
|
||||
orig_labels = wd.wolf_names.derive_db_labels
|
||||
|
||||
def translate(text, history, history_ctx=None):
|
||||
out = []
|
||||
for t in text:
|
||||
out.append(long_en if t == profile_src else f"EN_{t}")
|
||||
return [out, [1, 1]]
|
||||
|
||||
wd.translateAI = translate
|
||||
wd.ESTIMATE = False
|
||||
wd.WRAP = False
|
||||
wd.wolf_vocab.update_vocab_section = lambda *_a, **_k: None
|
||||
wd.wolf_names.derive_db_labels = lambda _p: {}
|
||||
try:
|
||||
(data, _t, err), _c = _WolfTranslateHarness().run(doc, "names.json")
|
||||
data, _t, err = wd.parseDocument(copy.deepcopy(doc), "names.json")
|
||||
finally:
|
||||
wd.translateAI = orig_t
|
||||
wd.ESTIMATE = orig_estimate
|
||||
wd.WRAP = orig_wrap
|
||||
wd.wolf_vocab.update_vocab_section = orig_update
|
||||
wd.wolf_names.derive_db_labels = orig_labels
|
||||
self.assertIsNone(err)
|
||||
names = {n["note"]: n["text"] for n in data["names"]}
|
||||
self.assertEqual(names["武器"], "EN_剣")
|
||||
|
|
|
|||
87
tests/test_wolfdawn_relayout.py
Normal file
87
tests/test_wolfdawn_relayout.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Unit tests for WolfDawn relayout / desc_relayout CLI wrappers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
os.chdir(ROOT)
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from util import wolfdawn # noqa: E402
|
||||
|
||||
|
||||
class TestRelayoutWrappers(unittest.TestCase):
|
||||
def test_relayout_dry_run_args(self):
|
||||
captured = {}
|
||||
|
||||
def fake_run(args, log_fn=None):
|
||||
captured["args"] = list(args)
|
||||
return wolfdawn.WolfResult(0, "", "", [str(a) for a in args])
|
||||
|
||||
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
||||
wolfdawn.relayout("/game/Data", width=55, max_rows=0)
|
||||
self.assertEqual(captured["args"][:2], ["relayout", "/game/Data"])
|
||||
self.assertIn("--width", captured["args"])
|
||||
self.assertIn("55", captured["args"])
|
||||
self.assertNotIn("-o", captured["args"])
|
||||
self.assertNotIn("--max-rows", captured["args"]) # 0 omitted
|
||||
|
||||
def test_relayout_write_args(self):
|
||||
captured = {}
|
||||
|
||||
def fake_run(args, log_fn=None):
|
||||
captured["args"] = list(args)
|
||||
return wolfdawn.WolfResult(0, "", "", [str(a) for a in args])
|
||||
|
||||
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
||||
wolfdawn.relayout(
|
||||
"/game/Data",
|
||||
out_dir="/tmp/out",
|
||||
width=60,
|
||||
max_rows=4,
|
||||
no_resolve=True,
|
||||
)
|
||||
args = captured["args"]
|
||||
self.assertEqual(args[0], "relayout")
|
||||
self.assertIn("-o", args)
|
||||
self.assertIn("/tmp/out", args)
|
||||
self.assertIn("--width", args)
|
||||
self.assertIn("60", args)
|
||||
self.assertIn("--max-rows", args)
|
||||
self.assertIn("4", args)
|
||||
self.assertIn("--no-resolve", args)
|
||||
|
||||
def test_desc_relayout_args(self):
|
||||
captured = {}
|
||||
|
||||
def fake_run(args, log_fn=None):
|
||||
captured["args"] = list(args)
|
||||
return wolfdawn.WolfResult(0, "", "", [str(a) for a in args])
|
||||
|
||||
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
||||
wolfdawn.desc_relayout(
|
||||
"/game/Data/BasicData/DataBase.project",
|
||||
"/tmp/DataBase.project",
|
||||
width=75,
|
||||
max_lines=0,
|
||||
keep_breaks=True,
|
||||
)
|
||||
args = captured["args"]
|
||||
self.assertEqual(args[0], "desc-relayout")
|
||||
self.assertIn("-o", args)
|
||||
self.assertIn("/tmp/DataBase.project", args)
|
||||
self.assertIn("--width", args)
|
||||
self.assertIn("75", args)
|
||||
self.assertIn("--max-lines", args)
|
||||
self.assertIn("0", args)
|
||||
self.assertIn("--keep-breaks", args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
@ -47,6 +47,8 @@ __all__ = [
|
|||
"pack",
|
||||
"save_update",
|
||||
"names_check",
|
||||
"relayout",
|
||||
"desc_relayout",
|
||||
]
|
||||
|
||||
# strings-inject: "applied N translation(s) (M drifted)"; names-inject uses "name change(s)".
|
||||
|
|
@ -553,3 +555,77 @@ def save_update(
|
|||
args.append("--translations")
|
||||
args += [_str(p) for p in translations]
|
||||
return _run(args, log_fn=log_fn)
|
||||
|
||||
|
||||
def relayout(
|
||||
data_dir: PathLike,
|
||||
out_dir: Optional[PathLike] = None,
|
||||
*,
|
||||
width: Optional[int] = None,
|
||||
width_face: Optional[int] = None,
|
||||
max_rows: Optional[int] = None,
|
||||
sub_width: Optional[int] = None,
|
||||
base_font: Optional[int] = None,
|
||||
no_formup: bool = False,
|
||||
no_resolve: bool = False,
|
||||
log_fn=None,
|
||||
) -> WolfResult:
|
||||
"""``wolf relayout <data-dir> [-o <out-dir>] [flags]``.
|
||||
|
||||
Without ``-o`` this is a dry run. With ``-o``, only changed map / CommonEvent
|
||||
files are written under ``out_dir`` (paths relative to ``data_dir``).
|
||||
"""
|
||||
args: list[str] = ["relayout", _str(data_dir)]
|
||||
if out_dir is not None:
|
||||
args += ["-o", _str(out_dir)]
|
||||
if width is not None:
|
||||
args += ["--width", str(int(width))]
|
||||
if width_face is not None:
|
||||
args += ["--width-face", str(int(width_face))]
|
||||
if max_rows is not None and int(max_rows) > 0:
|
||||
args += ["--max-rows", str(int(max_rows))]
|
||||
if sub_width is not None:
|
||||
args += ["--sub-width", str(int(sub_width))]
|
||||
if base_font is not None:
|
||||
args += ["--base-font", str(int(base_font))]
|
||||
if no_formup:
|
||||
args.append("--no-formup")
|
||||
if no_resolve:
|
||||
args.append("--no-resolve")
|
||||
return _run(args, log_fn=log_fn)
|
||||
|
||||
|
||||
def desc_relayout(
|
||||
project: PathLike,
|
||||
output: PathLike,
|
||||
*,
|
||||
width: Optional[int] = None,
|
||||
max_lines: Optional[int] = None,
|
||||
font: Optional[int] = None,
|
||||
min_font: Optional[int] = None,
|
||||
types: Optional[str] = None,
|
||||
keep_breaks: bool = False,
|
||||
no_formup: bool = False,
|
||||
log_fn=None,
|
||||
) -> WolfResult:
|
||||
"""``wolf desc-relayout <X.project> -o <out.project> [flags]``.
|
||||
|
||||
``width`` may be omitted when ``wolfdawn-roles.json`` defines ``descBoxes``.
|
||||
``max_lines=0`` means auto (read each field's ``[N行]`` hint).
|
||||
"""
|
||||
args: list[str] = ["desc-relayout", _str(project), "-o", _str(output)]
|
||||
if width is not None:
|
||||
args += ["--width", str(int(width))]
|
||||
if max_lines is not None:
|
||||
args += ["--max-lines", str(int(max_lines))]
|
||||
if font is not None:
|
||||
args += ["--font", str(int(font))]
|
||||
if min_font is not None:
|
||||
args += ["--min-font", str(int(min_font))]
|
||||
if types:
|
||||
args += ["--types", str(types)]
|
||||
if keep_breaks:
|
||||
args.append("--keep-breaks")
|
||||
if no_formup:
|
||||
args.append("--no-formup")
|
||||
return _run(args, log_fn=log_fn)
|
||||
|
|
|
|||
Loading…
Reference in a new issue