feat(clew): extract Ren'Py source files #19
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
8
src/clew/BUILD.bazel
Normal 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
101
src/clew/clew.py
Normal 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)
|
||||
Reference in New Issue
Block a user