diff --git a/lgsm/config-default/config-lgsm/arma3server/_default.cfg b/lgsm/config-default/config-lgsm/arma3server/_default.cfg index 7c4619c17f..8723ef79d3 100644 --- a/lgsm/config-default/config-lgsm/arma3server/_default.cfg +++ b/lgsm/config-default/config-lgsm/arma3server/_default.cfg @@ -27,6 +27,11 @@ mods="" ## Server-side Mods servermods="" +## Mods to be downloaded from Steam Workshop +# Use workshop ids +# workshopmods="450814997;2131302796" +workshopmods="" + ## Path to BattlEye # Leave empty for default bepath="" @@ -134,6 +139,8 @@ sleeptime="0.5" # Server appid appid="233780" steamcmdforcewindows="no" +# Game appid +gameappid="107410" # SteamCMD Branch | https://docs.linuxgsm.com/steamcmd/branch branch="" betapassword="" diff --git a/lgsm/functions/command_workshop_install.sh b/lgsm/functions/command_workshop_install.sh new file mode 100644 index 0000000000..f047aaaddd --- /dev/null +++ b/lgsm/functions/command_workshop_install.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# LinuxGSM command_workshop_install.sh module +# Author: Daniel Gibbs +# Contributors: http://linuxgsm.com/contrib +# Website: https://linuxgsm.com +# Description: List and installs available mods along with mods_list.sh and mods_core.sh. + +commandname="WORKSHOP-INSTALL" +commandaction="Installing Steam Workshop mods" +functionselfname="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")" +fn_firstcommand_set + +check.sh +workshop_core.sh + +fn_print_header +fn_create_workshop_dir +fn_workshop_get_list + +# Displays a list of installed mods. + +echo -e "" +echo -e "Installed workshop addons/mods" +echo -e "=================================" +fn_workshop_installed_list + +for modid in "${workshoplist[@]}"; do + # Check if the mod is already installed and warn the user. + # if ! fn_workshop_check_mod_update $modid; then + # fn_print_warning_nl "$(fn_workshop_get_mod_name ${modid}) is already installed" + # fn_script_log_warn "$(fn_workshop_get_mod_name ${modid}) is already installed" + # echo -e " * Any configs may be overwritten." + # if ! fn_prompt_yn "Continue?" Y; then + # core_exit.sh + # fi + # fn_script_log_info "User selected to continue" + # fi + echo -e "" + echo -e "Installing $(fn_workshop_get_mod_name ${modid})." + echo -e "=================================" + fn_workshop_download "${modid}" + fn_workshop_copy_destination "${modid}"s +done + +fn_workshop_lowercase +core_exit.sh diff --git a/lgsm/functions/command_workshop_update.sh b/lgsm/functions/command_workshop_update.sh new file mode 100644 index 0000000000..a5b7814a84 --- /dev/null +++ b/lgsm/functions/command_workshop_update.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# LinuxGSM command_workshop_install.sh module +# Author: Daniel Gibbs +# Contributors: http://linuxgsm.com/contrib +# Website: https://linuxgsm.com +# Description: List and installs available mods along with mods_list.sh and mods_core.sh. + +commandname="WORKSHOP-INSTALL" +commandaction="Installing Steam Workshop mods" +functionselfname="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")" +fn_firstcommand_set + +check.sh +workshop_core.sh +fn_print_header +fn_create_workshop_dir +fn_workshop_get_list + +# Displays a list of installed mods. +echo -e "Installed workshop addons/mods" +echo -e "=================================" +fn_workshop_installed_list + +for modid in "${workshoplist[@]}"; do + modname="$(fn_workshop_get_mod_name ${modid})" + if [ fn_workshop_check_mod_update "${modid}" ]; then + echo "Mod ${modname} (${modid}) is not up to date." + fn_workshop_download "${modid}" + fn_workshop_copy_destination "${modid}" + else + echo "Mod ${modname} is up to date." + fi +done + +fn_workshop_lowercase +core_exit.sh diff --git a/lgsm/functions/core_functions.sh b/lgsm/functions/core_functions.sh index bca5debf94..0744226243 100755 --- a/lgsm/functions/core_functions.sh +++ b/lgsm/functions/core_functions.sh @@ -151,6 +151,21 @@ command_mods_remove.sh() { fn_fetch_function } +command_workshop_install.sh() { + functionfile="${FUNCNAME[0]}" + fn_fetch_function +} + +command_workshop_update.sh() { + functionfile="${FUNCNAME[0]}" + fn_fetch_function +} + +command_workshop_remove.sh() { + functionfile="${FUNCNAME[0]}" + fn_fetch_function +} + command_fastdl.sh() { functionfile="${FUNCNAME[0]}" fn_fetch_function @@ -287,6 +302,13 @@ mods_core.sh() { fn_fetch_function } +# Steam Workshop + +workshop_core.sh() { + functionfile="${FUNCNAME[0]}" + fn_fetch_function +} + # Dev command_dev_clear_functions.sh() { diff --git a/lgsm/functions/core_getopt.sh b/lgsm/functions/core_getopt.sh index fdd66ab3f5..6ff8f2f1cb 100755 --- a/lgsm/functions/core_getopt.sh +++ b/lgsm/functions/core_getopt.sh @@ -37,6 +37,10 @@ cmd_validate=("v;validate" "command_validate.sh" "Validate server files with Ste cmd_mods_install=("mi;mods-install" "command_mods_install.sh" "View and install available mods/addons.") cmd_mods_remove=("mr;mods-remove" "command_mods_remove.sh" "View and remove an installed mod/addon.") cmd_mods_update=("mu;mods-update" "command_mods_update.sh" "Update installed mods/addons.") +# Server with Steam Workshop +cmd_workshop_install=("wi;workshop-install" "command_workshop_install.sh" "View and install mods/addons from Steam Workshop.") +cmd_workshop_remove=("wr;workshop-remove" "command_workshop_remove.sh" "View and remove an installed mod/addon from Steam Workshop.") +cmd_workshop_update=("wu;workshop-update" "command_workshop_update.sh" "Update installed mods/addons from Steam Workshop.") # Server specific. cmd_change_password=("pw;change-password" "command_ts3_server_pass.sh" "Change TS3 serveradmin password.") cmd_install_default_resources=("ir;install-default-resources" "command_install_resources_mta.sh" "Install the MTA default resources.") @@ -137,6 +141,11 @@ if [ "${engine}" == "source" ] || [ "${shortname}" == "rust" ] || [ "${shortname currentopt+=("${cmd_mods_install[@]}" "${cmd_mods_remove[@]}" "${cmd_mods_update[@]}") fi +## Workshop commands. +if [ "${engine}" == "realvirtuality" ]; then + currentopt+=("${cmd_workshop_install[@]}" "${cmd_workshop_remove[@]}" "${cmd_workshop_update[@]}") +fi + ## Installer. currentopt+=("${cmd_install[@]}" "${cmd_auto_install[@]}") diff --git a/lgsm/functions/workshop_core.sh b/lgsm/functions/workshop_core.sh new file mode 100644 index 0000000000..6120345b3b --- /dev/null +++ b/lgsm/functions/workshop_core.sh @@ -0,0 +1,318 @@ +#!/bin/bash +# LinuxGSM workshop_core.sh module +# Author: Daniel Gibbs +# Contributors: http://linuxgsm.com/contrib +# Website: https://linuxgsm.com +# Description: Core functions for mods list/install/update/remove + +functionselfname="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")" + +# Files and Directories. +steam="${steamcmd}/steam" +workshopmodsdir="${serverfiles}/mods" +keysdir="${serverfiles}/keys" +workshopmodsdldir="${lgsmdir}/workshop" +workshopmodslist="workshop-mods.txt" +workshopmodslistfullpath="${configdir}/${workshopmodslist}" +workshopmoddownloaddir="${steam}/steamapps/workshop/content/${appid}" + +## Installation. +core_steamcmd.sh +fn_check_steamcmd_exec + +# # Download management. +# For Workshop Mod Downloads, we need to use game app id, not the server app id. +fn_workshop_download() { + local modid=$1 + local workshopmodsrcdir="${workshopmoddownloaddir}/${modid}" + + if [ -d "${steamcmddir}" ]; then + cd "${steamcmddir}" || exit + fi + + # Unbuffer will allow the output of steamcmd not buffer allowing a smooth output. + # unbuffer us part of the expect package. + if [ "$(command -v unbuffer)" ]; then + unbuffer="unbuffer" + fi + + # To do error checking for SteamCMD the output of steamcmd will be saved to a log. + steamcmdlog="${lgsmlogdir}/${selfname}-steamcmd.log" + + # clear previous steamcmd log + if [ -f "${steamcmdlog}" ]; then + rm -f "${steamcmdlog:?}" + fi + counter=0 + while [ "${counter}" == "0" ] || [ "${exitcode}" != "0" ]; do + counter=$((counter + 1)) + # Select SteamCMD parameters + ${unbuffer} ${steamcmdcommand} +force_install_dir "${workshopmodsdldir}" +login "${steamuser}" "${steampass}" +workshop_download_item "${gameappid}" "${modid}" +quit | uniq | tee -a "${lgsmlog}" "${steamcmdlog}" + + # Error checking for SteamCMD. Some errors will loop to try again and some will just exit. + # Check also if we have more errors than retries to be sure that we do not loop to many times and error out. + exitcode=$? + if [ -n "$(grep -i "Error!" "${steamcmdlog}" | tail -1)" ] && [ "$(grep -ic "Error!" "${steamcmdlog}")" -ge "${counter}" ]; then + # Not enough space. + if [ -n "$(grep "0x202" "${steamcmdlog}" | tail -1)" ]; then + fn_print_failure_nl "${commandaction} ${selfname}: ${remotelocation}: Not enough disk space to download workshop mod" + fn_script_log_fatal "${commandaction} ${selfname}: ${remotelocation}: Not enough disk space to download workshop mod" + core_exit.sh + # Not enough space. + elif [ -n "$(grep "0x212" "${steamcmdlog}" | tail -1)" ]; then + fn_print_failure_nl "${commandaction} ${selfname}: ${remotelocation}: Not enough disk space to download workshop mod" + fn_script_log_fatal "${commandaction} ${selfname}: ${remotelocation}: Not enough disk space to download workshop mod" + core_exit.sh + # Need tp purchase game. + elif [ -n "$(grep "No subscription" "${steamcmdlog}" | tail -1)" ]; then + fn_print_failure_nl "${commandaction} ${selfname}: ${remotelocation}: Steam account does not have a license for the required game" + fn_script_log_fatal "${commandaction} ${selfname}: ${remotelocation}: Steam account does not have a license for the required game" + core_exit.sh + # Two-factor authentication failure + elif [ -n "$(grep "Two-factor code mismatch" "${steamcmdlog}" | tail -1)" ]; then + fn_print_failure_nl "${commandaction} ${selfname}: ${remotelocation}: Two-factor authentication failure" + fn_script_log_fatal "${commandaction} ${selfname}: ${remotelocation}: Two-factor authentication failure" + core_exit.sh + # Incorrect Branch password + elif [ -n "$(grep "Password check for AppId" "${steamcmdlog}" | tail -1)" ]; then + fn_print_failure_nl "${commandaction} ${selfname}: ${remotelocation}: betapassword is incorrect" + fn_script_log_fatal "${commandaction} ${selfname}: ${remotelocation}: betapassword is incorrect" + core_exit.sh + # Update did not finish. + elif [ -n "$(grep "0x402" "${steamcmdlog}" | tail -1)" ] || [ -n "$(grep "0x602" "${steamcmdlog}" | tail -1)" ]; then + fn_print_error2_nl "${commandaction} ${selfname}: ${remotelocation}: Update required but not completed - check network" + fn_script_log_error "${commandaction} ${selfname}: ${remotelocation}: Update required but not completed - check network" + else + fn_print_error2_nl "${commandaction} ${selfname}: ${remotelocation}: Unknown error occured" + echo -en "Please provide content log to LinuxGSM developers https://linuxgsm.com/steamcmd-error" + fn_script_log_error "${commandaction} ${selfname}: ${remotelocation}: Unknown error occured" + fi + elif [ "${exitcode}" != "0" ]; then + fn_print_error2_nl "${commandaction} ${selfname}: ${remotelocation}: Exit code: ${exitcode}" + fn_script_log_error "${commandaction} ${selfname}: ${remotelocation}: Exit code: ${exitcode}" + else + fn_print_complete_nl "${commandaction} ${selfname}: ${remotelocation}" + fn_script_log_pass "${commandaction} ${selfname}: ${remotelocation}" + fi + + if [ "${counter}" -gt "10" ]; then + fn_print_failure_nl "${commandaction} ${selfname}: ${remotelocation}: Did not complete the download, too many retrys" + fn_script_log_fatal "${commandaction} ${selfname}: ${remotelocation}: Did not complete the download, too many retrys" + core_exit.sh + fi + done +} + +fn_workshop_get_list() { + workshoplist=($(echo "${workshopmods}" | tr ";" "\n")) +} + +fn_workshop_get_latest_mod_version() { + local modid="$1" + local serverresp="$(curl -s -d "itemcount=1&publishedfileids[0]=${modid}" "http://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1")" + local remupd= + if [[ "${serverresp}" =~ \"hcontent_file\":[[:space:]]*([^,]*) ]]; then + remupd="${BASH_REMATCH[1]}" + fi + echo "${remupd}" | tr -d '"' +} + +fn_workshop_get_name_from_steam() { + local modid="$1" + local serverresp="$(curl -s -d "itemcount=1&publishedfileids[0]=${modid}" "http://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1")" + local title= + if [[ "${serverresp}" =~ \"title\":[[:space:]]*([^,]*) ]]; then + title="${BASH_REMATCH[1]}" + fi + echo "${title}" | tr -d '"' +} + +fn_workshop_check_mod_update() { + local modid="$1" + if [ ! -f "${workshopmodsdldir}/steamapps/workshop/appworkshop_${gameappid}.acf" ]; then return 0; fi + local instmft="$(sed -n '/^\t"WorkshopItemsInstalled"$/,/^\t[}]$/{/^\t\t"'"${modid}"'"$/,/^\t\t[}]$/{s|^\t\t\t"manifest"\t\t"\(.*\)"$|\1|p}}' <"${workshopmodsdldir}/steamapps/workshop/appworkshop_${gameappid}.acf")" + if [ -z "${instmft}" ]; then return 0; fi + local remmft="$(fn_workshop_get_latest_mod_version "${modid}")" + if [[ -n "${remmft}" && "${instmft}" != "${remmft}" ]]; then + return 0 # true + fi + return 1 # false +} + +fn_workshop_is_mod_copy_needed(){ + local modid="$1" + local modsrc="${workshopmodsdldir}/steamapps/workshop/content/${gameappid}/${modid}" + if [ ! -f "${workshopmodsdir}/${modid}/meta.cpp" ]; then return 0; fi + local instmft="$(grep "timestamp" ${workshopmodsdir}/${modid}/meta.cpp)" + if [ -z "${instmft}" ]; then return 0; fi + local remmft="$(grep "timestamp" ${modsrc}/meta.cpp)" + if [[ -n "${remmft}" && "${instmft}" != "${remmft}" ]]; then + return 0 # true + fi + return 1 +} + + +fn_workshop_get_mod_name(){ + local modid="$1" + # Each game has different Steam Workshop structure, so mod id will be stored in a different place + if [ "${engine}" == "realvirtuality" ]; then + if ! [ -d "${workshopmodsdir}/${modid}" ]; then + echo "$(grep -Po '(?<=name = ").+?(?=")' ${workshopmodsdir}/${modid}/mod.cpp)" + elif ! [ -d "${workshopmodsdldir}/steamapps/workshop/content/${gameappid}/${modid}" ]; then + echo "$(grep -Po '(?<=name = ").+?(?=")' ${workshopmodsdldir}/steamapps/workshop/content/${gameappid}/${modid}/mod.cpp)" + else + echo "$(fn_workshop_get_name_from_steam ${modid})" + fi + else + echo "$(fn_workshop_get_name_from_steam ${modid})" + fi +} + +# Convert workshop mod files to lowercase if needed. +fn_workshop_lowercase() { + # local modid="$1" + # local modname="$(fn_workshop_get_mod_name $modid)" + # Arma 3 requires lowercase + if [ "${engine}" == "realvirtuality" ]; then + echo -en "Converting ${modname} files to lowercase..." + fn_sleep_time + fn_script_log_info "Converting ${modname} files to lowercase" + # Total files and directories for the mod, to output to the user + fileswc=$(find "${workshopmodsdir}" | wc -l) + # Total uppercase files and directories for the mod, to output to the user + filesupperwc=$(find "${workshopmodsdir}/" -name '*[[:upper:]]*' | wc -l) + fn_script_log_info "Found ${filesupperwc} uppercase files out of ${fileswc}, converting" + echo -en "Found ${filesupperwc} uppercase files out of ${fileswc}, converting..." + # + # Coudln't get this to work on WSL. Needs to be verified on an acutal linux server. + # + # Convert files and directories starting from the deepest to prevent issues (-depth argument) + while IFS= read -r -d '' src; do + # We have to convert only the last file from the path, otherwise we will fail to convert anything if a parent dir has any uppercase + # therefore, we have to separate the end of the filename to only lowercase it rather than the whole line + # Gather parent dir, filename lowercase filename, and set lowercase destination name + latestparentdir=$(dirname "${src}") + latestfilelc=$(basename "${src}" | tr '[:upper:]' '[:lower:]') + dst="${latestparentdir}/${latestfilelc}" + # Only convert if destination does not already exist for some reason + if [ ! -e "${dst}" ]; then + # Finally we can rename the file + mv "${src}" "${dst}" + # Exit if it fails for any reason + local exitcode=$? + if [ "${exitcode}" != 0 ]; then + fn_print_fail_eol_nl + core_exit.sh + fi + fi + done < <(find "${workshopmodsdir}" -depth -name '*[[:upper:]]*' -print0) + fn_print_ok_eol_nl + fi +} + +# # Copy the mod into serverfiles. +fn_workshop_copy_destination() { + local modid="$1" + local modname="$(fn_workshop_get_mod_name ${modid})" + if fn_workshop_is_mod_copy_needed ${modid}; then + echo "Copying mod ${modname} (${modid})" + # If workshop mod exists in installation folder, delete it for clean install + if [ -d "${workshopmodsdir}/${modid}" ]; then + rm -rf "${workshopmodsdir}/${modid}" + fi + modsrc="${workshopmodsdldir}/steamapps/workshop/content/${gameappid}/${modid}" + cp -fa ${modsrc} ${workshopmodsdir} + if [ "${engine}" == "realvirtuality" ]; then + modkey="${workshopmodsdldir}/steamapps/workshop/content/${gameappid}/${modid}/keys" + if ! [ -d "${modkey}" ]; then + modkey="$steamcmd/steamapps/workshop/content/${gameappid}/${modid}/Keys" + fi + if ! [ -d "${modkey}" ]; then + modkey="$steamcmd/steamapps/workshop/content/${gameappid}/${modid}/key" + fi + if ! [ -d "${modkey}" ]; then + modkey="$steamcmd/steamapps/workshop/content/${gameappid}/${modid}/Key" + fi + if ! [ -d "${modkey}" ]; then + echo "Mod ${modname} seems to be missing key folder. Tring to copy key from the main folder." + cp -fa "${workshopmodsdir}/${modid}/*.bikey" ${keysdir} + else + cp -fa ${modkey}/*.bikey ${keysdir} + fi + fi + else + echo "Mod ${modname} is already in mods folder." + fi +} + +# ## Directory management. + +# Create mods files and directories if it doesn't exist. +fn_create_workshop_dir() { + # Create lgsm data modsdir. + if [ ! -d "${workshopmodsdldir}" ]; then + echo -en "creating LinuxGSM Steam Workshop data directory ${workshopmodsdldir}..." + mkdir -p "${workshopmodsdldir}" + exitcode=$? + if [ "${exitcode}" != 0 ]; then + fn_print_fail_eol_nl + fn_script_log_fatal "Creating mod download dir ${workshopmodsdldir}" + core_exit.sh + else + fn_print_ok_eol_nl + fn_script_log_pass "Creating mod download dir ${workshopmodsdldir}" + fi + fi + # Create mod install directory. + if [ ! -d "${workshopmodsdir}" ]; then + echo -en "creating Steam Workshop install directory ${workshopmodsdir}..." + mkdir -p "${workshopmodsdir}" + exitcode=$? + if [ "${exitcode}" != 0 ]; then + fn_print_fail_eol_nl + fn_script_log_fatal "Creating mod install directory ${workshopmodsdir}" + core_exit.sh + else + fn_print_ok_eol_nl + fn_script_log_pass "Creating mod install directory ${workshopmodsdir}" + fi + fi +} + +# Counts how many mods were installed. +fn_workshop_count_installed() { + if [ -f "${workshopmodsdir}" ]; then + installedworkshopmodscount=$(ls -l "${workshopmodsdir}" | grep -c ^d) + else + installedworkshopmodscount=0 + fi +} + +# Exits if no mods were installed. +fn_workshop_check_installed() { + # Count installed mods. + fn_workshop_count_installed + # If no mods are found. + if [ ${installedworkshopmodscount}/* -eq 0 ]; then + echo -e "" + fn_print_failure_nl "No installed workshop mods or addons were found" + echo -e " * Install mods using LinuxGSM first with: ./${selfname} workshop-install" + fn_script_log_error "No installed workshop mods or addons were found." + core_exit.sh + fi +} + +# Builds list of installed Steam Workshop mods. +fn_workshop_installed_list() { + fn_workshop_count_installed + for folder in ${workshopmodsdir}/*; do + # If it is a folder, then use it's name as Steam Workshop Mod Id + if [ -d "${folder}" ]; then + echo -e "$(fn_workshop_get_mod_name $(basename ${f})) ($(basename ${f}))" + fi + done + if [ "${installedworkshopmodscount}" ]; then + fn_script_log_info "${installedworkshopmodscount} addons/mods are currently installed" + fi +}