Last active
June 30, 2025 22:15
-
-
Save doesdev/0bc2e71e47a8c47d45214a020898dea6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # Binding of Isaac Mod Merger v5 | |
| # – Portable, non‐destructive, interactive or headless | |
| # – Dry‐run, conflict resolution, dependency scan | |
| # – File‐list editing (-f), metadata/log/README, Lua syntax check + dump on failure | |
| # – Tracks conflicted mods and shows merge progress bar | |
| # | |
| set -euo pipefail | |
| IFS=$'\n\t' | |
| trap 'echo "❌ Script failed at line $LINENO"' ERR | |
| # Colors | |
| RED='\033[0;31m'; YEL='\033[0;33m'; GRN='\033[0;32m'; BLU='\033[0;34m'; NC='\033[0m' | |
| # Bash ≥4 | |
| if (( BASH_VERSINFO[0] < 4 )); then | |
| echo -e "${RED}Bash 4+ required (found ${BASH_VERSINFO[0]}).${NC}" | |
| exit 1 | |
| fi | |
| VERSION="v5" | |
| TIMESTAMP=$(date +%Y%m%d_%H%M%S) | |
| DEF_NAME="merged_mod_${TIMESTAMP}" | |
| DEF_AUTHOR="SteamDeckUser" | |
| DEF_DESC="Merged mod created on Steam Deck" | |
| DEF_VER="1.0" | |
| POSSIBLE=( | |
| "$HOME/.local/share/Steam/steamapps/common/The Binding of Isaac Rebirth/mods" | |
| "$HOME/.steam/steam/steamapps/common/The Binding of Isaac Rebirth/mods" | |
| "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/The Binding of Isaac Rebirth/mods" | |
| ) | |
| ASSUME_YES=false | |
| SKIP_DRY_RUN=false | |
| QUIET=false | |
| USE_LIST_FILE=false | |
| CR_MODE="" | |
| # track conflicted mods | |
| declare -A mod_conflict_seen | |
| conflict_mods=() | |
| show_help(){ | |
| cat <<EOF | |
| Usage: $(basename "$0") [options] | |
| Options: | |
| -h, --help Show help | |
| -y, --yes Accept defaults | |
| -n, --no-dry-run Skip dry-run | |
| -q, --quiet Quiet output | |
| -f, --file Edit source-mod list in $(pwd)/mods_to_merge.txt | |
| EOF | |
| } | |
| # progress bar function | |
| progress_bar(){ | |
| local cur=$1 total=$2 width=40 filled empty i | |
| (( filled = cur * width / total )) | |
| (( empty = width - filled )) | |
| printf "\rProgress: [" | |
| for ((i=0; i<filled; i++)); do printf '#'; done | |
| for ((i=0; i<empty; i++)); do printf '-'; done | |
| printf "] %d/%d\n" "$cur" "$total" | |
| } | |
| # parse flags | |
| while (( "$#" )); do | |
| case "$1" in | |
| -h|--help) show_help; exit 0;; | |
| -y|--yes) ASSUME_YES=true; shift;; | |
| -n|--no-dry-run) SKIP_DRY_RUN=true; shift;; | |
| -q|--quiet) QUIET=true; shift;; | |
| -f|--file) USE_LIST_FILE=true; shift;; | |
| *) echo "Unknown option: $1"; show_help; exit 1;; | |
| esac | |
| done | |
| log(){ $QUIET || echo -e "$@"; } | |
| info(){ log "${BLU}➔${NC} $@"; } | |
| warn(){ log "${YEL}⚠️${NC} $@"; } | |
| error(){ log "${RED}❌${NC} $@"; } | |
| info "Isaac Mod Merger ${VERSION}" | |
| # autodetect mods dir | |
| MOD_ROOT="" | |
| for p in "${POSSIBLE[@]}"; do | |
| [[ -d "$p" ]] && { MOD_ROOT="$p"; break; } | |
| done | |
| [[ -z "$MOD_ROOT" ]] && MOD_ROOT="${POSSIBLE[0]}" | |
| # interactive prompts | |
| if ! $ASSUME_YES; then | |
| read -rp "Merged mod name [${DEF_NAME}]: " MOD_NAME; MOD_NAME="${MOD_NAME:-$DEF_NAME}" | |
| read -rp "Author [${DEF_AUTHOR}]: " AUTHOR; AUTHOR="${AUTHOR:-$DEF_AUTHOR}" | |
| read -rp "Description [${DEF_DESC}]: " DESC; DESC="${DESC:-$DEF_DESC}" | |
| read -rp "Version [${DEF_VER}]: " VER; VER="${VER:-$DEF_VER}" | |
| read -rp "Mods directory [${MOD_ROOT}]: " input; MOD_ROOT="${input:-$MOD_ROOT}" | |
| else | |
| MOD_NAME="$DEF_NAME"; AUTHOR="$DEF_AUTHOR"; DESC="$DEF_DESC"; VER="$DEF_VER" | |
| fi | |
| # sanitize & set dest | |
| MOD_NAME="${MOD_NAME//[^A-Za-z0-9_-]/_}" | |
| DEST="$MOD_ROOT/$MOD_NAME" | |
| info "Destination: $DEST" | |
| [[ -d "$MOD_ROOT" ]] || { error "Mods dir not found: $MOD_ROOT"; exit 1; } | |
| # dry-run decision | |
| if ! $SKIP_DRY_RUN && ! $ASSUME_YES; then | |
| read -rp "Dry run first? (Y/n): " dr | |
| SKIP_DRY_RUN=$([[ "${dr:-Y}" =~ ^[Nn]$ ]] && echo true || echo false) | |
| fi | |
| # collect source-mods | |
| if $USE_LIST_FILE; then | |
| LIST_FILE="$(pwd)/mods_to_merge.txt" | |
| [[ ! -f "$LIST_FILE" ]] && cat >"$LIST_FILE" <<EOF | |
| # One absolute path per line. Blank/# lines ignored. | |
| # Save & exit to continue. | |
| EOF | |
| ${EDITOR:-vi} "$LIST_FILE" | |
| MODS=(); while read -r m; do [[ "$m" =~ ^# ]] && continue; [[ -z "$m" ]] && continue; MODS+=("$m"); done <"$LIST_FILE" | |
| else | |
| echo; info "Enter mod paths (one per line), blank to finish:" | |
| MODS=(); while read -r m && [[ -n "$m" ]]; do MODS+=("$m"); done | |
| fi | |
| (( ${#MODS[@]} )) || { error "No mods specified"; exit 1; } | |
| # validate & analyze | |
| declare -A MODULES; VALID=(); TOTAL_MB=0 | |
| echo; info "🔍 Analyzing mods..." | |
| for mp in "${MODS[@]}"; do | |
| [[ -d "$mp" ]] || { warn "Not a dir: $mp"; continue; } | |
| base=$(basename "$mp"); info "• $base" | |
| [[ ! -f "$mp/main.lua" ]] && warn " Missing main.lua" | |
| MODULES["$base"]=1 | |
| if command -v du &>/dev/null; then mb=$(du -sm "$mp" | cut -f1); TOTAL_MB=$((TOTAL_MB+mb)); (( mb>100 )) && warn " Large (${mb}MB)"; fi | |
| VALID+=("$mp") | |
| done | |
| (( ${#VALID[@]} )) || { error "No valid mods"; exit 1; } | |
| info "Mods to merge: ${#VALID[@]}, total ≈ ${TOTAL_MB}MB" | |
| # prune backups >7d | |
| pruned=$(find "$MOD_ROOT" -maxdepth 1 -type d -name "*_backup_*" -mtime +7 | wc -l) | |
| (( pruned>0 )) && info "Pruned $pruned old backups" | |
| # dry-run preview | |
| if ! $SKIP_DRY_RUN; then | |
| echo; info "🔍 Dry run:" | |
| EXIST=() | |
| [[ -d "$DEST" ]] && while read -r f; do EXIST+=("${f#"$DEST/"}"); done < <(find "$DEST" -type f) | |
| for mp in "${VALID[@]}"; do | |
| base=$(basename "$mp"); info "• $base" | |
| find "$mp" -type f ! -name main.lua ! -name metadata.xml | while read -r f; do | |
| prefix="$mp/"; rel="${f#$prefix}" | |
| for e in "${EXIST[@]}"; do [[ "$rel" == "$e" ]] && warn " Conflict: $rel"; done | |
| done | |
| done | |
| if ! $ASSUME_YES; then read -rp "Proceed? (y/N): " ok; [[ "${ok^^}" == "Y" ]] || { info "Cancelled."; exit 0; }; fi | |
| fi | |
| # conflict mode | |
| if ! $ASSUME_YES; then | |
| until [[ $CR_MODE =~ ^[1-4]$ ]]; do echo; info "Conflict modes: 1)ask 2)rename 3)skip 4)overwrite"; read -rp "Choose [2]: " CR_MODE; CR_MODE="${CR_MODE:-2}"; done | |
| else | |
| CR_MODE=2 | |
| fi | |
| # backup existing\[[ -d "$DEST" ]] && { mv "$DEST" "${DEST}_backup_${TIMESTAMP}"; info "Backed up existing mod"; } | |
| mkdir -p "$DEST" | |
| # start merged_main.lua | |
| MERGED="$DEST/merged_main.lua" | |
| { | |
| echo "-- $MOD_NAME merged" | |
| echo "-- by $AUTHOR on $(date)" | |
| echo | |
| } >"$MERGED" | |
| # merge | |
| info "🔄 Merging mods..." | |
| total=${#VALID[@]}; idx=0 | |
| for mp in "${VALID[@]}"; do | |
| idx=$((idx+1)); progress_bar $idx $total | |
| base=$(basename "$mp"); ns="${base//[^A-Za-z0-9_]/_}" | |
| info "• $base → $ns" | |
| # assets | |
| find "$mp" -type f ! -name main.lua ! -name metadata.xml | while read -r f; do | |
| prefix="$mp/"; rel="${f#$prefix}"; dest="$DEST/$rel" | |
| if [[ -f "$dest" ]]; then | |
| warn " Conflict: $rel" | |
| # record mod conflict | |
| if [[ -z ${mod_conflict_seen[$base]:-} ]]; then mod_conflict_seen[$base]=1; conflict_mods+=("$base"); fi | |
| case $CR_MODE in | |
| 2) : ;; 1) echo " [1] rename [2] skip [3] overwrite"; read -rp " choice: " ch;; *) ch=$CR_MODE;; | |
| esac | |
| case $ch in | |
| 2) nm="${rel%.*}_$ns.${rel##*.}"; dest="$DEST/$nm"; info " renamed → $nm";; | |
| 3) info " overwriting";; | |
| *) continue;; | |
| esac | |
| fi | |
| mkdir -p "$(dirname "$dest")"; cp "$f" "$dest" | |
| done | |
| # main.lua | |
| if [[ -f "$mp/main.lua" ]]; then | |
| info " merging main.lua" | |
| { | |
| echo; echo "-- from $base/main.lua" | |
| echo "do local mod = RegisterMod(\"$base\", 1)" | |
| sed -E '/^[[:space:]]*local[[:space:]]+mod[[:space:]]*=[[:space:]]*RegisterMod/ s/^/-- /; \$a\\' "$mp/main.lua" | |
| echo "end" | |
| echo | |
| } >>"$MERGED" | |
| fi | |
| done | |
| # final newline | |
| echo >>"$MERGED" | |
| # finalize & syntax-check | |
| cp "$MERGED" "$DEST/main.lua" | |
| if command -v luac &>/dev/null; then | |
| if ! luac -p "$DEST/main.lua"; then | |
| error "Lua syntax error; dumping merged main.lua to MERGE_LOG.txt" | |
| { | |
| echo; echo "=== merged main.lua begin ===" | |
| cat "$DEST/main.lua" | |
| echo "=== merged main.lua end ===" | |
| } >>"$DEST/MERGE_LOG.txt" | |
| exit 1 | |
| fi | |
| fi | |
| # write metadata.xml | |
| cat >"$DEST/metadata.xml" <<EOF | |
| <metadata> | |
| <name>$MOD_NAME</name> | |
| <description>$DESC</description> | |
| <author>$AUTHOR</author> | |
| <version>$VER</version> | |
| <visibility>Public</visibility> | |
| </metadata> | |
| EOF | |
| # write logs & README | |
| cat >"$DEST/MERGE_LOG.txt" <<EOF | |
| Merged on $(date) | |
| Sources: ${VALID[*]##*/} | |
| EOF | |
| cat >"$DEST/README.txt" <<EOF | |
| $MOD_NAME | |
| Merged mods: ${VALID[*]##*/} | |
| EOF | |
| # final summary | |
| echo -e "${GRN}🎉 Merge complete!${NC}" | |
| echo "Total files: $(find "$DEST" -type f | wc -l)" | |
| echo "Mods with conflicts: ${conflict_mods[*]:-None}" | |
| echo "Location: $DEST" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment