diff --git a/.gitignore b/.gitignore index 5eee554..8ab06e1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # But not these scripts and the .gitignore file itself !.gitignore !README.md +!CLAUDE.md !install-minecraft-splitscreen.sh !minecraftSplitscreen.sh !add-to-steam.py diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2f9342d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,408 @@ +# CLAUDE.md - AI Assistant Guide for MinecraftSplitscreenSteamdeck + +This document provides essential context for AI assistants working on this codebase. + +## Project Overview + +**Minecraft Splitscreen Steam Deck & Linux Installer** - An automated installer for setting up splitscreen Minecraft (1-4 players) on Steam Deck and Linux systems. + +**Version:** 2.0.0 +**Repository:** https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +**License:** MIT + +### Core Concept: Hybrid Launcher Approach + +The project uses two launchers strategically: +- **PrismLauncher**: For CLI-based automated instance creation (has excellent CLI but requires Microsoft account) +- **PollyMC**: For gameplay (no license verification, offline-friendly) + +After successful setup, PrismLauncher files are cleaned up, leaving only PollyMC for gameplay. + +## Repository Structure + +``` +/ +├── install-minecraft-splitscreen.sh # Main entry point (386 lines) +├── add-to-steam.py # Python script for Steam integration +├── accounts.json # Pre-configured offline accounts (P1-P4) +├── token.enc # Encrypted CurseForge API token +├── README.md # User documentation +├── .github/workflows/release.yml # GitHub Actions release workflow +└── modules/ # 14 specialized bash modules + ├── version_info.sh # Version constants + ├── utilities.sh # Print functions, system detection + ├── path_configuration.sh # CRITICAL: Centralized path management + ├── launcher_setup.sh # PrismLauncher detection/installation + ├── launcher_script_generator.sh # Generates minecraftSplitscreen.sh + ├── java_management.sh # Java auto-detection/installation + ├── version_management.sh # Minecraft version selection + ├── lwjgl_management.sh # LWJGL version detection + ├── mod_management.sh # Mod compatibility (largest module, ~1900 lines) + ├── instance_creation.sh # Creates 4 Minecraft instances + ├── pollymc_setup.sh # PollyMC launcher setup + ├── steam_integration.sh # Steam library integration + ├── desktop_launcher.sh # Desktop .desktop file creation + └── main_workflow.sh # Main orchestration (~1300 lines) +``` + +## Key Architectural Concepts + +### Path Configuration (CRITICAL) + +`modules/path_configuration.sh` is the **single source of truth** for all paths. It manages two launcher configurations: + +```bash +# CREATION launcher (PrismLauncher) - for CLI instance creation +CREATION_DATA_DIR, CREATION_INSTANCES_DIR, CREATION_EXECUTABLE + +# ACTIVE launcher (PollyMC) - for gameplay +ACTIVE_DATA_DIR, ACTIVE_INSTANCES_DIR, ACTIVE_EXECUTABLE, ACTIVE_LAUNCHER_SCRIPT +``` + +**Never hardcode paths.** Always use these variables. + +### Module Loading Order + +Modules are sourced in dependency order in `install-minecraft-splitscreen.sh`: +1. version_info.sh +2. utilities.sh +3. path_configuration.sh (must be early - other modules depend on it) +4. All other modules... +5. main_workflow.sh (last - orchestrates everything) + +### Variable Naming Conventions + +```bash +UPPERCASE # Global constants and exported variables +lowercase # Local variables and functions +ACTIVE_* # Related to gameplay launcher (PollyMC) +CREATION_* # Related to instance creation launcher (PrismLauncher) +PRISM_* # PrismLauncher-specific +POLLYMC_* # PollyMC-specific +``` + +### Function Naming Patterns + +```bash +check_*() # Validation functions +detect_*() # Detection/discovery functions +setup_*() # Setup/configuration functions +get_*() # Getter functions (return values) +handle_*() # Error/event handlers +download_*() # Download operations +install_*() # Installation functions +create_*() # Creation functions +cleanup_*() # Cleanup functions +``` + +## Code Conventions + +### Bash Standards + +```bash +set -euo pipefail # Always at script start (exit on error, undefined vars, pipe failures) +readonly VAR # For immutable constants +local var # For function-scoped variables +[[ ]] # For conditionals (not [ ]) +$() # For command substitution (not backticks) +``` + +### Documentation Standard (JSDoc-style) + +All modules use this header format: + +```bash +#!/usr/bin/env bash +# shellcheck disable=SC2034 # (if needed) +# +# @file module_name.sh +# @version X.Y.Z +# @date YYYY-MM-DD +# @author Author Name +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Brief description of module purpose. +# +# @dependencies +# - dependency1 +# - dependency2 +# +# @exports +# - VARIABLE1 +# - function1() +``` + +### Print Functions (from utilities.sh) + +```bash +print_header "Section Title" # Blue header with borders +print_success "Success message" # Green with checkmark +print_warning "Warning message" # Yellow with warning symbol +print_error "Error message" # Red with X +print_info "Info message" # Cyan with info symbol +print_progress "Progress..." # Progress indicator +``` + +### Error Handling Pattern + +```bash +# Non-fatal errors with fallback +if ! some_operation; then + print_warning "Operation failed, trying fallback..." + fallback_operation || { + print_error "Fallback also failed" + return 1 + } +fi +``` + +## External APIs Used + +| API | Purpose | Auth Required | +|-----|---------|---------------| +| Modrinth API | Mod versions, compatibility | No | +| CurseForge API | Alternative mod source | Yes (token.enc) | +| Fabric Meta API | Fabric Loader, LWJGL versions | No | +| Mojang API | Minecraft versions, Java requirements | No | +| SteamGridDB API | Custom artwork | No | +| GitHub API | PrismLauncher/PollyMC releases | No | + +## Development Commands + +### Running the Installer + +```bash +# Local development (uses git remote to detect repo) +./install-minecraft-splitscreen.sh + +# With custom source URL +INSTALLER_SOURCE_URL="https://raw.githubusercontent.com/USER/REPO/BRANCH/install-minecraft-splitscreen.sh" \ + ./install-minecraft-splitscreen.sh + +# Via curl (production) +curl -fsSL https://raw.githubusercontent.com/aradanmn/MinecraftSplitscreenSteamdeck/main/install-minecraft-splitscreen.sh | bash +``` + +### Testing Modules Individually + +Modules can't run standalone - they're sourced by the main script. For testing: + +```bash +# Source dependencies manually for testing +source modules/version_info.sh +source modules/utilities.sh +source modules/path_configuration.sh +# ... then source your module +``` + +### Release Process + +Releases are automated via GitHub Actions (`.github/workflows/release.yml`): +1. Create a tag: `git tag v2.0.1` +2. Push the tag: `git push origin v2.0.1` +3. GitHub Actions creates the release automatically + +## Common Development Tasks + +### Adding a New Mod + +1. Edit `modules/mod_management.sh` +2. Add to the `MODS` array with format: `"ModName|platform|project_id|required|dependencies"` +3. Platform: `modrinth` or `curseforge` +4. Dependencies are auto-resolved via API + +### Adding a New Module + +1. Create `modules/new_module.sh` with JSDoc header +2. Add to the sourcing list in `install-minecraft-splitscreen.sh` (respect dependency order) +3. Export functions/variables clearly in the header + +### Modifying Path Logic + +**Always edit `modules/path_configuration.sh`** - never add path logic elsewhere. + +### Supporting a New Immutable OS + +Edit `modules/utilities.sh`: + +```bash +is_immutable_os() { + # Add detection for new OS + [[ -f /etc/new-os-release ]] && return 0 + # ... existing checks +} +``` + +## Important Implementation Details + +### Java Version Mapping + +In `modules/java_management.sh`: +- Minecraft 1.21+ → Java 21 +- Minecraft 1.18-1.20 → Java 17 +- Minecraft 1.17 → Java 16 +- Minecraft 1.13-1.16 → Java 8 + +### LWJGL Version Mapping + +In `modules/lwjgl_management.sh`: +- Minecraft 1.21+ → LWJGL 3.3.3 +- Minecraft 1.19-1.20 → LWJGL 3.3.1 +- Minecraft 1.18 → LWJGL 3.2.2 +- etc. + +### Instance Naming + +Instances are named: `latestUpdate-1`, `latestUpdate-2`, `latestUpdate-3`, `latestUpdate-4` + +### Generated Files + +The installer generates `minecraftSplitscreen.sh` at runtime with: +- Correct paths baked in +- Version metadata (SCRIPT_VERSION, COMMIT_HASH, GENERATION_DATE) +- Controller detection logic +- Steam Deck Game Mode handling + +## Pitfalls to Avoid + +1. **Never hardcode paths** - Always use path_configuration.sh variables +2. **Don't assume launcher type** - Always check both Flatpak and AppImage +3. **Don't skip API fallbacks** - Always have hardcoded fallback mappings +4. **Module order matters** - path_configuration.sh must load before modules that use paths +5. **Signal handling** - Ctrl+C cleanup is handled in main script; don't override +6. **Account merging** - Always preserve existing Microsoft accounts when adding offline accounts + +## Git Workflow + +- **Main branch:** `main` (stable releases) +- **Development:** Feature branches +- **Commit style:** Conventional commits (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`) + +## Key Files Quick Reference + +| File | Lines | Purpose | +|------|-------|---------| +| `install-minecraft-splitscreen.sh` | ~386 | Entry point, module loader | +| `modules/path_configuration.sh` | ~600+ | Path management (CRITICAL) | +| `modules/mod_management.sh` | ~1900 | Mod compatibility (largest) | +| `modules/main_workflow.sh` | ~1300 | Main orchestration | +| `modules/instance_creation.sh` | ~600+ | Instance creation logic | +| `add-to-steam.py` | ~195 | Steam integration | + +## Installation Flow (10 Phases) + +1. **Workspace Setup** - Temporary directories, signal handling +2. **Core Setup** - Java, PrismLauncher, CLI verification +3. **Version Detection** - Minecraft, Fabric, LWJGL versions +4. **Account Setup** - Offline player accounts (P1-P4) +5. **Mod Compatibility** - API checking for compatible versions +6. **User Selection** - Interactive mod choice +7. **Instance Creation** - 4 splitscreen instances +8. **Launcher Optimization** - PollyMC setup, PrismLauncher cleanup +9. **System Integration** - Steam, desktop shortcuts +10. **Completion Report** - Summary with paths and usage + +## TODO Items (from README) + +1. Steam Deck controller handling without system-wide disable +2. Pre-configuring controllers within Controllable mod + +## Active Development Backlog + +### Issue #1: Centralized User Input Handling for curl | bash Mode ✅ IMPLEMENTED +**Problem:** When running via `curl | bash`, stdin is consumed by the script download, breaking interactive prompts. The PollyMC Flatpak detection on SteamOS prompts for user choice but can't receive input. + +**Solution Implemented:** +- **Utility functions in `utilities.sh`:** + - `prompt_user(prompt, default, timeout)` - Works with curl | bash by reopening /dev/tty + - `prompt_yes_no(question, default)` - Simplified yes/no prompts with automatic logging + +**Modules Refactored:** +- `modules/version_management.sh` - Minecraft version selection (2 prompts) +- `modules/mod_management.sh` - Mod selection prompt +- `modules/steam_integration.sh` - "Add to Steam?" prompt (now uses `prompt_yes_no`) +- `modules/desktop_launcher.sh` - "Create desktop launcher?" prompt (now uses `prompt_yes_no`) + +**Note:** `modules/pollymc_setup.sh` was checked and contains no user prompts (contrary to original assumption) + +--- + +### Issue #2: Steam Deck Virtual Controller Detection (MEDIUM PRIORITY) +**Problem:** When launching on Steam Deck without external controllers, the script detects the Steam virtual controller, filters it out, and then stops because no "real" controllers remain. + +**Current State:** The launcher script correctly filters Steam virtual controllers but doesn't handle the case where that's the ONLY controller available. + +**Solution:** Modify controller detection logic to: +- If on Steam Deck AND only Steam virtual controller detected AND no external controllers → allow using Steam Deck as Player 1 +- Provide a fallback "keyboard only" mode or prompt user +- Consider: Steam Deck's built-in controls should count as 1 player + +**Files to modify:** `modules/launcher_script_generator.sh` (the generated script template) + +--- + +### Issue #3: Logging System ✅ IMPLEMENTED +**Problem:** Debugging issues across multiple machines (Bazzite, SteamOS, etc.) is difficult without logs. + +**Solution Implemented:** +- **Log location:** `~/.local/share/MinecraftSplitscreen/logs/` +- **Installer log:** `install-YYYY-MM-DD-HHMMSS.log` +- **Launcher log:** `launcher-YYYY-MM-DD-HHMMSS.log` +- Auto-rotation: keeps last 10 logs per type +- System info logged at startup (OS, kernel, environment, tools) + +**Key Design Decision:** Print functions auto-log (no separate log calls needed) +- `print_success()`, `print_error()`, etc. all automatically write to log +- `log()` is for debug-only info that shouldn't clutter terminal +- Cleaner code with no duplicate logging statements + +**Files modified:** +- `modules/utilities.sh` - logging infrastructure, print_* auto-log +- `modules/main_workflow.sh` - init_logging() call, log file display +- `modules/launcher_script_generator.sh` - log_info/log_error/log_warning in generated script + +--- + +### Issue #4: Minecraft New Versioning System (LOW PRIORITY - Future) +**Problem:** Minecraft is switching to a new version numbering system (announced at minecraft.net/en-us/article/minecraft-new-version-numbering-system). + +**Current State:** Version parsing assumes `1.X.Y` format throughout codebase. + +**Research Needed:** +- Fetch and document the new versioning scheme details +- Identify when this takes effect +- Likely format change from `1.21.x` to something like `25.1` (year-based?) + +**Files likely affected:** +- `modules/version_management.sh` - version parsing and comparison +- `modules/java_management.sh` - Java version mapping +- `modules/lwjgl_management.sh` - LWJGL version mapping +- `modules/mod_management.sh` - mod compatibility matching + +**Solution approach:** +- Create version parsing functions that handle both old and new formats +- Maintain backward compatibility for existing `1.x.x` versions +- Add detection for which format a version string uses + +--- + +### Implementation Order +1. ✅ **Issue #3 (Logging)** - DONE. All print_* functions auto-log. +2. ✅ **Issue #1 (User Input)** - DONE. All modules refactored to use `prompt_user()` and `prompt_yes_no()`. +3. ⏳ **Issue #2 (Controller Detection)** - Improves Steam Deck UX +4. ⏳ **Issue #4 (Versioning)** - Can wait until Minecraft actually releases new format + +## Useful Debugging + +```bash +# Check generated launcher script version +head -20 ~/.local/share/PollyMC/minecraftSplitscreen.sh + +# View instance configuration +cat ~/.local/share/PollyMC/instances/latestUpdate-1/instance.cfg + +# Check accounts +cat ~/.local/share/PollyMC/accounts.json | jq . +``` diff --git a/README.md b/README.md index 04bc58c..e5a22d8 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,15 @@ This project provides an easy way to set up splitscreen Minecraft on Steam Deck ## Features - **Automatic Java Installation:** Detects required Java version and installs automatically (no manual setup required) - **Optimized Installation:** Uses PrismLauncher for automated instance creation, then switches to PollyMC for gameplay +- **Auto-Generated Launcher Script:** The splitscreen launcher is generated at install time with correct paths baked in - no hardcoded paths +- **Flatpak & AppImage Support:** Works with both Flatpak and AppImage installations of PollyMC and PrismLauncher +- **Smart Launcher Detection:** Automatically detects existing launcher installations and uses them - Launch 1–4 Minecraft instances in splitscreen mode with proper Fabric support - Automatic controller detection and per-player config - Works on Steam Deck (Game Mode & Desktop Mode) and any Linux PC - Optionally adds a launcher to Steam and your desktop menu - Handles KDE/Plasma quirks for a clean splitscreen experience when running from Game Mode -- Self-updating launcher script +- **Version Tracking:** Generated scripts include version, commit hash, and generation date for troubleshooting - **Fabric Loader:** Complete dependency chain implementation ensures mods load and function correctly - **Automatic Dependency Resolution:** Uses live API calls to discover and install all mod dependencies without manual maintenance - **Smart Cleanup:** Automatically removes temporary files and directories after successful setup @@ -84,15 +87,23 @@ This hybrid approach ensures reliable automated installation while providing the - **Smart cleanup:** Automatically removes temporary PrismLauncher files after successful PollyMC setup ## Installation -1. **Download and run the installer:** - - You can get the latest installer script from the [Releases section](https://github.com/FlyingEwok/MinecraftSplitscreenSteamdeck/releases) (recommended for stable versions), or use the latest development version with: +1. **Quick Install (Recommended):** + + Run this single command to download and execute the installer: + ```sh + curl -fsSL https://raw.githubusercontent.com/aradanmn/MinecraftSplitscreenSteamdeck/main/install-minecraft-splitscreen.sh | bash + ``` + + **Alternative method** (download first, then run): ```sh - wget https://raw.githubusercontent.com/FlyingEwok/MinecraftSplitscreenSteamdeck/main/install-minecraft-splitscreen.sh + wget https://raw.githubusercontent.com/aradanmn/MinecraftSplitscreenSteamdeck/main/install-minecraft-splitscreen.sh chmod +x install-minecraft-splitscreen.sh ./install-minecraft-splitscreen.sh ``` **Note:** The installer will automatically detect which Java version you need based on your selected Minecraft version and install it if not present. No manual Java setup required! + + **Note:** If you already have PollyMC or PrismLauncher installed (via Flatpak or AppImage), the installer will detect and use your existing installation. 2. **Install Python 3 (optional)** - Only required if you want to add the launcher to Steam automatically @@ -148,9 +159,18 @@ This hybrid approach ensures reliable automated installation while providing the - **Steam Deck users:** For proper controller counting, you must disable the built-in Steam Deck controller when an external controller is connected. Use [Steam-Deck.Auto-Disable-Steam-Controller](https://github.com/scawp/Steam-Deck.Auto-Disable-Steam-Controller) to automate this process. ## Installation Locations + +**AppImage installations:** - **Primary installation:** `~/.local/share/PollyMC/` (instances, launcher, and game files) +- **Launcher script:** `~/.local/share/PollyMC/minecraftSplitscreen.sh` (auto-generated) + +**Flatpak installations:** +- **Primary installation:** `~/.var/app/org.fn2006.PollyMC/data/PollyMC/` +- **Launcher script:** `~/.var/app/org.fn2006.PollyMC/data/PollyMC/minecraftSplitscreen.sh` (auto-generated) + +**Note:** The launcher script is automatically generated during installation with the correct paths for your system. It includes version metadata for troubleshooting. + - **Temporary files:** Automatically cleaned up after successful installation -- **Launcher script:** `~/.local/share/PollyMC/minecraftSplitscreen.sh` ## Troubleshooting - **Java installation issues:** @@ -164,21 +184,14 @@ This hybrid approach ensures reliable automated installation while providing the ## Updating ### Launcher Updates -The launcher script (`minecraftSplitscreen.sh`) will auto-update itself when a new version is available. +To update the launcher script, simply re-run the installer. The script will be regenerated with the latest version and your existing settings will be preserved. ### Minecraft Version Updates -To update your Minecraft version or mod configuration: -1. Download the latest installer: - ```sh - wget https://raw.githubusercontent.com/FlyingEwok/MinecraftSplitscreenSteamdeck/main/install-minecraft-splitscreen.sh - chmod +x install-minecraft-splitscreen.sh - ``` -2. Run the installer: - ```sh - ./install-minecraft-splitscreen.sh - ``` -3. Select your new Minecraft version when prompted -4. The installer will: +To update your Minecraft version or mod configuration, re-run the installer: +```sh +curl -fsSL https://raw.githubusercontent.com/aradanmn/MinecraftSplitscreenSteamdeck/main/install-minecraft-splitscreen.sh | bash +``` +Select your new Minecraft version when prompted. The installer will: - Preserve your existing options.txt settings (keybindings, video settings, etc.) - Clear old mods and install fresh ones for the new version - Update the Fabric loader and all dependencies @@ -186,7 +199,8 @@ To update your Minecraft version or mod configuration: - Preserve all your existing worlds ## Uninstall -- Delete the PollyMC folder: `rm -rf ~/.local/share/PollyMC` +- **AppImage installations:** Delete the PollyMC folder: `rm -rf ~/.local/share/PollyMC` +- **Flatpak installations:** Delete the PollyMC data: `rm -rf ~/.var/app/org.fn2006.PollyMC/data/PollyMC` - Remove any desktop or Steam shortcuts you created. ## Credits @@ -197,6 +211,9 @@ To update your Minecraft version or mod configuration: - Steam Deck controller auto-disable tool by [scawp](https://github.com/scawp/Steam-Deck.Auto-Disable-Steam-Controller) - automatically disables built-in Steam Deck controller when external controllers are connected, essential for proper splitscreen controller counting. ## Technical Improvements +- **Launcher Detection Module:** Automatically detects AppImage and Flatpak installations with appropriate path handling for each +- **Script Generation with Version Tracking:** Generated launcher scripts include version, commit hash, and generation timestamp +- **Dynamic Path Resolution:** No hardcoded paths - all paths are determined at install time based on detected launcher type - **Complete Fabric Dependency Chain:** Ensures mods load and function correctly by including LWJGL 3, Minecraft, Intermediary Mappings, and Fabric Loader with proper dependency references - **API Filtering:** Both Modrinth and CurseForge APIs are filtered to only download Fabric-compatible mod versions - **Automatic Dependency Resolution:** Recursively resolves all mod dependencies using live API calls, eliminating the need to manually maintain dependency lists @@ -209,6 +226,10 @@ To update your Minecraft version or mod configuration: - **Figure out preconfiguring controllers within controllable (if possible)** - Investigate automatic controller assignment configuration to avoid having Controllable grab the same controllers as all the other instances, ensuring each player gets their own dedicated controller ## Recent Improvements +- ✅ **Auto-Generated Launcher Script**: The splitscreen launcher is now generated at install time with correct paths baked in - no more hardcoded paths +- ✅ **Flatpak Support**: Works with both Flatpak and AppImage installations of PollyMC and PrismLauncher +- ✅ **Smart Launcher Detection**: Automatically detects existing launcher installations and uses them instead of downloading new ones +- ✅ **Version Metadata**: Generated scripts include version, commit hash, and generation date for easier troubleshooting - ✅ **Automatic Java Installation**: No manual Java setup required - the installer automatically detects, downloads, and installs the correct Java version for your chosen Minecraft version - ✅ **Automatic Java Version Detection**: Automatically detects and uses the correct Java version for each Minecraft version (Java 8, 16, 17, or 21) with smart backward compatibility - ✅ **Intelligent Version Selection**: Only Minecraft versions supported by both Controllable and Splitscreen Support mods are offered to users, ensuring full compatibility @@ -220,4 +241,4 @@ To update your Minecraft version or mod configuration: --- -For more details, see the comments in the scripts or open an issue on the [GitHub repo](https://github.com/FlyingEwok/MinecraftSplitscreenSteamdeck). +For more details, see the comments in the scripts or open an issue on the [GitHub repo](https://github.com/aradanmn/MinecraftSplitscreenSteamdeck). diff --git a/add-to-steam.py b/add-to-steam.py index ddcade7..17be550 100644 --- a/add-to-steam.py +++ b/add-to-steam.py @@ -7,82 +7,122 @@ # Based on the original script by ArnoldSmith86: # https://github.com/ArnoldSmith86/minecraft-splitscreen # Modified and improved for portability and clarity. +# +# Usage: python3 add-to-steam.py +# Example: python3 add-to-steam.py ~/.local/share/PollyMC/minecraftSplitscreen.sh ~/.local/share/PollyMC PollyMC import os import re import struct -import zlib +import sys import urllib.request +import zlib -# --- Config: Set up paths and app info dynamically for the current user --- -HOME = os.path.expanduser("~") # Get the current user's home directory -APPNAME = "Minecraft Splitscreen" # Name as it will appear in Steam - -# Detect which launcher is being used (PollyMC only after cleanup) -def detect_launcher(): - """Detect PollyMC launcher for splitscreen gameplay.""" - pollymc_path = f'{HOME}/.local/share/PollyMC/PollyMC-Linux-x86_64.AppImage' - pollymc_script = f'{HOME}/.local/share/PollyMC/minecraftSplitscreen.sh' - - # Check for PollyMC (should be the only option after installer cleanup) - if os.path.exists(pollymc_path) and os.access(pollymc_path, os.X_OK): - if os.path.exists(pollymc_script): - return pollymc_script, f"{HOME}/.local/share/PollyMC", "PollyMC" - else: - # Use the script from current directory if PollyMC directory doesn't have it - return f"{HOME}/.local/share/PrismLauncher/minecraftSplitscreen.sh", f"{HOME}/.local/share/PollyMC", "PollyMC" - - # If PollyMC not found, something went wrong with installation - print("❌ Error: PollyMC not found!") - print(" Please run the Minecraft Splitscreen installer to set up PollyMC") - exit(1) - -EXE, STARTDIR, LAUNCHER_NAME = detect_launcher() -print(f"📱 Detected launcher: {LAUNCHER_NAME}") -print(f"🚀 Launch script: {EXE}") -print(f"📁 Working directory: {STARTDIR}") +# --- Config --- +APPNAME = "Minecraft Splitscreen" # Name as it will appear in Steam + +# ============================================================================= +# ARGUMENT PARSING +# ============================================================================= + + +def print_usage(): + """Print usage information.""" + print( + "Usage: python3 add-to-steam.py " + ) + print("") + print("Arguments:") + print(" launcher_script_path Path to minecraftSplitscreen.sh (the executable)") + print( + " data_dir Launcher data directory (working directory for Steam)" + ) + print( + " launcher_name Display name of the launcher (e.g., PollyMC, PrismLauncher)" + ) + print("") + print("Example:") + print( + " python3 add-to-steam.py ~/.local/share/PollyMC/minecraftSplitscreen.sh ~/.local/share/PollyMC PollyMC" + ) + + +if len(sys.argv) != 4: + print("Error: Expected 3 arguments, got", len(sys.argv) - 1) + print_usage() + sys.exit(1) + +EXE = os.path.expanduser(sys.argv[1]) # Path to minecraftSplitscreen.sh +STARTDIR = os.path.expanduser(sys.argv[2]) # Launcher data directory +LAUNCHER_NAME = sys.argv[3] # Launcher name for display + +# Validate arguments +if not os.path.exists(EXE): + print(f"Error: Launcher script not found: {EXE}") + sys.exit(1) + +if not os.path.isdir(STARTDIR): + print(f"Error: Data directory not found: {STARTDIR}") + sys.exit(1) + +print(f"Launcher: {LAUNCHER_NAME}") +print(f"Script: {EXE}") +print(f"Working directory: {STARTDIR}") + +# ============================================================================= +# STEAM INTEGRATION +# ============================================================================= # SteamGridDB artwork URLs for custom grid images, hero, logo, and icon STEAMGRIDDB_IMAGES = { "p": "https://cdn2.steamgriddb.com/grid/a73027901f88055aaa0fd1a9e25d36c7.png", # Portrait grid - "": "https://cdn2.steamgriddb.com/grid/e353b610e9ce20f963b4cca5da565605.jpg", # Main grid - "_hero": "https://cdn2.steamgriddb.com/hero/ecd812da02543c0269cfc2c56ab3c3c0.png", # Hero image - "_logo": "https://cdn2.steamgriddb.com/logo/90915208c601cc8c86ad01250ee90c12.png", # Logo - "_icon": "https://cdn2.steamgriddb.com/icon/add7a048049671970976f3e18f21ade3.ico" # Icon + "": "https://cdn2.steamgriddb.com/grid/e353b610e9ce20f963b4cca5da565605.jpg", # Main grid + "_hero": "https://cdn2.steamgriddb.com/hero/ecd812da02543c0269cfc2c56ab3c3c0.png", # Hero image + "_logo": "https://cdn2.steamgriddb.com/logo/90915208c601cc8c86ad01250ee90c12.png", # Logo + "_icon": "https://cdn2.steamgriddb.com/icon/add7a048049671970976f3e18f21ade3.ico", # Icon } # --- Locate Steam shortcuts file for the current user --- -userdata = os.path.expanduser("~/.steam/steam/userdata") # Steam userdata directory -user_id = next((d for d in os.listdir(userdata) if d.isdigit()), None) # Find the first numeric user ID +userdata = os.path.expanduser("~/.steam/steam/userdata") + +if not os.path.exists(userdata): + print("Error: Steam userdata directory not found.") + print(f"Expected: {userdata}") + sys.exit(1) + +user_id = next((d for d in os.listdir(userdata) if d.isdigit()), None) if not user_id: - print("❌ No Steam user found.") - exit(1) -config_dir = os.path.join(userdata, user_id, "config") # Path to config directory -shortcuts_file = os.path.join(config_dir, "shortcuts.vdf") # Path to shortcuts.vdf + print("Error: No Steam user found.") + sys.exit(1) + +config_dir = os.path.join(userdata, user_id, "config") +shortcuts_file = os.path.join(config_dir, "shortcuts.vdf") # --- Ensure shortcuts.vdf exists (create if missing) --- if not os.path.exists(shortcuts_file): + os.makedirs(config_dir, exist_ok=True) with open(shortcuts_file, "wb") as f: - f.write(b'\x00shortcuts\x00\x08\x08') # Write empty VDF structure + f.write(b"\x00shortcuts\x00\x08\x08") # --- Read current shortcuts.vdf into memory --- with open(shortcuts_file, "rb") as f: data = f.read() + def get_latest_index(data): """ Find the highest shortcut index in the VDF file. Steam shortcuts are stored as binary blobs with indices: \x00\x00 """ - matches = re.findall(rb'\x00(\d+)\x00', data) + matches = re.findall(rb"\x00(\d+)\x00", data) if matches: return int(matches[-1]) return -1 -# --- Determine the next shortcut index --- + index = get_latest_index(data) + 1 -# --- Helper: Create a binary shortcut entry for Steam's VDF format --- + def make_entry(index, appid, appname, exe, startdir): """ Build a binary VDF entry for a Steam shortcut. @@ -95,14 +135,26 @@ def make_entry(index, appid, appname, exe, startdir): Returns: bytes: Binary VDF entry """ - x00 = b'\x00'; x01 = b'\x01'; x02 = b'\x02'; x08 = b'\x08' - b = b'' + x00 = b"\x00" + x01 = b"\x01" + x02 = b"\x02" + x08 = b"\x08" + b = b"" b += x00 + str(index).encode() + x00 # Shortcut index - b += x02 + b'appid' + x00 + struct.pack('/dev/null 2>&1; then + local remote_url + remote_url=$(git -C "$SCRIPT_DIR" remote get-url origin 2>/dev/null || true) + + if [[ -n "$remote_url" ]]; then + # Parse git@github.com:owner/repo.git or https://github.com/owner/repo.git + if [[ "$remote_url" =~ github\.com[:/]([^/]+)/([^/.]+)(\.git)?$ ]]; then + BOOTSTRAP_REPO_OWNER="${BASH_REMATCH[1]}" + BOOTSTRAP_REPO_NAME="${BASH_REMATCH[2]}" + # Get current branch + BOOTSTRAP_REPO_BRANCH=$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || echo "$DEFAULT_REPO_BRANCH") + echo "📍 Detected repository from local git: $BOOTSTRAP_REPO_OWNER/$BOOTSTRAP_REPO_NAME ($BOOTSTRAP_REPO_BRANCH)" + return 0 + fi + fi + fi + + # Fall back to defaults + echo "📍 Using default repository: $DEFAULT_REPO_OWNER/$DEFAULT_REPO_NAME ($DEFAULT_REPO_BRANCH)" + return 0 +} + +# Detect source URL and set repository variables +detect_source_url "$@" -# List of required module files +# Build the modules URL from detected/default values +readonly BOOTSTRAP_REPO_MODULES_URL="https://raw.githubusercontent.com/${BOOTSTRAP_REPO_OWNER}/${BOOTSTRAP_REPO_NAME}/${BOOTSTRAP_REPO_BRANCH}/modules" + +# List of required module files (order matters for dependencies) +# path_configuration.sh MUST be loaded right after utilities.sh as it's the single source of truth for paths readonly MODULE_FILES=( + "version_info.sh" "utilities.sh" + "path_configuration.sh" +# "launcher_detection.sh" + "launcher_script_generator.sh" "java_management.sh" "launcher_setup.sh" "version_management.sh" @@ -73,23 +203,23 @@ readonly MODULE_FILES=( download_modules() { echo "🔄 Downloading required modules to temporary directory..." echo "📁 Temporary modules directory: $MODULES_DIR" - echo "🌐 Repository URL: $REPO_BASE_URL" - + echo "🌐 Repository URL: $BOOTSTRAP_REPO_MODULES_URL" + # Temporarily disable strict error handling for downloads set +e - + # The temporary directory is already created by mktemp local downloaded_count=0 local failed_count=0 - + # Download each required module for module in "${MODULE_FILES[@]}"; do local module_path="$MODULES_DIR/$module" - local module_url="$REPO_BASE_URL/$module" - + local module_url="$BOOTSTRAP_REPO_MODULES_URL/$module" + echo "⬇️ Downloading module: $module" echo " URL: $module_url" - + # Download the module file if command -v curl >/dev/null 2>&1; then curl_output=$(curl -fsSL "$module_url" -o "$module_path" 2>&1) @@ -126,10 +256,10 @@ download_modules() { exit 1 fi done - + # Re-enable strict error handling set -euo pipefail - + if [[ $failed_count -gt 0 ]]; then echo "❌ Failed to download $failed_count module(s)" echo "ℹ️ This might be because:" @@ -141,10 +271,10 @@ download_modules() { echo " mkdir -p '$SCRIPT_DIR/modules'" echo " # Then copy all .sh module files to that directory" echo "" - echo "🌐 Or check if the repository exists at: https://github.com/FlyingEwok/MinecraftSplitscreenSteamdeck" + echo "🌐 Or check if the repository exists at: https://github.com/${BOOTSTRAP_REPO_OWNER}/${BOOTSTRAP_REPO_NAME}" exit 1 fi - + echo "✅ Downloaded $downloaded_count module(s) to temporary directory" echo "ℹ️ Modules will be automatically cleaned up when script completes" } @@ -154,7 +284,7 @@ download_modules() { if [[ -d "$SCRIPT_DIR/modules" ]]; then echo "📁 Found local modules directory, copying to temporary location..." cp -r "$SCRIPT_DIR/modules/"* "$MODULES_DIR/" - chmod +x "$MODULES_DIR"/*.sh + chmod +x "$MODULES_DIR"/*.sh 2>/dev/null || true echo "✅ Copied local modules to temporary directory" else download_modules @@ -165,14 +295,22 @@ for module in "${MODULE_FILES[@]}"; do if [[ ! -f "$MODULES_DIR/$module" ]]; then echo "❌ Error: Required module missing: $module" echo "Please check your internet connection or download manually from:" - echo "$REPO_BASE_URL/$module" + echo "$BOOTSTRAP_REPO_MODULES_URL/$module" exit 1 fi done # Source all module files to load their functions -# Load modules in dependency order +# Load modules in dependency order: +# 1. version_info first for constants +# 2. utilities for logging functions +# 3. path_configuration for centralized path management (SINGLE SOURCE OF TRUTH) +# 4. All other modules +source "$MODULES_DIR/version_info.sh" source "$MODULES_DIR/utilities.sh" +#source "$MODULES_DIR/launcher_detection.sh" +source "$MODULES_DIR/path_configuration.sh" +source "$MODULES_DIR/launcher_script_generator.sh" source "$MODULES_DIR/java_management.sh" source "$MODULES_DIR/launcher_setup.sh" source "$MODULES_DIR/version_management.sh" @@ -184,20 +322,22 @@ source "$MODULES_DIR/steam_integration.sh" source "$MODULES_DIR/desktop_launcher.sh" source "$MODULES_DIR/main_workflow.sh" +# Now that version_info.sh is loaded, we can use REPO_MODULES_URL +# This is used by download_modules when running from curl | bash + # ============================================================================= # GLOBAL VARIABLES # ============================================================================= -# Script configuration paths -readonly TARGET_DIR="$HOME/.local/share/PrismLauncher" -readonly POLLYMC_DIR="$HOME/.local/share/PollyMC" +# NOTE: Launcher paths are now managed by path_configuration.sh +# Use ACTIVE_DATA_DIR, ACTIVE_INSTANCES_DIR, CREATION_DATA_DIR, etc. +# DO NOT use hardcoded PRISMLAUNCHER_DIR or POLLYMC_DIR # Runtime variables (set during execution) JAVA_PATH="" MC_VERSION="" FABRIC_VERSION="" LWJGL_VERSION="" -USE_POLLYMC=false # Mod configuration arrays declare -a REQUIRED_SPLITSCREEN_MODS=("Controllable (Fabric)" "Splitscreen Support") @@ -237,11 +377,9 @@ declare -a MISSING_MODS=() # SCRIPT ENTRY POINT # ============================================================================= -# Execute main function if script is run directly -# This allows the script to be sourced for testing without auto-execution -if [[ "${BASH_SOURCE[0]}" == "${0}" ]] && [[ -z "${TESTING_MODE:-}" ]]; then - main "$@" -fi +# Execute main function +# Works for both direct execution (./script.sh) and piped execution (curl | bash) +main "$@" # ============================================================================= # END OF MODULAR MINECRAFT SPLITSCREEN INSTALLER diff --git a/minecraftSplitscreen.sh b/minecraftSplitscreen.sh deleted file mode 100755 index 2d95daf..0000000 --- a/minecraftSplitscreen.sh +++ /dev/null @@ -1,355 +0,0 @@ -#!/bin/bash - -set +e # Allow script to continue on errors for robustness - -# ============================= -# Minecraft Splitscreen Launcher for Steam Deck & Linux -# ============================= -# This script launches 1–4 Minecraft instances in splitscreen mode. -# On Steam Deck Game Mode, it launches a nested KDE Plasma session for clean splitscreen. -# On desktop mode, it launches Minecraft instances directly. -# Handles controller detection, per-instance mod config, KDE panel hiding/restoring, and reliable autostart in a nested session. -# -# HOW IT WORKS: -# 1. If in Steam Deck Game Mode, launches a nested Plasma Wayland session (if not already inside). -# 2. Sets up an autostart .desktop file to re-invoke itself inside the nested session. -# 3. Detects how many controllers are connected (1–4, with Steam Input quirks handled). -# 4. For each player, writes the correct splitscreen mod config and launches a Minecraft instance. -# 5. Hides KDE panels for a clean splitscreen experience (by killing plasmashell), then restores them. -# 6. Logs out of the nested session when done. -# -# NOTE: This script is robust and heavily commented for clarity and future maintainers! -# The main script file should be named minecraftSplitscreen.sh for clarity and version-agnostic usage. - -# Set a temporary directory for intermediate files (used for wrappers, etc) -export target=/tmp - -# ============================= -# Function: detectLauncher -# ============================= -# Detects PollyMC launcher for splitscreen gameplay. -# Returns launcher paths and executable info. -detectLauncher() { - # Check if PollyMC is available - if [ -f "$HOME/.local/share/PollyMC/PollyMC-Linux-x86_64.AppImage" ] && [ -x "$HOME/.local/share/PollyMC/PollyMC-Linux-x86_64.AppImage" ]; then - export LAUNCHER_DIR="$HOME/.local/share/PollyMC" - export LAUNCHER_EXEC="$HOME/.local/share/PollyMC/PollyMC-Linux-x86_64.AppImage" - export LAUNCHER_NAME="PollyMC" - return 0 - fi - - echo "[Error] PollyMC not found at $HOME/.local/share/PollyMC/" >&2 - echo "[Error] Please run the Minecraft Splitscreen installer to set up PollyMC" >&2 - return 1 -} - -# Detect and set launcher variables at startup -if ! detectLauncher; then - echo "[Error] Cannot continue without a compatible Minecraft launcher" >&2 - exit 1 -fi - -echo "[Info] Using $LAUNCHER_NAME for splitscreen gameplay" - -# ============================= -# Function: selfUpdate -# ============================= -# Checks if this script is the latest version from GitHub. If not, downloads and replaces itself. -selfUpdate() { - local repo_url="https://raw.githubusercontent.com/FlyingEwok/MinecraftSplitscreenSteamdeck/main/minecraftSplitscreen.sh" - local tmpfile - tmpfile=$(mktemp) - local script_path - script_path="$(readlink -f "$0")" - # Download the latest version - if ! curl -fsSL "$repo_url" -o "$tmpfile"; then - echo "[Self-Update] Failed to check for updates." >&2 - rm -f "$tmpfile" - return - fi - # Compare files byte-for-byte - if ! cmp -s "$tmpfile" "$script_path"; then - # --- Terminal Detection and Relaunch Logic --- - # If not running in an interactive shell (no $PS1), not launched by a terminal program, and not attached to a tty, - # then we are likely running from a GUI (e.g., .desktop launcher) and cannot prompt the user for input. - if [ -z "$PS1" ] && [ -z "$TERM_PROGRAM" ] && ! tty -s; then - # Try to find a terminal emulator to relaunch the script for the update prompt. - # This loop checks for common terminal emulators in order of preference. - for term in x-terminal-emulator gnome-terminal konsole xfce4-terminal xterm; do - if command -v $term >/dev/null 2>&1; then - # Relaunch this script in the found terminal emulator, passing all arguments. - exec $term -e "$script_path" "$@" - fi - done - # If no terminal emulator is found, print an error and exit. - echo "[Self-Update] Update available, but no terminal found for prompt. Please run this script from a terminal to update." >&2 - rm -f "$tmpfile" - exit 1 - fi - # --- Interactive Update Prompt --- - # If we are running in a terminal, prompt the user for update confirmation. - echo "[Self-Update] A new version is available. Update now? [y/N]" - read -r answer - if [[ "$answer" =~ ^[Yy]$ ]]; then - echo "[Self-Update] Updating..." - cp "$tmpfile" "$script_path" - chmod +x "$script_path" - rm -f "$tmpfile" - echo "[Self-Update] Update complete. Restarting..." - exec "$script_path" "$@" - else - echo "[Self-Update] Update skipped by user." - rm -f "$tmpfile" - fi - else - rm -f "$tmpfile" - echo "[Self-Update] Already up to date." - fi -} - -# Call selfUpdate at the very start of the script -selfUpdate - -# ============================= -# Function: nestedPlasma -# ============================= -# Launches a nested KDE Plasma Wayland session and sets up Minecraft autostart. -# Needed so Minecraft can run in a clean, isolated desktop environment (avoiding SteamOS overlays, etc). -# The autostart .desktop file ensures Minecraft launches automatically inside the nested session. -nestedPlasma() { - # Unset variables that may interfere with launching a nested session - unset LD_PRELOAD XDG_DESKTOP_PORTAL_DIR XDG_SEAT_PATH XDG_SESSION_PATH - # Get current screen resolution (e.g., 1280x800) - RES=$(xdpyinfo 2>/dev/null | awk '/dimensions/{print $2}') - [ -z "$RES" ] && RES="1280x800" - # Create a wrapper for kwin_wayland with the correct resolution - cat < $target/kwin_wayland_wrapper -#!/bin/bash -/usr/bin/kwin_wayland_wrapper --width ${RES%x*} --height ${RES#*x} --no-lockscreen \$@ -EOF - chmod +x $target/kwin_wayland_wrapper - export PATH=$target:$PATH - # Write an autostart .desktop file that will re-invoke this script with a special argument - SCRIPT_PATH="$(readlink -f "$0")" - mkdir -p ~/.config/autostart - cat < ~/.config/autostart/minecraft-launch.desktop -[Desktop Entry] -Name=Minecraft Split Launch -Exec=$SCRIPT_PATH launchFromPlasma -Type=Application -X-KDE-AutostartScript=true -EOF - # Start nested Plasma session (never returns) - exec dbus-run-session startplasma-wayland -} - -# ============================= -# Function: launchGame -# ============================= -# Launches a single Minecraft instance using the detected launcher, with KDE inhibition to prevent -# the system from sleeping, activating the screensaver, or changing color profiles. -# Arguments: -# $1 = Launcher instance name (e.g., latestUpdate-1) -# $2 = Player name (e.g., P1) -launchGame() { - if command -v kde-inhibit >/dev/null 2>&1; then - kde-inhibit --power --screenSaver --colorCorrect --notifications "$LAUNCHER_EXEC" -l "$1" -a "$2" & - else - echo "[Warning] kde-inhibit not found. Running $LAUNCHER_NAME without KDE inhibition." - "$LAUNCHER_EXEC" -l "$1" -a "$2" & - fi - sleep 10 # Give time for the instance to start (avoid race conditions) -} - -# ============================= -# Function: hidePanels -# ============================= -# Kills all plasmashell processes to remove KDE panels and widgets. This is a brute-force workaround -# that works even in nested Plasma Wayland sessions, where scripting APIs may not work. -hidePanels() { - if command -v plasmashell >/dev/null 2>&1; then - pkill plasmashell - sleep 1 - if pgrep -u "$USER" plasmashell >/dev/null; then - killall plasmashell - sleep 1 - fi - if pgrep -u "$USER" plasmashell >/dev/null; then - pkill -9 plasmashell - sleep 1 - fi - else - echo "[Info] plasmashell not found. Skipping KDE panel hiding." - fi -} - -# ============================= -# Function: restorePanels -# ============================= -# Restarts plasmashell to restore all KDE panels and widgets after gameplay. -restorePanels() { - if command -v plasmashell >/dev/null 2>&1; then - nohup plasmashell >/dev/null 2>&1 & - sleep 2 - else - echo "[Info] plasmashell not found. Skipping KDE panel restore." - fi -} - -# ============================= -# Function: getControllerCount -# ============================= -# Detects the number of controllers (1–4) by counting /dev/input/js* devices. -# Steam Input (when Steam is running) creates duplicate devices, so we halve the count (rounding up). -# Ensures at least 1 and at most 4 controllers are reported. -# Logic: -# - Counts all /dev/input/js* devices (joysticks/gamepads recognized by the system) -# - Checks if the main Steam client is running (native or Flatpak) -# - Only halves the count if the main Steam client is running (not just helpers) -# - Returns a value between 1 and 4 (inclusive) -getControllerCount() { - local count - local steam_running=0 - # Count all joystick/gamepad devices - count=$(ls /dev/input/js* 2>/dev/null | wc -l) - # Only halve if the main Steam client is running (native or Flatpak) - # - pgrep -x steam: native Steam client - # - pgrep -f '^/app/bin/steam$': Flatpak Steam binary - # - pgrep -f 'flatpak run com.valvesoftware.Steam': Flatpak Steam launcher - if pgrep -x steam >/dev/null \ - || pgrep -f '^/app/bin/steam$' >/dev/null \ - || pgrep -f 'flatpak run com.valvesoftware.Steam' >/dev/null; then - steam_running=1 - fi - # If Steam is running, halve the count (rounding up) to account for Steam Input duplicates - if [ "$steam_running" -eq 1 ]; then - count=$(( (count + 1) / 2 )) - fi - # Clamp the count between 1 and 4 - [ "$count" -gt 4 ] && count=4 - [ "$count" -lt 1 ] && count=1 - # Output the detected controller count - echo "$count" -} - -# ============================= -# Function: setSplitscreenModeForPlayer -# ============================= -# Writes the splitscreen.properties config for the splitscreen mod for each player instance. -# This tells the mod which part of the screen each instance should use. -# Arguments: -# $1 = Player number (1–4) -# $2 = Total number of controllers/players -setSplitscreenModeForPlayer() { - local player=$1 - local numberOfControllers=$2 - local config_path="$LAUNCHER_DIR/instances/latestUpdate-${player}/.minecraft/config/splitscreen.properties" - mkdir -p "$(dirname $config_path)" - local mode="FULLSCREEN" - # Decide the splitscreen mode for this player based on total controllers - case "$numberOfControllers" in - 1) - mode="FULLSCREEN" # Single player: use whole screen - ;; - 2) - if [ "$player" = 1 ]; then mode="TOP"; else mode="BOTTOM"; fi # 2 players: split top/bottom - ;; - 3) - if [ "$player" = 1 ]; then mode="TOP"; - elif [ "$player" = 2 ]; then mode="BOTTOM_LEFT"; - else mode="BOTTOM_RIGHT"; fi # 3 players: 1 top, 2 bottom corners - ;; - 4) - if [ "$player" = 1 ]; then mode="TOP_LEFT"; - elif [ "$player" = 2 ]; then mode="TOP_RIGHT"; - elif [ "$player" = 3 ]; then mode="BOTTOM_LEFT"; - else mode="BOTTOM_RIGHT"; fi # 4 players: 4 corners - ;; - esac - # Write the config file for the mod - echo -e "gap=1\nmode=$mode" > "$config_path" - sync - sleep 0.5 -} - -# ============================= -# Function: launchGames -# ============================= -# Hides panels, launches the correct number of Minecraft instances, and restores panels after. -# Handles all splitscreen logic and per-player config. -launchGames() { - hidePanels # Remove KDE panels for a clean game view - numberOfControllers=$(getControllerCount) # Detect how many players - for player in $(seq 1 $numberOfControllers); do - setSplitscreenModeForPlayer "$player" "$numberOfControllers" # Write config for this player - launchGame "latestUpdate-$player" "P$player" # Launch Minecraft instance for this player - done - wait # Wait for all Minecraft instances to exit - restorePanels # Bring back KDE panels - sleep 2 # Give time for panels to reappear -} - -# ============================= -# Function: isSteamDeckGameMode -# ============================= -# Returns 0 if running on Steam Deck in Game Mode, 1 otherwise. -isSteamDeckGameMode() { - local dmi_file="/sys/class/dmi/id/product_name" - local dmi_contents="" - if [ -f "$dmi_file" ]; then - dmi_contents="$(cat "$dmi_file" 2>/dev/null)" - fi - if echo "$dmi_contents" | grep -Ei 'Steam Deck|Jupiter' >/dev/null; then - if [ "$XDG_SESSION_DESKTOP" = "gamescope" ] && [ "$XDG_CURRENT_DESKTOP" = "gamescope" ]; then - return 0 - fi - if pgrep -af 'steam' | grep -q '\-gamepadui'; then - return 0 - fi - else - # Fallback: If both XDG vars are gamescope and user is deck, assume Steam Deck Game Mode - if [ "$XDG_SESSION_DESKTOP" = "gamescope" ] && [ "$XDG_CURRENT_DESKTOP" = "gamescope" ] && [ "$USER" = "deck" ]; then - return 0 - fi - # Additional fallback: nested session (gamescope+KDE, user deck) - if [ "$XDG_SESSION_DESKTOP" = "gamescope" ] && [ "$XDG_CURRENT_DESKTOP" = "KDE" ] && [ "$USER" = "deck" ]; then - return 0 - fi - fi - return 1 -} - -# ============================= -# Always remove the autostart file on script exit to prevent unwanted autostart on boot -cleanup_autostart() { - rm -f "$HOME/.config/autostart/minecraft-launch.desktop" -} -trap cleanup_autostart EXIT - - -# ============================= -# MAIN LOGIC: Entry Point -# ============================= -# Universal: Steam Deck Game Mode = nested KDE, else just launch on current desktop -if isSteamDeckGameMode; then - if [ "$1" = launchFromPlasma ]; then - # Inside nested Plasma session: launch Minecraft splitscreen and logout when done - rm ~/.config/autostart/minecraft-launch.desktop - launchGames - qdbus org.kde.Shutdown /Shutdown org.kde.Shutdown.logout - else - # Not yet in nested session: start it - nestedPlasma - fi -else - # Not in Game Mode: just launch Minecraft instances directly - numberOfControllers=$(getControllerCount) - for player in $(seq 1 $numberOfControllers); do - setSplitscreenModeForPlayer "$player" "$numberOfControllers" - launchGame "latestUpdate-$player" "P$player" - done - wait -fi - - - diff --git a/modules/desktop_launcher.sh b/modules/desktop_launcher.sh index 604b290..3700b15 100644 --- a/modules/desktop_launcher.sh +++ b/modules/desktop_launcher.sh @@ -1,18 +1,42 @@ #!/bin/bash # ============================================================================= -# Minecraft Splitscreen Steam Deck Installer - Desktop Launcher Module -# ============================================================================= -# -# This module handles the creation of native desktop launchers and application -# menu integration for the Minecraft Splitscreen launcher. Provides seamless -# integration with Linux desktop environments. +# @file desktop_launcher.sh +# @version 2.0.0 +# @date 2026-01-25 +# @author Minecraft Splitscreen Steam Deck Project +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Creates native desktop launchers and application menu integration for the +# Minecraft Splitscreen launcher. Provides seamless integration with Linux +# desktop environments following freedesktop.org Desktop Entry Specification. +# +# Key features: +# - .desktop file generation for all Linux desktop environments +# - Application menu integration via ~/.local/share/applications/ +# - Desktop shortcut creation +# - SteamGridDB icon download with fallback hierarchy +# - Desktop database update for immediate availability # -# Functions provided: -# - create_desktop_launcher: Generate .desktop file for system integration +# @dependencies +# - utilities.sh (for print_header, print_success, print_warning, print_error, print_info, print_progress) +# - wget (for icon download) +# - path_configuration.sh (for ACTIVE_LAUNCHER_SCRIPT, ACTIVE_INSTANCES_DIR, ACTIVE_LAUNCHER) # +# @exports +# Functions: +# - create_desktop_launcher : Main function to create desktop integration +# +# @changelog +# 2.0.0 (2026-01-25) - Added comprehensive JSDoc documentation +# 1.0.0 (2024-XX-XX) - Initial implementation # ============================================================================= -# create_desktop_launcher: Generate .desktop file for system integration +# @function create_desktop_launcher +# @description Generate .desktop file for system integration with Linux desktops. +# Creates both a desktop shortcut and application menu entry. +# Downloads custom icon from SteamGridDB with intelligent fallbacks. # # DESKTOP LAUNCHER BENEFITS: # - Native desktop environment integration (GNOME, KDE, XFCE, etc.) @@ -31,53 +55,59 @@ # DESKTOP FILE LOCATIONS: # - Desktop shortcut: ~/Desktop/MinecraftSplitscreen.desktop # - System integration: ~/.local/share/applications/MinecraftSplitscreen.desktop +# +# @global ACTIVE_LAUNCHER_SCRIPT - (input) Path to the launcher script +# @global ACTIVE_INSTANCES_DIR - (input) Path to instances directory for icon fallback +# @global ACTIVE_LAUNCHER - (input) Name of active launcher for comments +# @stdin User confirmation from /dev/tty (for curl | bash compatibility) +# @return 0 on success or skip, 1 if ACTIVE_LAUNCHER_SCRIPT not set create_desktop_launcher() { print_header "🖥️ DESKTOP LAUNCHER SETUP" - + # ============================================================================= # DESKTOP LAUNCHER USER PROMPT # ============================================================================= - + # USER PREFERENCE GATHERING: Ask if they want desktop integration # Desktop launchers provide convenient access without terminal or Steam # Particularly useful for users who don't use Steam or prefer native desktop integration print_info "Desktop launcher creates a native shortcut for your desktop environment." print_info "Benefits: Desktop shortcut, application menu entry, search integration" echo "" - read -p "Do you want to create a desktop launcher for Minecraft Splitscreen? [y/N]: " create_desktop - if [[ "$create_desktop" =~ ^[Yy]$ ]]; then - + # Use centralized prompt function that handles curl | bash piping + if prompt_yes_no "Do you want to create a desktop launcher for Minecraft Splitscreen?" "n"; then + # ============================================================================= # DESKTOP FILE CONFIGURATION AND PATHS # ============================================================================= - + # DESKTOP FILE SETUP: Define paths and filenames following Linux standards # .desktop files follow the freedesktop.org Desktop Entry Specification # Standard locations ensure compatibility across all Linux desktop environments local desktop_file_name="MinecraftSplitscreen.desktop" local desktop_file_path="$HOME/Desktop/$desktop_file_name" # User desktop shortcut local app_dir="$HOME/.local/share/applications" # System integration directory - + # APPLICATIONS DIRECTORY CREATION: Ensure the applications directory exists # This directory is where desktop environments look for user-installed applications mkdir -p "$app_dir" print_info "Desktop file will be created at: $desktop_file_path" print_info "Application menu entry will be registered in: $app_dir" - + # ============================================================================= # ICON ACQUISITION AND CONFIGURATION # ============================================================================= - + # CUSTOM ICON DOWNLOAD: Get professional SteamGridDB icon for consistent branding # This provides the same visual identity as the Steam integration # SteamGridDB provides high-quality gaming artwork used by many Steam applications local icon_dir="$PWD/minecraft-splitscreen-icons" local icon_path="$icon_dir/minecraft-splitscreen-steamgriddb.ico" local icon_url="https://cdn2.steamgriddb.com/icon/add7a048049671970976f3e18f21ade3.ico" - + print_progress "Configuring desktop launcher icon..." mkdir -p "$icon_dir" # Ensure icon storage directory exists - + # ICON DOWNLOAD: Fetch SteamGridDB icon if not already present # This provides a professional-looking icon that matches Steam integration if [[ ! -f "$icon_path" ]]; then @@ -90,56 +120,52 @@ create_desktop_launcher() { else print_info " → Custom icon already present" fi - + # ============================================================================= # ICON SELECTION WITH FALLBACK HIERARCHY # ============================================================================= - + # ICON SELECTION: Determine the best available icon with intelligent fallbacks # Priority system ensures we always have a functional icon, preferring custom over generic local icon_desktop + local instance_icon_path="$ACTIVE_INSTANCES_DIR/latestUpdate-1/icon.png" + if [[ -f "$icon_path" ]]; then icon_desktop="$icon_path" # Best: Custom SteamGridDB icon print_info " → Using custom SteamGridDB icon for consistent branding" - elif [[ "$USE_POLLYMC" == true ]] && [[ -f "$HOME/.local/share/PollyMC/instances/latestUpdate-1/icon.png" ]]; then - icon_desktop="$HOME/.local/share/PollyMC/instances/latestUpdate-1/icon.png" # Good: PollyMC instance icon - print_info " → Using PollyMC instance icon" - elif [[ -f "$TARGET_DIR/instances/latestUpdate-1/icon.png" ]]; then - icon_desktop="$TARGET_DIR/instances/latestUpdate-1/icon.png" # Acceptable: PrismLauncher instance icon - print_info " → Using PrismLauncher instance icon" + elif [[ -f "$instance_icon_path" ]]; then + icon_desktop="$instance_icon_path" # Good: Instance icon from active launcher + print_info " → Using instance icon from $ACTIVE_LAUNCHER" else icon_desktop="application-x-executable" # Fallback: Generic system executable icon print_info " → Using system default executable icon" fi - + # ============================================================================= # LAUNCHER SCRIPT PATH CONFIGURATION # ============================================================================= - - # LAUNCHER SCRIPT PATH DETECTION: Set correct executable path based on active launcher - # The desktop file needs to point to the appropriate launcher script - # Different paths and descriptions for PollyMC vs PrismLauncher configurations - local launcher_script_path - local launcher_comment - if [[ "$USE_POLLYMC" == true ]]; then - launcher_script_path="$HOME/.local/share/PollyMC/minecraftSplitscreen.sh" - launcher_comment="Launch Minecraft splitscreen with PollyMC (optimized for offline gameplay)" - print_info " → Desktop launcher configured for PollyMC" - else - launcher_script_path="$TARGET_DIR/minecraftSplitscreen.sh" - launcher_comment="Launch Minecraft splitscreen with PrismLauncher" - print_info " → Desktop launcher configured for PrismLauncher" + + # LAUNCHER SCRIPT PATH: Use centralized path configuration + # The desktop file needs to point to ACTIVE_LAUNCHER_SCRIPT + if [[ -z "$ACTIVE_LAUNCHER_SCRIPT" ]]; then + print_error "ACTIVE_LAUNCHER_SCRIPT not set. Cannot create desktop launcher." + return 1 fi - + + local launcher_script_path="$ACTIVE_LAUNCHER_SCRIPT" + local launcher_comment="Launch Minecraft splitscreen with ${ACTIVE_LAUNCHER^}" + print_info " → Desktop launcher configured for ${ACTIVE_LAUNCHER^}" + print_info " → Script path: $launcher_script_path" + # ============================================================================= # DESKTOP ENTRY FILE GENERATION # ============================================================================= - + # DESKTOP FILE CREATION: Generate .desktop file following freedesktop.org specification # This creates a proper desktop entry that integrates with all Linux desktop environments # The file contains metadata, execution parameters, and display information print_progress "Generating desktop entry file..." - + # Desktop Entry Specification fields: # - Type=Application: Indicates this is an application launcher # - Name: Display name in menus and desktop @@ -148,7 +174,7 @@ create_desktop_launcher() { # - Icon: Icon file path or theme icon name # - Terminal: Whether to run in terminal (false for GUI applications) # - Categories: Menu categories for proper organization - + cat > "$desktop_file_path" </dev/null 2>&1; then update-desktop-database "$app_dir" 2>/dev/null || true print_success "✅ Desktop database updated - launcher available immediately" else print_info " → Desktop database update tool not found (launcher may need logout to appear)" fi - + # ============================================================================= # DESKTOP LAUNCHER COMPLETION SUMMARY # ============================================================================= - + print_success "🖥️ Desktop launcher setup complete!" print_info "" print_info "📋 Desktop Integration Summary:" @@ -231,14 +257,10 @@ EOF # ============================================================================= # DESKTOP LAUNCHER DECLINED # ============================================================================= - + print_info "⏭️ Skipping desktop launcher creation" print_info " → You can still launch via Steam (if configured) or manually run the script" print_info " → Manual launch command:" - if [[ "$USE_POLLYMC" == true ]]; then - print_info " $HOME/.local/share/PollyMC/minecraftSplitscreen.sh" - else - print_info " $TARGET_DIR/minecraftSplitscreen.sh" - fi + print_info " $ACTIVE_LAUNCHER_SCRIPT" fi } diff --git a/modules/instance_creation.sh b/modules/instance_creation.sh index 1b814af..fe26a4c 100644 --- a/modules/instance_creation.sh +++ b/modules/instance_creation.sh @@ -1,125 +1,166 @@ #!/bin/bash # ============================================================================= -# Minecraft Splitscreen Steam Deck Installer - Instance Creation Module -# ============================================================================= -# -# This module handles the creation of 4 separate Minecraft instances for splitscreen -# gameplay. Each instance is configured identically with mods but will be launched -# separately for multi-player splitscreen gaming. +# @file instance_creation.sh +# @version 2.0.0 +# @date 2026-01-24 +# @author FlyingEwok +# @license MIT +# @repository https://github.com/FlyingEwok/MinecraftSplitscreenSteamdeck +# +# @description +# Instance Creation Module for Minecraft Splitscreen Steam Deck Installer. +# Handles the creation of 4 separate Minecraft instances for splitscreen +# gameplay. Each instance is configured identically with mods but will be +# launched separately for multi-player splitscreen gaming. +# +# This module manages the complete lifecycle of instance creation including: +# - CLI-based instance creation via PrismLauncher +# - Manual fallback instance creation when CLI is unavailable +# - Fabric mod loader installation and configuration +# - Mod downloading and installation from Modrinth/CurseForge +# - Splitscreen audio configuration (music muted on instances 2-4) +# - Instance update handling with settings preservation +# +# @dependencies +# - path_configuration.sh (for CREATION_INSTANCES_DIR, CREATION_DATA_DIR) +# - ui_helpers.sh (for print_header, print_info, print_error, etc.) +# - mod_management.sh (for FINAL_MOD_INDEXES, MOD_URLS, SUPPORTED_MODS, etc.) +# - version_management.sh (for MC_VERSION, FABRIC_VERSION) +# - lwjgl_management.sh (for LWJGL_VERSION) +# - External: curl or wget, jq # -# Functions provided: -# - create_instances: Main function to create 4 splitscreen instances -# - install_fabric_and_mods: Install Fabric loader and mods for an instance +# @exports +# - create_instances(): Main function to create 4 splitscreen instances +# - install_fabric_and_mods(): Install Fabric loader and mods for an instance +# - handle_instance_update(): Handle updating an existing instance # +# @changelog +# 2026-01-24 - Added comprehensive documentation +# 2026-01-23 - Added instance update handling with settings preservation +# 2026-01-22 - Initial implementation with CLI and manual creation methods # ============================================================================= -# create_instances: Create 4 identical Minecraft instances for splitscreen play -# Uses PrismLauncher CLI when possible, falls back to manual creation if needed -# Each instance gets the same mods but separate configurations for splitscreen +# ============================================================================= +# MAIN INSTANCE CREATION FUNCTION +# ============================================================================= + +# @function create_instances +# @description +# Creates 4 identical Minecraft instances for splitscreen play. Uses +# PrismLauncher CLI when available, falling back to manual creation if needed. +# Each instance gets the same mods but separate configurations for splitscreen. +# +# The function handles both fresh installations and updates to existing +# instances, preserving user settings (options.txt) when updating. +# +# @global MC_VERSION - Target Minecraft version (read) +# @global FABRIC_VERSION - Fabric loader version to install (read) +# @global LWJGL_VERSION - LWJGL version for Minecraft (read) +# @global CREATION_INSTANCES_DIR - Directory where instances are created (read) +# @global FINAL_MOD_INDEXES - Array of mod indexes to install (read/write) +# @global MISSING_MODS - Array to track mods that fail to install (write) +# +# @stdout Progress messages and status updates +# @stderr Error messages for critical failures +# +# @return 0 on success, exits on critical failure +# +# @example +# MC_VERSION="1.21.3" +# FABRIC_VERSION="0.16.9" +# create_instances +# +# @note +# - Creates instances named latestUpdate-1 through latestUpdate-4 +# - Instance 1 downloads mods, instances 2-4 copy from instance 1 +# - Disables strict error handling during creation to prevent early exit create_instances() { - print_header "🚀 CREATING MINECRAFT INSTANCES" - + print_header "CREATING MINECRAFT INSTANCES" + # Verify required variables are set if [[ -z "${MC_VERSION:-}" ]]; then print_error "MC_VERSION is not set. Cannot create instances." exit 1 fi - + if [[ -z "${FABRIC_VERSION:-}" ]]; then print_error "FABRIC_VERSION is not set. Cannot create instances." exit 1 fi - + print_info "Creating instances for Minecraft $MC_VERSION with Fabric $FABRIC_VERSION" - + # Clean up the final mod selection list (remove any duplicates from dependency resolution) FINAL_MOD_INDEXES=( $(printf "%s\n" "${FINAL_MOD_INDEXES[@]}" | sort -u) ) - + # Initialize tracking for mods that fail to install MISSING_MODS=() - + + # Use centralized path configuration + # CREATION_INSTANCES_DIR is where we create instances (set by path_configuration.sh) + local instances_dir="$CREATION_INSTANCES_DIR" + + if [[ -z "$instances_dir" ]]; then + print_error "CREATION_INSTANCES_DIR not set. Call configure_launcher_paths() first." + exit 1 + fi + # Ensure instances directory exists - mkdir -p "$TARGET_DIR/instances" - + mkdir -p "$instances_dir" + # Check if we're updating existing instances - # We need to check both PrismLauncher and PollyMC directories local existing_instances=0 - local pollymc_dir="$HOME/.local/share/PollyMC" - local instances_dir="$TARGET_DIR/instances" - local using_pollymc=false - + for i in {1..4}; do local instance_name="latestUpdate-$i" - # Check in current TARGET_DIR (PrismLauncher) - if [[ -d "$TARGET_DIR/instances/$instance_name" ]]; then - existing_instances=$((existing_instances + 1)) - # Also check in PollyMC directory (for subsequent runs) - elif [[ -d "$pollymc_dir/instances/$instance_name" ]]; then + if [[ -d "$instances_dir/$instance_name" ]]; then existing_instances=$((existing_instances + 1)) - if [[ "$using_pollymc" == "false" ]]; then - instances_dir="$pollymc_dir/instances" - using_pollymc=true - print_info "Found existing instances in PollyMC directory" - fi fi done - + if [[ $existing_instances -gt 0 ]]; then - print_info "🔄 UPDATE MODE: Found $existing_instances existing instance(s)" - print_info " → Mods will be updated to match the selected Minecraft version" - print_info " → Your existing options.txt settings will be preserved" - print_info " → Instance configurations will be updated to new versions" - - # If we're updating from PollyMC, copy instances to working directory - if [[ "$using_pollymc" == "true" ]]; then - print_info " → Copying instances from PollyMC to workspace for processing..." - for i in {1..4}; do - local instance_name="latestUpdate-$i" - if [[ -d "$pollymc_dir/instances/$instance_name" ]]; then - cp -r "$pollymc_dir/instances/$instance_name" "$TARGET_DIR/instances/" - fi - done - # Now use the TARGET_DIR for processing - instances_dir="$TARGET_DIR/instances" - fi + print_info "UPDATE MODE: Found $existing_instances existing instance(s)" + print_info " -> Mods will be updated to match the selected Minecraft version" + print_info " -> Your existing options.txt settings will be preserved" + print_info " -> Instance configurations will be updated to new versions" else - print_info "🆕 FRESH INSTALL: Creating new splitscreen instances" + print_info "FRESH INSTALL: Creating new splitscreen instances" fi - + print_progress "Creating 4 splitscreen instances..." - + # Create exactly 4 instances: latestUpdate-1, latestUpdate-2, latestUpdate-3, latestUpdate-4 # This naming convention is expected by the splitscreen launcher script - + # Disable strict error handling for instance creation to prevent early exit print_info "Starting instance creation with improved error handling" set +e # Disable exit on error for this section - + for i in {1..4}; do local instance_name="latestUpdate-$i" local preserve_options_txt=false # Reset for each instance print_progress "Creating instance $i of 4: $instance_name" - + # Check if this is an update scenario - look in the correct instances directory if [[ -d "$instances_dir/$instance_name" ]]; then preserve_options_txt=$(handle_instance_update "$instances_dir/$instance_name" "$instance_name") fi - + # STAGE 1: Attempt CLI-based instance creation (preferred method) print_progress "Creating Minecraft $MC_VERSION instance with Fabric..." local cli_success=false - + # Check if PrismLauncher executable exists and is accessible local prism_exec if prism_exec=$(get_prism_executable) && [[ -x "$prism_exec" ]]; then # Try multiple CLI creation approaches with progressively fewer parameters # This handles different PrismLauncher versions that may have varying CLI support - + print_info "Attempting CLI instance creation..." - + # Temporarily disable strict error handling for CLI attempts set +e - + # Attempt 1: Full specification with Fabric loader if "$prism_exec" --cli create-instance \ --name "$instance_name" \ @@ -144,32 +185,32 @@ create_instances() { else print_info "All CLI creation attempts failed, will use manual method" fi - + # Re-enable strict error handling set -e else print_info "PrismLauncher executable not available, using manual method" fi - + # FALLBACK: Manual instance creation when CLI methods fail # This creates instances manually by writing configuration files directly # This ensures compatibility even with older PrismLauncher versions that lack CLI support if [[ "$cli_success" == false ]]; then print_info "Using manual instance creation method..." - local instance_dir="$TARGET_DIR/instances/$instance_name" - + local instance_dir="$instances_dir/$instance_name" + # Create instance directory structure mkdir -p "$instance_dir" || { print_error "Failed to create instance directory: $instance_dir" continue # Skip to next instance } - + # Create .minecraft subdirectory mkdir -p "$instance_dir/.minecraft" || { print_error "Failed to create .minecraft directory in $instance_dir" continue # Skip to next instance } - + # Create instance.cfg - PrismLauncher's main instance configuration file # This file defines the instance metadata, version, and launcher settings cat > "$instance_dir/instance.cfg" < Minecraft -> Intermediary -> Fabric # Components are loaded in dependency order to ensure proper mod support cat > "$instance_dir/mmc-pack.json" </dev/null; then print_progress "Adding Fabric loader to $instance_name..." - + # Create complete component stack with proper dependency chain - # Order matters: LWJGL3 → Minecraft → Intermediary Mappings → Fabric Loader + # Order matters: LWJGL3 -> Minecraft -> Intermediary Mappings -> Fabric Loader cat > "$pack_json" < 1) local instance_num="${instance_name##*-}" - + if [[ "$instance_num" == "1" ]]; then print_info "Downloading mods for first instance..." # Process each mod that was selected and has a compatible download URL @@ -385,31 +456,31 @@ EOF local mod_name="${SUPPORTED_MODS[$idx]}" local mod_id="${MOD_IDS[$idx]}" local mod_type="${MOD_TYPES[$idx]}" - + # RESOLVE MISSING URLs: For dependencies added without URLs, fetch the download URL now if [[ -z "$mod_url" || "$mod_url" == "null" ]] && [[ "$mod_type" == "modrinth" ]]; then print_progress "Resolving download URL for dependency: $mod_name" - + # Use the same comprehensive version matching as main mod compatibility checking local resolve_data="" local temp_resolve_file=$(mktemp) - + # Fetch all versions for this dependency local versions_url="https://api.modrinth.com/v2/project/$mod_id/version" local api_success=false - + if command -v curl >/dev/null 2>&1; then echo " Trying curl for $mod_name..." if curl -s -m 15 -o "$temp_resolve_file" "$versions_url" 2>/dev/null; then if [[ -s "$temp_resolve_file" ]]; then resolve_data=$(cat "$temp_resolve_file") api_success=true - echo " ✅ curl succeeded, got $(wc -c < "$temp_resolve_file") bytes" + echo " curl succeeded, got $(wc -c < "$temp_resolve_file") bytes" else - echo " ❌ curl returned empty file" + echo " curl returned empty file" fi else - echo " ❌ curl failed" + echo " curl failed" fi elif command -v wget >/dev/null 2>&1; then echo " Trying wget for $mod_name..." @@ -417,32 +488,32 @@ EOF if [[ -s "$temp_resolve_file" ]]; then resolve_data=$(cat "$temp_resolve_file") api_success=true - echo " ✅ wget succeeded, got $(wc -c < "$temp_resolve_file") bytes" + echo " wget succeeded, got $(wc -c < "$temp_resolve_file") bytes" else - echo " ❌ wget returned empty file" + echo " wget returned empty file" fi else - echo " ❌ wget failed" + echo " wget failed" fi fi - + # Debug: Save API response to a persistent file for examination local debug_file="/tmp/mod_${mod_name// /_}_${mod_id}_api_response.json" - + # More robust way to write the data if [[ -n "$resolve_data" ]]; then printf "%s" "$resolve_data" > "$debug_file" - echo "✅ Resolving data for $mod_name (ID: $mod_id) saved to: $debug_file" + echo "Resolving data for $mod_name (ID: $mod_id) saved to: $debug_file" echo " API URL: $versions_url" echo " Data length: ${#resolve_data} characters" else - echo "❌ No data received for $mod_name (ID: $mod_id)" - echo " API URL: $versions_url" + echo "No data received for $mod_name (ID: $mod_id)" + echo " API URL: $versions_url" echo " Check if the API call succeeded" # Special handling for known problematic dependencies if [[ "$mod_name" == *"Collective"* || "$mod_id" == "e0M1UDsY" ]]; then - echo " 💡 Note: Collective mod often has API issues and is usually an optional dependency" - echo " 💡 This is typically safe to ignore - the main mods will still work" + echo " Note: Collective mod often has API issues and is usually an optional dependency" + echo " This is typically safe to ignore - the main mods will still work" fi # Create empty file to indicate the attempt was made touch "$debug_file" @@ -450,30 +521,30 @@ EOF fi if [[ -n "$resolve_data" && "$resolve_data" != "[]" && "$resolve_data" != *"\"error\""* ]]; then - echo "🔍 DEBUG: Attempting URL resolution for $mod_name (MC: $MC_VERSION)" - + echo "DEBUG: Attempting URL resolution for $mod_name (MC: $MC_VERSION)" + # Try exact version match first mod_url=$(printf "%s" "$resolve_data" | jq -r --arg v "$MC_VERSION" '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .files[0].url' 2>/dev/null | head -n1) - echo " → Exact version match result: ${mod_url:-'(empty)'}" - - # Try major.minor version if exact match failed + echo " -> Exact version match result: ${mod_url:-'(empty)'}" + + # Try major.minor version if exact match failed if [[ -z "$mod_url" || "$mod_url" == "null" ]]; then local mc_major_minor mc_major_minor=$(echo "$MC_VERSION" | grep -oE '^[0-9]+\.[0-9]+') - echo " → Trying major.minor version: $mc_major_minor" - + echo " -> Trying major.minor version: $mc_major_minor" + # Try exact major.minor mod_url=$(printf "%s" "$resolve_data" | jq -r --arg v "$mc_major_minor" '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .files[0].url' 2>/dev/null | head -n1) - echo " → Major.minor match result: ${mod_url:-'(empty)'}" - + echo " -> Major.minor match result: ${mod_url:-'(empty)'}" + # Try wildcard version (e.g., "1.21.x") if [[ -z "$mod_url" || "$mod_url" == "null" ]]; then local mc_major_minor_x="$mc_major_minor.x" - echo " → Trying wildcard version: $mc_major_minor_x" + echo " -> Trying wildcard version: $mc_major_minor_x" mod_url=$(printf "%s" "$resolve_data" | jq -r --arg v "$mc_major_minor_x" '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .files[0].url' 2>/dev/null | head -n1) - echo " → Wildcard match result: ${mod_url:-'(empty)'}" + echo " -> Wildcard match result: ${mod_url:-'(empty)'}" fi - + # Try limited previous patch version (more restrictive than prefix matching) if [[ -z "$mod_url" || "$mod_url" == "null" ]]; then local mc_patch_version @@ -482,40 +553,40 @@ EOF # Try one patch version down (e.g., if looking for 1.21.6, try 1.21.5) local prev_patch=$((mc_patch_version - 1)) local mc_prev_version="$mc_major_minor.$prev_patch" - echo " → Trying limited backwards compatibility with: $mc_prev_version" + echo " -> Trying limited backwards compatibility with: $mc_prev_version" mod_url=$(printf "%s" "$resolve_data" | jq -r --arg v "$mc_prev_version" '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .files[0].url' 2>/dev/null | head -n1) - echo " → Limited backwards compatibility result: ${mod_url:-'(empty)'}" + echo " -> Limited backwards compatibility result: ${mod_url:-'(empty)'}" fi fi fi - + # If still no URL found, try the latest Fabric version for any compatible release if [[ -z "$mod_url" || "$mod_url" == "null" ]]; then - echo " → Trying latest Fabric version (any compatible release)" + echo " -> Trying latest Fabric version (any compatible release)" mod_url=$(printf "%s" "$resolve_data" | jq -r '.[] | select(.loaders[] == "fabric") | .files[0].url' 2>/dev/null | head -n1) - echo " → Latest Fabric match result: ${mod_url:-'(empty)'}" + echo " -> Latest Fabric match result: ${mod_url:-'(empty)'}" fi - - echo "🎯 FINAL URL for $mod_name: ${mod_url:-'(none found)'}" + + echo "FINAL URL for $mod_name: ${mod_url:-'(none found)'}" fi - + rm -f "$temp_resolve_file" 2>/dev/null fi - + # RESOLVE MISSING URLs for CurseForge dependencies if [[ -z "$mod_url" || "$mod_url" == "null" ]] && [[ "$mod_type" == "curseforge" ]]; then print_progress "Resolving download URL for CurseForge dependency: $mod_name" - + # Use our robust CurseForge URL resolution function mod_url=$(get_curseforge_download_url "$mod_id") - + if [[ -n "$mod_url" && "$mod_url" != "null" ]]; then print_success "Found compatible CurseForge file for $mod_name" else print_warning "No compatible CurseForge file found for $mod_name" fi fi - + # SKIP INVALID MODS: Handle cases where URL couldn't be resolved if [[ -z "$mod_url" || "$mod_url" == "null" ]]; then # Check if this is a critical required mod vs. optional dependency @@ -526,23 +597,23 @@ EOF break fi done - + if [[ "$is_required" == true ]]; then - print_error "❌ CRITICAL: Required mod '$mod_name' could not be downloaded!" + print_error "CRITICAL: Required mod '$mod_name' could not be downloaded!" print_error " This mod is essential for splitscreen functionality." - print_info " → However, continuing to create remaining instances..." - print_info " → You may need to manually install this mod later." + print_info " -> However, continuing to create remaining instances..." + print_info " -> You may need to manually install this mod later." MISSING_MODS+=("$mod_name") # Track for final summary continue else - print_warning "⚠️ Optional dependency '$mod_name' could not be downloaded." - print_info " → This is likely a dependency that doesn't support Minecraft $MC_VERSION" - print_info " → Continuing installation without this optional dependency" + print_warning "Optional dependency '$mod_name' could not be downloaded." + print_info " -> This is likely a dependency that doesn't support Minecraft $MC_VERSION" + print_info " -> Continuing installation without this optional dependency" MISSING_MODS+=("$mod_name") # Track for final summary continue fi fi - + # DOWNLOAD MOD FILE: Attempt to download the mod .jar file # Filename is sanitized (spaces replaced with underscores) for filesystem compatibility local mod_file="$mods_dir/${mod_name// /_}.jar" @@ -556,11 +627,11 @@ EOF else # For instances 2-4, copy mods from instance 1 print_info "Copying mods from instance 1 to $instance_name..." - local instance1_mods_dir="$TARGET_DIR/instances/latestUpdate-1/.minecraft/mods" + local instance1_mods_dir="$CREATION_INSTANCES_DIR/latestUpdate-1/.minecraft/mods" if [[ -d "$instance1_mods_dir" ]]; then cp -r "$instance1_mods_dir"/* "$mods_dir/" 2>/dev/null if [[ $? -eq 0 ]]; then - print_success "✅ Successfully copied mods from instance 1" + print_success "Successfully copied mods from instance 1" else print_error "Failed to copy mods from instance 1" fi @@ -568,38 +639,38 @@ EOF print_error "Could not find mods directory from instance 1" fi fi - + # ============================================================================= # MINECRAFT AUDIO CONFIGURATION # ============================================================================= - + # SPLITSCREEN AUDIO SETUP: Configure music volume for each instance # Instance 1 keeps music at default volume (0.3), instances 2-4 have music muted # This prevents audio overlap when multiple instances are running simultaneously print_progress "Configuring splitscreen audio settings for $instance_name..." - + # Extract instance number from instance name (latestUpdate-X format) local instance_number instance_number=$(echo "$instance_name" | grep -oE '[0-9]+$') - + # Determine music volume based on instance number local music_volume="0.3" # Default music volume if [[ "$instance_number" -gt 1 ]]; then music_volume="0.0" # Mute music for instances 2, 3, and 4 - print_info " → Music muted for $instance_name (prevents audio overlap)" + print_info " -> Music muted for $instance_name (prevents audio overlap)" else - print_info " → Music enabled for $instance_name (primary audio instance)" + print_info " -> Music enabled for $instance_name (primary audio instance)" fi - + # Create or update Minecraft options.txt file with splitscreen-optimized settings # This file contains all Minecraft client settings including audio, graphics, and controls local options_file="$instance_dir/.minecraft/options.txt" - + # Skip creating options.txt if we're preserving existing user settings if [[ "$preserve_options" == "true" ]] && [[ -f "$options_file" ]]; then - print_info " → Preserving existing options.txt settings" + print_info " -> Preserving existing options.txt settings" else - print_info " → Creating default splitscreen-optimized options.txt" + print_info " -> Creating default splitscreen-optimized options.txt" mkdir -p "$(dirname "$options_file")" cat > "$options_file" < This will update the instance to MC $MC_VERSION with Fabric $FABRIC_VERSION" + print_info " -> Your existing settings and preferences will be preserved" + # Check if there's a mods folder and clear it local mods_dir="$instance_dir/.minecraft/mods" if [[ -d "$mods_dir" ]]; then print_progress "Clearing old mods from $instance_name..." rm -rf "$mods_dir" - print_success "✅ Old mods cleared" + print_success "Old mods cleared" else - print_info "ℹ️ No existing mods folder found - will create fresh mod installation" + print_info "No existing mods folder found - will create fresh mod installation" fi - + # Ensure .minecraft directory exists mkdir -p "$instance_dir/.minecraft" - + # Check if options.txt exists local options_file="$instance_dir/.minecraft/options.txt" if [[ -f "$options_file" ]]; then - print_info "✅ Preserving existing options.txt (user settings will be kept)" + print_info "Preserving existing options.txt (user settings will be kept)" # Create a backup of options.txt cp "$options_file" "${options_file}.backup" else - print_info "ℹ️ No existing options.txt found - will create default splitscreen settings" + print_info "No existing options.txt found - will create default splitscreen settings" fi - + # Update the instance configuration files to match the new version # This ensures the instance uses the correct Minecraft and Fabric versions print_progress "Updating instance configuration for MC $MC_VERSION with Fabric $FABRIC_VERSION..." - + # Update instance.cfg if [[ -f "$instance_dir/instance.cfg" ]]; then # Update the IntendedVersion line sed -i "s/^IntendedVersion=.*/IntendedVersion=$MC_VERSION/" "$instance_dir/instance.cfg" - print_success "✅ Instance configuration updated" + print_success "Instance configuration updated" fi - + # Perform fabric and mod installation, making sure to preserve options.txt install_fabric_and_mods "$instance_dir" "$instance_name" true - + # Restore options.txt if it was backed up if [[ -f "${options_file}.backup" ]]; then mv "${options_file}.backup" "$options_file" - print_info "✅ Restored user's options.txt settings" + print_info "Restored user's options.txt settings" fi - + # Update mmc-pack.json with new component versions cat > "$instance_dir/mmc-pack.json" < Mods cleared and ready for new installation" + print_info " -> User settings preserved" + print_info " -> Version updated to MC $MC_VERSION with Fabric $FABRIC_VERSION" + # Return true if we found and preserved an options.txt file if [[ -f "$options_file" ]]; then echo "true" diff --git a/modules/java_management.sh b/modules/java_management.sh index 8603e9c..d20d50d 100644 --- a/modules/java_management.sh +++ b/modules/java_management.sh @@ -1,26 +1,71 @@ #!/bin/bash # ============================================================================= -# JAVA MANAGEMENT MODULE +# @file java_management.sh +# @version 2.0.0 +# @date 2026-01-25 +# @author Minecraft Splitscreen Steam Deck Project +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Automatic Java detection, installation, and management for Minecraft. +# Determines the correct Java version required for any Minecraft version +# by querying Mojang's API or using fallback version mappings. +# +# Key features: +# - Automatic Java version detection from Mojang API +# - Fallback mappings: 1.21+ → Java 21, 1.18-1.20 → Java 17, 1.17 → Java 16, older → Java 8 +# - User-space installation to ~/.local/jdk/ (no root required) +# - Multi-version coexistence support +# - Automatic environment variable configuration +# +# @dependencies +# - utilities.sh (for print_header, print_success, print_warning, print_error, print_info, print_progress) +# - curl (for Mojang API requests) +# - jq (for JSON parsing) +# - git (for downloading JDK installer) +# +# @global_outputs +# - JAVA_PATH: Path to the detected/installed Java executable +# +# @exports +# Functions: +# - get_required_java_version : Determine Java version for MC version +# - download_and_run_jdk_installer : Install Java automatically +# - find_java_installation : Search for existing Java installation +# - detect_and_install_java : Main function - find or install Java +# - detect_java : Legacy alias for detect_and_install_java +# +# @changelog +# 2.0.0 (2026-01-25) - Added comprehensive JSDoc documentation +# 1.0.0 (2024-XX-XX) - Initial implementation # ============================================================================= -# Automatic Java detection, installation and management functions -# get_required_java_version: Determine the required Java version for a Minecraft version -# Fetches version manifest from Mojang API to get the official Java requirements -# Parameters: -# $1 - mc_version: Minecraft version (e.g., "1.21.3") -# Returns: Java version number (e.g., "21", "17", "8") or exits on error +# ============================================================================= +# JAVA VERSION DETECTION +# ============================================================================= + +# @function get_required_java_version +# @description Determine the required Java version for a Minecraft version. +# Queries Mojang's version manifest API for official requirements, +# falling back to hardcoded mappings if API unavailable. +# @param $1 - mc_version: Minecraft version (e.g., "1.21.3") +# @stdout Java version number (e.g., "21", "17", "8") +# @return 0 on success, 1 if mc_version is empty +# @example +# required_java=$(get_required_java_version "1.21.3") # Returns "21" get_required_java_version() { local mc_version="$1" - + if [[ -z "$mc_version" ]]; then return 1 fi - + # Get version manifest from Mojang API (silent) local manifest_url="https://piston-meta.mojang.com/mc/game/version_manifest_v2.json" local manifest_json manifest_json=$(curl -s "$manifest_url" 2>/dev/null) - + if [[ -z "$manifest_json" ]]; then # Fallback logic based on known Minecraft Java requirements if [[ "$mc_version" =~ ^1\.2[1-9](\.|$) ]]; then @@ -36,11 +81,11 @@ get_required_java_version() { fi return 0 fi - + # Extract the version-specific manifest URL local version_url version_url=$(echo "$manifest_json" | jq -r --arg v "$mc_version" '.versions[] | select(.id == $v) | .url' 2>/dev/null) - + if [[ -z "$version_url" || "$version_url" == "null" ]]; then # Use same fallback logic as above if [[ "$mc_version" =~ ^1\.2[1-9](\.|$) ]]; then @@ -56,11 +101,11 @@ get_required_java_version() { fi return 0 fi - + # Fetch the specific version manifest (silent) local version_json version_json=$(curl -s "$version_url" 2>/dev/null) - + if [[ -z "$version_json" ]]; then # Fallback logic if [[ "$mc_version" =~ ^1\.2[1-9](\.|$) ]]; then @@ -76,11 +121,11 @@ get_required_java_version() { fi return 0 fi - + # Extract Java version requirement from the manifest local java_version java_version=$(echo "$version_json" | jq -r '.javaVersion.majorVersion // empty' 2>/dev/null) - + if [[ -n "$java_version" && "$java_version" != "null" ]]; then echo "$java_version" else @@ -99,21 +144,29 @@ get_required_java_version() { fi } -# download_and_run_jdk_installer: Download and execute the automatic JDK installer -# Downloads the JDK installer from the GitHub repository and runs it automatically -# Parameters: -# $1 - required_version: Required Java major version (e.g., "21", "17", "8") +# ============================================================================= +# JAVA INSTALLATION +# ============================================================================= + +# @function download_and_run_jdk_installer +# @description Download and execute the automatic JDK installer from GitHub. +# Installs Java to ~/.local/jdk/ without requiring root access. +# @param $1 - required_version: Required Java major version (e.g., "21", "17", "8") +# @env JDK_VERSION - Set to required_version for automated installation +# @return 0 on successful installation, 1 on failure +# @example +# download_and_run_jdk_installer "21" download_and_run_jdk_installer() { local required_version="$1" local temp_dir temp_dir=$(mktemp -d) local original_dir="$PWD" - + if [[ -z "$temp_dir" ]]; then print_error "Failed to create temporary directory for JDK installer" return 1 fi - + # Check if git is available if ! command -v git >/dev/null 2>&1; then print_error "Git is required to download the JDK installer but is not installed" @@ -121,15 +174,15 @@ download_and_run_jdk_installer() { rm -rf "$temp_dir" return 1 fi - + cd "$temp_dir" || { print_error "Failed to enter temporary directory" rm -rf "$temp_dir" return 1 } - + print_progress "Downloading automatic JDK installer..." - + # Clone the JDK installer repository if ! git clone --quiet https://github.com/FlyingEwok/install-jdk-on-steam-deck.git 2>/dev/null; then print_error "Failed to download JDK installer from GitHub" @@ -138,16 +191,16 @@ download_and_run_jdk_installer() { rm -rf "$temp_dir" return 1 fi - + # Make the install script executable chmod +x install-jdk-on-steam-deck/scripts/install-jdk.sh - + print_info "Running automatic JDK $required_version installer..." print_info "This will install Java $required_version to ~/.local/jdk/ (no root access required)" - + # Set environment variable to install specific version automatically export JDK_VERSION="$required_version" - + # Run the installer in automated mode if ./install-jdk-on-steam-deck/scripts/install-jdk.sh; then print_success "Java $required_version installed successfully!" @@ -162,15 +215,22 @@ download_and_run_jdk_installer() { fi } -# find_java_installation: Find a Java installation of the specified version -# Searches both system locations and the automatic installer location -# Parameters: -# $1 - required_version: Required Java major version (e.g., "21", "17", "8") -# Returns: Path to Java executable or empty string if not found +# ============================================================================= +# JAVA DETECTION +# ============================================================================= + +# @function find_java_installation +# @description Find an existing Java installation of the specified version. +# Search order: JAVA_N_HOME env vars → ~/.local/jdk/ → system paths → PATH +# @param $1 - required_version: Required Java major version (e.g., "21", "17", "8") +# @stdout Path to Java executable, or empty string if not found +# @return 0 if found, implicit failure if not found (empty stdout) +# @example +# java_path=$(find_java_installation "21") find_java_installation() { local required_version="$1" local java_path="" - + # First, check the automatic installer location (~/.local/jdk) local jdk_home_var="JAVA_${required_version}_HOME" if [[ -n "${!jdk_home_var:-}" && -x "${!jdk_home_var}/bin/java" ]]; then @@ -178,7 +238,7 @@ find_java_installation() { echo "$java_path" return 0 fi - + # Check ~/.local/jdk directory directly (in case env vars aren't loaded) if [[ -d "$HOME/.local/jdk" ]]; then for jdk_dir in "$HOME/.local/jdk"/*/; do @@ -226,7 +286,7 @@ find_java_installation() { fi done fi - + # Check system locations if not found in ~/.local/jdk if [[ -z "$java_path" ]]; then case "$required_version" in @@ -289,7 +349,7 @@ find_java_installation() { ;; esac fi - + # Check system default java and validate version if [[ -z "$java_path" ]] && command -v java >/dev/null 2>&1; then local version_output @@ -327,42 +387,53 @@ find_java_installation() { ;; esac fi - + echo "$java_path" } -# detect_and_install_java: Find required Java version and install if needed -# This function automatically detects the required Java version, searches for it, -# and installs it automatically if not found. No user interaction required. -# Must be called after MC_VERSION is set +# ============================================================================= +# MAIN ENTRY POINT +# ============================================================================= + +# @function detect_and_install_java +# @description Main function to find required Java version and install if needed. +# Automatically detects requirements, searches for existing installation, +# and installs if not found. No user interaction required. +# @global MC_VERSION - (input) Must be set before calling +# @global JAVA_PATH - (output) Set to path of Java executable +# @return 0 on success, exits on failure +# @example +# MC_VERSION="1.21.3" +# detect_and_install_java +# echo "Java at: $JAVA_PATH" detect_and_install_java() { if [[ -z "${MC_VERSION:-}" ]]; then print_error "MC_VERSION must be set before calling detect_and_install_java" exit 1 fi - + print_header "☕ AUTOMATIC JAVA SETUP" - + # Get the required Java version for this Minecraft version print_progress "Checking Java requirements for Minecraft $MC_VERSION..." local required_java_version required_java_version=$(get_required_java_version "$MC_VERSION") - + print_info "Minecraft $MC_VERSION requires Java $required_java_version" - + # Search for existing Java installation print_progress "Searching for Java $required_java_version installation..." - + # Source the profile to get any existing Java environment variables [[ -f ~/.profile ]] && source ~/.profile 2>/dev/null || true - + JAVA_PATH=$(find_java_installation "$required_java_version") - + if [[ -n "$JAVA_PATH" ]]; then # Validate that the found Java is actually the correct version local java_version_output java_version_output=$("$JAVA_PATH" -version 2>&1) - + # Verify version matches requirement local version_matches=false case "$required_java_version" in @@ -397,7 +468,7 @@ detect_and_install_java() { fi ;; esac - + if [[ "$version_matches" == true ]]; then print_success "Found compatible Java $required_java_version at: $JAVA_PATH" local java_version_line @@ -409,7 +480,7 @@ detect_and_install_java() { JAVA_PATH="" # Clear invalid path fi fi - + # Java not found or wrong version - install automatically print_warning "Java $required_java_version not found on system" print_info "Automatically installing Java $required_java_version using Steam Deck JDK installer..." @@ -418,15 +489,15 @@ detect_and_install_java() { print_info " • Installs to ~/.local/jdk/ (no root access needed)" print_info " • Supports multiple Java versions side-by-side" print_info " • Sets up proper environment variables automatically" - + # Attempt automatic installation if download_and_run_jdk_installer "$required_java_version"; then # Source the updated profile to load new environment variables [[ -f ~/.profile ]] && source ~/.profile 2>/dev/null || true - + # Try to find the newly installed Java JAVA_PATH=$(find_java_installation "$required_java_version") - + if [[ -n "$JAVA_PATH" ]]; then print_success "Java $required_java_version automatically installed and configured!" local java_version_output @@ -470,7 +541,9 @@ detect_and_install_java() { fi } -# Legacy function name for backward compatibility +# @function detect_java +# @description Legacy alias for detect_and_install_java (backward compatibility). +# @see detect_and_install_java detect_java() { detect_and_install_java } diff --git a/modules/launcher_script_generator.sh b/modules/launcher_script_generator.sh new file mode 100644 index 0000000..4ab74c1 --- /dev/null +++ b/modules/launcher_script_generator.sh @@ -0,0 +1,531 @@ +#!/bin/bash +# ============================================================================= +# @file launcher_script_generator.sh +# @version 2.0.0 +# @date 2026-01-25 +# @author Minecraft Splitscreen Steam Deck Project +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Generates the minecraftSplitscreen.sh launcher script with correct paths +# baked in based on the detected launcher configuration. The generated script +# handles Steam Deck Game Mode detection, controller counting, splitscreen +# configuration, and instance launching. +# +# Key features: +# - Template-based script generation with placeholder replacement +# - Support for both AppImage and Flatpak launchers +# - Steam Deck Game Mode detection with nested Plasma session +# - Controller detection with Steam Input duplicate handling +# - Per-instance splitscreen.properties configuration +# +# @dependencies +# - git (for commit hash embedding, optional) +# - sed (for placeholder replacement) +# +# @exports +# Functions: +# - generate_splitscreen_launcher : Main generation function +# - verify_generated_script : Validation utility +# - print_generation_config : Debug/info utility +# +# @changelog +# 2.0.0 (2026-01-25) - Added comprehensive JSDoc documentation +# 1.0.0 (2024-XX-XX) - Initial implementation +# ============================================================================= + +# ============================================================================= +# MAIN GENERATOR FUNCTION +# ============================================================================= + +# @function generate_splitscreen_launcher +# @description Generate the minecraftSplitscreen.sh launcher script with +# configuration values baked in via placeholder replacement. +# @param $1 - output_path: Path for the generated script +# @param $2 - launcher_name: "PollyMC" or "PrismLauncher" +# @param $3 - launcher_type: "appimage" or "flatpak" +# @param $4 - launcher_exec: Full path or flatpak command +# @param $5 - launcher_dir: Launcher data directory +# @param $6 - instances_dir: Instances directory path +# @global SCRIPT_VERSION - (input, optional) Version string for embedding +# @global REPO_URL - (input, optional) Repository URL for embedding +# @return 0 on success +# @example +# generate_splitscreen_launcher "/path/to/script.sh" "PollyMC" "flatpak" \ +# "flatpak run org.fn2006.PollyMC" "/home/user/.var/app/org.fn2006.PollyMC/data/PollyMC" \ +# "/home/user/.var/app/org.fn2006.PollyMC/data/PollyMC/instances" +generate_splitscreen_launcher() { + local output_path="$1" + local launcher_name="$2" + local launcher_type="$3" + local launcher_exec="$4" + local launcher_dir="$5" + local instances_dir="$6" + + # Get version info + local generation_date + local commit_hash + generation_date=$(date -Iseconds 2>/dev/null || date "+%Y-%m-%dT%H:%M:%S%z") + commit_hash=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") + + # Ensure output directory exists + mkdir -p "$(dirname "$output_path")" + + # Generate the script using heredoc + # Note: We use a mix of quoted and unquoted heredoc markers: + # - 'EOF' (quoted) prevents variable expansion in the heredoc + # - We then use sed to replace placeholders with actual values + cat > "$output_path" << 'LAUNCHER_SCRIPT_EOF' +#!/bin/bash +# ============================================================================= +# Minecraft Splitscreen Launcher for Steam Deck & Linux +# ============================================================================= +# Version: __SCRIPT_VERSION__ (commit: __COMMIT_HASH__) +# Generated: __GENERATION_DATE__ +# Generator: install-minecraft-splitscreen.sh v__SCRIPT_VERSION__ +# Source: __REPO_URL__ +# +# DO NOT EDIT - This file is auto-generated by the installer. +# To update, re-run the installer script. +# ============================================================================= +# +# This script launches 1-4 Minecraft instances in splitscreen mode. +# On Steam Deck Game Mode, it launches a nested KDE Plasma session. +# On desktop mode, it launches Minecraft instances directly. +# +# Features: +# - Controller detection (1-4 players) +# - Per-instance splitscreen configuration +# - KDE panel hiding/restoring +# - Steam Input duplicate device handling +# - Nested Plasma session for Steam Deck Game Mode +# ============================================================================= + +set +e # Allow script to continue on errors for robustness + +# ============================================================================= +# GENERATED CONFIGURATION - DO NOT MODIFY +# ============================================================================= +# These values were set by the installer based on your system configuration. + +LAUNCHER_NAME="__LAUNCHER_NAME__" +LAUNCHER_TYPE="__LAUNCHER_TYPE__" +LAUNCHER_EXEC="__LAUNCHER_EXEC__" +LAUNCHER_DIR="__LAUNCHER_DIR__" +INSTANCES_DIR="__INSTANCES_DIR__" + +# ============================================================================= +# END GENERATED CONFIGURATION +# ============================================================================= + +# Temporary directory for intermediate files +export target=/tmp + +# ============================================================================= +# LOGGING (prints to terminal AND logs to file) +# ============================================================================= + +LOG_DIR="$HOME/.local/share/MinecraftSplitscreen/logs" +LOG_FILE="" + +_init_log() { + mkdir -p "$LOG_DIR" 2>/dev/null || { LOG_DIR="/tmp/MinecraftSplitscreen/logs"; mkdir -p "$LOG_DIR"; } + LOG_FILE="$LOG_DIR/launcher-$(date +%Y-%m-%d-%H%M%S).log" + # Rotate old logs (keep last 10) + local c=0; while IFS= read -r f; do c=$((c+1)); [[ $c -gt 10 ]] && rm -f "$f"; done < <(ls -t "$LOG_DIR"/launcher-*.log 2>/dev/null) + { echo "=== Minecraft Splitscreen Launcher ==="; echo "Started: $(date)"; echo ""; } >> "$LOG_FILE" +} + +log() { [[ -n "$LOG_FILE" ]] && echo "[$(date '+%H:%M:%S')] $*" >> "$LOG_FILE" 2>/dev/null; } +log_info() { echo "[Info] $*"; log "INFO: $*"; } +log_error() { echo "[Error] $*" >&2; log "ERROR: $*"; } +log_warning() { echo "[Warning] $*"; log "WARNING: $*"; } + +_init_log + +# ============================================================================= +# Launcher Validation +# ============================================================================= + +# Validate that the configured launcher is available +validate_launcher() { + local launcher_available=false + + if [[ "$LAUNCHER_TYPE" == "flatpak" ]]; then + # For Flatpak, check if the app is installed + local flatpak_id + case "$LAUNCHER_NAME" in + "PollyMC") flatpak_id="org.fn2006.PollyMC" ;; + "PrismLauncher") flatpak_id="org.prismlauncher.PrismLauncher" ;; + esac + if command -v flatpak >/dev/null 2>&1 && flatpak list --app 2>/dev/null | grep -q "$flatpak_id"; then + launcher_available=true + fi + else + # For AppImage, check if the executable exists + # Handle both direct path and "flatpak run" style commands + local exec_path + exec_path=$(echo "$LAUNCHER_EXEC" | awk '{print $1}') + if [[ -x "$exec_path" ]] || command -v "$exec_path" >/dev/null 2>&1; then + launcher_available=true + fi + fi + + if [[ "$launcher_available" == false ]]; then + log_error "$LAUNCHER_NAME not found!" + log_error "Expected: $LAUNCHER_EXEC" + log_error "Please re-run the Minecraft Splitscreen installer." + return 1 + fi + + return 0 +} + +# Validate launcher at startup +if ! validate_launcher; then + exit 1 +fi + +log_info "Using $LAUNCHER_NAME ($LAUNCHER_TYPE) for splitscreen gameplay" +log_info "Log file: $LOG_FILE" + +# ============================================================================= +# Nested Plasma Session (Steam Deck Game Mode) +# ============================================================================= + +# Launches a nested KDE Plasma Wayland session and sets up Minecraft autostart. +# Needed so Minecraft can run in a clean, isolated desktop environment. +nestedPlasma() { + # Unset variables that may interfere with launching a nested session + unset LD_PRELOAD XDG_DESKTOP_PORTAL_DIR XDG_SEAT_PATH XDG_SESSION_PATH + + # Get current screen resolution + local RES + RES=$(xdpyinfo 2>/dev/null | awk '/dimensions/{print $2}') + [ -z "$RES" ] && RES="1280x800" + + # Create a wrapper for kwin_wayland with the correct resolution + cat < "$target/kwin_wayland_wrapper" +#!/bin/bash +/usr/bin/kwin_wayland_wrapper --width ${RES%x*} --height ${RES#*x} --no-lockscreen \$@ +EOF + chmod +x "$target/kwin_wayland_wrapper" + export PATH="$target:$PATH" + + # Write an autostart .desktop file that will re-invoke this script + local SCRIPT_PATH + SCRIPT_PATH="$(readlink -f "$0")" + mkdir -p ~/.config/autostart + cat < ~/.config/autostart/minecraft-launch.desktop +[Desktop Entry] +Name=Minecraft Split Launch +Exec=$SCRIPT_PATH launchFromPlasma +Type=Application +X-KDE-AutostartScript=true +EOF + + # Start nested Plasma session (never returns) + exec dbus-run-session startplasma-wayland +} + +# ============================================================================= +# Game Launching +# ============================================================================= + +# Launch a single Minecraft instance with KDE inhibition +# Arguments: +# $1 = Instance name (e.g., latestUpdate-1) +# $2 = Player name (e.g., P1) +launchGame() { + local instance_name="$1" + local player_name="$2" + + if command -v kde-inhibit >/dev/null 2>&1; then + kde-inhibit --power --screenSaver --colorCorrect --notifications \ + $LAUNCHER_EXEC -l "$instance_name" -a "$player_name" & + else + log_warning "kde-inhibit not found. Running $LAUNCHER_NAME without KDE inhibition." + $LAUNCHER_EXEC -l "$instance_name" -a "$player_name" & + fi + + sleep 10 # Give time for the instance to start +} + +# ============================================================================= +# KDE Panel Management +# ============================================================================= + +# Hide KDE panels by killing plasmashell +hidePanels() { + if command -v plasmashell >/dev/null 2>&1; then + pkill plasmashell + sleep 1 + if pgrep -u "$USER" plasmashell >/dev/null; then + killall plasmashell + sleep 1 + fi + if pgrep -u "$USER" plasmashell >/dev/null; then + pkill -9 plasmashell + sleep 1 + fi + else + log_info "plasmashell not found. Skipping KDE panel hiding." + fi +} + +# Restore KDE panels by restarting plasmashell +restorePanels() { + if command -v plasmashell >/dev/null 2>&1; then + nohup plasmashell >/dev/null 2>&1 & + sleep 2 + else + log_info "plasmashell not found. Skipping KDE panel restore." + fi +} + +# ============================================================================= +# Controller Detection +# ============================================================================= + +# Detect the number of controllers (1-4) +# Handles Steam Input device duplication when Steam is running +getControllerCount() { + local count + local steam_running=0 + + # Count all joystick/gamepad devices + count=$(ls /dev/input/js* 2>/dev/null | wc -l) + + # Check if Steam is running (native or Flatpak) + if pgrep -x steam >/dev/null \ + || pgrep -f '^/app/bin/steam$' >/dev/null \ + || pgrep -f 'flatpak run com.valvesoftware.Steam' >/dev/null; then + steam_running=1 + fi + + # Halve count if Steam is running (Steam Input creates duplicates) + if [ "$steam_running" -eq 1 ]; then + count=$(( (count + 1) / 2 )) + fi + + # Clamp between 1 and 4 + [ "$count" -gt 4 ] && count=4 + [ "$count" -lt 1 ] && count=1 + + echo "$count" +} + +# ============================================================================= +# Splitscreen Configuration +# ============================================================================= + +# Write splitscreen.properties for a player instance +# Arguments: +# $1 = Player number (1-4) +# $2 = Total number of controllers/players +setSplitscreenModeForPlayer() { + local player=$1 + local numberOfControllers=$2 + local config_path="$INSTANCES_DIR/latestUpdate-${player}/.minecraft/config/splitscreen.properties" + + mkdir -p "$(dirname "$config_path")" + + local mode="FULLSCREEN" + case "$numberOfControllers" in + 1) + mode="FULLSCREEN" + ;; + 2) + if [ "$player" = 1 ]; then mode="TOP"; else mode="BOTTOM"; fi + ;; + 3) + if [ "$player" = 1 ]; then mode="TOP" + elif [ "$player" = 2 ]; then mode="BOTTOM_LEFT" + else mode="BOTTOM_RIGHT"; fi + ;; + 4) + if [ "$player" = 1 ]; then mode="TOP_LEFT" + elif [ "$player" = 2 ]; then mode="TOP_RIGHT" + elif [ "$player" = 3 ]; then mode="BOTTOM_LEFT" + else mode="BOTTOM_RIGHT"; fi + ;; + esac + + echo -e "gap=1\nmode=$mode" > "$config_path" + sync + sleep 0.5 +} + +# ============================================================================= +# Main Game Launch Logic +# ============================================================================= + +# Launch all games based on controller count +launchGames() { + hidePanels + + local numberOfControllers + numberOfControllers=$(getControllerCount) + + for player in $(seq 1 $numberOfControllers); do + setSplitscreenModeForPlayer "$player" "$numberOfControllers" + launchGame "latestUpdate-$player" "P$player" + done + + wait + restorePanels + sleep 2 +} + +# ============================================================================= +# Steam Deck Detection +# ============================================================================= + +# Returns 0 if running on Steam Deck in Game Mode +isSteamDeckGameMode() { + local dmi_file="/sys/class/dmi/id/product_name" + local dmi_contents="" + + if [ -f "$dmi_file" ]; then + dmi_contents="$(cat "$dmi_file" 2>/dev/null)" + fi + + if echo "$dmi_contents" | grep -Ei 'Steam Deck|Jupiter' >/dev/null; then + if [ "$XDG_SESSION_DESKTOP" = "gamescope" ] && [ "$XDG_CURRENT_DESKTOP" = "gamescope" ]; then + return 0 + fi + if pgrep -af 'steam' | grep -q '\-gamepadui'; then + return 0 + fi + else + # Fallback checks + if [ "$XDG_SESSION_DESKTOP" = "gamescope" ] && [ "$XDG_CURRENT_DESKTOP" = "gamescope" ] && [ "$USER" = "deck" ]; then + return 0 + fi + if [ "$XDG_SESSION_DESKTOP" = "gamescope" ] && [ "$XDG_CURRENT_DESKTOP" = "KDE" ] && [ "$USER" = "deck" ]; then + return 0 + fi + fi + + return 1 +} + +# ============================================================================= +# Cleanup +# ============================================================================= + +# Remove autostart file on script exit +cleanup_autostart() { + rm -f "$HOME/.config/autostart/minecraft-launch.desktop" +} +trap cleanup_autostart EXIT + +# ============================================================================= +# MAIN ENTRY POINT +# ============================================================================= + +if isSteamDeckGameMode; then + if [ "$1" = launchFromPlasma ]; then + # Inside nested Plasma session + rm -f ~/.config/autostart/minecraft-launch.desktop + launchGames + qdbus org.kde.Shutdown /Shutdown org.kde.Shutdown.logout + else + # Start nested session + nestedPlasma + fi +else + # Desktop mode: launch directly + numberOfControllers=$(getControllerCount) + for player in $(seq 1 $numberOfControllers); do + setSplitscreenModeForPlayer "$player" "$numberOfControllers" + launchGame "latestUpdate-$player" "P$player" + done + wait +fi +LAUNCHER_SCRIPT_EOF + + # Replace placeholders with actual values + # Use | as delimiter since paths may contain / + sed -i "s|__LAUNCHER_NAME__|${launcher_name}|g" "$output_path" + sed -i "s|__LAUNCHER_TYPE__|${launcher_type}|g" "$output_path" + sed -i "s|__LAUNCHER_EXEC__|${launcher_exec}|g" "$output_path" + sed -i "s|__LAUNCHER_DIR__|${launcher_dir}|g" "$output_path" + sed -i "s|__INSTANCES_DIR__|${instances_dir}|g" "$output_path" + sed -i "s|__SCRIPT_VERSION__|${SCRIPT_VERSION:-2.0.0}|g" "$output_path" + sed -i "s|__COMMIT_HASH__|${commit_hash}|g" "$output_path" + sed -i "s|__GENERATION_DATE__|${generation_date}|g" "$output_path" + sed -i "s|__REPO_URL__|${REPO_URL:-https://github.com/aradanmn/MinecraftSplitscreenSteamdeck}|g" "$output_path" + + # Make executable + chmod +x "$output_path" + + print_success "Generated launcher script: $output_path" + return 0 +} + +# ============================================================================= +# UTILITY FUNCTIONS +# ============================================================================= + +# @function verify_generated_script +# @description Verify that a generated launcher script is valid. +# Checks existence, permissions, placeholder replacement, and syntax. +# @param $1 - script_path: Path to the generated script +# @return 0 if valid, 1 if invalid +# @example +# if verify_generated_script "/path/to/script.sh"; then echo "Valid"; fi +verify_generated_script() { + local script_path="$1" + + if [[ ! -f "$script_path" ]]; then + print_error "Generated script not found: $script_path" + return 1 + fi + + if [[ ! -x "$script_path" ]]; then + print_error "Generated script is not executable: $script_path" + return 1 + fi + + # Check for placeholder remnants + if grep -q '__LAUNCHER_' "$script_path"; then + print_error "Generated script contains unreplaced placeholders" + return 1 + fi + + # Basic syntax check + if ! bash -n "$script_path" 2>/dev/null; then + print_error "Generated script has syntax errors" + return 1 + fi + + print_success "Generated script verified: $script_path" + return 0 +} + +# @function print_generation_config +# @description Print the configuration that would be used for script generation. +# Useful for debugging and verification. +# @param $1-$6 - Same as generate_splitscreen_launcher +# @stdout Formatted configuration summary +# @return 0 always +print_generation_config() { + local output_path="$1" + local launcher_name="$2" + local launcher_type="$3" + local launcher_exec="$4" + local launcher_dir="$5" + local instances_dir="$6" + + echo "=== Launcher Script Generation Config ===" + echo "Output: $output_path" + echo "Launcher: $launcher_name" + echo "Type: $launcher_type" + echo "Executable: $launcher_exec" + echo "Data Dir: $launcher_dir" + echo "Instances: $instances_dir" + echo "Version: ${SCRIPT_VERSION:-2.0.0}" + echo "==========================================" +} diff --git a/modules/launcher_setup.sh b/modules/launcher_setup.sh index 546b7f0..0d86cc1 100644 --- a/modules/launcher_setup.sh +++ b/modules/launcher_setup.sh @@ -2,108 +2,292 @@ # ============================================================================= # LAUNCHER SETUP MODULE # ============================================================================= -# PrismLauncher setup and CLI verification functions -# PrismLauncher is used for automated instance creation via CLI -# It provides reliable Minecraft instance management and Fabric loader installation +# @file launcher_setup.sh +# @version 2.2.2 +# @date 2026-01-25 +# @author aradanmn +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Handles PrismLauncher detection, installation, and CLI verification. +# PrismLauncher is used for automated Minecraft instance creation via CLI, +# providing reliable instance management and Fabric loader installation. +# +# On immutable Linux systems (Bazzite, SteamOS, etc.), this module prefers +# installing PrismLauncher via Flatpak. On traditional systems, it downloads +# the AppImage from GitHub releases. +# +# @dependencies +# - curl (for GitHub API queries) +# - jq (for JSON parsing) +# - wget (for downloading AppImage) +# - flatpak (optional, for Flatpak installation) +# - utilities.sh (for print_* functions) +# - path_configuration.sh (for path constants, setters, and PREFER_FLATPAK) +# +# @exports +# Functions: +# - download_prism_launcher : Detect or install PrismLauncher +# - verify_prism_cli : Verify CLI capabilities +# - get_prism_executable : Get executable path/command +# +# Variables: +# - PRISM_INSTALL_TYPE : Installation type (appimage/flatpak) +# - PRISM_EXECUTABLE : Path or command to run PrismLauncher +# +# @changelog +# 2.2.2 (2026-01-25) - Fix: Try system-level Flatpak install first, then user-level (for Bazzite/SteamOS) +# 2.2.1 (2026-01-25) - Fix: Only create directories after successful download +# 2.2.0 (2026-01-25) - Use PREFER_FLATPAK from path_configuration instead of calling should_prefer_flatpak() +# 2.1.0 (2026-01-24) - Added Flatpak preference for immutable OS, arch detection +# 2.0.0 (2026-01-23) - Refactored to use centralized path configuration +# 1.0.0 (2026-01-22) - Initial version +# ============================================================================= + +# Module-level variables for tracking installation +PRISM_INSTALL_TYPE="" +PRISM_EXECUTABLE="" -# download_prism_launcher: Download the latest PrismLauncher AppImage -# PrismLauncher provides CLI tools for automated instance creation -# We download it to the target directory for temporary use during setup +# ----------------------------------------------------------------------------- +# @function download_prism_launcher +# @description Detects existing PrismLauncher installation or installs it. +# Uses different strategies based on the operating system: +# +# On immutable OS (Bazzite, SteamOS, etc.): +# 1) Use existing Flatpak if installed +# 2) Install Flatpak from Flathub +# 3) Use existing AppImage if present +# 4) Download AppImage from GitHub +# +# On traditional OS: +# 1) Use existing Flatpak if installed +# 2) Use existing AppImage if present +# 3) Download AppImage from GitHub +# +# @param None +# @global PREFER_FLATPAK - (input) Whether to prefer Flatpak (from path_configuration) +# @global PRISM_FLATPAK_ID - (input) Flatpak application ID +# @global PRISM_FLATPAK_DATA_DIR - (input) Flatpak data directory +# @global PRISM_APPIMAGE_PATH - (input) Expected AppImage location +# @global PRISM_APPIMAGE_DATA_DIR - (input) AppImage data directory +# @return 0 on success, exits on critical failure +# @sideeffect Calls set_creation_launcher_prismlauncher() to update paths +# ----------------------------------------------------------------------------- download_prism_launcher() { - # Skip download if AppImage already exists - if [[ -f "$TARGET_DIR/PrismLauncher.AppImage" ]]; then + print_progress "Detecting PrismLauncher installation..." + + # Priority 1: Check for existing Flatpak installation + if is_flatpak_installed "$PRISM_FLATPAK_ID" 2>/dev/null; then + print_success "Found existing PrismLauncher Flatpak installation" + + mkdir -p "$PRISM_FLATPAK_DATA_DIR/instances" + set_creation_launcher_prismlauncher "flatpak" "flatpak run $PRISM_FLATPAK_ID" + print_info " → Using Flatpak data directory: $PRISM_FLATPAK_DATA_DIR" + return 0 + fi + + # Priority 2 (immutable OS only): Install Flatpak if preferred + # PREFER_FLATPAK is set by configure_launcher_paths() in path_configuration.sh + if [[ "$PREFER_FLATPAK" == true ]]; then + print_info "Immutable OS detected - preferring Flatpak installation" + + if command -v flatpak &>/dev/null; then + print_progress "Installing PrismLauncher via Flatpak..." + + local flatpak_installed=false + + # Try system-level install first (works on Bazzite/SteamOS where Flathub is system-only) + # This may prompt for authentication on some systems + if flatpak install -y flathub "$PRISM_FLATPAK_ID" 2>/dev/null; then + flatpak_installed=true + print_success "PrismLauncher Flatpak installed (system)" + else + # Fall back to user-level install + # First ensure Flathub repo is available for user + if ! flatpak remote-list --user 2>/dev/null | grep -q flathub; then + print_progress "Adding Flathub repository for user..." + flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo 2>/dev/null || true + fi + + if flatpak install --user -y flathub "$PRISM_FLATPAK_ID" 2>/dev/null; then + flatpak_installed=true + print_success "PrismLauncher Flatpak installed (user)" + fi + fi + + if [[ "$flatpak_installed" == true ]]; then + mkdir -p "$PRISM_FLATPAK_DATA_DIR/instances" + set_creation_launcher_prismlauncher "flatpak" "flatpak run $PRISM_FLATPAK_ID" + print_info " → Using Flatpak data directory: $PRISM_FLATPAK_DATA_DIR" + return 0 + else + print_warning "Flatpak installation failed - falling back to AppImage" + fi + else + print_warning "Flatpak not available - falling back to AppImage" + fi + fi + + # Priority 3: Check for existing AppImage + if [[ -f "$PRISM_APPIMAGE_PATH" ]]; then print_success "PrismLauncher AppImage already present" + + set_creation_launcher_prismlauncher "appimage" "$PRISM_APPIMAGE_PATH" return 0 fi - - print_progress "Downloading latest PrismLauncher AppImage..." - - # Query GitHub API to get the latest release download URL - # We specifically look for AppImage files in the release assets + + # Priority 4: Download AppImage + print_progress "No existing PrismLauncher found - downloading AppImage..." + + # Query GitHub API for latest release matching system architecture local prism_url + local arch + arch=$(uname -m) + prism_url=$(curl -s https://api.github.com/repos/PrismLauncher/PrismLauncher/releases/latest | \ - jq -r '.assets[] | select(.name | test("AppImage$")) | .browser_download_url' | head -n1) - - # Validate that we got a valid download URL + jq -r --arg arch "$arch" '.assets[] | select(.name | test("AppImage$")) | select(.name | contains($arch)) | .browser_download_url' | head -n1) + if [[ -z "$prism_url" || "$prism_url" == "null" ]]; then print_error "Could not find latest PrismLauncher AppImage URL." print_error "Please check https://github.com/PrismLauncher/PrismLauncher/releases manually." exit 1 fi - - # Download and make executable - wget -O "$TARGET_DIR/PrismLauncher.AppImage" "$prism_url" - chmod +x "$TARGET_DIR/PrismLauncher.AppImage" + + # Download to temp location first, only create directory on success + local temp_appimage + temp_appimage=$(mktemp) + + if ! wget -q -O "$temp_appimage" "$prism_url"; then + print_error "Failed to download PrismLauncher AppImage." + rm -f "$temp_appimage" 2>/dev/null + exit 1 + fi + + # Download successful - now create directory and move file + mkdir -p "$PRISM_APPIMAGE_DATA_DIR" + mv "$temp_appimage" "$PRISM_APPIMAGE_PATH" + chmod +x "$PRISM_APPIMAGE_PATH" + + set_creation_launcher_prismlauncher "appimage" "$PRISM_APPIMAGE_PATH" print_success "PrismLauncher AppImage downloaded successfully" + print_info " → Installation type: appimage" } -# verify_prism_cli: Ensure PrismLauncher supports CLI operations -# We need CLI support for automated instance creation -# This function validates that the downloaded version has the required features +# ----------------------------------------------------------------------------- +# @function verify_prism_cli +# @description Verifies that PrismLauncher supports CLI operations needed for +# automated instance creation. Tests the --help output for CLI +# keywords. If AppImage fails due to FUSE issues, attempts to +# extract and run directly. +# +# @param None +# @global CREATION_LAUNCHER_TYPE - (input) "appimage" or "flatpak" +# @global CREATION_EXECUTABLE - (input/output) May be updated if extracted +# @global CREATION_DATA_DIR - (input) Data directory for extraction +# @global PRISM_FLATPAK_ID - (input) Flatpak application ID +# @return 0 if CLI verified, 1 if CLI not available +# @note Returns 1 (not exit) to allow fallback to manual creation +# ----------------------------------------------------------------------------- verify_prism_cli() { print_progress "Verifying PrismLauncher CLI capabilities..." - - local appimage="$TARGET_DIR/PrismLauncher.AppImage" - - # Ensure the AppImage is executable - chmod +x "$appimage" - - # Try to run the AppImage to check CLI support - local help_output - help_output=$("$appimage" --help 2>&1) - local exit_code=$? - - # Check if AppImage failed due to FUSE issues or squashfs problems - if [[ $exit_code -ne 0 ]] && echo "$help_output" | grep -q "FUSE\|Cannot mount\|squashfs\|Failed to open"; then - print_warning "AppImage execution failed due to FUSE/squashfs issues" - - # Try extracting AppImage to avoid FUSE dependency - print_progress "Attempting to extract AppImage contents..." - cd "$TARGET_DIR" - if "$appimage" --appimage-extract >/dev/null 2>&1; then - if [[ -d "$TARGET_DIR/squashfs-root" ]] && [[ -x "$TARGET_DIR/squashfs-root/AppRun" ]]; then - print_success "AppImage extracted successfully" - # Update appimage path to point to extracted version - appimage="$TARGET_DIR/squashfs-root/AppRun" - help_output=$("$appimage" --help 2>&1) - exit_code=$? + + local prism_exec="" + local help_output="" + local exit_code=0 + + # Determine the executable based on installation type + if [[ "$CREATION_LAUNCHER_TYPE" == "flatpak" ]]; then + prism_exec="flatpak run $PRISM_FLATPAK_ID" + print_info " → Testing Flatpak CLI..." + + help_output=$($prism_exec --help 2>&1) + exit_code=$? + + if [[ $exit_code -ne 0 ]]; then + print_warning "PrismLauncher Flatpak CLI test failed" + print_info "Error output: $(echo "$help_output" | head -3)" + return 1 + fi + else + # AppImage path + local appimage="$CREATION_EXECUTABLE" + + chmod +x "$appimage" 2>/dev/null || true + help_output=$("$appimage" --help 2>&1) + exit_code=$? + + # Check if AppImage failed due to FUSE issues + if [[ $exit_code -ne 0 ]] && echo "$help_output" | grep -q "FUSE\|Cannot mount\|squashfs\|Failed to open"; then + print_warning "AppImage execution failed due to FUSE/squashfs issues" + + # Try extracting AppImage to avoid FUSE dependency + print_progress "Attempting to extract AppImage contents..." + cd "$CREATION_DATA_DIR" + local extracted_path="$CREATION_DATA_DIR/squashfs-root/AppRun" + if "$appimage" --appimage-extract >/dev/null 2>&1; then + if [[ -d "$CREATION_DATA_DIR/squashfs-root" ]] && [[ -x "$extracted_path" ]]; then + print_success "AppImage extracted successfully" + CREATION_EXECUTABLE="$extracted_path" + prism_exec="$CREATION_EXECUTABLE" + help_output=$("$prism_exec" --help 2>&1) + exit_code=$? + else + print_warning "AppImage extraction failed or incomplete" + print_info "Will skip CLI creation and use manual instance creation method" + return 1 + fi else - print_warning "AppImage extraction failed or incomplete" + print_warning "AppImage extraction failed" print_info "Will skip CLI creation and use manual instance creation method" return 1 fi - else - print_warning "AppImage extraction failed" - print_info "Will skip CLI creation and use manual instance creation method" - return 1 fi + + prism_exec="${PRISM_EXECUTABLE:-$appimage}" fi - - # Check if help command worked after potential extraction + + # Check if help command worked if [[ $exit_code -ne 0 ]]; then print_warning "PrismLauncher execution failed, using manual instance creation" print_info "Error output: $(echo "$help_output" | head -3)" return 1 fi - - # Test for basic CLI support by checking help output - # Look for keywords that indicate CLI instance creation is available + + # Test for CLI support by checking help output if ! echo "$help_output" | grep -q -E "(cli|create|instance)"; then print_warning "PrismLauncher CLI may not support instance creation. Checking with --help-all..." - - # Fallback: try the extended help option + local extended_help - extended_help=$("$appimage" --help-all 2>&1) + extended_help=$($prism_exec --help-all 2>&1) if ! echo "$extended_help" | grep -q -E "(cli|create-instance)"; then print_warning "This version of PrismLauncher does not support CLI instance creation" print_info "Will use manual instance creation method instead" return 1 fi fi - - # Display available CLI commands for debugging purposes + print_info "Available PrismLauncher CLI commands:" echo "$help_output" | grep -E "(create|instance|cli)" || echo " (Basic CLI commands found)" - print_success "PrismLauncher CLI instance creation verified" + print_success "PrismLauncher CLI instance creation verified ($CREATION_LAUNCHER_TYPE)" return 0 } + +# ----------------------------------------------------------------------------- +# @function get_prism_executable +# @description Returns the PrismLauncher executable command or path from the +# centralized path configuration. +# +# @param None +# @global CREATION_EXECUTABLE - (input) Executable path/command +# @stdout Executable path or command +# @return 0 if executable set, 1 if not configured +# ----------------------------------------------------------------------------- +get_prism_executable() { + if [[ -n "$CREATION_EXECUTABLE" ]]; then + echo "$CREATION_EXECUTABLE" + else + echo "" + return 1 + fi +} diff --git a/modules/lwjgl_management.sh b/modules/lwjgl_management.sh index 2dcf943..c5707c4 100644 --- a/modules/lwjgl_management.sh +++ b/modules/lwjgl_management.sh @@ -1,21 +1,74 @@ #!/bin/bash # ============================================================================= -# LWJGL VERSION DETECTION +# @file lwjgl_management.sh +# @version 2.0.0 +# @date 2026-01-25 +# @author Minecraft Splitscreen Steam Deck Project +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Dynamic LWJGL (Lightweight Java Game Library) version detection for Minecraft. +# LWJGL provides the native bindings for OpenGL, OpenAL, and input handling +# that Minecraft requires. Different Minecraft versions need specific LWJGL versions. +# +# Detection strategy: +# 1. Query Fabric Meta API for exact LWJGL version +# 2. Fall back to hardcoded version mappings +# 3. Default to 3.3.3 for unknown versions +# +# @dependencies +# - utilities.sh (for print_progress, print_success, print_warning) +# - curl or wget (for API requests) +# - jq (for JSON parsing, optional) +# +# @global_inputs +# - MC_VERSION: Target Minecraft version +# +# @global_outputs +# - LWJGL_VERSION: Detected LWJGL version string (e.g., "3.3.3") +# +# @exports +# Functions: +# - get_lwjgl_version : Main detection function +# - get_lwjgl_version_by_mapping : Fallback mapping lookup +# - validate_lwjgl_version : Version format validation +# +# @changelog +# 2.0.0 (2026-01-25) - Added comprehensive JSDoc documentation +# 1.0.0 (2024-XX-XX) - Initial implementation # ============================================================================= -# Dynamic LWJGL version detection based on Minecraft version -# Global variable to store detected LWJGL version +# ----------------------------------------------------------------------------- +# Module Variables +# ----------------------------------------------------------------------------- + +# @global LWJGL_VERSION +# @description Stores the detected LWJGL version for the target Minecraft version LWJGL_VERSION="" -# get_lwjgl_version: Detect appropriate LWJGL version for Minecraft version -# Uses Fabric Meta API and version mapping logic +# ============================================================================= +# LWJGL VERSION DETECTION +# ============================================================================= + +# @function get_lwjgl_version +# @description Detect the appropriate LWJGL version for the current Minecraft version. +# First attempts to query Fabric Meta API, then falls back to +# hardcoded version mappings. +# @global MC_VERSION - (input) Target Minecraft version +# @global LWJGL_VERSION - (output) Set to detected version string +# @return 0 always (uses fallback on failure) +# @example +# MC_VERSION="1.21.3" +# get_lwjgl_version +# echo "LWJGL: $LWJGL_VERSION" # Outputs: "LWJGL: 3.3.3" get_lwjgl_version() { print_progress "Detecting LWJGL version for Minecraft $MC_VERSION..." - + # First try to get LWJGL version from Fabric Meta API local fabric_game_url="https://meta.fabricmc.net/v2/versions/game" local temp_file="/tmp/fabric_versions_$$.json" - + if command -v wget >/dev/null 2>&1; then if wget -q -O "$temp_file" "$fabric_game_url" 2>/dev/null; then if command -v jq >/dev/null 2>&1 && [[ -s "$temp_file" ]]; then @@ -35,31 +88,40 @@ get_lwjgl_version() { fi fi fi - + # Clean up temp file [[ -f "$temp_file" ]] && rm -f "$temp_file" - + # If API lookup failed, use version mapping logic if [[ -z "$LWJGL_VERSION" || "$LWJGL_VERSION" == "null" ]]; then LWJGL_VERSION=$(get_lwjgl_version_by_mapping "$MC_VERSION") fi - + # Final fallback if [[ -z "$LWJGL_VERSION" ]]; then print_warning "Could not detect LWJGL version, using fallback" LWJGL_VERSION="3.3.3" fi - + print_success "Using LWJGL version: $LWJGL_VERSION" } -# get_lwjgl_version_by_mapping: Map Minecraft version to LWJGL version -# Parameters: -# $1 - Minecraft version (e.g., "1.21.3") -# Returns: Appropriate LWJGL version +# ============================================================================= +# VERSION MAPPING +# ============================================================================= + +# @function get_lwjgl_version_by_mapping +# @description Map Minecraft version to LWJGL version using hardcoded mappings. +# Based on official Minecraft release data. +# @param $1 - mc_version: Minecraft version (e.g., "1.21.3") +# @stdout Appropriate LWJGL version string +# @return 0 always +# @see https://minecraft.wiki/w/Tutorials/Update_LWJGL +# @example +# lwjgl=$(get_lwjgl_version_by_mapping "1.21.3") # Returns "3.3.3" get_lwjgl_version_by_mapping() { local mc_version="$1" - + # LWJGL version mapping based on Minecraft releases # Source: https://minecraft.wiki/w/Tutorials/Update_LWJGL if [[ "$mc_version" =~ ^1\.2[1-9](\.|$) ]]; then @@ -79,13 +141,19 @@ get_lwjgl_version_by_mapping() { fi } -# validate_lwjgl_version: Ensure LWJGL version is valid -# Parameters: -# $1 - LWJGL version to validate -# Returns: 0 if valid, 1 if invalid +# ============================================================================= +# VALIDATION +# ============================================================================= + +# @function validate_lwjgl_version +# @description Validate that an LWJGL version string has the expected format. +# @param $1 - version: LWJGL version string to validate +# @return 0 if valid (matches X.Y.Z format), 1 if invalid +# @example +# if validate_lwjgl_version "3.3.3"; then echo "Valid"; fi validate_lwjgl_version() { local version="$1" - + # Check if version matches expected format (e.g., "3.3.3") if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then return 0 diff --git a/modules/main_workflow.sh b/modules/main_workflow.sh index 49682f8..cf8cd35 100644 --- a/modules/main_workflow.sh +++ b/modules/main_workflow.sh @@ -1,18 +1,51 @@ #!/bin/bash # ============================================================================= -# Minecraft Splitscreen Steam Deck Installer - Main Workflow Module -# ============================================================================= -# -# This module contains the main orchestration logic for the complete splitscreen -# installation process. It coordinates all the other modules and provides -# comprehensive status reporting and user guidance. +# @file main_workflow.sh +# @version 2.0.0 +# @date 2026-01-25 +# @author Minecraft Splitscreen Steam Deck Project +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Main orchestration module for the complete splitscreen installation process. +# Coordinates all other modules and provides comprehensive status reporting +# and user guidance throughout the installation. +# +# The module implements a 10-phase installation workflow: +# 1. Workspace Setup → 2. Core Setup → 3. Version Detection → +# 4. Account Setup → 5. Mod Compatibility → 6. User Selection → +# 7. Instance Creation → 8. Launcher Optimization → +# 9. System Integration → 10. Completion Report +# +# @dependencies +# - All other modules (sourced by install-minecraft-splitscreen.sh) +# - path_configuration.sh (for configure_launcher_paths, finalize_launcher_paths) +# - launcher_setup.sh (for download_prism_launcher, verify_prism_cli) +# - version_management.sh (for get_minecraft_version, get_fabric_version) +# - java_management.sh (for detect_java) +# - lwjgl_management.sh (for get_lwjgl_version) +# - mod_management.sh (for check_mod_compatibility, select_user_mods) +# - instance_creation.sh (for create_instances) +# - pollymc_setup.sh (for setup_pollymc) +# - launcher_script_generator.sh (for generate_splitscreen_launcher) +# - steam_integration.sh (for setup_steam_integration) +# - desktop_launcher.sh (for create_desktop_launcher) +# - utilities.sh (for print_* functions, merge_accounts_json) # -# Functions provided: -# - main: Primary function that orchestrates the complete installation process +# @exports +# Functions: +# - main : Primary orchestration function +# - generate_launcher_script: Generate minecraftSplitscreen.sh # +# @changelog +# 2.0.0 (2026-01-25) - Added comprehensive JSDoc documentation +# 1.0.0 (2024-XX-XX) - Initial implementation # ============================================================================= -# main: Primary function that orchestrates the complete splitscreen installation process +# @function main +# @description Primary function that orchestrates the complete splitscreen +# installation process. Coordinates all modules in sequence. # # INSTALLATION WORKFLOW: # 1. WORKSPACE SETUP: Create directories and initialize environment @@ -37,101 +70,146 @@ # - PrismLauncher: CLI automation for reliable instance creation with proper Fabric setup # - PollyMC: Offline-friendly gameplay launcher without forced authentication # - Smart cleanup: Removes PrismLauncher after successful PollyMC setup to save space +# +# @global Multiple globals from path_configuration.sh (ACTIVE_*, CREATION_*) +# @global MC_VERSION - (output) Set by get_minecraft_version +# @global JAVA_PATH - (output) Set by detect_java +# @global MISSING_MODS - (input) Array of mods that couldn't be installed +# @return 0 on successful completion main() { + # Initialize logging FIRST (before any print_* calls) + init_logging "install" + print_header "🎮 MINECRAFT SPLITSCREEN INSTALLER 🎮" - print_info "Advanced installation system with dual-launcher optimization" - print_info "Strategy: PrismLauncher CLI automation → PollyMC gameplay → Smart cleanup" + print_info "Advanced installation system with smart launcher detection" + print_info "Strategy: Detect available launchers → Create instances → Generate launcher script" + print_info "Log file: $(get_log_file)" echo "" - + + # ============================================================================= + # LAUNCHER DETECTION AND PATH CONFIGURATION (MUST BE FIRST) + # ============================================================================= + + # This sets up all path variables based on what launchers are available + # All subsequent code uses CREATION_* and ACTIVE_* variables from path_configuration.sh + configure_launcher_paths + # ============================================================================= # WORKSPACE INITIALIZATION PHASE # ============================================================================= - + # WORKSPACE SETUP: Create and navigate to working directory - # All temporary files, downloads, and initial setup happen in TARGET_DIR - # This provides a clean, isolated environment for the installation process - print_progress "Initializing installation workspace: $TARGET_DIR" - mkdir -p "$TARGET_DIR" - cd "$TARGET_DIR" || exit 1 + # Use CREATION_DATA_DIR as that's where we'll create instances initially + local workspace_dir="${CREATION_DATA_DIR:-$HOME/.local/share/PrismLauncher}" + print_progress "Initializing installation workspace: $workspace_dir" + mkdir -p "$workspace_dir" + cd "$workspace_dir" || exit 1 print_success "✅ Workspace initialized successfully" - + # ============================================================================= # CORE SYSTEM REQUIREMENTS VALIDATION # ============================================================================= - - download_prism_launcher # Download PrismLauncher AppImage for CLI automation - if ! verify_prism_cli; then # Test CLI functionality (non-fatal if it fails) + + # Only download PrismLauncher if we don't have a creation launcher yet + if [[ -z "$CREATION_LAUNCHER" ]]; then + download_prism_launcher # Download PrismLauncher AppImage for CLI automation + fi + + if [[ -n "$CREATION_EXECUTABLE" ]] && ! verify_prism_cli; then print_info "PrismLauncher CLI unavailable - will use manual instance creation" fi - + # ============================================================================= # VERSION DETECTION AND CONFIGURATION # ============================================================================= - + get_minecraft_version # Determine target Minecraft version (user choice or latest) detect_java # Automatically detect, install, and configure correct Java version for selected Minecraft version get_fabric_version # Get compatible Fabric loader version from API get_lwjgl_version # Detect appropriate LWJGL version for Minecraft version - + # ============================================================================= # OFFLINE ACCOUNTS CONFIGURATION # ============================================================================= - + print_progress "Setting up offline accounts for splitscreen gameplay..." print_info "Downloading pre-configured offline accounts for Player 1-4" - + # OFFLINE ACCOUNTS DOWNLOAD: Get splitscreen player account configurations # These accounts enable splitscreen without requiring multiple Microsoft accounts # Each player (P1, P2, P3, P4) gets a separate offline profile for identification - if ! wget -O accounts.json "https://raw.githubusercontent.com/FlyingEwok/MinecraftSplitscreenSteamdeck/main/accounts.json"; then + # IMPORTANT: We merge accounts to preserve any existing Microsoft/other accounts + local accounts_url="${REPO_RAW_URL:-https://raw.githubusercontent.com/aradanmn/MinecraftSplitscreenSteamdeck/${REPO_BRANCH:-main}}/accounts.json" + local accounts_temp + accounts_temp=$(mktemp) + local accounts_path="$CREATION_DATA_DIR/accounts.json" + + if wget -q -O "$accounts_temp" "$accounts_url"; then + # Merge downloaded accounts with any existing accounts (preserves Microsoft accounts, etc.) + if merge_accounts_json "$accounts_temp" "$accounts_path"; then + print_success "✅ Offline splitscreen accounts configured successfully" + print_info " → P1, P2, P3, P4 player accounts ready for offline gameplay" + if [[ -f "$accounts_path" ]] && command -v jq >/dev/null 2>&1; then + local existing_count + existing_count=$(jq '.accounts | map(select(.profile.name | test("^P[1-4]$") | not)) | length' "$accounts_path" 2>/dev/null || echo "0") + if [[ "$existing_count" -gt 0 ]]; then + print_info " → Preserved $existing_count existing account(s)" + fi + fi + fi + else print_warning "⚠️ Failed to download accounts.json from repository" print_info " → Attempting to use local copy if available..." - if [[ ! -f "accounts.json" ]]; then + if [[ ! -f "$accounts_path" ]]; then print_error "❌ No accounts.json found - splitscreen accounts may require manual setup" print_info " → Splitscreen will still work but players may have generic names" fi - else - print_success "✅ Offline splitscreen accounts configured successfully" - print_info " → P1, P2, P3, P4 player accounts ready for offline gameplay" fi - + rm -f "$accounts_temp" 2>/dev/null + # ============================================================================= # MOD ECOSYSTEM SETUP PHASE # ============================================================================= - + check_mod_compatibility # Query Modrinth/CurseForge APIs for compatible versions select_user_mods # Interactive mod selection interface with categories - + # ============================================================================= # MINECRAFT INSTANCE CREATION PHASE # ============================================================================= - - + + create_instances # Create 4 splitscreen instances using PrismLauncher CLI with comprehensive fallbacks - + # ============================================================================= # LAUNCHER OPTIMIZATION PHASE: Advanced launcher configuration # ============================================================================= - + setup_pollymc # Download PollyMC, migrate instances, verify, cleanup PrismLauncher - + + # ============================================================================= + # LAUNCHER SCRIPT GENERATION PHASE: Generate splitscreen launcher with correct paths + # ============================================================================= + + generate_launcher_script # Generate minecraftSplitscreen.sh with detected launcher paths + # ============================================================================= # SYSTEM INTEGRATION PHASE: Optional platform integration # ============================================================================= - + setup_steam_integration # Add splitscreen launcher to Steam library (optional) create_desktop_launcher # Create native desktop launcher and app menu entry (optional) - + # ============================================================================= # INSTALLATION COMPLETION AND STATUS REPORTING # ============================================================================= - + print_header "🎉 INSTALLATION ANALYSIS AND COMPLETION REPORT" - + # ============================================================================= # MISSING MODS ANALYSIS: Report any compatibility issues # ============================================================================= - + # MISSING MODS REPORT: Alert user to any mods that couldn't be installed # This helps users understand if specific functionality might be unavailable # Common causes: no Fabric version available, API changes, temporary download issues @@ -154,60 +232,46 @@ main() { print_info "These mods can be installed manually later if compatible versions become available" print_info "The splitscreen functionality will work without these optional mods" fi - + # ============================================================================= # COMPREHENSIVE INSTALLATION SUCCESS REPORT # ============================================================================= - + echo "" echo "==========================================" echo "🎮 MINECRAFT SPLITSCREEN INSTALLATION COMPLETE! 🎮" echo "==========================================" echo "" - + # ============================================================================= # LAUNCHER STRATEGY SUCCESS ANALYSIS # ============================================================================= - - # LAUNCHER STRATEGY REPORT: Explain which approach was successful and the benefits - # The dual-launcher approach provides the best of both worlds when successful - if [[ "$USE_POLLYMC" == true ]]; then - echo "✅ OPTIMIZED INSTALLATION SUCCESSFUL!" - echo "" - echo "🔧 DUAL-LAUNCHER STRATEGY COMPLETED:" - echo " 🛠️ PrismLauncher: CLI automation for reliable instance creation ✅ COMPLETED" - echo " 🎮 PollyMC: Primary launcher for offline splitscreen gameplay ✅ ACTIVE" - echo " 🧹 Smart cleanup: Removes PrismLauncher after successful setup ✅ CLEANED" - echo "" - echo "🎯 STRATEGY BENEFITS ACHIEVED:" - echo " • Reliable instance creation through proven CLI automation" + + # LAUNCHER STRATEGY REPORT: Explain which launcher is being used + echo "✅ INSTALLATION SUCCESSFUL!" + echo "" + echo "🔧 LAUNCHER CONFIGURATION:" + echo " 🎮 Active Launcher: ${ACTIVE_LAUNCHER^} ($ACTIVE_LAUNCHER_TYPE)" + echo " 📁 Data Directory: $ACTIVE_DATA_DIR" + echo " 📁 Instances: $ACTIVE_INSTANCES_DIR" + echo " 📜 Launcher Script: $ACTIVE_LAUNCHER_SCRIPT" + echo "" + if [[ "$ACTIVE_LAUNCHER" == "pollymc" ]]; then + echo "🎯 POLLYMC BENEFITS:" echo " • Offline-friendly gameplay without forced Microsoft login prompts" - echo " • Optimized disk usage through intelligent cleanup" - echo " • Best performance for splitscreen scenarios" - echo "" - echo "✅ Primary launcher: PollyMC (optimized for splitscreen)" - echo "✅ All instances migrated and verified in PollyMC" - echo "✅ Temporary PrismLauncher files cleaned up successfully" + echo " • Optimized for splitscreen scenarios" + echo " • Best performance for local multiplayer" else - echo "✅ FALLBACK INSTALLATION SUCCESSFUL!" - echo "" - echo "🔧 FALLBACK STRATEGY USED:" - echo " 🛠️ PrismLauncher: Instance creation + primary launcher ✅ ACTIVE" - echo " ⚠️ PollyMC: Download/setup encountered issues, using PrismLauncher for everything" - echo "" - echo "📋 FALLBACK EXPLANATION:" - echo " • PollyMC setup failed (network issues, system compatibility, or download problems)" - echo " • PrismLauncher provides full functionality as backup launcher" - echo " • Splitscreen works perfectly with PrismLauncher" - echo "" - echo "✅ Primary launcher: PrismLauncher (proven reliability)" - echo "⚠️ Note: PollyMC optimization unavailable, but full functionality preserved" + echo "🎯 PRISMLAUNCHER BENEFITS:" + echo " • Proven reliability and stability" + echo " • Full functionality for splitscreen gameplay" + echo " • Wide community support" fi - + # ============================================================================= # TECHNICAL ACHIEVEMENT SUMMARY # ============================================================================= - + # INSTALLATION COMPONENTS SUMMARY: List all successfully completed setup elements echo "" echo "🏆 TECHNICAL ACHIEVEMENTS COMPLETED:" @@ -223,78 +287,57 @@ main() { echo "✅ Instance verification and launcher registration completed" echo "✅ Comprehensive automatic dependency resolution system" echo "" - + # ============================================================================= # USER GUIDANCE AND LAUNCH INSTRUCTIONS # ============================================================================= - + echo "🚀 READY TO PLAY SPLITSCREEN MINECRAFT!" echo "" - + # LAUNCH METHODS: Comprehensive guide to starting splitscreen Minecraft echo "🎮 HOW TO LAUNCH SPLITSCREEN MINECRAFT:" echo "" - + # PRIMARY LAUNCH METHOD: Direct script execution echo "1. 🔧 DIRECT LAUNCH (Recommended):" - if [[ "$USE_POLLYMC" == true ]]; then - echo " Command: $HOME/.local/share/PollyMC/minecraftSplitscreen.sh" - echo " Description: Optimized PollyMC launcher with automatic controller detection" - else - echo " Command: $TARGET_DIR/minecraftSplitscreen.sh" - echo " Description: PrismLauncher-based splitscreen with automatic controller detection" - fi + echo " Command: $ACTIVE_LAUNCHER_SCRIPT" + echo " Description: ${ACTIVE_LAUNCHER^}-based splitscreen with automatic controller detection" echo "" - + # ALTERNATIVE LAUNCH METHODS: Other integration options echo "2. 🖥️ DESKTOP LAUNCHER:" echo " Method: Double-click desktop shortcut or search 'Minecraft Splitscreen' in app menu" echo " Availability: $(if [[ -f "$HOME/Desktop/MinecraftSplitscreen.desktop" ]]; then echo "✅ Configured"; else echo "❌ Not configured"; fi)" echo "" - + echo "3. 🎯 STEAM INTEGRATION:" echo " Method: Launch from Steam library or Big Picture mode" echo " Benefits: Steam Deck Game Mode integration, Steam Input support" echo " Availability: $(if grep -q "PollyMC\|PrismLauncher" ~/.steam/steam/userdata/*/config/shortcuts.vdf 2>/dev/null; then echo "✅ Configured"; else echo "❌ Not configured"; fi)" echo "" - + # ============================================================================= # SYSTEM REQUIREMENTS AND TECHNICAL DETAILS # ============================================================================= - + echo "⚙️ SYSTEM CONFIGURATION DETAILS:" echo "" - + # LAUNCHER DETAILS: Technical information about the setup - if [[ "$USE_POLLYMC" == true ]]; then - echo "🛠️ LAUNCHER CONFIGURATION:" - echo " • Instance creation: PrismLauncher CLI (automated)" - echo " • Gameplay launcher: PollyMC (offline-optimized)" - echo " • Strategy: Best of both worlds approach" - echo " • Benefits: CLI automation + offline gameplay + no forced login" - else - echo "🛠️ LAUNCHER CONFIGURATION:" - echo " • Primary launcher: PrismLauncher (all functions)" - echo " • Strategy: Single launcher approach" - echo " • Note: PollyMC optimization unavailable, but fully functional" - fi + echo "🛠️ LAUNCHER CONFIGURATION:" + echo " • Primary launcher: ${ACTIVE_LAUNCHER^} ($ACTIVE_LAUNCHER_TYPE)" + echo " • Data directory: $ACTIVE_DATA_DIR" + echo " • Instances directory: $ACTIVE_INSTANCES_DIR" echo "" - + # MINECRAFT ACCOUNT REQUIREMENTS: Important user information echo "💳 ACCOUNT REQUIREMENTS:" - if [[ "$USE_POLLYMC" == true ]]; then - echo " • Microsoft account: Required for initial setup and updates" - echo " • Account type: PAID Minecraft Java Edition required" - echo " • Login frequency: Minimal (PollyMC is offline-friendly)" - echo " • Splitscreen: Uses offline accounts (P1, P2, P3, P4) after initial login" - else - echo " • Microsoft account: Required for launcher access" - echo " • Account type: PAID Minecraft Java Edition required" - echo " • Note: PrismLauncher may prompt for periodic authentication" - echo " • Splitscreen: Uses offline accounts (P1, P2, P3, P4) after login" - fi + echo " • Microsoft account: Required for launcher access" + echo " • Account type: PAID Minecraft Java Edition required" + echo " • Splitscreen: Uses offline accounts (P1, P2, P3, P4) after login" echo "" - + # CONTROLLER INFORMATION: Hardware requirements and tips echo "🎮 CONTROLLER CONFIGURATION:" echo " • Supported: Xbox, PlayStation, generic USB/Bluetooth controllers" @@ -302,32 +345,23 @@ main() { echo " • Steam Deck: Built-in controls + external controllers" echo " • Recommendation: Use wired controllers for best performance" echo "" - + # ============================================================================= # INSTALLATION LOCATION SUMMARY # ============================================================================= - + echo "📁 INSTALLATION LOCATIONS:" - if [[ "$USE_POLLYMC" == true ]]; then - echo " • Primary installation: $HOME/.local/share/PollyMC/" - echo " • Launcher executable: $HOME/.local/share/PollyMC/PollyMC-Linux-x86_64.AppImage" - echo " • Splitscreen script: $HOME/.local/share/PollyMC/minecraftSplitscreen.sh" - echo " • Instance data: $HOME/.local/share/PollyMC/instances/" - echo " • Account configuration: $HOME/.local/share/PollyMC/accounts.json" - echo " • Temporary build files: Successfully removed after setup ✅" - else - echo " • Primary installation: $TARGET_DIR" - echo " • Launcher executable: $TARGET_DIR/PrismLauncher.AppImage" - echo " • Splitscreen script: $TARGET_DIR/minecraftSplitscreen.sh" - echo " • Instance data: $TARGET_DIR/instances/" - echo " • Account configuration: $TARGET_DIR/accounts.json" - fi + echo " • Primary installation: $ACTIVE_DATA_DIR" + echo " • Launcher executable: $ACTIVE_EXECUTABLE" + echo " • Splitscreen script: $ACTIVE_LAUNCHER_SCRIPT" + echo " • Instance data: $ACTIVE_INSTANCES_DIR" + echo " • Account configuration: $ACTIVE_DATA_DIR/accounts.json" echo "" - + # ============================================================================= # ADVANCED TECHNICAL FEATURE SUMMARY # ============================================================================= - + echo "🔧 ADVANCED FEATURES IMPLEMENTED:" echo " • Complete Fabric dependency chain with proper version matching" echo " • API-based mod compatibility verification (Modrinth + CurseForge)" @@ -335,18 +369,15 @@ main() { echo " • Automatic dependency resolution and installation" echo " • Enhanced error handling with multiple fallback strategies" echo " • Instance verification and launcher registration" - echo " • Smart cleanup with disk space optimization" - if [[ "$USE_POLLYMC" == true ]]; then - echo " • Dual-launcher optimization strategy successfully implemented" - fi + echo " • Centralized path configuration for reliable operation" echo " • Cross-platform Linux compatibility (Steam Deck + Desktop)" echo " • Professional Steam and desktop environment integration" echo "" - + # ============================================================================= # FINAL SUCCESS MESSAGE AND NEXT STEPS # ============================================================================= - + # Display summary of any optional dependencies that couldn't be installed local missing_summary_count=0 if [[ ${#MISSING_MODS[@]} -gt 0 ]]; then @@ -365,7 +396,7 @@ main() { echo " The core splitscreen functionality will work perfectly without them." echo "" fi - + echo "🎉 INSTALLATION COMPLETE - ENJOY SPLITSCREEN MINECRAFT! 🎉" echo "" echo "Next steps:" @@ -374,7 +405,93 @@ main() { echo "3. The system will automatically detect controller count and launch appropriate instances" echo "4. Each player gets their own screen and can play independently" echo "" + echo "📋 Log file: $(get_log_file)" + echo "" echo "For troubleshooting or updates, visit:" - echo "https://github.com/FlyingEwok/MinecraftSplitscreenSteamdeck" + echo "${REPO_URL:-https://github.com/aradanmn/MinecraftSplitscreenSteamdeck}" echo "==========================================" } + +# ============================================================================= +# LAUNCHER SCRIPT GENERATION FUNCTION +# ============================================================================= + +# @function generate_launcher_script +# @description Generate the minecraftSplitscreen.sh launcher with correct paths. +# Uses centralized path configuration to generate a customized +# launcher script with correct paths baked in. +# +# The generated script will: +# - Have version metadata embedded (version, commit, generation date) +# - Use the correct launcher executable path +# - Use the correct instances directory +# - Work for both AppImage and Flatpak installations +# +# @global ACTIVE_LAUNCHER - (input) Name of active launcher +# @global ACTIVE_LAUNCHER_TYPE - (input) Type (appimage/flatpak) +# @global ACTIVE_EXECUTABLE - (input) Path to launcher executable +# @global ACTIVE_DATA_DIR - (input) Launcher data directory +# @global ACTIVE_INSTANCES_DIR - (input) Instances directory +# @global ACTIVE_LAUNCHER_SCRIPT - (input) Output path for generated script +# @global GENERATED_LAUNCHER_SCRIPT - (output) Set to output path on success +# @return 0 on success, 1 on failure +generate_launcher_script() { + print_header "🔧 GENERATING SPLITSCREEN LAUNCHER SCRIPT" + + # Use the centralized path configuration + # These variables are set by configure_launcher_paths() or finalize_launcher_paths() + if [[ -z "$ACTIVE_LAUNCHER" ]] || [[ -z "$ACTIVE_DATA_DIR" ]]; then + print_error "Launcher paths not configured! Call configure_launcher_paths() first." + return 1 + fi + + local launcher_name="$ACTIVE_LAUNCHER" + local launcher_type="$ACTIVE_LAUNCHER_TYPE" + local launcher_exec="$ACTIVE_EXECUTABLE" + local launcher_dir="$ACTIVE_DATA_DIR" + local instances_dir="$ACTIVE_INSTANCES_DIR" + local output_path="$ACTIVE_LAUNCHER_SCRIPT" + + # Validate paths exist + if [[ ! -d "$instances_dir" ]]; then + print_warning "Instances directory does not exist: $instances_dir" + print_info "Creating directory..." + mkdir -p "$instances_dir" + fi + + # Print configuration summary + print_info "Generating launcher script with configuration:" + print_info " Launcher: $launcher_name" + print_info " Type: $launcher_type" + print_info " Executable: $launcher_exec" + print_info " Data Directory: $launcher_dir" + print_info " Instances: $instances_dir" + print_info " Output: $output_path" + + # Generate the launcher script + if generate_splitscreen_launcher \ + "$output_path" \ + "$launcher_name" \ + "$launcher_type" \ + "$launcher_exec" \ + "$launcher_dir" \ + "$instances_dir"; then + + # Verify the generated script + if verify_generated_script "$output_path"; then + print_success "✅ Launcher script generated and verified: $output_path" + + # Store the path for later reference (for Steam/Desktop integration) + GENERATED_LAUNCHER_SCRIPT="$output_path" + export GENERATED_LAUNCHER_SCRIPT + else + print_error "Generated script verification failed" + return 1 + fi + else + print_error "Failed to generate launcher script" + return 1 + fi + + return 0 +} diff --git a/modules/mod_management.sh b/modules/mod_management.sh index 3c58e82..70d2b0c 100644 --- a/modules/mod_management.sh +++ b/modules/mod_management.sh @@ -1,21 +1,84 @@ #!/bin/bash # ============================================================================= -# MOD MANAGEMENT MODULE +# @file mod_management.sh +# @version 2.0.0 +# @date 2026-01-25 +# @author Minecraft Splitscreen Steam Deck Project +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Comprehensive mod compatibility checking, dependency resolution, and user +# selection interface for Minecraft Fabric mods. Supports dual-platform +# integration with both Modrinth and CurseForge APIs. +# +# Key features: +# - Multi-stage version matching (exact -> major.minor -> wildcard -> fallback) +# - Automatic dependency resolution with recursive fetching +# - Interactive mod selection with range support (e.g., "1-5", "1 3 5") +# - CurseForge API authentication via encrypted token +# - External dependency fetching for mods not in initial list +# +# @dependencies +# - utilities.sh (for print_header, print_success, print_warning, print_error, print_info, print_progress) +# - curl (for API requests) +# - jq (for JSON parsing) +# - openssl (for CurseForge API token decryption) +# +# @global_inputs +# - MC_VERSION: Target Minecraft version (e.g., "1.21.3") +# - MODS[]: Array of mod definitions in format "ModName|platform|mod_id" +# - REQUIRED_SPLITSCREEN_MODS[]: Array of mod names that must always be installed +# +# @global_outputs +# - SUPPORTED_MODS[]: Array of compatible mod names +# - MOD_DESCRIPTIONS[]: Array of mod descriptions (parallel to SUPPORTED_MODS) +# - MOD_URLS[]: Array of download URLs (parallel to SUPPORTED_MODS) +# - MOD_IDS[]: Array of mod IDs (parallel to SUPPORTED_MODS) +# - MOD_TYPES[]: Array of platform types "modrinth"|"curseforge" (parallel to SUPPORTED_MODS) +# - MOD_DEPENDENCIES[]: Array of space-separated dependency IDs (parallel to SUPPORTED_MODS) +# - FINAL_MOD_INDEXES[]: Array of indexes into SUPPORTED_MODS for selected mods +# +# @exports +# Functions: +# - check_mod_compatibility : Check all mods for MC version compatibility +# - check_modrinth_mod : Check single Modrinth mod compatibility +# - check_curseforge_mod : Check single CurseForge mod compatibility +# - resolve_all_dependencies: Resolve dependencies for all selected mods +# - select_user_mods : Interactive mod selection interface +# - fetch_and_add_external_mod : Fetch and add external dependency mod +# - get_curseforge_download_url: Get download URL for CurseForge mod +# +# @changelog +# 2.0.0 (2026-01-25) - Added comprehensive JSDoc documentation +# 1.0.0 (2024-XX-XX) - Initial implementation with dual-platform support # ============================================================================= -# Mod compatibility checking, dependency resolution, and user selection functions -# Handles both Modrinth and CurseForge platforms -# check_mod_compatibility: Main coordination function for mod compatibility checking -# Iterates through all mods and delegates to platform-specific checkers +# ============================================================================= +# MOD COMPATIBILITY CHECKING +# ============================================================================= + +# @function check_mod_compatibility +# @description Main coordination function for mod compatibility checking. +# Iterates through all mods in the MODS array and delegates to +# platform-specific checkers (Modrinth or CurseForge). +# @global MODS - (input) Array of mod definitions "ModName|platform|mod_id" +# @global MC_VERSION - (input) Target Minecraft version +# @global SUPPORTED_MODS - (output) Populated with compatible mod names +# @return 0 always (individual mod failures are non-fatal) +# @example +# MC_VERSION="1.21.3" +# MODS=("Fabric API|modrinth|P7dR8mSH" "Controllable|curseforge|317269") +# check_mod_compatibility check_mod_compatibility() { print_header "🔍 CHECKING MOD COMPATIBILITY" print_progress "Checking mod compatibility for Minecraft $MC_VERSION..." - + # Process each mod in the MODS array - # Format: "ModName|platform|mod_id" + # Format: "ModName|platform|mod_id" for mod in "${MODS[@]}"; do IFS='|' read -r MOD_NAME MOD_TYPE MOD_ID <<< "$mod" - + # Route to appropriate platform-specific checker # Use || true to prevent set -e from exiting on mod check failures if [[ "$MOD_TYPE" == "modrinth" ]]; then @@ -24,7 +87,7 @@ check_mod_compatibility() { check_curseforge_mod "$MOD_NAME" "$MOD_ID" || true fi done - + print_success "Mod compatibility check completed" local supported_count=0 if [[ ${#SUPPORTED_MODS[@]} -gt 0 ]]; then @@ -33,14 +96,29 @@ check_mod_compatibility() { print_info "Found $supported_count compatible mods for Minecraft $MC_VERSION" } -# check_modrinth_mod: Check if a Modrinth mod is compatible with target MC version -# Modrinth is the preferred platform - it has better API and more reliable data -# This function implements complex version matching logic to handle various version formats +# @function check_modrinth_mod +# @description Check if a Modrinth mod is compatible with the target Minecraft version. +# Modrinth is the preferred platform due to better API and more reliable data. +# Implements multi-stage version matching: +# Stage 1: Exact version match (e.g., "1.21.3") +# Stage 2: Major.minor match (e.g., "1.21", "1.21.x", "1.21.0") +# Stage 3: Advanced pattern matching with comprehensive version range support +# @param $1 - mod_name: Human-readable mod name for display +# @param $2 - mod_id: Modrinth project ID (e.g., "P7dR8mSH" for Fabric API) +# @global MC_VERSION - (input) Target Minecraft version +# @global SUPPORTED_MODS - (output) Appended with mod name if compatible +# @global MOD_URLS - (output) Appended with download URL if compatible +# @global MOD_IDS - (output) Appended with mod ID if compatible +# @global MOD_TYPES - (output) Appended with "modrinth" if compatible +# @global MOD_DEPENDENCIES - (output) Appended with dependency IDs if compatible +# @return 0 if compatible version found, 1 if not compatible or API error +# @example +# check_modrinth_mod "Fabric API" "P7dR8mSH" check_modrinth_mod() { local mod_name="$1" # Human-readable mod name local mod_id="$2" # Modrinth project ID (e.g., "P7dR8mSH" for Fabric API) local api_url="https://api.modrinth.com/v2/project/$mod_id/version" - + # Create temporary file for API response local tmp_body tmp_body=$(mktemp) @@ -48,7 +126,7 @@ check_modrinth_mod() { print_warning "mktemp failed for $mod_name" return 1 fi - + # Fetch all version data for this mod from Modrinth API # Make HTTP request to Modrinth API and capture both response and status code local http_code @@ -56,59 +134,59 @@ check_modrinth_mod() { local version_json version_json=$(cat "$tmp_body") rm "$tmp_body" - + # Validate API response (must be HTTP 200 and valid JSON) if [[ "$http_code" != "200" ]] || ! printf "%s" "$version_json" | jq -e . > /dev/null 2>&1; then print_warning "Mod $mod_name ($mod_id) is not compatible with $MC_VERSION (API error)" return 1 fi - + # Complex version matching logic - handles multiple version format scenarios # This logic tries progressively more lenient matching patterns local file_url="" # Download URL for compatible mod file local dep_ids="" # Space-separated list of dependency mod IDs - + # STAGE 1: Try exact version match with Fabric loader requirement # Example: Looking for exactly "1.21.3" with "fabric" loader file_url=$(printf "%s" "$version_json" | jq -r --arg v "$MC_VERSION" '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .files[] | select(.primary == true) | .url' 2>/dev/null | head -n1) if [[ -n "$file_url" && "$file_url" != "null" ]]; then dep_ids=$(printf "%s" "$version_json" | jq -r --arg v "$MC_VERSION" '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .dependencies[]? | select(.dependency_type=="required") | .project_id' 2>/dev/null | tr '\n' ' ') fi - + # STAGE 2: Try major.minor version match if exact match failed # Example: "1.21.3" -> try "1.21", "1.21.x", "1.21.0" # BUT ONLY if no specific patch version exists that's higher than what we're looking for if [[ -z "$file_url" || "$file_url" == "null" ]]; then local mc_major_minor mc_major_minor=$(echo "$MC_VERSION" | grep -oE '^[0-9]+\.[0-9]+') # Extract "1.21" from "1.21.3" - + # Before trying major.minor match, check if this version is higher than existing patch versions # This prevents matching 1.21 when looking for 1.21.6 if the highest patch version is only 1.21.5 local mc_patch_version mc_patch_version=$(echo "$MC_VERSION" | grep -oE '^[0-9]+\.[0-9]+\.([0-9]+)' | grep -oE '[0-9]+$') local should_try_fallback=true - + # If we have a patch version (e.g. 1.21.6), check if it's higher than any available versions if [[ -n "$mc_patch_version" ]]; then # Check if there's a standalone major.minor version (e.g., "1.21" without patch) local has_standalone_major_minor=$(printf "%s" "$version_json" | jq -r --arg major_minor "$mc_major_minor" ' .[] | select(.game_versions[] == $major_minor and (.loaders[] == "fabric")) | .version_number' 2>/dev/null | head -n1) - + # Get the highest patch version available for this major.minor series local highest_patch=$(printf "%s" "$version_json" | jq -r --arg major_minor "$mc_major_minor" ' - [.[] | select(.game_versions[] | test("^" + $major_minor + "\\.[0-9]+$") and (.loaders[] == "fabric")) | - .game_versions[] | select(test("^" + $major_minor + "\\.[0-9]+$")) | + [.[] | select(.game_versions[] | test("^" + $major_minor + "\\.[0-9]+$") and (.loaders[] == "fabric")) | + .game_versions[] | select(test("^" + $major_minor + "\\.[0-9]+$")) | split(".")[2] | tonumber] | if length > 0 then max else empty end' 2>/dev/null) - + # Don't try fallback if: # 1. There's a standalone major.minor version (e.g., "1.21") and we're requesting a patch version, OR # 2. There are patch versions and our requested patch is higher than the highest available - if [[ -n "$has_standalone_major_minor" && "$has_standalone_major_minor" != "null" ]] || + if [[ -n "$has_standalone_major_minor" && "$has_standalone_major_minor" != "null" ]] || [[ -n "$highest_patch" && "$highest_patch" != "null" && "$mc_patch_version" -gt "$highest_patch" ]]; then should_try_fallback=false fi fi - + # Only try major.minor fallback if it's safe to do so if [[ "$should_try_fallback" == true ]]; then # Try exact major.minor (e.g., "1.21") @@ -116,8 +194,8 @@ check_modrinth_mod() { if [[ -n "$file_url" && "$file_url" != "null" ]]; then dep_ids=$(printf "%s" "$version_json" | jq -r --arg v "$mc_major_minor" '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .dependencies[]? | select(.dependency_type=="required") | .project_id' 2>/dev/null | tr '\n' ' ') fi - - # Try wildcard version format (e.g., "1.21.x") + + # Try wildcard version format (e.g., "1.21.x") if [[ -z "$file_url" || "$file_url" == "null" ]]; then local mc_major_minor_x="$mc_major_minor.x" file_url=$(printf "%s" "$version_json" | jq -r --arg v "$mc_major_minor_x" '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .files[] | select(.primary == true) | .url' 2>/dev/null | head -n1) @@ -125,7 +203,7 @@ check_modrinth_mod() { dep_ids=$(printf "%s" "$version_json" | jq -r --arg v "$mc_major_minor_x" '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .dependencies[]? | select(.dependency_type=="required") | .project_id' 2>/dev/null | tr '\n' ' ') fi fi - + # Try zero-padded version format (e.g., "1.21.0") if [[ -z "$file_url" || "$file_url" == "null" ]]; then local mc_major_minor_0="$mc_major_minor.0" @@ -135,13 +213,13 @@ check_modrinth_mod() { fi fi fi - + # DISABLED: Limited prefix matching - this was allowing false positives # We've disabled this section to prevent matching lower patch versions # when a higher patch version is requested that doesn't exist # (e.g., preventing 1.21.5 from matching when 1.21.6 is requested) fi - + # STAGE 3: Advanced pattern matching with comprehensive version range support # This handles complex version patterns like ranges, wildcards, and edge cases if [[ -z "$file_url" || "$file_url" == "null" ]]; then @@ -150,33 +228,33 @@ check_modrinth_mod() { mc_major_minor=$(echo "$MC_VERSION" | grep -oE '^[0-9]+\.[0-9]+') local mc_major_minor_x="$mc_major_minor.x" local mc_major_minor_0="$mc_major_minor.0" - + # Apply the same fallback safety check as in STAGE 2 local mc_patch_version mc_patch_version=$(echo "$MC_VERSION" | grep -oE '^[0-9]+\.[0-9]+\.([0-9]+)' | grep -oE '[0-9]+$') local should_try_stage3_fallback=true - + # If we have a patch version, check if it's higher than any available versions if [[ -n "$mc_patch_version" ]]; then # Check if there's a standalone major.minor version (e.g., "1.21" without patch) local has_standalone_major_minor=$(printf "%s" "$version_json" | jq -r --arg major_minor "$mc_major_minor" ' .[] | select(.game_versions[] == $major_minor and (.loaders[] == "fabric")) | .version_number' 2>/dev/null | head -n1) - + # Get the highest patch version available for this major.minor series local highest_patch=$(printf "%s" "$version_json" | jq -r --arg major_minor "$mc_major_minor" ' - [.[] | select(.game_versions[] | test("^" + $major_minor + "\\.[0-9]+$") and (.loaders[] == "fabric")) | - .game_versions[] | select(test("^" + $major_minor + "\\.[0-9]+$")) | + [.[] | select(.game_versions[] | test("^" + $major_minor + "\\.[0-9]+$") and (.loaders[] == "fabric")) | + .game_versions[] | select(test("^" + $major_minor + "\\.[0-9]+$")) | split(".")[2] | tonumber] | if length > 0 then max else empty end' 2>/dev/null) - + # Don't try fallback if: # 1. There's a standalone major.minor version (e.g., "1.21") and we're requesting a patch version, OR # 2. There are patch versions and our requested patch is higher than the highest available - if [[ -n "$has_standalone_major_minor" && "$has_standalone_major_minor" != "null" ]] || + if [[ -n "$has_standalone_major_minor" && "$has_standalone_major_minor" != "null" ]] || [[ -n "$highest_patch" && "$highest_patch" != "null" && "$mc_patch_version" -gt "$highest_patch" ]]; then should_try_stage3_fallback=false fi fi - + # Only proceed with STAGE 3 if fallback is safe if [[ "$should_try_stage3_fallback" == true ]]; then # Simplified and corrected jq filter with stricter version matching @@ -200,7 +278,7 @@ check_modrinth_mod() { } | @base64 ' - + # Execute the corrected jq filter with all version variants local jq_result jq_result=$(printf "%s" "$version_json" | jq -r \ @@ -220,7 +298,7 @@ check_modrinth_mod() { fi fi fi - + # Final result processing: Add to supported mods if we found a compatible version if [[ -n "$file_url" && "$file_url" != "null" ]]; then SUPPORTED_MODS+=("$mod_name") # Add to list of compatible mods @@ -235,33 +313,46 @@ check_modrinth_mod() { fi } -# check_curseforge_mod: Check CurseForge mod compatibility with encrypted API access -# CurseForge requires API key authentication and has more restrictive access -# API token is encrypted and stored in the GitHub repository for security +# @function check_curseforge_mod +# @description Check CurseForge mod compatibility with encrypted API access. +# CurseForge requires API key authentication with more restrictive access. +# The API token is encrypted (AES-256-CBC) and stored in the repository. +# @param $1 - mod_name: Human-readable mod name for display +# @param $2 - cf_project_id: CurseForge project ID (numeric) +# @global MC_VERSION - (input) Target Minecraft version +# @global SUPPORTED_MODS - (output) Appended with mod name if compatible +# @global MOD_URLS - (output) Appended with download URL if compatible +# @global MOD_IDS - (output) Appended with mod ID if compatible +# @global MOD_TYPES - (output) Appended with "curseforge" if compatible +# @global MOD_DEPENDENCIES - (output) Appended with dependency IDs if compatible +# @return 0 if compatible version found, 1 if not compatible or API error +# @note Uses modLoaderType=4 filter for Fabric mods +# @example +# check_curseforge_mod "Controllable" "317269" check_curseforge_mod() { local mod_name="$1" # Human-readable mod name local cf_project_id="$2" # CurseForge project ID (numeric) - + # Simplified CurseForge API access using a simpler method # Instead of the complex encrypted token approach, use alternative method local cf_api_key="" - + # Try to use a simple decryption method for the token local cf_token_enc_url="https://raw.githubusercontent.com/FlyingEwok/MinecraftSplitscreenSteamdeck/main/token.enc" local tmp_token_file - + # Create temporary file for encrypted token download with timeout tmp_token_file=$(mktemp) if [[ -z "$tmp_token_file" ]]; then print_warning "mktemp failed for $mod_name" return 1 fi - + # Download with timeout to prevent hanging local http_code http_code=$(timeout 10 curl -s -L -w "%{http_code}" -o "$tmp_token_file" "$cf_token_enc_url" 2>/dev/null) local curl_exit=$? - + if [[ $curl_exit -eq 124 ]]; then print_warning "CurseForge API token download timed out for $mod_name" rm -f "$tmp_token_file" @@ -271,7 +362,7 @@ check_curseforge_mod() { rm -f "$tmp_token_file" return 1 fi - + # Decrypt API token using OpenSSL (requires passphrase hardcoded for automation) if command -v openssl >/dev/null 2>&1; then cf_api_key=$(openssl enc -d -aes-256-cbc -a -pbkdf2 -in "$tmp_token_file" -pass pass:"MinecraftSplitscreenSteamDeck2025" 2>/dev/null | tr -d '\n\r' | sed 's/[[:space:]]*$//') @@ -280,16 +371,16 @@ check_curseforge_mod() { rm -f "$tmp_token_file" return 1 fi - + # Clean up temp file immediately rm -f "$tmp_token_file" - + # If OpenSSL decryption failed, skip this mod if [[ -z "$cf_api_key" ]]; then print_warning "Failed to decrypt CurseForge API token for $mod_name (skipping)" return 1 fi - + # Query CurseForge API with Fabric loader filter (modLoaderType=4 = Fabric) # Note: We filter by Fabric loader but not by game version in the URL # Game version filtering is done in post-processing for more flexibility @@ -300,14 +391,14 @@ check_curseforge_mod() { print_warning "mktemp failed for CurseForge API call" return 1 fi - + # Make authenticated API request to CurseForge with timeout http_code=$(timeout 15 curl -s -L -w "%{http_code}" -o "$tmp_body" -H "x-api-key: $cf_api_key" "$cf_api_url" 2>/dev/null) local curl_exit=$? local version_json version_json=$(cat "$tmp_body") rm "$tmp_body" - + # Check for timeout or API failure if [[ $curl_exit -eq 124 ]]; then print_warning "❌ $mod_name ($cf_project_id) - CurseForge API timeout" @@ -316,14 +407,14 @@ check_curseforge_mod() { print_warning "❌ $mod_name ($cf_project_id) - API error (HTTP $http_code)" return 1 fi - + # Version compatibility checking for CurseForge mods # Uses same versioning logic as Modrinth but with CurseForge API structure local mc_major_minor mc_major_minor=$(echo "$MC_VERSION" | grep -oE '^[0-9]+\.[0-9]+') local mc_major_minor_x="$mc_major_minor.x" local mc_major_minor_0="$mc_major_minor.0" - + # CurseForge-specific jq filter for version matching # Checks gameVersions array and extracts downloadUrl and dependencies # relationType == 3 means "required dependency" in CurseForge API @@ -338,7 +429,7 @@ check_curseforge_mod() { | {url: .downloadUrl, dependencies: (.dependencies // [] | map(select(.relationType == 3) | .modId))} | @base64 ' - + # Execute the jq filter to find compatible CurseForge mod version local jq_result jq_result=$(printf "%s" "$version_json" | jq -r \ @@ -347,14 +438,14 @@ check_curseforge_mod() { --arg mc_major_minor_x "$mc_major_minor_x" \ --arg mc_major_minor_0 "$mc_major_minor_0" \ "$jq_filter" 2>/dev/null | head -n1) - + # Process the result if we found a compatible version if [[ -n "$jq_result" ]]; then local decoded decoded=$(echo "$jq_result" | base64 --decode) file_url=$(echo "$decoded" | jq -r '.url') dep_ids=$(echo "$decoded" | jq -r '.dependencies[]?' | tr '\n' ' ') - + # Add to supported mods list with CurseForge-specific information SUPPORTED_MODS+=("$mod_name") MOD_DESCRIPTIONS+=("") # Placeholder for description @@ -368,13 +459,26 @@ check_curseforge_mod() { fi } -# resolve_all_dependencies: Main function to automatically resolve all mod dependencies -# This function builds a complete dependency tree and ensures all required mods are included -# Parameters: None (operates on FINAL_MOD_INDEXES global array) +# ============================================================================= +# DEPENDENCY RESOLUTION +# ============================================================================= + +# @function resolve_all_dependencies +# @description Main function to automatically resolve all mod dependencies. +# Builds a complete dependency tree and ensures all required mods +# are included. Uses single-pass resolution to avoid infinite loops. +# @global FINAL_MOD_INDEXES - (input/output) Array of selected mod indexes +# @global MOD_IDS - (input) Array of mod IDs +# @global MOD_TYPES - (input) Array of platform types +# @global SUPPORTED_MODS - (input/output) May be extended with external deps +# @return 0 always +# @example +# FINAL_MOD_INDEXES=(0 1 2) +# resolve_all_dependencies resolve_all_dependencies() { print_header "🔗 AUTOMATIC DEPENDENCY RESOLUTION" print_progress "Automatically resolving mod dependencies..." - + # Check if we have any mods to process local final_mod_count=0 if [[ ${#FINAL_MOD_INDEXES[@]} -gt 0 ]]; then @@ -384,28 +488,28 @@ resolve_all_dependencies() { print_info "No mods selected for dependency resolution" return 0 fi - + local initial_mod_count=$final_mod_count print_info "Starting dependency resolution with $initial_mod_count selected mods" - + # Simplified single-pass dependency resolution to avoid hangs local -A processed_mods local original_mod_indexes=("${FINAL_MOD_INDEXES[@]}") # Copy original list - + # Process each originally selected mod for immediate dependencies only for idx in "${original_mod_indexes[@]}"; do local mod_id="${MOD_IDS[$idx]}" local mod_type="${MOD_TYPES[$idx]}" local mod_name="${SUPPORTED_MODS[$idx]}" - + # Skip if already processed if [[ -n "${processed_mods[$mod_id]:-}" ]]; then continue fi - + processed_mods["$mod_id"]=1 print_info " → Checking dependencies for: $mod_name" - + # Get dependencies from API based on mod type local deps="" case "$mod_type" in @@ -416,7 +520,7 @@ resolve_all_dependencies() { deps=$(resolve_curseforge_dependencies_api "$mod_id" 2>/dev/null || echo "") ;; esac - + # Process found dependencies (single level only) if [[ -n "$deps" && "$deps" != " " ]]; then print_info " → Found dependencies: $deps" @@ -427,7 +531,7 @@ resolve_all_dependencies() { print_warning " → Skipping invalid dependency ID (appears to be mod name): $dep_id" continue fi - + # Additional validation - CurseForge IDs should be numeric, Modrinth IDs should be alphanumeric with specific patterns if [[ "$dep_id" =~ ^[0-9]+$ ]]; then # Valid CurseForge ID (numeric) @@ -439,7 +543,7 @@ resolve_all_dependencies() { print_warning " → Skipping dependency with invalid ID format: $dep_id" continue fi - + # Check if dependency is already in our mod list local found_internal=false for i in "${!MOD_IDS[@]}"; do @@ -452,7 +556,7 @@ resolve_all_dependencies() { break fi done - + if [[ "$already_selected" == false ]]; then FINAL_MOD_INDEXES+=("$i") print_info " → Added internal dependency: ${SUPPORTED_MODS[$i]}" @@ -461,11 +565,11 @@ resolve_all_dependencies() { break fi done - + # If not found internally, try to fetch as external dependency with timeout if [[ "$found_internal" == false ]]; then print_info " → Fetching external dependency: $dep_id" - + # Fetch external dependency (timeout handled within the function) if fetch_and_add_external_mod "$dep_id" "$dep_platform"; then print_info " → Successfully added external dependency: $dep_id" @@ -480,25 +584,29 @@ resolve_all_dependencies() { print_info " → No dependencies found" fi done - + local updated_mod_count=0 if [[ ${#FINAL_MOD_INDEXES[@]} -gt 0 ]]; then updated_mod_count=${#FINAL_MOD_INDEXES[@]} fi local added_count=$((updated_mod_count - initial_mod_count)) - + print_success "Dependency resolution complete!" print_info "Added $added_count dependencies ($initial_mod_count → $updated_mod_count total mods)" } -# resolve_mod_dependencies: Resolve dependencies for a specific mod -# Fetches dependency information from Modrinth or CurseForge API based on mod type -# Parameters: -# $1 - mod_id: The mod ID to resolve dependencies for -# Returns: Space-separated list of dependency mod IDs +# @function resolve_mod_dependencies +# @description Resolve dependencies for a specific mod by routing to the +# appropriate platform-specific resolver based on mod type. +# @param $1 - mod_id: The mod ID to resolve dependencies for +# @global MOD_IDS - (input) Array of mod IDs to find the mod +# @global MOD_TYPES - (input) Array of platform types +# @global SUPPORTED_MODS - (input) Array of mod names for logging +# @stdout Space-separated list of dependency mod IDs +# @return 0 if dependencies found, 1 if mod not found or unknown type resolve_mod_dependencies() { local mod_id="$1" - + # Find mod in our arrays to determine platform type local mod_type="" local mod_name="" @@ -509,11 +617,11 @@ resolve_mod_dependencies() { break fi done - + if [[ -z "$mod_type" ]]; then return 1 fi - + # Route to appropriate platform-specific dependency resolver case "$mod_type" in "modrinth") @@ -529,47 +637,48 @@ resolve_mod_dependencies() { esac } -# resolve_modrinth_dependencies: Get dependencies from Modrinth API -# Uses the same version matching logic as mod compatibility checking but focused on dependencies -# Parameters: -# $1 - mod_id: Modrinth project ID -# $2 - mod_name: Human-readable mod name for logging -# Returns: Space-separated list of required dependency mod IDs +# @function resolve_modrinth_dependencies +# @description Get dependencies from Modrinth API using version matching logic. +# @param $1 - mod_id: Modrinth project ID +# @param $2 - mod_name: Human-readable mod name for logging +# @global MC_VERSION - (input) Target Minecraft version for filtering +# @stdout Space-separated list of required dependency project IDs +# @return 0 if API call successful, 1 on error resolve_modrinth_dependencies() { local mod_id="$1" local mod_name="$2" local api_url="https://api.modrinth.com/v2/project/$mod_id/version" - + # Create temporary file for API response local tmp_body tmp_body=$(mktemp) if [[ -z "$tmp_body" ]]; then return 1 fi - + # Fetch version data from Modrinth API local http_code http_code=$(curl -s -L -w "%{http_code}" -o "$tmp_body" "$api_url" 2>/dev/null) local version_json version_json=$(cat "$tmp_body") rm "$tmp_body" - + # Validate API response if [[ "$http_code" != "200" ]] || ! printf "%s" "$version_json" | jq -e . > /dev/null 2>&1; then return 1 fi - + # Use the same version matching logic as mod compatibility checking local mc_major_minor mc_major_minor=$(echo "$MC_VERSION" | grep -oE '^[0-9]+\.[0-9]+') - + # Try exact version match first local dep_ids dep_ids=$(printf "%s" "$version_json" | jq -r \ --arg v "$MC_VERSION" \ '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .dependencies[]? | select(.dependency_type=="required") | .project_id' \ 2>/dev/null | tr '\n' ' ') - + # Try major.minor version if exact match failed if [[ -z "$dep_ids" ]]; then dep_ids=$(printf "%s" "$version_json" | jq -r \ @@ -577,7 +686,7 @@ resolve_modrinth_dependencies() { '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .dependencies[]? | select(.dependency_type=="required") | .project_id' \ 2>/dev/null | tr '\n' ' ') fi - + # Try wildcard version (1.21.x) if still no results if [[ -z "$dep_ids" ]]; then local mc_major_minor_x="$mc_major_minor.x" @@ -586,7 +695,7 @@ resolve_modrinth_dependencies() { '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .dependencies[]? | select(.dependency_type=="required") | .project_id' \ 2>/dev/null | tr '\n' ' ') fi - + # Clean up and return dependency IDs dep_ids=$(echo "$dep_ids" | xargs) # Trim whitespace if [[ -n "$dep_ids" ]]; then @@ -594,16 +703,18 @@ resolve_modrinth_dependencies() { fi } -# resolve_curseforge_dependencies: Get dependencies from CurseForge API -# Similar to Modrinth resolver but uses CurseForge API structure and authentication -# Parameters: -# $1 - mod_id: CurseForge project ID (numeric) -# $2 - mod_name: Human-readable mod name for logging -# Returns: Space-separated list of required dependency mod IDs +# @function resolve_curseforge_dependencies +# @description Get dependencies from CurseForge API with authentication. +# @param $1 - mod_id: CurseForge project ID (numeric) +# @param $2 - mod_name: Human-readable mod name for logging +# @global MC_VERSION - (input) Target Minecraft version for filtering +# @stdout Space-separated list of required dependency mod IDs +# @return 0 if API call successful, 1 on error +# @note relationType == 3 means "required dependency" in CurseForge API resolve_curseforge_dependencies() { local mod_id="$1" local mod_name="$2" - + # Download and decrypt CurseForge API token local cf_token_enc_url="https://raw.githubusercontent.com/FlyingEwok/MinecraftSplitscreenSteamdeck/main/token.enc" local tmp_token_file @@ -611,7 +722,7 @@ resolve_curseforge_dependencies() { if [[ -z "$tmp_token_file" ]]; then return 1 fi - + # Download encrypted token local http_code http_code=$(curl -s -L -w "%{http_code}" -o "$tmp_token_file" "$cf_token_enc_url" 2>/dev/null) @@ -619,7 +730,7 @@ resolve_curseforge_dependencies() { rm "$tmp_token_file" return 1 fi - + # Decrypt API token using OpenSSL (requires passphrase hardcoded for automation) local cf_api_key if command -v openssl >/dev/null 2>&1; then @@ -629,11 +740,11 @@ resolve_curseforge_dependencies() { return 1 fi rm "$tmp_token_file" - + if [[ -z "$cf_api_key" ]]; then return 1 fi - + # Query CurseForge API with Fabric loader filter local cf_api_url="https://api.curseforge.com/v1/mods/$mod_id/files?modLoaderType=4" local tmp_body @@ -641,24 +752,24 @@ resolve_curseforge_dependencies() { if [[ -z "$tmp_body" ]]; then return 1 fi - + # Make authenticated API request http_code=$(curl -s -L -w "%{http_code}" -o "$tmp_body" -H "x-api-key: $cf_api_key" "$cf_api_url" 2>/dev/null) local version_json version_json=$(cat "$tmp_body") rm "$tmp_body" - + # Validate API response if [[ "$http_code" != "200" ]] || ! printf "%s" "$version_json" | jq -e . > /dev/null 2>&1; then return 1 fi - + # Extract dependencies using CurseForge API structure local mc_major_minor mc_major_minor=$(echo "$MC_VERSION" | grep -oE '^[0-9]+\.[0-9]+') local mc_major_minor_x="$mc_major_minor.x" local mc_major_minor_0="$mc_major_minor.0" - + # CurseForge dependency extraction with version matching local dep_ids dep_ids=$(printf "%s" "$version_json" | jq -r \ @@ -673,7 +784,7 @@ resolve_curseforge_dependencies() { (.gameVersions[] == $mc_major_minor_0)) ) | .dependencies[]? | select(.relationType == 3) | .modId' \ 2>/dev/null | tr '\n' ' ') - + # Clean up and return dependency IDs dep_ids=$(echo "$dep_ids" | xargs) # Trim whitespace if [[ -n "$dep_ids" ]]; then @@ -681,28 +792,30 @@ resolve_curseforge_dependencies() { fi } -# resolve_modrinth_dependencies_api: Get dependencies from Modrinth API -# Fetches the project data from Modrinth and extracts required dependencies -# Parameters: -# $1 - mod_id: The Modrinth project ID or slug -# Returns: Space-separated list of dependency mod IDs +# @function resolve_modrinth_dependencies_api +# @description Get dependencies from Modrinth API with fallback support. +# Uses temporary files to handle large API responses. +# @param $1 - mod_id: The Modrinth project ID or slug +# @global MC_VERSION - (input) Target Minecraft version for filtering +# @stdout Space-separated list of dependency mod IDs +# @return 0 always (returns empty string on failure) resolve_modrinth_dependencies_api() { local mod_id="$1" local dependencies="" - + # Skip if essential commands are not available if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then echo "" # Return empty dependencies return 0 fi - + # Create temporary file for large API response to avoid "Argument list too long" error local tmp_file tmp_file=$(mktemp) || return 1 - + # Get the latest version for the Minecraft version we're using with timeout local versions_url="https://api.modrinth.com/v2/project/$mod_id/version" - + if command -v curl >/dev/null 2>&1; then if ! curl -s -m 10 "$versions_url" -o "$tmp_file" 2>/dev/null; then rm -f "$tmp_file" @@ -716,7 +829,7 @@ resolve_modrinth_dependencies_api() { return 0 fi fi - + # Check if we got valid JSON data if [[ ! -s "$tmp_file" ]] || ! jq -e . < "$tmp_file" > /dev/null 2>&1; then rm -f "$tmp_file" @@ -728,15 +841,15 @@ resolve_modrinth_dependencies_api() { if command -v jq >/dev/null 2>&1; then local mc_major_minor mc_major_minor=$(echo "$MC_VERSION" | grep -oE '^[0-9]+\.[0-9]+') # Extract "1.21" from "1.21.3" - + # Simple jq filter to get dependencies from compatible fabric versions with strict matching # Use temporary file to avoid command line length limits dependencies=$(jq -r " - .[] - | select(.loaders[]? == \"fabric\") + .[] + | select(.loaders[]? == \"fabric\") | select(.game_versions[]? | (. == \"$MC_VERSION\" or . == \"$mc_major_minor\" or . == \"${mc_major_minor}.x\" or . == \"${mc_major_minor}.0\")) - | .dependencies[]? - | select(.dependency_type == \"required\") + | .dependencies[]? + | select(.dependency_type == \"required\") | .project_id " < "$tmp_file" 2>/dev/null | sort -u | tr '\n' ' ' | sed 's/[[:space:]]*$//') else @@ -751,29 +864,31 @@ resolve_modrinth_dependencies_api() { # Clean up temporary file rm -f "$tmp_file" - + # Use fallback dependencies if API call failed if [[ -z "$dependencies" ]]; then dependencies=$(fallback_dependencies "$mod_id" "modrinth") fi - + echo "$dependencies" } -# resolve_curseforge_dependencies_api: Get dependencies from CurseForge API -# Fetches the mod data from CurseForge and extracts required dependencies -# Parameters: -# $1 - mod_id: The CurseForge project ID (numeric) -# Returns: Space-separated list of dependency mod IDs +# @function resolve_curseforge_dependencies_api +# @description Get dependencies from CurseForge API with authentication. +# Includes hardcoded fallbacks for critical mods. +# @param $1 - mod_id: The CurseForge project ID (numeric) +# @global MC_VERSION - (input) Target Minecraft version for filtering +# @stdout Space-separated list of dependency mod IDs +# @return 0 on success, 1 on authentication failure resolve_curseforge_dependencies_api() { local mod_id="$1" local dependencies="" - + # Download encrypted CurseForge API token from GitHub repository local token_url="https://raw.githubusercontent.com/FlyingEwok/MinecraftSplitscreenSteamdeck/main/token.enc" local encrypted_token_file=$(mktemp) local http_code - + if command -v curl >/dev/null 2>&1; then http_code=$(curl -s -w "%{http_code}" -o "$encrypted_token_file" "$token_url" 2>/dev/null) elif command -v wget >/dev/null 2>&1; then @@ -787,13 +902,13 @@ resolve_curseforge_dependencies_api() { echo "" return 1 fi - + if [[ "$http_code" != "200" || ! -s "$encrypted_token_file" ]]; then rm -f "$encrypted_token_file" echo "" return 1 fi - + # Decrypt the API token using OpenSSL (requires passphrase hardcoded for automation) local api_token if command -v openssl >/dev/null 2>&1; then @@ -803,18 +918,18 @@ resolve_curseforge_dependencies_api() { echo "" return 1 fi - + rm -f "$encrypted_token_file" - + if [[ -z "$api_token" ]]; then echo "" return 1 fi - + # Fetch mod info from CurseForge API with authentication local api_url="https://api.curseforge.com/v1/mods/$mod_id" local temp_file=$(mktemp) - + if command -v curl >/dev/null 2>&1; then curl -s -H "x-api-key: $api_token" -o "$temp_file" "$api_url" 2>/dev/null elif command -v wget >/dev/null 2>&1; then @@ -824,52 +939,52 @@ resolve_curseforge_dependencies_api() { echo "" return 1 fi - + # Extract required dependencies from mod info if [[ -s "$temp_file" ]] && command -v jq >/dev/null 2>&1; then # Get the latest files for this mod local files_url="https://api.curseforge.com/v1/mods/$mod_id/files?modLoaderType=4" local files_temp=$(mktemp) - + if command -v curl >/dev/null 2>&1; then curl -s -H "x-api-key: $api_token" -o "$files_temp" "$files_url" 2>/dev/null elif command -v wget >/dev/null 2>&1; then wget -q --header="x-api-key: $api_token" -O "$files_temp" "$files_url" 2>/dev/null fi - + if [[ -s "$files_temp" ]]; then # Find the most recent compatible file local mc_major_minor mc_major_minor=$(echo "$MC_VERSION" | grep -oE '^[0-9]+\.[0-9]+') - + # Extract file ID from the most recent compatible file with strict version matching local file_id=$(jq -r --arg v "$MC_VERSION" --arg mmv "$mc_major_minor" '.data[] | select(.gameVersions[] == $v or .gameVersions[] == $mmv or .gameVersions[] == ($mmv + ".x") or .gameVersions[] == ($mmv + ".0")) | .id' "$files_temp" 2>/dev/null | head -n1) - + if [[ -n "$file_id" && "$file_id" != "null" ]]; then # Get dependencies for this specific file local file_info_url="https://api.curseforge.com/v1/mods/$mod_id/files/$file_id" local file_info_temp=$(mktemp) - + if command -v curl >/dev/null 2>&1; then curl -s -H "x-api-key: $api_token" -o "$file_info_temp" "$file_info_url" 2>/dev/null elif command -v wget >/dev/null 2>&1; then wget -q --header="x-api-key: $api_token" -O "$file_info_temp" "$file_info_url" 2>/dev/null fi - + if [[ -s "$file_info_temp" ]]; then # Extract required dependencies dependencies=$(jq -r '.data.dependencies[]? | select(.relationType == 3) | .modId' "$file_info_temp" 2>/dev/null | tr '\n' ' ') fi - + rm -f "$file_info_temp" fi fi - + rm -f "$files_temp" fi - + rm -f "$temp_file" - + # Use fallback dependencies if API call failed if [[ -z "$dependencies" ]]; then dependencies=$(fallback_dependencies "$mod_id" "curseforge") @@ -887,32 +1002,43 @@ resolve_curseforge_dependencies_api() { "238222") # JEI dependencies="306612" # Fabric API ;; - "325471") # Controllable + "325471") # Controllable dependencies="634179" # Framework ;; esac fi - + echo "$dependencies" } -# fetch_and_add_external_mod: Fetch external mod data and add to mod arrays -# Downloads mod information from APIs and adds it to our internal mod arrays -# Parameters: -# $1 - mod_id: The external mod ID -# $2 - mod_type: The platform type (modrinth/curseforge) -# Returns: 0 if successful, 1 if failed +# ============================================================================= +# EXTERNAL MOD FETCHING +# ============================================================================= + +# @function fetch_and_add_external_mod +# @description Fetch external mod data from APIs and add to internal mod arrays. +# Used for dependencies not in the initial mod list. +# @param $1 - mod_id: The external mod ID +# @param $2 - mod_type: The platform type ("modrinth" or "curseforge") +# @global SUPPORTED_MODS - (output) Appended with mod name +# @global MOD_DESCRIPTIONS - (output) Appended with mod description +# @global MOD_IDS - (output) Appended with mod ID +# @global MOD_TYPES - (output) Appended with platform type +# @global MOD_URLS - (output) Appended with download URL +# @global MOD_DEPENDENCIES - (output) Appended with empty string +# @global FINAL_MOD_INDEXES - (output) Appended with new mod index +# @return 0 if successful, 1 if failed fetch_and_add_external_mod() { local ext_mod_id="$1" local ext_mod_type="$2" local success=false - + case "$ext_mod_type" in "modrinth") # Create temporary file for downloading large JSON responses local temp_file=$(mktemp) local api_url="https://api.modrinth.com/v2/project/$ext_mod_id" - + # Download to temp file without size restrictions local download_success=false if command -v curl >/dev/null 2>&1; then @@ -924,14 +1050,14 @@ fetch_and_add_external_mod() { download_success=true fi fi - + if [[ "$download_success" == true && -s "$temp_file" ]]; then # Check if the file contains valid JSON (not an error) if ! grep -q '"error"' "$temp_file" 2>/dev/null; then # Extract mod name from JSON file using jq if available, fallback to grep local mod_title="" local mod_description="" - + if command -v jq >/dev/null 2>&1; then mod_title=$(jq -r '.title // ""' "$temp_file" 2>/dev/null) mod_description=$(jq -r '.description // ""' "$temp_file" 2>/dev/null) @@ -940,7 +1066,7 @@ fetch_and_add_external_mod() { mod_title=$(grep -o '"title":"[^"]*"' "$temp_file" | sed 's/"title":"//g' | sed 's/"//g' | head -1) mod_description=$(grep -o '"description":"[^"]*"' "$temp_file" | sed 's/"description":"//g' | sed 's/"//g' | head -1) fi - + if [[ -n "$mod_title" ]]; then # Add to our arrays (keep all arrays synchronized) SUPPORTED_MODS+=("$mod_title") @@ -949,7 +1075,7 @@ fetch_and_add_external_mod() { MOD_TYPES+=("modrinth") MOD_URLS+=("") # Empty URL - will be resolved during download MOD_DEPENDENCIES+=("") # Will be populated if needed - + # Add to final selection local new_index=$((${#SUPPORTED_MODS[@]} - 1)) FINAL_MOD_INDEXES+=("$new_index") @@ -957,22 +1083,22 @@ fetch_and_add_external_mod() { fi fi fi - + # Clean up temp file rm -f "$temp_file" 2>/dev/null ;; - + "curseforge") # Use the new robust CurseForge API integration local mod_title="" local mod_description="" local download_url="" - + # Download encrypted CurseForge API token from GitHub repository local token_url="https://raw.githubusercontent.com/FlyingEwok/MinecraftSplitscreenSteamdeck/main/token.enc" local encrypted_token_file=$(mktemp) local http_code - + if command -v curl >/dev/null 2>&1; then http_code=$(curl -s -w "%{http_code}" -o "$encrypted_token_file" "$token_url" 2>/dev/null) elif command -v wget >/dev/null 2>&1; then @@ -982,40 +1108,40 @@ fetch_and_add_external_mod() { http_code="404" fi fi - + if [[ "$http_code" == "200" && -s "$encrypted_token_file" ]]; then # Decrypt the API token local api_token if command -v openssl >/dev/null 2>&1; then api_token=$(openssl enc -d -aes-256-cbc -a -pbkdf2 -in "$encrypted_token_file" -pass pass:"MinecraftSplitscreenSteamdeck2025" 2>/dev/null | tr -d '\n\r' | sed 's/[[:space:]]*$//') fi - + if [[ -n "$api_token" ]]; then # Fetch mod info from CurseForge API local api_url="https://api.curseforge.com/v1/mods/$ext_mod_id" local temp_file=$(mktemp) - + if command -v curl >/dev/null 2>&1; then curl -s -H "x-api-key: $api_token" -o "$temp_file" "$api_url" 2>/dev/null elif command -v wget >/dev/null 2>&1; then wget -q --header="x-api-key: $api_token" -O "$temp_file" "$api_url" 2>/dev/null fi - + # Extract mod title and description if [[ -s "$temp_file" ]] && command -v jq >/dev/null 2>&1; then mod_title=$(jq -r '.data.name // ""' "$temp_file" 2>/dev/null) mod_description=$(jq -r '.data.summary // ""' "$temp_file" 2>/dev/null) fi - + rm -f "$temp_file" - + # Get download URL using our robust function download_url=$(get_curseforge_download_url "$ext_mod_id") fi fi - + rm -f "$encrypted_token_file" - + # Fallback for known mods if API fails if [[ -z "$mod_title" ]]; then case "$ext_mod_id" in @@ -1037,7 +1163,7 @@ fetch_and_add_external_mod() { ;; esac fi - + # Add to our arrays SUPPORTED_MODS+=("$mod_title") MOD_DESCRIPTIONS+=("${mod_description:-External dependency from CurseForge}") @@ -1045,13 +1171,13 @@ fetch_and_add_external_mod() { MOD_TYPES+=("curseforge") MOD_URLS+=("$download_url") # May be empty if API failed MOD_DEPENDENCIES+=("") # Will be populated if needed - + local new_index=$((${#SUPPORTED_MODS[@]} - 1)) FINAL_MOD_INDEXES+=("$new_index") success=true ;; esac - + if [[ "$success" == true ]]; then return 0 else @@ -1059,20 +1185,22 @@ fetch_and_add_external_mod() { fi } -# get_curseforge_download_url: Get download URL for CurseForge mod -# Uses CurseForge API to find compatible mod file and return download URL -# Parameters: -# $1 - mod_id: The CurseForge project ID (numeric) -# Returns: Download URL for the compatible mod file, or empty string if not found +# @function get_curseforge_download_url +# @description Get download URL for CurseForge mod using authenticated API. +# Tries multiple version matching strategies. +# @param $1 - mod_id: The CurseForge project ID (numeric) +# @global MC_VERSION - (input) Target Minecraft version for filtering +# @stdout Download URL for the compatible mod file, or empty string +# @return 0 on success, 1 on authentication failure get_curseforge_download_url() { local mod_id="$1" local download_url="" - + # Download encrypted CurseForge API token from GitHub repository local token_url="https://raw.githubusercontent.com/FlyingEwok/MinecraftSplitscreenSteamdeck/main/token.enc" local encrypted_token_file=$(mktemp) local http_code - + if command -v curl >/dev/null 2>&1; then http_code=$(curl -s -w "%{http_code}" -o "$encrypted_token_file" "$token_url" 2>/dev/null) elif command -v wget >/dev/null 2>&1; then @@ -1086,13 +1214,13 @@ get_curseforge_download_url() { echo "" return 1 fi - + if [[ "$http_code" != "200" || ! -s "$encrypted_token_file" ]]; then rm -f "$encrypted_token_file" echo "" return 1 fi - + # Decrypt the API token using OpenSSL (requires passphrase hardcoded for automation) local api_token if command -v openssl >/dev/null 2>&1; then @@ -1102,18 +1230,18 @@ get_curseforge_download_url() { echo "" return 1 fi - + rm -f "$encrypted_token_file" - + if [[ -z "$api_token" ]]; then echo "" return 1 fi - + # Fetch mod files from CurseForge API with Fabric loader filter local files_url="https://api.curseforge.com/v1/mods/$mod_id/files?modLoaderType=4" local temp_file=$(mktemp) - + if command -v curl >/dev/null 2>&1; then curl -s -H "x-api-key: $api_token" -o "$temp_file" "$files_url" 2>/dev/null elif command -v wget >/dev/null 2>&1; then @@ -1123,26 +1251,26 @@ get_curseforge_download_url() { echo "" return 1 fi - + # Parse response and find compatible file if [[ -s "$temp_file" ]] && command -v jq >/dev/null 2>&1; then local mc_major_minor mc_major_minor=$(echo "$MC_VERSION" | grep -oE '^[0-9]+\.[0-9]+') - + # Try exact version match first download_url=$(jq -r --arg v "$MC_VERSION" '.data[]? | select(.gameVersions[]? == $v) | .downloadUrl' "$temp_file" 2>/dev/null | head -n1) - + # Try major.minor version if exact match failed if [[ -z "$download_url" || "$download_url" == "null" ]]; then download_url=$(jq -r --arg v "$mc_major_minor" '.data[]? | select(.gameVersions[]? == $v) | .downloadUrl' "$temp_file" 2>/dev/null | head -n1) fi - + # Try wildcard version (e.g., "1.21.x") if [[ -z "$download_url" || "$download_url" == "null" ]]; then local mc_major_minor_x="$mc_major_minor.x" download_url=$(jq -r --arg v "$mc_major_minor_x" '.data[]? | select(.gameVersions[]? == $v) | .downloadUrl' "$temp_file" 2>/dev/null | head -n1) fi - + # Try limited previous patch version (more restrictive than prefix matching) if [[ -z "$download_url" || "$download_url" == "null" ]]; then local mc_patch_version @@ -1154,25 +1282,35 @@ get_curseforge_download_url() { download_url=$(jq -r --arg v "$mc_prev_version" '.data[]? | select(.gameVersions[]? == $v) | .downloadUrl' "$temp_file" 2>/dev/null | head -n1) fi fi - + # If still no URL found, try the latest file if [[ -z "$download_url" || "$download_url" == "null" ]]; then download_url=$(jq -r '.data[0]?.downloadUrl // ""' "$temp_file" 2>/dev/null) fi fi - + rm -f "$temp_file" - + # Return the download URL (may be empty if not found) echo "$download_url" } -# select_user_mods: Interactive mod selection with intelligent categorization -# Separates framework mods (auto-installed) from user-selectable mods -# Handles dependency resolution and ensures required splitscreen mods are included +# ============================================================================= +# USER INTERACTION +# ============================================================================= + +# @function select_user_mods +# @description Interactive mod selection interface with intelligent categorization. +# Separates framework mods (auto-installed) from user-selectable mods. +# Supports individual numbers and ranges (e.g., "1 3 5" or "1-5"). +# @global SUPPORTED_MODS - (input) Array of compatible mod names +# @global REQUIRED_SPLITSCREEN_MODS - (input) Mods that must always be installed +# @global FINAL_MOD_INDEXES - (output) Populated with selected mod indexes +# @stdin User input from /dev/tty (for curl | bash compatibility) +# @return 0 always (exits on no compatible mods) select_user_mods() { print_header "🎯 MOD SELECTION" - + # Validate that we have compatible mods to present to the user local supported_count=0 if [[ ${#SUPPORTED_MODS[@]} -gt 0 ]]; then @@ -1182,22 +1320,22 @@ select_user_mods() { print_error "No compatible mods found for Minecraft $MC_VERSION" exit 1 fi - + # Build list of user-selectable mods by filtering out framework and required mods # Framework mods (Fabric API, etc.) are installed automatically as dependencies # Required mods (Controllable, Splitscreen Support) are always installed local user_mod_indexes=() # Indexes of mods user can choose from local install_all_mods=false # Flag for "install all" option - + echo "" echo "The following mods are available for Minecraft $MC_VERSION:" echo "" - + # Display numbered list of user-selectable mods local counter=1 for i in "${!SUPPORTED_MODS[@]}"; do local skip=false - + # Skip required splitscreen mods (these are automatically installed) for req in "${REQUIRED_SPLITSCREEN_MODS[@]}"; do if [[ "${SUPPORTED_MODS[$i]}" == "$req"* ]]; then @@ -1205,23 +1343,24 @@ select_user_mods() { break fi done - + if [[ "$skip" == false ]]; then echo " $counter. ${SUPPORTED_MODS[$i]}" user_mod_indexes+=("$i") ((counter++)) fi done - + echo "" echo "Enter the numbers of the mods you want to install (e.g., '1 3 5' or '1-5'):" echo " 0 = Install all available mods (default)" echo " -1 = Install only required mods (Controllable and Splitscreen Support)" echo "" - + local mod_selection - read -p "Your choice [0]: " mod_selection - + # Use centralized prompt function that handles curl | bash piping + mod_selection=$(prompt_user "Your choice [0]: " "0" 60) + # Process user selection if [[ -z "$mod_selection" || "$mod_selection" == "0" ]]; then install_all_mods=true @@ -1232,10 +1371,10 @@ select_user_mods() { else print_info "Installing selected mods" fi - + # Build final mod list including dependencies declare -A added - + if [[ "$install_all_mods" == true ]]; then for i in "${!SUPPORTED_MODS[@]}"; do FINAL_MOD_INDEXES+=("$i") @@ -1245,18 +1384,18 @@ select_user_mods() { # Add selected mods if [[ -n "$mod_selection" ]]; then echo "Selected mods:" - + # SELECTION PROCESSING: Parse user input supporting individual numbers and ranges # Examples: "1 3 5", "1-5", "1 3-7 9" local expanded_selection=() - + # Parse each token in the selection for token in $mod_selection; do if [[ "$token" =~ ^[0-9]+-[0-9]+$ ]]; then # RANGE PARSING: Handle range format like "1-5" local start_num=${token%-*} local end_num=${token#*-} - + # Validate range bounds local max_range=${#user_mod_indexes[@]} if ((start_num >= 1 && end_num <= max_range && start_num <= end_num)); then @@ -1278,10 +1417,10 @@ select_user_mods() { print_warning "Invalid format: $token (use numbers or ranges like 1-5)" fi done - + # Remove duplicates and sort expanded_selection=($(printf "%s\n" "${expanded_selection[@]}" | sort -nu)) - + # Process the expanded selection for sel in "${expanded_selection[@]}"; do local idx=${user_mod_indexes[$((sel-1))]} @@ -1289,7 +1428,7 @@ select_user_mods() { FINAL_MOD_INDEXES+=("$idx") added[$idx]=1 done - + # Add dependencies for selected mods for sel in "${expanded_selection[@]}"; do local idx=${user_mod_indexes[$((sel-1))]} @@ -1319,11 +1458,21 @@ select_user_mods() { print_success "Final mod list prepared: $final_count mods selected" } -# add_mod_dependencies: Add dependencies for a specific mod +# @function add_mod_dependencies +# @description Add dependencies for a specific mod to the final selection. +# Handles special cases like Controllable needing Framework. +# @param $1 - mod_idx: Index into SUPPORTED_MODS array +# @param $2 - added_ref: Name reference to associative array tracking added mods +# @global SUPPORTED_MODS - (input) Array of mod names +# @global MOD_DEPENDENCIES - (input) Array of dependency strings +# @global MOD_IDS - (input) Array of mod IDs +# @global MODS - (input) Original mod definitions array +# @global FINAL_MOD_INDEXES - (output) Appended with dependency indexes +# @return 0 always add_mod_dependencies() { local mod_idx="$1" local -n added_ref="$2" - + # Handle special case for Controllable (needs Framework) if [[ "${SUPPORTED_MODS[$mod_idx]}" == "Controllable (Fabric)"* ]]; then for j in "${!MODS[@]}"; do @@ -1338,7 +1487,7 @@ add_mod_dependencies() { fi done fi - + # Add Modrinth dependencies local dep_string="${MOD_DEPENDENCIES[$mod_idx]}" if [[ -n "$dep_string" ]]; then diff --git a/modules/path_configuration.sh b/modules/path_configuration.sh new file mode 100644 index 0000000..01664eb --- /dev/null +++ b/modules/path_configuration.sh @@ -0,0 +1,662 @@ +#!/bin/bash +# ============================================================================= +# PATH CONFIGURATION MODULE - SINGLE SOURCE OF TRUTH +# ============================================================================= +# @file path_configuration.sh +# @version 1.2.1 +# @date 2026-01-25 +# @author aradanmn +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Centralizes ALL path definitions and launcher detection for the Minecraft +# Splitscreen installer. All other modules MUST use these variables and +# functions - DO NOT hardcode paths anywhere else. +# +# This module manages two launcher configurations: +# - CREATION launcher: Used for CLI instance creation (PrismLauncher preferred) +# - ACTIVE launcher: Used for gameplay (PollyMC preferred if available) +# +# @dependencies +# - flatpak (optional, for Flatpak detection) +# - utilities.sh (for print_* functions, should_prefer_flatpak) +# +# @exports +# Constants: +# - PRISM_FLATPAK_ID : PrismLauncher Flatpak application ID +# - POLLYMC_FLATPAK_ID : PollyMC Flatpak application ID +# - PRISM_APPIMAGE_DATA_DIR : PrismLauncher AppImage data directory +# - POLLYMC_APPIMAGE_DATA_DIR : PollyMC AppImage data directory +# - PRISM_FLATPAK_DATA_DIR : PrismLauncher Flatpak data directory +# - POLLYMC_FLATPAK_DATA_DIR : PollyMC Flatpak data directory +# - PRISM_APPIMAGE_PATH : Path to PrismLauncher AppImage +# - POLLYMC_APPIMAGE_PATH : Path to PollyMC AppImage +# +# Variables (set by configure_launcher_paths): +# - PREFER_FLATPAK : Whether to prefer Flatpak over AppImage (true/false) +# - IMMUTABLE_OS_DETECTED : Whether running on immutable OS (true/false) +# - ACTIVE_LAUNCHER : Active launcher name ("prismlauncher"/"pollymc") +# - ACTIVE_LAUNCHER_TYPE : Active launcher type ("appimage"/"flatpak") +# - ACTIVE_DATA_DIR : Active launcher data directory +# - ACTIVE_INSTANCES_DIR : Active launcher instances directory +# - ACTIVE_EXECUTABLE : Command to run active launcher +# - ACTIVE_LAUNCHER_SCRIPT : Path to minecraftSplitscreen.sh +# - CREATION_LAUNCHER : Creation launcher name +# - CREATION_LAUNCHER_TYPE : Creation launcher type +# - CREATION_DATA_DIR : Creation launcher data directory +# - CREATION_INSTANCES_DIR : Creation launcher instances directory +# - CREATION_EXECUTABLE : Command to run creation launcher +# +# Functions: +# - is_flatpak_installed : Check if Flatpak app is installed +# - is_appimage_available : Check if AppImage exists +# - detect_prismlauncher : Detect PrismLauncher installation +# - detect_pollymc : Detect PollyMC installation +# - configure_launcher_paths : Main configuration function +# - set_creation_launcher_prismlauncher : Set PrismLauncher as creation launcher +# - set_active_launcher_pollymc : Set PollyMC as active launcher +# - revert_to_prismlauncher : Revert active launcher to PrismLauncher +# - finalize_launcher_paths : Finalize and verify configuration +# - get_creation_instances_dir : Get creation instances directory +# - get_active_instances_dir : Get active instances directory +# - get_launcher_script_path : Get launcher script path +# - get_active_executable : Get active launcher executable +# - get_active_data_dir : Get active data directory +# - needs_instance_migration : Check if migration needed +# - get_migration_source_dir : Get migration source directory +# - get_migration_dest_dir : Get migration destination directory +# - validate_path_configuration : Validate all paths are set +# - print_path_configuration : Debug print all paths +# +# @changelog +# 1.2.1 (2026-01-25) - Fix: Don't create directories in configure_launcher_paths() detection phase +# 1.2.0 (2026-01-25) - Centralized PREFER_FLATPAK decision; set once, used by all modules +# 1.1.1 (2026-01-25) - Prefer Flatpak over AppImage on immutable OS (Bazzite, SteamOS, etc.) +# 1.1.0 (2026-01-24) - Added revert_to_prismlauncher function +# 1.0.0 (2026-01-23) - Initial version with centralized path management +# ============================================================================= + +# ============================================================================= +# LAUNCHER IDENTIFIERS (Constants) +# ============================================================================= +readonly PRISM_FLATPAK_ID="org.prismlauncher.PrismLauncher" +readonly POLLYMC_FLATPAK_ID="org.fn2006.PollyMC" + +# ============================================================================= +# BASE PATH DEFINITIONS (Constants) +# ============================================================================= +# AppImage data directories (where AppImage launchers store their data) +readonly PRISM_APPIMAGE_DATA_DIR="$HOME/.local/share/PrismLauncher" +readonly POLLYMC_APPIMAGE_DATA_DIR="$HOME/.local/share/PollyMC" + +# Flatpak data directories (where Flatpak launchers store their data) +readonly PRISM_FLATPAK_DATA_DIR="$HOME/.var/app/${PRISM_FLATPAK_ID}/data/PrismLauncher" +readonly POLLYMC_FLATPAK_DATA_DIR="$HOME/.var/app/${POLLYMC_FLATPAK_ID}/data/PollyMC" + +# AppImage executable locations +readonly PRISM_APPIMAGE_PATH="$PRISM_APPIMAGE_DATA_DIR/PrismLauncher.AppImage" +readonly POLLYMC_APPIMAGE_PATH="$POLLYMC_APPIMAGE_DATA_DIR/PollyMC-Linux-x86_64.AppImage" + +# ============================================================================= +# SYSTEM DETECTION VARIABLES +# ============================================================================= +# These are set once by configure_launcher_paths() and used by all modules + +# Whether to prefer Flatpak installations over AppImage +# Set based on OS type detection (immutable OS = prefer Flatpak) +PREFER_FLATPAK=false + +# Whether an immutable OS was detected +IMMUTABLE_OS_DETECTED=false + +# ============================================================================= +# ACTIVE CONFIGURATION VARIABLES +# ============================================================================= +# These are set by configure_launcher_paths() based on what's detected + +# Primary launcher (the one used for gameplay) +ACTIVE_LAUNCHER="" # "prismlauncher" or "pollymc" +ACTIVE_LAUNCHER_TYPE="" # "appimage" or "flatpak" +ACTIVE_DATA_DIR="" # Where launcher stores its data +ACTIVE_INSTANCES_DIR="" # Where instances are stored +ACTIVE_EXECUTABLE="" # Command to run the launcher +ACTIVE_LAUNCHER_SCRIPT="" # Path to minecraftSplitscreen.sh + +# Creation launcher (used for initial instance creation, may differ from primary) +CREATION_LAUNCHER="" # "prismlauncher" or "pollymc" +CREATION_LAUNCHER_TYPE="" # "appimage" or "flatpak" +CREATION_DATA_DIR="" # Where to create instances +CREATION_INSTANCES_DIR="" # Instance creation directory +CREATION_EXECUTABLE="" # Command to run creation launcher + +# ============================================================================= +# DETECTION FUNCTIONS +# ============================================================================= + +# ----------------------------------------------------------------------------- +# @function is_flatpak_installed +# @description Checks if a Flatpak application is installed on the system. +# @param $1 - Flatpak application ID (e.g., "org.prismlauncher.PrismLauncher") +# @return 0 if installed, 1 if not installed or flatpak unavailable +# @example +# if is_flatpak_installed "org.prismlauncher.PrismLauncher"; then +# echo "PrismLauncher Flatpak is installed" +# fi +# ----------------------------------------------------------------------------- +is_flatpak_installed() { + local flatpak_id="$1" + command -v flatpak >/dev/null 2>&1 && flatpak list --app 2>/dev/null | grep -q "$flatpak_id" +} + +# ----------------------------------------------------------------------------- +# @function is_appimage_available +# @description Checks if an AppImage file exists and is executable. +# @param $1 - Full path to the AppImage file +# @return 0 if exists and executable, 1 otherwise +# @example +# if is_appimage_available "$HOME/.local/share/PrismLauncher/PrismLauncher.AppImage"; then +# echo "AppImage is ready to use" +# fi +# ----------------------------------------------------------------------------- +is_appimage_available() { + local appimage_path="$1" + [[ -f "$appimage_path" ]] && [[ -x "$appimage_path" ]] +} + +# ----------------------------------------------------------------------------- +# @function detect_prismlauncher +# @description Detects if PrismLauncher is installed (AppImage or Flatpak). +# Sets PRISM_TYPE, PRISM_DATA_DIR, and PRISM_EXECUTABLE variables. +# Uses PREFER_FLATPAK (set by configure_launcher_paths) to determine +# check order: Flatpak first on immutable OS, AppImage first otherwise. +# @param None +# @global PREFER_FLATPAK - (input) Whether to prefer Flatpak +# @global PRISM_DETECTED - (output) Set to true/false +# @global PRISM_TYPE - (output) "appimage" or "flatpak" +# @global PRISM_DATA_DIR - (output) Path to data directory +# @global PRISM_EXECUTABLE - (output) Command to run PrismLauncher +# @return 0 if detected, 1 if not found +# ----------------------------------------------------------------------------- +detect_prismlauncher() { + PRISM_DETECTED=false + PRISM_TYPE="" + PRISM_DATA_DIR="" + PRISM_EXECUTABLE="" + + # Check order depends on PREFER_FLATPAK (set during system detection) + if [[ "$PREFER_FLATPAK" == true ]]; then + # Immutable OS: Check Flatpak first, then AppImage + if is_flatpak_installed "$PRISM_FLATPAK_ID"; then + PRISM_TYPE="flatpak" + PRISM_DATA_DIR="$PRISM_FLATPAK_DATA_DIR" + PRISM_EXECUTABLE="flatpak run $PRISM_FLATPAK_ID" + print_info "Detected Flatpak PrismLauncher (preferred)" + return 0 + fi + + if is_appimage_available "$PRISM_APPIMAGE_PATH"; then + PRISM_TYPE="appimage" + PRISM_DATA_DIR="$PRISM_APPIMAGE_DATA_DIR" + PRISM_EXECUTABLE="$PRISM_APPIMAGE_PATH" + print_info "Detected AppImage PrismLauncher (fallback)" + return 0 + fi + else + # Traditional OS: Check AppImage first, then Flatpak + if is_appimage_available "$PRISM_APPIMAGE_PATH"; then + PRISM_TYPE="appimage" + PRISM_DATA_DIR="$PRISM_APPIMAGE_DATA_DIR" + PRISM_EXECUTABLE="$PRISM_APPIMAGE_PATH" + print_info "Detected AppImage PrismLauncher (preferred)" + return 0 + fi + + if is_flatpak_installed "$PRISM_FLATPAK_ID"; then + PRISM_TYPE="flatpak" + PRISM_DATA_DIR="$PRISM_FLATPAK_DATA_DIR" + PRISM_EXECUTABLE="flatpak run $PRISM_FLATPAK_ID" + print_info "Detected Flatpak PrismLauncher (fallback)" + return 0 + fi + fi + + return 1 +} + +# ----------------------------------------------------------------------------- +# @function detect_pollymc +# @description Detects if PollyMC is installed (AppImage or Flatpak). +# Sets POLLYMC_TYPE, POLLYMC_DATA_DIR, and POLLYMC_EXECUTABLE. +# Uses PREFER_FLATPAK (set by configure_launcher_paths) to determine +# check order: Flatpak first on immutable OS, AppImage first otherwise. +# @param None +# @global PREFER_FLATPAK - (input) Whether to prefer Flatpak +# @global POLLYMC_DETECTED - (output) Set to true/false +# @global POLLYMC_TYPE - (output) "appimage" or "flatpak" +# @global POLLYMC_DATA_DIR - (output) Path to data directory +# @global POLLYMC_EXECUTABLE - (output) Command to run PollyMC +# @return 0 if detected, 1 if not found +# ----------------------------------------------------------------------------- +detect_pollymc() { + POLLYMC_DETECTED=false + POLLYMC_TYPE="" + POLLYMC_DATA_DIR="" + POLLYMC_EXECUTABLE="" + + # Check order depends on PREFER_FLATPAK (set during system detection) + if [[ "$PREFER_FLATPAK" == true ]]; then + # Immutable OS: Check Flatpak first, then AppImage + if is_flatpak_installed "$POLLYMC_FLATPAK_ID"; then + POLLYMC_TYPE="flatpak" + POLLYMC_DATA_DIR="$POLLYMC_FLATPAK_DATA_DIR" + POLLYMC_EXECUTABLE="flatpak run $POLLYMC_FLATPAK_ID" + print_info "Detected Flatpak PollyMC (preferred)" + return 0 + fi + + if is_appimage_available "$POLLYMC_APPIMAGE_PATH"; then + POLLYMC_TYPE="appimage" + POLLYMC_DATA_DIR="$POLLYMC_APPIMAGE_DATA_DIR" + POLLYMC_EXECUTABLE="$POLLYMC_APPIMAGE_PATH" + print_info "Detected AppImage PollyMC (fallback)" + return 0 + fi + else + # Traditional OS: Check AppImage first, then Flatpak + if is_appimage_available "$POLLYMC_APPIMAGE_PATH"; then + POLLYMC_TYPE="appimage" + POLLYMC_DATA_DIR="$POLLYMC_APPIMAGE_DATA_DIR" + POLLYMC_EXECUTABLE="$POLLYMC_APPIMAGE_PATH" + print_info "Detected AppImage PollyMC (preferred)" + return 0 + fi + + if is_flatpak_installed "$POLLYMC_FLATPAK_ID"; then + POLLYMC_TYPE="flatpak" + POLLYMC_DATA_DIR="$POLLYMC_FLATPAK_DATA_DIR" + POLLYMC_EXECUTABLE="flatpak run $POLLYMC_FLATPAK_ID" + print_info "Detected Flatpak PollyMC (fallback)" + return 0 + fi + fi + + return 1 +} + +# ============================================================================= +# MAIN CONFIGURATION FUNCTION +# ============================================================================= + +# ----------------------------------------------------------------------------- +# @function configure_launcher_paths +# @description Main configuration function that detects installed launchers +# and sets up CREATION_* and ACTIVE_* variables. This MUST be +# called early in the installation process before any other +# module tries to access launcher paths. +# +# Priority: +# - Creation launcher: PrismLauncher (has CLI support) +# - Active launcher: PollyMC if available, else PrismLauncher +# +# @param None +# @global All CREATION_* and ACTIVE_* variables are set +# @return 0 always +# ----------------------------------------------------------------------------- +configure_launcher_paths() { + print_header "DETECTING LAUNCHER CONFIGURATION" + + # ========================================================================= + # SYSTEM TYPE DETECTION (MUST BE FIRST) + # ========================================================================= + # Detect if we're on an immutable OS and set PREFER_FLATPAK accordingly. + # This decision is made ONCE here and used by all subsequent modules. + + if is_immutable_os; then + IMMUTABLE_OS_DETECTED=true + PREFER_FLATPAK=true + print_info "Detected immutable OS: ${IMMUTABLE_OS_NAME:-unknown}" + print_info "Flatpak installations will be preferred over AppImage" + else + IMMUTABLE_OS_DETECTED=false + PREFER_FLATPAK=false + print_info "Traditional Linux system detected" + print_info "AppImage installations will be preferred" + fi + + # ========================================================================= + # LAUNCHER DETECTION + # ========================================================================= + + # Determine creation launcher (PrismLauncher preferred for CLI instance creation) + if detect_prismlauncher; then + CREATION_LAUNCHER="prismlauncher" + CREATION_LAUNCHER_TYPE="$PRISM_TYPE" + CREATION_DATA_DIR="$PRISM_DATA_DIR" + CREATION_INSTANCES_DIR="$PRISM_DATA_DIR/instances" + CREATION_EXECUTABLE="$PRISM_EXECUTABLE" + print_success "Creation launcher: PrismLauncher ($PRISM_TYPE)" + print_info " Data directory: $CREATION_DATA_DIR" + print_info " Instances: $CREATION_INSTANCES_DIR" + else + # No PrismLauncher - will need to download or use PollyMC + CREATION_LAUNCHER="" + print_warning "No PrismLauncher detected - will attempt download" + fi + + # Determine active/gameplay launcher (PollyMC preferred if available) + if detect_pollymc; then + ACTIVE_LAUNCHER="pollymc" + ACTIVE_LAUNCHER_TYPE="$POLLYMC_TYPE" + ACTIVE_DATA_DIR="$POLLYMC_DATA_DIR" + ACTIVE_INSTANCES_DIR="$POLLYMC_DATA_DIR/instances" + ACTIVE_EXECUTABLE="$POLLYMC_EXECUTABLE" + ACTIVE_LAUNCHER_SCRIPT="$POLLYMC_DATA_DIR/minecraftSplitscreen.sh" + print_success "Active launcher: PollyMC ($POLLYMC_TYPE)" + print_info " Data directory: $ACTIVE_DATA_DIR" + print_info " Launcher script: $ACTIVE_LAUNCHER_SCRIPT" + elif detect_prismlauncher; then + ACTIVE_LAUNCHER="prismlauncher" + ACTIVE_LAUNCHER_TYPE="$PRISM_TYPE" + ACTIVE_DATA_DIR="$PRISM_DATA_DIR" + ACTIVE_INSTANCES_DIR="$PRISM_DATA_DIR/instances" + ACTIVE_EXECUTABLE="$PRISM_EXECUTABLE" + ACTIVE_LAUNCHER_SCRIPT="$PRISM_DATA_DIR/minecraftSplitscreen.sh" + print_success "Active launcher: PrismLauncher ($PRISM_TYPE)" + print_info " Data directory: $ACTIVE_DATA_DIR" + print_info " Launcher script: $ACTIVE_LAUNCHER_SCRIPT" + else + print_warning "No launcher detected - will configure after download" + fi + + # NOTE: Directories are NOT created here during detection phase. + # They are created later by launcher_setup.sh and pollymc_setup.sh + # only after successful installation/download to avoid empty directories. +} + +# ============================================================================= +# POST-DOWNLOAD CONFIGURATION FUNCTIONS +# ============================================================================= + +# ----------------------------------------------------------------------------- +# @function set_creation_launcher_prismlauncher +# @description Updates the creation launcher configuration after PrismLauncher +# is downloaded or installed. Also sets ACTIVE_* variables if no +# active launcher is configured yet. +# @param $1 - type: "appimage" or "flatpak" +# @param $2 - executable: Path or command to run PrismLauncher +# @global CREATION_* variables are updated +# @global ACTIVE_* variables may be updated if not set +# @return 0 always +# ----------------------------------------------------------------------------- +set_creation_launcher_prismlauncher() { + local type="$1" + local executable="$2" + + CREATION_LAUNCHER="prismlauncher" + CREATION_LAUNCHER_TYPE="$type" + + if [[ "$type" == "appimage" ]]; then + CREATION_DATA_DIR="$PRISM_APPIMAGE_DATA_DIR" + else + CREATION_DATA_DIR="$PRISM_FLATPAK_DATA_DIR" + fi + + CREATION_INSTANCES_DIR="$CREATION_DATA_DIR/instances" + CREATION_EXECUTABLE="$executable" + + mkdir -p "$CREATION_INSTANCES_DIR" + + # If no active launcher set yet, use PrismLauncher + if [[ -z "$ACTIVE_LAUNCHER" ]]; then + ACTIVE_LAUNCHER="prismlauncher" + ACTIVE_LAUNCHER_TYPE="$type" + ACTIVE_DATA_DIR="$CREATION_DATA_DIR" + ACTIVE_INSTANCES_DIR="$CREATION_INSTANCES_DIR" + ACTIVE_EXECUTABLE="$executable" + ACTIVE_LAUNCHER_SCRIPT="$ACTIVE_DATA_DIR/minecraftSplitscreen.sh" + fi +} + +# ----------------------------------------------------------------------------- +# @function set_active_launcher_pollymc +# @description Updates the active launcher configuration to use PollyMC +# after it has been downloaded or detected. +# @param $1 - type: "appimage" or "flatpak" +# @param $2 - executable: Path or command to run PollyMC +# @global ACTIVE_* variables are updated +# @return 0 always +# ----------------------------------------------------------------------------- +set_active_launcher_pollymc() { + local type="$1" + local executable="$2" + + ACTIVE_LAUNCHER="pollymc" + ACTIVE_LAUNCHER_TYPE="$type" + + if [[ "$type" == "appimage" ]]; then + ACTIVE_DATA_DIR="$POLLYMC_APPIMAGE_DATA_DIR" + else + ACTIVE_DATA_DIR="$POLLYMC_FLATPAK_DATA_DIR" + fi + + ACTIVE_INSTANCES_DIR="$ACTIVE_DATA_DIR/instances" + ACTIVE_EXECUTABLE="$executable" + ACTIVE_LAUNCHER_SCRIPT="$ACTIVE_DATA_DIR/minecraftSplitscreen.sh" + + mkdir -p "$ACTIVE_INSTANCES_DIR" +} + +# ----------------------------------------------------------------------------- +# @function revert_to_prismlauncher +# @description Reverts the active launcher back to PrismLauncher. Called when +# PollyMC setup fails and we need to fall back to PrismLauncher +# for gameplay. +# @param None +# @global ACTIVE_* variables are reset to match CREATION_* values +# @return 0 always +# ----------------------------------------------------------------------------- +revert_to_prismlauncher() { + print_info "Reverting to PrismLauncher as active launcher..." + + ACTIVE_LAUNCHER="prismlauncher" + ACTIVE_LAUNCHER_TYPE="$CREATION_LAUNCHER_TYPE" + ACTIVE_DATA_DIR="$CREATION_DATA_DIR" + ACTIVE_INSTANCES_DIR="$CREATION_INSTANCES_DIR" + ACTIVE_EXECUTABLE="$CREATION_EXECUTABLE" + ACTIVE_LAUNCHER_SCRIPT="$ACTIVE_DATA_DIR/minecraftSplitscreen.sh" + + print_success "Active launcher reverted to PrismLauncher ($ACTIVE_LAUNCHER_TYPE)" + print_info " Data directory: $ACTIVE_DATA_DIR" + print_info " Instances: $ACTIVE_INSTANCES_DIR" +} + +# ----------------------------------------------------------------------------- +# @function finalize_launcher_paths +# @description Finalizes path configuration after all downloads and setup +# are complete. Verifies that instances exist in the expected +# location and falls back to PrismLauncher if PollyMC migration +# failed. +# @param None +# @global ACTIVE_* variables may be updated if verification fails +# @return 0 always +# ----------------------------------------------------------------------------- +finalize_launcher_paths() { + print_info "Finalizing launcher configuration..." + + # If we're using PollyMC as active but instances were created in PrismLauncher, + # they should have been migrated. Verify. + if [[ "$ACTIVE_LAUNCHER" == "pollymc" ]] && [[ "$CREATION_LAUNCHER" == "prismlauncher" ]]; then + if [[ -d "$ACTIVE_INSTANCES_DIR/latestUpdate-1" ]]; then + print_success "Instances verified in PollyMC directory" + else + print_warning "Instances not found in PollyMC, falling back to PrismLauncher" + ACTIVE_LAUNCHER="prismlauncher" + ACTIVE_LAUNCHER_TYPE="$CREATION_LAUNCHER_TYPE" + ACTIVE_DATA_DIR="$CREATION_DATA_DIR" + ACTIVE_INSTANCES_DIR="$CREATION_INSTANCES_DIR" + ACTIVE_EXECUTABLE="$CREATION_EXECUTABLE" + ACTIVE_LAUNCHER_SCRIPT="$ACTIVE_DATA_DIR/minecraftSplitscreen.sh" + fi + fi + + print_success "Final configuration:" + print_info " Launcher: $ACTIVE_LAUNCHER ($ACTIVE_LAUNCHER_TYPE)" + print_info " Data: $ACTIVE_DATA_DIR" + print_info " Instances: $ACTIVE_INSTANCES_DIR" + print_info " Script: $ACTIVE_LAUNCHER_SCRIPT" +} + +# ============================================================================= +# PATH ACCESSOR FUNCTIONS +# ============================================================================= + +# ----------------------------------------------------------------------------- +# @function get_creation_instances_dir +# @description Returns the directory where instances should be created. +# @param None +# @stdout Path to creation instances directory +# @return 0 always +# ----------------------------------------------------------------------------- +get_creation_instances_dir() { + echo "$CREATION_INSTANCES_DIR" +} + +# ----------------------------------------------------------------------------- +# @function get_active_instances_dir +# @description Returns the directory where instances are stored for gameplay. +# @param None +# @stdout Path to active instances directory +# @return 0 always +# ----------------------------------------------------------------------------- +get_active_instances_dir() { + echo "$ACTIVE_INSTANCES_DIR" +} + +# ----------------------------------------------------------------------------- +# @function get_launcher_script_path +# @description Returns the path where minecraftSplitscreen.sh should be created. +# @param None +# @stdout Path to launcher script +# @return 0 always +# ----------------------------------------------------------------------------- +get_launcher_script_path() { + echo "$ACTIVE_LAUNCHER_SCRIPT" +} + +# ----------------------------------------------------------------------------- +# @function get_active_executable +# @description Returns the command to run the active launcher. +# @param None +# @stdout Executable path or command +# @return 0 always +# ----------------------------------------------------------------------------- +get_active_executable() { + echo "$ACTIVE_EXECUTABLE" +} + +# ----------------------------------------------------------------------------- +# @function get_active_data_dir +# @description Returns the active launcher's data directory. +# @param None +# @stdout Path to active data directory +# @return 0 always +# ----------------------------------------------------------------------------- +get_active_data_dir() { + echo "$ACTIVE_DATA_DIR" +} + +# ----------------------------------------------------------------------------- +# @function needs_instance_migration +# @description Checks if instances need to be migrated from creation launcher +# to active launcher (i.e., they are different launchers). +# @param None +# @return 0 if migration needed, 1 if not needed +# ----------------------------------------------------------------------------- +needs_instance_migration() { + [[ "$CREATION_LAUNCHER" != "$ACTIVE_LAUNCHER" ]] || [[ "$CREATION_DATA_DIR" != "$ACTIVE_DATA_DIR" ]] +} + +# ----------------------------------------------------------------------------- +# @function get_migration_source_dir +# @description Returns the source directory for instance migration. +# @param None +# @stdout Path to migration source (creation instances directory) +# @return 0 always +# ----------------------------------------------------------------------------- +get_migration_source_dir() { + echo "$CREATION_INSTANCES_DIR" +} + +# ----------------------------------------------------------------------------- +# @function get_migration_dest_dir +# @description Returns the destination directory for instance migration. +# @param None +# @stdout Path to migration destination (active instances directory) +# @return 0 always +# ----------------------------------------------------------------------------- +get_migration_dest_dir() { + echo "$ACTIVE_INSTANCES_DIR" +} + +# ============================================================================= +# VALIDATION FUNCTIONS +# ============================================================================= + +# ----------------------------------------------------------------------------- +# @function validate_path_configuration +# @description Validates that all required path variables are set. Used to +# verify configuration is complete before proceeding. +# @param None +# @stderr Error messages for missing variables +# @return Number of errors (0 if all valid) +# ----------------------------------------------------------------------------- +validate_path_configuration() { + local errors=0 + + if [[ -z "$ACTIVE_DATA_DIR" ]]; then + print_error "ACTIVE_DATA_DIR not set" + ((errors++)) + elif [[ ! -d "$ACTIVE_DATA_DIR" ]]; then + print_warning "ACTIVE_DATA_DIR does not exist: $ACTIVE_DATA_DIR" + fi + + if [[ -z "$ACTIVE_INSTANCES_DIR" ]]; then + print_error "ACTIVE_INSTANCES_DIR not set" + ((errors++)) + fi + + if [[ -z "$ACTIVE_LAUNCHER_SCRIPT" ]]; then + print_error "ACTIVE_LAUNCHER_SCRIPT not set" + ((errors++)) + fi + + if [[ -z "$ACTIVE_EXECUTABLE" ]]; then + print_error "ACTIVE_EXECUTABLE not set" + ((errors++)) + fi + + return $errors +} + +# ----------------------------------------------------------------------------- +# @function print_path_configuration +# @description Prints all path configuration variables for debugging purposes. +# @param None +# @stdout Formatted configuration dump +# @return 0 always +# ----------------------------------------------------------------------------- +print_path_configuration() { + echo "=== PATH CONFIGURATION ===" + echo "Creation Launcher: $CREATION_LAUNCHER ($CREATION_LAUNCHER_TYPE)" + echo "Creation Data Dir: $CREATION_DATA_DIR" + echo "Creation Instances: $CREATION_INSTANCES_DIR" + echo "Creation Executable: $CREATION_EXECUTABLE" + echo "" + echo "Active Launcher: $ACTIVE_LAUNCHER ($ACTIVE_LAUNCHER_TYPE)" + echo "Active Data Dir: $ACTIVE_DATA_DIR" + echo "Active Instances: $ACTIVE_INSTANCES_DIR" + echo "Active Executable: $ACTIVE_EXECUTABLE" + echo "Launcher Script: $ACTIVE_LAUNCHER_SCRIPT" + echo "==========================" +} diff --git a/modules/pollymc_setup.sh b/modules/pollymc_setup.sh index 9d498f4..d91701e 100644 --- a/modules/pollymc_setup.sh +++ b/modules/pollymc_setup.sh @@ -1,183 +1,327 @@ #!/bin/bash # ============================================================================= -# Minecraft Splitscreen Steam Deck Installer - PollyMC Setup Module +# POLLYMC SETUP MODULE # ============================================================================= -# -# This module handles the setup and optimization of PollyMC as the primary -# launcher for splitscreen gameplay, providing better offline support and -# handling of multiple simultaneous instances compared to PrismLauncher. +# @file pollymc_setup.sh +# @version 1.3.2 +# @date 2026-01-25 +# @author aradanmn +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck # -# Functions provided: -# - setup_pollymc: Configure PollyMC as the primary splitscreen launcher -# - setup_pollymc_launcher: Configure splitscreen launcher script for PollyMC -# - cleanup_prism_launcher: Clean up PrismLauncher files after PollyMC setup +# @description +# Handles the setup and optimization of PollyMC as the primary launcher for +# splitscreen gameplay. PollyMC provides better offline support and handling +# of multiple simultaneous instances compared to PrismLauncher. # +# Note: As of 2026-01, the PollyMC GitHub repository (fn2006/PollyMC) is no +# longer available. This module will attempt to download but gracefully falls +# back to PrismLauncher when the download fails. +# +# PollyMC advantages for splitscreen: +# - No forced Microsoft login requirements (offline-friendly) +# - Better handling of multiple simultaneous instances +# - Cleaner interface without authentication popups +# - More stable for automated controller-based launching +# +# @dependencies +# - wget (for downloading AppImage) +# - rsync (for instance migration) +# - jq (optional, for account merging) +# - file (for download validation) +# - utilities.sh (for print_* functions, merge_accounts_json) +# - path_configuration.sh (for path constants, setters, and PREFER_FLATPAK) +# +# @exports +# Functions: +# - setup_pollymc : Main setup function +# - setup_pollymc_launcher : Prepare for launcher script generation (deprecated) +# - cleanup_prism_launcher : Remove PrismLauncher after successful setup +# +# @changelog +# 1.3.2 (2026-01-25) - Fix: Try system-level Flatpak install first, then user-level (for Bazzite/SteamOS) +# 1.3.1 (2026-01-25) - Fix: Only create directories after successful download/install +# 1.3.0 (2026-01-25) - Added Flatpak installation for immutable OS using PREFER_FLATPAK +# 1.2.0 (2026-01-24) - Added proper fallback handling, empty dir cleanup +# 1.1.0 (2026-01-23) - Added instance migration with options.txt preservation +# 1.0.0 (2026-01-22) - Initial version # ============================================================================= -# setup_pollymc: Configure PollyMC as the primary launcher for splitscreen gameplay -# -# POLLYMC ADVANTAGES FOR SPLITSCREEN: -# - No forced Microsoft login requirements (offline-friendly) -# - Better handling of multiple simultaneous instances -# - Cleaner interface without authentication popups -# - More stable for automated controller-based launching -# -# PROCESS OVERVIEW: -# 1. Download PollyMC AppImage from GitHub releases -# 2. Migrate all instances from PrismLauncher to PollyMC -# 3. Copy offline accounts configuration -# 4. Test PollyMC compatibility and functionality -# 5. Set up splitscreen launcher script for PollyMC -# 6. Clean up PrismLauncher files to save space +# ----------------------------------------------------------------------------- +# @function setup_pollymc +# @description Main function to configure PollyMC as the primary launcher for +# splitscreen gameplay. Handles detection, download, instance +# migration, account configuration, and verification. +# +# Process: +# 1. Detect or download PollyMC (Flatpak/AppImage) +# 2. Migrate instances from PrismLauncher to PollyMC +# 3. Merge offline accounts configuration +# 4. Configure PollyMC to skip setup wizard +# 5. Verify PollyMC compatibility +# 6. Clean up PrismLauncher if successful +# +# Falls back to PrismLauncher if any step fails. # -# FALLBACK STRATEGY: -# If PollyMC fails at any step, we fall back to PrismLauncher -# This ensures the installation completes successfully regardless +# @param None +# @global PREFER_FLATPAK - (input) Whether to prefer Flatpak (from path_configuration) +# @global POLLYMC_FLATPAK_ID - (input) PollyMC Flatpak ID +# @global POLLYMC_APPIMAGE_PATH - (input) Expected AppImage location +# @global CREATION_INSTANCES_DIR - (input) Source instances directory +# @global CREATION_DATA_DIR - (input) PrismLauncher data directory +# @global ACTIVE_* - (output) Updated via set_active_launcher_pollymc +# @global JAVA_PATH - (input) Java executable path for config +# @return 0 always (failures handled internally with fallback) +# ----------------------------------------------------------------------------- setup_pollymc() { - print_header "🎮 SETTING UP POLLYMC" - - print_progress "Downloading PollyMC for optimized splitscreen gameplay..." - - # ============================================================================= - # POLLYMC DIRECTORY INITIALIZATION - # ============================================================================= - - # Create PollyMC data directory structure - # PollyMC stores instances, accounts, configuration, and launcher script here - # Structure: ~/.local/share/PollyMC/{instances/, accounts.json, PollyMC AppImage} - mkdir -p "$HOME/.local/share/PollyMC" - - # ============================================================================= - # POLLYMC APPIMAGE DOWNLOAD AND VERIFICATION - # ============================================================================= - - # Download PollyMC AppImage from official GitHub releases - # AppImage format provides universal Linux compatibility without dependencies - # PollyMC GitHub releases API endpoint for latest version - # We download the x86_64 Linux AppImage which works on most modern Linux systems - local pollymc_url="https://github.com/fn2006/PollyMC/releases/latest/download/PollyMC-Linux-x86_64.AppImage" - print_progress "Fetching PollyMC from GitHub releases: $(basename "$pollymc_url")..." - - # DOWNLOAD WITH FALLBACK HANDLING - # If PollyMC download fails, we continue with PrismLauncher as the primary launcher - # This ensures installation doesn't fail completely due to network issues or GitHub downtime - if ! wget -O "$HOME/.local/share/PollyMC/PollyMC-Linux-x86_64.AppImage" "$pollymc_url"; then - print_warning "❌ PollyMC download failed - continuing with PrismLauncher as primary launcher" - print_info " This is not a critical error - PrismLauncher works fine for splitscreen" - USE_POLLYMC=false # Global flag tracks which launcher is active - return 0 - else - # APPIMAGE PERMISSIONS: Make the downloaded AppImage executable - # AppImages require execute permissions to run properly - chmod +x "$HOME/.local/share/PollyMC/PollyMC-Linux-x86_64.AppImage" - print_success "✅ PollyMC AppImage downloaded and configured successfully" - USE_POLLYMC=true # Mark PollyMC as available for further setup + print_header "SETTING UP POLLYMC" + + print_progress "Detecting PollyMC installation method..." + + local pollymc_type="" + local pollymc_data_dir="" + local pollymc_executable="" + + # Priority 1: Check for existing Flatpak installation + if is_flatpak_installed "$POLLYMC_FLATPAK_ID" 2>/dev/null; then + print_success "Found existing PollyMC Flatpak installation" + pollymc_type="flatpak" + pollymc_data_dir="$POLLYMC_FLATPAK_DATA_DIR" + pollymc_executable="flatpak run $POLLYMC_FLATPAK_ID" + + mkdir -p "$pollymc_data_dir/instances" + print_info " -> Using Flatpak data directory: $pollymc_data_dir" + + # Priority 2: Check for existing AppImage + elif [[ -x "$POLLYMC_APPIMAGE_PATH" ]]; then + print_success "Found existing PollyMC AppImage" + pollymc_type="appimage" + pollymc_data_dir="$POLLYMC_APPIMAGE_DATA_DIR" + pollymc_executable="$POLLYMC_APPIMAGE_PATH" + print_info " -> Using existing AppImage: $POLLYMC_APPIMAGE_PATH" + + # Priority 3 (immutable OS only): Install Flatpak if preferred + # PREFER_FLATPAK is set by configure_launcher_paths() in path_configuration.sh + elif [[ "$PREFER_FLATPAK" == true ]]; then + print_info "Immutable OS detected - preferring Flatpak installation for PollyMC" + + if command -v flatpak &>/dev/null; then + print_progress "Installing PollyMC via Flatpak..." + + local flatpak_installed=false + + # Try system-level install first (works on Bazzite/SteamOS where Flathub is system-only) + if flatpak install -y flathub "$POLLYMC_FLATPAK_ID" 2>/dev/null; then + flatpak_installed=true + print_success "PollyMC Flatpak installed (system)" + else + # Fall back to user-level install + # First ensure Flathub repo is available for user + if ! flatpak remote-list --user 2>/dev/null | grep -q flathub; then + print_progress "Adding Flathub repository for user..." + flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo 2>/dev/null || true + fi + + if flatpak install --user -y flathub "$POLLYMC_FLATPAK_ID" 2>/dev/null; then + flatpak_installed=true + print_success "PollyMC Flatpak installed (user)" + fi + fi + + if [[ "$flatpak_installed" == true ]]; then + pollymc_type="flatpak" + pollymc_data_dir="$POLLYMC_FLATPAK_DATA_DIR" + pollymc_executable="flatpak run $POLLYMC_FLATPAK_ID" + + mkdir -p "$pollymc_data_dir/instances" + print_info " -> Using Flatpak data directory: $pollymc_data_dir" + else + print_warning "PollyMC Flatpak installation failed - falling back to AppImage download" + # Fall through to AppImage download below + fi + else + print_warning "Flatpak not available - falling back to AppImage download" + fi + fi + + # Priority 4: Download AppImage (fallback for traditional OS or if Flatpak install failed) + if [[ -z "$pollymc_type" ]]; then + print_progress "No existing PollyMC found - downloading AppImage..." + + local pollymc_url="https://github.com/fn2006/PollyMC/releases/latest/download/PollyMC-Linux-x86_64.AppImage" + print_progress "Fetching PollyMC from GitHub releases: $(basename "$pollymc_url")..." + + # Download to temp location first, only create directory on success + local temp_appimage + temp_appimage=$(mktemp) + + # Download with fallback handling + if ! wget -q -O "$temp_appimage" "$pollymc_url"; then + print_warning "PollyMC download failed - continuing with PrismLauncher as primary launcher" + print_info " This is not a critical error - PrismLauncher works fine for splitscreen" + rm -f "$temp_appimage" 2>/dev/null + return 0 + fi + + # Verify downloaded file is valid + if [[ ! -s "$temp_appimage" ]] || file "$temp_appimage" | grep -q "HTML\|text"; then + print_warning "PollyMC download produced invalid file - continuing with PrismLauncher" + rm -f "$temp_appimage" 2>/dev/null + return 0 + fi + + # Download successful - now create directory and move file + pollymc_type="appimage" + pollymc_data_dir="$POLLYMC_APPIMAGE_DATA_DIR" + mkdir -p "$pollymc_data_dir" + mv "$temp_appimage" "$POLLYMC_APPIMAGE_PATH" + chmod +x "$POLLYMC_APPIMAGE_PATH" + pollymc_executable="$POLLYMC_APPIMAGE_PATH" + print_success "PollyMC AppImage downloaded and configured successfully" fi - # ============================================================================= - # INSTANCE MIGRATION: Transfer all Minecraft instances from PrismLauncher - # ============================================================================= - - # INSTANCE DIRECTORY MIGRATION - # Copy the complete instances directory structure from PrismLauncher to PollyMC - # This includes all 4 splitscreen instances with their configurations, mods, and saves + # Update centralized path configuration + set_active_launcher_pollymc "$pollymc_type" "$pollymc_executable" + + print_info " -> PollyMC installation type: $pollymc_type" + print_info " -> Active data directory: $ACTIVE_DATA_DIR" + + # ========================================================================= + # Instance Migration + # ========================================================================= + _migrate_instances_to_pollymc + + # ========================================================================= + # Account Configuration Migration + # ========================================================================= + _migrate_accounts_to_pollymc + + # ========================================================================= + # PollyMC Configuration + # ========================================================================= + _configure_pollymc_settings + + # ========================================================================= + # Compatibility Verification + # ========================================================================= + _verify_pollymc_and_finalize +} + +# ----------------------------------------------------------------------------- +# @function _migrate_instances_to_pollymc +# @description Internal function to migrate Minecraft instances from +# PrismLauncher to PollyMC, preserving options.txt settings. +# @param None +# @global CREATION_INSTANCES_DIR - (input) Source directory +# @global ACTIVE_INSTANCES_DIR - (input) Destination directory +# @global ACTIVE_DATA_DIR - (input) For backup directory +# @return 0 always +# ----------------------------------------------------------------------------- +_migrate_instances_to_pollymc() { print_progress "Migrating PrismLauncher instances to PollyMC data directory..." - - # INSTANCES TRANSFER: Copy entire instances folder with all splitscreen configurations - # Each instance (latestUpdate-1 through latestUpdate-4) contains: - # - Minecraft version configuration - # - Fabric mod loader setup - # - All downloaded mods and their dependencies - # - Splitscreen-specific mod configurations - # - Instance-specific settings (memory, Java args, etc.) - if [[ -d "$TARGET_DIR/instances" ]]; then - # Create instances directory if it doesn't exist - mkdir -p "$HOME/.local/share/PollyMC/instances" - - # For updates: preserve options.txt and replace instances - if [[ -d "$HOME/.local/share/PollyMC/instances" ]]; then - for i in {1..4}; do - local instance_name="latestUpdate-$i" - local instance_path="$HOME/.local/share/PollyMC/instances/$instance_name" - local options_file="$instance_path/.minecraft/options.txt" - - if [[ -d "$instance_path" ]]; then - print_info " → Updating $instance_name while preserving settings" - - # Backup options.txt if it exists - if [[ -f "$options_file" ]]; then - print_info " → Preserving existing options.txt for $instance_name" - # Create a temporary directory for backups - local backup_dir="$HOME/.local/share/PollyMC/options_backup" - mkdir -p "$backup_dir" - # Copy with path structure to keep track of which instance it belongs to - cp "$options_file" "$backup_dir/${instance_name}_options.txt" - fi - - # Remove old instance but keep options backup - rm -rf "$instance_path" + + local source_instances="$CREATION_INSTANCES_DIR" + local dest_instances="$ACTIVE_INSTANCES_DIR" + + if [[ -d "$source_instances" ]] && [[ "$source_instances" != "$dest_instances" ]]; then + mkdir -p "$dest_instances" + + # Preserve options.txt during update + for i in {1..4}; do + local instance_name="latestUpdate-$i" + local instance_path="$dest_instances/$instance_name" + local options_file="$instance_path/.minecraft/options.txt" + + if [[ -d "$instance_path" ]]; then + print_info " -> Updating $instance_name while preserving settings" + + if [[ -f "$options_file" ]]; then + print_info " -> Preserving existing options.txt for $instance_name" + local backup_dir="$ACTIVE_DATA_DIR/options_backup" + mkdir -p "$backup_dir" + cp "$options_file" "$backup_dir/${instance_name}_options.txt" fi - done - fi - - # Copy the updated instances while excluding options.txt files - rsync -a --exclude='*.minecraft/options.txt' "$TARGET_DIR/instances/"* "$HOME/.local/share/PollyMC/instances/" - - # Restore options.txt files from temporary backup location - local backup_dir="$HOME/.local/share/PollyMC/options_backup" + + rm -rf "$instance_path" + fi + done + + # Copy instances excluding options.txt + rsync -a --exclude='*.minecraft/options.txt' "$source_instances/"* "$dest_instances/" + + # Restore options.txt files + local backup_dir="$ACTIVE_DATA_DIR/options_backup" for i in {1..4}; do local instance_name="latestUpdate-$i" - local instance_path="$HOME/.local/share/PollyMC/instances/$instance_name" + local instance_path="$dest_instances/$instance_name" local options_file="$instance_path/.minecraft/options.txt" local backup_file="$backup_dir/${instance_name}_options.txt" - + if [[ -f "$backup_file" ]]; then - print_info " → Restoring saved options.txt for $instance_name" + print_info " -> Restoring saved options.txt for $instance_name" mkdir -p "$(dirname "$options_file")" cp "$backup_file" "$options_file" fi done - - print_success "✅ Splitscreen instances migrated to PollyMC" - - # Clean up the temporary backup directory - if [[ -d "$backup_dir" ]]; then - rm -rf "$backup_dir" - fi - - # INSTANCE COUNT VERIFICATION: Ensure all 4 instances were copied successfully + + print_success "Splitscreen instances migrated to PollyMC" + + [[ -d "$backup_dir" ]] && rm -rf "$backup_dir" + local instance_count - instance_count=$(find "$HOME/.local/share/PollyMC/instances" -maxdepth 1 -name "latestUpdate-*" -type d 2>/dev/null | wc -l) - print_info " → $instance_count splitscreen instances available in PollyMC" + instance_count=$(find "$dest_instances" -maxdepth 1 -name "latestUpdate-*" -type d 2>/dev/null | wc -l) + print_info " -> $instance_count splitscreen instances available in PollyMC" + elif [[ "$source_instances" == "$dest_instances" ]]; then + print_info " -> Instances already in correct location, no migration needed" else - print_warning "⚠️ No instances directory found in PrismLauncher - this shouldn't happen" + print_warning "No instances directory found to migrate" fi - - # ============================================================================= - # ACCOUNT CONFIGURATION MIGRATION - # ============================================================================= - - # OFFLINE ACCOUNTS TRANSFER: Copy splitscreen player account configurations - # The accounts.json file contains offline player profiles for Player 1-4 - # These accounts allow splitscreen gameplay without requiring multiple Microsoft accounts - if [[ -f "$TARGET_DIR/accounts.json" ]]; then - cp "$TARGET_DIR/accounts.json" "$HOME/.local/share/PollyMC/" - print_success "✅ Offline splitscreen accounts copied to PollyMC" - print_info " → Player accounts P1, P2, P3, P4 configured for offline gameplay" +} + +# ----------------------------------------------------------------------------- +# @function _migrate_accounts_to_pollymc +# @description Internal function to merge splitscreen accounts (P1-P4) into +# PollyMC's accounts.json while preserving existing accounts. +# @param None +# @global CREATION_DATA_DIR - (input) Source accounts location +# @global ACTIVE_DATA_DIR - (input) Destination accounts location +# @return 0 always +# ----------------------------------------------------------------------------- +_migrate_accounts_to_pollymc() { + local source_accounts="$CREATION_DATA_DIR/accounts.json" + local dest_accounts="$ACTIVE_DATA_DIR/accounts.json" + + if [[ -f "$source_accounts" ]] && [[ "$source_accounts" != "$dest_accounts" ]]; then + if merge_accounts_json "$source_accounts" "$dest_accounts"; then + print_success "Offline splitscreen accounts merged into PollyMC" + print_info " -> Player accounts P1, P2, P3, P4 configured for offline gameplay" + if command -v jq >/dev/null 2>&1; then + local existing_count + existing_count=$(jq '.accounts | map(select(.profile.name | test("^P[1-4]$") | not)) | length' "$dest_accounts" 2>/dev/null || echo "0") + if [[ "$existing_count" -gt 0 ]]; then + print_info " -> Preserved $existing_count existing account(s)" + fi + fi + fi + elif [[ -f "$dest_accounts" ]]; then + print_info " -> Accounts already configured in PollyMC" else - print_warning "⚠️ accounts.json not found - splitscreen accounts may need manual setup" + print_warning "accounts.json not found - splitscreen accounts may need manual setup" fi +} - # ============================================================================= - # POLLYMC CONFIGURATION: Skip Setup Wizard - # ============================================================================= - - # SETUP WIZARD BYPASS: Create PollyMC configuration using user's proven working settings - # This uses the exact configuration from the user's working PollyMC installation - # Guarantees compatibility and skips all setup wizard prompts +# ----------------------------------------------------------------------------- +# @function _configure_pollymc_settings +# @description Internal function to create PollyMC configuration file that +# skips the setup wizard and sets Java/memory defaults. +# @param None +# @global ACTIVE_DATA_DIR - (input) Where to write pollymc.cfg +# @global JAVA_PATH - (input) Java executable path +# @return 0 always +# ----------------------------------------------------------------------------- +_configure_pollymc_settings() { print_progress "Configuring PollyMC with proven working settings..." - - # Get the current hostname for dynamic configuration with multiple fallback methods + local current_hostname if command -v hostname >/dev/null 2>&1; then current_hostname=$(hostname) @@ -188,8 +332,8 @@ setup_pollymc() { else current_hostname="localhost" fi - - cat > "$HOME/.local/share/PollyMC/pollymc.cfg" < "$ACTIVE_DATA_DIR/pollymc.cfg" < Setup wizard will not appear on first launch" + print_info " -> Java path and memory settings pre-configured" +} + +# ----------------------------------------------------------------------------- +# @function _verify_pollymc_and_finalize +# @description Internal function to verify PollyMC works and finalize setup. +# Reverts to PrismLauncher if verification fails. +# @param None +# @global ACTIVE_LAUNCHER_TYPE - (input) "appimage" or "flatpak" +# @global POLLYMC_FLATPAK_ID - (input) Flatpak ID for testing +# @global POLLYMC_APPIMAGE_PATH - (input) AppImage path for testing +# @global ACTIVE_INSTANCES_DIR - (input) For instance verification +# @global CREATION_DATA_DIR - (input) For cleanup comparison +# @global ACTIVE_DATA_DIR - (input) For cleanup comparison +# @return 0 always +# ----------------------------------------------------------------------------- +_verify_pollymc_and_finalize() { print_progress "Testing PollyMC compatibility and basic functionality..." - - # APPIMAGE EXECUTION TEST: Run PollyMC with --help flag to verify it works - # Timeout prevents hanging if AppImage has issues - # This tests: AppImage execution, basic CLI functionality, system compatibility - if timeout 5s "$HOME/.local/share/PollyMC/PollyMC-Linux-x86_64.AppImage" --help >/dev/null 2>&1; then - print_success "✅ PollyMC compatibility test passed - AppImage executes properly" - - # ============================================================================= - # POLLYMC INSTANCE VERIFICATION AND FINAL SETUP - # ============================================================================= - - # INSTANCE ACCESS VERIFICATION: Confirm PollyMC can detect and access migrated instances - # This ensures PollyMC properly recognizes the instance format from PrismLauncher - # Both launchers use similar formats, but compatibility should be verified - print_progress "Verifying PollyMC can access migrated splitscreen instances..." + + local pollymc_test_passed=false + + if [[ "$ACTIVE_LAUNCHER_TYPE" == "flatpak" ]]; then + if flatpak run "$POLLYMC_FLATPAK_ID" --help >/dev/null 2>&1; then + pollymc_test_passed=true + print_success "PollyMC Flatpak compatibility test passed" + fi + else + if timeout 5s "$POLLYMC_APPIMAGE_PATH" --help >/dev/null 2>&1; then + pollymc_test_passed=true + print_success "PollyMC AppImage compatibility test passed" + fi + fi + + if [[ "$pollymc_test_passed" == true ]]; then + print_progress "Verifying PollyMC can access splitscreen instances..." local polly_instances_count - polly_instances_count=$(find "$HOME/.local/share/PollyMC/instances" -maxdepth 1 -name "latestUpdate-*" -type d 2>/dev/null | wc -l) - + polly_instances_count=$(find "$ACTIVE_INSTANCES_DIR" -maxdepth 1 -name "latestUpdate-*" -type d 2>/dev/null | wc -l) + if [[ "$polly_instances_count" -eq 4 ]]; then - print_success "✅ PollyMC instance verification successful - all 4 instances accessible" - print_info " → latestUpdate-1, latestUpdate-2, latestUpdate-3, latestUpdate-4 ready" - - # LAUNCHER SCRIPT CONFIGURATION: Set up the splitscreen launcher for PollyMC - # This configures the controller detection and multi-instance launch script + print_success "PollyMC instance verification successful - all 4 instances accessible" + print_info " -> latestUpdate-1, latestUpdate-2, latestUpdate-3, latestUpdate-4 ready" + setup_pollymc_launcher - - # CLEANUP PHASE: Remove PrismLauncher since PollyMC is working - # This saves significant disk space (~500MB+) and avoids launcher confusion - # PrismLauncher was only needed for the CLI-based instance creation process - cleanup_prism_launcher - - print_success "🎮 PollyMC is now the primary launcher for splitscreen gameplay" - print_info " → PrismLauncher files cleaned up to save disk space" + + if [[ "$CREATION_DATA_DIR" != "$ACTIVE_DATA_DIR" ]]; then + cleanup_prism_launcher + fi + + print_success "PollyMC is now the primary launcher for splitscreen gameplay" + print_info " -> Installation type: $ACTIVE_LAUNCHER_TYPE" else - print_warning "⚠️ PollyMC instance verification failed - found $polly_instances_count instances instead of 4" - print_info " → Falling back to PrismLauncher as primary launcher" - USE_POLLYMC=false + print_warning "PollyMC instance verification failed - found $polly_instances_count instances instead of 4" + print_info " -> Falling back to PrismLauncher as primary launcher" + revert_to_prismlauncher fi else - print_warning "❌ PollyMC compatibility test failed - AppImage execution issues detected" - print_info " → This may be due to system restrictions, missing dependencies, or AppImage incompatibility" - print_info " → Falling back to PrismLauncher for gameplay (still fully functional)" - USE_POLLYMC=false + print_warning "PollyMC compatibility test failed" + print_info " -> This may be due to system restrictions or missing dependencies" + print_info " -> Falling back to PrismLauncher for gameplay (still fully functional)" + revert_to_prismlauncher fi } -# Configure the splitscreen launcher script for PollyMC -# Downloads and modifies the launcher script to use PollyMC instead of PrismLauncher +# ----------------------------------------------------------------------------- +# @function setup_pollymc_launcher +# @description Prepares PollyMC for launcher script generation. This function +# is now mostly a placeholder as the actual script generation +# happens in generate_launcher_script() in main_workflow.sh. +# +# @deprecated Use generate_launcher_script() instead +# @param None +# @return 0 always +# ----------------------------------------------------------------------------- setup_pollymc_launcher() { - print_progress "Setting up launcher script for PollyMC..." - - # LAUNCHER SCRIPT DOWNLOAD: Get the splitscreen launcher script from GitHub - # This script handles controller detection and multi-instance launching - if wget -O "$HOME/.local/share/PollyMC/minecraftSplitscreen.sh" \ - "https://raw.githubusercontent.com/FlyingEwok/MinecraftSplitscreenSteamdeck/main/minecraftSplitscreen.sh"; then - chmod +x "$HOME/.local/share/PollyMC/minecraftSplitscreen.sh" - - # LAUNCHER SCRIPT CONFIGURATION: Modify paths to use PollyMC instead of PrismLauncher - # Replace PrismLauncher AppImage path with PollyMC AppImage path - sed -i 's|PrismLauncher/PrismLauncher.AppImage|PollyMC/PollyMC-Linux-x86_64.AppImage|g' \ - "$HOME/.local/share/PollyMC/minecraftSplitscreen.sh" - # Replace PrismLauncher data directory with PollyMC data directory - sed -i 's|/.local/share/PrismLauncher/|/.local/share/PollyMC/|g' \ - "$HOME/.local/share/PollyMC/minecraftSplitscreen.sh" - - print_success "Launcher script configured and copied to PollyMC" - else - print_warning "Failed to download launcher script" - fi + print_progress "Preparing PollyMC for launcher script generation..." + print_info "Launcher script will be generated in the next phase with correct paths" + print_success "PollyMC configured for launcher script generation" } -# Clean up PrismLauncher installation after successful PollyMC setup -# This removes the temporary PrismLauncher directory to save disk space -# PrismLauncher was only needed for automated instance creation via CLI +# ----------------------------------------------------------------------------- +# @function cleanup_prism_launcher +# @description Removes PrismLauncher installation after successful PollyMC +# setup to save disk space. Includes safety checks to prevent +# accidental deletion of important directories. +# +# @param None +# @global CREATION_DATA_DIR - (input) PrismLauncher data directory to remove +# @return 0 on success, 1 if cd fails +# @note Only removes directories containing "PrismLauncher" in the path +# ----------------------------------------------------------------------------- cleanup_prism_launcher() { print_progress "Cleaning up PrismLauncher (no longer needed)..." - - # SAFETY: Navigate to home directory before removal operations - # This prevents accidental deletion if we're currently in the target directory + + # Safety: Navigate to home directory first cd "$HOME" || return 1 - - # SAFETY CHECKS: Multiple validations before removing directories - # Ensure we're not deleting critical system directories or user home - if [[ -d "$TARGET_DIR" && "$TARGET_DIR" != "$HOME" && "$TARGET_DIR" != "/" && "$TARGET_DIR" == *"PrismLauncher"* ]]; then - rm -rf "$TARGET_DIR" - print_success "Removed PrismLauncher directory: $TARGET_DIR" + + local prism_dir="$CREATION_DATA_DIR" + + # Safety checks before removal + if [[ -d "$prism_dir" && "$prism_dir" != "$HOME" && "$prism_dir" != "/" && "$prism_dir" == *"PrismLauncher"* ]]; then + rm -rf "$prism_dir" + print_success "Removed PrismLauncher directory: $prism_dir" print_info "All essential files now in PollyMC directory" else - print_warning "Skipped directory removal for safety: $TARGET_DIR" + print_info "Skipped directory removal (not a PrismLauncher directory): $prism_dir" fi } diff --git a/modules/steam_integration.sh b/modules/steam_integration.sh index a63e0e2..24b2495 100644 --- a/modules/steam_integration.sh +++ b/modules/steam_integration.sh @@ -1,18 +1,43 @@ #!/bin/bash # ============================================================================= -# Minecraft Splitscreen Steam Deck Installer - Steam Integration Module -# ============================================================================= -# -# This module handles the integration of Minecraft Splitscreen launcher with -# Steam, providing native Steam library integration, Big Picture mode support, -# and Steam Deck Game Mode integration. +# @file steam_integration.sh +# @version 2.0.0 +# @date 2026-01-25 +# @author Minecraft Splitscreen Steam Deck Project +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Handles the integration of Minecraft Splitscreen launcher with Steam, +# providing native Steam library integration, Big Picture mode support, +# and Steam Deck Game Mode integration. +# +# Key features: +# - Automatic Steam shortcut creation via shortcuts.vdf modification +# - SteamGridDB artwork download for professional appearance +# - Duplicate shortcut detection and prevention +# - Safe Steam shutdown/restart procedure +# - Steam Deck-aware process management # -# Functions provided: -# - setup_steam_integration: Add Minecraft Splitscreen launcher to Steam library +# @dependencies +# - utilities.sh (for print_header, print_success, print_warning, print_error, print_info, print_progress) +# - path_configuration.sh (for ACTIVE_LAUNCHER_SCRIPT, ACTIVE_DATA_DIR, ACTIVE_LAUNCHER) +# - curl (for downloading Steam integration script) +# - python3 (for running add-to-steam.py) # +# @exports +# Functions: +# - setup_steam_integration : Main function to add launcher to Steam +# +# @changelog +# 2.0.0 (2026-01-25) - Added comprehensive JSDoc documentation +# 1.0.0 (2024-XX-XX) - Initial implementation # ============================================================================= -# setup_steam_integration: Add Minecraft Splitscreen launcher to Steam library +# @function setup_steam_integration +# @description Add Minecraft Splitscreen launcher to Steam library. +# Handles Steam shutdown, shortcut creation via Python script, +# artwork download from SteamGridDB, and Steam restart. # # STEAM INTEGRATION BENEFITS: # - Launch directly from Steam's game library interface @@ -35,42 +60,51 @@ # - Creates backup before modifications # - Uses official Steam binary format handling # - Handles multiple Steam installation types (native, Flatpak) +# +# @global ACTIVE_LAUNCHER_SCRIPT - (input) Path to the launcher script +# @global ACTIVE_DATA_DIR - (input) Launcher data directory +# @global ACTIVE_LAUNCHER - (input) Name of active launcher +# @global REPO_RAW_URL - (input, optional) Repository raw content URL +# @global REPO_BRANCH - (input, optional) Repository branch name +# @stdin User confirmation from /dev/tty (for curl | bash compatibility) +# @return 0 on success or skip, 1 if ACTIVE_LAUNCHER_SCRIPT not set setup_steam_integration() { print_header "🎯 STEAM INTEGRATION SETUP" - + # ============================================================================= # STEAM INTEGRATION USER PROMPT # ============================================================================= - + # USER PREFERENCE GATHERING: Ask if they want Steam integration # Steam integration is optional but highly recommended for Steam Deck users # Desktop users may prefer to launch manually or from application menu print_info "Steam integration adds Minecraft Splitscreen to your Steam library." print_info "Benefits: Easy access from Steam, Big Picture mode support, Steam Deck Game Mode integration" echo "" - read -p "Do you want to add Minecraft Splitscreen launcher to Steam? [y/N]: " add_to_steam - if [[ "$add_to_steam" =~ ^[Yy]$ ]]; then - + # Use centralized prompt function that handles curl | bash piping + if prompt_yes_no "Do you want to add Minecraft Splitscreen launcher to Steam?" "n"; then + # ============================================================================= # LAUNCHER PATH DETECTION AND CONFIGURATION # ============================================================================= - - # LAUNCHER TYPE DETECTION: Determine which launcher is active for Steam integration + + # LAUNCHER TYPE DETECTION: Use centralized path configuration # The Steam shortcut needs to point to the correct launcher executable and script - # Path fragments are used by the duplicate detection system - local launcher_path="" - if [[ "$USE_POLLYMC" == true ]]; then - launcher_path="local/share/PollyMC/minecraft" # PollyMC path signature for duplicate detection - print_info "Configuring Steam integration for PollyMC launcher" - else - launcher_path="local/share/PrismLauncher/minecraft" # PrismLauncher path signature - print_info "Configuring Steam integration for PrismLauncher" + # Use ACTIVE_LAUNCHER_SCRIPT from path_configuration.sh + if [[ -z "$ACTIVE_LAUNCHER_SCRIPT" ]]; then + print_error "ACTIVE_LAUNCHER_SCRIPT not set. Cannot configure Steam integration." + return 1 fi - + + # Path fragment for duplicate detection (extract from full path) + local launcher_path="${ACTIVE_LAUNCHER_SCRIPT#$HOME/}" + print_info "Configuring Steam integration for ${ACTIVE_LAUNCHER^} launcher" + print_info " Launcher script: $ACTIVE_LAUNCHER_SCRIPT" + # ============================================================================= # DUPLICATE SHORTCUT PREVENTION # ============================================================================= - + # EXISTING SHORTCUT CHECK: Search Steam's shortcuts database for existing entries # Prevents creating duplicate shortcuts which can cause confusion and clutter # Searches all Steam user accounts on the system for existing Minecraft shortcuts @@ -79,45 +113,45 @@ setup_steam_integration() { # ============================================================================= # STEAM SHUTDOWN AND BACKUP PROCEDURE # ============================================================================= - + print_progress "Adding Minecraft Splitscreen launcher to Steam library..." - + # STEAM PROCESS TERMINATION: Safely shut down Steam before modifying shortcuts # Steam must be completely closed to safely modify the shortcuts.vdf binary database # The shortcuts.vdf file is locked while Steam is running and changes may be lost # STEAM DECK SAFETY: Use precise process targeting to avoid killing SteamOS components print_progress "Shutting down Steam to safely modify shortcuts database..." - + # Temporarily disable strict error handling for Steam shutdown set +e - + # Steam Deck-aware shutdown approach print_info " → Attempting graceful Steam shutdown..." steam -shutdown 2>/dev/null || true sleep 3 - + # Only force close the actual Steam client process, avoiding SteamOS components print_info " → Force closing Steam client process (preserving SteamOS)..." # Use exact process name matching to avoid killing SteamOS processes pkill -x "steam" 2>/dev/null || true sleep 2 - + # Re-enable strict error handling set -e - + # STEAM SHUTDOWN VERIFICATION: Wait for complete shutdown # Check for Steam processes and wait until Steam fully exits # This prevents corruption of the shortcuts database during modification local shutdown_attempts=0 local max_attempts=10 - + while [[ $shutdown_attempts -lt $max_attempts ]]; do # Check for Steam client processes (Steam Deck-safe approach) local steam_running=false - + # Temporarily disable error handling for process checks set +e - + # Check only for the main Steam client process, not SteamOS components if pgrep -x "steam" >/dev/null 2>&1; then steam_running=true @@ -128,38 +162,38 @@ setup_steam_integration() { steam_running=true fi fi - + # Re-enable strict error handling set -e - + if [[ "$steam_running" == false ]]; then break fi - + sleep 1 ((shutdown_attempts++)) done - + if [[ $shutdown_attempts -ge $max_attempts ]]; then print_warning "⚠️ Steam shutdown timeout - proceeding anyway (may cause issues)" print_info " → Some Steam processes may still be running" else print_success "✅ Steam shutdown complete" fi - + # ============================================================================= # STEAM SHORTCUTS BACKUP SYSTEM # ============================================================================= - + # BACKUP CREATION: Create safety backup of existing Steam shortcuts - # Backup stored in current working directory (safer than TARGET_DIR which may be cleaned) + # Backup stored in current working directory (safer than PRISMLAUNCHER_DIR which may be cleaned) # Compressed archive saves space and preserves all user shortcuts databases local backup_path="$PWD/steam-shortcuts-backup-$(date +%Y%m%d_%H%M%S).tar.xz" print_progress "Creating backup of Steam shortcuts database..." - + # Disable strict error handling for backup creation set +e - + # Check if Steam userdata directory exists first if [[ -d ~/.steam/steam/userdata ]]; then # Try to create backup with better error handling @@ -173,14 +207,14 @@ setup_steam_integration() { print_warning "⚠️ Steam userdata directory not found - skipping backup" print_info " → Steam may not be properly installed or configured" fi - + # Re-enable strict error handling set -e - + # ============================================================================= # STEAM INTEGRATION SCRIPT EXECUTION # ============================================================================= - + # PYTHON INTEGRATION SCRIPT: Download and execute Steam shortcut creation tool # Uses the official add-to-steam.py script from the repository # This script handles the complex shortcuts.vdf binary format safely @@ -189,50 +223,51 @@ setup_steam_integration() { print_info " → Downloading launcher detection and shortcut creation script" print_info " → Modifying Steam shortcuts.vdf binary database" print_info " → Downloading custom artwork from SteamGridDB" - + # Execute the Steam integration script with error handling # Download script to temporary file first to avoid pipefail issues local steam_script_temp steam_script_temp=$(mktemp) - + # Disable strict error handling for script download and execution set +e - + print_info " → Downloading Steam integration script..." - if curl -sSL https://raw.githubusercontent.com/FlyingEwok/MinecraftSplitscreenSteamdeck/main/add-to-steam.py -o "$steam_script_temp" 2>/dev/null; then + if curl -sSL "${REPO_RAW_URL:-https://raw.githubusercontent.com/aradanmn/MinecraftSplitscreenSteamdeck/${REPO_BRANCH:-main}}/add-to-steam.py" -o "$steam_script_temp" 2>/dev/null; then print_info " → Executing Steam integration script..." - # Execute the downloaded script with proper error handling - if python3 "$steam_script_temp" 2>/dev/null; then + # Pass launcher paths from path_configuration.sh to the Python script + # Arguments: + if python3 "$steam_script_temp" "$ACTIVE_LAUNCHER_SCRIPT" "$ACTIVE_DATA_DIR" "${ACTIVE_LAUNCHER^}" 2>/dev/null; then print_success "✅ Minecraft Splitscreen successfully added to Steam library" print_info " → Custom artwork downloaded and applied" print_info " → Shortcut configured with proper launch parameters" else print_warning "⚠️ Steam integration script encountered errors" print_info " → You may need to add the shortcut manually" - print_info " → Common causes: PollyMC not found, Steam not installed, or permissions issues" + print_info " → Common causes: Launcher script not found, Steam not installed, or permissions issues" fi else print_warning "⚠️ Failed to download Steam integration script" print_info " → You may need to add the shortcut manually" print_info " → Check your internet connection and try again later" fi - + # Clean up temporary file rm -f "$steam_script_temp" 2>/dev/null || true - + # Re-enable strict error handling set -e - + # ============================================================================= # STEAM RESTART AND VERIFICATION # ============================================================================= - + # STEAM RESTART: Launch Steam in background after successful modification # Use nohup to prevent Steam from being tied to terminal session # Steam will automatically detect the new shortcut in its library print_progress "Restarting Steam with new shortcut..." nohup steam >/dev/null 2>&1 & - + print_success "🎮 Steam integration complete!" print_info " → Minecraft Splitscreen should now appear in your Steam library" print_info " → Accessible from Steam Big Picture mode and Steam Deck Game Mode" @@ -241,7 +276,7 @@ setup_steam_integration() { # ============================================================================= # DUPLICATE SHORTCUT HANDLING # ============================================================================= - + print_info "✅ Minecraft Splitscreen launcher already present in Steam library" print_info " → No changes needed - existing shortcut is functional" print_info " → If you need to update the shortcut, please remove it manually from Steam first" @@ -250,7 +285,7 @@ setup_steam_integration() { # ============================================================================= # STEAM INTEGRATION DECLINED # ============================================================================= - + print_info "⏭️ Skipping Steam integration" print_info " → You can still launch Minecraft Splitscreen manually or from desktop launcher" print_info " → To add to Steam later, run this installer again or use the add-to-steam.py script" diff --git a/modules/utilities.sh b/modules/utilities.sh index 116749b..0a1c21d 100644 --- a/modules/utilities.sh +++ b/modules/utilities.sh @@ -2,49 +2,500 @@ # ============================================================================= # UTILITY FUNCTIONS MODULE # ============================================================================= -# Progress and status reporting functions and general utilities -# These functions provide consistent, colored output for better user experience +# @file utilities.sh +# @version 1.2.0 +# @date 2026-01-26 +# @author aradanmn +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Core utility functions for the Minecraft Splitscreen installer. +# Provides logging, output formatting, user input handling, system detection, +# and account management functionality used by all other modules. +# +# LOGGING: All print_* functions automatically log to file. The log() function +# is for debug info that shouldn't clutter the terminal. +# +# @dependencies +# - jq (optional, for JSON merging - falls back to overwrite if missing) +# - flatpak (optional, for Flatpak preference detection) +# - ostree (optional, for immutable OS detection) +# +# @exports +# Functions: +# - init_logging : Initialize logging system (call first in main) +# - log : Write debug info to log only (not terminal) +# - get_log_file : Get current log file path +# - prompt_user : Get user input (works with curl | bash) +# - prompt_yes_no : Simplified yes/no prompts +# - get_prism_executable : Locate PrismLauncher executable +# - is_immutable_os : Detect immutable Linux distributions +# - should_prefer_flatpak : Determine preferred package format +# - print_header : Display section headers (auto-logs) +# - print_success : Display success messages (auto-logs) +# - print_warning : Display warning messages (auto-logs) +# - print_error : Display error messages (auto-logs) +# - print_info : Display info messages (auto-logs) +# - print_progress : Display progress messages (auto-logs) +# - merge_accounts_json : Merge Minecraft account configurations +# +# Variables: +# - LOG_FILE : Current log file path (set by init_logging) +# - LOG_DIR : Log directory path +# - IMMUTABLE_OS_NAME : Set by is_immutable_os() with detected OS name +# +# @changelog +# 1.2.0 (2026-01-26) - Added logging system, prompt_user for curl|bash support +# 1.1.0 (2026-01-24) - Added immutable OS detection and Flatpak preference +# 1.0.0 (2026-01-23) - Initial version with print functions and account merging +# ============================================================================= + +# ============================================================================= +# LOGGING SYSTEM +# ============================================================================= +# Logging is automatic - all print_* functions log to file. +# Use log() directly only for debug info that shouldn't show in terminal. + +LOG_FILE="" +LOG_DIR="$HOME/.local/share/MinecraftSplitscreen/logs" +LOG_MAX_FILES=10 + +# ----------------------------------------------------------------------------- +# @function init_logging +# @description Initialize logging. Creates log directory, rotates old logs, +# and logs system info. Call at the start of main(). +# @param $1 - Log type: "install" or "launcher" (default: "install") +# ----------------------------------------------------------------------------- +init_logging() { + local log_type="${1:-install}" + local timestamp + timestamp=$(date +%Y-%m-%d-%H%M%S) + + mkdir -p "$LOG_DIR" 2>/dev/null || { + LOG_DIR="/tmp/MinecraftSplitscreen/logs" + mkdir -p "$LOG_DIR" + } + + LOG_FILE="$LOG_DIR/${log_type}-${timestamp}.log" + + # Rotate old logs (keep last N) + local count=0 + while IFS= read -r file; do + count=$((count + 1)) + [[ $count -gt $LOG_MAX_FILES ]] && rm -f "$file" 2>/dev/null + done < <(ls -t "$LOG_DIR"/${log_type}-*.log 2>/dev/null) + + # Write log header + { + echo "================================================================================" + echo "Minecraft Splitscreen ${log_type^} Log" + echo "Started: $(date '+%Y-%m-%d %H:%M:%S %Z')" + echo "================================================================================" + echo "" + echo "=== SYSTEM INFO ===" + echo "User: $(whoami)" + echo "Hostname: $(hostname 2>/dev/null || echo 'unknown')" + [[ -f /etc/os-release ]] && grep -E '^(PRETTY_NAME|ID)=' /etc/os-release 2>/dev/null + echo "Kernel: $(uname -r 2>/dev/null)" + echo "Arch: $(uname -m 2>/dev/null)" + echo "" + echo "=== ENVIRONMENT ===" + echo "DISPLAY: ${DISPLAY:-not set}" + echo "XDG_SESSION_TYPE: ${XDG_SESSION_TYPE:-not set}" + echo "STEAM_DECK: ${STEAM_DECK:-not set}" + echo "" + echo "================================================================================" + echo "" + } >> "$LOG_FILE" 2>/dev/null +} -# get_prism_executable: Get the correct path to PrismLauncher executable -# Handles both AppImage and extracted versions (for FUSE issues) +# ----------------------------------------------------------------------------- +# @function log +# @description Write debug info to log file ONLY (not terminal). Use for +# verbose details that help debugging but clutter the screen. +# ----------------------------------------------------------------------------- +log() { + [[ -n "$LOG_FILE" ]] && echo "[$(date '+%H:%M:%S')] $*" >> "$LOG_FILE" 2>/dev/null +} + +# ----------------------------------------------------------------------------- +# @function get_log_file +# @description Returns the current log file path. +# ----------------------------------------------------------------------------- +get_log_file() { + echo "$LOG_FILE" +} + +# ============================================================================= +# USER INPUT HANDLING +# ============================================================================= +# These functions work both in normal execution AND curl | bash mode. + +# ----------------------------------------------------------------------------- +# @function prompt_user +# @description Get user input. Works with curl | bash by reopening /dev/tty. +# @param $1 - Prompt text +# @param $2 - Default value +# @param $3 - Timeout in seconds (default: 30, 0 for none) +# @stdout User's response (or default) +# ----------------------------------------------------------------------------- +prompt_user() { + local prompt="$1" + local default="${2:-}" + local timeout="${3:-30}" + local response saved_stdin + + log "PROMPT: $prompt (default: $default, timeout: ${timeout}s)" + + # Reopen /dev/tty if stdin isn't a terminal (curl | bash case) + if [[ ! -t 0 ]]; then + if [[ -e /dev/tty ]]; then + exec {saved_stdin}<&0 + exec 0/dev/null; then + IMMUTABLE_OS_NAME="Bazzite" + return 0 + fi + + # SteamOS (Steam Deck) + if [[ -f /etc/steamos-release ]] || grep -qi "steamos" /etc/os-release 2>/dev/null; then + IMMUTABLE_OS_NAME="SteamOS" + return 0 + fi + + # Fedora Silverblue/Kinoite/Atomic + if grep -qi "fedora" /etc/os-release 2>/dev/null; then + if grep -qi "silverblue\|kinoite\|atomic\|ostree" /etc/os-release 2>/dev/null || \ + [[ -d /ostree ]] || rpm-ostree status &>/dev/null; then + IMMUTABLE_OS_NAME="Fedora Atomic" + return 0 + fi + fi + + # Universal Blue variants (Aurora, Bluefin, etc.) + if [[ -f /etc/ublue-os/image_name ]] || grep -qi "ublue\|aurora\|bluefin" /etc/os-release 2>/dev/null; then + IMMUTABLE_OS_NAME="Universal Blue" + return 0 + fi + + # NixOS (immutable by design) + if [[ -f /etc/NIXOS ]] || grep -qi "nixos" /etc/os-release 2>/dev/null; then + IMMUTABLE_OS_NAME="NixOS" + return 0 + fi + + # openSUSE MicroOS/Aeon/Kalpa + if grep -qi "microos\|aeon\|kalpa" /etc/os-release 2>/dev/null; then + IMMUTABLE_OS_NAME="openSUSE MicroOS" + return 0 fi + + # Endless OS + if grep -qi "endless" /etc/os-release 2>/dev/null; then + IMMUTABLE_OS_NAME="Endless OS" + return 0 + fi + + # Generic ostree-based detection (catches other atomic distros) + if [[ -d /ostree ]] && command -v ostree &>/dev/null; then + IMMUTABLE_OS_NAME="ostree-based" + return 0 + fi + + return 1 } -# print_header: Display a section header with visual separation +# ----------------------------------------------------------------------------- +# @function should_prefer_flatpak +# @description Determines if Flatpak should be preferred over AppImage for +# application installation. Returns true for immutable systems +# or systems where Flatpak appears to be the primary package format. +# @param None +# @return 0 if Flatpak preferred, 1 if AppImage preferred +# @example +# if should_prefer_flatpak; then +# flatpak install --user flathub org.prismlauncher.PrismLauncher +# else +# wget -O app.AppImage "$appimage_url" +# fi +# ----------------------------------------------------------------------------- +should_prefer_flatpak() { + # Prefer Flatpak on immutable systems + if is_immutable_os; then + return 0 + fi + + # Also prefer Flatpak if it's the primary package manager + if command -v flatpak &>/dev/null; then + if ! command -v apt &>/dev/null && ! command -v dnf &>/dev/null && ! command -v pacman &>/dev/null; then + return 0 + fi + fi + + return 1 +} + +# ============================================================================= +# OUTPUT FORMATTING FUNCTIONS +# ============================================================================= + +# ----------------------------------------------------------------------------- +# @function print_header +# @description Displays a prominent section header with visual separators. +# @param $1 - Header text to display +# @stdout Formatted header with separator lines +# @return 0 always +# @example +# print_header "INSTALLING DEPENDENCIES" +# ----------------------------------------------------------------------------- print_header() { echo "==========================================" echo "$1" echo "==========================================" + log "========== $1 ==========" } -# print_success: Display successful operation with green checkmark +# ----------------------------------------------------------------------------- +# @function print_success +# @description Displays a success message with green checkmark emoji. Auto-logs. +# @param $1 - Success message text +# @stdout Formatted success message +# @return 0 always +# ----------------------------------------------------------------------------- print_success() { echo "✅ $1" + log "SUCCESS: $1" } -# print_warning: Display warning message with yellow warning symbol +# ----------------------------------------------------------------------------- +# @function print_warning +# @description Displays a warning message with yellow warning emoji. Auto-logs. +# @param $1 - Warning message text +# @stdout Formatted warning message +# @return 0 always +# ----------------------------------------------------------------------------- print_warning() { echo "⚠️ $1" + log "WARNING: $1" } -# print_error: Display error message with red X symbol (sent to stderr) +# ----------------------------------------------------------------------------- +# @function print_error +# @description Displays an error message with red X emoji to stderr. Auto-logs. +# @param $1 - Error message text +# @stderr Formatted error message +# @return 0 always +# ----------------------------------------------------------------------------- print_error() { echo "❌ $1" >&2 + log "ERROR: $1" } -# print_info: Display informational message with blue info symbol +# ----------------------------------------------------------------------------- +# @function print_info +# @description Displays an informational message with lightbulb emoji. Auto-logs. +# @param $1 - Info message text +# @stdout Formatted info message +# @return 0 always +# ----------------------------------------------------------------------------- print_info() { echo "💡 $1" + log "INFO: $1" } -# print_progress: Display in-progress operation with spinning arrow +# ----------------------------------------------------------------------------- +# @function print_progress +# @description Displays a progress/in-progress message with spinner emoji. Auto-logs. +# @param $1 - Progress message text +# @stdout Formatted progress message +# @return 0 always +# ----------------------------------------------------------------------------- print_progress() { echo "🔄 $1" + log "PROGRESS: $1" +} + +# ============================================================================= +# ACCOUNT MANAGEMENT FUNCTIONS +# ============================================================================= + +# ----------------------------------------------------------------------------- +# @function merge_accounts_json +# @description Merges splitscreen player accounts (P1-P4) into an existing +# accounts.json file while preserving any other accounts (e.g., +# Microsoft accounts). If jq is not available, falls back to +# overwriting the destination file. +# +# @param $1 - source_file: Path to accounts.json with P1-P4 accounts +# @param $2 - dest_file: Path to destination accounts.json (created if missing) +# +# @return 0 on success (merge or copy completed) +# 1 on failure (source file not found) +# +# @example +# merge_accounts_json "/tmp/splitscreen_accounts.json" "$HOME/.local/share/PrismLauncher/accounts.json" +# +# @note Requires jq for proper merging. Without jq, existing accounts +# will be overwritten with splitscreen accounts only. +# ----------------------------------------------------------------------------- +merge_accounts_json() { + local source_file="$1" + local dest_file="$2" + + # Validate source file exists + if [[ ! -f "$source_file" ]]; then + print_error "Source accounts file not found: $source_file" + return 1 + fi + + # If destination doesn't exist, just copy source + if [[ ! -f "$dest_file" ]]; then + cp "$source_file" "$dest_file" + print_info "Created new accounts.json with splitscreen accounts" + return 0 + fi + + # Check if jq is available for JSON merging + if ! command -v jq >/dev/null 2>&1; then + print_warning "jq not installed - attempting basic merge" + cp "$source_file" "$dest_file" + print_warning "Existing accounts may have been overwritten (install jq for proper merging)" + return 0 + fi + + # Extract player names from source (P1, P2, P3, P4) + local splitscreen_names + splitscreen_names=$(jq -r '.accounts[].profile.name' "$source_file" 2>/dev/null) + + # Create a temporary file for the merged result + local temp_file + temp_file=$(mktemp) + + # Merge accounts: + # 1. Keep all existing accounts that are NOT P1-P4 (preserve Microsoft accounts, etc.) + # 2. Add all accounts from source (P1-P4 splitscreen accounts) + if jq -s ' + (.[0].accounts | map(.profile.name)) as $splitscreen_names | + { + "accounts": ( + (.[1].accounts // [] | map(select(.profile.name as $name | $splitscreen_names | index($name) | not))) + + .[0].accounts + ), + "formatVersion": (.[1].formatVersion // .[0].formatVersion // 3) + } + ' "$source_file" "$dest_file" > "$temp_file" 2>/dev/null; then + # Validate the merged JSON + if jq empty "$temp_file" 2>/dev/null; then + mv "$temp_file" "$dest_file" + print_success "Merged splitscreen accounts with existing accounts" + return 0 + else + print_warning "Merged JSON validation failed, using source file" + rm -f "$temp_file" + cp "$source_file" "$dest_file" + return 0 + fi + else + print_warning "JSON merge failed, using source file" + rm -f "$temp_file" + cp "$source_file" "$dest_file" + return 0 + fi } diff --git a/modules/version_info.sh b/modules/version_info.sh new file mode 100644 index 0000000..8e8e51a --- /dev/null +++ b/modules/version_info.sh @@ -0,0 +1,175 @@ +#!/bin/bash +# ============================================================================= +# VERSION INFORMATION MODULE +# ============================================================================= +# @file version_info.sh +# @version 2.0.0 +# @date 2026-01-24 +# @author aradanmn +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Provides version constants, repository information, and version utility +# functions used throughout the installer and generated scripts. This module +# is the single source of truth for versioning information. +# +# @dependencies +# - git (optional, for commit hash detection) +# - date (for timestamp generation) +# +# @exports +# Constants: +# - SCRIPT_VERSION : Current version (semver format) +# - REPO_OWNER : GitHub repository owner +# - REPO_NAME : GitHub repository name +# - REPO_BRANCH : Active branch for downloads +# - REPO_URL : Full repository URL +# - REPO_RAW_URL : Raw content URL for file downloads +# - REPO_MODULES_URL : URL for modules directory +# +# Functions: +# - get_commit_hash : Get current git commit (short) +# - get_timestamp : Get ISO 8601 timestamp +# - generate_version_header: Generate script header block +# - print_version_info : Print version to stdout +# - verify_repo_source : Verify running from expected repo +# +# @changelog +# 2.0.0 (2026-01-24) - Updated for modular installer architecture +# 1.0.0 (2026-01-22) - Initial version +# ============================================================================= + +# ============================================================================= +# VERSION CONSTANTS +# ============================================================================= + +# Script version - update this when making releases +readonly SCRIPT_VERSION="2.0.0" + +# Repository information +readonly REPO_OWNER="aradanmn" +readonly REPO_NAME="MinecraftSplitscreenSteamdeck" +readonly REPO_BRANCH="main" + +# Derived URLs +readonly REPO_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}" +readonly REPO_RAW_URL="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${REPO_BRANCH}" +readonly REPO_MODULES_URL="${REPO_RAW_URL}/modules" + +# ============================================================================= +# VERSION UTILITY FUNCTIONS +# ============================================================================= + +# ----------------------------------------------------------------------------- +# @function get_commit_hash +# @description Returns the current git commit hash in short form (7 characters). +# Returns "unknown" if not in a git repository or git unavailable. +# @param None +# @stdout Short commit hash or "unknown" +# @return 0 always +# @example +# commit=$(get_commit_hash) +# echo "Current commit: $commit" +# ----------------------------------------------------------------------------- +get_commit_hash() { + if command -v git >/dev/null 2>&1; then + git rev-parse --short HEAD 2>/dev/null || echo "unknown" + else + echo "unknown" + fi +} + +# ----------------------------------------------------------------------------- +# @function get_timestamp +# @description Returns the current timestamp in ISO 8601 format. +# @param None +# @stdout ISO 8601 formatted timestamp (e.g., 2026-01-24T14:30:00-06:00) +# @return 0 always +# ----------------------------------------------------------------------------- +get_timestamp() { + date -Iseconds 2>/dev/null || date "+%Y-%m-%dT%H:%M:%S%z" +} + +# ----------------------------------------------------------------------------- +# @function generate_version_header +# @description Generates a standardized version header block for auto-generated +# scripts. Includes version, commit, timestamp, and source info. +# @param $1 - script_name: Name of the script (default: "unknown") +# @param $2 - description: Brief description (default: "Auto-generated script") +# @stdout Multi-line header block suitable for bash scripts +# @return 0 always +# @example +# generate_version_header "minecraftSplitscreen.sh" "Minecraft Splitscreen Launcher" +# ----------------------------------------------------------------------------- +generate_version_header() { + local script_name="${1:-unknown}" + local description="${2:-Auto-generated script}" + local commit_hash + local timestamp + + commit_hash=$(get_commit_hash) + timestamp=$(get_timestamp) + + cat << EOF +# ============================================================================= +# ${description} +# ============================================================================= +# Version: ${SCRIPT_VERSION} (commit: ${commit_hash}) +# Generated: ${timestamp} +# Generator: install-minecraft-splitscreen.sh v${SCRIPT_VERSION} +# Source: ${REPO_URL} +# +# DO NOT EDIT - This file is auto-generated by the installer. +# To update, re-run the installer script. +# ============================================================================= +EOF +} + +# ----------------------------------------------------------------------------- +# @function print_version_info +# @description Prints human-readable version information to stdout. Useful for +# --version flags or debugging output. +# @param None +# @stdout Formatted version information +# @return 0 always +# ----------------------------------------------------------------------------- +print_version_info() { + local commit_hash + commit_hash=$(get_commit_hash) + + echo "Minecraft Splitscreen Installer" + echo "Version: ${SCRIPT_VERSION} (commit: ${commit_hash})" + echo "Repository: ${REPO_URL}" + echo "Branch: ${REPO_BRANCH}" +} + +# ----------------------------------------------------------------------------- +# @function verify_repo_source +# @description Verifies that the script is running from the expected repository. +# Prints a warning if running from a different repository but does +# not fail (allows forks and local modifications). +# @param None +# @stderr Warning message if repository mismatch +# @return 0 if matches or cannot verify, 1 if mismatch detected +# ----------------------------------------------------------------------------- +verify_repo_source() { + if ! command -v git >/dev/null 2>&1; then + return 0 + fi + + local remote_url + remote_url=$(git config --get remote.origin.url 2>/dev/null || echo "") + + if [[ -z "$remote_url" ]]; then + return 0 + fi + + if echo "$remote_url" | grep -qi "${REPO_OWNER}/${REPO_NAME}"; then + return 0 + fi + + echo "[Warning] Running from a different repository: $remote_url" >&2 + echo "[Warning] Expected: ${REPO_URL}" >&2 + return 1 +} diff --git a/modules/version_management.sh b/modules/version_management.sh index 6d16872..d8a2627 100644 --- a/modules/version_management.sh +++ b/modules/version_management.sh @@ -1,23 +1,67 @@ #!/bin/bash # ============================================================================= -# VERSION MANAGEMENT MODULE +# @file version_management.sh +# @version 2.0.0 +# @date 2026-01-25 +# @author Minecraft Splitscreen Steam Deck Project +# @license MIT +# @repository https://github.com/aradanmn/MinecraftSplitscreenSteamdeck +# +# @description +# Minecraft and Fabric version selection and detection functions. +# Implements intelligent version selection based on required mod compatibility, +# querying APIs to verify that both essential splitscreen mods (Controllable +# and Splitscreen Support) are available for each version. +# +# Key features: +# - Dynamic version compatibility checking via Modrinth/CurseForge APIs +# - Multi-stage version matching (exact → major.minor → wildcard) +# - Interactive version selection with fallback recommendations +# - Fabric loader version detection from official API +# +# @dependencies +# - utilities.sh (for print_progress, print_success, print_warning, print_error, print_info, print_header) +# - curl (for API requests) +# - jq (for JSON parsing) +# - openssl (for CurseForge API token decryption) +# +# @global_outputs +# - MC_VERSION: Selected Minecraft version +# - FABRIC_VERSION: Detected Fabric loader version +# +# @exports +# Functions: +# - get_supported_minecraft_versions : Get list of compatible MC versions +# - check_mod_version_compatibility : Check single mod/version compatibility +# - fallback_dependencies : Hardcoded dependency fallbacks +# - get_minecraft_version : Interactive version selection +# - get_fabric_version : Fetch latest Fabric loader version +# +# @changelog +# 2.0.0 (2026-01-25) - Added comprehensive JSDoc documentation +# 1.0.0 (2024-XX-XX) - Initial implementation # ============================================================================= -# Minecraft and Fabric version selection and detection functions -# Intelligent version selection based on required mod compatibility -# get_supported_minecraft_versions: Check what Minecraft versions support required mods -# Queries APIs for Controllable and Splitscreen Support to find compatible versions -# Returns: Array of supported Minecraft versions in descending order (newest first) +# ============================================================================= +# VERSION COMPATIBILITY CHECKING +# ============================================================================= + +# @function get_supported_minecraft_versions +# @description Check what Minecraft versions support both required splitscreen mods +# (Controllable and Splitscreen Support). Queries APIs to find compatible versions. +# @stdout Array of supported Minecraft versions in descending order (newest first) +# @stderr Progress and status messages +# @return 0 if versions found, 1 if none found or API error get_supported_minecraft_versions() { print_progress "Checking supported Minecraft versions for essential splitscreen mods..." >&2 - + local -a supported_versions=() local -a all_versions=() - + # Get all Minecraft versions from Mojang API local mojang_versions mojang_versions=$(curl -s "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json" 2>/dev/null | jq -r '.versions[] | select(.type=="release") | .id' 2>/dev/null) - + if [[ -z "$mojang_versions" ]]; then print_error "Could not fetch Minecraft versions from Mojang API" >&2 print_error "Please check your internet connection and try again" >&2 @@ -27,27 +71,27 @@ get_supported_minecraft_versions() { readarray -t all_versions <<< "$mojang_versions" all_versions=("${all_versions[@]:0:15}") fi - + print_info "Checking compatibility for required splitscreen mods..." >&2 - + # Check each Minecraft version for compatibility with BOTH required mods # This is the ONLY filter - actual API testing, no hardcoded exclusions for mc_version in "${all_versions[@]}"; do print_progress " Testing $mc_version..." >&2 - + local controllable_compatible=false local splitscreen_compatible=false - + # Check Controllable (CurseForge mod 317269) if check_mod_version_compatibility "317269" "curseforge" "$mc_version"; then controllable_compatible=true fi - - # Check Splitscreen Support (Modrinth mod yJgqfSDR) + + # Check Splitscreen Support (Modrinth mod yJgqfSDR) if check_mod_version_compatibility "yJgqfSDR" "modrinth" "$mc_version"; then splitscreen_compatible=true fi - + # Only include versions where BOTH essential mods are available if [[ "$controllable_compatible" == true && "$splitscreen_compatible" == true ]]; then supported_versions+=("$mc_version") @@ -56,29 +100,30 @@ get_supported_minecraft_versions() { print_info " ❌ $mc_version - Missing essential mod support" >&2 fi done - + if [[ ${#supported_versions[@]} -eq 0 ]]; then print_error "No Minecraft versions found with both required mods available!" >&2 print_error "This may be due to API issues. Please try again later or check your internet connection." >&2 return 1 fi - + # Return the supported versions array (to stdout only) printf '%s\n' "${supported_versions[@]}" } -# check_mod_version_compatibility: Check if a specific mod supports a specific MC version -# This is a lightweight version check that doesn't add mods to arrays -# Parameters: -# $1 - mod_id: Mod ID (Modrinth project ID or CurseForge project ID) -# $2 - platform: "modrinth" or "curseforge" -# $3 - mc_version: Minecraft version to check (e.g. "1.21.3") -# Returns: 0 if compatible, 1 if not compatible +# @function check_mod_version_compatibility +# @description Check if a specific mod supports a specific Minecraft version. +# Lightweight version check that doesn't add mods to arrays. +# Uses multi-stage version matching for flexible compatibility. +# @param $1 - mod_id: Mod ID (Modrinth project ID or CurseForge project ID) +# @param $2 - platform: "modrinth" or "curseforge" +# @param $3 - mc_version: Minecraft version to check (e.g., "1.21.3") +# @return 0 if compatible, 1 if not compatible or API error check_mod_version_compatibility() { local mod_id="$1" local platform="$2" local mc_version="$3" - + if [[ "$platform" == "modrinth" ]]; then # Check Modrinth mod for version compatibility using same logic as check_modrinth_mod local api_url="https://api.modrinth.com/v2/project/$mod_id/version" @@ -87,41 +132,41 @@ check_mod_version_compatibility() { if [[ -z "$tmp_body" ]]; then return 1 fi - + # Fetch version data from Modrinth API local http_code http_code=$(curl -s -L -w "%{http_code}" -o "$tmp_body" "$api_url") local version_json version_json=$(cat "$tmp_body") rm "$tmp_body" - + # Validate API response if [[ "$http_code" != "200" ]] || ! printf "%s" "$version_json" | jq -e . > /dev/null 2>&1; then return 1 fi - + # Use the same multi-stage version matching logic as check_modrinth_mod local file_url="" - + # STAGE 1: Try exact version match with Fabric loader requirement file_url=$(printf "%s" "$version_json" | jq -r --arg v "$mc_version" '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .files[] | select(.primary == true) | .url' 2>/dev/null | head -n1) - + # STAGE 2: Strict fallback to major.minor version if exact match failed if [[ -z "$file_url" || "$file_url" == "null" ]]; then local mc_major_minor mc_major_minor=$(echo "$mc_version" | grep -oE '^[0-9]+\.[0-9]+') local requested_patch requested_patch=$(echo "$mc_version" | grep -oE '^[0-9]+\.[0-9]+\.([0-9]+)' | grep -oE '[0-9]+$') - + # Get all available game versions for this mod to validate fallback logic local all_game_versions all_game_versions=$(printf "%s" "$version_json" | jq -r '.[] | select(.loaders[] == "fabric") | .game_versions[]' 2>/dev/null | sort -u) - + # Check if any patch versions or standalone major.minor exist for this series local has_patch_versions=false local has_standalone_major_minor=false local highest_patch_version=0 - + while IFS= read -r version; do if [[ "$version" =~ ^${mc_major_minor//./\.}\.([0-9]+)$ ]]; then has_patch_versions=true @@ -133,29 +178,29 @@ check_mod_version_compatibility() { has_standalone_major_minor=true fi done <<< "$all_game_versions" - + # Apply strict fallback rules: # 1. If we have patch versions AND the requested patch > highest available patch, block fallback # 2. Only allow fallback to major.minor if no patch versions exist OR standalone major.minor exists local allow_fallback=true - + if [[ $has_patch_versions == true && -n "$requested_patch" ]]; then if [[ $requested_patch -gt $highest_patch_version ]]; then allow_fallback=false fi fi - + # Only proceed with fallback if allowed if [[ $allow_fallback == true ]]; then # Try exact major.minor (e.g., "1.21") file_url=$(printf "%s" "$version_json" | jq -r --arg v "$mc_major_minor" '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .files[] | select(.primary == true) | .url' 2>/dev/null | head -n1) - - # Try wildcard version format (e.g., "1.21.x") + + # Try wildcard version format (e.g., "1.21.x") if [[ -z "$file_url" || "$file_url" == "null" ]]; then local mc_major_minor_x="$mc_major_minor.x" file_url=$(printf "%s" "$version_json" | jq -r --arg v "$mc_major_minor_x" '.[] | select(.game_versions[] == $v and (.loaders[] == "fabric")) | .files[] | select(.primary == true) | .url' 2>/dev/null | head -n1) fi - + # Try zero-padded version format (e.g., "1.21.0") if [[ -z "$file_url" || "$file_url" == "null" ]]; then local mc_major_minor_0="$mc_major_minor.0" @@ -163,18 +208,18 @@ check_mod_version_compatibility() { fi fi fi - + # Return success if we found a compatible version if [[ -n "$file_url" && "$file_url" != "null" ]]; then return 0 # Compatible fi - + elif [[ "$platform" == "curseforge" ]]; then # Check CurseForge mod for version compatibility using same logic as check_curseforge_mod # First get the encrypted API token local token_url="https://raw.githubusercontent.com/FlyingEwok/MinecraftSplitscreenSteamdeck/main/token.enc" local encrypted_token_file=$(mktemp) - + if command -v curl >/dev/null 2>&1; then curl -s -L -o "$encrypted_token_file" "$token_url" 2>/dev/null elif command -v wget >/dev/null 2>&1; then @@ -183,17 +228,17 @@ check_mod_version_compatibility() { rm -f "$encrypted_token_file" return 1 fi - + # Decrypt the API token local fixed_passphrase="MinecraftSplitscreenSteamDeck2025" local cf_api_key cf_api_key=$(openssl enc -aes-256-cbc -d -a -pbkdf2 -pass pass:"$fixed_passphrase" -in "$encrypted_token_file" 2>/dev/null) rm -f "$encrypted_token_file" - + if [[ -z "$cf_api_key" ]]; then return 1 # Can't get API key fi - + # Query CurseForge API with Fabric loader filter local cf_api_url="https://api.curseforge.com/v1/mods/$mod_id/files?modLoaderType=4" local tmp_body @@ -201,25 +246,25 @@ check_mod_version_compatibility() { if [[ -z "$tmp_body" ]]; then return 1 fi - + # Make authenticated API request local http_code http_code=$(curl -s -L -w "%{http_code}" -o "$tmp_body" -H "x-api-key: $cf_api_key" "$cf_api_url") local version_json version_json=$(cat "$tmp_body") rm "$tmp_body" - + # Validate API response if [[ "$http_code" != "200" ]] || ! printf "%s" "$version_json" | jq -e . > /dev/null 2>&1; then return 1 fi - + # Version compatibility checking using same logic as check_curseforge_mod local mc_major_minor mc_major_minor=$(echo "$mc_version" | grep -oE '^[0-9]+\.[0-9]+') local mc_major_minor_x="$mc_major_minor.x" local mc_major_minor_0="$mc_major_minor.0" - + # CurseForge-specific jq filter for version matching (same as in check_curseforge_mod) local jq_filter=' .data[] @@ -231,7 +276,7 @@ check_mod_version_compatibility() { ) | .downloadUrl ' - + local jq_result jq_result=$(printf "%s" "$version_json" | jq -r \ --arg mc_version "$mc_version" \ @@ -239,21 +284,30 @@ check_mod_version_compatibility() { --arg mc_major_minor_x "$mc_major_minor_x" \ --arg mc_major_minor_0 "$mc_major_minor_0" \ "$jq_filter" 2>/dev/null | head -n1) - + # Return success if we found a compatible version if [[ -n "$jq_result" && "$jq_result" != "null" ]]; then return 0 # Compatible fi fi - + return 1 # Not compatible } -# Add fallback dependencies for critical mods when API calls fail +# ============================================================================= +# FALLBACK DATA +# ============================================================================= + +# @function fallback_dependencies +# @description Provide hardcoded dependency information for critical mods when API calls fail. +# @param $1 - mod_id: The mod ID to look up +# @param $2 - platform: "modrinth" or "curseforge" +# @stdout Space-separated list of dependency mod IDs, or empty string +# @return 0 always fallback_dependencies() { local mod_id="$1" local platform="$2" - + case "$platform:$mod_id" in "modrinth:P7dR8mSH") # Fabric API echo "" @@ -273,15 +327,23 @@ fallback_dependencies() { esac } -# get_minecraft_version: Get target Minecraft version with intelligent compatibility checking -# Only offers versions that support both Controllable and Splitscreen Support mods +# ============================================================================= +# USER INTERACTION +# ============================================================================= + +# @function get_minecraft_version +# @description Interactive Minecraft version selection with intelligent compatibility checking. +# Only offers versions that support both Controllable and Splitscreen Support mods. +# @global MC_VERSION - (output) Set to selected Minecraft version +# @stdin User input from /dev/tty (for curl | bash compatibility) +# @return 0 on success, exits on failure to determine versions get_minecraft_version() { print_header "🎯 MINECRAFT VERSION SELECTION" - + # Get list of supported Minecraft versions local -a supported_versions readarray -t supported_versions <<< "$(get_supported_minecraft_versions)" - + # Filter out any empty entries local -a clean_versions=() for version in "${supported_versions[@]}"; do @@ -290,15 +352,15 @@ get_minecraft_version() { fi done supported_versions=("${clean_versions[@]}") - + if [[ ${#supported_versions[@]} -eq 0 ]]; then print_error "Could not determine supported Minecraft versions. Please check your internet connection and try again." exit 1 fi - + # Display supported versions to user echo "🎮 Available Minecraft versions (with full splitscreen mod support):" - + local counter=1 for version in "${supported_versions[@]}"; do if [[ $counter -le 10 ]]; then # Show top 10 most recent supported versions @@ -306,11 +368,11 @@ get_minecraft_version() { ((counter++)) fi done - + echo "These versions have been verified to support both essential splitscreen mods:" - echo " ✅ Controllable (controller support)" + echo " ✅ Controllable (controller support)" echo " ✅ Splitscreen Support (split-screen functionality)" - + # Get user choice local latest_supported="${supported_versions[0]}" echo "Enter your choice:" @@ -318,24 +380,26 @@ get_minecraft_version() { echo " [Enter] = Use latest supported version ($latest_supported) [RECOMMENDED]" echo " custom = Enter a custom version (may not have full mod support)" echo " Or directly type a Minecraft version (e.g., 1.21.3)" - + local user_choice - read -p "Your choice [latest]: " user_choice - + # Use centralized prompt function that handles curl | bash piping + user_choice=$(prompt_user "Your choice [latest]: " "latest" 60) + if [[ -z "$user_choice" || "$user_choice" == "latest" ]]; then # Use latest supported version MC_VERSION="$latest_supported" print_success "Using latest supported version: $MC_VERSION" - + elif [[ "$user_choice" =~ ^[0-9]+$ ]] && [[ $user_choice -ge 1 && $user_choice -le ${#supported_versions[@]} ]]; then # User selected a number from the list local selected_index=$((user_choice - 1)) MC_VERSION="${supported_versions[$selected_index]}" print_success "Using selected version: $MC_VERSION" - + elif [[ "$user_choice" == "custom" ]]; then # User wants to enter a custom version - read -p "Enter custom Minecraft version (e.g., 1.21.3): " custom_version + local custom_version + custom_version=$(prompt_user "Enter custom Minecraft version (e.g., 1.21.3): " "" 60) if [[ -n "$custom_version" ]]; then MC_VERSION="$custom_version" print_warning "Using custom version: $MC_VERSION" @@ -345,11 +409,11 @@ get_minecraft_version() { print_warning "No version entered, using latest supported: $latest_supported" MC_VERSION="$latest_supported" fi - + elif [[ "$user_choice" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then # User directly entered a version number (e.g., 1.21.3, 1.21, etc.) MC_VERSION="$user_choice" - + # Check if it's in the supported list local is_supported=false for supported_ver in "${supported_versions[@]}"; do @@ -358,7 +422,7 @@ get_minecraft_version() { break fi done - + if [[ "$is_supported" == true ]]; then print_success "Using directly entered supported version: $MC_VERSION" else @@ -366,7 +430,7 @@ get_minecraft_version() { print_warning "⚠️ This version may not support all required splitscreen mods!" print_info "If installation fails, try using a supported version from the list above." fi - + else # Invalid input, use latest supported print_warning "Invalid choice, using latest supported version: $latest_supported" @@ -375,19 +439,26 @@ get_minecraft_version() { print_info "Selected Minecraft version: $MC_VERSION" } -# get_fabric_version: Fetch the latest Fabric loader version from official API -# Fabric loader provides the mod loading framework for Minecraft +# ============================================================================= +# FABRIC VERSION DETECTION +# ============================================================================= + +# @function get_fabric_version +# @description Fetch the latest Fabric loader version from official Fabric Meta API. +# Fabric loader provides the mod loading framework for Minecraft. +# @global FABRIC_VERSION - (output) Set to detected Fabric version +# @return 0 always (uses fallback on API failure) get_fabric_version() { print_progress "Detecting latest Fabric loader version..." - + # Query Fabric Meta API for the latest loader version FABRIC_VERSION=$(curl -s "https://meta.fabricmc.net/v2/versions/loader" | jq -r '.[0].version' 2>/dev/null) - + # Fallback to known stable version if API call fails if [[ -z "$FABRIC_VERSION" || "$FABRIC_VERSION" == "null" ]]; then print_warning "Could not detect latest Fabric version, using fallback" FABRIC_VERSION="0.16.9" # Known stable version that works with most mods fi - + print_success "Using Fabric loader version: $FABRIC_VERSION" }