#!/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. # # Functions provided: # - create_instances: Main function to create 4 splitscreen instances # - install_fabric_and_mods: Install Fabric loader and mods for an instance # # ============================================================================= # 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 create_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=() # Ensure instances directory exists mkdir -p "$TARGET_DIR/instances" # 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 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 else 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" \ --mc-version "$MC_VERSION" \ --group "Splitscreen" \ --loader "fabric" 2>/dev/null; then cli_success=true print_success "Created with Fabric loader" # Try without loader specification elif "$prism_exec" --cli create-instance \ --name "$instance_name" \ --mc-version "$MC_VERSION" \ --group "Splitscreen" 2>/dev/null; then cli_success=true print_success "Created without specific loader" # Try basic creation with minimal parameters elif "$prism_exec" --cli create-instance \ --name "$instance_name" \ --mc-version "$MC_VERSION" 2>/dev/null; then cli_success=true print_success "Created with minimal parameters" 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" # 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" < "$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 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 # FINAL_MOD_INDEXES contains indices of mods that passed compatibility checking for idx in "${FINAL_MOD_INDEXES[@]}"; do local mod_url="${MOD_URLS[$idx]}" 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" else echo " ❌ curl returned empty file" fi else echo " ❌ curl failed" fi elif command -v wget >/dev/null 2>&1; then echo " Trying wget for $mod_name..." if wget -q -O "$temp_resolve_file" --timeout=15 "$versions_url" 2>/dev/null; then 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" else echo " ❌ wget returned empty file" fi else 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 " 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 " 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" fi # Create empty file to indicate the attempt was made touch "$debug_file" echo " Empty debug file created at: $debug_file" fi if [[ -n "$resolve_data" && "$resolve_data" != "[]" && "$resolve_data" != *"\"error\""* ]]; then 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 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" # 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)'}" # 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" 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)'}" fi # Try limited previous patch version (more restrictive than prefix matching) if [[ -z "$mod_url" || "$mod_url" == "null" ]]; then local mc_patch_version mc_patch_version=$(echo "$MC_VERSION" | grep -oE '^[0-9]+\.[0-9]+\.([0-9]+)' | grep -oE '[0-9]+$') if [[ -n "$mc_patch_version" && $mc_patch_version -gt 0 ]]; then # 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" 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)'}" 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)" 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)'}" fi 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 local is_required=false for req in "${REQUIRED_SPLITSCREEN_MODS[@]}"; do if [[ "$mod_name" == "$req"* ]]; then is_required=true break fi done if [[ "$is_required" == true ]]; then 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." 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" 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" if wget -O "$mod_file" "$mod_url" >/dev/null 2>&1; then print_success "Success: $mod_name" else print_warning "Failed: $mod_name" MISSING_MODS+=("$mod_name") # Track download failures for summary fi done 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" 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" else print_error "Failed to copy mods from instance 1" fi else 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)" else 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" else print_info " → Creating default splitscreen-optimized options.txt" mkdir -p "$(dirname "$options_file")" cat > "$options_file" < "$instance_dir/mmc-pack.json" <