workshop_core.sh 23 KB

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