moremoremoremore

This commit is contained in:
dazedanon 2026-03-12 22:24:11 -05:00
parent 9b5915888a
commit 174fbcad98

View file

@ -123,6 +123,23 @@ class _ImportWorker(QThread):
def run(self):
try:
from util.project_scanner import import_to_files
# Clear existing files/ contents before importing so stale files
# from a previous game don't linger. translated/ is intentionally
# left untouched.
dest = Path(self.dest_dir)
if dest.exists():
removed = 0
for fp in dest.iterdir():
if fp.is_file() and fp.name != ".gitkeep":
try:
fp.unlink()
removed += 1
except Exception as e:
self.log.emit(f" ⚠ Could not remove {fp.name}: {e}")
if removed:
self.log.emit(f"Cleared {removed} existing file(s) from {dest.name}/")
self.log.emit(f"Importing {len(self.file_items)} file(s) into files/ …")
count, errors = import_to_files(self.file_items, self.dest_dir)
self.done.emit(count, errors)
@ -134,15 +151,23 @@ class _ExportWorker(QThread):
done = pyqtSignal(int, list)
log = pyqtSignal(str)
def __init__(self, game_data_path: str):
def __init__(self, game_data_path: str, filter_names: list[str] | None = None):
super().__init__()
self.game_data_path = game_data_path
self.filter_names = filter_names # if set, only export these filenames
def run(self):
try:
from util.project_scanner import export_to_game
self.log.emit(f"Exporting translated/ → {self.game_data_path}")
count, errors = export_to_game("translated", self.game_data_path)
if self.filter_names:
self.log.emit(
f"Exporting {len(self.filter_names)} active file(s) → {self.game_data_path}"
)
else:
self.log.emit(f"Exporting translated/ → {self.game_data_path}")
count, errors = export_to_game(
"translated", self.game_data_path, filenames=self.filter_names
)
self.done.emit(count, errors)
except Exception as exc:
self.done.emit(0, [str(exc)])
@ -456,9 +481,9 @@ class WorkflowTab(QWidget):
# Action row
row1 = QHBoxLayout()
sel_all = _make_btn("Select All", "#555")
sel_all.clicked.connect(self._select_all_files)
row1.addWidget(sel_all)
self.sel_all_btn = _make_btn("Select All", "#555")
self.sel_all_btn.clicked.connect(self._select_all_files)
row1.addWidget(self.sel_all_btn)
sel_core = _make_btn("Core Only", "#555")
sel_core.setToolTip("Select only core database files; deselect map files")
@ -654,6 +679,82 @@ class WorkflowTab(QWidget):
"translation tool only recognises the plain hyphen."
)
_WRAP_PROMPT = (
"You are helping me configure text-wrap widths for a Japanese RPGMaker MV/MZ translation tool.\n"
"\n"
"The tool wraps translated English text using a character-count limit — NOT pixels — so\n"
"I need to know how many English characters fit comfortably on one line for three\n"
"different window types:\n"
"\n"
" width — main dialogue / message box (Show Text / code 401)\n"
" listWidth — item / skill / help description windows\n"
" noteWidth — database note fields (item, weapon, armour, skill descriptions)\n"
"\n"
"## Step 1 — Gather screen and font settings\n"
"\n"
"Read System.json and report:\n"
" - screenWidth and screenHeight (or window_width / window_height for older MV)\n"
" - fontSize (default 26 for MV; may be absent in MZ if using default)\n"
" - advanced.uiAreaWidth / advanced.uiAreaHeight if present\n"
"\n"
"## Step 2 — Check plugins for window overrides\n"
"\n"
"Search plugins.js (or js/plugins.js) for any of the following and report their\n"
"relevant parameters if present:\n"
"\n"
" VisuMZ_1_MessageCore → MessageWidth, DefaultRows, FontSize\n"
" YEP_MessageCore → messageWidth, messageRows\n"
" MOG_MessageSystem → message_width, message_height\n"
" Any other plugin whose name contains 'Message' or 'Window' with size config\n"
"\n"
"## Step 3 — Calculate character widths\n"
"\n"
"Use this formula for each window type:\n"
"\n"
" content_px = window_pixel_width - (horizontal_padding × 2)\n"
" assume 24px padding per side if unknown\n"
" avg_char_width_px = font_size_px × 0.58 (empirical average for English sans-serif)\n"
" chars_per_line = floor(content_px / avg_char_width_px)\n"
"\n"
"For listWidth, the relevant window is the help/description pane in the menu\n"
"(usually full screen width or half, depending on the game's menu layout).\n"
"For noteWidth, use the narrowest description area you can find — often an item\n"
"detail window that is roughly 4050% of screen width.\n"
"\n"
"Show your working for each of the three values.\n"
"\n"
"## Step 4 — Font size advice\n"
"\n"
"If the game's font size is larger than 26px, consider whether reducing it would\n"
"let more translated text fit without excessive wrapping. Only recommend a change\n"
"if the gain is meaningful (e.g. 30px → 26px adds ~6 characters per line).\n"
"\n"
"If a font-size change is advisable, identify exactly where to make it:\n"
" a) System.json 'fontSize' field\n"
" b) VisuMZ_1_MessageCore or YEP_MessageCore 'FontSize' parameter in plugins.js\n"
" c) A CSS / font override file if the game uses a custom web font\n"
"\n"
"## Required output format\n"
"\n"
"Reply with EXACTLY this block — fill in the numbers from your analysis:\n"
"\n"
"```\n"
"# Text wrap recommendations\n"
"\n"
"Screen resolution : <W>x<H> (source: System.json / plugin override)\n"
"Font size : <N>px (source: System.json / plugin override)\n"
"\n"
"Dialogue window : <pixel_width>px wide → width=<N>\n"
"List/help window : <pixel_width>px wide → listWidth=<N>\n"
"Note window : <pixel_width>px wide → noteWidth=<N>\n"
"\n"
"Font size change : <'No change needed' OR 'Change Xpx → Ypx in <location>'>\n"
"```\n"
"\n"
"After the block, include a short paragraph explaining any assumptions you made\n"
"(e.g. padding estimates, windows whose pixel width could not be determined directly).\n"
)
# ── Step 1 (Optional): Pre-process ────────────────────────────────
def _build_preprocess(self, layout: QVBoxLayout):
@ -823,7 +924,7 @@ class WorkflowTab(QWidget):
layout.addWidget(hint)
# ---- Parse Speakers -------------------------------------------------
spk_box = QGroupBox("2a — Parse Speakers (Auto-detect Names)")
spk_box = QGroupBox("3a — Parse Speakers (Auto-detect Names)")
spk_box.setStyleSheet(
"QGroupBox{color:#ccc;border:1px solid #444;border-radius:3px;"
"margin-top:8px;font-size:11px;}"
@ -858,7 +959,7 @@ class WorkflowTab(QWidget):
layout.addWidget(spk_box)
# ---- Copilot / Cursor prompt helpers --------------------------------
prompt_box = QGroupBox("2b — AI Prompt Helpers (Copilot / Cursor)")
prompt_box = QGroupBox("3b — AI Prompt Helpers (Copilot / Cursor)")
prompt_box.setStyleSheet(
"QGroupBox{color:#ccc;border:1px solid #444;border-radius:3px;"
"margin-top:8px;font-size:11px;}"
@ -992,6 +1093,71 @@ class WorkflowTab(QWidget):
def _build_step4(self, layout: QVBoxLayout):
layout.addWidget(_make_section_label("Step 4 — Translation"))
# ---- Pre-flight: text wrap configuration ----------------------------
wrap_box = QGroupBox("Pre-flight — Text Wrap Width")
wrap_box.setStyleSheet(
"QGroupBox{color:#ccc;border:1px solid #444;border-radius:3px;"
"margin-top:4px;font-size:11px;}"
"QGroupBox::title{padding:0 6px;}"
)
wrap_inner = QVBoxLayout(wrap_box)
wrap_inner.setSpacing(6)
wrap_hint = QLabel(
"Copy the prompt, paste it into Copilot with System.json and plugins.js attached. "
"It will calculate how many English characters fit per line for each window type. "
"Enter the recommended values and click Apply to update .env."
)
wrap_hint.setWordWrap(True)
wrap_hint.setStyleSheet("color:#888;font-size:10px;")
wrap_inner.addWidget(wrap_hint)
copy_wrap_row = QHBoxLayout()
copy_wrap_btn = _make_btn("📋 Copy Wrap Prompt", "#555")
copy_wrap_btn.setToolTip("Copy the text-wrap analysis prompt to clipboard")
copy_wrap_btn.clicked.connect(self._copy_wrap_prompt)
copy_wrap_row.addWidget(copy_wrap_btn)
copy_wrap_row.addStretch()
wrap_inner.addLayout(copy_wrap_row)
def _spin_pair(label_text: str, default: int):
lbl = QLabel(label_text)
lbl.setStyleSheet("color:#aaa;font-size:10px;")
sp = QSpinBox()
sp.setRange(20, 300)
sp.setValue(default)
sp.setStyleSheet(
"QSpinBox{background:#2d2d30;color:#ccc;border:1px solid #555;"
"padding:2px 4px;font-size:10px;border-radius:2px;}"
)
sp.setFixedWidth(68)
return lbl, sp
lbl_w, self.wrap_width_spin = _spin_pair("Dialogue (width)", 60)
lbl_lw, self.wrap_list_spin = _spin_pair("List/Help (listWidth)", 80)
lbl_nw, self.wrap_note_spin = _spin_pair("Notes (noteWidth)", 60)
for lbl, sp in [(lbl_w, self.wrap_width_spin),
(lbl_lw, self.wrap_list_spin),
(lbl_nw, self.wrap_note_spin)]:
row = QHBoxLayout()
row.setSpacing(6)
lbl.setFixedWidth(160)
row.addWidget(lbl)
row.addWidget(sp)
row.addStretch()
wrap_inner.addLayout(row)
apply_wrap_row = QHBoxLayout()
apply_wrap_btn = _make_btn("✔ Apply to .env", "#3a7a3a")
apply_wrap_btn.setToolTip("Write width / listWidth / noteWidth into .env")
apply_wrap_btn.clicked.connect(self._apply_wrap_config)
apply_wrap_row.addStretch()
apply_wrap_row.addWidget(apply_wrap_btn)
wrap_inner.addLayout(apply_wrap_row)
layout.addWidget(wrap_box)
p1_box = QGroupBox("Phase 1 Safe Codes (dialogue + choices)")
p1_box.setStyleSheet(
"QGroupBox{color:#ccc;border:1px solid #444;border-radius:3px;"
@ -999,13 +1165,17 @@ class WorkflowTab(QWidget):
"QGroupBox::title{padding:0 6px;}"
)
p1_inner = QVBoxLayout(p1_box)
p1_desc = QLabel("Codes: 401 (Show Text), 405 (continued), 102 (Choices), 408 (extra lines)\n"
"These rarely break game logic — safe to translate first.")
p1_desc = QLabel(
"Codes ON: 101 (Name), 401 (Show Text), 405 (continued), 102 (Choices), 408 (extra lines)\n"
"Clicking the button below applies these codes, then takes you to the Translation tab.\n"
"While translating, watch the log — speaker lines should look like [Speaker]: Dialogue."
)
p1_desc.setStyleSheet("color:#888;font-size:10px;")
p1_desc.setWordWrap(True)
p1_inner.addWidget(p1_desc)
p1_row = QHBoxLayout()
run_p1 = _make_btn("▶ Run Phase 1", "#007acc")
run_p1 = _make_btn("⚙ Set Codes \u2192 Go to Translation", "#007acc")
run_p1.setToolTip("Applies Phase 1 code settings and switches to the Translation tab")
run_p1.clicked.connect(lambda: self._run_phase(1))
p1_row.addWidget(run_p1)
p1_row.addStretch()
@ -1020,9 +1190,9 @@ class WorkflowTab(QWidget):
)
p2_inner = QVBoxLayout(p2_box)
p2_desc = QLabel(
"Codes: 122 (Control Variables), 357 (Plugin Commands), 111 (Conditional Branch)\n"
"Strings in these codes are often used in script comparisons — mistranslations "
"can break game logic. Scan first, then translate carefully."
"Codes ON: 122 (Control Variables), 357 (Plugin Commands), 111 (Conditional Branch)\n"
"Strings here are often used in script comparisons — mistranslations can break game "
"logic. Use Scan first to preview what will be translated, then apply and go translate."
)
p2_desc.setStyleSheet("color:#888;font-size:10px;")
p2_desc.setWordWrap(True)
@ -1037,7 +1207,8 @@ class WorkflowTab(QWidget):
scan_p2.clicked.connect(self._scan_risky_strings)
p2_row.addWidget(scan_p2)
run_p2 = _make_btn("▶ Run Phase 2", "#7a4a00")
run_p2 = _make_btn("⚙ Set Codes \u2192 Go to Translation", "#7a4a00")
run_p2.setToolTip("Applies Phase 2 code settings and switches to the Translation tab")
run_p2.clicked.connect(lambda: self._run_phase(2))
p2_row.addWidget(run_p2)
p2_row.addStretch()
@ -1049,17 +1220,25 @@ class WorkflowTab(QWidget):
def _build_step6(self, layout: QVBoxLayout):
layout.addWidget(_make_section_label("Step 5 — Export to Game"))
hint = QLabel(
"Copy all files from translated/ back into the game's data folder "
"to patch the game in-place."
"Copy translated files back into the game's data folder to patch the game in-place."
)
hint.setWordWrap(True)
hint.setStyleSheet("color:#888;font-size:10px;padding-bottom:4px;")
layout.addWidget(hint)
row = QHBoxLayout()
export_btn = _make_btn("📤 Export translated/ → Game Folder", "#3a7a3a")
export_btn.clicked.connect(self._export_to_game)
row.addWidget(export_btn)
export_active_btn = _make_btn("📤 Export Active Files → Game Folder", "#3a7a3a")
export_active_btn.setToolTip(
"Only export files whose names match those currently in files/\n"
"(i.e. the files you imported for this project)"
)
export_active_btn.clicked.connect(self._export_active_files)
row.addWidget(export_active_btn)
export_all_btn = _make_btn("📤 Export ALL translated/ → Game Folder", "#555")
export_all_btn.setToolTip("Export every file in translated/ regardless of what is in files/")
export_all_btn.clicked.connect(self._export_to_game)
row.addWidget(export_all_btn)
row.addStretch()
layout.addLayout(row)
@ -1130,15 +1309,27 @@ class WorkflowTab(QWidget):
self.file_list.addItem(lw)
self.import_btn.setEnabled(len(items) > 0)
self.sel_all_btn.setText("Select All")
self._log(f"Found {len(items)} importable file(s).")
self._populate_preprocess_paths()
def _select_all_files(self):
count = self.file_list.count()
if not count:
return
all_checked = all(
self.file_list.item(i).checkState() == Qt.Checked
for i in range(count)
)
new_state = Qt.Unchecked if all_checked else Qt.Checked
for i in range(count):
self.file_list.item(i).setCheckState(Qt.Checked)
if count:
self.file_list.item(i).setCheckState(new_state)
if new_state == Qt.Checked:
self.sel_all_btn.setText("Deselect All")
self._log(f"✔ Selected all {count} file(s).")
else:
self.sel_all_btn.setText("Select All")
self._log(f"✔ Deselected all {count} file(s).")
def _select_core_only(self):
core = other = 0
@ -1316,6 +1507,38 @@ class WorkflowTab(QWidget):
QApplication.clipboard().setText(self._SPEAKER_PROMPT)
self._log("Speaker format prompt copied to clipboard.")
def _copy_wrap_prompt(self):
QApplication.clipboard().setText(self._WRAP_PROMPT)
self._log("Text-wrap analysis prompt copied to clipboard.")
def _apply_wrap_config(self):
"""Write width / listWidth / noteWidth back into .env."""
import re as _re
updates = {
"width": str(self.wrap_width_spin.value()),
"listWidth": str(self.wrap_list_spin.value()),
"noteWidth": str(self.wrap_note_spin.value()),
}
env_path = Path(".env")
try:
text = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
for key, val in updates.items():
text, n = _re.subn(
rf"^({_re.escape(key)}\s*=\s*')[^']*(')",
rf"\g<1>{val}\2",
text,
flags=_re.MULTILINE,
)
if n == 0:
text = text.rstrip("\n") + f"\n{key}='{val}'\n"
env_path.write_text(text, encoding="utf-8")
self._log(
"✅ .env updated — "
+ ", ".join(f"{k}={v}" for k, v in updates.items())
)
except Exception as exc:
self._log(f"❌ Could not update .env: {exc}")
def _apply_speaker_flags(self):
cfg = {
"INLINE401SPEAKERS": self.spk_inline_cb.isChecked(),
@ -1347,57 +1570,61 @@ class WorkflowTab(QWidget):
config = PHASE1_CONFIG if phase == 1 else PHASE2_CONFIG
label = "Phase 1 (safe codes)" if phase == 1 else "Phase 2 (risky codes)"
# Apply config profile
# Apply config profile so the Translation tab uses the right codes
try:
from gui.config_integration import ConfigIntegration
ci = ConfigIntegration()
ci.update_rpgmaker_config(config)
self._log(f"Applied {label} config profile.")
# Sync the live Settings tab if it is open
try:
if self.parent_window and hasattr(self.parent_window, "config_tab"):
ct = self.parent_window.config_tab
if hasattr(ct, "rpgmaker_tab"):
ct.rpgmaker_tab.set_config(
ct.rpgmaker_tab.get_config() | config
)
except Exception:
pass
self._log(f"{label} config applied — codes set:")
on = [k for k, v in config.items() if v]
off = [k for k, v in config.items() if not v]
if on:
self._log(" ON : " + " ".join(on))
if off:
self._log(" OFF: " + " ".join(off))
except Exception as exc:
self._log(f"❌ Could not apply phase config: {exc}")
return
# Launch translation worker
# Phase-specific guidance
if phase == 1:
self._log("")
self._log("" * 54)
self._log("👉 Switch to the Translation tab and start the run.")
self._log("")
self._log("⚠ While translating, watch the log for speaker lines.")
self._log(" They should look like: [Speaker]: Dialogue text")
self._log(" If names are missing or garbled, stop the run and")
self._log(" revisit Step 2 (speaker flags) before continuing.")
self._log("" * 54)
else:
self._log("")
self._log("" * 54)
self._log("👉 Switch to the Translation tab and start the run.")
self._log(" Phase 2 targets script/variable strings — review")
self._log(" the Risky String scan output before proceeding.")
self._log("" * 54)
# Navigate to the Translation tab (index 0 in content_stack)
try:
from gui.translation_tab import TranslationWorker
except Exception as exc:
self._log(f"❌ Could not import TranslationWorker: {exc}")
return
project_root = Path(__file__).parent.parent
module_info = ["RPG Maker MV/MZ", [".json"], None]
# Build file order: core DB files first (they populate the glossary),
# then map files. Within each group, sort alphabetically.
files_dir = project_root / "files"
all_json = sorted(
p.name for p in files_dir.glob("*.json") if p.name != ".gitkeep"
) if files_dir.exists() else []
core_files = [f for f in all_json if not f.lower().startswith("map")]
map_files = [f for f in all_json if f.lower().startswith("map")]
ordered_files = core_files + map_files
if ordered_files:
self._log(
f"File order: {len(core_files)} core file(s) first, "
f"then {len(map_files)} map file(s)."
)
self._log(f"Starting {label} translation…")
worker = TranslationWorker(
project_root, module_info, estimate_only=False,
selected_files=ordered_files if ordered_files else None,
)
worker.log_signal.connect(self._log)
worker.progress_signal.connect(
lambda cur, tot, fn: self._log(f" [{cur}/{tot}] {fn}")
)
worker.finished_signal.connect(
lambda ok, msg: self._log(
f"{'' if ok else ''} {label} finished: {msg}"
)
)
self._worker = worker
worker.start()
if self.parent_window and hasattr(self.parent_window, "content_stack"):
self.parent_window.content_stack.setCurrentIndex(0)
# Keep nav buttons in sync
if hasattr(self.parent_window, "nav_buttons"):
for i, btn in enumerate(self.parent_window.nav_buttons):
btn.setChecked(i == 0)
except Exception:
pass
# ─────────────────────────────────────────────────────────────────────────
# Risky string scanner (preview mode)
@ -1447,21 +1674,47 @@ class WorkflowTab(QWidget):
# Step 5 Export to game
# ─────────────────────────────────────────────────────────────────────────
def _export_to_game(self):
game_data = self._data_path
def _export_active_files(self):
"""Export only translated files whose names match what is in files/."""
files_dir = Path("files")
active = sorted(
fp.name for fp in files_dir.glob("*.json") if fp.name != ".gitkeep"
) if files_dir.exists() else []
if not active:
self._log("⚠ No files found in files/ — run Step 0 (Import) first.")
return
game_data = self._resolve_export_path()
if not game_data:
# Prompt the user
game_data = QFileDialog.getExistingDirectory(
self, "Select Game Data Folder to Export Into"
)
if not game_data:
return
self._data_path = game_data
return
reply = QMessageBox.question(
self,
"Export Active Files to Game",
f"Export {len(active)} file(s) matching files/ contents into:\n{game_data}\n\n"
"Make a backup first if needed. Continue?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
if reply != QMessageBox.Yes:
return
w = _ExportWorker(game_data, filter_names=active)
w.log.connect(self._log)
w.done.connect(self._on_export_done)
self._worker = w
w.start()
def _export_to_game(self):
game_data = self._resolve_export_path()
if not game_data:
return
reply = QMessageBox.question(
self,
"Export to Game",
f"This will overwrite files in:\n{game_data}\n\n"
f"This will overwrite ALL translated files in:\n{game_data}\n\n"
"Make a backup first if needed. Continue?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
@ -1475,6 +1728,18 @@ class WorkflowTab(QWidget):
self._worker = w
w.start()
def _resolve_export_path(self) -> str | None:
"""Return the game data path, prompting if not yet set."""
game_data = self._data_path
if not game_data:
game_data = QFileDialog.getExistingDirectory(
self, "Select Game Data Folder to Export Into"
)
if not game_data:
return None
self._data_path = game_data
return game_data
def _on_export_done(self, count: int, errors: list):
if errors:
self._log(f"{len(errors)} error(s) during export:")