workshop_core.sh 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. #!/bin/bash
  2. # LinuxGSM workshop_core.sh module
  3. # Author: Daniel Gibbs
  4. # Contributors: https://linuxgsm.com/contrib
  5. # Website: https://linuxgsm.com
  6. # Description: Downloads and installs Steam Workshop mods for game servers that cannot download them.
  7. moduleselfname="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")"
  8. # Installed mods are named ${workshopprefix}<id> in the directory the game loads them from.
  9. if [ "${shortname}" == "squad" ] || [ "${shortname}" == "squad44" ]; then
  10. workshopdir="${systemdir}/Plugins/Mods"
  11. workshopprefix=""
  12. elif [ "${shortname}" == "ohd" ]; then
  13. workshopdir="${systemdir}/Mods"
  14. workshopprefix=""
  15. workshopflatten=1
  16. elif [ "${shortname}" == "bt" ]; then
  17. workshopdir="${serverfiles}/LocalMods"
  18. workshopprefix=""
  19. elif [ "${shortname}" == "st" ]; then
  20. workshopdir="${serverfiles}/mods"
  21. workshopprefix=""
  22. else
  23. workshopdir="${serverfiles}/workshop"
  24. workshopprefix="@"
  25. fi
  26. workshopdldir="${tmpdir}/workshop"
  27. workshopmodlist="${datadir}/${selfname}-workshop.txt"
  28. workshopapi="https://api.steampowered.com/ISteamRemoteStorage"
  29. workshopapimax="100"
  30. workshopretries="5"
  31. # Converts workshopmods into an ordered list of IDs. Accepts space, comma or semicolon separators.
  32. fn_workshop_config_ids() {
  33. workshopconfigids=()
  34. local id
  35. for id in ${workshopmods//[,;]/ }; do
  36. if [[ "${id}" =~ ^[0-9]+$ ]]; then
  37. workshopconfigids+=("${id}")
  38. else
  39. fn_print_warn_nl "Ignoring invalid Workshop ID: ${id}"
  40. fn_script_log_warn "Ignoring invalid Workshop ID: ${id}"
  41. fi
  42. done
  43. }
  44. # Queries the Steam Web API. Usage: fn_workshop_api <GetCollectionDetails|GetPublishedFileDetails> <id>...
  45. fn_workshop_api() {
  46. local method="$1"
  47. shift
  48. local countparam="itemcount"
  49. if [ "${method}" == "GetCollectionDetails" ]; then
  50. countparam="collectioncount"
  51. fi
  52. local postdata=("--data-urlencode" "${countparam}=$#")
  53. local index=0
  54. local id
  55. for id in "$@"; do
  56. postdata+=("--data-urlencode" "publishedfileids[${index}]=${id}")
  57. index=$((index + 1))
  58. done
  59. curl -sSL --connect-timeout 10 --max-time 60 "${postdata[@]}" "${workshopapi}/${method}/v1/"
  60. }
  61. # Records which IDs are collections and their children, in batches.
  62. fn_workshop_get_collections() {
  63. local ids=("$@")
  64. local start response
  65. for ((start = 0; start < ${#ids[@]}; start += workshopapimax)); do
  66. if ! response="$(fn_workshop_api GetCollectionDetails "${ids[@]:start:workshopapimax}")" || ! jq -e '.response' <<< "${response}" > /dev/null 2>&1; then
  67. fn_print_fail_nl "Checking for update: ${remotelocation}: Unable to get collection details from Steam"
  68. fn_script_log_fail "Checking for update: ${remotelocation}: Unable to get collection details from Steam"
  69. core_exit.sh
  70. fi
  71. local id result children
  72. while IFS=$'\t' read -r id result children; do
  73. workshopchecked["${id}"]=1
  74. if [ "${result}" == "1" ]; then
  75. workshopchildren["${id}"]="${children}"
  76. fi
  77. done < <(jq -r '.response.collectiondetails[] | [.publishedfileid, (.result | tostring), ([.children[]? | "\(.publishedfileid):\(.filetype)"] | join(" "))] | @tsv' <<< "${response}")
  78. done
  79. }
  80. # Adds an ID to workshopitems, expanding collections in their sort order.
  81. fn_workshop_expand() {
  82. local id="$1"
  83. local depth="$2"
  84. if [ -n "${workshopseen[${id}]}" ]; then
  85. return
  86. fi
  87. workshopseen["${id}"]=1
  88. if [ -n "${workshopchildren[${id}]+set}" ]; then
  89. if [ "${depth}" -gt 3 ]; then
  90. fn_print_warn_nl "Skipping Workshop collection ${id}: collections are nested too deeply"
  91. fn_script_log_warn "Skipping Workshop collection ${id}: collections are nested too deeply"
  92. return
  93. fi
  94. local child
  95. for child in ${workshopchildren[${id}]}; do
  96. fn_workshop_expand "${child%%:*}" $((depth + 1))
  97. done
  98. else
  99. workshopitems+=("${id}")
  100. fi
  101. }
  102. # Builds workshopitems: the ordered list of Workshop items to install, with collections expanded.
  103. fn_workshop_resolve() {
  104. declare -gA workshopchecked=()
  105. declare -gA workshopchildren=()
  106. declare -gA workshopseen=()
  107. workshopitems=()
  108. fn_workshop_config_ids
  109. if [ "${#workshopconfigids[@]}" -eq 0 ]; then
  110. return
  111. fi
  112. local tocheck=("${workshopconfigids[@]}")
  113. local round id child
  114. for ((round = 0; round < 4 && ${#tocheck[@]} > 0; round++)); do
  115. fn_workshop_get_collections "${tocheck[@]}"
  116. local nested=()
  117. for id in "${tocheck[@]}"; do
  118. for child in ${workshopchildren[${id}]}; do
  119. if [ "${child##*:}" == "2" ] && [ -z "${workshopchecked[${child%%:*}]}" ]; then
  120. nested+=("${child%%:*}")
  121. fi
  122. done
  123. done
  124. tocheck=("${nested[@]}")
  125. done
  126. for id in "${workshopconfigids[@]}"; do
  127. fn_workshop_expand "${id}" 0
  128. done
  129. }
  130. # Gets title, game appid and last updated time for each item in workshopitems.
  131. fn_workshop_get_details() {
  132. declare -gA workshoptitle=()
  133. declare -gA workshopappid=()
  134. declare -gA workshoptimeupdated=()
  135. local validitems=()
  136. local start response
  137. for ((start = 0; start < ${#workshopitems[@]}; start += workshopapimax)); do
  138. if ! response="$(fn_workshop_api GetPublishedFileDetails "${workshopitems[@]:start:workshopapimax}")" || ! jq -e '.response' <<< "${response}" > /dev/null 2>&1; then
  139. fn_print_fail_nl "Checking for update: ${remotelocation}: Unable to get Workshop item details from Steam"
  140. fn_script_log_fail "Checking for update: ${remotelocation}: Unable to get Workshop item details from Steam"
  141. core_exit.sh
  142. fi
  143. local id result appid timeupdated title
  144. while IFS=$'\t' read -r id result appid timeupdated title; do
  145. if [ "${result}" != "1" ] || [ -z "${timeupdated}" ]; then
  146. fn_print_warn_nl "Skipping Workshop item ${id}: not found, or not public"
  147. fn_script_log_warn "Skipping Workshop item ${id}: not found, or not public"
  148. continue
  149. fi
  150. workshoptitle["${id}"]="${title}"
  151. workshopappid["${id}"]="${appid}"
  152. workshoptimeupdated["${id}"]="${timeupdated}"
  153. done < <(jq -r '.response.publishedfiledetails[] | [.publishedfileid, (.result | tostring), (.consumer_app_id // "" | tostring), (.time_updated // "" | tostring), (.title // "" | gsub("[\t\n\r]"; " "))] | @tsv' <<< "${response}")
  154. done
  155. local item
  156. for item in "${workshopitems[@]}"; do
  157. if [ -n "${workshoptimeupdated[${item}]}" ]; then
  158. validitems+=("${item}")
  159. fi
  160. done
  161. workshopitems=("${validitems[@]}")
  162. }
  163. # Prints the directory a Workshop item is installed in, if it is installed.
  164. fn_workshop_moddir() {
  165. local statefile
  166. if [ -f "${workshopdir}/${workshopprefix}${1}/.lgsm-workshop" ]; then
  167. echo "${workshopdir}/${workshopprefix}${1}"
  168. return
  169. fi
  170. for statefile in "${workshopdir}"/*/.lgsm-workshop; do
  171. if [ -f "${statefile}" ] && grep -qx "id=${1}" "${statefile}"; then
  172. echo "${statefile%/.lgsm-workshop}"
  173. return
  174. fi
  175. done
  176. }
  177. # Reads a value from an installed mod's state file. Usage: fn_workshop_state <id> <key>
  178. fn_workshop_state() {
  179. local moddir
  180. moddir="$(fn_workshop_moddir "${1}")"
  181. if [ -n "${moddir}" ]; then
  182. sed -n "s/^${2}=//p" "${moddir}/.lgsm-workshop"
  183. fi
  184. }
  185. # Works out which mods need downloading and which installed mods are no longer used.
  186. fn_workshop_compare() {
  187. workshopupdates=()
  188. workshopremovals=()
  189. local item
  190. for item in "${workshopitems[@]}"; do
  191. if [ "$(fn_workshop_state "${item}" timeupdated)" != "${workshoptimeupdated[${item}]}" ]; then
  192. workshopupdates+=("${item}")
  193. fi
  194. done
  195. if [ -d "${workshopdir}" ]; then
  196. # Mods used by any instance sharing these serverfiles are kept.
  197. local inuse=" ${workshopitems[*]} "
  198. local listfile
  199. for listfile in "${datadir}"/*-workshop.txt; do
  200. if [ -f "${listfile}" ] && [ "${listfile}" != "${workshopmodlist}" ]; then
  201. inuse+="$(grep -v '^#' "${listfile}" | tr '\n' ' ') "
  202. fi
  203. done
  204. local statefile moddir
  205. for statefile in "${workshopdir}"/*/.lgsm-workshop; do
  206. if [ -f "${statefile}" ]; then
  207. item="$(sed -n 's/^id=//p' "${statefile}")"
  208. if [ -z "${item}" ]; then
  209. moddir="${statefile%/*}"
  210. moddir="${moddir##*/}"
  211. item="${moddir#"${workshopprefix}"}"
  212. fi
  213. if [[ "${inuse}" != *" ${item} "* ]]; then
  214. workshopremovals+=("${item}")
  215. fi
  216. fi
  217. done
  218. fi
  219. }
  220. # Downloads Workshop items with a single SteamCMD login. Failed items are retried.
  221. fn_workshop_download() {
  222. local pending=("$@")
  223. local attempt=0
  224. local item params
  225. steamcmdlog="${lgsmlogdir}/${selfname}-steamcmd.log"
  226. rm -rf "${workshopdldir:?}"
  227. mkdir -p "${workshopdldir}"
  228. if [ -d "${steamcmddir}" ]; then
  229. cd "${steamcmddir}" || exit
  230. fi
  231. unbuffercommand=()
  232. if [ "$(command -v unbuffer 2> /dev/null)" ]; then
  233. unbuffercommand=("unbuffer")
  234. fi
  235. while [ "${#pending[@]}" -gt 0 ] && [ "${attempt}" -lt "${workshopretries}" ]; do
  236. attempt=$((attempt + 1))
  237. params=(+force_install_dir "${workshopdldir}" +login "${steamuser}" "${steampass}")
  238. for item in "${pending[@]}"; do
  239. params+=(+workshop_download_item "${workshopappid[${item}]}" "${item}")
  240. done
  241. params+=(+quit)
  242. rm -f "${steamcmdlog:?}"
  243. "${unbuffercommand[@]}" "${steamcmdcommand}" "${params[@]}" | uniq | tee -a "${lgsmlog}" "${steamcmdlog}"
  244. if grep -q "FAILED (" "${steamcmdlog}" || grep -q "Two-factor code mismatch" "${steamcmdlog}"; then
  245. fn_print_failure_nl "${commandaction} ${selfname}: ${remotelocation}: Steam login failed"
  246. fn_print_nl "Check steamuser and steampass in ${configdirserver}"
  247. fn_script_log_fail "${commandaction} ${selfname}: ${remotelocation}: Steam login failed"
  248. core_exit.sh
  249. fi
  250. local failed=()
  251. for item in "${pending[@]}"; do
  252. if [ -z "$(find "${workshopdldir}/steamapps/workshop/content/${workshopappid[${item}]}/${item}" -mindepth 1 -print -quit 2> /dev/null)" ]; then
  253. failed+=("${item}")
  254. fi
  255. done
  256. pending=("${failed[@]}")
  257. if [ "${#pending[@]}" -gt 0 ] && [ "${attempt}" -lt "${workshopretries}" ]; then
  258. fn_print_error2_nl "${commandaction} ${selfname}: ${remotelocation}: ${#pending[@]} item(s) did not download, retrying"
  259. fn_script_log_error "${commandaction} ${selfname}: ${remotelocation}: ${#pending[@]} item(s) did not download, retrying"
  260. fi
  261. done
  262. workshopfailed=("${pending[@]}")
  263. if [ "${#workshopfailed[@]}" -gt 0 ]; then
  264. fn_print_failure_nl "${commandaction} ${selfname}: ${remotelocation}: ${#workshopfailed[@]} item(s) failed to download"
  265. for item in "${workshopfailed[@]}"; do
  266. fn_print_nl "* ${workshoptitle[${item}]} (${item})"
  267. fn_script_log_fail "${commandaction} ${selfname}: ${remotelocation}: ${workshoptitle[${item}]} (${item}) failed to download"
  268. done
  269. fn_print_nl "The Steam account must own the game for its Workshop items to download"
  270. fi
  271. }
  272. # Renames files and directories in a mod to lowercase, deepest first.
  273. fn_workshop_lowercase() {
  274. local path name lower
  275. while IFS= read -r -d '' path; do
  276. name="${path##*/}"
  277. lower="${name,,}"
  278. if [ -e "${path%/*}/${lower}" ]; then
  279. fn_script_log_warn "Lowercase name already exists, not renaming: ${path}"
  280. else
  281. mv "${path}" "${path%/*}/${lower}"
  282. fi
  283. done < <(find "$1" -mindepth 1 -depth -name '*[[:upper:]]*' -print0)
  284. }
  285. # Removes the BattlEye keys a mod installed, unless another installed mod also uses them.
  286. fn_workshop_remove_keys() {
  287. local item="$1"
  288. local key statefile moddir
  289. moddir="$(fn_workshop_moddir "${item}")"
  290. for key in $(fn_workshop_state "${item}" keys); do
  291. local shared=""
  292. for statefile in "${workshopdir}"/*/.lgsm-workshop; do
  293. if [ "${statefile}" != "${moddir}/.lgsm-workshop" ] && [[ " $(sed -n 's/^keys=//p' "${statefile}" 2> /dev/null) " == *" ${key} "* ]]; then
  294. shared=1
  295. fi
  296. done
  297. if [ -z "${shared}" ]; then
  298. rm -f "${serverfiles:?}/keys/${key}"
  299. fi
  300. done
  301. }
  302. # Removes the links a mod added to the Starbound mods directory.
  303. fn_workshop_remove_links() {
  304. local link
  305. for link in $(fn_workshop_state "$1" links); do
  306. rm -f "${serverfiles:?}/mods/${link}"
  307. done
  308. }
  309. # Moves a downloaded item into the game's mod directory and prepares it for the game.
  310. fn_workshop_install() {
  311. local item="$1"
  312. local src="${workshopdldir}/steamapps/workshop/content/${workshopappid[${item}]}/${item}"
  313. local dst="${workshopdir}/${workshopprefix}${item}"
  314. local installed
  315. local keys=()
  316. local links=()
  317. local key pak
  318. fn_print_dots "Installing ${workshoptitle[${item}]} (${item})"
  319. installed="$(fn_workshop_moddir "${item}")"
  320. if [ -n "${installed}" ]; then
  321. fn_workshop_remove_keys "${item}"
  322. fn_workshop_remove_links "${item}"
  323. rm -rf "${installed:?}"
  324. fi
  325. # A mod that is a single plugin folder is installed as that folder, for games that load mods by folder name.
  326. if [ -n "${workshopflatten}" ]; then
  327. local subdirs=()
  328. mapfile -t subdirs < <(find "${src}" -mindepth 1 -maxdepth 1 -type d)
  329. if [ "${#subdirs[@]}" -eq 1 ] && [ -z "$(find "${src}" -mindepth 1 -maxdepth 1 -type f -print -quit)" ]; then
  330. dst="${workshopdir}/${subdirs[0]##*/}"
  331. src="${subdirs[0]}"
  332. fi
  333. fi
  334. if [ -e "${dst}" ]; then
  335. fn_print_fail_nl "Installing ${workshoptitle[${item}]} (${item}): ${dst} already exists"
  336. fn_script_log_fail "Installing ${workshoptitle[${item}]} (${item}): ${dst} already exists"
  337. return
  338. fi
  339. mkdir -p "${workshopdir}"
  340. if ! mv "${src}" "${dst}"; then
  341. fn_print_fail_nl "Installing ${workshoptitle[${item}]} (${item})"
  342. fn_script_log_fail "Installing ${workshoptitle[${item}]} (${item})"
  343. core_exit.sh
  344. fi
  345. if [ "${shortname}" == "arma3" ] || [ "${shortname}" == "dayz" ]; then
  346. fn_workshop_lowercase "${dst}"
  347. mkdir -p "${serverfiles}/keys"
  348. while IFS= read -r -d '' key; do
  349. cp -f "${key}" "${serverfiles}/keys/"
  350. keys+=("${key##*/}")
  351. done < <(find "${dst}" -type f -name '*.bikey' -print0)
  352. elif [ "${shortname}" == "sb" ]; then
  353. mkdir -p "${serverfiles}/mods"
  354. while IFS= read -r -d '' pak; do
  355. ln -sfn "../workshop/${workshopprefix}${item}/${pak##*/}" "${serverfiles}/mods/workshop-${item}-${pak##*/}"
  356. links+=("workshop-${item}-${pak##*/}")
  357. done < <(find "${dst}" -maxdepth 1 -type f -name '*.pak' -print0)
  358. fi
  359. {
  360. echo "id=${item}"
  361. echo "title=${workshoptitle[${item}]}"
  362. echo "timeupdated=${workshoptimeupdated[${item}]}"
  363. echo "keys=${keys[*]}"
  364. echo "links=${links[*]}"
  365. } > "${dst}/.lgsm-workshop"
  366. fn_print_ok_nl "Installing ${workshoptitle[${item}]} (${item})"
  367. fn_script_log_pass "Installing ${workshoptitle[${item}]} (${item})"
  368. }
  369. # Removes an installed mod that is no longer in workshopmods.
  370. fn_workshop_remove() {
  371. local item="$1"
  372. local title moddir
  373. title="$(fn_workshop_state "${item}" title)"
  374. moddir="$(fn_workshop_moddir "${item}")"
  375. fn_print_dots "Removing ${title} (${item})"
  376. fn_workshop_remove_keys "${item}"
  377. fn_workshop_remove_links "${item}"
  378. if [ -n "${moddir}" ]; then
  379. rm -rf "${moddir:?}"
  380. fi
  381. fn_print_ok_nl "Removing ${title} (${item})"
  382. fn_script_log_pass "Removing ${title} (${item})"
  383. }
  384. # Barotrauma only loads mods listed in config_player.xml. Replaces LinuxGSM's entries with the installed mods, in order.
  385. fn_workshop_bt_packages() {
  386. local config="${serverfiles}/config_player.xml"
  387. local item managed=() enabled=()
  388. if [ ! -f "${config}" ]; then
  389. fn_print_warn_nl "${config} not found. Start the server once, then run ./${selfname} workshop-update to enable Workshop mods"
  390. fn_script_log_warn "${config} not found, Workshop mods not enabled"
  391. return
  392. fi
  393. managed=("${workshopremovals[@]}" "${workshopitems[@]}")
  394. for item in "${workshopitems[@]}"; do
  395. if [ -f "${workshopdir}/${item}/filelist.xml" ]; then
  396. enabled+=("${item}")
  397. fi
  398. done
  399. python3 - "${config}" "${managed[*]}" "${enabled[*]}" << 'PYEOF'
  400. import re
  401. import sys
  402. config, managed, enabled = sys.argv[1], sys.argv[2].split(), sys.argv[3].split()
  403. with open(config, encoding="utf-8-sig", newline="") as f:
  404. text = f.read()
  405. text = text.replace("<regularpackages />", "<regularpackages>\n </regularpackages>")
  406. for item in managed:
  407. text = re.sub(r'\s*<package\b[^>]*?path="LocalMods/' + re.escape(item) + r'/filelist\.xml"[^>]*/>', "", text)
  408. entries = "".join('\n <package\n path="LocalMods/' + item + '/filelist.xml" />' for item in enabled)
  409. text = re.sub(r"(\s*)</regularpackages>", entries + r"\1</regularpackages>", text, count=1)
  410. text = re.sub(r"<regularpackages>\s*</regularpackages>", "<regularpackages />", text)
  411. with open(config, "w", encoding="utf-8", newline="") as f:
  412. f.write(text)
  413. PYEOF
  414. }
  415. # Installs downloaded items and removes unused ones. Downloads happen first, so the server only stops for this step.
  416. fn_workshop_apply() {
  417. local item
  418. for item in "${workshopinstalls[@]}"; do
  419. fn_workshop_install "${item}"
  420. done
  421. for item in "${workshopremovals[@]}"; do
  422. fn_workshop_remove "${item}"
  423. done
  424. if [ "${shortname}" == "bt" ]; then
  425. fn_workshop_bt_packages
  426. fi
  427. rm -rf "${workshopdldir:?}"
  428. }
  429. # Saves the resolved mod order so start can build the mod list without querying Steam.
  430. fn_workshop_save_list() {
  431. if [ "${#workshopitems[@]}" -eq 0 ]; then
  432. rm -f "${workshopmodlist}"
  433. return
  434. fi
  435. {
  436. echo "# workshopmods=${workshopmods}"
  437. printf '%s\n' "${workshopitems[@]}"
  438. } > "${workshopmodlist}"
  439. }
  440. # Checks for Workshop mod updates and, for update commands, applies them.
  441. fn_workshop_update() {
  442. remotelocation="Steam Workshop"
  443. fn_print_dots "Checking for update: ${remotelocation}"
  444. fn_workshop_resolve
  445. if [ "${#workshopitems[@]}" -gt 0 ]; then
  446. fn_workshop_get_details
  447. fi
  448. fn_workshop_compare
  449. fn_print_ok_nl "Checking for update: ${remotelocation}"
  450. fn_script_log_info "Checking for update: ${remotelocation}"
  451. local item
  452. if [ "${#workshopupdates[@]}" -eq 0 ] && [ "${#workshopremovals[@]}" -eq 0 ]; then
  453. fn_print "\n"
  454. fn_print_nl "${bold}${underline}No update${default} available"
  455. fn_print_nl "* Workshop mods: ${#workshopitems[@]}"
  456. fn_print "\n"
  457. fn_script_log_info "No update available: ${remotelocation}"
  458. if [ "${commandname}" != "CHECK-UPDATE" ]; then
  459. fn_workshop_save_list
  460. fi
  461. return
  462. fi
  463. fn_print "\n"
  464. fn_print_nl "${bold}${underline}Update${default} available"
  465. for item in "${workshopupdates[@]}"; do
  466. if [ -n "$(fn_workshop_moddir "${item}")" ]; then
  467. fn_print_nl "* Update: ${workshoptitle[${item}]} (${item})"
  468. else
  469. fn_print_nl "* Install: ${workshoptitle[${item}]} (${item})"
  470. fi
  471. fn_script_log_info "Update available: ${workshoptitle[${item}]} (${item})"
  472. done
  473. for item in "${workshopremovals[@]}"; do
  474. fn_print_nl "* Remove: $(fn_workshop_state "${item}" title) (${item})"
  475. fn_script_log_info "Remove: $(fn_workshop_state "${item}" title) (${item})"
  476. done
  477. fn_print "\n"
  478. if [ "${commandname}" == "CHECK-UPDATE" ]; then
  479. return
  480. fi
  481. fn_workshop_save_list
  482. date '+%s' > "${lockdir:?}/update.lock"
  483. workshopinstalls=()
  484. workshopfailed=()
  485. if [ "${#workshopupdates[@]}" -gt 0 ]; then
  486. fn_print_start_nl "${remotelocation}"
  487. fn_workshop_download "${workshopupdates[@]}"
  488. for item in "${workshopupdates[@]}"; do
  489. if [[ " ${workshopfailed[*]} " != *" ${item} "* ]]; then
  490. workshopinstalls+=("${item}")
  491. fi
  492. done
  493. fi
  494. if [ "${#workshopinstalls[@]}" -eq 0 ] && [ "${#workshopremovals[@]}" -eq 0 ]; then
  495. rm -rf "${workshopdldir:?}"
  496. return
  497. fi
  498. check_status.sh
  499. if [ "${status}" == "0" ]; then
  500. fn_workshop_apply
  501. else
  502. fn_print_restart_warning
  503. exitbypass=1
  504. command_stop.sh
  505. fn_firstcommand_reset
  506. fn_workshop_apply
  507. exitbypass=1
  508. command_start.sh
  509. fn_firstcommand_reset
  510. fi
  511. unset exitbypass
  512. }
  513. # Adds installed Workshop mods to the mods start parameter, in workshopmods order.
  514. fn_workshop_mods_startparameters() {
  515. if [ -n "${workshopmodsadded}" ]; then
  516. return
  517. fi
  518. workshopmodsadded=1
  519. local items=()
  520. if [ -f "${workshopmodlist}" ]; then
  521. mapfile -t items < <(grep -v '^#' "${workshopmodlist}")
  522. if [ "$(sed -n 's/^# workshopmods=//p' "${workshopmodlist}")" != "${workshopmods}" ]; then
  523. fn_print_warn_nl "workshopmods has changed. Run ./${selfname} workshop-update to apply it"
  524. fn_script_log_warn "workshopmods has changed. Run ./${selfname} workshop-update to apply it"
  525. fi
  526. else
  527. fn_workshop_config_ids
  528. items=("${workshopconfigids[@]}")
  529. fi
  530. local workshopmodsparam=""
  531. local missing=0
  532. local item
  533. for item in "${items[@]}"; do
  534. if [ -d "${workshopdir}/${workshopprefix}${item}" ]; then
  535. workshopmodsparam+="${workshopmodsparam:+\\;}workshop/${workshopprefix}${item}"
  536. else
  537. missing=$((missing + 1))
  538. fi
  539. done
  540. if [ "${missing}" -gt 0 ]; then
  541. fn_print_warn_nl "${missing} Workshop mod(s) not installed. Run ./${selfname} workshop-update to install them"
  542. fn_script_log_warn "${missing} Workshop mod(s) not installed"
  543. fi
  544. if [ -n "${workshopmodsparam}" ]; then
  545. mods="${mods:+${mods}\\;}${workshopmodsparam}"
  546. fi
  547. }