feat(clew): extract Ren'Py source files #19

Merged
alexion merged 1 commits from clew-extract-game-files into main 2026-09-07 15:39:47 -04:00
3 changed files with 112 additions and 0 deletions
Showing only changes of commit df03bfbcbb - Show all commits

3
.gitignore vendored
View File

@@ -20,6 +20,9 @@ Thumbs.db
# Bazel convenience symlinks
/bazel-*
# local generated output
/output/
# language package-manager caches and outputs
node_modules/
dist/

8
src/clew/BUILD.bazel Normal file
View File

@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_binary")
py_binary(
name = "clew",
srcs = ["clew.py"],
main = "clew.py",
deps = ["//third_party/python:rpycdec"],
)

101
src/clew/clew.py Normal file
View File

@@ -0,0 +1,101 @@
from __future__ import annotations
import argparse
import logging
import os
import shutil
import sys
from collections import deque
from pathlib import Path
from rpycdec.rpa import extract_rpa
LOG = logging.getLogger(__name__)
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="clew",
description="Extract Ren'Py game files.",
)
parser.add_argument(
"--game-root",
required=True,
type=Path,
help="Game installation root to scan.",
)
parser.add_argument(
"--output-root",
type=Path,
default=Path("output"),
help="Output root. Defaults to repository output/.",
)
parser.add_argument(
"--overwrite",
action="store_true",
help="Replace Clew-managed output directories.",
)
return parser.parse_args(argv)
def resolve_output_root(output_root: Path) -> Path:
if output_root.is_absolute():
return output_root.resolve()
workspace = os.environ.get("BUILD_WORKSPACE_DIRECTORY")
if workspace:
return (Path(workspace) / output_root).resolve()
return output_root.resolve()
def get_output_directory(output_root: Path, folder: str) -> Path:
directory = output_root / folder
directory.mkdir(parents=True, exist_ok=True)
return directory
def extract_archive(archive: Path, destination: Path) -> None:
destination.mkdir(parents=True, exist_ok=True)
with archive.open("rb") as handle:
extract_rpa(handle, output_dir=str(destination))
def extract_game_files(game_root: Path, output_path: Path) -> None:
queue = deque(game_root.iterdir())
while queue:
item = queue.popleft()
destination = output_path / item.relative_to(game_root)
if item.is_dir():
destination.mkdir(parents=True, exist_ok=True)
queue.extend(item.iterdir())
elif item.is_file() and item.suffix.lower() == ".rpa":
extract_archive(item, destination.with_suffix(""))
elif item.is_file():
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item, destination)
def run(args: argparse.Namespace) -> None:
game_root = args.game_root.resolve()
output_root = resolve_output_root(args.output_root)
if not game_root.is_dir():
LOG.critical(f'game root is not a directory: {game_root}')
raise SystemExit(2)
extracted = get_output_directory(output_root, "extracted")
extract_game_files(game_root, extracted)
if __name__ == "__main__":
args = parse_args(sys.argv[1:])
logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] %(levelname)s %(filename)s:%(lineno)d | %(message)s",
datefmt="%Y-%m-%d][%H:%M:%S",
)
run(args)