35302aa7c7
* Print parent folder of mod variants Because some mods have multiple variants with the same name. As is, it is impossible to tell them apart in the selection. This commit changes the selection to also print the parent folder of the variants. * remove the name of the mod zip/dir itself because it not necessary --------- Co-authored-by: v4n <105587619+v4n00@users.noreply.github.com>
1982 lines
63 KiB
Bash
Executable File
1982 lines
63 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
VERSION="0.5.3"
|
|
|
|
# --- Globals ---
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
ORANGE='\033[0;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
MODS_DIR=""
|
|
DB_FILE=""
|
|
MODPACKS_DIR=""
|
|
MODPACKS_DB_FILE=""
|
|
|
|
H2CONFIG_DIR="${HOME}/.config/h2mm"
|
|
H2PATH_FILE="${H2CONFIG_DIR}/h2path"
|
|
API_KEY_FILE="${H2CONFIG_DIR}/api_key"
|
|
UPDATES_AVAILABLE_FILE="${H2CONFIG_DIR}/updates_available"
|
|
LAST_CHECKED_UPDATE_FILE="${H2CONFIG_DIR}/last_update"
|
|
BACKUPS_DIR="${H2CONFIG_DIR}/backups"
|
|
|
|
VERSION_URL="https://raw.githubusercontent.com/v4n00/h2mm-cli/refs/heads/master/version"
|
|
|
|
breaking_changes_patches=(
|
|
["2"]='sed -i "s/^\([0-9]\+\),/\1,ENABLED,/" "$1/mods.csv"'
|
|
["3"]='sed -i "1 i\\3" "$1/mods.csv"'
|
|
["4"]='tmp_file=$(mktemp) && awk '\''BEGIN {FS=OFS=","} NR==1 {print 4; next} {print NR-1, $2, $3, $4, $5}'\'' "$1/mods.csv" > "$tmp_file" && tee "$1/mods.csv" < "$tmp_file" > /dev/null && rm "$tmp_file"'
|
|
["5"]='sed -i "s/^\([0-9]\+\),\(.*\),\(.*\),\(.*\)/\1,\2,\3,,,,\4/" "$1/mods.csv"; sed -i "1 s/4/5/" "$1/mods.csv"'
|
|
)
|
|
|
|
# --- Utility Functions ---
|
|
|
|
function substitute_home() {
|
|
echo "${1/#\~/$HOME}"
|
|
}
|
|
|
|
function get_version_major() {
|
|
echo "$1" | awk -F. '{print $2}'
|
|
}
|
|
|
|
function get_filename_without_path() {
|
|
echo "$1" | awk -F/ '{print $NF}'
|
|
}
|
|
|
|
function get_patch_number() {
|
|
echo "$1" | grep -oP '(?<=patch_)\d+'
|
|
}
|
|
|
|
function get_basename() {
|
|
get_filename_without_path "$1" | sed -E 's/\.+.*//'
|
|
}
|
|
|
|
function get_extension() {
|
|
get_filename_without_path "$1" | sed -E 's/.*patch_[0-9]+//'
|
|
}
|
|
|
|
function get_files_by_entry_from_db() {
|
|
echo "$1" | cut -d',' -f7- | tr ',' ' ' | head -1
|
|
}
|
|
|
|
function get_name_by_entry_from_db() {
|
|
echo "$1" | awk -F, '{print $3}'
|
|
}
|
|
|
|
function get_index_by_entry_from_db() {
|
|
echo "$1" | awk -F, '{print $1}'
|
|
}
|
|
|
|
function get_nexus_mod_id_by_entry_from_db() {
|
|
echo "$1" | awk -F, '{print $4}'
|
|
}
|
|
|
|
function get_nexus_mod_file_id_by_entry_from_db() {
|
|
echo "$1" | awk -F, '{print $5}'
|
|
}
|
|
|
|
function get_nexus_mod_version_by_entry_from_db() {
|
|
echo "$1" | awk -F, '{print $6}'
|
|
}
|
|
|
|
function disable_all_modpacks() {
|
|
sed -i 's/ENABLED/DISABLED/' "$MODPACKS_DB_FILE"
|
|
}
|
|
|
|
function get_entry_from_db_by_nexus_mod_id() {
|
|
echo "$(awk -F, -v id="$1" 'NR > 1 && $4 == id {print $0}' "$DB_FILE")"
|
|
}
|
|
|
|
function parse_help_no_arguments() {
|
|
display_help="$1"
|
|
[[ "$2" == "--help" || "$2" == "-h" ]] && { $display_help; exit 0; }
|
|
}
|
|
|
|
function parse_help_has_arguments() {
|
|
display_help="$1"
|
|
[[ $# -eq 1 || "$2" == "--help" || "$2" == "-h" ]] && { $display_help; exit 0; }
|
|
}
|
|
|
|
function log() {
|
|
local type="$1"
|
|
shift
|
|
case "$type" in
|
|
INFO)
|
|
[[ "$silent" == "true" ]] && return
|
|
echo -e "$*" >&2
|
|
;;
|
|
WARNING)
|
|
[[ "$silent" == "true" ]] && return
|
|
echo -e "${ORANGE}[!]${NC} $*" >&2
|
|
;;
|
|
ERROR)
|
|
echo -e "${RED}[ERROR]${NC} $*" >&2
|
|
;;
|
|
PROMPT)
|
|
echo -ne "$*" >&2
|
|
;;
|
|
*)
|
|
echo -e "$*" >&2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# --- Functions ---
|
|
|
|
function check_nexus_mod_arguments() {
|
|
if [[ -n "$nexus_mod_id" || -n "$nexus_mod_file_id" || -n "$nexus_mod_version" ]]; then
|
|
if [[ -z "$nexus_mod_id" || -z "$nexus_mod_file_id" || -z "$nexus_mod_version" ]]; then
|
|
log ERROR "Nexus mod ID, file ID and version are required when specifing at least one of them."
|
|
exit 1
|
|
fi
|
|
has_nexus_mod_arguments=true
|
|
fi
|
|
}
|
|
|
|
function get_nexus_api_key() {
|
|
[[ ! -f "$API_KEY_FILE" ]] && { log ERROR "Nexus API key file not found. Please run h2mm nexus-setup."; exit 1; }
|
|
api_key=$(cat "$API_KEY_FILE")
|
|
[[ -z "$api_key" ]] && { log ERROR "Nexus API key is empty. Please run h2mm nexus-setup."; exit 1; }
|
|
}
|
|
|
|
function remove_disabled_prefix() {
|
|
local disabledFile="$1"
|
|
while [[ "$disabledFile" == disabled_* ]]; do
|
|
disabledFile=$(echo "$disabledFile" | sed 's/^disabled_//')
|
|
done
|
|
echo "$disabledFile"
|
|
}
|
|
|
|
function get_mod_name_and_index() {
|
|
# if calling install multiple times in the same call of h2mm, the mod_index needs to be reset if it was -1
|
|
[[ $mod_index -eq -1 ]] && unset mod_index
|
|
|
|
if [[ -n "$mod_index" ]]; then # if mod index exists
|
|
entry=$(grep "^${mod_index}," "$DB_FILE")
|
|
mod_name=$(get_name_by_entry_from_db "$entry")
|
|
elif [[ -n "$mod_name" ]]; then # if mod name exists
|
|
entry=$(grep -F ",$mod_name," "$DB_FILE")
|
|
mod_index=$(echo "$entry" | awk -F, '{print $1}' | head -1)
|
|
fi
|
|
|
|
if [[ -z "$entry" || -z "$mod_index" || -z "$mod_name" ]]; then
|
|
[[ "$1" != "--do-not-exit" ]] && { log ERROR "Mod not found."; exit 1; }
|
|
mod_index=-1
|
|
fi
|
|
|
|
status=$(echo "$entry" | awk -F, '{print $2}')
|
|
}
|
|
|
|
function get_modpack_name_and_index() {
|
|
if [[ -n "$modpack_index" ]]; then # if modpack index exists
|
|
entry=$(grep "^${modpack_index}," "$MODPACKS_DB_FILE")
|
|
modpack_name=$(get_name_by_entry_from_db "$entry")
|
|
elif [[ -n "$modpack_name" ]]; then # if modpack name exists
|
|
entry=$(grep ",$modpack_name$" "$MODPACKS_DB_FILE")
|
|
modpack_index=$(echo "$entry" | awk -F, '{print $1}' | head -1)
|
|
fi
|
|
|
|
[[ -z "$entry" || -z "$modpack_index" || -z "$modpack_name" ]] && { log ERROR "Modpack not found."; exit 1; }
|
|
}
|
|
|
|
function find_game_directory() {
|
|
local search_dir="${HOME}"
|
|
local target_dir="steamapps/common/Helldivers\ 2/data"
|
|
|
|
# check if path is saved
|
|
saved_dir=$(cat "$H2PATH_FILE")
|
|
[[ -d "$saved_dir" ]] && { echo "$saved_dir"; return; }
|
|
|
|
# first time setup, or directory is not valid anymore
|
|
log INFO "Searching for the Helldivers 2 data directory... (10 seconds timeout)"
|
|
game_dir=$(timeout 10 find "$search_dir" -type d -path "*/$target_dir" 2>/dev/null | head -n 1)
|
|
|
|
if [[ -z "$game_dir" ]]; then
|
|
# if not found, ask user for the directory
|
|
log INFO "Could not find the Helldivers 2 data directory automatically."
|
|
log PROMPT "Please enter the path to the Helldivers 2 data directory: "
|
|
IFS= read -e game_dir; unset IFS
|
|
game_dir="$(substitute_home "$game_dir")"
|
|
|
|
[[ ! -d "$game_dir" ]] && { log ERROR "Provided path is not a valid directory."; exit 1; }
|
|
else
|
|
# confirm with the user that the directory is ok
|
|
log INFO "Found Helldivers 2 data directory: $game_dir"
|
|
log PROMPT "Is this the correct directory? (Y/n): "
|
|
read confirm
|
|
|
|
if [[ "$confirm" != "y" && "$confirm" != "Y" && "$confirm" != "" ]]; then
|
|
log PROMPT "Please enter the path to the Helldivers 2 data directory: "
|
|
IFS= read -e game_dir; unset IFS
|
|
game_dir="$(substitute_home "$game_dir")"
|
|
|
|
[[ ! -d "$game_dir" ]] && { log ERROR "Provided path is not a valid directory."; exit 1; }
|
|
fi
|
|
fi
|
|
|
|
# save path
|
|
echo "$game_dir" > "$H2PATH_FILE"
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not save game directory."; exit 1; }
|
|
log INFO "Game directory ${GREEN}saved${NC}: $game_dir"
|
|
|
|
# return the directory
|
|
echo "$game_dir"
|
|
}
|
|
|
|
function initialize_config_directory() {
|
|
mkdir -p "$H2CONFIG_DIR"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not create config directory ($H2CONFIG_DIR)."; exit 1; }
|
|
|
|
mkdir -p "$BACKUPS_DIR"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not create backups directory ($BACKUPS_DIR)."; exit 1; }
|
|
|
|
if [[ ! -f "$H2PATH_FILE" ]]; then
|
|
touch "$H2PATH_FILE"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not create path file."; exit 1; }
|
|
fi
|
|
|
|
if [[ ! -f "$LAST_CHECKED_UPDATE_FILE" ]]; then
|
|
touch "$LAST_CHECKED_UPDATE_FILE"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not create last checked update file."; exit 1; }
|
|
fi
|
|
|
|
if [[ ! -f "$UPDATES_AVAILABLE_FILE" ]]; then
|
|
touch "$UPDATES_AVAILABLE_FILE"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not create updates available file."; exit 1; }
|
|
fi
|
|
}
|
|
|
|
function initialize_directories() {
|
|
initialize_config_directory
|
|
MODS_DIR=$(find_game_directory)
|
|
DB_FILE="$MODS_DIR/mods.csv"
|
|
|
|
if [[ ! -f "$DB_FILE" ]]; then
|
|
touch "$DB_FILE"
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not create database file."; exit 1; }
|
|
|
|
echo "$VERSION" | awk -F. '{print $2}' > "$DB_FILE"
|
|
log INFO "Database file ${GREEN}created${NC}: $DB_FILE"
|
|
fi
|
|
}
|
|
|
|
function initialize_modpack_directories() {
|
|
MODPACKS_DIR="$MODS_DIR/modpacks"
|
|
MODPACKS_DB_FILE="$MODPACKS_DIR/modpacks.csv"
|
|
|
|
if [[ ! -d "$MODPACKS_DIR" || ! -f "$MODPACKS_DB_FILE" ]]; then
|
|
mkdir -p "$MODPACKS_DIR"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not create modpacks directory."; exit 1; }
|
|
|
|
touch "$MODPACKS_DB_FILE"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not create modpacks database file."; exit 1; }
|
|
|
|
echo "$VERSION" | awk -F. '{print $2}' > "$MODPACKS_DB_FILE"
|
|
log INFO "Modpacks directory and file ${GREEN}created${NC}: $MODPACKS_DB_FILE"
|
|
fi
|
|
}
|
|
|
|
# --- Help Functions ---
|
|
|
|
function display_help_main() {
|
|
cat << EOF
|
|
Helldivers 2 Mod Manager v${VERSION}
|
|
Usage: h2mm [OPTION] COMMAND
|
|
Commands:
|
|
i, install Install a mod by the file provided (directory, zip, patch).
|
|
u, uninstall Uninstall a mod.
|
|
l, list List all installed mods.
|
|
e, enable Enable a mod.
|
|
d, disable Disable a mod.
|
|
o, order Change load order of a mod.
|
|
ex, export Export installed mods to a zip file.
|
|
im, import Import mods from a zip file.
|
|
mc, modpack-create Create a modpack from the currently installed mods.
|
|
ms, modpack-switch Switch to a modpack.
|
|
ml, modpack-list List all installed modpacks.
|
|
mc, modpack-delete Delete a modpack.
|
|
mo, modpack-overwrite Overwrite a modpack.
|
|
mr, modpack-reset Reset all installed modpacks.
|
|
ns, nexus-setup Setup Nexus Mods integration.
|
|
nu, nexus-update Start Nexus mods update process.
|
|
up, update Update h2mm to the latest version.
|
|
r, reset Reset all installed mods.
|
|
help Display this help message.
|
|
For more information on usage, use h2mm <COMMAND> --help.
|
|
Usage:
|
|
h2mm install /path/to/mod.zip
|
|
h2mm uninstall -n "Example mod"
|
|
h2mm list
|
|
EOF
|
|
}
|
|
|
|
function display_help_install() {
|
|
cat << EOF
|
|
Usage: h2mm install [OPTIONS] <MOD_FILES|MOD_DIRECTORIES|MOD_ZIPS>
|
|
Install a mod with any combination of mod files, directories, and zip files.
|
|
Options:
|
|
-n "MOD_NAME" Name of the mod.
|
|
MOD_ZIPS Zip file(s) containing mod files.
|
|
MOD_FILES Mod file(s), accepts wildcards.
|
|
MOD_DIRECTORIES Directory/directories containing mod files.
|
|
Example:
|
|
h2mm install /path/to/mod.zip
|
|
h2mm install /path/to/mod/files
|
|
h2mm install /path/to/mod.zip /path/to/mod2.zip /path/to/mod/files
|
|
h2mm install mod.patch_0 mod.patch_0.stream -n "Example mod"
|
|
EOF
|
|
}
|
|
|
|
function display_help_uninstall() {
|
|
cat << EOF
|
|
Usage: h2mm uninstall [OPTIONS] <"MOD_NAME"|MOD_INDEX>
|
|
Uninstall a mod by name or index.
|
|
Options:
|
|
-n "MOD_NAME" Name of the mod to uninstall.
|
|
-i MOD_INDEX Index of the mod to uninstall.
|
|
Usage:
|
|
h2mm uninstall -n "Example mod"
|
|
h2mm uninstall -i 3
|
|
EOF
|
|
}
|
|
|
|
function display_help_enable() {
|
|
cat << EOF
|
|
Usage: h2mm enable [OPTIONS] <"MOD_NAME"|MOD_INDEX>
|
|
Enable a mod by name or index.
|
|
Options:
|
|
-n "MOD_NAME" Name of the mod to enable.
|
|
-i MOD_INDEX Index of the mod to enable.
|
|
Usage:
|
|
h2mm enable -n "Example mod"
|
|
h2mm enable -i 3
|
|
EOF
|
|
}
|
|
|
|
function display_help_disable() {
|
|
cat << EOF
|
|
Usage: h2mm disable [OPTIONS] <"MOD_NAME"|MOD_INDEX>
|
|
Disable a mod by name or index.
|
|
Options:
|
|
-n "MOD_NAME" Name of the mod to disable.
|
|
-i MOD_INDEX Index of the mod to disable.
|
|
Usage:
|
|
h2mm disable -n "Example mod"
|
|
h2mm disable -i 3
|
|
EOF
|
|
}
|
|
|
|
function display_help_list() {
|
|
cat << EOF
|
|
Usage: h2mm list
|
|
Database of mods is stored in Steam/steamapps/common/Helldivers\ 2/data/mods.csv
|
|
You can rename, delete, or edit this file to manage mods manually.
|
|
Options:
|
|
-v Verbose mode.
|
|
EOF
|
|
}
|
|
|
|
function display_help_reset() {
|
|
cat << EOF
|
|
Usage: h2mm reset
|
|
Reset all installed mods.
|
|
Deletes all installed mods/modpacks and the database file.
|
|
Database of mods is stored in Steam/steamapps/common/Helldivers 2/data/mods.csv, along with the mods.
|
|
EOF
|
|
}
|
|
|
|
function display_help_export() {
|
|
cat << EOF
|
|
Usage: h2mm export
|
|
Export installed mods, modpacks and database to a zip file (in h2mm format - archive with csv) in the current working directory.
|
|
EOF
|
|
}
|
|
|
|
function display_help_import() {
|
|
cat << EOF
|
|
Usage: h2mm import <ARCHIVE_FILE>
|
|
Import mods, modpacks and database from an archive file (coming from h2mm).
|
|
EOF
|
|
}
|
|
|
|
function display_help_order() {
|
|
cat << EOF
|
|
Usage: h2mm order [OPTIONS] <"MOD_NAME"|MOD_INDEX> <NEW_INDEX>
|
|
Change order of a mod by name or index.
|
|
Options:
|
|
-i index Index of the mod to order.
|
|
-n "MOD_NAME" Name of the mod to order.
|
|
Usage:
|
|
h2mm order -n "Example mod" 6
|
|
h2mm order -i 3 6
|
|
EOF
|
|
}
|
|
|
|
function display_help_modpack_list() {
|
|
cat << EOF
|
|
Usage: h2mm modpack-list
|
|
List all installed modpacks.
|
|
Database of modpacks is stored in Steam/steamapps/common/Helldivers\ 2/data/modpacks/modpacks.csv
|
|
You can rename, delete, or edit this file to manage modpacks manually.
|
|
Options:
|
|
-v Verbose mode.
|
|
EOF
|
|
}
|
|
|
|
function display_help_modpack_create() {
|
|
cat << EOF
|
|
Usage: h2mm modpack-create -n "MODPACK_NAME"
|
|
Create a modpack from a range of mods specified after command is called.
|
|
EOF
|
|
}
|
|
|
|
function display_help_modpack_switch() {
|
|
cat << EOF
|
|
Usage: h2mm modpack-switch [OPTIONS] <"MODPACK_NAME"|MODPACK_INDEX>
|
|
Switch to a modpack by name or index.
|
|
Options:
|
|
-n "MODPACK_NAME" Name of the modpack to switch to.
|
|
-i index Index of the modpack to switch to.
|
|
EOF
|
|
}
|
|
|
|
function display_help_modpack_reset() {
|
|
cat << EOF
|
|
Usage: h2mm modpack-reset
|
|
Reset all installed modpacks.
|
|
Deletes all installed modpacks and the database file.
|
|
Database of modpacks is stored in Steam/steamapps/common/Helldivers\ 2/data/modpacks/modpacks.csv, along with the modpacks.
|
|
EOF
|
|
}
|
|
|
|
function display_help_modpack_delete() {
|
|
cat << EOF
|
|
Usage: h2mm modpack-delete [OPTIONS] <"MODPACK_NAME"|MODPACK_INDEX>
|
|
Delete a modpack by name or index.
|
|
Options:
|
|
-n "MODPACK_NAME" Name of the modpack to delete.
|
|
-i index Index of the modpack to delete.
|
|
EOF
|
|
}
|
|
|
|
function display_help_modpack_overwrite() {
|
|
cat << EOF
|
|
Usage: h2mm modpack-overwrite [OPTIONS] <"MODPACK_NAME"|MODPACK_INDEX>
|
|
Overwrite a modpack (the mods that it uses) by name or index.
|
|
Options:
|
|
-n "MODPACK_NAME" Name of the modpack to overwrite.
|
|
-i index Index of the modpack to overwrite.
|
|
EOF
|
|
}
|
|
|
|
function display_help_nexus_setup() {
|
|
cat << EOF
|
|
Usage: h2mm nexus-setup
|
|
Setup nexusmods integration.
|
|
This will create a config file in ~/.config/h2mm/apikey with the API key.
|
|
This will create a desktop entry in ~/.local/share/applications/h2mm.desktop.
|
|
Run this again in case you change the API key or want to change the desktop entry.
|
|
EOF
|
|
}
|
|
|
|
function display_help_nexus_update() {
|
|
cat << EOF
|
|
Usage: h2mm nexus-update
|
|
Display a list of Nexus mods that have updates available.
|
|
Due to Nexus API limitations, this will prompt the user to access the Nexus website to download the updates.
|
|
EOF
|
|
}
|
|
|
|
# --- Main Functions ---
|
|
|
|
# Upgrade/downgrade logic
|
|
|
|
function downgrade_mods() {
|
|
local files="$1"
|
|
declare -A downgrades_versions
|
|
declare -A downgrades_to_apply
|
|
|
|
for file in $files; do
|
|
# save the basename for the files that were deleted into a hash table, so we can downgrade mods with greater version number
|
|
# also depending on how many patches the mod has, we need to downgrade with more versions
|
|
current_version=$(get_patch_number "$file")
|
|
base_name=$(get_basename "$file")
|
|
|
|
downgrades_versions["$base_name"]=$current_version
|
|
|
|
# count how many unique patches the mod has
|
|
[[ -z "${downgrades_to_apply["$base_name"]+unset}" ]] && downgrades_to_apply["$base_name"]=$(echo $files | tr ' ' '\n' | grep "$base_name" | sed -E "s/(.*_[0-9]+).*/\1/" | sort -u | wc -l)
|
|
done
|
|
|
|
# downgrade any necessary mods - it takes ~20 minutes to re-understand this code so I'm writing a comment here
|
|
# for each base name, find all files that have the same base name, and are greater than the current version, and downgrade them
|
|
# the downgrades_versions hash table stores the current version, so we can compare it with the version of the files
|
|
# the downgrades_to_apply hash table stores the number of patches the mod has, so we can downgrade with more versions
|
|
# for example, if we have:
|
|
# mod 1: AAA.patch_0 AAA.patch_0.stream AAA.patch_1 AAA.patch_1.stream
|
|
# mod 2: AAA.patch_2 AAA.patch_2.stream AAA.patch_3 AAA.patch_3.stream
|
|
# mod 3: AAA.patch_4 AAA.patch_4.stream AAA.patch_5 AAA.patch_5.stream
|
|
# if we remove mod with index 2, we need to downgrade mod with index 3 (which has patch version 4 and 5 > 3 (biggest last patch removed)) by 2 versions,
|
|
# the number 2 we get by counting the number of unique base names (without extensions like .stream, but with the .patch_[0-9]) in the files
|
|
for base_name in "${!downgrades_to_apply[@]}"; do
|
|
# find all files that have the same base name, and are greater than the current version, and downgrade them
|
|
IFS=$'\n' mods_to_downgrade=($(ls "$MODS_DIR/$base_name"*.patch_* 2>/dev/null | sort -V)); unset IFS
|
|
|
|
for mod in "${mods_to_downgrade[@]}"; do
|
|
mod=$(get_filename_without_path "$mod")
|
|
patch_version=$(get_patch_number "$mod")
|
|
|
|
if [[ $patch_version -gt ${downgrades_versions[$base_name]} ]]; then
|
|
new_version=$((patch_version - downgrades_to_apply["$base_name"]))
|
|
extension=$(get_extension "$mod")
|
|
|
|
new_patch="${base_name}.patch_${new_version}${extension}"
|
|
mv "$MODS_DIR/$mod" "$MODS_DIR/$new_patch"
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not downgrade mod file $mod."; exit 1; }
|
|
log INFO "Downgraded ${ORANGE}$mod${NC} to ${GREEN}\$MODS_DIR/$new_patch${NC}."
|
|
|
|
# save changes in database as well
|
|
sed -i "s/\(\b$mod\b\)/$new_patch/" "$DB_FILE"
|
|
fi
|
|
done
|
|
done
|
|
}
|
|
|
|
function upgrade_mods() {
|
|
local files="$1"
|
|
declare -A upgrade_versions
|
|
declare -A upgrades_to_apply
|
|
|
|
# opposite of downgrade_mods
|
|
for file in $files; do
|
|
# remove disabled_ prefix if it exists
|
|
file=$(remove_disabled_prefix "$file")
|
|
|
|
current_version=$(get_patch_number "$file")
|
|
base_name=$(get_basename "$file")
|
|
|
|
# basically save the lowest number, by limiting the setting of the key to the first time we see it
|
|
[[ -z "${upgrade_versions["$base_name"]+unset}" ]] && upgrade_versions["$base_name"]=$current_version
|
|
|
|
[[ -z "${upgrades_to_apply["$base_name"]+unset}" ]] && upgrades_to_apply["$base_name"]=$(echo $files | tr ' ' '\n' | grep "$base_name" | sed -E "s/(.*_[0-9]+).*/\1/" | sort -u | wc -l)
|
|
done
|
|
|
|
for base_name in "${!upgrades_to_apply[@]}"; do
|
|
IFS=$'\n' mods_to_upgrade=($(ls "$MODS_DIR/$base_name"*.patch_* 2>/dev/null | sort -rV)); unset IFS
|
|
|
|
for mod in "${mods_to_upgrade[@]}"; do
|
|
mod=$(get_filename_without_path "$mod")
|
|
patch_version=$(get_patch_number "$mod")
|
|
|
|
if [[ $patch_version -ge ${upgrade_versions[$base_name]} ]]; then
|
|
new_version=$((patch_version + upgrades_to_apply["$base_name"]))
|
|
extension=$(get_extension "$mod")
|
|
|
|
new_patch="${base_name}.patch_${new_version}${extension}"
|
|
mv "$MODS_DIR/$mod" "$MODS_DIR/$new_patch"
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not upgrade mod file $mod."; exit 1; }
|
|
log INFO "Upgraded ${ORANGE}$mod${NC} to ${GREEN}\$MODS_DIR/$new_patch${NC}."
|
|
|
|
sed -i "s/\(\b$mod\b\)/$new_patch/" "$DB_FILE"
|
|
fi
|
|
done
|
|
done
|
|
}
|
|
|
|
# Mod management
|
|
|
|
function mod_disable() {
|
|
parse_help_has_arguments display_help_disable "$@"
|
|
local mod_name=""
|
|
local mod_index=""
|
|
|
|
# parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
"-i")
|
|
[[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]] && { log ERROR "Invalid mod index."; exit 1; }
|
|
mod_index="$2"; shift 2
|
|
;;
|
|
"-n")
|
|
[[ -z "$2" ]] && { log ERROR "Mod name is required."; exit 1; }
|
|
mod_name="$2"; shift 2
|
|
;;
|
|
*)
|
|
$display_help; exit 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$mod_name" && -z "$mod_index" ]]; then
|
|
log ERROR "Mod name or index is required to disable."
|
|
exit 1
|
|
fi
|
|
|
|
# find mod files
|
|
get_mod_name_and_index
|
|
|
|
if [[ "$status" == "DISABLED" ]]; then
|
|
log ERROR "Mod $mod_name is already disabled."
|
|
exit 1
|
|
fi
|
|
|
|
# disable each mod file by adding disabled_ to the start of the filename
|
|
current_mod_files=$(get_files_by_entry_from_db "$entry")
|
|
for file in $current_mod_files; do
|
|
[[ ! -f "$MODS_DIR/$file" ]] && { log ERROR "Mod file $file does not exist."; exit 1; }
|
|
|
|
disabled_file="disabled_$file"
|
|
while [[ -f "$MODS_DIR/$disabled_file" ]]; do
|
|
disabled_file="disabled_$disabled_file"
|
|
done
|
|
|
|
mv "$MODS_DIR/$file" "$MODS_DIR/$disabled_file"
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not disable mod file $file."; exit 1; }
|
|
log INFO "Disabled ${ORANGE}$file${NC} (changed to ${GREEN}\$MODS_DIR/$disabled_file${NC})."
|
|
|
|
# save change to db
|
|
sed -i "/^$mod_index,/ s/\(\b$file\b\)/$disabled_file/" "$DB_FILE"
|
|
done
|
|
|
|
# downgrade mods with greater version number
|
|
downgrade_mods "$current_mod_files"
|
|
|
|
# update the database
|
|
sed -i "/^$mod_index,/s/ENABLED/DISABLED/" "$DB_FILE"
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not disable mod."; exit 1; }
|
|
log INFO "Mod ${GREEN}successfully${NC} disabled: $mod_name."
|
|
|
|
disable_all_modpacks
|
|
}
|
|
|
|
function mod_enable() {
|
|
parse_help_has_arguments display_help_enable "$@"
|
|
local mod_name=""
|
|
local mod_index=""
|
|
|
|
# parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
"-i")
|
|
[[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]] && { log ERROR "Invalid mod index."; exit 1; }
|
|
mod_index="$2"; shift 2
|
|
;;
|
|
"-n")
|
|
[[ -z "$2" ]] && { log ERROR "Mod name is required."; exit 1; }
|
|
mod_name="$2"; shift 2
|
|
;;
|
|
*)
|
|
$display_help; exit 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$mod_name" && -z "$mod_index" ]] && { log ERROR "Mod name or index is required to enable."; exit 1; }
|
|
|
|
# find mod files
|
|
get_mod_name_and_index
|
|
|
|
[[ "$status" == "ENABLED" ]] && { log ERROR "Mod $mod_name is already enabled."; exit 1; }
|
|
|
|
current_mod_files=$(get_files_by_entry_from_db "$entry")
|
|
|
|
# upgrade mods with lower version number
|
|
upgrade_mods "$current_mod_files"
|
|
|
|
# enable each mod file by removing disabled_ from the start of the filename
|
|
for file in $current_mod_files; do
|
|
enabled_file=$(remove_disabled_prefix "$file")
|
|
|
|
# check if the files exists
|
|
[[ -f "$MODS_DIR/$file" ]] || { log ERROR "Mod file $file does not exist."; exit 1; }
|
|
|
|
mv "$MODS_DIR/$file" "$MODS_DIR/$enabled_file"
|
|
|
|
# check if the file was moved successfully
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not enable mod file $file."; exit 1; }
|
|
log INFO "Enabled ${ORANGE}$file${NC} (changed to ${GREEN}\$MODS_DIR/$enabled_file${NC})."
|
|
|
|
# save change to db
|
|
sed -i "/^$mod_index,/ s/\(\b$file\b\)/$enabled_file/" "$DB_FILE"
|
|
done
|
|
|
|
# update the database
|
|
sed -i "/^$mod_index,/s/DISABLED/ENABLED/" "$DB_FILE"
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not enable mod."; exit 1; }
|
|
log INFO "Mod ${GREEN}successfully${NC} enabled: $mod_name."
|
|
|
|
disable_all_modpacks
|
|
}
|
|
|
|
function mod_reset() {
|
|
parse_help_no_arguments display_help_reset "$@"
|
|
local no_path_reset=false; [[ "$1" == "--no-path-reset" ]] && no_path_reset=true
|
|
|
|
log PROMPT "Are you sure you want to ${RED}reset${NC} all installed mods? (Y/n): "
|
|
read confirm
|
|
|
|
if [[ "$confirm" == "y" || "$confirm" == "Y" || "$confirm" = "" ]]; then
|
|
rm -f "$MODS_DIR"/*.patch_*
|
|
rm -f "$DB_FILE"
|
|
[[ $no_path_reset == false ]] && rm -f "$H2PATH_FILE"
|
|
log INFO "Mods ${GREEN}successfully${NC} reset."
|
|
else
|
|
log INFO "Reset cancelled."
|
|
exit 0
|
|
fi
|
|
}
|
|
|
|
function mod_install() {
|
|
parse_help_has_arguments display_help_install "$@"
|
|
local mod_name=""
|
|
local mod_dir=()
|
|
local mod_files=()
|
|
local mod_zip=()
|
|
local nexus_mod_version=""
|
|
local nexus_mod_file_id=""
|
|
local nexus_mod_id=""
|
|
local is_not_zip=false
|
|
|
|
# parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
"-n")
|
|
[[ -z "$2" ]] && { log ERROR "Mod name is required."; exit 1; }
|
|
mod_name="$2"; shift 2
|
|
;;
|
|
"--version")
|
|
[[ -z "$2" ]] && { log ERROR "Nexus mod version is required."; exit 1; }
|
|
nexus_mod_version="$2"; shift 2
|
|
;;
|
|
"--file-id")
|
|
[[ -z "$2" ]] && { log ERROR "Nexus mod file ID is required."; exit 1; }
|
|
nexus_mod_file_id="$2"; shift 2
|
|
;;
|
|
"--mod-id")
|
|
[[ -z "$2" ]] && { log ERROR "Nexus mod ID is required."; exit 1; }
|
|
nexus_mod_id="$2"; shift 2
|
|
;;
|
|
*)
|
|
if [[ "$1" == *.zip || "$1" == *.rar || "$1" == *.7z ]]; then
|
|
mod_zip+=("$1")
|
|
[[ "$1" != *.zip ]] && is_not_zip=true
|
|
elif [[ -d "$1" ]]; then
|
|
mod_dir+=("$1")
|
|
else
|
|
mod_files+=("$1")
|
|
fi
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
check_nexus_mod_arguments
|
|
|
|
# edge case when there is a combination of mod zips and directories, call function for all zips and dirs
|
|
if [[ ${mod_zip} && ${mod_dir} ]]; then
|
|
mod_install "${mod_zip[@]}"
|
|
mod_install "${mod_dir[@]}"
|
|
|
|
# reset arrays
|
|
mod_zip=()
|
|
mod_dir=()
|
|
|
|
# if there are no more arguments, exit
|
|
[[ ${#mod_files[@]} -eq 0 ]] && exit 0
|
|
fi
|
|
|
|
# if there's more than 1 zip, call recursively
|
|
while [[ ${#mod_zip[@]} -gt 1 ]]; do
|
|
mod_install "${mod_zip[0]}"
|
|
mod_zip=("${mod_zip[@]:1}")
|
|
done
|
|
|
|
# if zip, extract the zip file and pass it to mod dirs
|
|
if [[ -n "$mod_zip" ]]; then
|
|
if [[ $is_not_zip == true ]]; then
|
|
command -v unar &> /dev/null || { log ERROR "Archive in 7z/rar format could not be extracted because package \"unarchiver\" is not installed."; exit 1; }
|
|
else
|
|
command -v unzip &> /dev/null || { log ERROR "Archive in zip format could not be extracted because package \"unzip\" is not installed."; exit 1; }
|
|
fi
|
|
|
|
if [[ ! -f "$mod_zip" ]]; then
|
|
log ERROR "File $mod_zip does not exist."
|
|
log INFO "Are you sure the file exists? Check with 'ls -l'."
|
|
log INFO "If yes, check if it's written correctly, you must escape special characters like spaces and quotes."
|
|
log INFO "Simplest way to do this is to type a few letters and then press Tab to auto-complete the name."
|
|
exit 1
|
|
fi
|
|
|
|
# if the name is not specified, use the name of the directory, last sed for making nexusmods names not have numbers
|
|
if [[ -z "$mod_name" ]]; then
|
|
mod_name=$(get_filename_without_path "$mod_zip" | sed -E 's/\.zip//' | sed -E 's/\.rar//' | sed -E 's/-[0-9]+-[^a-zA-Z]*-[0-9]+$//')
|
|
fi
|
|
|
|
# mod_dir as a temporary directory
|
|
mod_dir+=$(mktemp -d)
|
|
if [[ $is_not_zip == true ]]; then
|
|
unar -q "$mod_zip" -o "$mod_dir"
|
|
else
|
|
unzip -qq "$mod_zip" -d "$mod_dir"
|
|
fi
|
|
fi
|
|
|
|
# if there's more than 1 directory, call recursively
|
|
while [[ ${#mod_dir[@]} -gt 1 ]]; do
|
|
mod_install "${mod_dir[0]}"
|
|
mod_dir=("${mod_dir[@]:1}")
|
|
done
|
|
|
|
# directory containing mod files
|
|
if [[ -n "$mod_dir" ]]; then
|
|
# verify directory exists
|
|
[[ ! -d "$mod_dir" ]] && { log ERROR "Directory $mod_dir does not exist."; exit 1; }
|
|
|
|
# read every file from the directory
|
|
readarray -d '' mod_files < <(find "$mod_dir" -type f -name "*.patch_*" -print0)
|
|
# if the name is not specified, use the name of the directory, last sed for making nexusmods names not have numbers
|
|
if [[ -z "$mod_name" ]]; then
|
|
mod_name=$(echo "$mod_dir" | sed 's:/*$::' | awk -F/ '{print $NF}' | sed -E 's/-[0-9]+-.*//')
|
|
fi
|
|
|
|
# check for mod variants and handle
|
|
# if the mod directory contains more than 1 directory, it means there are multiple variants for the mod
|
|
# prompt the user to choose which variant to install, or install multiple
|
|
readarray -d '' all_dirs < <(find "$mod_dir" -mindepth 1 -type d -print0)
|
|
|
|
# filter so that we only have dirs that have *.patch_* files inside them
|
|
filtered_dirs=()
|
|
for dir in "${all_dirs[@]}"; do
|
|
if find "$dir" -maxdepth 1 -type f -name "*.patch_*" -print -quit | grep -q .; then
|
|
filtered_dirs+=("$dir")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#filtered_dirs[@]} -gt 1 ]]; then
|
|
log INFO "Multiple mod variants found for mod ${mod_name}."
|
|
|
|
# print the variant name by display all the directories (and how they're nested)
|
|
# first, take the basename of the directory/zip we're installing from
|
|
# then, make the variant name by removing the tmp dir name from the path (if it's a zip)
|
|
# finally, take out this name, if it exists
|
|
|
|
if [[ -n "$mod_zip" ]]; then
|
|
mod_file_name="$(get_basename "$mod_zip")"
|
|
else
|
|
mod_file_name="*"
|
|
fi
|
|
|
|
for i in "${!filtered_dirs[@]}"; do
|
|
variant_name="${filtered_dirs[$i]#$mod_dir/}" # remote temp name in case its a zip
|
|
log INFO "$((i + 1)). ${variant_name#$mod_file_name/}" # remote the name of the zip/dir if it exists
|
|
done
|
|
|
|
# prompt user to choose
|
|
log PROMPT "Enter the number of the variant(s) to install (separated by space) or press Enter to install all: "
|
|
read -a variant_indices
|
|
if [[ -n "${variant_indices[0]}" ]]; then
|
|
# clear mod_files
|
|
mod_files=()
|
|
|
|
# get the files from the chosen variant
|
|
for index in "${variant_indices[@]}"; do
|
|
[[ ! "$index" =~ ^[0-9]+$ ]] && { log ERROR "Invalid variant index."; exit 1; }
|
|
[[ $index -lt 1 || $index -gt ${#filtered_dirs[@]} ]] && { log ERROR "Variant index out of range."; exit 1; }
|
|
|
|
readarray -d '' variant_files < <(find "${filtered_dirs[$((index - 1))]}" -type f -name "*.patch_*" -print0)
|
|
|
|
# update mod_name to contain the variant name
|
|
mod_name="${mod_name} [$(basename "${filtered_dirs[$((index - 1))]}")]"
|
|
|
|
# add the files to the mod_files array
|
|
mod_files+=("${variant_files[@]}")
|
|
done
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# verify minimum information required
|
|
[[ ${#mod_files[@]} -eq 0 ]] && { log ERROR "No mod files found."; exit 1; }
|
|
[[ -z "$mod_name" ]] && { log ERROR "Mod name is required."; exit 1; }
|
|
|
|
# verify mod files exist
|
|
for file in "${mod_files[@]}"; do
|
|
[[ ! -f "$file" ]] && { log ERROR "Mod file $file does not exist."; exit 1; }
|
|
done
|
|
|
|
# sanitize mod name so it doesn't contain commas
|
|
mod_name=$(echo "$mod_name" | sed 's/,//g')
|
|
|
|
# verify duplicate mod names
|
|
if [[ $has_nexus_mod_arguments == false ]]; then
|
|
get_mod_name_and_index --do-not-exit
|
|
[[ $mod_index -ne -1 ]] && { log ERROR "The mod '$mod_name' is already installed."; exit 1; }
|
|
fi
|
|
|
|
# store the target files so we can put them in the database later
|
|
target_files=()
|
|
# sort the mod files because with the below logic, the .stream and .gpu_resources files need to come after their respective patch files
|
|
IFS=$'\n' mod_files=($(printf "%s\n" "${mod_files[@]}" | sort -t. -k1,1 -k2,2n)); unset IFS
|
|
|
|
for file in "${mod_files[@]}"; do
|
|
base_name=$(get_basename "$file")
|
|
extension=$(get_extension "$file")
|
|
|
|
# count already installed patches
|
|
count=$(ls "$MODS_DIR/${base_name}.patch_"* 2>/dev/null | grep -E '([0-9]+$)' 2>/dev/null | wc -l)
|
|
|
|
# if the file has an extension, look for the last patch number and subtract 1, otherwise, the count will be wrong
|
|
target_file="${base_name}.patch_"
|
|
if [[ -n "$extension" ]]; then
|
|
target_file="${target_file}$(($count - 1))${extension}"
|
|
else
|
|
target_file="${target_file}${count}"
|
|
fi
|
|
|
|
target_files+=($target_file)
|
|
cp "$file" "$MODS_DIR/$target_file"
|
|
|
|
# verify installation worked
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not install mod file $file."; exit 1; }
|
|
log INFO "Mod file ${ORANGE}$(echo "$file" | cut -d'/' -f4-)${NC} installed at ${GREEN}\$MODS_DIR/$target_file${NC}."
|
|
done
|
|
|
|
# add entry to database
|
|
next_id=$(awk -F, 'NR > 1 {last_id = $1} END {print last_id + 1}' "$DB_FILE")
|
|
entry="$next_id,ENABLED,$mod_name,$nexus_mod_id,$nexus_mod_file_id,$nexus_mod_version,${target_files[*]}"
|
|
|
|
echo "$entry" >> "$DB_FILE"
|
|
log INFO "Mod ${GREEN}successfully${NC} installed: $mod_name."
|
|
|
|
# disable any modpack
|
|
disable_all_modpacks
|
|
}
|
|
|
|
function mod_uninstall() {
|
|
parse_help_has_arguments display_help_uninstall "$@"
|
|
local mod_name=""
|
|
local mod_index=""
|
|
|
|
# parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
"-i")
|
|
[[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]] && { log ERROR "Invalid mod index."; exit 1; }
|
|
mod_index="$2"; shift 2
|
|
;;
|
|
"-n")
|
|
[[ -z "$2" ]] && { log ERROR "Mod name is required."; exit 1; }
|
|
mod_name="$2"; shift 2
|
|
;;
|
|
*)
|
|
$display_help; exit 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$mod_name" && -z "$mod_index" ]] && { log ERROR "Mod name or index is required to uninstall."; exit 1; }
|
|
|
|
# find mod files
|
|
get_mod_name_and_index
|
|
|
|
# delete mod files
|
|
current_mod_files=$(get_files_by_entry_from_db "$entry")
|
|
|
|
for file in $current_mod_files; do
|
|
[[ ! -f "$MODS_DIR/$file" ]] && { log ERROR "Mod file $file does not exist."; exit 1; }
|
|
|
|
log INFO "Removing ${ORANGE}\$MODS_DIR/$file${NC}."
|
|
rm "$MODS_DIR/$file"
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not remove mod file $file."; exit 1; }
|
|
done
|
|
|
|
# downgrade mods with greater version number, only if the mod is enabled
|
|
[[ "$status" == "ENABLED" ]] && downgrade_mods "$current_mod_files"
|
|
|
|
# remove entry from database
|
|
sed -i "/^$mod_index,/d" "$DB_FILE"
|
|
|
|
# reindex the database
|
|
mods_to_reindex=($(awk -F, -v idx=$mod_index 'NR > 1 && $1 > idx {print $1}' "$DB_FILE"))
|
|
for idx in "${mods_to_reindex[@]}"; do
|
|
sed -i "s/^$idx,/$(($idx - 1)),/" "$DB_FILE"
|
|
done
|
|
|
|
log INFO "Mod ${GREEN}successfully${NC} uninstalled: $mod_name."
|
|
|
|
# disable any modpack
|
|
disable_all_modpacks
|
|
}
|
|
|
|
function mod_list() {
|
|
parse_help_no_arguments display_help_list "$@"
|
|
local verbose=false
|
|
|
|
# parse arguments
|
|
[[ "$1" == "--verbose" || "$1" == "-v" ]] && verbose=true
|
|
|
|
# quit if no mods are installed
|
|
[[ $(wc -l < "$DB_FILE") -le 1 ]] && { log INFO "No mods installed."; exit 0; }
|
|
|
|
# check if a modpack is enabled
|
|
modpack_entry=$(grep "ENABLED" "$MODPACKS_DB_FILE")
|
|
if [[ -n "$modpack_entry" ]]; then
|
|
modpack_name=$(echo "$modpack_entry" | awk -F, '{print $3}')
|
|
log INFO "Modpack ${GREEN}enabled${NC}: $modpack_name."
|
|
fi
|
|
|
|
log INFO "Installed mods:"
|
|
|
|
awk -v GREEN="$GREEN" -v ORANGE="$ORANGE" -v BLUE="$BLUE" -v RED="$RED" -v NC="$NC" -v verbose="$verbose" -F, 'NR > 1 {
|
|
mod_index = $1;
|
|
mod_status = $2;
|
|
mod_name = $3;
|
|
mod_id = $4;
|
|
mod_file_id = $5;
|
|
mod_version = $6;
|
|
mod_files = $7;
|
|
mod_details = "";
|
|
|
|
STATUS_COLOR = (mod_status == "DISABLED") ? RED : GREEN;
|
|
if (mod_id == "") {
|
|
mod_type = "LOCAL";
|
|
MOD_TYPE_COLOR = BLUE;
|
|
mod_version = "";
|
|
} else {
|
|
mod_type = "NEXUS";
|
|
MOD_TYPE_COLOR = ORANGE;
|
|
mod_version = "| ver: " mod_version;
|
|
}
|
|
|
|
printf "%2s. [%s%s%s/%s%s%s] %s %s\n", mod_index, STATUS_COLOR, mod_status, NC, MOD_TYPE_COLOR, mod_type, NC, mod_name, mod_version;
|
|
if (verbose == "true") {
|
|
gsub(/ /,"\n -> ", mod_files);
|
|
if (mod_id != "") printf " => Nexus mod ID: %s / Nexus file ID: %s\n", mod_id, mod_file_id;
|
|
printf " -> %s\n", mod_files;
|
|
}
|
|
}' "$DB_FILE"
|
|
}
|
|
|
|
function mod_export() {
|
|
parse_help_no_arguments display_help_export "$@"
|
|
local save_dir=${BACKUPS_DIR}
|
|
local archive_name="HD2-Mods-$(date +%Y-%m-%d_%H-%M-%S)"
|
|
local modpack_export=false
|
|
local force=false
|
|
|
|
if [[ "$1" == "--modpack" ]]; then
|
|
modpack_export=true
|
|
save_dir="$2"
|
|
archive_name="$3"
|
|
fi
|
|
|
|
[[ $(wc -l < "$DB_FILE") -le 1 ]] && { log ERROR "No mods installed."; exit 1; }
|
|
|
|
if [[ $modpack_export == false ]]; then
|
|
log PROMPT "Archive file will be saved to directory ${save_dir}. Make? (Y/n/path): "
|
|
read -e confirm
|
|
fi
|
|
|
|
[[ ! "$confirm" =~ ^[YyNn]$ && -n "$confirm" && $modpack_export == false ]] && { save_dir=$(substitute_home "$confirm"); confirm=""; }
|
|
|
|
[[ -d "$save_dir" ]] || { log ERROR "Directory $save_dir does not exist."; exit 1; }
|
|
|
|
if [[ $modpack_export == true || "$confirm" == "y" || "$confirm" == "Y" || "$confirm" = "" ]]; then
|
|
# create a temporary directory to store the mods
|
|
OUT_DIR=$(mktemp -d)
|
|
MODS_EXPORT_DIR="$OUT_DIR/Helldivers 2 Mods"
|
|
mkdir -p "$MODS_EXPORT_DIR"
|
|
cp "$DB_FILE" "$MODS_EXPORT_DIR"
|
|
|
|
# copy modpacks if modpack_export is false
|
|
if [[ $modpack_export == false ]]; then
|
|
mkdir -p "$MODS_EXPORT_DIR/modpacks"
|
|
cp "$MODPACKS_DIR"/* "$MODS_EXPORT_DIR/modpacks"
|
|
fi
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not copy mods to target directory."; exit 1; }
|
|
|
|
# copy all mod files to the export directory
|
|
for file in $(ls "$MODS_DIR/" 2>/dev/null | grep -E 'patch_.*'); do
|
|
cp "$MODS_DIR/$file" "$MODS_EXPORT_DIR"
|
|
done
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not export mods. Possibly because no mods are present."; exit 1; }
|
|
|
|
# zip up the mods with the current date and time in the name
|
|
[[ -f "$save_dir/${archive_name}.tar.gz" ]] && { log ERROR "File $save_dir/${archive_name}.tar.gz already exists."; exit 1; }
|
|
tar -czf "$save_dir/${archive_name}.tar.gz" -C "$OUT_DIR" "Helldivers 2 Mods"
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Failed to export mods."; exit 1; }
|
|
[[ "$modpack_export" == false ]] && log INFO "Mods ${GREEN}successfully${NC} exported."
|
|
fi
|
|
}
|
|
|
|
function mod_import() {
|
|
parse_help_has_arguments display_help_import "$@"
|
|
local modpack=false; [[ "$1" == "--modpack" ]] && { modpack=true; shift 1; }
|
|
|
|
# check if the file exists
|
|
[[ ! -f "$1" ]] && { log ERROR "File $1 does not exist."; exit 1; }
|
|
|
|
# reset mods before importing
|
|
[[ $modpack == false ]] && log INFO "Importing mods will ${RED}reset${NC} your mods."
|
|
mod_reset --no-path-reset
|
|
|
|
# extract in temp directory
|
|
OUT_DIR=$(mktemp -d)
|
|
tar -xzf "$1" -C "$OUT_DIR"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not import mods. Possibly because the archive is invalid."; exit 1; }
|
|
|
|
# get the mods directory
|
|
MODS_EXPORT_DIR="$OUT_DIR/Helldivers 2 Mods"
|
|
[[ ! -d "$MODS_EXPORT_DIR" ]] && { log ERROR "Could not import mods. Possibly because the archive is invalid."; exit 1; }
|
|
|
|
# fix breaking changes if the version number (from the first line) is different
|
|
db_version=$(head -n 1 "$MODS_EXPORT_DIR/mods.csv")
|
|
current_version=$(echo "$VERSION" | awk -F. '{print $2}')
|
|
|
|
[[ -z "$db_version" || ! "$db_version" =~ ^[0-9]+$ ]] && { log ERROR "Invalid version number inside mods.csv from imported archive."; exit 1; }
|
|
|
|
if [[ "$db_version" != "$current_version" ]]; then
|
|
log INFO "Import version mismatch detected: ${ORANGE}0.$db_version.x${NC} -> ${GREEN}0.$current_version.x${NC}."
|
|
|
|
# iterate from installed major number to latest major number
|
|
for ((i = db_version + 1; i <= current_version; i++)); do
|
|
if [[ -n "${breaking_changes_patches[$i]}" ]]; then
|
|
# apply breaking changes patch
|
|
eval $(echo "${breaking_changes_patches[$i]}" | sed "s:\$1:$MODS_EXPORT_DIR:g")
|
|
else
|
|
log INFO "No breaking changes for version 0.$i.x."
|
|
continue
|
|
fi
|
|
|
|
if [[ $? -ne 0 ]]; then
|
|
log ERROR "Failed to apply breaking changes patch for version $i. Do you want to continue? (Y/n): "
|
|
read response
|
|
|
|
[[ "$response" != "y" && "$response" != "Y" && -n "$response" ]] && { log INFO "Exiting." ; exit 1; }
|
|
else
|
|
log INFO "Upgrade ${GREEN}successfully${NC} applied for mismatch: ${GREEN}0.$i.x${NC}."
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# copy everything in
|
|
cp -r "$MODS_EXPORT_DIR"/* "$MODS_DIR"
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Failed to import mods."; exit 1; }
|
|
|
|
[[ $modpack == false ]] && log INFO "Mods ${GREEN}successfully${NC} imported."
|
|
}
|
|
|
|
function mod_order {
|
|
parse_help_has_arguments display_help_order "$@"
|
|
local mod_name=""
|
|
local mod_index=""
|
|
local new_index=""
|
|
|
|
# parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
"-i")
|
|
[[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]] && { log ERROR "Invalid mod index."; exit 1; }
|
|
mod_index="$2"; shift 2
|
|
;;
|
|
"-n")
|
|
[[ -z "$2" ]] && { log ERROR "Mod name is required."; exit 1; }
|
|
mod_name="$2"; shift 2
|
|
;;
|
|
*)
|
|
new_index="$1"; shift 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$mod_name" && -z "$mod_index" ]] && { log ERROR "Mod name or index is required to change order."; exit 1; }
|
|
|
|
# find mod files
|
|
get_mod_name_and_index
|
|
|
|
# get the number of mods
|
|
mod_count=$(($(cat "$DB_FILE" | wc -l) - 1))
|
|
|
|
# check if the mod is already at the desired index and if new index is valid
|
|
[[ "$mod_index" == "$new_index" ]] && { log ERROR "Mod $mod_name is already at index $new_index."; exit 1; }
|
|
[[ $new_index -lt 1 ]] && { log ERROR "Index can not be less than 1."; exit 1; }
|
|
[[ $new_index -gt $mod_count ]] && { log ERROR "Index can not be more than mod count (${mod_count})."; exit 1; }
|
|
|
|
# assert ascending or descending order
|
|
ascending_order=true
|
|
[[ $mod_index -gt $new_index ]] && ascending_order=false
|
|
|
|
# get entries between the indexes
|
|
if [[ $ascending_order == true ]]; then
|
|
entries=$(awk -v mod_index="$mod_index" -v new_index="$new_index" -F, 'NR > 1 && $1 > mod_index && $1 <= new_index {print $0}' "$DB_FILE")
|
|
else
|
|
entries=$(awk -v mod_index="$mod_index" -v new_index="$new_index" -F, 'NR > 1 && $1 < mod_index && $1 >= new_index {print $0}' "$DB_FILE")
|
|
fi
|
|
|
|
# get files for the current mod
|
|
IFS= current_mod_files=$(get_files_by_entry_from_db "$entry"); unset IFS
|
|
|
|
# step 1 - count how many replaces need to be done in hash table
|
|
declare -A replace_count
|
|
for file in $current_mod_files; do
|
|
extension=$(get_extension "$file")
|
|
base_name=$(get_basename "$file")
|
|
|
|
# if the file has no extension, add the basename to the hash table or increment the count
|
|
if [[ -z "$extension" ]]; then
|
|
replace_count["$base_name"]=$(( ${replace_count["$base_name"]:-0} + 1 ))
|
|
fi
|
|
done
|
|
|
|
# step 2.1 - iterate over the basenames, find and store the files with the same basenames as the ones we want to upgrade/downgrade
|
|
declare -A files_to_replace
|
|
for base_name in "${!replace_count[@]}"; do
|
|
IFS= files_to_replace["$base_name"]=$(echo "$entries" | awk -F, '{print $7}' | grep -o "$base_name\.patch_[^ ]*"); unset IFS
|
|
done
|
|
|
|
# step 2.2 - reverse sort the files_to_replace if we are in descending order
|
|
[[ $ascending_order == false ]] && for base_name in "${!files_to_replace[@]}"; do
|
|
files_to_replace["$base_name"]=$(echo "${files_to_replace["$base_name"]}" | sort -rV)
|
|
done
|
|
|
|
# move current mod files to "t_$file" so we can move the new files to the correct index
|
|
for file in $current_mod_files; do
|
|
mv "$MODS_DIR/$file" "$MODS_DIR/t_$file"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not move mod file $file."; exit 1; }
|
|
done
|
|
|
|
# step 3 - for every basename in files_to_replace, add replace_count to each file's patch number and move
|
|
for base_name in "${!files_to_replace[@]}"; do
|
|
files=$(echo "${files_to_replace["$base_name"]}" | tr ' ' '\n')
|
|
|
|
for file in $files; do
|
|
base_name=$(get_basename "$file")
|
|
patch_number=$(get_patch_number "$file")
|
|
extension=$(get_extension "$file")
|
|
|
|
# if ascending order, subtract replace_count to the patch number, otherwise add
|
|
operation="-"; [[ $ascending_order == false ]] && operation="+"
|
|
new_file="${base_name}.patch_$(($patch_number $operation ${replace_count["$base_name"]}))${extension}"
|
|
|
|
mv "$MODS_DIR/$file" "$MODS_DIR/$new_file"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not move mod file $file."; exit 1; }
|
|
|
|
log INFO "Reindexing ${ORANGE}$file${NC} to ${GREEN}$new_file${NC}."
|
|
|
|
# update db
|
|
sed -i "s/\(\b$file\b\)/$new_file/" "$DB_FILE"
|
|
done
|
|
done
|
|
|
|
# step 4 - for every file in current_mod_files, subtract the basename's array size of files_to_replace from the patch number
|
|
for file in $current_mod_files; do
|
|
base_name=$(get_basename "$file")
|
|
patch_number=$(get_patch_number "$file")
|
|
extension=$(get_extension "$file")
|
|
|
|
# get size of only files without an extension
|
|
size=$(echo "${files_to_replace["$base_name"]}" | grep -E "${base_name}.patch_[0-9]+$" | tr ' ' '\n' | wc -l)
|
|
|
|
# in case size is 0, move t_file back to file later
|
|
new_file=${file}
|
|
|
|
if [[ $size -gt 0 ]]; then
|
|
# if ascending order, add size from the patch number, otherwise subtract
|
|
operation="+"; [[ $ascending_order == false ]] && operation="-"
|
|
new_file="${base_name}.patch_$(($patch_number $operation $size))${extension}"
|
|
|
|
log INFO "Reindexing ${ORANGE}$file${NC} to ${GREEN}$new_file${NC}."
|
|
|
|
# dont forget current mod files are prefixed with t_
|
|
mv "$MODS_DIR/t_$file" "$MODS_DIR/t_$new_file"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not move mod file $file."; exit 1; }
|
|
|
|
# update file names
|
|
entry=$(echo "$entry" | sed "s/\(\b$file\b\)/$new_file/")
|
|
fi
|
|
|
|
# move back temp file
|
|
mv "$MODS_DIR/t_$new_file" "$MODS_DIR/$new_file"
|
|
|
|
# update index (will be replaced in db later)
|
|
entry=$(echo "$entry" | sed "s/^$mod_index,/$new_index,/")
|
|
done
|
|
|
|
# step 5.1 - change current mod to t_index so we can move the other indexes
|
|
sed -i "s/^$mod_index,/t_$mod_index,/" "$DB_FILE"
|
|
|
|
# step 5.2 - reindex the rest of the mods
|
|
idxs=$(echo "$entries" | awk -F, '{print $1}' | tr ' ' '\n')
|
|
|
|
# step 5.3 - reverse sort the idxs if we are in descending order
|
|
[[ $ascending_order == false ]] && idxs=$(echo "$idxs" | sort -r)
|
|
|
|
for idx in $idxs; do
|
|
# if ascending order, subtract 1 from the index, otherwise add 1
|
|
operation="-"; [[ $ascending_order == false ]] && operation="+"
|
|
|
|
sed -i "s/^$idx,/$(($idx $operation 1)),/" "$DB_FILE"
|
|
done
|
|
|
|
# step 6 - move the entry to the new index
|
|
sed -i "/^t_$mod_index,/d" "$DB_FILE"
|
|
|
|
# weird edge case where the new index is the last index
|
|
if [[ $((new_index + 1)) -gt $(wc -l < "$DB_FILE") ]]; then
|
|
echo "$entry" >> "$DB_FILE"
|
|
else
|
|
sed -i "$((new_index + 1))i $entry" "$DB_FILE"
|
|
fi
|
|
|
|
log INFO "Mod ${GREEN}successfully${NC} reindexed: \"$mod_name\" went from $mod_index to $new_index."
|
|
}
|
|
|
|
# --- Modpack management ---
|
|
|
|
function modpack_list() {
|
|
parse_help_no_arguments display_help_modpack_list "$@"
|
|
local verbose=false
|
|
|
|
# parse arguments
|
|
[[ "$1" == "--verbose" || "$1" == "-v" ]] && verbose=true
|
|
|
|
# quit if no modpacks are saved
|
|
[[ $(wc -l < "$MODPACKS_DB_FILE") -le 1 ]] && { log INFO "No modpacks saved."; exit 0; }
|
|
|
|
if [[ $verbose == true ]]; then
|
|
modpack_contents=""
|
|
|
|
# get modpack files
|
|
modpack_files=$(awk -F, 'NR > 1 {print $3 ".tar.gz"}' "$MODPACKS_DB_FILE")
|
|
|
|
for file in $modpack_files; do
|
|
mods=$(tar -xzvf "$MODPACKS_DIR/$file" --to-stdout "Helldivers 2 Mods/mods.csv" 2>/dev/null | awk -F, 'NR > 1 {print " -> " $3}')
|
|
|
|
modpack_contents="${modpack_contents}${mods};"
|
|
done
|
|
fi
|
|
|
|
log INFO "Saved modpacks:"
|
|
|
|
awk -v GREEN="$GREEN" -v RED="$RED" -v NC="$NC" -v verbose="$verbose" -v modpack_contents="$modpack_contents" -F, 'BEGIN {
|
|
if(verbose == "true") {
|
|
split(modpack_contents, modpack_array, ";")
|
|
}
|
|
}
|
|
NR > 1{
|
|
color = ($2 == "DISABLED") ? RED : GREEN;
|
|
if (verbose == "false") {
|
|
$4 = "";
|
|
} else {
|
|
$4 = "\n" modpack_array[NR - 1];
|
|
}
|
|
printf "%2s. [%s%s%s] %s%s\n", $1, color, $2, NC, $3, $4}' "$MODPACKS_DB_FILE"
|
|
}
|
|
|
|
function modpack_create() {
|
|
parse_help_has_arguments display_help_modpack_create "$@"
|
|
local modpack_name=""
|
|
|
|
# parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
"-n")
|
|
[[ -z "$2" ]] && { log ERROR "Modpack name is required."; exit 1; }
|
|
modpack_name="$2"; shift 2
|
|
;;
|
|
*)
|
|
$display_help; exit 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# if no mods are installed, exit
|
|
[[ $(wc -l < "$DB_FILE") -le 1 ]] && { log ERROR "No mods installed."; exit 1; }
|
|
|
|
# if the same modpack name already exists, exit
|
|
[[ -f "$MODPACKS_DIR/${modpack_name}.tar.gz" ]] && { log ERROR "Modpack \"$modpack_name\" already exists."; exit 1; }
|
|
|
|
# let user select mods to save
|
|
mod_list
|
|
log PROMPT "Enter the mod indices to use for modpack (separated by space) or press Enter to select all: "
|
|
read -a mod_indices
|
|
if [[ -n "${mod_indices[0]}" ]]; then
|
|
# get the mod entries from the indices
|
|
mod_entries=()
|
|
|
|
# grab all the entries from the database from the indices
|
|
for index in "${mod_indices[@]}"; do
|
|
[[ ! "$index" =~ ^[0-9]+$ ]] && { log ERROR "Invalid mod index."; exit 1; }
|
|
[[ $index -lt 1 || $index -gt $(awk -F, 'END {print $1}' "$DB_FILE" ) ]] && { log ERROR "Mod index out of range."; exit 1; }
|
|
|
|
IFS= mod_entries+=($(awk -v idx=$index -F, 'NR > 1 && $1 == idx {print $0}' "$DB_FILE")); unset IFS
|
|
done
|
|
|
|
# overwrite all paths to create a custom directory that will hold the mods specified, the mods will be "installed" there
|
|
OLD_MODS_DIR="$MODS_DIR"
|
|
MODS_DIR="$(mktemp -d)"
|
|
DB_FILE="$MODS_DIR/mods.csv"
|
|
echo "$(get_version_major "$VERSION")" > "$DB_FILE"
|
|
|
|
# install selected mods to temp directory
|
|
for entry in "${mod_entries[@]}"; do
|
|
mod_name=$(get_name_by_entry_from_db "$entry")
|
|
IFS= _mod_files=$(get_files_by_entry_from_db "$entry"); unset IFS
|
|
mod_files=()
|
|
|
|
# add the OLD_MODS_DIR prefix to every entry from mod_files
|
|
for file in $_mod_files; do
|
|
mod_files+=("$OLD_MODS_DIR/$file")
|
|
done
|
|
|
|
silent=true
|
|
mod_install -n "$mod_name" "${mod_files[@]}"
|
|
silent=false
|
|
done
|
|
fi
|
|
|
|
# use built-in export function
|
|
mod_export --modpack "$MODPACKS_DIR" "$modpack_name"
|
|
|
|
log INFO "Modpack ${GREEN}successfully${NC} created: $modpack_name"
|
|
log INFO "Switch to the modpack with 'h2mm modpack-switch -n \"$modpack_name\"'."
|
|
|
|
# warn user to create a modpack with his main mods
|
|
[[ $(wc -l < "$MODPACKS_DB_FILE") -le 1 ]] && log INFO "If this is your first modpack, it is ${ORANGE}recommended${NC} to create a separate modpack with your main mods."
|
|
|
|
# add entry to database
|
|
next_id=$(awk -F, 'NR > 1 {last_id = $1} END {print last_id + 1}' "$MODPACKS_DB_FILE")
|
|
echo "$next_id,DISABLED,$modpack_name" >> "$MODPACKS_DB_FILE"
|
|
}
|
|
|
|
function modpack_switch() {
|
|
parse_help_has_arguments display_help_modpack_switch "$@"
|
|
local modpack_name=""
|
|
local modpack_index=""
|
|
|
|
# parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
"-i")
|
|
[[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]] && { log ERROR "Invalid modpack index."; exit 1; }
|
|
modpack_index="$2"; shift 2
|
|
;;
|
|
"-n")
|
|
[[ -z "$2" ]] && { log ERROR "Modpack name is required."; exit 1; }
|
|
modpack_name="$2"; shift 2
|
|
;;
|
|
*)
|
|
$display_help; exit 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$modpack_name" && -z "$modpack_index" ]] && { log ERROR "Modpack name or index is required to switch."; exit 1; }
|
|
|
|
# find modpack files
|
|
get_modpack_name_and_index "$modpack_name" "$modpack_index"
|
|
|
|
log INFO "Switching modpacks mods will ${RED}reset${NC} your mods."
|
|
mod_import --modpack "$MODPACKS_DIR/$modpack_name.tar.gz"
|
|
|
|
log INFO "Modpack ${GREEN}successfully${NC} switched: $modpack_name."
|
|
|
|
# save status to db
|
|
sed -i "s/ENABLED/DISABLED/" "$MODPACKS_DB_FILE"
|
|
sed -i "/^$modpack_index,/s/DISABLED/ENABLED/" "$MODPACKS_DB_FILE"
|
|
}
|
|
|
|
function modpack_reset() {
|
|
parse_help_no_arguments display_help_modpack_reset "$@"
|
|
|
|
log PROMPT "Are you sure you want to ${RED}reset${NC} all installed modpacks? (Y/n): "
|
|
read confirm
|
|
if [[ "$confirm" == "y" || "$confirm" == "Y" || "$confirm" = "" ]]; then
|
|
rm -f "$MODPACKS_DIR"/*.tar.gz
|
|
rm -f "$MODPACKS_DB_FILE"
|
|
rmdir "$MODPACKS_DIR"
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not reset modpacks."; exit 1; }
|
|
log INFO "Modpacks ${GREEN}successfully${NC} reset."
|
|
else
|
|
log INFO "Reset cancelled."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
function modpack_delete() {
|
|
parse_help_has_arguments display_help_modpack_delete "$@"
|
|
local modpack_name=""
|
|
local modpack_index=""
|
|
|
|
# parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
"-i")
|
|
[[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]] && { log ERROR "Invalid modpack index."; exit 1; }
|
|
modpack_index="$2"; shift 2
|
|
;;
|
|
"-n")
|
|
[[ -z "$2" ]] && { log ERROR "Modpack name is required."; exit 1; }
|
|
modpack_name="$2"; shift 2
|
|
;;
|
|
*)
|
|
$display_help; exit 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$modpack_name" && -z "$modpack_index" ]] && { log ERROR "Modpack name or index is required to delete."; exit 1; }
|
|
|
|
get_modpack_name_and_index "$modpack_name" "$modpack_index"
|
|
|
|
rm -f "$MODPACKS_DIR/$modpack_name.tar.gz"
|
|
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not delete modpack."; exit 1; }
|
|
log INFO "Modpack ${GREEN}successfully${NC} deleted: \$MODPACKS_DIRECTORY/$modpack_name.tar.gz"
|
|
|
|
# remove entry from database
|
|
sed -i "/^$modpack_index,/d" "$MODPACKS_DB_FILE"
|
|
}
|
|
|
|
function modpack_overwrite() {
|
|
parse_help_has_arguments display_help_modpack_overwrite "$@"
|
|
local modpack_name=""
|
|
local modpack_index=""
|
|
|
|
# parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
"-i")
|
|
[[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]] && { log ERROR "Invalid modpack index."; exit 1; }
|
|
modpack_index="$2"; shift 2
|
|
;;
|
|
"-n")
|
|
[[ -z "$2" ]] && { log ERROR "Modpack name is required."; exit 1; }
|
|
modpack_name="$2"; shift 2
|
|
;;
|
|
*)
|
|
$display_help; exit 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$modpack_name" && -z "$modpack_index" ]] && { log ERROR "Modpack name or index is required to overwrite."; exit 1; }
|
|
|
|
get_modpack_name_and_index "$modpack_name" "$modpack_index"
|
|
|
|
# if the modpack doesn't exist, exit
|
|
[[ ! -f "$MODPACKS_DIR/$modpack_name.tar.gz" ]] && { log ERROR "Modpack $modpack_name does not exist."; exit 1; }
|
|
|
|
rm -f "$MODPACKS_DIR/$modpack_name.tar.gz"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not delete modpack."; exit 1; }
|
|
|
|
# use built-in export function
|
|
mod_export --modpack "$MODPACKS_DIR" "$modpack_name"
|
|
|
|
log INFO "Modpack ${GREEN}successfully${NC} overwritten: \$MODPACKS_DIRECTORY/$modpack_name.tar.gz"
|
|
sed -i "/^$modpack_index,/s/DISABLED/ENABLED/" "$MODPACKS_DB_FILE"
|
|
}
|
|
|
|
function self_update() {
|
|
latest_version=$(curl -sS "$VERSION_URL")
|
|
|
|
if [[ "$latest_version" == "$VERSION" ]]; then
|
|
log INFO "Mod manager is already up-to-date."
|
|
exit 0
|
|
fi
|
|
|
|
log INFO "Starting update script..."
|
|
|
|
# run the installer for the latest version
|
|
bash -c "$(curl -fsSL https://raw.githubusercontent.com/v4n00/h2mm-cli/refs/heads/master/install.sh)"
|
|
exit 0
|
|
}
|
|
|
|
# --- Nexus Mods Integration ---
|
|
|
|
function nexus_setup() {
|
|
parse_help_no_arguments display_help_nexus_setup "$@"
|
|
local nexus_api_key=""
|
|
|
|
log INFO "This is the setup wizard for the Nexus Mods integration."
|
|
log INFO "You need to provide your Nexus Mods API key to use this feature."
|
|
log INFO "You can find your API key in your Nexus Mods by:"
|
|
log INFO " -> Going to Site Preferences -> API Keys"
|
|
log INFO " -> Going to https://next.nexusmods.com/settings/api-keys"
|
|
log INFO "Scroll down and request/copy your API key."
|
|
log INFO ""
|
|
log PROMPT "Enter your Nexus Mods API key: "
|
|
read -e nexus_api_key
|
|
|
|
[[ -z "$nexus_api_key" ]] && { log ERROR "Nexus Mods API key is required."; exit 1; }
|
|
|
|
# save api key to config dir, trim it
|
|
echo "$nexus_api_key" | tr -d '[:space:]' > "$API_KEY_FILE"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not save Nexus Mods API key."; exit 1; }
|
|
|
|
# test API key
|
|
response=$(curl -sSIH "apikey: $nexus_api_key" -X GET "https://api.nexusmods.com/v1/users/validate.json")
|
|
[[ $? -ne 0 ]] && { log ERROR "curl failed."; exit 1; }
|
|
[[ ! "$response" =~ "HTTP/2 200" ]] && { log ERROR "Invalid Nexus Mods API key."; exit 1; }
|
|
|
|
log INFO "Nexus Mods API key ${GREEN}successfully${NC} tested and saved."
|
|
log INFO ""
|
|
|
|
# ask for default terminal
|
|
local terminal_command=""
|
|
|
|
log INFO "Now the manager needs to create a desktop entry for h2mm."
|
|
log INFO "In order to do that, it needs to know which terminal you are using."
|
|
log INFO "Here's a list of supported terminals:"
|
|
log INFO " -> gnome-terminal (On GNOME systems like Ubuntu)"
|
|
log INFO " -> konsole (On KDE systems like SteamOS, Steam Deck's default)"
|
|
log INFO " -> alacritty"
|
|
log INFO " -> kitty"
|
|
log INFO ""
|
|
log INFO "If you are not using any of these terminals, you need to provide"
|
|
log INFO "the command to spawn your terminal along with the flags to run a command."
|
|
log INFO "To test if the command is valid, the manager spawn a terminal."
|
|
log INFO "For example, if you are using ghostty you need to provide this:"
|
|
log INFO "ghostty -e <COMMAND>"
|
|
log INFO ""
|
|
log PROMPT "Enter your terminal (or terminal command): "
|
|
read -e terminal_command
|
|
|
|
[[ -z "$terminal_command" ]] && { log ERROR "Terminal command is required."; exit 1; }
|
|
command -v "$(echo "$terminal_command" | awk '{print $1}')" >/dev/null 2>&1 || { log ERROR "Terminal command does not work."; exit 1; }
|
|
|
|
# check the 4 supported terminals
|
|
case "$terminal_command" in
|
|
"gnome-terminal")
|
|
terminal_command="$terminal_command -- <COMMAND>"
|
|
;;
|
|
"konsole"|"alacritty"|"kitty")
|
|
terminal_command="$terminal_command -e <COMMAND>"
|
|
;;
|
|
*)
|
|
# check if terminal even exists
|
|
command -v "$(echo "$terminal_command" | awk '{print $1}')" >/dev/null 2>&1 || { log ERROR "Terminal command does not work."; exit 1; }
|
|
# check if the format is valid
|
|
[[ "$terminal_command" != *"<COMMAND>"* ]] && { log ERROR "Terminal command is invalid. You did not include <COMMAND>"; exit 1; }
|
|
|
|
# test terminal -e command
|
|
wait_command="read -p 'Press any key to validate terminal...'"
|
|
test_command=$(echo "$terminal_command" | sed "s/<COMMAND>/$wait_command/")
|
|
|
|
log INFO "Using test command: $test_command"
|
|
$test_command
|
|
[[ $? -ne 0 ]] && { log ERROR "Terminal command is invalid."; exit 1; }
|
|
;;
|
|
esac
|
|
|
|
# build terminal command
|
|
terminal_command=$(echo "$terminal_command" | sed "s/<COMMAND>/h2mm nexus/")
|
|
|
|
# build the desktop entry
|
|
local desktop_entry="[Desktop Entry]
|
|
Name=h2mm
|
|
Comment=Link application to be used with Nexus Mods
|
|
Exec=$terminal_command
|
|
Terminal=false
|
|
Type=Application
|
|
Categories=Game;
|
|
MimeType=x-scheme-handler/nxm;"
|
|
|
|
local desktop_file="$HOME/.local/share/applications/h2mm.desktop"
|
|
|
|
# create the desktop entry file
|
|
echo "$desktop_entry" > "$desktop_file"
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not create desktop entry."; exit 1; }
|
|
|
|
log INFO ""
|
|
log INFO "Desktop entry ${GREEN}successfully${NC} created."
|
|
log INFO "Your system needs to refresh the desktop database to use the new entry."
|
|
log INFO "This requires sudo permissions. Please enter your password."
|
|
|
|
sudo update-desktop-database
|
|
[[ $? -ne 0 ]] && { log ERROR "Could not refresh desktop database."; exit 1; }
|
|
|
|
log INFO "You can now use the Nexus Mods integration."
|
|
log INFO "To use it, go to a mod page and click on the \"Vortex\" or \"Download with Manager\" button."
|
|
log INFO "Your browser will ask you to open the link with h2mm."
|
|
}
|
|
|
|
function nexus_get_mod_info() {
|
|
local mod_id="$1"
|
|
local mod_file_id="$2"
|
|
|
|
[[ -z "$mod_id" || -z "$mod_file_id" ]] && { log ERROR "Mod ID and file ID are required (inside Nexus info check)."; return 1; }
|
|
|
|
# api_key
|
|
get_nexus_api_key
|
|
|
|
api_url="https://api.nexusmods.com/v1/games/helldivers2/mods/$mod_id/files/$mod_file_id.json"
|
|
response=$(curl -sSw " http:%{http_code}" -H "apikey: $api_key" "$api_url")
|
|
[[ $? -ne 0 ]] && { log ERROR "curl failed (inside Nexus info check)."; return 1; }
|
|
|
|
# checks
|
|
status="${response: -3}"
|
|
[[ "$status" != "200" ]] && { log ERROR "Invalid response from Nexus Mods API (inside Nexus info check)."; return 1; }
|
|
|
|
# extract info
|
|
mod_name=$(echo "$response" | grep -oP '"name":"\K[^"]+')
|
|
nexus_mod_version=$(echo "$response" | grep -oP '"version":"\K[^"]+')
|
|
[[ -z "$mod_name" || -z "$nexus_mod_version" ]] && { log ERROR "Could not extract mod name and version (inside Nexus info check)."; return 1; }
|
|
}
|
|
|
|
function nexus_check_for_updates() {
|
|
# get list of entries that have a nexus mod_id
|
|
IFS=$'\n' nexus_mods=($(awk -F, 'NR > 1 && $4 != "" {print $0}' "$DB_FILE")); unset IFS
|
|
[[ -z "$nexus_mods" ]] && return
|
|
|
|
local update_count=0
|
|
[[ -f "$UPDATES_AVAILABLE_FILE" ]] && rm -f "$UPDATES_AVAILABLE_FILE"
|
|
|
|
# call the API for each mod_id and get the latest version
|
|
for entry in "${nexus_mods[@]}"; do
|
|
nexus_mod_id=$(get_nexus_mod_id_by_entry_from_db "$entry")
|
|
nexus_mod_file_id=$(get_nexus_mod_file_id_by_entry_from_db "$entry")
|
|
local_mod_version=$(get_nexus_mod_version_by_entry_from_db "$entry")
|
|
|
|
nexus_get_mod_info "$nexus_mod_id" "$nexus_mod_file_id"
|
|
[[ $? -ne 0 ]] && { return 1; }
|
|
|
|
# compare the latest version with the installed version
|
|
if [[ "$nexus_mod_version" != "$local_mod_version" ]]; then
|
|
mod_name=$(get_name_by_entry_from_db "$entry")
|
|
|
|
# store in a temp file (inside config dir) the mods that have updates
|
|
echo "$mod_name,$local_mod_version,$nexus_mod_version,$nexus_mod_id,$nexus_mod_file_id" >> "$UPDATES_AVAILABLE_FILE"
|
|
update_count=$((update_count + 1))
|
|
fi
|
|
done
|
|
|
|
[[ $update_count -eq 0 ]] && return 0;
|
|
log WARNING "Updates available for ${ORANGE}$update_count${NC} Nexus mods => run 'h2mm nexus-update'."
|
|
}
|
|
|
|
function nexus_update_mods() {
|
|
parse_help_no_arguments display_help_nexus_update "$@"
|
|
|
|
[[ $(wc -l < "$UPDATES_AVAILABLE_FILE") -eq 0 ]] && { log INFO "No updates available."; return 0; }
|
|
|
|
log INFO "Due to Nexus Mods API limitations, the update process is not automated."
|
|
log INFO "Please visit the direct download links to install the latest version of your mods:"
|
|
|
|
awk -v GREEN="$GREEN" -v ORANGE="$ORANGE" -v NC="$NC" -F, '{
|
|
printf "%2s. %s\n", NR, $1
|
|
printf " => Update from %s%s%s -> %s%s%s\n", ORANGE, $2, NC, GREEN, $3, NC
|
|
printf " -> Direct install: https://www.nexusmods.com/helldivers2/mods/%s?tab=files&file_id=%s&nmm=1\n", $4, $5
|
|
printf " -> Mod page: https://www.nexusmods.com/helldivers2/mods/%s\n", $4
|
|
}' "$UPDATES_AVAILABLE_FILE"
|
|
}
|
|
|
|
|
|
function nexus() {
|
|
trap 'read -p "Press any key to continue..."' EXIT
|
|
local mod_nxm_link="$1"
|
|
|
|
[[ -z "$mod_nxm_link" ]] && { log ERROR "Nexus Mods nxm link is required."; exit 1; }
|
|
[[ ! "$mod_nxm_link" =~ ^nxm://helldivers2/mods/[0-9]+/files/[0-9]+\?key=.*\&expires=[0-9]+\&user_id=[0-9]+$ ]] && { log ERROR "Invalid Nexus Mods nxm link."; exit 1; }
|
|
|
|
log INFO "Received NXM link: $mod_nxm_link"
|
|
|
|
# api_key
|
|
get_nexus_api_key
|
|
|
|
# extract info
|
|
nexus_mod_id=$(echo "$mod_nxm_link" | awk -F/ '{print $5}')
|
|
nexus_mod_file_id=$(echo "$mod_nxm_link" | awk -F/ '{print $7}' | cut -d\? -f1)
|
|
key=$(echo "$mod_nxm_link" | awk -F= '{print $2}' | cut -d\& -f1)
|
|
expires=$(echo "$mod_nxm_link" | awk -F= '{print $3}' | cut -d\& -f1)
|
|
|
|
api_url="https://api.nexusmods.com/v1/games/helldivers2/mods/$nexus_mod_id/files/$nexus_mod_file_id/download_link.json?key=$key&expires=$expires"
|
|
log INFO "Using API URL: $api_url"
|
|
|
|
response=$(curl -sSw " http:%{http_code}" -H "apikey: $api_key" "$api_url")
|
|
[[ $? -ne 0 ]] && { log ERROR "curl failed."; exit 1; }
|
|
|
|
# checks
|
|
status="${response: -3}"
|
|
[[ "$status" != "200" ]] && { log ERROR "Invalid response from Nexus Mods API."; exit 1; }
|
|
log INFO "Received response from Nexus Mods API: $response"
|
|
|
|
# extract URI
|
|
download_url=$(printf "$response" | sed -E 's/.*"URI":"([^"]+)".*/\1/' | sed 's/\ /%20/g')
|
|
log INFO "Using download URL: $download_url"
|
|
log INFO ""
|
|
|
|
# extract file name
|
|
file_name=$(echo "$download_url" | awk -F/ '{print $6}' | cut -d\? -f1)
|
|
output_file="$(mktemp -d)/$file_name"
|
|
|
|
# download the file
|
|
log INFO "Download progress:"
|
|
curl -Lo "$output_file" "$download_url"
|
|
[[ $? -ne 0 ]] && { log ERROR "Download failed."; exit 1; }
|
|
|
|
log INFO "Download ${GREEN}successfully${NC} completed."
|
|
log INFO ""
|
|
|
|
# get mod info
|
|
nexus_get_mod_info "$nexus_mod_id" "$nexus_mod_file_id"
|
|
[[ $? -ne 0 ]] && { exit 1; }
|
|
|
|
# check if the mod is already installed, if yes, update it, otherwise install it
|
|
existing_mod_entry=$(get_entry_from_db_by_nexus_mod_id "$nexus_mod_id")
|
|
if [[ -z "$existing_mod_entry" ]]; then
|
|
# install the mod if it doesn't exist
|
|
mod_install "$output_file" -n "$mod_name" --mod-id "$nexus_mod_id" --file-id "$nexus_mod_file_id" --version "$nexus_mod_version"
|
|
else
|
|
# replace the mod if it exists
|
|
existing_mod_index=$(get_index_by_entry_from_db "$existing_mod_entry")
|
|
mod_index=$(get_index_by_entry_from_db "$existing_mod_entry")
|
|
|
|
mod_uninstall -i "$mod_index"
|
|
|
|
mod_install "$output_file" -n "$mod_name" --mod-id "$nexus_mod_id" --file-id "$nexus_mod_file_id" --version "$nexus_mod_version"
|
|
|
|
# get the current mod index from what we just installed, last line of the db
|
|
new_mod_index=$(get_index_by_entry_from_db "$(awk -F, 'END {print $1}' "$DB_FILE")")
|
|
[[ -z "$new_mod_index" ]] && { log ERROR "Could not get current mod index."; exit 1; }
|
|
|
|
[[ $mod_index -ne $old_mod_index ]] && mod_order -i "$new_mod_index" "$mod_index"
|
|
|
|
log INFO "Mod ${GREEN}successfully${NC} updated: $mod_name."
|
|
|
|
disable_all_modpacks
|
|
fi
|
|
}
|
|
|
|
# --- Update Check ---
|
|
|
|
function check_h2mm_update() {
|
|
latest_version=$(curl -sS "$VERSION_URL")
|
|
if [[ $? -ne 0 ]]; then
|
|
log ERROR "Could not check for updates."
|
|
return
|
|
fi
|
|
|
|
if [[ "$latest_version" != "$VERSION" ]]; then
|
|
log WARNING "A new version of h2mm is available: ${ORANGE}$VERSION${NC} -> ${GREEN}$latest_version${NC} => run 'h2mm update'."
|
|
fi
|
|
}
|
|
|
|
function check_for_updates() {
|
|
last_update=$(cat "$LAST_CHECKED_UPDATE_FILE")
|
|
|
|
# if last_update is not in unix time format, reset it (for version <= 0.5.0)
|
|
if [[ ! "$last_update" =~ ^[0-9]+$ ]]; then
|
|
rm -f "$LAST_CHECKED_UPDATE_FILE"
|
|
echo "$(date +%s)" > "$LAST_CHECKED_UPDATE_FILE"
|
|
last_update=$(cat "$LAST_CHECKED_UPDATE_FILE")
|
|
fi
|
|
|
|
# check for updates by comparing the last update time + 3 hpurs with the current time
|
|
if [[ -n "$last_update" && "$(date +%s)" -lt $(("$last_update" + 7200)) ]]; then
|
|
return
|
|
fi
|
|
|
|
log INFO "Checking for updates..."
|
|
|
|
check_h2mm_update
|
|
nexus_check_for_updates
|
|
|
|
echo "$(date +%s)" > "$LAST_CHECKED_UPDATE_FILE"
|
|
}
|
|
|
|
# --- Main ---
|
|
|
|
function main() {
|
|
parse_help_has_arguments display_help_main "$@"
|
|
|
|
command="$1"; shift
|
|
|
|
initialize_directories
|
|
initialize_modpack_directories
|
|
check_for_updates
|
|
|
|
case "$command" in
|
|
"install"|"i")
|
|
mod_install "$@"
|
|
;;
|
|
"list"|"l")
|
|
mod_list "$@"
|
|
;;
|
|
"uninstall"|"u")
|
|
mod_uninstall "$@"
|
|
;;
|
|
"enable"|"e")
|
|
mod_enable "$@"
|
|
;;
|
|
"disable"|"d")
|
|
mod_disable "$@"
|
|
;;
|
|
"export"|"ex")
|
|
mod_export "$@"
|
|
;;
|
|
"import"|"im")
|
|
mod_import "$@"
|
|
;;
|
|
"order"|"o")
|
|
mod_order "$@"
|
|
;;
|
|
"modpack-list"|"ml")
|
|
modpack_list "$@"
|
|
;;
|
|
"modpack-create"|"mc")
|
|
modpack_create "$@"
|
|
;;
|
|
"modpack-delete"|"md")
|
|
modpack_delete "$@"
|
|
;;
|
|
"modpack-overwrite"|"mo")
|
|
modpack_overwrite "$@"
|
|
;;
|
|
"modpack-switch"|"ms")
|
|
modpack_switch "$@"
|
|
;;
|
|
"modpack-reset"|"mr")
|
|
modpack_reset "$@"
|
|
;;
|
|
"nexus-setup"|"ns")
|
|
nexus_setup "$@"
|
|
;;
|
|
"nexus-update"|"nu")
|
|
nexus_update_mods "$@"
|
|
;;
|
|
"nexus")
|
|
nexus "$@"
|
|
;;
|
|
"reset"|"r")
|
|
mod_reset "$@"
|
|
;;
|
|
"version"|"v"|"-v"|"--version")
|
|
echo "$VERSION"
|
|
;;
|
|
"update"|"up")
|
|
self_update
|
|
;;
|
|
*)
|
|
$display_help
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|