workshop_core.sh 23 KB

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