workshop_core.sh 22 KB

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