Forge
This commit is contained in:
parent
9ff0a0c2c7
commit
9f206eb39d
14 changed files with 1873 additions and 156 deletions
|
|
@ -83,3 +83,6 @@ frequency_penalty= 0.2
|
|||
# TL Inspector (Step 8 playtest) — editor opened when clicking a source line in-game
|
||||
# tlEditorCmd: 'auto' scans for VS Code / Cursor, or set an absolute path to any editor exe
|
||||
tlEditorCmd='auto'
|
||||
# Hotkeys (event.key names, e.g. F9, F10, Control)
|
||||
tlHotkey='F9'
|
||||
forgeHotkey='F10'
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -21,6 +21,7 @@ __pycache__
|
|||
!vocab_base.txt
|
||||
!requirements.txt
|
||||
!util/tl_inspector/TLInspector.js
|
||||
!util/forge/Forge_MZ.js
|
||||
util/ace/*.exe
|
||||
util/ace/.tools_version.json
|
||||
!util/ace/offline/*.exe
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ An AI-powered game translation tool with a GUI. Translate RPG Maker, Ren'Py, Tyr
|
|||
|
||||
- **[Sinflower](https://github.com/Sinflower)** — [RV2JSON](https://github.com/Sinflower/RV2JSON) — enables RPGMaker Ace games to be translated the same way as MV/MZ by converting rvdata2 files to JSON and back. A copy is bundled offline in `util/ace/offline/`; newer builds are downloaded when online.
|
||||
- **Sakura & Kao_SSS** — TL Inspector (`util/tl_inspector/`) — in-game translation source inspector and live-edit plugin for RPG Maker MV/MZ playtesting.
|
||||
- **Len** — Forge (`util/forge/`) — MZ playtest cheat & editor overlay (install alongside TL Inspector).
|
||||
- **Len** — batch translation mode — Anthropic Message Batches API integration (collect/consume pipeline, cost estimation, resume, and GUI/workflow support).
|
||||
|
||||
## Table of Contents
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Provides a guided, step-by-step interface:
|
|||
Step 5 – Translation Phase 2 (risky codes)
|
||||
Step 6 – Translate visible strings in js/plugins.js (or Ace scripts)
|
||||
Step 7 – Export translated/ back to the game folder
|
||||
Step 8 – Install TL Inspector for playtesting and live in-game edits (MV/MZ)
|
||||
Step 8 – Install TL Inspector and/or Forge playtest plugins
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -729,8 +729,8 @@ class WorkflowTab(QWidget):
|
|||
if index == 5:
|
||||
self._populate_p2_checkboxes()
|
||||
if index == 8:
|
||||
self._refresh_tl_inspector_status()
|
||||
self._load_tli_editor_settings()
|
||||
self._refresh_playtest_status()
|
||||
self._load_playtest_settings()
|
||||
|
||||
def _register_import_button(self, button: QPushButton) -> None:
|
||||
self._import_buttons.append(button)
|
||||
|
|
@ -2491,131 +2491,212 @@ class WorkflowTab(QWidget):
|
|||
# ── Step 8: Playtest (TL Inspector) ─────────────────────────────────────
|
||||
|
||||
def _build_step8_playtest(self, layout: QVBoxLayout):
|
||||
self._step8_section_label = _make_section_label("Step 8 — Playtest with TL Inspector")
|
||||
self._step8_section_label = _make_section_label("Step 8 — Playtest Tools")
|
||||
layout.addWidget(self._step8_section_label)
|
||||
|
||||
hint = QLabel(
|
||||
"Install <b>TL Inspector</b> into your RPG Maker MV/MZ game for playtesting. "
|
||||
"Press <b>F10</b> in-game to see which source file each line of text comes from, "
|
||||
"open it in VSCode, or <b>edit the text live</b> and save directly to the JSON file."
|
||||
self._step8_main_hint = QLabel(
|
||||
"Install playtest plugins into your RPG Maker game. "
|
||||
"<b>TL Inspector</b> works on MV and MZ. "
|
||||
"<b>Forge</b> is MZ-only. Use <b>Install Both</b> on MZ to add both plugins at once."
|
||||
)
|
||||
hint.setWordWrap(True)
|
||||
hint.setTextFormat(Qt.RichText)
|
||||
hint.setStyleSheet("color:#9d9d9d;font-size:13px;padding-bottom:4px;")
|
||||
layout.addWidget(hint)
|
||||
self._step8_main_hint.setWordWrap(True)
|
||||
self._step8_main_hint.setTextFormat(Qt.RichText)
|
||||
self._step8_main_hint.setStyleSheet("color:#9d9d9d;font-size:13px;padding-bottom:4px;")
|
||||
layout.addWidget(self._step8_main_hint)
|
||||
|
||||
credits = QLabel("Idea by Sakura · Plugin by Kao_SSS")
|
||||
credits.setStyleSheet("color:#6a6a6a;font-size:11px;font-style:italic;padding-bottom:6px;")
|
||||
layout.addWidget(credits)
|
||||
settings_box = QWidget()
|
||||
settings_box.setObjectName("tbox")
|
||||
settings_box.setStyleSheet(self._task_box_style())
|
||||
settings_inner = QVBoxLayout(settings_box)
|
||||
settings_inner.setContentsMargins(12, 10, 12, 10)
|
||||
settings_inner.setSpacing(8)
|
||||
|
||||
box = QWidget()
|
||||
box.setObjectName("tbox")
|
||||
box.setStyleSheet(self._task_box_style())
|
||||
inner = QVBoxLayout(box)
|
||||
inner.setContentsMargins(10, 8, 10, 8)
|
||||
inner.setSpacing(6)
|
||||
_PT_LABEL_W = 118
|
||||
_PT_FIELD_W = 96
|
||||
_PT_BTN_W = 96
|
||||
_PT_LBL_STYLE = "color:#9d9d9d;font-size:12px;"
|
||||
_PT_SECTION_STYLE = "color:#4ec9b0;font-size:12px;font-weight:bold;"
|
||||
|
||||
self._tli_status_label = QLabel("Status: (detect a project folder first)")
|
||||
self._tli_status_label.setWordWrap(True)
|
||||
self._tli_status_label.setStyleSheet("color:#7a7a7a;font-size:13px;")
|
||||
inner.addWidget(self._tli_status_label)
|
||||
hotkey_title = QLabel("Hotkeys")
|
||||
hotkey_title.setStyleSheet(_PT_SECTION_STYLE)
|
||||
settings_inner.addWidget(hotkey_title)
|
||||
|
||||
hotkey_row = QHBoxLayout()
|
||||
hotkey_row.setSpacing(8)
|
||||
|
||||
insp_lbl = QLabel("Inspector toggle:")
|
||||
insp_lbl.setFixedWidth(_PT_LABEL_W)
|
||||
insp_lbl.setStyleSheet(_PT_LBL_STYLE)
|
||||
insp_lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
hotkey_row.addWidget(insp_lbl)
|
||||
|
||||
self._pt_hotkey_edit = QLineEdit("F9")
|
||||
self._pt_hotkey_edit.setFixedWidth(_PT_FIELD_W)
|
||||
self._pt_hotkey_edit.setPlaceholderText("F9")
|
||||
hotkey_row.addWidget(self._pt_hotkey_edit)
|
||||
|
||||
hotkey_row.addSpacing(16)
|
||||
|
||||
self._pt_forge_hotkey_lbl = QLabel("Forge toggle:")
|
||||
self._pt_forge_hotkey_lbl.setFixedWidth(_PT_LABEL_W)
|
||||
self._pt_forge_hotkey_lbl.setStyleSheet(_PT_LBL_STYLE)
|
||||
self._pt_forge_hotkey_lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
hotkey_row.addWidget(self._pt_forge_hotkey_lbl)
|
||||
|
||||
self._pt_forge_hotkey_edit = QLineEdit("F10")
|
||||
self._pt_forge_hotkey_edit.setFixedWidth(_PT_FIELD_W)
|
||||
self._pt_forge_hotkey_edit.setPlaceholderText("F10")
|
||||
hotkey_row.addWidget(self._pt_forge_hotkey_edit)
|
||||
|
||||
hotkey_row.addStretch(1)
|
||||
settings_inner.addLayout(hotkey_row)
|
||||
|
||||
editor_title = QLabel("Editor settings")
|
||||
editor_title.setStyleSheet("color:#4ec9b0;font-size:12px;font-weight:bold;padding-top:4px;")
|
||||
inner.addWidget(editor_title)
|
||||
editor_title.setStyleSheet(_PT_SECTION_STYLE + "padding-top:2px;")
|
||||
settings_inner.addWidget(editor_title)
|
||||
|
||||
_TLI_LABEL_W = 80
|
||||
_TLI_BTN_W = 88
|
||||
_tli_lbl_style = "color:#9d9d9d;font-size:12px;"
|
||||
|
||||
tli_grid = QGridLayout()
|
||||
tli_grid.setHorizontalSpacing(6)
|
||||
tli_grid.setVerticalSpacing(6)
|
||||
tli_grid.setColumnStretch(1, 1)
|
||||
editor_grid = QGridLayout()
|
||||
editor_grid.setHorizontalSpacing(8)
|
||||
editor_grid.setVerticalSpacing(6)
|
||||
editor_grid.setColumnMinimumWidth(0, _PT_LABEL_W)
|
||||
editor_grid.setColumnStretch(1, 1)
|
||||
|
||||
editor_lbl = QLabel("Editor:")
|
||||
editor_lbl.setFixedWidth(_TLI_LABEL_W)
|
||||
editor_lbl.setStyleSheet(_tli_lbl_style)
|
||||
editor_lbl.setStyleSheet(_PT_LBL_STYLE)
|
||||
editor_lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
tli_grid.addWidget(editor_lbl, 0, 0)
|
||||
editor_grid.addWidget(editor_lbl, 0, 0)
|
||||
|
||||
self._tli_editor_combo = QComboBox()
|
||||
self._tli_editor_combo.currentIndexChanged.connect(self._on_tli_editor_combo_changed)
|
||||
tli_grid.addWidget(self._tli_editor_combo, 0, 1)
|
||||
editor_grid.addWidget(self._tli_editor_combo, 0, 1)
|
||||
|
||||
detect_btn = _make_btn("Detect", "#4a4a4a")
|
||||
detect_btn.setFixedWidth(_TLI_BTN_W)
|
||||
detect_btn.setFixedWidth(_PT_BTN_W)
|
||||
detect_btn.setToolTip("Scan this PC for VS Code, Insiders, or Cursor")
|
||||
detect_btn.clicked.connect(self._detect_tli_editors)
|
||||
tli_grid.addWidget(detect_btn, 0, 2)
|
||||
editor_grid.addWidget(detect_btn, 0, 2)
|
||||
|
||||
custom_lbl = QLabel("Custom path:")
|
||||
custom_lbl.setStyleSheet(_PT_LBL_STYLE)
|
||||
custom_lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
editor_grid.addWidget(custom_lbl, 1, 0)
|
||||
|
||||
self._tli_editor_custom = QLineEdit()
|
||||
self._tli_editor_custom.setPlaceholderText("Path to editor executable (when Custom is selected)")
|
||||
self._tli_editor_custom.setPlaceholderText("Editor executable when Custom is selected")
|
||||
self._tli_editor_custom.setEnabled(False)
|
||||
tli_grid.addWidget(self._tli_editor_custom, 1, 1)
|
||||
editor_grid.addWidget(self._tli_editor_custom, 1, 1)
|
||||
|
||||
browse_editor_btn = _make_btn("Browse…", "#4a4a4a")
|
||||
browse_editor_btn.setFixedWidth(_TLI_BTN_W)
|
||||
browse_editor_btn.setFixedWidth(_PT_BTN_W)
|
||||
browse_editor_btn.clicked.connect(self._browse_tli_editor)
|
||||
tli_grid.addWidget(browse_editor_btn, 1, 2)
|
||||
editor_grid.addWidget(browse_editor_btn, 1, 2)
|
||||
|
||||
self._tli_detect_label = QLabel("")
|
||||
self._tli_detect_label.setWordWrap(True)
|
||||
self._tli_detect_label.setStyleSheet("color:#6a6a6a;font-size:11px;")
|
||||
tli_grid.addWidget(self._tli_detect_label, 2, 1, 1, 2)
|
||||
editor_grid.addWidget(self._tli_detect_label, 2, 1, 1, 2)
|
||||
|
||||
cfg_btn_wrap = QWidget()
|
||||
cfg_btn_row = QHBoxLayout(cfg_btn_wrap)
|
||||
cfg_btn_row.setContentsMargins(0, 0, 0, 0)
|
||||
cfg_btn_row.setSpacing(8)
|
||||
save_tli_btn = _make_btn("✔ Save settings", "#3a5a7a")
|
||||
save_tli_btn.setFixedWidth(140)
|
||||
save_tli_btn.setToolTip("Write editor settings to .env (used on Install / Apply)")
|
||||
save_tli_btn.clicked.connect(self._save_tli_editor_settings)
|
||||
cfg_btn_row.addWidget(save_tli_btn)
|
||||
apply_tli_btn = _make_btn("↻ Apply to game", "#3a5a7a")
|
||||
apply_tli_btn.setFixedWidth(140)
|
||||
apply_tli_btn.setToolTip("Update the installed TLInspector.js in your game folder")
|
||||
apply_tli_btn.clicked.connect(self._apply_tli_editor_settings)
|
||||
cfg_btn_row.addWidget(apply_tli_btn)
|
||||
cfg_btn_row.addStretch()
|
||||
tli_grid.addWidget(cfg_btn_wrap, 3, 1, 1, 2)
|
||||
settings_inner.addLayout(editor_grid)
|
||||
|
||||
inner.addLayout(tli_grid)
|
||||
action_row = QHBoxLayout()
|
||||
action_row.setSpacing(8)
|
||||
save_pt_btn = _make_btn("✔ Save settings", "#3a5a7a")
|
||||
save_pt_btn.setFixedHeight(30)
|
||||
save_pt_btn.setToolTip("Write hotkeys and editor settings to .env (used on Install / Apply)")
|
||||
save_pt_btn.clicked.connect(self._save_playtest_settings)
|
||||
action_row.addWidget(save_pt_btn)
|
||||
|
||||
tips = QLabel(
|
||||
"<ul style='margin:4px 0;padding-left:18px;color:#9d9d9d;font-size:12px;'>"
|
||||
"<li>Run this <b>after exporting</b> translations so the game files are up to date.</li>"
|
||||
"<li>Overlay <b>save to file</b> reloads instantly; VSCode saves reload once the file is stable on disk</li>"
|
||||
"<li><b>F9</b> — force reload database + current map</li>"
|
||||
"<li><b>F10</b> — toggle the inspector panel</li>"
|
||||
"<li>Click a source location to open in VSCode; click <b>edit</b> to change text in-game</li>"
|
||||
"<li>Remove the plugin before shipping a release build</li>"
|
||||
"</ul>"
|
||||
apply_pt_btn = _make_btn("↻ Apply to game", "#3a5a7a")
|
||||
apply_pt_btn.setFixedHeight(30)
|
||||
apply_pt_btn.setToolTip("Update the installed playtest plugin(s) in your game folder")
|
||||
apply_pt_btn.clicked.connect(self._apply_playtest_settings)
|
||||
action_row.addWidget(apply_pt_btn)
|
||||
|
||||
action_row.addStretch(1)
|
||||
|
||||
self._install_both_btn = _make_btn("⬇ Install Both (MZ)", "#3a5a7a")
|
||||
self._install_both_btn.setFixedHeight(30)
|
||||
self._install_both_btn.setToolTip(
|
||||
"Install TL Inspector and Forge as separate plugins (MZ only)"
|
||||
)
|
||||
tips.setWordWrap(True)
|
||||
tips.setTextFormat(Qt.RichText)
|
||||
inner.addWidget(tips)
|
||||
self._install_both_btn.clicked.connect(self._install_both_playtest)
|
||||
action_row.addWidget(self._install_both_btn)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.setSpacing(8)
|
||||
_BTN_W = 200
|
||||
settings_inner.addLayout(action_row)
|
||||
layout.addWidget(settings_box)
|
||||
self._step8_settings_box = settings_box
|
||||
|
||||
# ── Plugins (TL Inspector + Forge) ────────────────────────────────────
|
||||
plugins_box = QWidget()
|
||||
plugins_box.setObjectName("tbox")
|
||||
plugins_box.setStyleSheet(self._task_box_style())
|
||||
plugins_inner = QVBoxLayout(plugins_box)
|
||||
plugins_inner.setContentsMargins(12, 10, 12, 10)
|
||||
plugins_inner.setSpacing(8)
|
||||
|
||||
_PT_SECTION_STYLE = "color:#4ec9b0;font-size:12px;font-weight:bold;"
|
||||
|
||||
tli_title = QLabel("TL Inspector")
|
||||
tli_title.setStyleSheet(_PT_SECTION_STYLE)
|
||||
plugins_inner.addWidget(tli_title)
|
||||
|
||||
self._tli_status_label = QLabel("Status: (detect a project folder first)")
|
||||
self._tli_status_label.setWordWrap(True)
|
||||
self._tli_status_label.setStyleSheet("color:#7a7a7a;font-size:13px;")
|
||||
plugins_inner.addWidget(self._tli_status_label)
|
||||
|
||||
tli_btn_row = QHBoxLayout()
|
||||
tli_btn_row.setSpacing(8)
|
||||
self._tli_install_btn = _make_btn("⬇ Install TL Inspector", "#3a7a3a")
|
||||
self._tli_install_btn.setFixedWidth(_BTN_W)
|
||||
self._tli_install_btn.setMinimumHeight(30)
|
||||
self._tli_install_btn.clicked.connect(self._install_tl_inspector)
|
||||
btn_row.addWidget(self._tli_install_btn)
|
||||
tli_btn_row.addWidget(self._tli_install_btn, 1)
|
||||
|
||||
self._tli_uninstall_btn = _make_btn("⬆ Uninstall TL Inspector", "#7a3a3a")
|
||||
self._tli_uninstall_btn.setFixedWidth(_BTN_W)
|
||||
self._tli_uninstall_btn.setMinimumHeight(30)
|
||||
self._tli_uninstall_btn.clicked.connect(self._uninstall_tl_inspector)
|
||||
btn_row.addWidget(self._tli_uninstall_btn)
|
||||
btn_row.addStretch()
|
||||
inner.addLayout(btn_row)
|
||||
tli_btn_row.addWidget(self._tli_uninstall_btn, 1)
|
||||
plugins_inner.addLayout(tli_btn_row)
|
||||
|
||||
self._step8_forge_section = QWidget()
|
||||
forge_section_layout = QVBoxLayout(self._step8_forge_section)
|
||||
forge_section_layout.setContentsMargins(0, 4, 0, 0)
|
||||
forge_section_layout.setSpacing(8)
|
||||
|
||||
forge_title = QLabel("Forge (MZ only)")
|
||||
forge_title.setStyleSheet(_PT_SECTION_STYLE)
|
||||
forge_section_layout.addWidget(forge_title)
|
||||
|
||||
self._forge_status_label = QLabel("Status: (MZ project required)")
|
||||
self._forge_status_label.setWordWrap(True)
|
||||
self._forge_status_label.setStyleSheet("color:#7a7a7a;font-size:13px;")
|
||||
forge_section_layout.addWidget(self._forge_status_label)
|
||||
|
||||
forge_btn_row = QHBoxLayout()
|
||||
forge_btn_row.setSpacing(8)
|
||||
self._forge_install_btn = _make_btn("⬇ Install Forge", "#3a7a3a")
|
||||
self._forge_install_btn.setMinimumHeight(30)
|
||||
self._forge_install_btn.clicked.connect(self._install_forge)
|
||||
forge_btn_row.addWidget(self._forge_install_btn, 1)
|
||||
|
||||
self._forge_uninstall_btn = _make_btn("⬆ Uninstall Forge", "#7a3a3a")
|
||||
self._forge_uninstall_btn.setMinimumHeight(30)
|
||||
self._forge_uninstall_btn.clicked.connect(self._uninstall_forge)
|
||||
forge_btn_row.addWidget(self._forge_uninstall_btn, 1)
|
||||
forge_section_layout.addLayout(forge_btn_row)
|
||||
|
||||
plugins_inner.addWidget(self._step8_forge_section)
|
||||
|
||||
self._step8_tli_credits = QLabel("Idea by Sakura · Plugin by Kao_SSS")
|
||||
self._step8_tli_credits.setStyleSheet("color:#6a6a6a;font-size:11px;font-style:italic;padding-top:2px;")
|
||||
plugins_inner.addWidget(self._step8_tli_credits)
|
||||
|
||||
layout.addWidget(plugins_box)
|
||||
self._step8_playtest_box = plugins_box
|
||||
self._step8_forge_box = self._step8_forge_section
|
||||
|
||||
layout.addWidget(box)
|
||||
self._step8_playtest_box = box
|
||||
self._populate_tli_editor_combo()
|
||||
self._load_tli_editor_settings()
|
||||
self._load_playtest_settings()
|
||||
|
||||
def _populate_tli_editor_combo(self, select: str | None = None):
|
||||
"""Fill editor dropdown with auto-detect, found editors, and custom."""
|
||||
|
|
@ -2663,18 +2744,24 @@ class WorkflowTab(QWidget):
|
|||
"No VS Code / Cursor found — install one or choose Custom path."
|
||||
)
|
||||
|
||||
def _load_tli_editor_settings(self):
|
||||
"""Load TL Inspector editor settings from .env into Step 8 controls."""
|
||||
def _load_playtest_settings(self):
|
||||
"""Load playtest hotkeys and editor settings from .env into Step 8 controls."""
|
||||
try:
|
||||
from util.tl_inspector.config import load_config
|
||||
from util.playtest.config import load_config
|
||||
cfg = load_config()
|
||||
except Exception:
|
||||
cfg = {"editorCmd": "auto"}
|
||||
cfg = {
|
||||
"hotkey": "F9",
|
||||
"forgeHotkey": "F10",
|
||||
"editorCmd": "auto",
|
||||
}
|
||||
|
||||
self._pt_hotkey_edit.setText(cfg.get("hotkey", "F9"))
|
||||
self._pt_forge_hotkey_edit.setText(cfg.get("forgeHotkey", "F10"))
|
||||
self._populate_tli_editor_combo(select=cfg.get("editorCmd", "auto"))
|
||||
|
||||
def _resolve_tli_config(self) -> dict:
|
||||
"""Build config dict from Step 8 editor controls."""
|
||||
def _resolve_playtest_config(self) -> dict:
|
||||
"""Build playtest config dict from Step 8 controls."""
|
||||
mode = self._tli_editor_combo.currentData()
|
||||
if mode == "__custom__":
|
||||
editor = self._tli_editor_custom.text().strip() or "auto"
|
||||
|
|
@ -2683,7 +2770,74 @@ class WorkflowTab(QWidget):
|
|||
else:
|
||||
editor = "auto"
|
||||
|
||||
return {"editorCmd": editor, "workspaceFolder": "auto"}
|
||||
return {
|
||||
"hotkey": self._pt_hotkey_edit.text().strip() or "F9",
|
||||
"forgeHotkey": self._pt_forge_hotkey_edit.text().strip() or "F10",
|
||||
"editorCmd": editor,
|
||||
"workspaceFolder": "auto",
|
||||
}
|
||||
|
||||
def _save_playtest_settings(self):
|
||||
cfg = self._resolve_playtest_config()
|
||||
try:
|
||||
from util.playtest.config import save_config
|
||||
save_config(cfg)
|
||||
self._log(
|
||||
"✅ Playtest settings saved — "
|
||||
f"inspector={cfg['hotkey']}, forge={cfg['forgeHotkey']}, "
|
||||
f"editor={cfg['editorCmd']}"
|
||||
)
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not save playtest settings: {exc}")
|
||||
|
||||
def _apply_playtest_settings(self):
|
||||
game_root = self.folder_edit.text().strip()
|
||||
if not game_root:
|
||||
self._log("⚠ No game folder set. Complete Step 0 first.")
|
||||
return
|
||||
cfg = self._resolve_playtest_config()
|
||||
try:
|
||||
from util.forge.installer import detect_mz
|
||||
from util.playtest.config import save_config
|
||||
|
||||
save_config(cfg)
|
||||
is_mz = detect_mz(Path(game_root)) is not None
|
||||
msgs: list[str] = []
|
||||
if is_mz:
|
||||
from util.forge.installer import apply_config as apply_forge
|
||||
from util.forge.installer import status as forge_status
|
||||
if forge_status(Path(game_root)).get("plugin_file"):
|
||||
ok, msg = apply_forge(Path(game_root), cfg)
|
||||
msgs.append(("✅ " if ok else "❌ ") + msg)
|
||||
from util.tl_inspector.installer import apply_config as apply_tli
|
||||
from util.tl_inspector.installer import status as tli_status
|
||||
if tli_status(Path(game_root)).get("plugin_file"):
|
||||
ok, msg = apply_tli(Path(game_root), cfg)
|
||||
msgs.append(("✅ " if ok else "❌ ") + msg)
|
||||
if not msgs:
|
||||
self._log("⚠ No playtest plugins installed in this game folder.")
|
||||
return
|
||||
for line in msgs:
|
||||
self._log(line)
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not apply playtest settings: {exc}")
|
||||
return
|
||||
self._refresh_playtest_status()
|
||||
|
||||
def _refresh_playtest_status(self):
|
||||
"""Update Step 8 status labels for the current engine."""
|
||||
if getattr(self, "_step8_playtest_box", None) is not None:
|
||||
self._refresh_tl_inspector_status()
|
||||
game_root = self.folder_edit.text().strip()
|
||||
is_mz = False
|
||||
if game_root:
|
||||
try:
|
||||
from util.forge.installer import detect_mz
|
||||
is_mz = detect_mz(Path(game_root)) is not None
|
||||
except Exception:
|
||||
is_mz = False
|
||||
if is_mz:
|
||||
self._refresh_forge_status()
|
||||
|
||||
def _on_tli_editor_combo_changed(self, _index: int | None = None):
|
||||
custom = self._tli_editor_combo.currentData() == "__custom__"
|
||||
|
|
@ -2715,32 +2869,13 @@ class WorkflowTab(QWidget):
|
|||
self._tli_editor_custom.setText(path)
|
||||
|
||||
def _save_tli_editor_settings(self):
|
||||
cfg = self._resolve_tli_config()
|
||||
try:
|
||||
from util.tl_inspector.config import save_config
|
||||
save_config(cfg)
|
||||
self._log(f"✅ TL Inspector settings saved — editor={cfg['editorCmd']}")
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not save TL Inspector settings: {exc}")
|
||||
self._save_playtest_settings()
|
||||
|
||||
def _apply_tli_editor_settings(self):
|
||||
game_root = self.folder_edit.text().strip()
|
||||
if not game_root:
|
||||
self._log("⚠ No game folder set. Complete Step 0 first.")
|
||||
return
|
||||
cfg = self._resolve_tli_config()
|
||||
try:
|
||||
from util.tl_inspector.config import save_config
|
||||
from util.tl_inspector.installer import apply_config
|
||||
save_config(cfg)
|
||||
ok, msg = apply_config(Path(game_root), cfg)
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not apply TL Inspector settings: {exc}")
|
||||
return
|
||||
self._log(("✅ " if ok else "❌ ") + msg)
|
||||
self._apply_playtest_settings()
|
||||
|
||||
def _refresh_tl_inspector_status(self):
|
||||
"""Update Step 8 status label from the current game folder."""
|
||||
"""Update Step 8 TL Inspector status label from the current game folder."""
|
||||
label = getattr(self, "_tli_status_label", None)
|
||||
if label is None:
|
||||
return
|
||||
|
|
@ -2782,9 +2917,9 @@ class WorkflowTab(QWidget):
|
|||
if not game_root:
|
||||
self._log("⚠ No game folder set. Complete Step 0 first.")
|
||||
return
|
||||
cfg = self._resolve_tli_config()
|
||||
cfg = self._resolve_playtest_config()
|
||||
try:
|
||||
from util.tl_inspector.config import save_config
|
||||
from util.playtest.config import save_config
|
||||
from util.tl_inspector.installer import install
|
||||
save_config(cfg)
|
||||
ok, msg = install(Path(game_root), cfg=cfg)
|
||||
|
|
@ -2792,7 +2927,7 @@ class WorkflowTab(QWidget):
|
|||
self._log(f"❌ TL Inspector install failed: {exc}")
|
||||
return
|
||||
self._log(("✅ " if ok else "❌ ") + msg)
|
||||
self._refresh_tl_inspector_status()
|
||||
self._refresh_playtest_status()
|
||||
|
||||
def _uninstall_tl_inspector(self):
|
||||
game_root = self.folder_edit.text().strip()
|
||||
|
|
@ -2815,7 +2950,114 @@ class WorkflowTab(QWidget):
|
|||
self._log(f"❌ TL Inspector uninstall failed: {exc}")
|
||||
return
|
||||
self._log(("✅ " if ok else "❌ ") + msg)
|
||||
self._refresh_tl_inspector_status()
|
||||
self._refresh_playtest_status()
|
||||
|
||||
def _refresh_forge_status(self):
|
||||
"""Update Step 8 Forge status label from the current game folder."""
|
||||
label = getattr(self, "_forge_status_label", None)
|
||||
if label is None:
|
||||
return
|
||||
game_root = self.folder_edit.text().strip()
|
||||
if not game_root:
|
||||
label.setText("Status: no game folder set — complete Step 0 first.")
|
||||
label.setStyleSheet("color:#7a7a7a;font-size:13px;")
|
||||
return
|
||||
try:
|
||||
from util.forge.installer import detect_mz, status
|
||||
if detect_mz(Path(game_root)) is None:
|
||||
label.setText("Status: not an MZ project (Forge is MZ-only).")
|
||||
label.setStyleSheet("color:#e9a12a;font-size:13px;")
|
||||
return
|
||||
st = status(Path(game_root))
|
||||
except Exception as exc:
|
||||
label.setText(f"Status: error — {exc}")
|
||||
label.setStyleSheet("color:#f48771;font-size:13px;")
|
||||
return
|
||||
|
||||
msg = st.get("message", "")
|
||||
if st.get("declared") and st.get("plugin_file"):
|
||||
detail = "plugin declared in plugins.js and file present"
|
||||
elif st.get("declared"):
|
||||
detail = "declared in plugins.js (plugin file missing)"
|
||||
elif st.get("plugin_file"):
|
||||
detail = "plugin file present (not declared in plugins.js)"
|
||||
else:
|
||||
detail = "not installed"
|
||||
label.setText(f"Status: RPG Maker MZ · {msg} — {detail}")
|
||||
color = "#6a9a6a" if st.get("declared") and st.get("plugin_file") else "#9d9d9d"
|
||||
label.setStyleSheet(f"color:{color};font-size:13px;")
|
||||
|
||||
def _install_forge(self):
|
||||
game_root = self.folder_edit.text().strip()
|
||||
if not game_root:
|
||||
self._log("⚠ No game folder set. Complete Step 0 first.")
|
||||
return
|
||||
cfg = self._resolve_playtest_config()
|
||||
try:
|
||||
from util.playtest.config import save_config
|
||||
from util.forge.installer import install
|
||||
save_config(cfg)
|
||||
ok, msg = install(Path(game_root), cfg=cfg)
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Forge install failed: {exc}")
|
||||
return
|
||||
self._log(("✅ " if ok else "❌ ") + msg)
|
||||
self._refresh_playtest_status()
|
||||
|
||||
def _install_both_playtest(self):
|
||||
game_root = self.folder_edit.text().strip()
|
||||
if not game_root:
|
||||
self._log("⚠ No game folder set. Complete Step 0 first.")
|
||||
return
|
||||
try:
|
||||
from util.forge.installer import detect_mz
|
||||
if detect_mz(Path(game_root)) is None:
|
||||
self._log("⚠ Install Both is for MZ projects only.")
|
||||
return
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not detect engine: {exc}")
|
||||
return
|
||||
|
||||
cfg = self._resolve_playtest_config()
|
||||
try:
|
||||
from util.playtest.config import save_config
|
||||
from util.tl_inspector.installer import install as install_tli
|
||||
from util.forge.installer import install as install_forge
|
||||
|
||||
save_config(cfg)
|
||||
ok_tli, msg_tli = install_tli(Path(game_root), cfg=cfg)
|
||||
self._log(("✅ " if ok_tli else "❌ ") + msg_tli)
|
||||
if not ok_tli:
|
||||
return
|
||||
ok_forge, msg_forge = install_forge(Path(game_root), cfg=cfg)
|
||||
self._log(("✅ " if ok_forge else "❌ ") + msg_forge)
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Install Both failed: {exc}")
|
||||
return
|
||||
self._refresh_playtest_status()
|
||||
|
||||
def _uninstall_forge(self):
|
||||
game_root = self.folder_edit.text().strip()
|
||||
if not game_root:
|
||||
self._log("⚠ No game folder set. Complete Step 0 first.")
|
||||
return
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Uninstall Forge",
|
||||
"Remove Forge_MZ from plugins.js and delete the plugin file?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
if reply != QMessageBox.Yes:
|
||||
return
|
||||
try:
|
||||
from util.forge.installer import uninstall
|
||||
ok, msg = uninstall(Path(game_root))
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Forge uninstall failed: {exc}")
|
||||
return
|
||||
self._log(("✅ " if ok else "❌ ") + msg)
|
||||
self._refresh_playtest_status()
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 0 – Project Folder logic
|
||||
|
|
@ -2930,10 +3172,32 @@ class WorkflowTab(QWidget):
|
|||
if is_ace and self._step_tabs.currentIndex() == 8:
|
||||
self._step_tabs.setCurrentIndex(7)
|
||||
box = getattr(self, "_step8_playtest_box", None)
|
||||
if box is not None:
|
||||
box.setEnabled(not is_ace)
|
||||
forge_section = getattr(self, "_step8_forge_section", None)
|
||||
is_mz = False
|
||||
if not is_ace:
|
||||
self._refresh_tl_inspector_status()
|
||||
game_root = self.folder_edit.text().strip()
|
||||
if game_root:
|
||||
try:
|
||||
from util.forge.installer import detect_mz
|
||||
is_mz = detect_mz(Path(game_root)) is not None
|
||||
except Exception:
|
||||
is_mz = False
|
||||
if box is not None:
|
||||
box.setVisible(not is_ace)
|
||||
box.setEnabled(not is_ace)
|
||||
if forge_section is not None:
|
||||
forge_section.setVisible(is_mz)
|
||||
install_both_btn = getattr(self, "_install_both_btn", None)
|
||||
if install_both_btn is not None:
|
||||
install_both_btn.setVisible(is_mz)
|
||||
forge_hk_lbl = getattr(self, "_pt_forge_hotkey_lbl", None)
|
||||
forge_hk_edit = getattr(self, "_pt_forge_hotkey_edit", None)
|
||||
if forge_hk_lbl is not None:
|
||||
forge_hk_lbl.setVisible(is_mz)
|
||||
if forge_hk_edit is not None:
|
||||
forge_hk_edit.setVisible(is_mz)
|
||||
if not is_ace:
|
||||
self._refresh_playtest_status()
|
||||
|
||||
def _detect_folder(self):
|
||||
folder = self.folder_edit.text().strip()
|
||||
|
|
@ -3060,6 +3324,7 @@ class WorkflowTab(QWidget):
|
|||
"border-radius:4px;margin:4px 0;"
|
||||
)
|
||||
self._log(f"Detected data folder: {data_path} (engine: {engine})")
|
||||
self._update_step6_for_engine(False)
|
||||
|
||||
worker = _ScanWorker(self._data_path, self._engine)
|
||||
worker.done.connect(self._on_scan_done)
|
||||
|
|
|
|||
105
util/forge/Forge_MZ.bat
Normal file
105
util/forge/Forge_MZ.bat
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
@echo off
|
||||
rem ============================================================================
|
||||
rem Forge_MZ installer / uninstaller (RPG Maker MZ only)
|
||||
rem Put this file and Forge_MZ.js in the GAME ROOT (next to the .exe /
|
||||
rem index.html), then double-click this file.
|
||||
rem - Not installed yet -> installs it (copies the plugin into the plugins
|
||||
rem folder and declares it at the end of plugins.js).
|
||||
rem - Already installed -> asks whether to remove it.
|
||||
rem ============================================================================
|
||||
chcp 65001 >nul
|
||||
setlocal
|
||||
set "FORGE_ROOT=%~dp0"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$l=Get-Content -LiteralPath '%~f0'; $i=[Array]::IndexOf($l,'#:PSSTART'); Invoke-Expression (($l[($i+1)..($l.Count-1)]) -join [char]10)"
|
||||
endlocal
|
||||
exit /b
|
||||
#:PSSTART
|
||||
$ErrorActionPreference = 'Stop'
|
||||
try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch {}
|
||||
function PauseExit { [void](Read-Host "`nPress Enter to exit"); exit }
|
||||
|
||||
$root = $env:FORGE_ROOT
|
||||
Write-Host "============================================"
|
||||
Write-Host " Forge_MZ installer (RPG Maker MZ only)"
|
||||
Write-Host "============================================"
|
||||
|
||||
# --- Detect MZ / locate plugins.js ------------------------------------------
|
||||
$mzJs = Join-Path $root 'js\plugins.js'
|
||||
if (Test-Path -LiteralPath $mzJs) {
|
||||
$pluginsJs = $mzJs; $pluginsDir = Join-Path $root 'js\plugins'
|
||||
} else {
|
||||
Write-Host ""
|
||||
Write-Host "ERROR: No RPG Maker MZ game found here." -ForegroundColor Red
|
||||
Write-Host "Could not find 'js\plugins.js'."
|
||||
Write-Host "Forge is MZ-only. Place this installer and Forge_MZ.js in the"
|
||||
Write-Host "game ROOT folder (the one containing the .exe / index.html)."
|
||||
PauseExit
|
||||
}
|
||||
Write-Host "Detected RPG Maker MZ"
|
||||
Write-Host ("plugins list: {0}" -f $pluginsJs)
|
||||
|
||||
$target = Join-Path $pluginsDir 'Forge_MZ.js'
|
||||
$srcRoot = Join-Path $root 'Forge_MZ.js'
|
||||
$utf8 = New-Object System.Text.UTF8Encoding($false) # no BOM
|
||||
$content = [IO.File]::ReadAllText($pluginsJs)
|
||||
$nl = if ($content -match "`r`n") { "`r`n" } else { "`n" }
|
||||
$declared = [regex]::IsMatch($content, '"name"\s*:\s*"Forge_MZ"')
|
||||
$fileThere = Test-Path -LiteralPath $target
|
||||
|
||||
# --- Already installed -> offer to remove -----------------------------------
|
||||
if ($declared -or $fileThere) {
|
||||
Write-Host ""
|
||||
Write-Host "Forge_MZ is currently INSTALLED." -ForegroundColor Yellow
|
||||
$ans = Read-Host "Remove it? (Y/N)"
|
||||
if ($ans -match '^(y|yes)$') {
|
||||
if ($declared) {
|
||||
$kept = ($content -split "`r?`n") | Where-Object { $_ -notmatch '"name"\s*:\s*"Forge_MZ"' }
|
||||
$newText = ($kept -join $nl)
|
||||
$newText = [regex]::Replace($newText, ',(\s*)\];', '$1];')
|
||||
[IO.File]::WriteAllText($pluginsJs, $newText, $utf8)
|
||||
Write-Host "Removed the Forge_MZ entry from plugins.js"
|
||||
}
|
||||
if ($fileThere) {
|
||||
Remove-Item -LiteralPath $target -Force
|
||||
Write-Host ("Deleted {0}" -f $target)
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host "Uninstalled." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Cancelled - nothing changed."
|
||||
}
|
||||
PauseExit
|
||||
}
|
||||
|
||||
# --- Not installed -> install -----------------------------------------------
|
||||
if (-not (Test-Path -LiteralPath $srcRoot)) {
|
||||
Write-Host ""
|
||||
Write-Host "ERROR: Forge_MZ.js was not found next to this installer." -ForegroundColor Red
|
||||
Write-Host ("Expected: {0}" -f $srcRoot)
|
||||
Write-Host "Copy Forge_MZ.js into the game root and run this again."
|
||||
PauseExit
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $pluginsDir)) {
|
||||
New-Item -ItemType Directory -Path $pluginsDir -Force | Out-Null
|
||||
}
|
||||
Copy-Item -LiteralPath $srcRoot -Destination $target -Force
|
||||
Write-Host ("Copied plugin -> {0}" -f $target)
|
||||
|
||||
$entry = ' { "name": "Forge_MZ", "status": true, "description": "Forge — in-game cheat & editor overlay", "parameters": {} }'
|
||||
$idx = $content.LastIndexOf('];')
|
||||
if ($idx -lt 0) {
|
||||
Write-Host ""
|
||||
Write-Host "ERROR: could not find the end of the plugin list ( ]; ) in plugins.js." -ForegroundColor Red
|
||||
Write-Host "The plugin file was copied; please add this line before the ']' yourself:"
|
||||
Write-Host $entry
|
||||
PauseExit
|
||||
}
|
||||
$before = $content.Substring(0, $idx).TrimEnd()
|
||||
$after = $content.Substring($idx)
|
||||
$sep = if ($before.EndsWith(',')) { $nl } else { ',' + $nl }
|
||||
$newText = $before + $sep + $entry + $nl + ' ' + $after
|
||||
[IO.File]::WriteAllText($pluginsJs, $newText, $utf8)
|
||||
Write-Host "Declared Forge_MZ at the end of plugins.js"
|
||||
Write-Host ""
|
||||
Write-Host "Installed! Start the game and press F10 to open Forge." -ForegroundColor Green
|
||||
PauseExit
|
||||
1093
util/forge/Forge_MZ.js
Normal file
1093
util/forge/Forge_MZ.js
Normal file
File diff suppressed because it is too large
Load diff
12
util/forge/__init__.py
Normal file
12
util/forge/__init__.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
"""Forge — in-game cheat & editor overlay for RPG Maker MZ."""
|
||||
|
||||
from util.forge.installer import apply_config, bundled_plugin_path, detect_mz, install, status, uninstall
|
||||
|
||||
__all__ = [
|
||||
"apply_config",
|
||||
"bundled_plugin_path",
|
||||
"detect_mz",
|
||||
"install",
|
||||
"status",
|
||||
"uninstall",
|
||||
]
|
||||
54
util/forge/config.py
Normal file
54
util/forge/config.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""Forge_MZ build — hotkey injection at install time."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from util.playtest.config import load_config
|
||||
|
||||
_PKG_ROOT = Path(__file__).resolve().parent
|
||||
BUNDLED_PLUGIN = _PKG_ROOT / "Forge_MZ.js"
|
||||
|
||||
|
||||
def _js_literal(value) -> str:
|
||||
if value is None:
|
||||
return "null"
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
return json.dumps(str(value))
|
||||
|
||||
|
||||
def plugin_entry(hotkey: str) -> str:
|
||||
hk = _js_literal(hotkey.strip() or "F10")
|
||||
return (
|
||||
f' {{ "name": "Forge_MZ", "status": true, '
|
||||
f'"description": "Forge — in-game cheat & editor overlay", '
|
||||
f'"parameters": {{ "hotkey": {hk}, "speedKey": "Control", '
|
||||
f'"startOpen": "false", "itemMaxOverride": "0" }} }}'
|
||||
)
|
||||
|
||||
|
||||
def _patch_forge_hotkey(forge_text: str, hotkey: str) -> str:
|
||||
hk = hotkey.strip() or "F10"
|
||||
pattern = (
|
||||
r"(\* @param hotkey\s*\n"
|
||||
r"(?:\s*\*[^\n]*\n)*?"
|
||||
r"\s*\* @default )F10"
|
||||
)
|
||||
forge_text, n = re.subn(pattern, rf"\g<1>{hk}", forge_text, count=1)
|
||||
if n == 0:
|
||||
raise ValueError("Could not patch @default hotkey in Forge_MZ.js")
|
||||
forge_text = re.sub(r"\(P\.hotkey \|\| 'F10'\)", f"(P.hotkey || '{hk}')", forge_text)
|
||||
forge_text = re.sub(r"API\._hotkey \|\| 'F10'", f"API._hotkey || '{hk}'", forge_text)
|
||||
return forge_text
|
||||
|
||||
|
||||
def prepare_forge_mz_js(source: Path | None = None, cfg: dict | None = None) -> str:
|
||||
"""Build Forge_MZ.js with the configured toggle hotkey."""
|
||||
src = source or BUNDLED_PLUGIN
|
||||
effective = {**load_config(), **(cfg or {})}
|
||||
return _patch_forge_hotkey(src.read_text(encoding="utf-8"), effective["forgeHotkey"])
|
||||
144
util/forge/installer.py
Normal file
144
util/forge/installer.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
"""Install / uninstall Forge_MZ into an RPG Maker MZ game folder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from util.forge.config import plugin_entry, prepare_forge_mz_js
|
||||
|
||||
PLUGIN_NAME = "Forge_MZ"
|
||||
|
||||
_PKG_ROOT = Path(__file__).resolve().parent
|
||||
DEFAULT_PLUGIN_SRC = _PKG_ROOT / "Forge_MZ.js"
|
||||
|
||||
|
||||
def bundled_plugin_path() -> Path:
|
||||
return DEFAULT_PLUGIN_SRC
|
||||
|
||||
|
||||
def detect_mz(game_root: Path) -> tuple[Path, Path] | None:
|
||||
"""Return (plugins_js, plugins_dir) or None if not MZ."""
|
||||
root = Path(game_root)
|
||||
mz_js = root / "js" / "plugins.js"
|
||||
if mz_js.is_file():
|
||||
return mz_js, root / "js" / "plugins"
|
||||
return None
|
||||
|
||||
|
||||
def _read_plugins_js(plugins_js: Path) -> tuple[str, str]:
|
||||
content = plugins_js.read_text(encoding="utf-8")
|
||||
nl = "\r\n" if "\r\n" in content else "\n"
|
||||
return content, nl
|
||||
|
||||
|
||||
def _is_declared(content: str) -> bool:
|
||||
return bool(re.search(rf'"name"\s*:\s*"{re.escape(PLUGIN_NAME)}"', content))
|
||||
|
||||
|
||||
def status(game_root: Path) -> dict:
|
||||
"""Return install state for the game folder."""
|
||||
info = detect_mz(game_root)
|
||||
if info is None:
|
||||
return {
|
||||
"ok": False,
|
||||
"engine": None,
|
||||
"installed": False,
|
||||
"declared": False,
|
||||
"plugin_file": None,
|
||||
"message": "No RPG Maker MZ game found (missing js/plugins.js).",
|
||||
}
|
||||
plugins_js, plugins_dir = info
|
||||
target = plugins_dir / f"{PLUGIN_NAME}.js"
|
||||
content, _ = _read_plugins_js(plugins_js)
|
||||
declared = _is_declared(content)
|
||||
file_there = target.is_file()
|
||||
return {
|
||||
"ok": True,
|
||||
"engine": "MZ",
|
||||
"installed": declared or file_there,
|
||||
"declared": declared,
|
||||
"plugin_file": file_there,
|
||||
"plugins_js": str(plugins_js),
|
||||
"target": str(target),
|
||||
"message": (
|
||||
"Installed"
|
||||
if declared and file_there
|
||||
else "Partially installed"
|
||||
if declared or file_there
|
||||
else "Not installed"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def install(game_root: Path, source_js: Path | None = None, cfg: dict | None = None) -> tuple[bool, str]:
|
||||
"""Copy Forge_MZ.js into the game and declare it in plugins.js."""
|
||||
info = detect_mz(game_root)
|
||||
if info is None:
|
||||
return False, "No RPG Maker MZ game found at that path (Forge is MZ-only)."
|
||||
|
||||
plugins_js, plugins_dir = info
|
||||
if source_js and not Path(source_js).is_file():
|
||||
return False, f"Forge_MZ.js not found: {source_js}"
|
||||
if not source_js and not DEFAULT_PLUGIN_SRC.is_file():
|
||||
return False, f"Forge_MZ.js not found: {DEFAULT_PLUGIN_SRC}"
|
||||
|
||||
target = plugins_dir / f"{PLUGIN_NAME}.js"
|
||||
content, nl = _read_plugins_js(plugins_js)
|
||||
|
||||
plugins_dir.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(prepare_forge_mz_js(source_js, cfg), encoding="utf-8")
|
||||
|
||||
hotkey = (cfg or {}).get("forgeHotkey", "F10")
|
||||
entry = plugin_entry(hotkey)
|
||||
if not _is_declared(content):
|
||||
idx = content.rfind("];")
|
||||
if idx < 0:
|
||||
return False, "Could not find plugin list end ( ]; ) in plugins.js."
|
||||
before = content[:idx].rstrip()
|
||||
after = content[idx:]
|
||||
sep = nl if before.endswith(",") else "," + nl
|
||||
content = before + sep + entry + nl + " " + after
|
||||
plugins_js.write_text(content, encoding="utf-8", newline="")
|
||||
|
||||
return True, f"Forge installed for RPG Maker MZ. Press {hotkey} in-game to open."
|
||||
|
||||
|
||||
def uninstall(game_root: Path) -> tuple[bool, str]:
|
||||
"""Remove Forge_MZ from plugins.js and delete the plugin file."""
|
||||
info = detect_mz(game_root)
|
||||
if info is None:
|
||||
return False, "No RPG Maker MZ game found at that path."
|
||||
|
||||
plugins_js, plugins_dir = info
|
||||
target = plugins_dir / f"{PLUGIN_NAME}.js"
|
||||
content, nl = _read_plugins_js(plugins_js)
|
||||
|
||||
if _is_declared(content):
|
||||
kept = [
|
||||
line for line in re.split(r"\r?\n", content)
|
||||
if not re.search(rf'"name"\s*:\s*"{re.escape(PLUGIN_NAME)}"', line)
|
||||
]
|
||||
new_text = nl.join(kept)
|
||||
new_text = re.sub(r",(\s*)\];", r"\1];", new_text)
|
||||
plugins_js.write_text(new_text, encoding="utf-8", newline="")
|
||||
|
||||
if target.is_file():
|
||||
target.unlink()
|
||||
|
||||
return True, "Forge uninstalled."
|
||||
|
||||
|
||||
def apply_config(game_root: Path, cfg: dict | None = None) -> tuple[bool, str]:
|
||||
"""Rewrite an installed Forge_MZ.js with the current Forge hotkey."""
|
||||
info = detect_mz(game_root)
|
||||
if info is None:
|
||||
return False, "No RPG Maker MZ game found at that path."
|
||||
|
||||
_, plugins_dir = info
|
||||
target = plugins_dir / f"{PLUGIN_NAME}.js"
|
||||
if not target.is_file():
|
||||
return False, "Forge is not installed in this game folder."
|
||||
|
||||
target.write_text(prepare_forge_mz_js(cfg=cfg), encoding="utf-8")
|
||||
return True, "Forge hotkey applied to the installed plugin."
|
||||
3
util/playtest/__init__.py
Normal file
3
util/playtest/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from util.playtest.config import load_config, save_config
|
||||
|
||||
__all__ = ["load_config", "save_config"]
|
||||
55
util/playtest/config.py
Normal file
55
util/playtest/config.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""Shared playtest plugin settings — hotkeys and editor (.env)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import dotenv_values
|
||||
|
||||
ENV_TL_HOTKEY = "tlHotkey"
|
||||
ENV_FORGE_HOTKEY = "forgeHotkey"
|
||||
ENV_EDITOR = "tlEditorCmd"
|
||||
|
||||
DEFAULTS = {
|
||||
"hotkey": "F9",
|
||||
"forgeHotkey": "F10",
|
||||
"editorCmd": "auto",
|
||||
"workspaceFolder": "auto",
|
||||
}
|
||||
|
||||
ENV_PATH = Path(".env")
|
||||
|
||||
|
||||
def load_config(env_path: Path | None = None) -> dict:
|
||||
path = env_path or ENV_PATH
|
||||
env = dotenv_values(path) if path.is_file() else {}
|
||||
cfg = dict(DEFAULTS)
|
||||
for key, env_key in (
|
||||
("hotkey", ENV_TL_HOTKEY),
|
||||
("forgeHotkey", ENV_FORGE_HOTKEY),
|
||||
("editorCmd", ENV_EDITOR),
|
||||
):
|
||||
val = (env.get(env_key) or "").strip()
|
||||
if val:
|
||||
cfg[key] = val
|
||||
return cfg
|
||||
|
||||
|
||||
def save_config(cfg: dict, env_path: Path | None = None) -> None:
|
||||
path = env_path or ENV_PATH
|
||||
text = path.read_text(encoding="utf-8") if path.is_file() else ""
|
||||
updates = {
|
||||
ENV_TL_HOTKEY: cfg.get("hotkey", DEFAULTS["hotkey"]),
|
||||
ENV_FORGE_HOTKEY: cfg.get("forgeHotkey", DEFAULTS["forgeHotkey"]),
|
||||
ENV_EDITOR: cfg.get("editorCmd", DEFAULTS["editorCmd"]),
|
||||
}
|
||||
for key, val in updates.items():
|
||||
quoted = f"'{val}'"
|
||||
pattern = rf"^({re.escape(key)}\s*=\s*)(?:'[^']*'|\"[^\"]*\"|[^\n]*)"
|
||||
text, n = re.subn(pattern, rf"\g<1>{quoted}", text, count=1, flags=re.MULTILINE)
|
||||
if n == 0:
|
||||
text = text.rstrip("\n") + f"\n{key}={quoted}\n"
|
||||
if not path.exists():
|
||||
path.touch()
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
|
@ -30,7 +30,6 @@
|
|||
* In-game overlay "save to file" reloads that JSON into memory immediately.
|
||||
* VSCode / external saves: we watch data/*.json; once the file mtime is stable
|
||||
* (save finished), reload instantly. The game cannot hook Ctrl+S in VSCode itself.
|
||||
* F9 — manual reload of all database JSON + the current map
|
||||
*
|
||||
* Limitations:
|
||||
* - NW.js desktop playtest only (not browser)
|
||||
|
|
@ -55,8 +54,7 @@
|
|||
//=========================================================================
|
||||
var CFG = {
|
||||
enabled: true,
|
||||
hotkey: 'F10', // key (event.key) that toggles the overlay
|
||||
reloadHotkey: 'F9', // key that reloads on-screen game elements from disk
|
||||
hotkey: 'F9', // key (event.key) that toggles the overlay
|
||||
editorCmd: 'auto', // 'auto' = auto-detect installed editors (VS Code, Cursor,
|
||||
// VSCodium, Insiders, Windsurf). Or set an absolute path to
|
||||
// force a specific editor exe.
|
||||
|
|
@ -1821,7 +1819,7 @@
|
|||
'<select class="tl-edsel" data-act="edsel" title="Which installed editor opens clicked file:line results"></select>' +
|
||||
'<label class="tl-btn tl-frz" title="Block mouse/keyboard from reaching the game while the panel is open">' +
|
||||
'<input type="checkbox" data-act="freeze"' + (ui.freeze ? ' checked' : '') + '> Freeze</label>' +
|
||||
'<span data-act="refresh" class="tl-btn" title="Reload on-screen text & images from disk so saved edits show immediately (F9)">↻ Reload</span>' +
|
||||
'<span data-act="refresh" class="tl-btn" title="Reload on-screen text & images from disk (auto-reloads on save too)">↻ Reload</span>' +
|
||||
'</div>';
|
||||
root.appendChild(head);
|
||||
|
||||
|
|
@ -2330,10 +2328,6 @@
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggle();
|
||||
} else if (CFG.reloadHotkey && e.key === CFG.reloadHotkey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
refreshGameElements();
|
||||
} else if (e.key === 'Escape' && ui.pick) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -2356,7 +2350,7 @@
|
|||
if (inPanel(e.target)) { return; } // our panel / built-in editor get input untouched
|
||||
var isKey = (e.type === 'keydown' || e.type === 'keyup' || e.type === 'keypress');
|
||||
if (isKey) {
|
||||
if (e.key === CFG.hotkey || e.key === CFG.reloadHotkey || e.key === 'Escape') { return; } // hotkeys always work
|
||||
if (e.key === CFG.hotkey || e.key === 'Escape') { return; } // hotkeys always work
|
||||
// Always block the game from seeing the key (stopPropagation), but do NOT
|
||||
// cancel the browser default for copy/select shortcuts (Ctrl+C / Ctrl+A) so
|
||||
// the user can still copy selected text from the panel.
|
||||
|
|
|
|||
|
|
@ -8,15 +8,16 @@ import re
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import dotenv_values
|
||||
from util.playtest.config import load_config as _load_playtest_config, save_config as _save_playtest_config
|
||||
|
||||
ENV_EDITOR = "tlEditorCmd"
|
||||
|
||||
CFG_KEYS = ("editorCmd", "workspaceFolder")
|
||||
CFG_KEYS = ("editorCmd", "workspaceFolder", "hotkey")
|
||||
|
||||
DEFAULTS = {
|
||||
"editorCmd": "auto",
|
||||
"workspaceFolder": "auto",
|
||||
"hotkey": "F9",
|
||||
}
|
||||
|
||||
_PKG_ROOT = Path(__file__).resolve().parent
|
||||
|
|
@ -100,30 +101,13 @@ def detect_primary_editor() -> Path | None:
|
|||
|
||||
|
||||
def load_config(env_path: Path | None = None) -> dict:
|
||||
path = env_path or ENV_PATH
|
||||
env = dotenv_values(path) if path.is_file() else {}
|
||||
cfg = dict(DEFAULTS)
|
||||
editor = (env.get(ENV_EDITOR) or "").strip()
|
||||
if editor:
|
||||
cfg["editorCmd"] = editor
|
||||
return cfg
|
||||
return _load_playtest_config(env_path)
|
||||
|
||||
|
||||
def save_config(cfg: dict, env_path: Path | None = None) -> None:
|
||||
path = env_path or ENV_PATH
|
||||
text = path.read_text(encoding="utf-8") if path.is_file() else ""
|
||||
updates = {
|
||||
ENV_EDITOR: cfg.get("editorCmd", "auto"),
|
||||
}
|
||||
for key, val in updates.items():
|
||||
quoted = f"'{val}'"
|
||||
pattern = rf"^({re.escape(key)}\s*=\s*)(?:'[^']*'|\"[^\"]*\"|[^\n]*)"
|
||||
text, n = re.subn(pattern, rf"\g<1>{quoted}", text, count=1, flags=re.MULTILINE)
|
||||
if n == 0:
|
||||
text = text.rstrip("\n") + f"\n{key}={quoted}\n"
|
||||
if not path.exists():
|
||||
path.touch()
|
||||
path.write_text(text, encoding="utf-8")
|
||||
current = _load_playtest_config(env_path)
|
||||
current.update(cfg)
|
||||
_save_playtest_config(current, env_path)
|
||||
|
||||
|
||||
def _js_literal(value) -> str:
|
||||
|
|
|
|||
|
|
@ -109,7 +109,10 @@ def install(game_root: Path, source_js: Path | None = None, cfg: dict | None = N
|
|||
content = before + sep + PLUGIN_ENTRY + nl + " " + after
|
||||
plugins_js.write_text(content, encoding="utf-8", newline="")
|
||||
|
||||
return True, f"TLInspector installed for RPG Maker {engine}. Press F10 in-game to open."
|
||||
return True, (
|
||||
f"TLInspector installed for RPG Maker {engine}. "
|
||||
f"Press {(cfg or {}).get('hotkey', 'F9')} in-game to open."
|
||||
)
|
||||
|
||||
|
||||
def uninstall(game_root: Path) -> tuple[bool, str]:
|
||||
|
|
|
|||
Loading…
Reference in a new issue