Compare commits

...

24 Commits

Author SHA1 Message Date
a417a51e6b feat(clew): extract rpa archives into game tree 2026-09-07 15:34:26 -04:00
a96d3cdd16 feat(clew): copy game directory tree 2026-09-07 15:31:33 -04:00
dedb0f5913 refactor(clew): create log file on first write 2026-09-07 14:47:37 -04:00
cf337b6c4e refactor(clew): add folder creation helper 2026-09-07 14:46:44 -04:00
d6cdf482e4 refactor(clew): configure logging at startup 2026-09-07 14:45:09 -04:00
5f1b29858c refactor(clew): create extracted directory on demand 2026-09-07 14:43:10 -04:00
1d8cf09b34 refactor(clew): stop managing logs directory 2026-09-07 14:42:29 -04:00
24a7c2783f refactor(clew): rename force flag to overwrite 2026-09-07 14:40:45 -04:00
48bd0cd374 refactor(clew): inline process exits 2026-09-07 14:28:29 -04:00
16ca280959 refactor(clew): recreate extracted game tree 2026-09-07 14:26:38 -04:00
40fed73944 style(clew): split inline entrypoint 2026-09-07 14:21:16 -04:00
254384bd09 refactor(clew): inline binary entrypoint 2026-09-07 14:20:51 -04:00
f3526cd93b refactor(clew): resolve default output uniformly 2026-09-07 14:19:03 -04:00
f3def452e8 docs(clew): remove placeholder README 2026-09-07 14:16:21 -04:00
59197bd574 docs(clew): remove examples from help 2026-09-07 14:15:17 -04:00
5e99a89c95 docs(clew): simplify command help 2026-09-07 14:14:38 -04:00
de203a4232 docs(clew): describe tool independently 2026-09-07 14:13:00 -04:00
e99269bb48 docs(clew): move command details into help 2026-09-07 14:12:11 -04:00
311030279f docs(clew): explain extraction workflow 2026-09-07 14:10:09 -04:00
7fcc12bbd9 fix(clew): default output to repository root 2026-09-07 14:07:29 -04:00
4de3e8dca0 fix(clew): extract full archives by default 2026-09-07 14:01:50 -04:00
688c655d5a fix(clew): collect compiled script archives 2026-09-07 14:00:11 -04:00
a2d3520230 refactor(clew): use one source file for binary 2026-09-07 13:54:31 -04:00
04b8b74e1b feat(clew): extract Ren'Py source files 2026-09-07 13:48:20 -04:00
3 changed files with 112 additions and 0 deletions

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)