Remove giant ace exe
This commit is contained in:
parent
ce48052677
commit
6d536d6ca0
10 changed files with 302 additions and 7 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
|
|
|||
|
|
@ -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]}")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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
3
util/ace/__init__.py
Normal 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"]
|
||||
BIN
util/ace/offline/RPGMakerDecrypter-cli.exe
Normal file
BIN
util/ace/offline/RPGMakerDecrypter-cli.exe
Normal file
Binary file not shown.
255
util/ace/update_tools.py
Normal file
255
util/ace/update_tools.py
Normal 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())
|
||||
Loading…
Reference in a new issue