#!/bin/sh # ============================================================================= # Mario64 — Render96ex / SM64EX + DynOS + Odyssey Mario's Moveset (OMM) # Linux Installer # # PURPOSE # Installs the Super Mario 64 PC port with Odyssey Mario's Moveset (OMM) # on top of one of two bases (user chooses at runtime): # 1) Render96ex — Render96 HD models + DynOS (tester branch, recommended) # 2) SM64EX — vanilla sm64ex (master) + Render96 HD assets # Both options receive the Render96 HD texture pack + improved 3D models # (DynOS pack v3.25 + OMM-compatible zip) and the OMM patch (Cappy, # captures, roll, dive, cap bounce, etc.). # # USAGE # ./install.sh # One-liner (curl): # curl -fsSL https://raw.githubusercontent.com/zonaro/Mario64/main/install.sh -o install.sh && chmod +x install.sh && ./install.sh # bash -c "$(curl -fsSL https://raw.githubusercontent.com/zonaro/Mario64/main/install.sh)" # # Base selection (one-liner friendly): # - If ENGINE env is set (render96|sm64ex), it is used without prompting. # Aliases: r96/render96ex → render96, ex/sm64/sm64ex → sm64ex # Example: ENGINE=sm64ex ./install.sh or ENGINE=render96 bash -c "$(curl ...)" # - Otherwise the script PROMPTS: 1) Render96ex 2) SM64EX (empty = 1) # - Both bases get Render96 HD textures + improved models. # # ROM handling (one-liner friendly): # - If ROM_ZIP is set and points to a file, it is used. # - Otherwise the script looks for common ROM names in the script dir / PWD. # - If still not found, it PROMPTS for the ROM path (drag & drop supported). # - If the prompt is left empty, it auto-searches ~/Downloads, ~/ROMs, # ~/Games, ~/Documents and other common subfolders (up to 3 levels deep). # # Install dir handling (one-liner friendly): # - If FINAL_DIR env is set, it is used as-is. # - Otherwise the script PROMPTS for the install path. # - If left empty, defaults to ~/Mario64. # - Clone/downloads/compilation always happen in a temp dir (mktemp) # and are deleted afterwards; only the compiled game stays in FINAL_DIR. # # Optional environment overrides (all optional): # ENGINE=render96|sm64ex Base port (also: BASE, R96, SM64EX) # ROM_ZIP=/path/to/rom.zip FINAL_DIR=/path/to/output # BRANCH=tester|master JOBS=8 (default: tester for r96, master for ex) # SKIP_DEPS=1 SKIP_MODELS=1 # FORCE=1 Always re-fetch latest repo + latest OMM patch, # and clean-rebuild if the source changed (or FORCE) # ALLOW_REJECTS=1 Continue even if OMM patch hunks are rejected # (off by default — rejected hunks break the build) # # DEPENDENCIES (auto-installed via the detected package manager) # git, curl, make, gcc/g++, python3, unzip, zip, p7zip (7z), SDL2, GLEW # # OUTPUT (movido para FINAL_DIR após compilar; padrão ~/Mario64) # Game binary : $FINAL_DIR/build/us_pc/sm64.r96x (r96) or sm64.us.f3dex2e (ex) # Launcher : $FINAL_DIR/launch.sh (+ ~/.local/bin/render96-odyssey or sm64ex-odyssey) # Desktop : ~/.local/share/applications/render96-odyssey.desktop (or sm64ex) # Icon : ~/.local/share/icons/render96-odyssey.svg (generated inline) # DynOS packs : $FINAL_DIR/build/us_pc/dynos/packs/Render96/ # OMM packs : $FINAL_DIR/build/us_pc/res/packs/Render96_DynOs_v3.25.zip # (Both bases receive the Render96 HD texture/model packs) # # NOTES # - Você deve possuir uma ROM legalmente dumpada, NÃO MODIFICADA de # Super Mario 64 (US). O build verifica o hash e aborta se for hackeada. # - Clone, downloads e compilação acontecem em pasta temporária (mktemp). # Ao final, apenas o jogo compilado fica em FINAL_DIR; o temporário é apagado. # - PortMaster is NOT used: it targets handheld CFWs (ArkOS, ROCKNIX, muOS, # Knulli...) and has no render96ex port for desktop Linux. This script # builds the same Render96ex code natively instead. # ============================================================================= set -eu # --- Resolve script directory (onde a ROM deve estar) ----------------------- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" if command -v readlink >/dev/null 2>&1; then _REAL="$(readlink -f "$0" 2>/dev/null || echo "$0")" case "$_REAL" in /*) SCRIPT_DIR="$(cd "$(dirname "$_REAL")" && pwd)" ;; esac fi # Default install dir is ~/Mario64 when user leaves the prompt empty DEFAULT_FINAL_DIR="$HOME/Mario64" FINAL_DIR="${FINAL_DIR:-}" # FINAL_BUILD_DIR / FINAL_GAME_BIN are resolved after the install-dir prompt FINAL_BUILD_DIR="" FINAL_GAME_BIN="" # --- Find local ROM (procura nomes comuns no diretório do script) ---------- find_local_rom() { # 1) nomes exatos mais comuns (prioridade) — no SCRIPT_DIR e no PWD (para one-liner) for _base in "$SCRIPT_DIR" "$PWD"; do [ -d "$_base" ] || continue for _n in \ "baserom.us.z64" \ "baserom.z64" \ "Super Mario 64 (USA).z64" \ "Super Mario 64 (U) [!].z64" \ "Super Mario 64.z64" \ "sm64.z64" \ "mario64.z64" \ "Super Mario 64 (USA).n64" \ "Super Mario 64.n64" \ "Super Mario 64 (USA).v64" \ "Super Mario 64.zip" \ "Super Mario 64 (USA).zip" \ "sm64.zip" \ "mario64.zip" \ "baserom.us.n64" \ "baserom.us.v64"; do if [ -f "$_base/$_n" ]; then echo "$_base/$_n" return 0 fi done done # 2) fallback: qualquer .z64/.n64/.v64/.zip no diretório do script ou PWD for _base in "$SCRIPT_DIR" "$PWD"; do [ -d "$_base" ] || continue for _f in "$_base"/*.z64 "$_base"/*.Z64 \ "$_base"/*.n64 "$_base"/*.N64 \ "$_base"/*.v64 "$_base"/*.V64 \ "$_base"/*.zip "$_base"/*.ZIP; do [ -f "$_f" ] || continue case "$_f" in *.sh) continue ;; esac echo "$_f" return 0 done done return 1 } # --- Search ROM in common user subfolders (fallback when prompt is empty) --- search_home_rom() { # Procura em pastas comuns do usuário, até 3 níveis de profundidade for _base in \ "$HOME/Downloads" "$HOME/Documentos" "$HOME/Documents" \ "$HOME/ROMs" "$HOME/roms" "$HOME/Games" "$HOME/games" \ "$HOME/Jogos" "$HOME/jogos" "$HOME/Downloads/ROMs" "$HOME/Downloads/roms" \ "$HOME"; do [ -d "$_base" ] || continue _found=$(find "$_base" -maxdepth 3 -type f \( \ -iname "*mario*64*.z64" -o -iname "*mario*64*.n64" -o -iname "*mario*64*.v64" -o -iname "*mario*64*.zip" \ -o -iname "baserom*.z64" -o -iname "baserom*.n64" -o -iname "baserom*.v64" \ -o -iname "sm64*.z64" -o -iname "sm64*.zip" \) -print -quit 2>/dev/null) if [ -n "${_found:-}" ] && [ -f "$_found" ]; then echo "$_found" return 0 fi done # Fallback mais amplo: qualquer .z64 com "mario" no nome até 4 níveis em $HOME _found=$(find "$HOME" -maxdepth 4 -type f -iname "*.z64" 2>/dev/null | grep -i mario | head -n 1) if [ -n "${_found:-}" ] && [ -f "$_found" ]; then echo "$_found" return 0 fi return 1 } # --- Prompt interativo para ROM (one-liner friendly) ------------------------ prompt_for_rom() { printf '\n' printf '\033[1;33m[INPUT]\033[0m Super Mario 64 (US) ROM is required to build.\n' printf ' Accepted: .z64 / .n64 / .v64 / .zip (unmodified US ROM)\n' printf ' Tip: drag & drop the file here or paste the full path.\n' printf ' Leave empty to auto-search in your home folders.\n' _input="" if [ -t 0 ]; then printf 'ROM path (empty = auto-search): ' IFS= read -r _input || true elif [ -e /dev/tty ]; then printf 'ROM path (empty = auto-search): ' > /dev/tty IFS= read -r _input < /dev/tty || true else _input="" fi # trim + remove surrounding quotes + \r _input=$(printf '%s' "$_input" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e "s/^['\"]//" -e "s/['\"]$//" | tr -d '\r') # expand ~ case "$_input" in "~/"*) _input="$HOME/${_input#\~/}" ;; "~") _input="$HOME" ;; esac if [ -n "$_input" ]; then if [ -f "$_input" ]; then ROM_ZIP="$_input" ok "ROM selected: $ROM_ZIP" return 0 elif [ -d "$_input" ]; then _found=$(find "$_input" -maxdepth 2 -type f \( -iname "*.z64" -o -iname "*.n64" -o -iname "*.v64" -o -iname "*.zip" \) -print -quit 2>/dev/null) if [ -n "${_found:-}" ] && [ -f "$_found" ]; then ROM_ZIP="$_found" ok "ROM found in directory: $ROM_ZIP" return 0 else warn "No ROM found in directory: $_input" fi else warn "File not found: $_input" fi fi info "Searching for ROM in home folders..." if _FOUND_HOME="$(search_home_rom)"; then ROM_ZIP="$_FOUND_HOME" ok "ROM auto-found: $ROM_ZIP" return 0 fi warn "No ROM found automatically." return 1 } ensure_rom() { if [ -n "${ROM_ZIP:-}" ] && [ -f "$ROM_ZIP" ]; then ok "ROM found: $ROM_ZIP" return 0 fi if [ -n "${ROM_ZIP:-}" ] && [ ! -f "$ROM_ZIP" ]; then warn "ROM_ZIP set but not found: $ROM_ZIP" ROM_ZIP="" fi if _FOUND_ROM="$(find_local_rom)"; then ROM_ZIP="$_FOUND_ROM" ok "ROM auto-detected: $ROM_ZIP" return 0 fi # Precisa pedir ao usuário (funciona também no one-liner via /dev/tty) if prompt_for_rom; then return 0 fi die "ROM not found. Place your Super Mario 64 (US) ROM (.z64/.n64/.v64/.zip) in the script folder or enter its path. You can also set ROM_ZIP=/path/to/rom.zip" } if [ -z "${ROM_ZIP:-}" ]; then if _FOUND_ROM="$(find_local_rom)"; then ROM_ZIP="$_FOUND_ROM" else ROM_ZIP="" fi else case "$ROM_ZIP" in "~/"*) ROM_ZIP="$HOME/${ROM_ZIP#\~/}" ;; "~") ROM_ZIP="$HOME" ;; esac if [ ! -f "$ROM_ZIP" ]; then ROM_ZIP="" fi fi # --- Prompt for install directory (one-liner friendly) ---------------------- resolve_final_dir() { # If FINAL_DIR was set via env and is non-empty, use it (expand ~) if [ -n "${FINAL_DIR:-}" ]; then case "$FINAL_DIR" in "~/"*) FINAL_DIR="$HOME/${FINAL_DIR#\~/}" ;; "~") FINAL_DIR="$HOME" ;; esac FINAL_BUILD_DIR="$FINAL_DIR/build/us_pc" FINAL_GAME_BIN="$FINAL_BUILD_DIR/sm64.r96x" return 0 fi printf '\n' printf '\033[1;33m[INPUT]\033[0m Where should the compiled game be installed?\n' printf ' Leave empty for default: %s\n' "$DEFAULT_FINAL_DIR" printf ' Tip: paste the full path or drag & drop a folder.\n' _idir="" if [ -t 0 ]; then printf 'Install path (empty = %s): ' "$DEFAULT_FINAL_DIR" IFS= read -r _idir || true elif [ -e /dev/tty ]; then printf 'Install path (empty = %s): ' "$DEFAULT_FINAL_DIR" > /dev/tty IFS= read -r _idir < /dev/tty || true else _idir="" fi _idir=$(printf '%s' "$_idir" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e "s/^['\"]//" -e "s/['\"]$//" | tr -d '\r') case "$_idir" in "~/"*) _idir="$HOME/${_idir#\~/}" ;; "~") _idir="$HOME" ;; esac if [ -z "$_idir" ]; then FINAL_DIR="$DEFAULT_FINAL_DIR" else FINAL_DIR="$_idir" fi FINAL_BUILD_DIR="$FINAL_DIR/build/us_pc" FINAL_GAME_BIN="$FINAL_BUILD_DIR/sm64.r96x" info "Install directory: $FINAL_DIR" } # --- Engine selection (render96 vs sm64ex) -------------------------------- # ENGINE env: render96 (default) or sm64ex. Aliases accepted. # Also honors BASE / R96 / SM64EX for convenience. _ENGINE_RAW="${ENGINE:-${BASE:-${R96:-${SM64EX:-}}}}" ENGINE="" if [ -n "${_ENGINE_RAW:-}" ]; then case "$(printf '%s' "$_ENGINE_RAW" | tr '[:upper:]' '[:lower:]')" in render96|r96|render96ex|96) ENGINE="render96" ;; sm64ex|sm64|ex|vanilla) ENGINE="sm64ex" ;; *) warn "ENGINE='$_ENGINE_RAW' desconhecido — use render96 ou sm64ex. Usando render96."; ENGINE="render96" ;; esac fi resolve_engine() { if [ -n "${ENGINE:-}" ]; then return 0 fi printf '\n' printf '\033[1;34m[INFO]\033[0m Escolha a base do port:\n' printf ' \033[1m1)\033[0m Render96ex (recomendado) — modelos HD + DynOS + OMM\n' printf ' \033[1m2)\033[0m SM64EX (vanilla) — sm64ex + texturas/modelos HD Render96 + OMM\n' printf ' Ambas as opções recebem o pacote de texturas e modelos 3D melhorados do Render96.\n' _choice="" if [ -t 0 ]; then printf 'Escolha [1/2] (vazio = 1): ' IFS= read -r _choice || true elif [ -e /dev/tty ]; then printf 'Escolha [1/2] (vazio = 1): ' > /dev/tty IFS= read -r _choice < /dev/tty || true else _choice="" fi _choice=$(printf '%s' "$_choice" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') case "$_choice" in 2|sm64ex|sm64|ex|vanilla) ENGINE="sm64ex" ;; *) ENGINE="render96" ;; esac info "Base selecionada: $ENGINE" } # --- Temporary build directory (clone + downloads + compilação) ------------- BUILD_TMP="$(mktemp -d -t mario64_build_XXXXXX)" # shellcheck disable=SC2329 cleanup_tmp() { rm -rf "$BUILD_TMP"; } trap cleanup_tmp EXIT INT TERM # INSTALL_DIR agora é dentro do temporário; resultado final vai para FINAL_DIR INSTALL_DIR="$BUILD_TMP/src" JOBS="${JOBS:-$(nproc 2>/dev/null || echo 4)}" # Repo/branch por engine (BRANCH pode sobrescrever) REPO_URL_RENDER96="https://github.com/Render96/Render96ex.git" REPO_URL_SM64EX="https://github.com/sm64pc/sm64ex.git" BRANCH_DEFAULT_RENDER96="tester" BRANCH_DEFAULT_SM64EX="master" OMM_PATCH_URL="https://raw.githubusercontent.com/PeachyPeachSM64/sm64ex-omm/refs/heads/patch/omm.patch" DYNOS_7Z_URL="https://github.com/Render96/ModelPack/releases/download/3.25/Render96_DynOs_v3.25.7z" DYNOS_ZIP_URL="https://github.com/PeachyPeachSM64/sm64ex-omm-resources/raw/packs/Render96_v3.25/Render96_DynOs_v3.25.zip" # Fallback: se BRANCH foi passado via env, respeita; senão usa default por engine (resolvido após resolve_engine) BRANCH="${BRANCH:-}" BASEROM="$INSTALL_DIR/baserom.us.z64" BUILD_DIR="$INSTALL_DIR/build/us_pc" GAME_BIN="$BUILD_DIR/sm64.r96x" # FINAL_BUILD_DIR / FINAL_GAME_BIN are set by resolve_final_dir() (default ~/Mario64) # Expected SHA1 of the unmodified Super Mario 64 (US) .z64 ROM ROM_SHA1="9bef1128717f958171a4afac3ed78ee2bb4e86ce" # --- Helpers ---------------------------------------------------------------- info() { printf '\033[1;34m[INFO]\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m[ OK ]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; } die() { printf '\033[1;31m[FAIL]\033[0m %s\n' "$*" >&2; exit 1; } need_cmd() { if ! command -v "$1" >/dev/null 2>&1; then die "Required command '$1' not found. Install it and re-run." fi } run_priv() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else die "Root privileges required but sudo is missing. Run: $*" fi } # --- Step 0: PortMaster sanity check (informational) ------------------------- check_portmaster() { if command -v portmaster >/dev/null 2>&1 || [ -d /opt/portmaster ] || [ -d "$HOME/ports" ]; then info "PortMaster detected on this system, but it is not used by this" info "installer: PortMaster targets handheld CFWs and has no render96ex" info "port for desktop Linux. We build Render96ex natively instead." else info "PortMaster not installed — not needed. Building Render96ex natively." fi } # --- Step 1: Detect package manager ------------------------------------------ detect_pkg_mgr() { if command -v pacman >/dev/null 2>&1; then PM="pacman" elif command -v apt-get >/dev/null 2>&1; then PM="apt" elif command -v dnf >/dev/null 2>&1; then PM="dnf" elif command -v zypper >/dev/null 2>&1; then PM="zypper" else die "Unsupported package manager. Install the build deps manually." fi info "Package manager detected: $PM" } # --- Step 2: Install build dependencies -------------------------------------- install_deps() { need_cmd git need_cmd curl need_cmd make need_cmd gcc need_cmd python3 need_cmd unzip if command -v 7z >/dev/null 2>&1 && command -v zip >/dev/null 2>&1; then ok "Build tools already present" return 0 fi info "Installing missing build dependencies via $PM (may ask for your password)..." case "$PM" in pacman) run_priv pacman -S --needed --noconfirm \ base-devel git curl python sdl2 glew zip unzip p7zip atool ;; apt) run_priv apt-get update -y run_priv apt-get install -y \ build-essential git curl python3 libsdl2-dev libglew-dev \ zlib1g-dev zip unzip p7zip-full atool ;; dnf) run_priv dnf install -y \ git curl make gcc gcc-c++ python3 SDL2-devel glew-devel \ zlib-devel zip unzip p7zip p7zip-plugins atool ;; zypper) run_priv zypper install -y \ git curl make gcc gcc-c++ python3 SDL2-devel glew-devel \ zlib-devel zip unzip p7zip atool ;; esac for c in 7z zip; do command -v "$c" >/dev/null 2>&1 || warn "'$c' still missing — model pack install may fail" done } # --- Step 3: Extract the ROM ------------------------------------------------- convert_rom() { python3 - "$1" "$2" <<'PYEOF' import sys src, dst = sys.argv[1], sys.argv[2] data = bytearray(open(src, 'rb').read()) magic = bytes(data[:4]) if magic == b'\x80\x37\x12\x40': pass elif magic == b'\x40\x12\x37\x80': for i in range(0, len(data), 4): data[i:i+4] = data[i:i+4][::-1] elif magic == b'\x37\x80\x40\x12': for i in range(0, len(data), 2): data[i], data[i+1] = data[i+1], data[i] else: sys.exit("Unknown ROM byte order (not a valid N64 ROM)") open(dst, 'wb').write(data) PYEOF } extract_rom() { [ -f "$BASEROM" ] && { ok "ROM already present: $BASEROM"; return 0; } [ -f "$ROM_ZIP" ] || die "ROM não encontrada: $ROM_ZIP\nColoque a ROM no mesmo diretório do script. Nomes aceitos: baserom.us.z64, Super Mario 64 (USA).z64, sm64.z64, mario64.z64, .n64/.v64/.zip, etc. Ou defina ROM_ZIP=/caminho/para/rom.zip" mkdir -p "$INSTALL_DIR" info "Extraindo ROM de: $ROM_ZIP" # Se já é uma ROM crua (.z64/.n64/.v64), copia/converte direto sem unzip case "$ROM_ZIP" in *.z64|*.Z64) cp -f "$ROM_ZIP" "$BASEROM" ;; *.n64|*.N64|*.v64|*.V64) info "Convertendo $(basename "$ROM_ZIP") para .z64 byte order" convert_rom "$ROM_ZIP" "$BASEROM" ;; *.zip|*.ZIP) TMPDIR_ROM="$BUILD_TMP/rom_extract" mkdir -p "$TMPDIR_ROM" unzip -o -j "$ROM_ZIP" -d "$TMPDIR_ROM" >/dev/null ROM_FILE="" for f in "$TMPDIR_ROM"/*; do [ -f "$f" ] || continue case "$f" in *.z64|*.Z64|*.n64|*.N64|*.v64|*.V64) ROM_FILE="$f" ;; esac done [ -n "$ROM_FILE" ] || die "Nenhuma ROM .z64/.n64/.v64 encontrada dentro de $ROM_ZIP" case "$ROM_FILE" in *.z64|*.Z64) cp -f "$ROM_FILE" "$BASEROM" ;; *) info "Convertendo $(basename "$ROM_FILE") para .z64 byte order" convert_rom "$ROM_FILE" "$BASEROM" ;; esac rm -rf "$TMPDIR_ROM" ;; *) # tenta tratar como ROM crua por conteúdo info "Formato de ROM desconhecido pela extensão, tentando copiar direto..." cp -f "$ROM_ZIP" "$BASEROM" ;; esac SIZE=$(wc -c < "$BASEROM") [ "$SIZE" = "8388608" ] || warn "ROM size is $SIZE (expected 8388608) — may be a hack" if command -v sha1sum >/dev/null 2>&1; then HASH=$(sha1sum "$BASEROM" | awk '{print $1}') if [ "$HASH" = "$ROM_SHA1" ]; then ok "ROM verified: unmodified Super Mario 64 (US)" else warn "ROM hash $HASH does not match the expected US ROM." warn "If the build fails with a hash error, your ROM is extended/hacked." fi fi ok "ROM ready: $BASEROM" } # --- Step 4: Clone base repo (Render96ex or SM64EX) -------------------------- resolve_repo() { if [ "$ENGINE" = "sm64ex" ]; then REPO_URL="$REPO_URL_SM64EX" BRANCH="${BRANCH:-$BRANCH_DEFAULT_SM64EX}" else REPO_URL="$REPO_URL_RENDER96" BRANCH="${BRANCH:-$BRANCH_DEFAULT_RENDER96}" fi } clone_repo() { resolve_repo _label="Render96ex" [ "$ENGINE" = "sm64ex" ] && _label="SM64EX" if [ -d "$INSTALL_DIR/.git" ]; then info "Repository present — updating to latest '$BRANCH' ($_label)..." cd "$INSTALL_DIR" if git fetch --depth 1 origin "$BRANCH" 2>/dev/null && \ git checkout -f "$BRANCH" 2>/dev/null && \ git reset --hard "origin/$BRANCH" 2>/dev/null; then ok "Repository updated to latest '$BRANCH'" else warn "Update failed — re-cloning fresh (this re-downloads the repo)..." cd / rm -rf "$INSTALL_DIR" git clone -c core.autocrlf=false -c core.eol=lf \ --single-branch --depth 1 -b "$BRANCH" "$REPO_URL" "$INSTALL_DIR" fi rm -f "$INSTALL_DIR/omm.patch" find "$INSTALL_DIR" -maxdepth 1 -name '*.rej' -delete 2>/dev/null || true return 0 fi info "Cloning $_label ($BRANCH branch)..." git clone -c core.autocrlf=false -c core.eol=lf \ --single-branch --depth 1 -b "$BRANCH" "$REPO_URL" "$INSTALL_DIR" ok "Repository cloned ($_label)" } # --- Step 5: Apply the OMM (Odyssey) patch ----------------------------------- apply_omm_patch() { cd "$INSTALL_DIR" # Always fetch the latest OMM patch so we never build against a stale one. info "Downloading latest Odyssey Mario's Moveset patch (~40 MB)..." rm -f omm.patch curl -L --fail -o omm.patch "$OMM_PATCH_URL" # Make sure the tree is clean before applying (revert any old patch / rejects). git checkout -- . 2>/dev/null || true find "$INSTALL_DIR" -name '*.rej' -delete 2>/dev/null || true info "Applying OMM patch..." # --reject applies the good hunks and writes a .rej file for every hunk that # could not be applied. We then scan for those .rej files and surface them. git apply --reject omm.patch 2>apply.log || true # The OMM patch's Makefile hunk is known to be rejected on current Render96ex # (the BASEPACK_PATH dependency line). This rejection is benign — the build # still works — so we auto-fix it instead of aborting the whole install. if [ -f "$INSTALL_DIR/Makefile.rej" ] && grep -q "BASEPACK_PATH" "$INSTALL_DIR/Makefile.rej"; then info "Auto-fixing benign Makefile rejection (BASEPACK_PATH)..." sed -i 's|BASEPACK_PATH): $(BASEPACK_LST)|BASEPACK_PATH):|' "$INSTALL_DIR/Makefile" rm -f "$INSTALL_DIR/Makefile.rej" ok "Makefile rejection resolved" fi REJECTS=$(find "$INSTALL_DIR" -name '*.rej' -print 2>/dev/null) if [ -n "$REJECTS" ]; then warn "==================================================================" warn "OMM patch applied, BUT SOME HUNKS WERE REJECTED." warn "A rejected hunk in star/behaviour/save-file code is the most common" warn "cause of the 'crash when collecting a star' bug in Render96 + OMM." warn "Rejected files:" echo "$REJECTS" | while read -r f; do warn " - $f"; done warn "Full apply log: $INSTALL_DIR/apply.log" warn "==================================================================" if [ "${ALLOW_REJECTS:-0}" = "1" ]; then warn "ALLOW_REJECTS=1 set — continuing anyway (build may be broken)." else die "Aborting: rejected hunks mean the build is likely broken. Fix the conflicts (or re-pin Render96ex 'tester' to a commit that matches the patch), then re-run." fi else ok "OMM patch applied cleanly (no rejected hunks)" fi # Record a signature of the current source tree for smart rebuilds. SRC_SIG="$(git -C "$INSTALL_DIR" rev-parse HEAD 2>/dev/null):$(sha256sum "$INSTALL_DIR/omm.patch" 2>/dev/null | cut -d' ' -f1)" echo "$SRC_SIG" > "$INSTALL_DIR/.omm_src_sig" } # --- Step 5b: Apply defensive OMM fixes (NULL-deref + index-bound) ------------ # Guards against the known "crash when collecting a star" class of bugs in OMM # (e.g. sm64ex-omm issue #38). Applied AFTER the OMM patch so it survives a # re-patch. Idempotent: skipped if already applied. apply_omm_fixes() { cd "$INSTALL_DIR" local f1="data/omm/object/omm_obj_star_celebration.c" local f2="data/omm/object/omm_stars.c" if grep -q "marioGfxY" "$f1" 2>/dev/null; then info "OMM defensive fixes already applied — skipping" return 0 fi info "Applying OMM defensive fixes (NULL-deref guard + color index clamp)..." python3 - "$f1" "$f2" <<'PY' || die "Failed to apply OMM defensive fixes" import sys f1, f2 = sys.argv[1], sys.argv[2] # 1) NULL-deref guard on m->marioObj in the star celebration update s1 = open(f1).read() old1 = " o->oPosY = o->oHomeY + o->oCelebStarHeight + (m->marioObj->oGfxPos[1] - m->pos[1]);" new1 = " f32 marioGfxY = (m->marioObj ? m->marioObj->oGfxPos[1] : m->pos[1]);\n o->oPosY = o->oHomeY + o->oCelebStarHeight + (marioGfxY - m->pos[1]);" assert old1 in s1, "star celebration pattern not found in %s" % f1 s1 = s1.replace(old1, new1) open(f1, 'w').write(s1) # 2) Clamp the star-color array index so it can never read out of bounds s2 = open(f2).read() old2 = " return sOmmStarsColors[clamp_s(courseNum, 0, 16) + OMM_STAR_COLOR_OFFSET(modeIndex)];" new2 = " s32 idx = clamp_s(courseNum, 0, 16) + OMM_STAR_COLOR_OFFSET(modeIndex);\n return sOmmStarsColors[clamp_s(idx, 0, OMM_STAR_COLOR_COUNT - 1)];" assert old2 in s2, "stars color pattern not found in %s" % f2 s2 = s2.replace(old2, new2) open(f2, 'w').write(s2) print("OMM defensive fixes applied") PY ok "OMM defensive fixes applied" } # --- Step 6: Build ----------------------------------------------------------- build_game() { local NEED_BUILD=0 if [ ! -x "$GAME_BIN" ]; then NEED_BUILD=1; fi if [ "${FORCE:-0}" = "1" ]; then NEED_BUILD=1; fi local CUR_SIG="$(cat "$INSTALL_DIR/.omm_src_sig" 2>/dev/null)" local BUILT_SIG="$(cat "$BUILD_DIR/.omm_built_sig" 2>/dev/null)" if [ "$CUR_SIG" != "$BUILT_SIG" ]; then NEED_BUILD=1; fi if [ "$NEED_BUILD" = "0" ]; then ok "Game already built and up to date: $GAME_BIN" return 0 fi if [ "${FORCE:-0}" = "1" ] || [ "$CUR_SIG" != "$BUILT_SIG" ]; then info "Source changed (or FORCE=1) — cleaning build directory for a fresh build..." rm -rf "$BUILD_DIR" fi cd "$INSTALL_DIR" [ -f baserom.us.z64 ] || die "baserom.us.z64 missing — ROM extraction failed" chmod 755 extract_assets.py 2>/dev/null || true # SM64EX usa binário sm64.us.f3dex2e; Render96ex usa sm64.r96x — detecta após build info "Building ($ENGINE) with $JOBS parallel jobs (this takes 10–30 minutes)..." make -j"$JOBS" RENDER_API=GL WINDOW_API=SDL2 AUDIO_API=SDL2 \ CONTROLLER_API=SDL2 GRUCODE=f3dex2e # Resolve GAME_BIN real (pode ser sm64.r96x ou sm64.us.f3dex2e) if [ ! -x "$GAME_BIN" ]; then for _cand in "$BUILD_DIR/sm64.r96x" "$BUILD_DIR/sm64.us.f3dex2e" "$BUILD_DIR/sm64"; do if [ -x "$_cand" ]; then GAME_BIN="$_cand"; break; fi done fi [ -x "$GAME_BIN" ] || die "Build finished but no binary was produced in $BUILD_DIR" echo "$CUR_SIG" > "$BUILD_DIR/.omm_built_sig" ok "Build complete: $GAME_BIN" } # --- Step 7: Install Render96 HD packs (both engines) ------------------------ install_model_packs() { [ "${SKIP_MODELS:-0}" = "1" ] && { info "SKIP_MODELS=1 — skipping model packs"; return 0; } info "Installing Render96 HD texture/model packs (both engines use them)..." if [ "${FORCE:-0}" = "1" ]; then info "FORCE=1: removing existing model packs to re-install the latest..." rm -rf "$BUILD_DIR/dynos/packs/Render96_DynOS_v3.25" "$BUILD_DIR/dynos/packs/Render96" rm -f "$BUILD_DIR/res/packs/Render96_DynOs_v3.25.zip" fi DYNOS_PACKS="$BUILD_DIR/dynos/packs" if [ -d "$DYNOS_PACKS/Render96_DynOS_v3.25" ] || [ -d "$DYNOS_PACKS/Render96" ]; then ok "DynOS pack already installed" else info "Downloading Render96 DynOS model pack (~93 MB)..." curl -L --fail -o "$BUILD_TMP/Render96_DynOs_v3.25.7z" "$DYNOS_7Z_URL" mkdir -p "$DYNOS_PACKS" info "Extracting DynOS pack..." 7z x -y "$BUILD_TMP/Render96_DynOs_v3.25.7z" -o"$DYNOS_PACKS" >/dev/null rm -f "$BUILD_TMP/Render96_DynOs_v3.25.7z" if [ -d "$DYNOS_PACKS/Render96_DynOS_v3.25" ] || [ -d "$DYNOS_PACKS/Render96" ]; then ok "DynOS pack installed (enable in-game: Pause -> DynOS -> Packs)" else # SM64EX pode não ter dynos/ — ainda instala o zip OMM abaixo warn "DynOS pack not applicable for this base (will still install OMM Render96 zip)" fi fi if [ -f "$BUILD_DIR/res/packs/Render96_DynOs_v3.25.zip" ]; then ok "OMM Render96 pack already installed" else info "Downloading OMM-compatible Render96 pack (~61 MB)..." mkdir -p "$BUILD_DIR/res/packs" curl -L --fail -o "$BUILD_DIR/res/packs/Render96_DynOs_v3.25.zip" "$DYNOS_ZIP_URL" ok "OMM Render96 pack installed (enable in-game: Options -> Model Packs)" fi } # --- Step 8: Move jogo compilado para FINAL_DIR + limpeza -------------------- move_to_final() { [ -x "$GAME_BIN" ] || die "Binário não encontrado em $GAME_BIN — build falhou" [ -n "${FINAL_DIR:-}" ] || die "FINAL_DIR não definido (resolve_final_dir não foi chamado)" # Atualiza FINAL_GAME_BIN para o binário real (pode ter mudado no build_game) FINAL_GAME_BIN="$FINAL_BUILD_DIR/$(basename "$GAME_BIN")" info "Movendo jogo compilado para: $FINAL_DIR" mkdir -p "$FINAL_DIR" rm -rf "$FINAL_BUILD_DIR" mkdir -p "$(dirname "$FINAL_BUILD_DIR")" cp -a "$BUILD_DIR" "$FINAL_BUILD_DIR" for _extra in README.md; do [ -f "$INSTALL_DIR/$_extra" ] && cp -f "$INSTALL_DIR/$_extra" "$FINAL_DIR/" 2>/dev/null || true done [ -x "$FINAL_GAME_BIN" ] || die "Falha ao mover jogo para $FINAL_GAME_BIN" ok "Jogo movido para: $FINAL_GAME_BIN" info "Limpando downloads e pasta temporária: $BUILD_TMP" # Remove downloads intermediários dentro do temporário (omm.patch, 7z, etc já foram limpos) rm -f "$BUILD_TMP"/*.7z "$BUILD_TMP"/*.zip 2>/dev/null || true rm -f "$INSTALL_DIR/omm.patch" "$INSTALL_DIR/apply.log" 2>/dev/null || true } # --- Step 9: Launcher + desktop entry (with inline SVG icon) ----------------- create_launcher() { [ -n "${FINAL_DIR:-}" ] || die "FINAL_DIR não definido" _bin_name="$(basename "$GAME_BIN")" cat > "$FINAL_DIR/launch.sh" </dev/null || true ok "Launcher: $FINAL_DIR/launch.sh (também em ~/.local/bin/sm64ex-odyssey)" else ln -sf "$FINAL_DIR/launch.sh" "$HOME/.local/bin/render96-odyssey" ok "Launcher: $FINAL_DIR/launch.sh (também em ~/.local/bin/render96-odyssey)" fi } create_desktop_entry() { mkdir -p "$HOME/.local/share/applications" "$HOME/.local/share/icons" if [ "$ENGINE" = "sm64ex" ]; then _app_id="sm64ex-odyssey" _app_name="Super Mario 64 - SM64EX (Odyssey + Render96 HD)" _app_comment="SM64EX with Odyssey Mario's Moveset + Render96 HD textures/models" _icon_text="EX" else _app_id="render96-odyssey" _app_name="Super Mario 64 - Render96 EX (Odyssey)" _app_comment="Render96 EX with DynOS and Odyssey Mario's Moveset + HD textures" _icon_text="96" fi ICON="$HOME/.local/share/icons/${_app_id}.svg" cat > "$ICON" < $_icon_text EOF cat > "$HOME/.local/share/applications/${_app_id}.desktop" <}" info "Build jobs : $JOBS" echo check_portmaster detect_pkg_mgr [ "${SKIP_DEPS:-0}" = "1" ] && info "SKIP_DEPS=1 — skipping dependency install" || install_deps ensure_rom extract_rom clone_repo apply_omm_patch apply_omm_fixes build_game install_model_packs move_to_final create_launcher create_desktop_entry # Limpeza final explícita da pasta temporária (além do trap) info "Limpando pasta temporária..." rm -rf "$BUILD_TMP" trap - EXIT INT TERM echo ok "Instalação completa! ($ENGINE)" echo info "Jogo compilado em: $FINAL_GAME_BIN" info "Execute com: $FINAL_DIR/launch.sh" if [ "$ENGINE" = "sm64ex" ]; then info "ou pelo menu: Super Mario 64 - SM64EX (Odyssey + Render96 HD)" info "ou via binário: ~/.local/bin/sm64ex-odyssey" else info "ou pelo menu: Super Mario 64 - Render96 EX (Odyssey)" info "ou via binário: ~/.local/bin/render96-odyssey" fi info "Pasta temporária limpa: $BUILD_TMP (removida)" echo info "Dicas de primeiro uso:" info " - Texturas/modelos HD Render96: já instalados em ambos os modos" info " - Render96ex: Pause -> DynOS -> Packs -> enable 'Render96_DynOS_v3.25'" info " - SM64EX/OMM: Options -> Model Packs -> Render96_DynOs_v3.25" info " - Movimentos Odyssey : já ativos (Cappy, capturas, rolamento, mergulho...)" info " - Controles : Options -> Controls (teclado/controle)" } main "$@"