Remove giant ace exe

This commit is contained in:
DazedAnon 2026-06-12 15:13:41 -05:00
parent ce48052677
commit 6d536d6ca0
10 changed files with 302 additions and 7 deletions

3
.gitignore vendored
View file

@ -21,3 +21,6 @@ __pycache__
!vocab_base.txt
!requirements.txt
!util/tl_inspector/TLInspector.js
util/ace/*.exe
util/ace/.tools_version.json
!util/ace/offline/*.exe

View file

@ -4,7 +4,7 @@ An AI-powered game translation tool with a GUI. Translate RPG Maker, Ren'Py, Tyr
## Credits
- **[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.
- **[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.
## Table of Contents

View file

@ -178,6 +178,14 @@ if errorlevel 1 (
)
echo.
:: RPG Maker Ace tools (RV2JSON + decrypter) — downloaded from GitHub, not stored in git
echo Checking RPG Maker Ace tools...
python -m util.ace.update_tools
if errorlevel 1 (
echo WARNING: Ace tool download failed. Ace translation features may not work until fixed.
)
echo.
:: Launch the GUI
echo ==========================================
echo Launching DazedMTLTool GUI...

View file

@ -132,6 +132,12 @@ class UpdateThread(QThread):
shutil.copy2(src, dst)
Path(self.SHA_FILE).write_text(latest_sha)
try:
from util.ace.update_tools import ensure_ace_tools
self.progress.emit("Updating RPG Maker Ace tools…")
ensure_ace_tools(log_fn=lambda m: self.progress.emit(m))
except Exception as exc:
self.progress.emit(f"Ace tool update skipped: {exc}")
self.finished.emit(True, f"updated:{latest_sha[:8]}")

View file

@ -3868,7 +3868,12 @@ class WorkflowTab(QWidget):
@staticmethod
def _ace_tool_path(name: str) -> Path:
return Path(__file__).resolve().parent.parent / "util" / "ace" / name
from util.ace.update_tools import ace_tool_path
return ace_tool_path(name)
def _ensure_ace_tools(self) -> bool:
from util.ace.update_tools import ensure_ace_tools
return ensure_ace_tools(log_fn=self._log)
def _show_ace_decrypt_notice(self, game_root: str, rgss_path: str):
"""Show a dialog explaining how to decrypt the encrypted Ace archive."""
@ -3895,12 +3900,17 @@ class WorkflowTab(QWidget):
self._run_ace_decrypter(game_root)
def _run_ace_decrypter(self, game_root: str):
decrypter = self._ace_tool_path("RPGMakerDecrypter.exe")
if not decrypter.is_file():
self._log(f"❌ RPGMakerDecrypter.exe not found at {decrypter}")
if not self._ensure_ace_tools():
return
self._log(f"Running RPGMakerDecrypter.exe in {game_root}")
w = _SubprocessWorker([str(decrypter)], cwd=game_root, label="RPGMakerDecrypter")
try:
from util.ace.update_tools import build_decrypter_command
cmd = build_decrypter_command(Path(game_root))
except FileNotFoundError as exc:
self._log(f"{exc}")
return
decrypter = Path(cmd[0])
self._log(f"Running {decrypter.name} in {game_root}")
w = _SubprocessWorker(cmd, cwd=game_root, label=decrypter.stem)
w.log.connect(self._log)
w.done.connect(lambda ok, msg: self._log(("" if ok else "") + msg))
self._worker = w
@ -3908,6 +3918,8 @@ class WorkflowTab(QWidget):
def _run_rv2json_create(self):
"""Run RV2JSON.exe -c to convert rvdata2 → JSON files (run from game root)."""
if not self._ensure_ace_tools():
return
rv2json = self._ace_tool_path("RV2JSON.exe")
if not rv2json.is_file():
self._log(f"❌ RV2JSON.exe not found at {rv2json}")
@ -3962,6 +3974,8 @@ class WorkflowTab(QWidget):
def _run_rv2json_update(self):
"""Run RV2JSON.exe -u to write translated JSON back to rvdata2 files."""
if not self._ensure_ace_tools():
return
rv2json = self._ace_tool_path("RV2JSON.exe")
if not rv2json.is_file():
self._log(f"❌ RV2JSON.exe not found at {rv2json}")

View file

@ -43,6 +43,12 @@ def main():
# Check dependencies
if not check_dependencies():
sys.exit(1)
try:
from util.ace.update_tools import ensure_ace_tools
ensure_ace_tools()
except Exception as exc:
print(f"Warning: Ace tool setup failed ({exc}). Ace features may be unavailable.")
# Import and run GUI
try:

3
util/ace/__init__.py Normal file
View file

@ -0,0 +1,3 @@
"""RPG Maker Ace helper binaries (downloaded on demand, not committed to git)."""
__all__ = ["ace_tool_path", "build_decrypter_command", "ensure_ace_tools"]

Binary file not shown.

255
util/ace/update_tools.py Normal file
View file

@ -0,0 +1,255 @@
"""Download / update RPG Maker Ace tools from upstream GitHub releases.
RV2JSON.exe https://github.com/Sinflower/RV2JSON (bin/RV2JSON.exe)
Decrypter CLI https://github.com/uuksu/RPGMakerDecrypter (release asset)
"""
from __future__ import annotations
import json
import shutil
import sys
import urllib.error
import urllib.request
from pathlib import Path
ACE_DIR = Path(__file__).resolve().parent
OFFLINE_DIR = ACE_DIR / "offline"
VERSION_FILE = ACE_DIR / ".tools_version.json"
RV2JSON_REPO = "Sinflower/RV2JSON"
RV2JSON_REMOTE = "bin/RV2JSON.exe"
RV2JSON_LOCAL = ACE_DIR / "RV2JSON.exe"
DECRYPTER_REPO = "uuksu/RPGMakerDecrypter"
DECRYPTER_ASSET = "RPGMakerDecrypter-cli.exe"
DECRYPTER_LOCAL = ACE_DIR / "RPGMakerDecrypter-cli.exe"
# Legacy name shipped in older DazedMTLTool commits (local only, not in git).
DECRYPTER_LEGACY = ACE_DIR / "RPGMakerDecrypter.exe"
USER_AGENT = "DazedMTLTool"
def _load_versions() -> dict:
if not VERSION_FILE.is_file():
return {}
try:
return json.loads(VERSION_FILE.read_text(encoding="utf-8"))
except Exception:
return {}
def _save_versions(data: dict) -> None:
ACE_DIR.mkdir(parents=True, exist_ok=True)
VERSION_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
def _github_json(url: str) -> dict | list:
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, "Accept": "application/vnd.github+json"})
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode("utf-8"))
def _download(url: str, dest: Path) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
tmp = dest.with_suffix(dest.suffix + ".part")
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=600) as resp, open(tmp, "wb") as fh:
shutil.copyfileobj(resp, fh)
tmp.replace(dest)
def _log(msg: str, log_fn) -> None:
if log_fn:
log_fn(msg)
else:
print(msg, flush=True)
def _seed_from_offline(local: Path, offline_name: str, log_fn) -> bool:
"""Copy a bundled offline exe into the active path if missing."""
if local.is_file():
return True
src = OFFLINE_DIR / offline_name
if not src.is_file():
return False
local.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, local)
_log(f"Using offline bundled {offline_name}", log_fn)
return True
def _rv2json_upstream() -> tuple[str, str]:
"""Return (blob_sha, download_url) for RV2JSON.exe on main."""
url = f"https://api.github.com/repos/{RV2JSON_REPO}/contents/{RV2JSON_REMOTE}?ref=main"
info = _github_json(url)
sha = info.get("sha", "")
dl = info.get("download_url") or ""
if not sha or not dl:
raise RuntimeError(f"Could not resolve {RV2JSON_REPO}/{RV2JSON_REMOTE}")
return sha, dl
def _decrypter_upstream() -> tuple[str, str]:
"""Return (release_tag, browser_download_url) for the Windows CLI decrypter."""
url = f"https://api.github.com/repos/{DECRYPTER_REPO}/releases/latest"
release = _github_json(url)
tag = release.get("tag_name") or release.get("name") or "latest"
for asset in release.get("assets") or []:
if asset.get("name") == DECRYPTER_ASSET:
dl = asset.get("browser_download_url") or ""
if dl:
return tag, dl
raise RuntimeError(f"No {DECRYPTER_ASSET} asset in latest {DECRYPTER_REPO} release")
def ensure_rv2json(force: bool = False, log_fn=print) -> bool:
"""Download RV2JSON.exe if missing or upstream changed. Returns True if ready."""
_seed_from_offline(RV2JSON_LOCAL, "RV2JSON.exe", log_fn)
versions = _load_versions()
try:
remote_sha, download_url = _rv2json_upstream()
except Exception as exc:
if RV2JSON_LOCAL.is_file():
_log(f"Warning: Could not check RV2JSON update ({exc}); using local copy.", log_fn)
return True
if _seed_from_offline(RV2JSON_LOCAL, "RV2JSON.exe", log_fn):
return True
_log(f"ERROR: RV2JSON unavailable ({exc}).", log_fn)
return False
local_sha = versions.get("rv2json_sha", "")
if RV2JSON_LOCAL.is_file() and not force and local_sha == remote_sha:
return True
_log(f"Downloading RV2JSON.exe ({RV2JSON_REPO})...", log_fn)
try:
_download(download_url, RV2JSON_LOCAL)
except Exception as exc:
if RV2JSON_LOCAL.is_file():
_log(f"Warning: RV2JSON update failed ({exc}); keeping existing copy.", log_fn)
return True
if _seed_from_offline(RV2JSON_LOCAL, "RV2JSON.exe", log_fn):
return True
_log(f"ERROR: RV2JSON download failed: {exc}", log_fn)
return False
versions["rv2json_sha"] = remote_sha
_save_versions(versions)
_log("RV2JSON.exe ready", log_fn)
return True
def ensure_decrypter(force: bool = False, log_fn=print) -> bool:
"""Download RPGMakerDecrypter CLI if missing or upstream release changed."""
_seed_from_offline(DECRYPTER_LOCAL, DECRYPTER_ASSET, log_fn)
if DECRYPTER_LOCAL.is_file():
pass # prefer CLI (offline bundle or download)
elif DECRYPTER_LEGACY.is_file():
_log("Using legacy RPGMakerDecrypter.exe (local).", log_fn)
return True
versions = _load_versions()
try:
tag, download_url = _decrypter_upstream()
except Exception as exc:
if DECRYPTER_LOCAL.is_file() or DECRYPTER_LEGACY.is_file():
_log(f"Warning: Could not check decrypter update ({exc}); using local copy.", log_fn)
return True
if _seed_from_offline(DECRYPTER_LOCAL, DECRYPTER_ASSET, log_fn):
return True
_log(f"ERROR: Decrypter unavailable ({exc}).", log_fn)
return False
local_tag = versions.get("decrypter_tag", "")
if DECRYPTER_LOCAL.is_file() and not force and local_tag == tag:
return True
_log(f"Downloading {DECRYPTER_ASSET} ({DECRYPTER_REPO} {tag})...", log_fn)
try:
_download(download_url, DECRYPTER_LOCAL)
except Exception as exc:
if DECRYPTER_LOCAL.is_file() or DECRYPTER_LEGACY.is_file():
_log(f"Warning: Decrypter update failed ({exc}); keeping existing copy.", log_fn)
return True
if _seed_from_offline(DECRYPTER_LOCAL, DECRYPTER_ASSET, log_fn):
return True
_log(f"ERROR: Decrypter download failed: {exc}", log_fn)
return False
versions["decrypter_tag"] = tag
_save_versions(versions)
_log("RPG Maker decrypter CLI ready", log_fn)
return True
def ensure_ace_tools(force: bool = False, log_fn=print) -> bool:
"""Ensure both Ace tools are present (download / update if needed)."""
ok_rv = ensure_rv2json(force=force, log_fn=log_fn)
ok_dec = ensure_decrypter(force=force, log_fn=log_fn)
return ok_rv and ok_dec
def ace_tool_path(name: str) -> Path:
"""Resolve a tool path under util/ace/."""
if name == "RV2JSON.exe":
return RV2JSON_LOCAL
if name in ("RPGMakerDecrypter.exe", "RPGMakerDecrypter-cli.exe"):
if DECRYPTER_LOCAL.is_file():
return DECRYPTER_LOCAL
if DECRYPTER_LEGACY.is_file():
return DECRYPTER_LEGACY
return DECRYPTER_LOCAL
return ACE_DIR / name
def build_decrypter_command(game_root: Path) -> list[str]:
"""Build argv to decrypt Game.rgss* in game_root."""
exe = ace_tool_path("RPGMakerDecrypter-cli.exe")
if not exe.is_file():
raise FileNotFoundError(
f"No RPG Maker decrypter found in {ACE_DIR}. "
"Run: python -m util.ace.update_tools"
)
rgss = sorted(game_root.glob("Game.rgss*"))
if not rgss:
raise FileNotFoundError(f"No Game.rgss* archive in {game_root}")
# Legacy GUI exe ran with no args; CLI needs the archive path.
if exe.name == DECRYPTER_LEGACY.name:
return [str(exe)]
return [str(exe), str(rgss[0])]
def refresh_offline_bundle(log_fn=print) -> bool:
"""Download upstream tools into util/ace/offline/ (for maintainers to commit)."""
OFFLINE_DIR.mkdir(parents=True, exist_ok=True)
ok = True
try:
_, rv_url = _rv2json_upstream()
_log("Refreshing offline RV2JSON.exe...", log_fn)
_download(rv_url, OFFLINE_DIR / "RV2JSON.exe")
except Exception as exc:
_log(f"ERROR: offline RV2JSON refresh failed: {exc}", log_fn)
ok = False
try:
_, dec_url = _decrypter_upstream()
_log(f"Refreshing offline {DECRYPTER_ASSET}...", log_fn)
_download(dec_url, OFFLINE_DIR / DECRYPTER_ASSET)
except Exception as exc:
_log(f"ERROR: offline decrypter refresh failed: {exc}", log_fn)
ok = False
if ok:
_log("Offline bundle refreshed in util/ace/offline/", log_fn)
return ok
def main() -> int:
if "--refresh-offline" in sys.argv:
return 0 if refresh_offline_bundle() else 1
force = "--force" in sys.argv or "-f" in sys.argv
ok = ensure_ace_tools(force=force)
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())