#!/usr/bin/env bash set -euo pipefail usage() { cat <<'USAGE' Usage: extract-and-decompile --game-root [--output-root ] [--force] Prepare local game-owned source for Ariadne. Use the game installation directory as the game root to process all installed seasons. The utility writes recovered source under /source/, transient work under /work/, and local logs under /logs/. The default output root is /src/ariadne/output. Without --force, it refuses to run when any of those directories already exists. With --force, it deletes and recreates only source/, work/, and logs/ under the output root. The script enters a Nix shell for rpycdec when required tools are not already available. Game-derived preparation output must remain local and must not be committed. USAGE } fail() { printf 'extract-and-decompile: %s\n' "$*" >&2 exit 2 } require_tool() { local name=$1 if ! command -v "$name" >/dev/null 2>&1; then fail "missing required tool '$name'" fi } tools_available() { command -v python3 >/dev/null 2>&1 && command -v rpycdec >/dev/null 2>&1 } enter_nix_shell_if_needed() { if tools_available; then return fi if [[ "${ARIADNE_NIX_SHELL:-}" == 1 ]]; then require_tool python3 require_tool rpycdec return fi require_tool nix local tools_nix if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then tools_nix=$BUILD_WORKSPACE_DIRECTORY/src/ariadne/extract-tools.nix else tools_nix=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/extract-tools.nix fi [[ -f "$tools_nix" ]] || fail "missing Ariadne tool definition: $tools_nix" say "entering Nix shell for rpycdec" export ARIADNE_NIX_SHELL=1 exec nix shell \ --extra-experimental-features 'nix-command flakes' \ --file "$tools_nix" \ --command bash "$0" "$@" } say() { printf 'extract-and-decompile: %s\n' "$*" >&2 } game_root= default_output_root=${BUILD_WORKSPACE_DIRECTORY:-$(pwd)}/src/ariadne/output output_root=$default_output_root force=false original_args=("$@") while [[ $# -gt 0 ]]; do case "$1" in --game-root) [[ $# -ge 2 ]] || fail '--game-root requires a path' game_root=$2 shift 2 ;; --output-root) [[ $# -ge 2 ]] || fail '--output-root requires a path' output_root=$2 shift 2 ;; --force) force=true shift ;; -h|--help) usage exit 0 ;; *) fail "unexpected argument: $1" ;; esac done [[ -n "$game_root" ]] || fail 'missing --game-root ' [[ -d "$game_root" ]] || fail "game root is not a directory: $game_root" enter_nix_shell_if_needed "${original_args[@]}" preparation_dirs=(source work logs) for name in "${preparation_dirs[@]}"; do path="$output_root/$name" if [[ -e "$path" && "$force" != true ]]; then fail "refusing to overwrite existing preparation path without --force: $path" fi done if [[ "$force" == true ]]; then say "removing existing preparation directories under $output_root" for name in "${preparation_dirs[@]}"; do rm -rf -- "${output_root:?}/$name" done fi say "creating preparation directories under $output_root" for name in "${preparation_dirs[@]}"; do mkdir -p -- "$output_root/$name" done source_dir=$output_root/source work_dir=$output_root/work logs_dir=$output_root/logs summary_log=$logs_dir/extract-and-decompile.log : > "$summary_log" log() { printf '%s\n' "$*" >> "$summary_log" } record() { say "$*" log "$*" } relative_to_game_root() { realpath --relative-to="$game_root" "$1" } is_renpy_runtime_path() { local relative=$1 [[ "$relative" == renpy/* || "$relative" == */renpy/* ]] } copy_source_file() { local input=$1 local input_root=$2 local label=$3 local relative output relative=$(realpath --relative-to="$input_root" "$input") if is_renpy_runtime_path "$relative"; then log "skipped $label Ren'Py runtime source: $relative" return fi output=$source_dir/$relative mkdir -p -- "$(dirname -- "$output")" cp -- "$input" "$output" record "copied $label source: $relative" } copy_loose_source() { copy_source_file "$1" "$game_root" loose } decompile_compiled_script() { local input=$1 local input_root=$2 local relative output_dir log_file relative=$(realpath --relative-to="$input_root" "$input") if is_renpy_runtime_path "$relative"; then log "skipped Ren'Py runtime compiled script: $relative" return fi output_dir=$source_dir/$(dirname -- "$relative") log_file=$logs_dir/decompile-${relative//\//__}.log mkdir -p -- "$output_dir" say "decompiling script: $relative" RPYCDEC_NO_WARNING=1 rpycdec decompile --output "$output_dir" "$input" > "$log_file" 2>&1 log "decompiled script: $relative" } extract_archive() { local archive=$1 local relative archive_output log_file relative=$(relative_to_game_root "$archive") archive_output=$work_dir/archives/${relative%.rpa} log_file=$logs_dir/unrpa-${relative//\//__}.log mkdir -p -- "$archive_output" say "extracting archive: $relative" RPYCDEC_NO_WARNING=1 rpycdec unrpa --output "$archive_output" "$archive" > "$log_file" 2>&1 log "extracted archive: $relative" while IFS= read -r -d '' archived_source; do copy_source_file "$archived_source" "$archive_output" archived done < <(find "$archive_output" -type f -name '*.rpy' -print0 | sort -z) while IFS= read -r -d '' compiled; do decompile_compiled_script "$compiled" "$archive_output" done < <(find "$archive_output" -type f \( -name '*.rpyc' -o -name '*.rpymc' \) -print0 | sort -z) } say "scanning loose source files" while IFS= read -r -d '' source_file; do copy_loose_source "$source_file" done < <(find "$game_root" -type f -name '*.rpy' -print0 | sort -z) say "scanning archives" while IFS= read -r -d '' archive; do extract_archive "$archive" done < <(find "$game_root" -type f -name '*.rpa' -print0 | sort -z) say "scanning loose compiled scripts" while IFS= read -r -d '' compiled; do decompile_compiled_script "$compiled" "$game_root" done < <(find "$game_root" -type f \( -name '*.rpyc' -o -name '*.rpymc' \) -print0 | sort -z) say "done"