feat(rpgmaker): integrate images into the guided workflow
- Add image readiness checks and navigation from workflow Step 6 - Keep long Image Manager folder lists bounded and scrollable - Update workflow guidance, button labels, and regression coverage - Disable code 357 and 108 translation by default
This commit is contained in:
parent
979d0bd374
commit
56453e922b
7 changed files with 525 additions and 31 deletions
|
|
@ -14,7 +14,8 @@ This page is the overview.
|
|||
| **3 TL Phase 1** | Phase 0 DB → Phase 1 dialogue → Phase 1b code 111 cache |
|
||||
| **4 TL Phase 2** | Risky codes (122, plugins, etc.) after you audit |
|
||||
| **5 Export** | MV/MZ: `plugins.js` helpers. Ace: Ruby `.rb` scripts. Then export / pack |
|
||||
| **6 Playtest** | TL Inspector / Forge (MV/MZ; hidden for Ace) |
|
||||
| **6 Images** | Check image setup, then decrypt → AI translate → review → patch (MV/MZ) |
|
||||
| **7 Playtest** | TL Inspector / Forge (MV/MZ; hidden for Ace) |
|
||||
|
||||
## Example: first RPG Maker run
|
||||
|
||||
|
|
@ -42,6 +43,24 @@ Project Setup's `speakers` block recommends ENABLE / SKIP with evidence.
|
|||
- Use **Normal** mode when you want live iteration on a handful of files.
|
||||
- Phase 2 can break games if you translate logic keys - audit with the Plugin Prompt first.
|
||||
|
||||
## Images (Step 6, MV/MZ)
|
||||
|
||||
Step 6 keeps the Image Manager on the separate **Images** page, but verifies that you are ready
|
||||
to use it:
|
||||
|
||||
- Step 0 points to the actual game root containing `img/` or `www/img/`.
|
||||
- Encrypted projects have a valid key in `System.json`.
|
||||
- `<game>/vocab.txt` exists for the copied AI skill.
|
||||
- Editable PNGs are under `.dazedtl/images/<game-relative>/img/...`.
|
||||
- No PNGs were accidentally placed beside that expected `img/` tree.
|
||||
|
||||
Click **Open Images**, decrypt the images you want to translate, then click **Copy skill** in the
|
||||
Image Manager and paste it into Codex, Cursor, Copilot, or a similar coding agent. Review the
|
||||
resulting PNGs before using **Patch selected** or **Patch all**.
|
||||
|
||||
Do not edit the runtime images directly. The Image Manager maintains editable copies and patches
|
||||
the reviewed results back into the game.
|
||||
|
||||
## Ace note (Ruby scripts, not plugins.js)
|
||||
|
||||
Ace has no `plugins.js`. UI / menu strings often live in **Ruby scripts** packed inside `Data/Scripts.rvdata2`.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,18 @@ Workflow jumps here when you start a phase.
|
|||
|
||||
**Example:** check only `Map003.json`, module = RPG Maker MV/MZ, click Translate, then export from Workflow Step 5 (or copy from `translated/` yourself).
|
||||
|
||||
## Images
|
||||
|
||||
MV/MZ bitmap UI workflow. Workflow Step 6 checks the project setup and opens this page.
|
||||
|
||||
1. Decrypt selected images (or all images) into `.dazedtl/images/.../img/`.
|
||||
2. Click **Copy skill** and paste the scoped bitmap-localization instructions into your coding
|
||||
agent.
|
||||
3. Review the edited PNGs here.
|
||||
4. Patch highlighted images, or clear highlights to patch every editable PNG.
|
||||
|
||||
The runtime game images remain untouched until you deliberately patch the reviewed copies.
|
||||
|
||||
## Batches
|
||||
|
||||
History for Anthropic Message Batch jobs.
|
||||
|
|
@ -32,6 +44,7 @@ Edit tool-level markdown:
|
|||
- `data/skills/wrap_config.md` - RPG Maker wrap-width analysis prompt
|
||||
- `data/skills/plugin_translation.md` - MV/MZ plugin translation prompt
|
||||
- `data/skills/ace_script_translation.md` - VX Ace Ruby script translation prompt
|
||||
- `data/skills/image_translation.md` - scoped bitmap UI translation skill copied from Images
|
||||
- `data/skills/risky_codes.md` - RPG Maker optional event-code audit prompt
|
||||
- `data/skills/wolf_speakers.md` - WOLF speaker-format audit prompt
|
||||
- `data/translation_contexts.json` - per-call history templates
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ When the sample map looks good:
|
|||
|
||||
1. Step 4 - copy the Plugin / risky-codes prompt, audit the game in your IDE, enable only codes that hold player-visible text, then run Phase 2 carefully.
|
||||
2. Step 5 - for MV/MZ, copy vocab + the plugins.js prompt and edit `plugins.js` in the IDE (player-facing strings only). For **Ace**, edit `ace_json/scripts/*.rb` the same way, then Export so **RV2JSON** packs `Scripts.rvdata2`.
|
||||
3. Step 6 (MV/MZ) - install TL Inspector / Forge if you want in-game inspection helpers.
|
||||
3. Step 6 (MV/MZ) - check image setup, then decrypt / translate / patch any bitmap UI text.
|
||||
4. Step 7 (MV/MZ) - install TL Inspector / Forge if you want in-game inspection helpers.
|
||||
|
||||
### What “done enough” looks like
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtCore import (
|
||||
QPoint,
|
||||
Qt,
|
||||
QSize,
|
||||
QThread,
|
||||
|
|
@ -23,6 +24,7 @@ from PyQt5.QtWidgets import (
|
|||
QLineEdit,
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
QListView,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
|
|
@ -55,6 +57,87 @@ _ASSET_ID_ROLE = Qt.UserRole + 1
|
|||
_PAGE_SIZE = 1000
|
||||
|
||||
|
||||
class _BoundedComboBox(QComboBox):
|
||||
"""Keep large combo popups on-screen and scrollable."""
|
||||
|
||||
_MAX_VISIBLE_ROWS = 14
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
view = QListView(self)
|
||||
view.setVerticalScrollMode(QAbstractItemView.ScrollPerItem)
|
||||
view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
view.setTextElideMode(Qt.ElideMiddle)
|
||||
view.setUniformItemSizes(True)
|
||||
view.setStyleSheet(
|
||||
"QListView {"
|
||||
" background: #404040; color: #f2f2f2;"
|
||||
" border: 1px solid #5a5a5a; outline: none; padding: 3px;"
|
||||
"}"
|
||||
"QListView::item { min-height: 30px; padding: 3px 8px; }"
|
||||
"QListView::item:selected { background: #087dcc; color: white; }"
|
||||
"QScrollBar:vertical { background: #303030; width: 12px; margin: 0; }"
|
||||
"QScrollBar::handle:vertical {"
|
||||
" background: #777; min-height: 28px; border-radius: 5px;"
|
||||
"}"
|
||||
"QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {"
|
||||
" height: 0;"
|
||||
"}"
|
||||
)
|
||||
self.setView(view)
|
||||
self.setMaxVisibleItems(self._MAX_VISIBLE_ROWS)
|
||||
|
||||
def showPopup(self) -> None:
|
||||
self.view().setVerticalScrollBarPolicy(
|
||||
Qt.ScrollBarAlwaysOn
|
||||
if self.count() > self._MAX_VISIBLE_ROWS
|
||||
else Qt.ScrollBarAsNeeded
|
||||
)
|
||||
super().showPopup()
|
||||
view = self.view()
|
||||
popup = view.window()
|
||||
screen = QApplication.screenAt(self.mapToGlobal(self.rect().center()))
|
||||
screen = screen or QApplication.primaryScreen()
|
||||
if screen is None:
|
||||
return
|
||||
|
||||
available = screen.availableGeometry()
|
||||
row_height = view.sizeHintForRow(0)
|
||||
if row_height <= 0:
|
||||
row_height = self.fontMetrics().height() + 10
|
||||
visible_rows = min(max(1, self.count()), self._MAX_VISIBLE_ROWS)
|
||||
height_cap = min(
|
||||
row_height * visible_rows + view.frameWidth() * 2 + 8,
|
||||
max(row_height * 4, int(available.height() * 0.6)),
|
||||
)
|
||||
popup_height = min(popup.height(), height_cap)
|
||||
popup_width = min(max(popup.width(), self.width()), available.width())
|
||||
|
||||
combo_top_left = self.mapToGlobal(QPoint(0, 0))
|
||||
below_y = combo_top_left.y() + self.height()
|
||||
above_y = combo_top_left.y() - popup_height
|
||||
if below_y + popup_height <= available.bottom() + 1:
|
||||
popup_y = below_y
|
||||
elif above_y >= available.top():
|
||||
popup_y = above_y
|
||||
else:
|
||||
popup_y = max(
|
||||
available.top(),
|
||||
min(popup.y(), available.bottom() - popup_height + 1),
|
||||
)
|
||||
popup_x = max(
|
||||
available.left(),
|
||||
min(combo_top_left.x(), available.right() - popup_width + 1),
|
||||
)
|
||||
popup.setGeometry(popup_x, popup_y, popup_width, popup_height)
|
||||
if self.currentIndex() >= 0:
|
||||
view.scrollTo(
|
||||
self.model().index(self.currentIndex(), self.modelColumn()),
|
||||
QAbstractItemView.PositionAtCenter,
|
||||
)
|
||||
|
||||
|
||||
class _UserSelectionList(QListWidget):
|
||||
"""Report user selection changes without reacting to list rebuilding."""
|
||||
|
||||
|
|
@ -228,7 +311,7 @@ class RPGMakerImageManager(QWidget):
|
|||
self.search_edit.setPlaceholderText("Filter by any part of the folder or filename…")
|
||||
self.search_edit.textChanged.connect(self._apply_filters)
|
||||
filters.addWidget(self.search_edit, 2)
|
||||
self.folder_combo = QComboBox()
|
||||
self.folder_combo = _BoundedComboBox()
|
||||
self.folder_combo.addItem("All folders", "")
|
||||
self.folder_combo.currentIndexChanged.connect(self._apply_filters)
|
||||
filters.addWidget(self.folder_combo, 1)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ Provides a guided, step-by-step interface:
|
|||
Step 3 – Translation: Phase 0 (DB), Phase 1 (dialogue), Phase 1b (111 cache)
|
||||
Step 4 – Translation Phase 2 (risky codes)
|
||||
Step 5 – Plugins.js prompt helpers + export translated/ to the game
|
||||
Step 6 – Install TL Inspector and/or Forge playtest plugins
|
||||
Step 6 – Prepare and translate editable bitmap UI images
|
||||
Step 7 – Install TL Inspector and/or Forge playtest plugins
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -497,7 +498,19 @@ _STEP_HELP: dict[int, str] = {
|
|||
"• <b>Export ALL</b> - everything under <code>translated/</code>"
|
||||
),
|
||||
6: (
|
||||
"<b>Step 6 - Playtest</b><br><br>"
|
||||
"<b>Step 6 - Images (MV/MZ)</b><br><br>"
|
||||
"Use the existing <b>Images</b> page for bitmap UI translation:<br>"
|
||||
"• Confirm this step reports the correct game root, image tree, encryption key, and "
|
||||
"<code>vocab.txt</code><br>"
|
||||
"• Open Images and decrypt the images you want to edit<br>"
|
||||
"• Click <b>Copy skill</b>, paste it into Codex/Cursor/Copilot, and let the agent edit "
|
||||
"only the generated <code>.dazedtl/images/.../img</code> copies<br>"
|
||||
"• Review the results, then use <b>Patch selected</b> or <b>Patch all</b><br><br>"
|
||||
"Editable PNGs must preserve the same <code>img/...</code> hierarchy as the game. "
|
||||
"This step warns when PNGs were placed elsewhere in the workspace."
|
||||
),
|
||||
7: (
|
||||
"<b>Step 7 - Playtest</b><br><br>"
|
||||
"Install playtest plugins into the MV/MZ game:<br>"
|
||||
"• <b>TL Inspector</b> - inspect translated text in-game<br>"
|
||||
"• <b>Forge</b> - additional playtest helpers<br><br>"
|
||||
|
|
@ -634,6 +647,67 @@ def _make_icon_btn(icon_text: str, tooltip: str = "") -> QPushButton:
|
|||
return btn
|
||||
|
||||
|
||||
def _inspect_image_workflow(game_root: str | Path) -> dict:
|
||||
"""Return lightweight MV/MZ image-workflow readiness details."""
|
||||
root = Path(game_root).expanduser().resolve()
|
||||
report = {
|
||||
"root": root,
|
||||
"ok": False,
|
||||
"error": "",
|
||||
"runtime": 0,
|
||||
"encrypted": 0,
|
||||
"editable": 0,
|
||||
"misplaced": 0,
|
||||
"vocab": root / "vocab.txt",
|
||||
"editable_root": None,
|
||||
"key_ok": None,
|
||||
}
|
||||
try:
|
||||
from util.rpgmaker_images import (
|
||||
editable_workspace_root,
|
||||
read_encryption_key,
|
||||
resolve_content_root,
|
||||
scan_image_assets,
|
||||
)
|
||||
|
||||
content_root = resolve_content_root(root)
|
||||
workspace = editable_workspace_root(root)
|
||||
expected_root = workspace / content_root.relative_to(root) / "img"
|
||||
assets = scan_image_assets(root)
|
||||
encrypted = sum(asset.has_encrypted for asset in assets)
|
||||
report.update(
|
||||
{
|
||||
"ok": True,
|
||||
"runtime": sum(
|
||||
asset.has_encrypted or asset.has_runtime_plain for asset in assets
|
||||
),
|
||||
"encrypted": encrypted,
|
||||
"editable": sum(asset.has_plain for asset in assets),
|
||||
"editable_root": expected_root,
|
||||
}
|
||||
)
|
||||
if encrypted:
|
||||
try:
|
||||
read_encryption_key(root)
|
||||
report["key_ok"] = True
|
||||
except Exception:
|
||||
report["key_ok"] = False
|
||||
|
||||
if workspace.is_dir():
|
||||
misplaced = 0
|
||||
for path in workspace.rglob("*"):
|
||||
if not path.is_file() or path.suffix.casefold() != ".png":
|
||||
continue
|
||||
try:
|
||||
path.relative_to(expected_root)
|
||||
except ValueError:
|
||||
misplaced += 1
|
||||
report["misplaced"] = misplaced
|
||||
except Exception as exc:
|
||||
report["error"] = str(exc)
|
||||
return report
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Helpers
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -752,7 +826,8 @@ class WorkflowTab(QWidget):
|
|||
("3 TL Phase 1", self._build_step4_translation),
|
||||
("4 TL Phase 2", self._build_step5_tl_phase2),
|
||||
("5 Export", self._build_step5_finish),
|
||||
("6 Playtest", self._build_step8_playtest),
|
||||
("6 Images", self._build_step6_images),
|
||||
("7 Playtest", self._build_step8_playtest),
|
||||
]
|
||||
self._step_labels = [label for label, _ in _tab_defs]
|
||||
|
||||
|
|
@ -924,6 +999,7 @@ class WorkflowTab(QWidget):
|
|||
"Phase1",
|
||||
"Phase2",
|
||||
"Export",
|
||||
"Images",
|
||||
"Playtest",
|
||||
)
|
||||
name = short_names[idx] if 0 <= idx < len(short_names) else str(idx)
|
||||
|
|
@ -988,6 +1064,8 @@ class WorkflowTab(QWidget):
|
|||
if index == 4:
|
||||
self._populate_p2_checkboxes()
|
||||
if index == 6:
|
||||
self._refresh_image_workflow_status()
|
||||
if index == 7:
|
||||
self._refresh_playtest_status()
|
||||
self._load_playtest_settings()
|
||||
|
||||
|
|
@ -2083,7 +2161,7 @@ class WorkflowTab(QWidget):
|
|||
)
|
||||
self._step6_vocab_btn.clicked.connect(self._copy_vocab_to_game)
|
||||
|
||||
self._step6_copy_btn = _make_btn("📋 Copy Prompt for Copilot", "#555")
|
||||
self._step6_copy_btn = _make_btn("TL Plugins Skill", "#555")
|
||||
self._step6_copy_btn.setToolTip(
|
||||
"Copy a prompt that audits plugins.js and enabled plugin sources, asks what "
|
||||
"needs translation, then edits approved player-visible strings in place."
|
||||
|
|
@ -2115,11 +2193,93 @@ class WorkflowTab(QWidget):
|
|||
inner.addLayout(_labeled_row(export_lbl, export_active_btn, export_all_btn))
|
||||
layout.addWidget(box, 0, Qt.AlignLeft)
|
||||
|
||||
# ── Step 6: Playtest (TL Inspector) ─────────────────────────────────────
|
||||
# ── Step 6: Editable images ─────────────────────────────────────────────
|
||||
|
||||
def _build_step6_images(self, layout: QVBoxLayout):
|
||||
self._add_step_header(layout, "Step 6 — Images", 6)
|
||||
|
||||
intro = QLabel(
|
||||
"Translate text embedded in RPG Maker MV/MZ images without moving the Image Manager. "
|
||||
"This step verifies the project setup, then sends you to the Images page."
|
||||
)
|
||||
intro.setWordWrap(True)
|
||||
intro.setStyleSheet("color:#b8b8b8;font-size:13px;padding-bottom:4px;")
|
||||
layout.addWidget(intro)
|
||||
|
||||
status_box = QWidget()
|
||||
status_box.setObjectName("tbox")
|
||||
status_box.setStyleSheet(self._task_box_style())
|
||||
status_layout = QVBoxLayout(status_box)
|
||||
status_layout.setContentsMargins(14, 12, 14, 12)
|
||||
status_layout.setSpacing(8)
|
||||
|
||||
status_title = QLabel("Setup check")
|
||||
status_title.setStyleSheet("color:#4ec9b0;font-size:12px;font-weight:bold;")
|
||||
status_layout.addWidget(status_title)
|
||||
|
||||
self._image_workflow_status = QLabel("Open this step to check image readiness.")
|
||||
self._image_workflow_status.setWordWrap(True)
|
||||
self._image_workflow_status.setTextFormat(Qt.RichText)
|
||||
self._image_workflow_status.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||
self._image_workflow_status.setStyleSheet(
|
||||
"color:#c8c8c8;font-size:12px;line-height:1.4;"
|
||||
)
|
||||
status_layout.addWidget(self._image_workflow_status)
|
||||
|
||||
refresh_btn = _make_btn("↻ Refresh check", "#555")
|
||||
refresh_btn.clicked.connect(self._refresh_image_workflow_status)
|
||||
status_layout.addWidget(refresh_btn, 0, Qt.AlignLeft)
|
||||
layout.addWidget(status_box)
|
||||
|
||||
flow_box = QWidget()
|
||||
flow_box.setObjectName("tbox")
|
||||
flow_box.setStyleSheet(self._task_box_style())
|
||||
flow_layout = QVBoxLayout(flow_box)
|
||||
flow_layout.setContentsMargins(14, 12, 14, 12)
|
||||
flow_layout.setSpacing(8)
|
||||
|
||||
flow_title = QLabel("Image translation flow")
|
||||
flow_title.setStyleSheet("color:#4ec9b0;font-size:12px;font-weight:bold;")
|
||||
flow_layout.addWidget(flow_title)
|
||||
|
||||
flow_text = QLabel(
|
||||
"1. Open Images and decrypt the source images into editable PNG copies.<br>"
|
||||
"2. Click <b>Copy skill</b> in the Image Manager and paste it into your coding "
|
||||
"agent.<br>"
|
||||
"3. Review the translated PNGs in the Image Manager.<br>"
|
||||
"4. Highlight finished images and click <b>Patch selected</b>, or clear highlights "
|
||||
"and click <b>Patch all</b>."
|
||||
)
|
||||
flow_text.setWordWrap(True)
|
||||
flow_text.setTextFormat(Qt.RichText)
|
||||
flow_text.setStyleSheet("color:#c8c8c8;font-size:12px;")
|
||||
flow_layout.addWidget(flow_text)
|
||||
|
||||
actions = QHBoxLayout()
|
||||
actions.setSpacing(8)
|
||||
copy_vocab_btn = _make_btn("📄 Copy vocab.txt → Game", "#555")
|
||||
copy_vocab_btn.setToolTip(
|
||||
"Copy the current glossary to <game root>/vocab.txt for the image translation skill."
|
||||
)
|
||||
copy_vocab_btn.clicked.connect(self._copy_vocab_to_game)
|
||||
actions.addWidget(copy_vocab_btn)
|
||||
|
||||
self._open_images_btn = _make_btn("🖼 Open Images", "#3a5a7a")
|
||||
self._open_images_btn.setToolTip(
|
||||
"Open the existing Image Manager using the Step 0 game folder."
|
||||
)
|
||||
self._open_images_btn.clicked.connect(self._open_image_manager)
|
||||
actions.addWidget(self._open_images_btn)
|
||||
actions.addStretch()
|
||||
flow_layout.addLayout(actions)
|
||||
layout.addWidget(flow_box)
|
||||
layout.addStretch()
|
||||
|
||||
# ── Step 7: Playtest (TL Inspector) ─────────────────────────────────────
|
||||
|
||||
def _build_step8_playtest(self, layout: QVBoxLayout):
|
||||
self._step8_section_label = self._add_step_header(
|
||||
layout, "Step 6 — Playtest Tools", 6
|
||||
layout, "Step 7 — Playtest Tools", 7
|
||||
)
|
||||
|
||||
settings_box = QWidget()
|
||||
|
|
@ -2354,6 +2514,117 @@ class WorkflowTab(QWidget):
|
|||
self._populate_tli_editor_combo()
|
||||
self._load_playtest_settings()
|
||||
|
||||
def _refresh_image_workflow_status(self):
|
||||
"""Check that Step 0 points at an MV/MZ root ready for image work."""
|
||||
label = getattr(self, "_image_workflow_status", None)
|
||||
button = getattr(self, "_open_images_btn", None)
|
||||
if label is None:
|
||||
return
|
||||
if self._ace_encrypted or self._ace_rvdata_dir or self._ace_json_dir:
|
||||
label.setText(
|
||||
"<span style='color:#e9a12a'>⚠ Image Manager supports RPG Maker MV/MZ "
|
||||
"projects only; this project is RPG Maker Ace.</span>"
|
||||
)
|
||||
if button is not None:
|
||||
button.setEnabled(False)
|
||||
return
|
||||
|
||||
game_root = self.folder_edit.text().strip()
|
||||
if not game_root:
|
||||
label.setText(
|
||||
"<span style='color:#e9a12a'>⚠ Complete Step 0 and select the actual game "
|
||||
"root first.</span>"
|
||||
)
|
||||
if button is not None:
|
||||
button.setEnabled(False)
|
||||
return
|
||||
|
||||
import html
|
||||
|
||||
report = _inspect_image_workflow(game_root)
|
||||
if not report["ok"]:
|
||||
label.setText(
|
||||
"<span style='color:#f48771'>✗ The Step 0 folder is not ready for image "
|
||||
f"management: {html.escape(report['error'])}</span><br>"
|
||||
"Select the folder that directly contains <code>img/</code>, or contains "
|
||||
"<code>www/img/</code>."
|
||||
)
|
||||
if button is not None:
|
||||
button.setEnabled(False)
|
||||
return
|
||||
|
||||
root = html.escape(str(report["root"]))
|
||||
editable_root = html.escape(str(report["editable_root"]))
|
||||
vocab = html.escape(str(report["vocab"]))
|
||||
lines = [
|
||||
f"<span style='color:#8fbc8f'>✓ Project root:</span> <code>{root}</code>",
|
||||
f"<span style='color:#8fbc8f'>✓ Runtime images:</span> "
|
||||
f"{report['runtime']:,} ({report['encrypted']:,} encrypted)",
|
||||
]
|
||||
if report["key_ok"] is False:
|
||||
lines.append(
|
||||
"<span style='color:#f48771'>✗ Encryption key:</span> missing or invalid in "
|
||||
"<code>System.json</code>; encrypted images cannot be decrypted."
|
||||
)
|
||||
elif report["key_ok"] is True:
|
||||
lines.append("<span style='color:#8fbc8f'>✓ Encryption key:</span> ready")
|
||||
else:
|
||||
lines.append("<span style='color:#8fbc8f'>✓ Encryption key:</span> not required")
|
||||
|
||||
if Path(report["vocab"]).is_file():
|
||||
lines.append(
|
||||
f"<span style='color:#8fbc8f'>✓ Glossary:</span> <code>{vocab}</code>"
|
||||
)
|
||||
else:
|
||||
lines.append(
|
||||
"<span style='color:#e9a12a'>⚠ Glossary:</span> "
|
||||
f"<code>{vocab}</code> is missing. Copy it before using the AI skill."
|
||||
)
|
||||
|
||||
if report["editable"]:
|
||||
lines.append(
|
||||
f"<span style='color:#8fbc8f'>✓ Editable PNGs:</span> "
|
||||
f"{report['editable']:,} under <code>{editable_root}</code>"
|
||||
)
|
||||
else:
|
||||
lines.append(
|
||||
"<span style='color:#9d9d9d'>• Editable PNGs:</span> none yet. Open Images "
|
||||
"and decrypt the images you want to translate."
|
||||
)
|
||||
|
||||
if report["misplaced"]:
|
||||
lines.append(
|
||||
f"<span style='color:#e9a12a'>⚠ Workspace layout:</span> "
|
||||
f"{report['misplaced']:,} PNG(s) are outside <code>{editable_root}</code> and "
|
||||
"will not appear in the Image Manager."
|
||||
)
|
||||
else:
|
||||
lines.append(
|
||||
"<span style='color:#8fbc8f'>✓ Workspace layout:</span> editable PNGs use "
|
||||
"the expected game-relative hierarchy."
|
||||
)
|
||||
label.setText("<br>".join(lines))
|
||||
if button is not None:
|
||||
button.setEnabled(True)
|
||||
|
||||
def _open_image_manager(self):
|
||||
"""Open the Images page with the current Step 0 project root."""
|
||||
game_root = self.folder_edit.text().strip()
|
||||
report = _inspect_image_workflow(game_root) if game_root else {"ok": False}
|
||||
if not report.get("ok"):
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Images Setup",
|
||||
"Select a valid RPG Maker MV/MZ game root in Step 0 first.",
|
||||
)
|
||||
return
|
||||
self._save_setting("last_game_folder", str(report["root"]))
|
||||
parent = self.parent_window
|
||||
if hasattr(parent, "switch_page"):
|
||||
parent.switch_page(getattr(parent, "PAGE_IMAGES", 2))
|
||||
return
|
||||
self._log("⚠ Could not open the Images page from this window.")
|
||||
|
||||
def _populate_tli_editor_combo(self, select: str | None = None):
|
||||
"""Fill editor dropdown with auto-detect, found editors, and custom."""
|
||||
from util.tl_inspector.config import detect_editors
|
||||
|
|
@ -2811,28 +3082,29 @@ class WorkflowTab(QWidget):
|
|||
"Copy a prompt that audits plugins.js and enabled plugin sources, asks what "
|
||||
"needs translation, then edits approved player-visible strings in place."
|
||||
)
|
||||
# Step 6 - TL Inspector (MV/MZ only; hidden for Ace)
|
||||
show_playtest = not is_ace
|
||||
playtest_idx = 6
|
||||
if hasattr(self, "_step_tabs") and self._step_tabs.count() > playtest_idx:
|
||||
if hasattr(self._step_tabs, "setTabVisible"):
|
||||
self._step_tabs.setTabVisible(playtest_idx, show_playtest)
|
||||
else:
|
||||
self._step_tabs.setTabEnabled(playtest_idx, show_playtest)
|
||||
if is_ace and self._step_tabs.currentIndex() == playtest_idx:
|
||||
self._goto_step(5)
|
||||
if hasattr(self, "_step_buttons") and len(self._step_buttons) > playtest_idx:
|
||||
self._step_buttons[playtest_idx].setVisible(show_playtest)
|
||||
self._step_buttons[playtest_idx].setEnabled(show_playtest)
|
||||
# Steps 6–7 - Image Manager and playtest tools are MV/MZ only.
|
||||
show_mvmz_tools = not is_ace
|
||||
tool_indices = (6, 7)
|
||||
for tool_idx in tool_indices:
|
||||
if hasattr(self, "_step_tabs") and self._step_tabs.count() > tool_idx:
|
||||
if hasattr(self._step_tabs, "setTabVisible"):
|
||||
self._step_tabs.setTabVisible(tool_idx, show_mvmz_tools)
|
||||
else:
|
||||
self._step_tabs.setTabEnabled(tool_idx, show_mvmz_tools)
|
||||
if hasattr(self, "_step_buttons") and len(self._step_buttons) > tool_idx:
|
||||
self._step_buttons[tool_idx].setVisible(show_mvmz_tools)
|
||||
self._step_buttons[tool_idx].setEnabled(show_mvmz_tools)
|
||||
if is_ace and self._step_tabs.currentIndex() in tool_indices:
|
||||
self._goto_step(5)
|
||||
self._refresh_step_strip()
|
||||
box = getattr(self, "_step8_playtest_box", None)
|
||||
install_both_btn = getattr(self, "_install_both_btn", None)
|
||||
if box is not None:
|
||||
box.setVisible(not is_ace)
|
||||
box.setEnabled(not is_ace)
|
||||
box.setVisible(show_mvmz_tools)
|
||||
box.setEnabled(show_mvmz_tools)
|
||||
if install_both_btn is not None:
|
||||
install_both_btn.setVisible(not is_ace)
|
||||
if not is_ace:
|
||||
install_both_btn.setVisible(show_mvmz_tools)
|
||||
if show_mvmz_tools:
|
||||
self._refresh_playtest_status()
|
||||
|
||||
def _detect_folder(self):
|
||||
|
|
@ -3396,7 +3668,7 @@ class WorkflowTab(QWidget):
|
|||
|
||||
src = VOCAB_PATH
|
||||
if not src.exists():
|
||||
self._log("⚠ vocab.txt not found — save it in Step 3 first.")
|
||||
self._log("⚠ vocab.txt not found — save it in Step 2 first.")
|
||||
return
|
||||
|
||||
import shutil
|
||||
|
|
@ -3404,6 +3676,7 @@ class WorkflowTab(QWidget):
|
|||
try:
|
||||
shutil.copy2(src, dst)
|
||||
self._log(f"✅ vocab.txt copied to {dst}")
|
||||
self._refresh_image_workflow_status()
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not copy vocab.txt: {exc}")
|
||||
|
||||
|
|
|
|||
|
|
@ -163,14 +163,14 @@ CODE122_VAR_MAX = 2000
|
|||
|
||||
# Plugins / Scripts
|
||||
CODE355655 = False
|
||||
CODE357 = True
|
||||
CODE357 = False
|
||||
CODE657 = False
|
||||
CODE356 = False
|
||||
CODE320 = False
|
||||
CODE324 = False
|
||||
CODE325 = False
|
||||
CODE111 = False
|
||||
CODE108 = True
|
||||
CODE108 = False
|
||||
|
||||
# ─── Plugin Manager ──────────────────────────────────────────────────────────
|
||||
# All known code-357 headerMapping entries. Enable entries via ENABLED_PLUGINS_357.
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|||
from PIL import Image
|
||||
from PyQt5.QtCore import QSettings, Qt
|
||||
from PyQt5.QtTest import QTest
|
||||
from PyQt5.QtWidgets import QApplication, QAbstractItemView, QMessageBox
|
||||
from PyQt5.QtWidgets import QApplication, QAbstractItemView, QMainWindow, QMessageBox
|
||||
|
||||
from gui.rpgmaker_image_manager import RPGMakerImageManager, _PAGE_SIZE
|
||||
from gui.workflow_tab import _inspect_image_workflow
|
||||
|
||||
|
||||
class RPGMakerImageManagerSelectionTests(unittest.TestCase):
|
||||
|
|
@ -251,6 +252,37 @@ class RPGMakerImageManagerSelectionTests(unittest.TestCase):
|
|||
self.manager.open_workspace_button.geometry().top(),
|
||||
)
|
||||
|
||||
def test_folder_popup_is_bounded_and_scrolls_long_lists(self):
|
||||
combo = self.manager.folder_combo
|
||||
combo.blockSignals(True)
|
||||
combo.clear()
|
||||
combo.addItem("All folders", "")
|
||||
for index in range(80):
|
||||
combo.addItem(f"img/pictures/group_{index:03d}/nested_folder", str(index))
|
||||
combo.setCurrentIndex(combo.count() - 1)
|
||||
combo.blockSignals(False)
|
||||
|
||||
combo.showPopup()
|
||||
self.app.processEvents()
|
||||
try:
|
||||
view = combo.view()
|
||||
popup = view.window()
|
||||
screen = QApplication.screenAt(combo.mapToGlobal(combo.rect().center()))
|
||||
screen = screen or QApplication.primaryScreen()
|
||||
available = screen.availableGeometry()
|
||||
|
||||
self.assertLessEqual(popup.height(), int(available.height() * 0.6))
|
||||
self.assertGreater(view.verticalScrollBar().maximum(), 0)
|
||||
self.assertTrue(view.verticalScrollBar().isVisible())
|
||||
self.assertGreaterEqual(popup.geometry().top(), available.top())
|
||||
self.assertLessEqual(popup.geometry().bottom(), available.bottom())
|
||||
current = combo.model().index(combo.currentIndex(), combo.modelColumn())
|
||||
current_rect = view.visualRect(current)
|
||||
self.assertLess(current_rect.top(), view.viewport().height())
|
||||
self.assertGreaterEqual(current_rect.bottom(), 0)
|
||||
finally:
|
||||
combo.hidePopup()
|
||||
|
||||
def test_translation_skill_is_enabled_only_for_editable_images(self):
|
||||
self.assertFalse(self.manager.copy_translation_button.isEnabled())
|
||||
|
||||
|
|
@ -292,6 +324,23 @@ class RPGMakerImageManagerSelectionTests(unittest.TestCase):
|
|||
self.manager.status_label.text(),
|
||||
)
|
||||
|
||||
def test_workflow_readiness_detects_editable_and_misplaced_pngs(self):
|
||||
asset = self.manager.assets[0]
|
||||
asset.plain_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
asset.plain_path.write_bytes(asset.runtime_plain_path.read_bytes())
|
||||
self.game_root.joinpath("vocab.txt").write_text("Menu (Menu)\n", encoding="utf-8")
|
||||
misplaced = self.game_root / ".dazedtl" / "images" / "img (2)" / "pictures"
|
||||
misplaced.mkdir(parents=True)
|
||||
misplaced.joinpath("menu.png").write_bytes(asset.runtime_plain_path.read_bytes())
|
||||
|
||||
report = _inspect_image_workflow(self.game_root)
|
||||
|
||||
self.assertTrue(report["ok"])
|
||||
self.assertEqual(report["runtime"], 4)
|
||||
self.assertEqual(report["editable"], 1)
|
||||
self.assertEqual(report["misplaced"], 1)
|
||||
self.assertTrue(report["vocab"].is_file())
|
||||
|
||||
def test_open_folder_uses_highlighted_images_editable_parent(self):
|
||||
self._click(0)
|
||||
asset_id = self.manager.image_list.currentItem().data(Qt.UserRole + 1)
|
||||
|
|
@ -350,7 +399,63 @@ class RPGMakerImageManagerNavigationTests(unittest.TestCase):
|
|||
self.assertIn("PAGE_IMAGES = 2", main_source)
|
||||
self.assertIn("self.image_manager_tab = RPGMakerImageManager", main_source)
|
||||
self.assertIn('create_nav_button("🖼", "Images")', main_source)
|
||||
self.assertNotIn("Open Image Manager", workflow_source)
|
||||
self.assertIn('(\"6 Images\", self._build_step6_images)', workflow_source)
|
||||
self.assertIn('self._open_images_btn = _make_btn("🖼 Open Images"', workflow_source)
|
||||
|
||||
|
||||
class RPGMakerWorkflowImageStepTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.app = QApplication.instance() or QApplication([])
|
||||
|
||||
def test_images_step_checks_setup_and_opens_existing_manager(self):
|
||||
from gui.workflow_tab import WorkflowTab
|
||||
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
root = Path(raw)
|
||||
game_root = root / "Game"
|
||||
image_root = game_root / "img" / "pictures"
|
||||
data_root = game_root / "data"
|
||||
image_root.mkdir(parents=True)
|
||||
data_root.mkdir(parents=True)
|
||||
Image.new("RGBA", (12, 12), "purple").save(image_root / "menu.png")
|
||||
data_root.joinpath("System.json").write_text("{}", encoding="utf-8")
|
||||
game_root.joinpath("vocab.txt").write_text("Menu (Menu)\n", encoding="utf-8")
|
||||
|
||||
settings = QSettings(str(root / "settings.ini"), QSettings.IniFormat)
|
||||
|
||||
class Host(QMainWindow):
|
||||
PAGE_IMAGES = 2
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.switched_to = None
|
||||
|
||||
def switch_page(self, index):
|
||||
self.switched_to = index
|
||||
|
||||
host = Host()
|
||||
with patch("gui.workflow_tab.QSettings", return_value=settings):
|
||||
workflow = WorkflowTab(host)
|
||||
try:
|
||||
workflow.folder_edit.setText(str(game_root))
|
||||
workflow._goto_step(6)
|
||||
workflow._refresh_image_workflow_status()
|
||||
|
||||
self.assertEqual(workflow._step_tabs.count(), 8)
|
||||
self.assertEqual(workflow._step_tabs.currentIndex(), 6)
|
||||
self.assertIn("Runtime images:</span> 1", workflow._image_workflow_status.text())
|
||||
self.assertIn("Glossary:</span>", workflow._image_workflow_status.text())
|
||||
|
||||
workflow._open_image_manager()
|
||||
|
||||
self.assertEqual(host.switched_to, host.PAGE_IMAGES)
|
||||
self.assertEqual(
|
||||
settings.value("workflow/last_game_folder"), str(game_root.resolve())
|
||||
)
|
||||
finally:
|
||||
workflow.close()
|
||||
host.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Reference in a new issue