workshop_core.sh 18 KB

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