Explorar el Código

Release v26.3.0 (#5007)

Merge branch release/v26.3.0 into master.
Daniel Gibbs hace 1 semana
padre
commit
ab3145948f
Se han modificado 78 ficheros con 1535 adiciones y 287 borrados
  1. 4 0
      .editorconfig
  2. 11 0
      .github/linters/.ruff.toml
  3. 44 44
      .github/scripts/details-check-generate-matrix.sh
  4. 66 0
      .github/scripts/details-check-run.sh
  5. 1 1
      .github/scripts/sync-game-labels.sh
  6. 53 86
      .github/workflows/action-details-check.yml
  7. 9 3
      .github/workflows/action-issue-triage-automation.yml
  8. 1 1
      .github/workflows/action-prettier.yml
  9. 1 1
      .github/workflows/action-serverlist-validate.yml
  10. 2 2
      .github/workflows/action-super-linter.yml
  11. 2 6
      .github/workflows/action-update-copyright-years-in-license-file.yml
  12. 1 1
      .github/workflows/action-version-check.yml
  13. 1 1
      .github/workflows/ai-triage.yml
  14. 1 1
      .github/workflows/sync-game-labels.yml
  15. 1 1
      lgsm/config-default/config-lgsm/colserver/_default.cfg
  16. 6 1
      lgsm/config-default/config-lgsm/csgoserver/_default.cfg
  17. 1 1
      lgsm/config-default/config-lgsm/fctrserver/_default.cfg
  18. 195 0
      lgsm/config-default/config-lgsm/hytserver/_default.cfg
  19. 1 1
      lgsm/config-default/config-lgsm/pzserver/_default.cfg
  20. 199 0
      lgsm/config-default/config-lgsm/rsdwserver/_default.cfg
  21. 3 4
      lgsm/config-default/config-lgsm/sfserver/_default.cfg
  22. 3 0
      lgsm/config-default/config-lgsm/vhserver/_default.cfg
  23. 4 2
      lgsm/data/almalinux-8.csv
  24. 4 2
      lgsm/data/almalinux-9.csv
  25. 4 2
      lgsm/data/centos-7.csv
  26. 4 2
      lgsm/data/centos-8.csv
  27. 4 2
      lgsm/data/centos-9.csv
  28. 4 2
      lgsm/data/debian-10.csv
  29. 4 2
      lgsm/data/debian-11.csv
  30. 4 2
      lgsm/data/debian-12.csv
  31. 4 2
      lgsm/data/debian-13.csv
  32. 4 2
      lgsm/data/debian-9.csv
  33. BIN
      lgsm/data/gameicons/hyt-icon.png
  34. BIN
      lgsm/data/gameicons/rsdw-icon.png
  35. BIN
      lgsm/data/gameicons/sm-icon.png
  36. 4 2
      lgsm/data/rhel-7.csv
  37. 4 2
      lgsm/data/rhel-8.csv
  38. 4 2
      lgsm/data/rhel-9.csv
  39. 4 2
      lgsm/data/rocky-8.csv
  40. 4 2
      lgsm/data/rocky-9.csv
  41. 2 0
      lgsm/data/serverlist.csv
  42. 4 2
      lgsm/data/ubuntu-16.04.csv
  43. 4 2
      lgsm/data/ubuntu-18.04.csv
  44. 4 2
      lgsm/data/ubuntu-20.04.csv
  45. 4 2
      lgsm/data/ubuntu-22.04.csv
  46. 4 2
      lgsm/data/ubuntu-23.04.csv
  47. 4 2
      lgsm/data/ubuntu-23.10.csv
  48. 4 2
      lgsm/data/ubuntu-24.04.csv
  49. 4 2
      lgsm/data/ubuntu-26.04.csv
  50. 13 0
      lgsm/modules/alert.sh
  51. 26 8
      lgsm/modules/check_deps.sh
  52. 6 0
      lgsm/modules/check_ip.sh
  53. 4 0
      lgsm/modules/check_system_requirements.sh
  54. 2 0
      lgsm/modules/command_check_update.sh
  55. 2 0
      lgsm/modules/command_dev_parse_game_details.sh
  56. 5 1
      lgsm/modules/command_fastdl.sh
  57. 19 0
      lgsm/modules/command_mods_install.sh
  58. 5 0
      lgsm/modules/command_mods_remove.sh
  59. 8 0
      lgsm/modules/command_mods_update.sh
  60. 10 10
      lgsm/modules/command_monitor.sh
  61. 59 0
      lgsm/modules/command_start.sh
  62. 2 0
      lgsm/modules/command_update.sh
  63. 16 1
      lgsm/modules/core_dl.sh
  64. 3 3
      lgsm/modules/core_getopt.sh
  65. 3 1
      lgsm/modules/core_messages.sh
  66. 6 1
      lgsm/modules/core_modules.sh
  67. 22 3
      lgsm/modules/fix_csgo.sh
  68. 54 4
      lgsm/modules/info_game.sh
  69. 28 4
      lgsm/modules/info_messages.sh
  70. 8 0
      lgsm/modules/install_config.sh
  71. 3 1
      lgsm/modules/install_factorio_save.sh
  72. 4 0
      lgsm/modules/install_gslt.sh
  73. 3 0
      lgsm/modules/install_server_files.sh
  74. 108 3
      lgsm/modules/mods_core.sh
  75. 74 29
      lgsm/modules/mods_list.sh
  76. 1 3
      lgsm/modules/query_gsquery.py
  77. 307 0
      lgsm/modules/update_bb.sh
  78. 34 14
      linuxgsm.sh

+ 4 - 0
.editorconfig

@@ -30,3 +30,7 @@ indent_size = 4
 [*.py]
 indent_style = space
 indent_size = 4
+
+# Markdown Files
+[*.md]
+indent_size = unset

+ 11 - 0
.github/linters/.ruff.toml

@@ -0,0 +1,11 @@
+# Store the cache in a temporary location
+cache-dir = "/tmp/.ruff_cache"
+
+# Same as Black
+line-length = 88
+
+# Oldest Python on supported distros (Ubuntu 20.04)
+target-version = "py38"
+
+[lint]
+ignore = ["E203"]

+ 44 - 44
.github/scripts/details-check-generate-matrix.sh

@@ -1,50 +1,50 @@
 #!/bin/bash
+# Splits the servers to check into shards (one job each) and a list of legacy servers.
+# Legacy servers need an older glibc than the hosted runners have, so they run in an ubuntu:20.04 container.
+# Writes shards.json (matrix) and legacy.txt (space-separated shortnames).
 
 ref="${LGSM_REF:-${GITHUB_REF#refs/heads/}}"
-curl "https://raw.githubusercontent.com/GameServerManagers/LinuxGSM/${ref}/lgsm/data/serverlist.csv" | grep -v '^[[:blank:]]*$' > serverlist.csv
+maxshards="${DETAILS_CHECK_SHARDS:-12}"
+legacyservers="bfv bf1942 btl onset"
 
-echo -n "{" > "shortnamearray.json"
-echo -n "\"include\":[" >> "shortnamearray.json"
+if [ -f lgsm/data/serverlist.csv ]; then
+	serverlist="lgsm/data/serverlist.csv"
+else
+	curl -fsS "https://raw.githubusercontent.com/GameServerManagers/LinuxGSM/${ref}/lgsm/data/serverlist.csv" -o serverlist.csv
+	serverlist="serverlist.csv"
+fi
 
-while read -r line; do
-	shortname=$(echo "$line" | awk -F, '{ print $1 }')
-	# If TARGETED_SHORTNAMES is set, skip servers not in the list
-	if [ -n "${TARGETED_SHORTNAMES:-}" ]; then
-		if ! echo "${TARGETED_SHORTNAMES}" | grep -qw "${shortname}"; then
-			continue
-		fi
+shortnames=()
+legacy=()
+while IFS=, read -r shortname _; do
+	[ -z "${shortname}" ] && continue
+	if [ -n "${TARGETED_SHORTNAMES:-}" ] && ! echo "${TARGETED_SHORTNAMES}" | grep -qw "${shortname}"; then
+		continue
 	fi
-	export shortname
-	servername=$(echo "$line" | awk -F, '{ print $2 }')
-	export servername
-	gamename=$(echo "$line" | awk -F, '{ print $3 }')
-	export gamename
-	distro=$(echo "$line" | awk -F, '{ print $4 }')
-	export distro
-	# Legacy servers that require older Ubuntu/Debian versions due to glibc compatibility
-	case "${shortname}" in
-		bfv | bf1942)
-			# Requires Ubuntu <= 22.04 or Debian <= 12 (glibc 2.31 compatible)
-			runner="ubuntu-22.04"
-			;;
-		btl | onset)
-			# Requires Ubuntu <= 20.04 or Debian <= 11 (glibc 2.31 compatible)
-			runner="ubuntu-20.04"
-			;;
-		*)
-			runner="ubuntu-latest"
-			;;
-	esac
-	{
-		echo -n "{"
-		echo -n "\"shortname\":"
-		echo -n "\"${shortname}\""
-		echo -n ",\"runner\":"
-		echo -n "\"${runner}\""
-		echo -n "},"
-	} >> "shortnamearray.json"
-done < <(tail -n +2 serverlist.csv)
-sed -i '$ s/.$//' "shortnamearray.json"
-echo -n "]" >> "shortnamearray.json"
-echo -n "}" >> "shortnamearray.json"
-rm serverlist.csv
+	if echo "${legacyservers}" | grep -qw "${shortname}"; then
+		legacy+=("${shortname}")
+	else
+		shortnames+=("${shortname}")
+	fi
+done < <(tail -n +2 "${serverlist}" | grep -v '^[[:blank:]]*$')
+
+count="${#shortnames[@]}"
+shards=$((count < maxshards ? count : maxshards))
+declare -a shard
+for i in "${!shortnames[@]}"; do
+	shard[i % shards]+="${shortnames[i]} "
+done
+
+{
+	echo -n '{"include":['
+	for ((i = 0; i < shards; i++)); do
+		[ "${i}" -gt 0 ] && echo -n ','
+		echo -n "{\"shard\":\"$((i + 1))/${shards}\",\"shortnames\":\"${shard[i]% }\"}"
+	done
+	echo -n ']}'
+} > shards.json
+
+echo -n "${legacy[*]}" > legacy.txt
+[ "${serverlist}" == "serverlist.csv" ] && rm serverlist.csv
+
+echo "Servers: ${count} in ${shards} shard(s), legacy: ${legacy[*]:-none}"

+ 66 - 0
.github/scripts/details-check-run.sh

@@ -0,0 +1,66 @@
+#!/bin/bash
+# Runs the details check for each shortname given, in one shared LinuxGSM directory.
+# Modules, default configs and data come from the checkout, so the code under test is used and nothing is downloaded per server.
+# Usage: details-check-run.sh <shortname>...
+
+src="${GITHUB_WORKSPACE:-${PWD}}"
+workdir="${DETAILS_CHECK_DIR:-${HOME}/details-check}"
+summary="${GITHUB_STEP_SUMMARY:-/dev/null}"
+export LGSM_GITHUBBRANCH="${LGSM_REF:?LGSM_REF must be set}"
+
+rm -rf "${workdir}"
+mkdir -p "${workdir}/lgsm" "${workdir}/serverfiles"
+cp "${src}/linuxgsm.sh" "${workdir}/"
+cp -r "${src}/lgsm/modules" "${src}/lgsm/config-default" "${src}/lgsm/data" "${workdir}/lgsm/"
+cd "${workdir}" || exit 1
+
+result() {
+	if [ "${1}" -eq 0 ]; then echo "✅ pass"; else echo "❌ fail"; fi
+}
+
+failed=()
+{
+	echo "| Server | Config | Details | Parse game details | Query raw |"
+	echo "|--------|--------|---------|--------------------|-----------|"
+} >> "${summary}"
+
+for shortname in "$@"; do
+	gameserver="${shortname}server"
+	echo "::group::${gameserver}"
+	./linuxgsm.sh "${gameserver}"
+	./"${gameserver}" developer
+
+	servercfg=$(sed -n "/^\<servercfgdefault\>/ { s/.*= *\"\?\([^\"']*\)\"\?/\1/p;q }" "lgsm/config-lgsm/${gameserver}/_default.cfg")
+	if [ -n "${servercfg}" ]; then
+		cfgpath="${servercfg}"
+		[[ "${cfgpath}" == \$\{*\}/* ]] && cfgpath="${cfgpath#*\}/}"
+		curl -fsS -o "config-${shortname}" "https://raw.githubusercontent.com/GameServerManagers/Game-Server-Configs/main/${shortname}/${cfgpath}"
+		config=$?
+		[ "${config}" -eq 0 ] && cat "config-${shortname}"
+		configresult=$(result "${config}")
+	else
+		echo "This game server has no config file."
+		config=0
+		configresult="➖ none"
+	fi
+	grep "startparameters" "lgsm/config-default/config-lgsm/${gameserver}/_default.cfg"
+
+	./"${gameserver}" details
+	details=$?
+	./"${gameserver}" parse-game-details
+	parse=$?
+	./"${gameserver}" query-raw
+	query=$?
+	echo "::endgroup::"
+
+	if [ "${config}" -ne 0 ] || [ "${details}" -ne 0 ] || [ "${parse}" -ne 0 ] || [ "${query}" -ne 0 ]; then
+		failed+=("${gameserver}")
+		echo "::error title=${gameserver}::config=${config} details=${details} parse-game-details=${parse} query-raw=${query}"
+	fi
+	echo "| ${gameserver} | ${configresult} | $(result "${details}") | $(result "${parse}") | $(result "${query}") |" >> "${summary}"
+done
+
+if [ "${#failed[@]}" -gt 0 ]; then
+	echo -e "\n**Failed:** ${failed[*]}" >> "${summary}"
+	exit 1
+fi

+ 1 - 1
.github/scripts/sync-game-labels.sh

@@ -13,7 +13,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Add to Project
-        uses: actions/add-to-project@v1.0.2
+        uses: actions/add-to-project@v2.0.0
         with:
           project-url: https://github.com/orgs/GameServerManagers/projects/11
           github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}

+ 53 - 86
.github/workflows/action-details-check.yml

@@ -16,18 +16,21 @@ concurrency:
   group: details-check-${{ github.ref_name }}
   cancel-in-progress: true
 
+env:
+  LGSM_REF: ${{ github.event.pull_request.head.sha || github.sha }}
+
 jobs:
   create-matrix:
     if: github.repository_owner == 'GameServerManagers'
     runs-on: ubuntu-latest
-    env:
-      LGSM_REF: ${{ github.event.pull_request.head.sha || github.ref_name }}
     outputs:
       matrix: ${{ steps.set-matrix.outputs.matrix }}
+      legacy: ${{ steps.set-matrix.outputs.legacy }}
     steps:
       - name: Checkout
-        uses: actions/checkout@v6
+        uses: actions/checkout@v7
         with:
+          ref: ${{ env.LGSM_REF }}
           # Full history so the base branch ref is available for the
           # `git diff origin/<base>...HEAD` comparison below (a shallow
           # checkout has no origin/<base> and the diff fails).
@@ -44,7 +47,7 @@ jobs:
           # Only these modules directly affect what 'details', 'parse-game-details'
           # and 'query-raw' output. Everything else (install, fix, update, check_*,
           # data CSVs, game icons, etc.) is irrelevant to this check.
-          details_core='^(linuxgsm\.sh|lgsm/modules/(info_game|info_messages|command_details|command_dev_parse_game_details|command_dev_query_raw|core_modules)\.sh)$'
+          details_core='^(linuxgsm\.sh|lgsm/modules/(info_game|info_messages|command_details|command_dev_parse_game_details|command_dev_query_raw|core_modules)\.sh|\.github/workflows/action-details-check\.yml|\.github/scripts/details-check-.*\.sh)$'
           if echo "$changed" | grep -qE "${details_core}"; then
             echo "Details-relevant core file changed — using full matrix."
             echo "mode=full" >> "$GITHUB_OUTPUT"
@@ -60,11 +63,11 @@ jobs:
             else
               echo "Targeted servers: $shortnames"
               echo "mode=targeted" >> "$GITHUB_OUTPUT"
-              printf '%s\n' $shortnames > targeted_shortnames.txt
+              echo "$shortnames" > targeted_shortnames.txt
             fi
           fi
 
-      - name: Generate matrix with generate-matrix.sh
+      - name: Generate matrix
         if: github.event_name != 'pull_request' || steps.targeted.outputs.mode != 'skip'
         run: |
           if [ -f targeted_shortnames.txt ]; then
@@ -76,106 +79,70 @@ jobs:
       - name: Set Matrix
         id: set-matrix
         run: |
-          if [ ! -f shortnamearray.json ]; then
+          if [ ! -f shards.json ]; then
             echo 'matrix={"include":[]}' >> "$GITHUB_OUTPUT"
+            echo 'legacy=' >> "$GITHUB_OUTPUT"
           else
-            shortnamearray=$(cat shortnamearray.json)
-            echo "${shortnamearray}"
-            echo -n "matrix=${shortnamearray}" >> "$GITHUB_OUTPUT"
+            cat shards.json
+            echo "matrix=$(cat shards.json)" >> "$GITHUB_OUTPUT"
+            echo "legacy=$(cat legacy.txt)" >> "$GITHUB_OUTPUT"
           fi
 
   details-check:
-    if: github.repository_owner == 'GameServerManagers'
+    name: details-check (${{ matrix.shard }})
+    # Skip (rather than fail the run) when create-matrix found nothing to check.
+    if: github.repository_owner == 'GameServerManagers' && needs.create-matrix.outputs.matrix != '{"include":[]}'
     needs: create-matrix
     continue-on-error: true
-    runs-on: ${{ matrix.runner }}
-    env:
-      LGSM_REF: ${{ github.event.pull_request.head.sha || github.ref_name }}
-
+    runs-on: ubuntu-latest
     strategy:
+      fail-fast: false
       matrix: ${{ fromJSON(needs.create-matrix.outputs.matrix) }}
-
     steps:
-      - name: Download linuxgsm.sh
-        run: wget "https://raw.githubusercontent.com/GameServerManagers/LinuxGSM/${LGSM_REF}/linuxgsm.sh"; chmod +x linuxgsm.sh
+      - name: Checkout
+        uses: actions/checkout@v7
+        with:
+          ref: ${{ env.LGSM_REF }}
 
       - name: Cache apt packages
-        uses: actions/cache@v4
+        uses: actions/cache@v6
         with:
           path: /var/cache/apt/archives
           key: apt-${{ runner.os }}-libxml2-utils-jq
 
       - name: Install dependencies
-        run: sudo apt-get install -y --no-install-recommends libxml2-utils jq
-
-      - name: Create serverfiles directory
-        run: mkdir -p serverfiles
-
-      - name: Grab server
-        run: LGSM_GITHUBBRANCH="${LGSM_REF}" ./linuxgsm.sh ${{ matrix.shortname }}server
-
-      - name: Enable developer mode
-        run: LGSM_GITHUBBRANCH="${LGSM_REF}" ./${{ matrix.shortname }}server developer
-
-      - name: Generate servercfgname
-        id: sets-servercfgname
-        run: |
-          servercfg=$(sed -n "/^\<servercfgdefault\>/ { s/.*= *\"\?\([^\"']*\)\"\?/\1/p;q }" lgsm/config-lgsm/${{ matrix.shortname }}server/_default.cfg)
-          echo "servercfgname=$servercfg" >> "$GITHUB_OUTPUT"
-
-      - name: Download config
-        run: |
-          if [ "${{ steps.sets-servercfgname.outputs.servercfgname }}" == "" ]; then
-            echo "This game server has no config file."
-          else
-            curl -f -o config "https://raw.githubusercontent.com/GameServerManagers/Game-Server-Configs/main/${{ matrix.shortname }}/${{ steps.sets-servercfgname.outputs.servercfgname }}"
-          fi
+        run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libxml2-utils jq
 
-      - name: Pre-load LinuxGSM
-        run: LGSM_GITHUBBRANCH="${LGSM_REF}" ./${{ matrix.shortname }}server details
+      - name: Details check
+        run: .github/scripts/details-check-run.sh ${{ matrix.shortnames }}
 
-      - name: Display config
+  details-check-legacy:
+    name: details-check (legacy)
+    if: github.repository_owner == 'GameServerManagers' && needs.create-matrix.outputs.legacy != ''
+    needs: create-matrix
+    continue-on-error: true
+    runs-on: ubuntu-latest
+    container: public.ecr.aws/ubuntu/ubuntu:20.04
+    steps:
+      - name: Install dependencies
+        env:
+          DEBIAN_FRONTEND: noninteractive
         run: |
-          if [ "${{ steps.sets-servercfgname.outputs.servercfgname }}" == "" ]; then
-            echo "This game server has no config file."
-          else
-            cat config
-          fi
-
-      - name: Display parameters
-        run: grep "startparameters" lgsm/config-default/config-lgsm/${{ matrix.shortname }}server/_default.cfg
-
-      - name: Details
-        id: details
-        run: LGSM_GITHUBBRANCH="${LGSM_REF}" ./${{ matrix.shortname }}server details
+          apt-get update
+          apt-get install -y --no-install-recommends ca-certificates curl wget git sudo iproute2 libxml2-utils jq \
+            bc binutils bsdmainutils bzip2 cpio distro-info file gzip hostname lib32gcc-s1 lib32stdc++6 \
+            netcat-openbsd pigz python3 tar tmux unzip util-linux uuid-runtime xz-utils
 
-      - name: Detect details
-        id: detect-details
-        run: LGSM_GITHUBBRANCH="${LGSM_REF}" ./${{ matrix.shortname }}server parse-game-details
-
-      - name: Query Raw
-        id: query-raw
-        run: LGSM_GITHUBBRANCH="${LGSM_REF}" ./${{ matrix.shortname }}server query-raw
+      - name: Checkout
+        uses: actions/checkout@v7
+        with:
+          ref: ${{ env.LGSM_REF }}
 
-      - name: Write job summary
-        if: always()
+      - name: Details check
         run: |
-          status_icon() {
-            case "$1" in
-              success)  echo "✅" ;;
-              failure)  echo "❌" ;;
-              skipped)  echo "⏭️" ;;
-              *)         echo "⚠️" ;;
-            esac
-          }
-          details_icon=$(status_icon "${{ steps.details.outcome }}")
-          parse_icon=$(status_icon "${{ steps.detect-details.outcome }}")
-          query_icon=$(status_icon "${{ steps.query-raw.outcome }}")
-          {
-            echo "## ${{ matrix.shortname }}server"
-            echo "| Step | Result |"
-            echo "|------|--------|"
-            echo "| Details | ${details_icon} ${{ steps.details.outcome }} |"
-            echo "| Parse game details | ${parse_icon} ${{ steps.detect-details.outcome }} |"
-            echo "| Query raw | ${query_icon} ${{ steps.query-raw.outcome }} |"
-          } >> "$GITHUB_STEP_SUMMARY"
+          useradd -m linuxgsm
+          echo "linuxgsm ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/linuxgsm
+          chown -R linuxgsm:linuxgsm "${GITHUB_WORKSPACE}"
+          chmod a+w "${GITHUB_STEP_SUMMARY}"
+          sudo -u linuxgsm -H env LGSM_REF="${LGSM_REF}" GITHUB_WORKSPACE="${GITHUB_WORKSPACE}" GITHUB_STEP_SUMMARY="${GITHUB_STEP_SUMMARY}" \
+            "${GITHUB_WORKSPACE}/.github/scripts/details-check-run.sh" ${{ needs.create-matrix.outputs.legacy }}

+ 9 - 3
.github/workflows/action-issue-triage-automation.yml

@@ -43,7 +43,9 @@ on:
       - created
       - edited
       - deleted
-  pull_request:
+  # pull_request_target so pr-labeler can label PRs from forks (pull_request gets a read-only token for forks).
+  # Safe here: no job checks out or runs PR code on this event.
+  pull_request_target:
     types:
       - opened
       - edited
@@ -2267,8 +2269,12 @@ jobs:
             }
 
   pr-labeler:
-    if: github.repository_owner == 'GameServerManagers' && github.event_name == 'pull_request'
+    if: github.repository_owner == 'GameServerManagers' && github.event_name == 'pull_request_target'
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      issues: write
+      pull-requests: write
     steps:
       - name: PR Labeler
         uses: github/issue-labeler@v3.5
@@ -2294,7 +2300,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v5
+        uses: actions/checkout@v7
 
       - name: Sync game labels from serverlist
         env:

+ 1 - 1
.github/workflows/action-prettier.yml

@@ -19,7 +19,7 @@ jobs:
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v6
+        uses: actions/checkout@v7
         with:
           persist-credentials: false
 

+ 1 - 1
.github/workflows/action-serverlist-validate.yml

@@ -15,7 +15,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v6
+        uses: actions/checkout@v7
 
       - name: Compare Versions
         run: .github/scripts/serverlist-validate.sh

+ 2 - 2
.github/workflows/action-super-linter.yml

@@ -25,7 +25,7 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v6
+        uses: actions/checkout@v7
         with:
           # super-linter needs the full git history to get the
           # list of files that changed across commits
@@ -37,7 +37,7 @@ jobs:
           npm install --no-save prettier prettier-plugin-sh prettier-plugin-jinja-template || true
 
       - name: Super-linter
-        uses: super-linter/super-linter@v8
+        uses: super-linter/super-linter@v9
         env:
           # To report GitHub Actions status checks
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 2 - 6
.github/workflows/action-update-copyright-years-in-license-file.yml

@@ -7,13 +7,14 @@ on:
 
 permissions:
   contents: write
+  pull-requests: write
 
 jobs:
   update-license-year:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v6
+        uses: actions/checkout@v7
         with:
           fetch-depth: 0
           persist-credentials: false
@@ -22,8 +23,3 @@ jobs:
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           path: LICENSE.md
-      - name: Merge pull request
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        run: |
-          gh pr merge --merge --delete-branch

+ 1 - 1
.github/workflows/action-version-check.yml

@@ -11,7 +11,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v6
+        uses: actions/checkout@v7
 
       - name: Version Check
         run: .github/scripts/version-check.sh

+ 1 - 1
.github/workflows/ai-triage.yml

@@ -21,7 +21,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Triage issue with GitHub Models
-        uses: actions/github-script@v7
+        uses: actions/github-script@v9
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         with:

+ 1 - 1
.github/workflows/sync-game-labels.yml

@@ -18,7 +18,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v5
+        uses: actions/checkout@v7
 
       - name: Sync game labels from serverlist
         env:

+ 1 - 1
lgsm/config-default/config-lgsm/colserver/_default.cfg

@@ -154,7 +154,7 @@ stopmode="2"
 # 3: gamedig
 # 4: gsquery
 # 5: tcp
-querymode="2"
+querymode="5"
 querytype="protocol-valve"
 
 ## Console type

+ 6 - 1
lgsm/config-default/config-lgsm/csgoserver/_default.cfg

@@ -43,6 +43,7 @@ tickrate="64"
 
 ## Game Server Login Token (GSLT): Required
 # GSLT is required for running a public server.
+# The GSLT must be created using the App ID set in clientappid (default 4465480).
 # More info: https://docs.linuxgsm.com/steamcmd/gslt
 gslt=""
 
@@ -168,8 +169,12 @@ ansi="on"
 sleeptime="0.5"
 
 ## SteamCMD Settings | https://docs.linuxgsm.com/steamcmd
-# Server appid
+# App ID SteamCMD downloads the dedicated server files from. Do not change.
 appid="740"
+# App ID the server advertises to players. Only clients of this App ID can connect, and the GSLT must be created for it.
+# 4465480: standalone CS:GO | 730: CS2 csgo_legacy beta branch | "": leave steam_appid.txt and steam.inf unmanaged
+# https://docs.linuxgsm.com/game-servers/counter-strike-global-offensive#client-app-id
+clientappid="4465480"
 steamcmdforcewindows="no"
 # SteamCMD Branch | https://docs.linuxgsm.com/steamcmd/branch
 branch=""

+ 1 - 1
lgsm/config-default/config-lgsm/fctrserver/_default.cfg

@@ -16,7 +16,7 @@ rconpassword="CHANGE_ME"
 branch="stable" # values: stable, experimental
 
 ## Server Parameters | https://docs.linuxgsm.com/configuration/start-parameters#additional-parameters
-startparameters="--bind ${ip} --start-server ${serverfiles}/save1.zip --server-settings ${servercfgfullpath} --port ${port} --rcon-port ${rconport} --rcon-password ${rconpassword}"
+startparameters="--bind ${ip} --start-server-load-latest --server-settings ${servercfgfullpath} --port ${port} --rcon-port ${rconport} --rcon-password ${rconpassword}"
 
 #### LinuxGSM Settings ####
 

+ 195 - 0
lgsm/config-default/config-lgsm/hytserver/_default.cfg

@@ -0,0 +1,195 @@
+##################################
+######## Default Settings ########
+##################################
+# DO NOT EDIT, ANY CHANGES WILL BE OVERWRITTEN!
+# Copy settings from here and use them in either:
+# common.cfg - applies settings to every instance.
+# [instance].cfg - applies settings to a specific instance.
+
+#### Game Server Settings ####
+
+## Predefined Parameters | https://docs.linuxgsm.com/configuration/start-parameters
+# A Hytale account that owns the game is needed for two one-time logins:
+# 1. Install: the Hytale downloader shows a login URL to approve in a browser (also in docker logs).
+# 2. First start: LinuxGSM shows the server login URL (and sends it as an alert) to approve.
+#    The server saves it (Server/auth.enc) and restores it on restart. To log in again manually:
+#    ./hytserver send "/auth login device"
+# https://docs.linuxgsm.com/game-servers/hytale
+# Server name, password and max players are set in Server/config.json, created on first start.
+ip="0.0.0.0"
+port="5520"
+
+## Server Parameters | https://docs.linuxgsm.com/configuration/start-parameters#additional-parameters
+startparameters="--bind ${ip}:${port}"
+
+## Release Settings
+# Patchline (release|pre-release)
+hytalepatchline="release"
+
+#### LinuxGSM Settings ####
+
+## LinuxGSM Stats
+# Send useful stats to LinuxGSM developers.
+# https://docs.linuxgsm.com/configuration/linuxgsm-stats
+# (on|off)
+stats="off"
+
+## Notification Alerts
+# (on|off)
+
+# Display IP | https://docs.linuxgsm.com/alerts#display-ip
+displayip=""
+
+# More info | https://docs.linuxgsm.com/alerts#more-info
+postalert="off"
+
+# Alert on Start/Stop/Restart
+statusalert="off"
+
+# Discord Alerts | https://docs.linuxgsm.com/alerts/discord
+discordalert="off"
+discordwebhook="webhook"
+
+# Email Alerts | https://docs.linuxgsm.com/alerts/email
+emailalert="off"
+email="email@example.com"
+emailfrom=""
+
+# Gotify Alerts | https://docs.linuxgsm.com/alerts/gotify
+gotifyalert="off"
+gotifytoken="token"
+gotifywebhook="webhook"
+
+# IFTTT Alerts | https://docs.linuxgsm.com/alerts/ifttt
+iftttalert="off"
+ifttttoken="accesstoken"
+iftttevent="linuxgsm_alert"
+
+# ntfy Alerts | https://docs.linuxgsm.com/alerts/ntfy
+ntfyalert="off"
+ntfytopic="LinuxGSM"
+ntfyserver="https://ntfy.sh"
+ntfytoken=""
+ntfyusername=""
+ntfypassword=""
+ntfypriority=""
+ntfytags=""
+
+# Pushbullet Alerts | https://docs.linuxgsm.com/alerts/pushbullet
+pushbulletalert="off"
+pushbullettoken="accesstoken"
+channeltag=""
+
+# Pushover Alerts | https://docs.linuxgsm.com/alerts/pushover
+pushoveralert="off"
+pushovertoken="accesstoken"
+pushoveruserkey="userkey"
+
+# Rocket.Chat Alerts | https://docs.linuxgsm.com/alerts/rocket.chat
+rocketchatalert="off"
+rocketchatwebhook="webhook"
+
+# Slack Alerts | https://docs.linuxgsm.com/alerts/slack
+slackalert="off"
+slackwebhook="webhook"
+
+# Telegram Alerts | https://docs.linuxgsm.com/alerts/telegram
+# You can add a custom cURL string eg proxy (useful in Russia) in "curlcustomstring".
+# For example "--socks5 ipaddr:port" for socks5 proxy see more in "curl --help all".
+telegramapi="api.telegram.org"
+telegramalert="off"
+telegramtoken="accesstoken"
+telegramchatid=""
+telegramthreadid=""
+telegramsilentnotification="false"
+curlcustomstring=""
+
+## Updating | https://docs.linuxgsm.com/commands/update
+updateonstart="off"
+
+## Backup | https://docs.linuxgsm.com/commands/backup
+maxbackups="4"
+maxbackupdays="30"
+stoponbackup="on"
+
+## Logging | https://docs.linuxgsm.com/features/logging
+consolelogging="on"
+logdays="7"
+
+## Monitor | https://docs.linuxgsm.com/commands/monitor
+# Query delay time
+querydelay="1"
+
+## ANSI Colors | https://docs.linuxgsm.com/features/ansi-colors
+ansi="on"
+
+#### Advanced Settings ####
+
+## Message Display Time | https://docs.linuxgsm.com/features/message-display-time
+sleeptime="0.5"
+
+## Stop Mode | https://docs.linuxgsm.com/features/stop-mode
+# 1: tmux kill
+# 2: CTRL+c
+# 3: quit
+# 4: quit 120s
+# 5: stop
+# 6: q
+# 7: exit
+# 8: 7 Days to Die
+# 9: GoldSrc
+# 10: Avorion
+# 11: end
+stopmode="5"
+
+## Query mode
+# 1: session only
+# 2: gamedig (gsquery fallback)
+# 3: gamedig
+# 4: gsquery
+# 5: tcp
+querymode="1"
+querytype=""
+
+## Console type
+consoleverbose="yes"
+consoleinteract="yes"
+
+## Game Server Details
+# Do not edit
+gamename="Hytale"
+engine="hytale"
+glibc="null"
+
+#### Directories ####
+# Edit with care
+
+## Game Server Directories
+systemdir="${serverfiles}/Server"
+executabledir="${serverfiles}"
+executable="./start.sh"
+servercfgdir="${systemdir}"
+servercfg="config.json"
+servercfgdefault="config.json"
+servercfgfullpath="${servercfgdir}/${servercfg}"
+
+## Backup Directory
+backupdir="${lgsmdir}/backup"
+
+## Logging Directories
+[ -n "${LGSM_LOGDIR}" ] && logdir="${LGSM_LOGDIR}" || logdir="${rootdir}/log"
+gamelogdir="${systemdir}/logs"
+lgsmlogdir="${logdir}/script"
+consolelogdir="${logdir}/console"
+lgsmlog="${lgsmlogdir}/${selfname}-script.log"
+consolelog="${consolelogdir}/${selfname}-console.log"
+alertlog="${lgsmlogdir}/${selfname}-alert.log"
+postdetailslog="${lgsmlogdir}/${selfname}-postdetails.log"
+
+## Logs Naming
+lgsmlogdate="${lgsmlogdir}/${selfname}-script-$(date '+%Y-%m-%d-%H:%M:%S').log"
+consolelogdate="${consolelogdir}/${selfname}-console-$(date '+%Y-%m-%d-%H:%M:%S').log"
+
+## Log Parameters
+logtimestamp="off"
+logtimestampformat="%Y-%m-%d %H:%M:%S"

+ 1 - 1
lgsm/config-default/config-lgsm/pzserver/_default.cfg

@@ -13,7 +13,7 @@ ip="0.0.0.0"
 adminpassword="CHANGE_ME"
 
 ## Server Parameters | https://docs.linuxgsm.com/configuration/start-parameters#additional-parameters
-startparameters="-servername ${selfname}"
+startparameters="-servername ${selfname} -adminpassword ${adminpassword}"
 
 #### LinuxGSM Settings ####
 

+ 199 - 0
lgsm/config-default/config-lgsm/rsdwserver/_default.cfg

@@ -0,0 +1,199 @@
+##################################
+######## Default Settings ########
+##################################
+# DO NOT EDIT, ANY CHANGES WILL BE OVERWRITTEN!
+# Copy settings from here and use them in either:
+# common.cfg - applies settings to every instance.
+# [instance].cfg - applies settings to a specific instance.
+
+#### Game Server Settings ####
+
+## Predefined Parameters | https://docs.linuxgsm.com/configuration/start-parameters
+# Server name, passwords, owner ID and world name are read by the server from DedicatedServer.ini, not set here.
+# Edit them in: serverfiles/RSDragonwilds/Saved/Config/LinuxServer/DedicatedServer.ini
+# REQUIRED: the server will not finish starting until OwnerId is set in DedicatedServer.ini
+# (your Player ID, shown in the in-game settings). DedicatedServer.ini is created during install,
+# so OwnerId can be set before the first start.
+port="7777"
+
+## Server Parameters | https://docs.linuxgsm.com/configuration/start-parameters#additional-parameters
+## Game Server Docs | https://dragonwilds.runescape.wiki/w/Dedicated_Servers
+startparameters="-Port=${port} -log"
+
+#### LinuxGSM Settings ####
+
+## LinuxGSM Stats
+# Send useful stats to LinuxGSM developers.
+# https://docs.linuxgsm.com/configuration/linuxgsm-stats
+# (on|off)
+stats="off"
+
+## Notification Alerts
+# (on|off)
+
+# Display IP | https://docs.linuxgsm.com/alerts#display-ip
+displayip=""
+
+# More info | https://docs.linuxgsm.com/alerts#more-info
+postalert="off"
+
+# Alert on Start/Stop/Restart
+statusalert="off"
+
+# Discord Alerts | https://docs.linuxgsm.com/alerts/discord
+discordalert="off"
+discordwebhook="webhook"
+
+# Email Alerts | https://docs.linuxgsm.com/alerts/email
+emailalert="off"
+email="email@example.com"
+emailfrom=""
+
+# Gotify Alerts | https://docs.linuxgsm.com/alerts/gotify
+gotifyalert="off"
+gotifytoken="token"
+gotifywebhook="webhook"
+
+# IFTTT Alerts | https://docs.linuxgsm.com/alerts/ifttt
+iftttalert="off"
+ifttttoken="accesstoken"
+iftttevent="linuxgsm_alert"
+
+# ntfy Alerts | https://docs.linuxgsm.com/alerts/ntfy
+ntfyalert="off"
+ntfytopic="LinuxGSM"
+ntfyserver="https://ntfy.sh"
+ntfytoken=""
+ntfyusername=""
+ntfypassword=""
+ntfypriority=""
+ntfytags=""
+
+# Pushbullet Alerts | https://docs.linuxgsm.com/alerts/pushbullet
+pushbulletalert="off"
+pushbullettoken="accesstoken"
+channeltag=""
+
+# Pushover Alerts | https://docs.linuxgsm.com/alerts/pushover
+pushoveralert="off"
+pushovertoken="accesstoken"
+pushoveruserkey="userkey"
+
+# Rocket.Chat Alerts | https://docs.linuxgsm.com/alerts/rocket.chat
+rocketchatalert="off"
+rocketchatwebhook="webhook"
+
+# Slack Alerts | https://docs.linuxgsm.com/alerts/slack
+slackalert="off"
+slackwebhook="webhook"
+
+# Telegram Alerts | https://docs.linuxgsm.com/alerts/telegram
+# You can add a custom cURL string eg proxy (useful in Russia) in "curlcustomstring".
+# For example "--socks5 ipaddr:port" for socks5 proxy see more in "curl --help all".
+telegramapi="api.telegram.org"
+telegramalert="off"
+telegramtoken="accesstoken"
+telegramchatid=""
+telegramthreadid=""
+telegramsilentnotification="false"
+curlcustomstring=""
+
+## Updating | https://docs.linuxgsm.com/commands/update
+updateonstart="off"
+
+## Backup | https://docs.linuxgsm.com/commands/backup
+maxbackups="4"
+maxbackupdays="30"
+stoponbackup="on"
+
+## Logging | https://docs.linuxgsm.com/features/logging
+consolelogging="on"
+logdays="7"
+
+## Monitor | https://docs.linuxgsm.com/commands/monitor
+# Query delay time
+querydelay="5"
+
+## ANSI Colors | https://docs.linuxgsm.com/features/ansi-colors
+ansi="on"
+
+#### Advanced Settings ####
+
+## Message Display Time | https://docs.linuxgsm.com/features/message-display-time
+sleeptime="0.5"
+
+## SteamCMD Settings | https://docs.linuxgsm.com/steamcmd
+# Server appid
+appid="4019830"
+steamcmdforcewindows="no"
+# SteamCMD Branch | https://docs.linuxgsm.com/steamcmd/branch
+branch=""
+betapassword=""
+# Master Server | https://docs.linuxgsm.com/steamcmd/steam-master-server
+steammaster="false"
+
+## Stop Mode | https://docs.linuxgsm.com/features/stop-mode
+# 1: tmux kill
+# 2: CTRL+c
+# 3: quit
+# 4: quit 120s
+# 5: stop
+# 6: q
+# 7: exit
+# 8: 7 Days to Die
+# 9: GoldSrc
+# 10: Avorion
+# 11: end
+stopmode="2"
+
+## Query mode
+# 1: session only
+# 2: gamedig (gsquery fallback)
+# 3: gamedig
+# 4: gsquery
+# 5: tcp
+querymode="1"
+querytype=""
+
+## Console type
+consoleverbose="yes"
+consoleinteract="no"
+
+## Game Server Details
+# Do not edit
+gamename="RuneScape: Dragonwilds"
+engine="unreal5"
+glibc="2.17"
+
+#### Directories ####
+# Edit with care
+
+## Game Server Directories
+systemdir="${serverfiles}/RSDragonwilds"
+executabledir="${systemdir}/Binaries/Linux"
+executable="./RSDragonwildsServer-Linux-Shipping"
+servercfgdir="${systemdir}/Saved/Config/LinuxServer"
+servercfg="DedicatedServer.ini"
+servercfgdefault="DedicatedServer.ini"
+servercfgfullpath="${servercfgdir}/${servercfg}"
+
+## Backup Directory
+backupdir="${lgsmdir}/backup"
+
+## Logging Directories
+[ -n "${LGSM_LOGDIR}" ] && logdir="${LGSM_LOGDIR}" || logdir="${rootdir}/log"
+gamelogdir="${systemdir}/Saved/Logs"
+lgsmlogdir="${logdir}/script"
+consolelogdir="${logdir}/console"
+lgsmlog="${lgsmlogdir}/${selfname}-script.log"
+consolelog="${consolelogdir}/${selfname}-console.log"
+alertlog="${lgsmlogdir}/${selfname}-alert.log"
+postdetailslog="${lgsmlogdir}/${selfname}-postdetails.log"
+
+## Logs Naming
+lgsmlogdate="${lgsmlogdir}/${selfname}-script-$(date '+%Y-%m-%d-%H:%M:%S').log"
+consolelogdate="${consolelogdir}/${selfname}-console-$(date '+%Y-%m-%d-%H:%M:%S').log"
+
+## Log Parameters
+logtimestamp="off"
+logtimestampformat="%Y-%m-%d %H:%M:%S"

+ 3 - 4
lgsm/config-default/config-lgsm/sfserver/_default.cfg

@@ -10,14 +10,13 @@
 
 ## Predefined Parameters | https://docs.linuxgsm.com/configuration/start-parameters
 ip="0.0.0.0"
-queryport="15777"
-beaconport="15000"
-reliableport="8888"
+# Satisfactory 1.0+: port is used for the game (UDP) and the server API (TCP).
 port="7777"
+reliableport="8888"
 
 ## Server Parameters | https://docs.linuxgsm.com/configuration/start-parameters#additional-parameters
 ## Game Server Docs | https://satisfactory.fandom.com/wiki/Dedicated_servers
-startparameters="FactoryGame -Port=${port} -ServerQueryPort=${queryport} -BeaconPort=${beaconport} -ReliablePort=${reliableport} -log"
+startparameters="FactoryGame -Port=${port} -ReliablePort=${reliableport} -log"
 
 #### LinuxGSM Settings ####
 

+ 3 - 0
lgsm/config-default/config-lgsm/vhserver/_default.cfg

@@ -50,6 +50,9 @@ instanceid="1"
 worldmodifiers=""
 
 ## Server Parameters | https://docs.linuxgsm.com/configuration/start-parameters#additional-parameters
+# -crossplay routes connections through the PlayFab relay so Xbox/Game Pass players can join.
+# Steam-only servers joined by direct IP: copy startparameters to your instance config and remove -crossplay.
+# https://docs.linuxgsm.com/game-servers/valheim#crossplay
 startparameters="-name '${servername}' -password ${serverpassword} -port ${port} -world ${worldname} -public ${public} -savedir '${savedir}' -saveinterval ${saveinterval} -backups ${backups} -backupshort ${backupshort} -backuplong ${backuplong} -crossplay -instanceid ${instanceid} ${logFile:+ -logFile '${logFile}'} ${worldmodifiers:+ ${worldmodifiers}}"
 
 #### LinuxGSM Settings ####

+ 4 - 2
lgsm/data/almalinux-8.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl,openssl-libs
-ats
+ats,libatomic
 av
 bb
 bb2,libcurl.i686
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic
 fctr
 fof
 gmod,ncurses-libs.i686
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,zlib-devel
+hyt,java-25-openjdk
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,zlib-devel
 rw,java-11-openjdk

+ 4 - 2
lgsm/data/almalinux-9.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl,openssl-libs
-ats
+ats,libatomic
 av
 bb
 bb2,libcurl.i686
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic
 fctr
 fof
 gmod,ncurses-libs.i686
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,zlib-devel
+hyt,java-25-openjdk
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,zlib-devel
 rw,java-21-openjdk

+ 4 - 2
lgsm/data/centos-7.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl,openssl11-libs
-ats
+ats,libatomic
 av
 bb
 bb2,libcurl.i686
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic
 fctr
 fof
 gmod,ncurses-libs.i686
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,zlib-devel
+hyt,java-25-openjdk
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,zlib-devel
 rw,java-11-openjdk

+ 4 - 2
lgsm/data/centos-8.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl,openssl-libs
-ats
+ats,libatomic
 av
 bb
 bb2,libcurl.i686
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic
 fctr
 fof
 gmod,ncurses-libs.i686
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,zlib-devel
+hyt,java-25-openjdk
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,zlib-devel
 rw,java-11-openjdk

+ 4 - 2
lgsm/data/centos-9.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl,openssl-libs
-ats
+ats,libatomic
 av
 bb
 bb2,libcurl.i686
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic
 fctr
 fof
 gmod,ncurses-libs.i686
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,zlib-devel
+hyt,java-25-openjdk
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,zlib-devel
 rw,java-21-openjdk

+ 4 - 2
lgsm/data/debian-10.csv

@@ -5,7 +5,7 @@ all,bc,binutils,bsdmainutils,bzip2,ca-certificates,cpio,curl,distro-info,file,gz
 ark
 arma3
 armar,libcurl4
-ats
+ats,libatomic1
 av
 bb
 bb2,libcurl4-gnutls-dev:i386
@@ -44,7 +44,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod,libtinfo5:i386
@@ -53,6 +53,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-11-jre

+ 4 - 2
lgsm/data/debian-11.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl4
-ats
+ats,libatomic1
 av
 bb
 bb2,libcurl4-gnutls-dev:i386
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod,libtinfo5:i386
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -96,6 +97,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-17-jre

+ 4 - 2
lgsm/data/debian-12.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl4
-ats
+ats,libatomic1
 av
 bb
 bb2,libcurl4-gnutls-dev:i386
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod,libtinfo5:i386
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -96,6 +97,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-17-jre

+ 4 - 2
lgsm/data/debian-13.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl4
-ats
+ats,libatomic1
 av
 bb
 bb2,libcurl4-gnutls-dev:i386
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -96,6 +97,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-25-jre

+ 4 - 2
lgsm/data/debian-9.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl4
-ats
+ats,libatomic1
 av
 bb
 bb2,libcurl4-gnutls-dev:i386
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod,libtinfo5:i386
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -96,6 +97,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-8-jre

BIN
lgsm/data/gameicons/hyt-icon.png


BIN
lgsm/data/gameicons/rsdw-icon.png


BIN
lgsm/data/gameicons/sm-icon.png


+ 4 - 2
lgsm/data/rhel-7.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl,openssl11-libs
-ats
+ats,libatomic
 av
 bb
 pw
@@ -46,7 +46,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic
 fctr
 fof
 gmod,ncurses-libs.i686
@@ -55,6 +55,7 @@ hl2dm
 hldm
 hldms
 hw,zlib-devel
+hyt,java-25-openjdk
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,zlib-devel
 rw,java-11-openjdk

+ 4 - 2
lgsm/data/rhel-8.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl,openssl-libs
-ats
+ats,libatomic
 av
 bb
 bb2,libcurl.i686
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic
 fctr
 fof
 gmod,ncurses-libs.i686
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,zlib-devel
+hyt,java-25-openjdk
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,zlib-devel
 rw,java-21-openjdk

+ 4 - 2
lgsm/data/rhel-9.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl,openssl-libs
-ats
+ats,libatomic
 av
 bb
 bb2,libcurl.i686
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic
 fctr
 fof
 gmod,ncurses-libs.i686
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,zlib-devel
+hyt,java-25-openjdk
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,zlib-devel
 rw,java-21-openjdk

+ 4 - 2
lgsm/data/rocky-8.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl,openssl-libs
-ats
+ats,libatomic
 av
 bb
 bb2,libcurl.i686
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic
 fctr
 fof
 gmod,ncurses-libs.i686
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,zlib-devel
+hyt,java-25-openjdk
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,zlib-devel
 rw,java-21-openjdk

+ 4 - 2
lgsm/data/rocky-9.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl,openssl-libs
-ats
+ats,libatomic
 av
 bb
 bb2,libcurl.i686
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic
 fctr
 fof
 gmod,ncurses-libs.i686
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,zlib-devel
+hyt,java-25-openjdk
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,zlib-devel
 rw,java-21-openjdk

+ 2 - 0
lgsm/data/serverlist.csv

@@ -53,6 +53,7 @@ hl2dm,hl2dmserver,Half-Life 2: Deathmatch,ubuntu-24.04
 hldm,hldmserver,Half-Life: Deathmatch,ubuntu-24.04
 hldms,hldmsserver,Half-Life Deathmatch: Source,ubuntu-24.04
 hw,hwserver,Hurtworld,ubuntu-24.04
+hyt,hytserver,Hytale,ubuntu-24.04
 hz,hzserver,Humanitz,ubuntu-24.04
 ins,insserver,Insurgency,ubuntu-24.04
 inss,inssserver,Insurgency: Sandstorm,ubuntu-24.04
@@ -94,6 +95,7 @@ ql,qlserver,Quake Live,ubuntu-24.04
 qw,qwserver,Quake World,ubuntu-24.04
 ricochet,ricochetserver,Ricochet,ubuntu-24.04
 ro,roserver,Red Orchestra: Ostfront 41-45,ubuntu-24.04
+rsdw,rsdwserver,RuneScape: Dragonwilds,ubuntu-24.04
 rtcw,rtcwserver,Return to Castle Wolfenstein,ubuntu-24.04
 rust,rustserver,Rust,ubuntu-24.04
 rw,rwserver,Rising World,ubuntu-24.04

+ 4 - 2
lgsm/data/ubuntu-16.04.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl4
-ats
+ats,libatomic1
 av
 bb
 pw
@@ -46,7 +46,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod,libtinfo5:i386
@@ -55,6 +55,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -96,6 +97,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-8-jre

+ 4 - 2
lgsm/data/ubuntu-18.04.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl4
-ats
+ats,libatomic1
 av
 pw
 bb
@@ -46,7 +46,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod,libtinfo5:i386
@@ -55,6 +55,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -96,6 +97,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-11-jre

+ 4 - 2
lgsm/data/ubuntu-20.04.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl4
-ats
+ats,libatomic1
 av
 bb
 bb2,libcurl4-gnutls-dev:i386
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod,libtinfo5:i386
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -96,6 +97,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-21-jre

+ 4 - 2
lgsm/data/ubuntu-22.04.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl4
-ats
+ats,libatomic1
 av
 bb
 bb2,libcurl4-gnutls-dev:i386
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod,libtinfo5:i386
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -96,6 +97,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-21-jre

+ 4 - 2
lgsm/data/ubuntu-23.04.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl4
-ats
+ats,libatomic1
 av
 bb
 bb2,libcurl4-gnutls-dev:i386
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod,libtinfo5:i386
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -96,6 +97,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-21-jre

+ 4 - 2
lgsm/data/ubuntu-23.10.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl4
-ats
+ats,libatomic1
 av
 bb
 bb2,libcurl4-gnutls-dev:i386
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod,libtinfo5:i386
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -96,6 +97,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-21-jre

+ 4 - 2
lgsm/data/ubuntu-24.04.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl4t64
-ats
+ats,libatomic1
 av
 bb
 bb2,libcurl4-gnutls-dev:i386
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-25-jre

+ 4 - 2
lgsm/data/ubuntu-26.04.csv

@@ -6,7 +6,7 @@ ahl2
 ark
 arma3
 armar,libcurl4t64
-ats
+ats,libatomic1
 av
 bb
 bb2,libcurl4-gnutls-dev:i386
@@ -45,7 +45,7 @@ dys
 eco,libgdiplus
 em
 etl
-ets2
+ets2,libatomic1
 fctr
 fof
 gmod
@@ -54,6 +54,7 @@ hl2dm
 hldm
 hldms
 hw,lib32z1
+hyt,openjdk-25-jre
 hz
 ins
 inss
@@ -95,6 +96,7 @@ ql
 qw
 ricochet
 ro
+rsdw
 rtcw
 rust,lib32z1
 rw,openjdk-25-jre

+ 13 - 0
lgsm/modules/alert.sh

@@ -169,6 +169,17 @@ fn_alert_backup() {
 	alertcolourdec="52480"
 }
 
+fn_alert_hytale_login() {
+	fn_script_log_info "Sending alert: ${selfname} needs a Hytale server login"
+	alertaction="Login Required"
+	alertemoji="🔑"
+	alertsound="2"
+	alertmessage="${selfname} needs a Hytale server login. Open ${hytloginurl} and log in with a Hytale account that owns the game. The code expires in 10 minutes."
+	# Blue
+	alertcolourhex="#1e90ff"
+	alertcolourdec="2003199"
+}
+
 fn_alert_permissions() {
 	fn_script_log_info "Sending alert: ${selfname} has permissions issues"
 	alertaction="Checked Permissions"
@@ -250,6 +261,8 @@ elif [ "${alert}" == "update-linuxgsm" ]; then
 	fn_alert_update_linuxgsm
 elif [ "${alert}" == "backup" ]; then
 	fn_alert_backup
+elif [ "${alert}" == "hytale-login" ]; then
+	fn_alert_hytale_login
 else
 	fn_print_fail_nl "Missing alert type"
 	fn_script_log_fail "Missing alert type"

+ 26 - 8
lgsm/modules/check_deps.sh

@@ -156,13 +156,17 @@ fn_install_missing_deps() {
 			fn_script_log_warn "Missing dependencies: ${array_deps_missing[*]}"
 		fi
 
-		# Attempt automatic dependency installation
-		if [ "${autoinstall}" == "1" ]; then
-			sudo -n true > /dev/null 2>&1
+		# Attempt automatic dependency installation.
+		# Check for passwordless sudo first: sudo-rs (default sudo from Ubuntu 25.10) always asks
+		# for a password for sudo -v, even when the user has NOPASSWD.
+		if sudo -n true > /dev/null 2>&1; then
+			autodepinstall="0"
+		elif [ "${autoinstall}" == "1" ]; then
+			autodepinstall="1"
 		else
 			sudo -v > /dev/null 2>&1
+			autodepinstall="$?"
 		fi
-		autodepinstall="$?"
 
 		if [ "${monoinstalled}" == "false" ]; then
 			fn_install_mono_repo
@@ -272,7 +276,8 @@ fn_deps_detector() {
 		array_deps_required=("${array_deps_required[@]/steamcmd/}")
 		steamcmdstatus=1
 		return
-	elif [ "${deptocheck}" == "steamcmd" ] && [ "${distroid}" == "debian" ] && ! grep -qE '[^deb]+non-free([^-]|$)' /etc/apt/sources.list; then
+	# Checks classic and deb822 sources for an uncommented non-free component (non-free-firmware does not count).
+	elif [ "${deptocheck}" == "steamcmd" ] && [ "${distroid}" == "debian" ] && ! grep -qE '^[[:space:]]*[^#[:space:]].*[[:space:]]non-free([[:space:]]|$)' /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources 2> /dev/null; then
 		array_deps_required=("${array_deps_required[@]/steamcmd/}")
 		steamcmdstatus=1
 		return
@@ -280,9 +285,22 @@ fn_deps_detector() {
 	elif [[ ${deptocheck} == "openjdk"* ]] || [[ ${deptocheck} == "java"* ]]; then
 		# Is java already installed?
 		if [ -n "${javaversion}" ]; then
-			# Added for users using Oracle JRE to bypass check.
-			depstatus=0
-			deptocheck="${javaversion}"
+			javamajorversion="$(echo "${javaversion}" | grep -Eo 'version "[0-9]+' | grep -Eo '[0-9]+' | head -1)"
+			if [ "${shortname}" == "hyt" ] && { [ -z "${javamajorversion}" ] || [ "${javamajorversion}" -lt "25" ]; }; then
+				# Java 25 installed but not the active java: installing it again would not change that.
+				if compgen -G "/usr/lib/jvm/*25*/bin/java" > /dev/null; then
+					depstatus=0
+					deptocheck="${javaversion}"
+					fn_print_warn_nl "Java 25 is installed but the active java is version ${javamajorversion:-unknown}. Select Java 25 with: sudo update-alternatives --config java"
+					fn_script_log_warn "Java 25 is installed but the active java is version ${javamajorversion:-unknown}"
+				else
+					depstatus=1
+				fi
+			else
+				# Added for users using Oracle JRE to bypass check.
+				depstatus=0
+				deptocheck="${javaversion}"
+			fi
 		else
 			depstatus=1
 		fi

+ 6 - 0
lgsm/modules/check_ip.sh

@@ -61,3 +61,9 @@ else
 	httpip=("${ip}")
 	telnetip=("${ip}")
 fi
+
+# If the game config binds its query interface to a specific IPv4 address (e.g. TeamSpeak 3 query_ip),
+# query that address first. This also covers a query interface bound to localhost.
+if grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' <<< "${configqueryip}" && [ "${configqueryip}" != "0.0.0.0" ] && [[ " ${queryips[*]} " != *" ${configqueryip} "* ]]; then
+	queryips=("${configqueryip}" "${queryips[@]}")
+fi

+ 4 - 0
lgsm/modules/check_system_requirements.sh

@@ -31,6 +31,8 @@ elif [ "${shortname}" == "dodr" ]; then
 	ramrequirementgb="2"
 elif [ "${shortname}" == "hw" ]; then
 	ramrequirementgb="3"
+elif [ "${shortname}" == "hyt" ]; then
+	ramrequirementgb="4"
 elif [ "${shortname}" == "mc" ]; then
 	ramrequirementgb="1"
 elif [ "${shortname}" == "pmc" ]; then
@@ -45,6 +47,8 @@ elif [ "${shortname}" == "pvr" ]; then
 	ramrequirementgb="1"
 elif [ "${shortname}" == "pz" ]; then
 	ramrequirementgb="3"
+elif [ "${shortname}" == "rsdw" ]; then
+	ramrequirementgb="2"
 elif [ "${shortname}" == "rust" ]; then
 	ramrequirementgb="9"
 elif [ "${shortname}" == "sdtd" ]; then

+ 2 - 0
lgsm/modules/command_check_update.sh

@@ -24,6 +24,8 @@ elif [ "${shortname}" == "pmc" ] || [ "${shortname}" == "vpmc" ] || [ "${shortna
 	update_pmc.sh
 elif [ "${shortname}" == "fctr" ]; then
 	update_fctr.sh
+elif [ "${shortname}" == "hyt" ]; then
+	update_hyt.sh
 elif [ "${shortname}" == "mta" ]; then
 	update_mta.sh
 elif [ "${shortname}" == "jk2" ]; then

+ 2 - 0
lgsm/modules/command_dev_parse_game_details.sh

@@ -77,12 +77,14 @@ declare -A server_details=(
 	['Max Players']="${maxplayers}"
 	['Mod Server Port']="${modserverport}"
 	['OldQueryPortNumber']="${oldqueryportnumber}"
+	['Owner ID']="${ownerid}"
 	['Port 401']="${port401}"
 	['Port IPv6']="${portipv6}"
 	['Port']="${port}"
 	['Query Enabled']="${queryenabled}"
 	['Query HTTP Port']="${httpqueryport}"
 	['Query HTTPS Port']="${httpsqueryport}"
+	['Query IP']="${configqueryip}"
 	['Query Mode']="${querymode}"
 	['Query Port GS']="${gamespyqueryport}"
 	['Query Port']="${queryport}"

+ 5 - 1
lgsm/modules/command_fastdl.sh

@@ -162,7 +162,7 @@ fn_fastdl_preview() {
 		done
 	# Source engine
 	else
-		fastdl_directories_array=("maps" "materials" "models" "particles" "sound" "resources")
+		fastdl_directories_array=("maps" "materials" "models" "particles" "sound" "resource")
 		for directory in "${fastdl_directories_array[@]}"; do
 			if [ -d "${systemdir}/${directory}" ]; then
 				if [ "${directory}" == "maps" ]; then
@@ -175,6 +175,8 @@ fn_fastdl_preview() {
 					local allowed_extentions_array=("*.pcf")
 				elif [ "${directory}" == "sound" ]; then
 					local allowed_extentions_array=("*.wav" "*.mp3" "*.ogg")
+				elif [ "${directory}" == "resource" ]; then
+					local allowed_extentions_array=("*")
 				fi
 				for allowed_extention in "${allowed_extentions_array[@]}"; do
 					fileswc=0
@@ -323,6 +325,8 @@ fn_fastdl_source() {
 				local allowed_extentions_array=("*.pcf")
 			elif [ "${directory}" == "sound" ]; then
 				local allowed_extentions_array=("*.wav" "*.mp3" "*.ogg")
+			elif [ "${directory}" == "resource" ]; then
+				local allowed_extentions_array=("*")
 			fi
 			for allowed_extention in "${allowed_extentions_array[@]}"; do
 				fileswc=0

+ 19 - 0
lgsm/modules/command_mods_install.sh

@@ -109,6 +109,11 @@ if [ -f "${modsinstalledlistfullpath}" ]; then
 fi
 
 ## Installation.
+# CounterStrikeSharp requires Metamod:Source 2.0.
+if [ "${modcommand}" == "counterstrikesharp" ]; then
+	fn_mod_exist "metamodsource2"
+fi
+
 # If amxmodx check if metamod exists first
 if [ "${modcommand}" == "amxmodx" ]; then
 	fn_mod_exist "metamod"
@@ -122,17 +127,31 @@ if [ "${modcommand}" == "amxmodxcs" ] \
 	fn_mod_exist "amxmodx"
 fi
 
+# If nolobbyreservation check if sourcemod exists first as it is compiled using the SourceMod compiler
+if [ "${modcommand}" == "nolobbyreservation" ]; then
+	fn_mod_exist "sourcemod"
+fi
+
 fn_create_mods_dir
 fn_mods_clear_tmp_dir
 fn_mods_create_tmp_dir
 fn_mod_install_files
 fn_mod_lowercase
+# NoLobbyReservation is only distributed as source
+if [ "${modcommand}" == "nolobbyreservation" ]; then
+	fn_mod_compile_sourcemod_plugin "nolobbyreservation"
+fi
 fn_mod_create_filelist
 fn_mod_copy_destination
 fn_mod_add_list
 fn_mod_tidy_files_list
 fn_mods_clear_tmp_dir
 
+# Install the Metamod:Source search path for CS2.
+if [ "${modcommand}" == "metamodsource2" ]; then
+	fn_mod_install_gameinfo_gi_file
+fi
+
 # Create/modify existing liblist.gam file for Metamod
 if [ "${modcommand}" == "metamod" ]; then
 	fn_mod_install_liblist_gam_file

+ 5 - 0
lgsm/modules/command_mods_remove.sh

@@ -151,6 +151,11 @@ if [ "${engine}" == "unity3d" ] && [[ "${modprettyname}" == *"Oxide"* ]]; then
 	unset exitbypass
 fi
 
+# Remove the Metamod:Source search path for CS2.
+if [ "${modcommand}" == "metamodsource2" ]; then
+	fn_mod_remove_gameinfo_gi_file
+fi
+
 # Remove/modify existing liblist.gam file for Metamod
 if [ "${modcommand}" == "metamod" ]; then
 	fn_mod_remove_liblist_gam_file

+ 8 - 0
lgsm/modules/command_mods_update.sh

@@ -88,11 +88,19 @@ while [ "${installedmodsline}" -le "${installedmodscount}" ]; do
 			fn_mod_install_files
 			fn_mod_lowercase
 			fn_remove_cfg_files
+			# NoLobbyReservation is only distributed as source
+			if [ "${modcommand}" == "nolobbyreservation" ]; then
+				fn_mod_compile_sourcemod_plugin "nolobbyreservation"
+			fi
 			fn_mod_create_filelist
 			fn_mod_copy_destination
 			fn_mod_add_list
 			fn_mod_tidy_files_list
 			fn_mods_clear_tmp_dir
+			# Restore the Metamod:Source search path if a CS2 update removed it.
+			if [ "${modcommand}" == "metamodsource2" ]; then
+				fn_mod_install_gameinfo_gi_file
+			fi
 		fi
 		((installedmodsline++))
 	else

+ 10 - 10
lgsm/modules/command_monitor.sh

@@ -76,7 +76,7 @@ fn_monitor_check_starting() {
 		fi
 	fi
 
-	if [ -f "${lockdir}/${selfname}-starting.lock" ] && [[ "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} start")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} s")" != "0" ]]; then
+	if [ -f "${lockdir}/${selfname}-starting.lock" ] && [[ "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} start")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} st")" != "0" ]]; then
 		fn_print_dots "Checking start"
 		fn_print_checking_eol
 		fn_print_info "Checking start: LinuxGSM is starting"
@@ -99,7 +99,7 @@ fn_monitor_check_stopping() {
 		fi
 	fi
 
-	if [ -f "${lockdir}/${selfname}-stopping.lock" ] && [[ "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} stop")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} s")" != "0" ]]; then
+	if [ -f "${lockdir}/${selfname}-stopping.lock" ] && [[ "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} stop")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} sp")" != "0" ]]; then
 		fn_print_dots "Checking stop"
 		fn_print_checking_eol
 		fn_print_info "Checking stop: LinuxGSM is stopping"
@@ -165,7 +165,7 @@ fn_monitor_check_update() {
 		fi
 	fi
 
-	if [ -f "${lockdir}/update.lock" ] && [[ "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} update")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} validate")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} v")" != "0" || "$(pgrep -fc force-update "${USER}" "/bin/bash ./${selfname} fu")" != "0" ]]; then
+	if [ -f "${lockdir}/update.lock" ] && [[ "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} update")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} u")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} validate")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} v")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} force-update")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} fu")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} update-restart")" != "0" || "$(pgrep -fcx -u "${USER}" "/bin/bash ./${selfname} ur")" != "0" ]]; then
 		fn_print_dots "Checking update"
 		fn_print_checking_eol
 		fn_print_info "Checking update: LinuxGSM is updating the game server"
@@ -211,23 +211,23 @@ fn_monitor_check_session() {
 		command_restart.sh
 		core_exit.sh
 	# Check for tmux pids with the same tmux session and socket names. This will reduce issues with migration to release v23.5.0. #4296
-	elif [ "$(pgrep -fc -u "${USER}" "tmux -L ${sessionname} new-session -d -x ${sessionwidth} -y ${sessionheight} -s ${sessionname}")" != "0" ]; then
+	elif [ "$(pgrep -fc -u "${USER}" "^tmux -L ${sessionname} new-session -d -x ${sessionwidth} -y ${sessionheight} -s ${sessionname}( |$)")" != "0" ]; then
 		fn_print_error "Checking session: PIDS with the same tmux session and socket names are running"
 		fn_print_error_eol_nl
 		fn_script_log_error "Checking session: ERROR"
 		fn_script_log_error "Checking session: PIDS with the same tmux session and socket names are running"
 		fn_script_log_error "Checking session: Killing session with the socketname name ${sessionname} and session name ${sessionname}"
-		pkill -f "tmux -L ${sessionname} new-session -d -x ${sessionwidth} -y ${sessionheight} -s ${sessionname}"
+		pkill -f "^tmux -L ${sessionname} new-session -d -x ${sessionwidth} -y ${sessionheight} -s ${sessionname}( |$)"
 		command_restart.sh
 		core_exit.sh
 	# Check for tmux pids that are using the old type of tmux session. This will reduce issues with migration to release v23.5.0. #4296
-	elif [ "$(pgrep -fc -u "${USER}" "tmux new-session -d -x ${sessionwidth} -y ${sessionheight} -s ${sessionname}")" != "0" ]; then
+	elif [ "$(pgrep -fc -u "${USER}" "^tmux new-session -d -x ${sessionwidth} -y ${sessionheight} -s ${sessionname}( |$)")" != "0" ]; then
 		fn_print_error "Checking session: PIDS with old type tmux session are running"
 		fn_print_error_eol_nl
 		fn_script_log_error "Checking session: ERROR"
 		fn_script_log_error "Checking session: PIDS with old type tmux session are running"
 		fn_script_log_error "Checking session: Killing session with the session name ${sessionname}"
-		pkill -f "tmux new-session -d -x ${sessionwidth} -y ${sessionheight} -s ${sessionname}"
+		pkill -f "^tmux new-session -d -x ${sessionwidth} -y ${sessionheight} -s ${sessionname}( |$)"
 		command_restart.sh
 		core_exit.sh
 	elif [ "${status}" != "0" ]; then
@@ -291,9 +291,9 @@ fn_monitor_query() {
 			fn_script_log_info "Querying port: ${querymethod}: ${queryip}:${queryport} : ${queryattempt} : QUERYING"
 			# querydelay
 			if [ "$(head -n 1 "${lockdir}/${selfname}-started.lock")" -gt "$(date "+%s" -d "${querydelay} mins ago")" ]; then
-				fn_print_ok "Querying port: ${querymethod}: ${ip}:${queryport} : ${totalseconds}/${queryattempt}"
+				fn_print_ok "Querying port: ${querymethod}: ${queryip}:${queryport} : ${totalseconds}/${queryattempt}"
 				fn_print_delay_eol_nl
-				fn_script_log_info "Querying port: ${querymethod}: ${ip}:${queryport} : ${queryattempt} : DELAY"
+				fn_script_log_info "Querying port: ${querymethod}: ${queryip}:${queryport} : ${queryattempt} : DELAY"
 				fn_script_log_info "Query bypassed: ${gameservername} started less than ${querydelay} minutes ago"
 				fn_script_log_info "Server started: $(date -d "@$(head -n 1 "${lockdir}/${selfname}-started.lock")")"
 				fn_script_log_info "Current time: $(date)"
@@ -364,7 +364,7 @@ fn_monitor_query() {
 		done
 		# Second counter will wait for 15s before breaking loop.
 		for seconds in {1..15}; do
-			fn_print_fail "Querying port: ${querymethod}: ${ip}:${queryport} : ${totalseconds}/${queryattempt}"
+			fn_print_fail "Querying port: ${querymethod}: ${queryip}:${queryport} : ${totalseconds}/${queryattempt}"
 			fn_print_wait_eol
 			fn_sleep_time_1
 			totalseconds=$((totalseconds + 1))

+ 59 - 0
lgsm/modules/command_start.sh

@@ -18,6 +18,55 @@ fn_start_jk2() {
 	TERM=screen tmux -L "${socketname}" end -t "${sessionname}" version ENTER > /dev/null 2>&1
 }
 
+# Hytale: after the server boots, check whether it logged in. If it didn't (no saved login, or one that
+# can't be decrypted, e.g. created on another machine or container), start the login, then show the
+# login URL and send it as an alert, so it can be approved without opening the console.
+# Prints the server console: the console log, or the tmux pane when console logging is off.
+fn_start_hyt_console() {
+	if [ "${consolelogging}" == "off" ]; then
+		TERM=screen tmux -L "${socketname}" capture-pane -p -J -S - -t "${sessionname}" 2> /dev/null
+	else
+		cat "${consolelog}" 2> /dev/null
+	fi
+}
+
+fn_start_hyt_login() {
+	hytloginurl=""
+	local hytloginsent="${hytloginpending}"
+	for _ in {1..120}; do
+		hytloginurl=$(fn_start_hyt_console | grep -a -oE "https://[^ ]*device/verify\?user_code=[A-Za-z0-9]+" | tail -1)
+		if [ -n "${hytloginurl}" ]; then
+			break
+		fi
+		# Logged in with the saved server login: nothing to do.
+		if fn_start_hyt_console | grep -a -q "Authentication successful"; then
+			fn_script_log_info "Hytale server login restored from saved credentials"
+			return
+		fi
+		# Booted without a usable login: ask the server to start one.
+		if [ "${hytloginsent}" != "1" ] && fn_start_hyt_console | grep -a -q "Hytale Server Booted" && fn_start_hyt_console | grep -a -q "No server tokens configured"; then
+			fn_script_log_info "Hytale server has no usable saved login, starting login"
+			TERM=screen tmux -L "${socketname}" send-keys -t "${sessionname}" "auth login device" ENTER
+			hytloginsent="1"
+		fi
+		sleep 1
+	done
+	echo -e ""
+	if [ -n "${hytloginurl}" ]; then
+		echo -e "${bold}${lightyellow}Hytale server login required${default}"
+		echo -e "Open the URL below in a browser and log in with a Hytale account that owns the game."
+		echo -e "The code expires in 10 minutes. This is only needed once; the server saves the login."
+		echo -e "${hytloginurl}"
+		fn_script_log_info "Hytale server login required: ${hytloginurl}"
+		alert="hytale-login"
+		alert.sh
+	else
+		fn_print_warn_nl "Hytale server login required, but no login URL was found in the console yet"
+		echo -e "* Run ./${selfname} send \"/auth login device\" and approve the code shown in the console."
+		fn_script_log_warn "Hytale server login required, but no login URL was found in the console"
+	fi
+}
+
 fn_start_tmux() {
 	# check for tmux size variables.
 	if [[ "${servercfgtmuxwidth}" =~ ^[0-9]+$ ]]; then
@@ -48,6 +97,13 @@ fn_start_tmux() {
 
 	fn_reload_startparameters
 
+	# Hytale: with no saved server login file, have the server start its login (OAuth device code) on boot.
+	# A saved login that can't be used is handled after boot by fn_start_hyt_login.
+	if [ "${shortname}" == "hyt" ] && [ ! -f "${systemdir}/auth.enc" ]; then
+		startparameters="${startparameters} --boot-command 'auth login device'"
+		hytloginpending="1"
+	fi
+
 	# Create uid to ensure unique tmux socket name.
 	if [ ! -f "${datadir}/${selfname}.uid" ]; then
 		check_status.sh
@@ -160,6 +216,9 @@ fn_start_tmux() {
 			alert="restarted"
 			alert.sh
 		fi
+		if [ "${shortname}" == "hyt" ]; then
+			fn_start_hyt_login
+		fi
 	fi
 	rm -f "${lgsmlogdir:?}/.${selfname}-tmux-error.tmp" 2> /dev/null
 	echo -en "\n"

+ 2 - 0
lgsm/modules/command_update.sh

@@ -25,6 +25,8 @@ elif [ "${shortname}" == "pmc" ] || [ "${shortname}" == "vpmc" ] || [ "${shortna
 	update_pmc.sh
 elif [ "${shortname}" == "fctr" ]; then
 	update_fctr.sh
+elif [ "${shortname}" == "hyt" ]; then
+	update_hyt.sh
 elif [ "${shortname}" == "mta" ]; then
 	update_mta.sh
 elif [ "${shortname}" == "jk2" ]; then

+ 16 - 1
lgsm/modules/core_dl.sh

@@ -65,6 +65,7 @@ fn_dl_steamcmd() {
 	if [ -f "${steamcmdlog}" ]; then
 		rm -f "${steamcmdlog:?}"
 	fi
+	local invalidplatformworkaround=""
 	counter=0
 	while [ "${counter}" -eq 0 ] || [ "${exitcode}" -ne 0 ]; do
 		counter=$((counter + 1))
@@ -139,7 +140,16 @@ fn_dl_steamcmd() {
 				fn_print_failure_nl "${commandaction} ${selfname}: ${remotelocation}: Not enough disk space to download server files"
 				fn_script_log_fail "${commandaction} ${selfname}: ${remotelocation}: Not enough disk space to download server files"
 				core_exit.sh
-			# Invalid platform for app/update request.
+			# Invalid platform (SteamCMD bug, e.g. Left 4 Dead 2): retry once after a Windows platform download.
+			elif [ -n "$(grep -i "Invalid platform" "${steamcmdlog}" | tail -1)" ] && [ "${steamcmdforcewindows}" != "yes" ] && [ -z "${invalidplatformworkaround}" ]; then
+				invalidplatformworkaround="1"
+				fn_print_error2_nl "${commandaction} ${selfname}: ${remotelocation}: Invalid platform - applying SteamCMD Windows platform workaround"
+				fn_script_log_error "${commandaction} ${selfname}: ${remotelocation}: Invalid platform - applying SteamCMD Windows platform workaround"
+				"${unbuffercommand[@]}" "${steamcmdcommandarray[@]}" +@sSteamCmdForcePlatformType windows +force_install_dir "${serverfiles}" +login "${steamuser}" "${steampass}" +app_update "${appid}" +quit | uniq | tee -a "${lgsmlog}" "${steamcmdlog}"
+				validateparam=("validate")
+				rm -f "${steamcmdlog:?}"
+				counter=0
+			# Invalid platform for app/update request (after the workaround, or when forcing Windows).
 			elif [ -n "$(grep -i "Invalid platform" "${steamcmdlog}" | tail -1)" ]; then
 				fn_print_failure_nl "${commandaction} ${selfname}: ${remotelocation}: Invalid platform for AppID ${appid}"
 				fn_print_nl "Check steamcmdforcewindows setting and system architecture (x86_64 required for most servers)"
@@ -175,6 +185,11 @@ fn_dl_steamcmd() {
 			elif [ -n "$(grep "0x6A6" "${steamcmdlog}" | tail -1)" ]; then
 				fn_print_error2_nl "${commandaction} ${selfname}: ${remotelocation}: Corrupt update files"
 				fn_script_log_error "${commandaction} ${selfname}: ${remotelocation}: Corrupt update files"
+			# Steam app metadata unavailable.
+			elif [ -n "$(grep -i "Missing configuration" "${steamcmdlog}" | tail -1)" ]; then
+				fn_print_error2_nl "${commandaction} ${selfname}: ${remotelocation}: Missing configuration - Steam app metadata unavailable for AppID ${appid}"
+				fn_print_nl "Please provide content log to LinuxGSM developers https://linuxgsm.com/steamcmd-error"
+				fn_script_log_error "${commandaction} ${selfname}: ${remotelocation}: Missing configuration - Steam app metadata unavailable for AppID ${appid}"
 			else
 				fn_print_error2_nl "${commandaction} ${selfname}: ${remotelocation}: Unknown error occurred"
 				fn_print_nl "Please provide content log to LinuxGSM developers https://linuxgsm.com/steamcmd-error"

+ 3 - 3
lgsm/modules/core_getopt.sh

@@ -69,8 +69,8 @@ currentopt+=("${cmd_update_linuxgsm[@]}")
 if [ "${shortname}" == "jk2" ] || [ "${shortname}" == "etl" ] || [ "${engine}" != "idtech3" ]; then
 	if [ "${shortname}" != "bf1942" ] && [ "${shortname}" != "bfv" ] && [ "${engine}" != "idtech2" ] && [ "${engine}" != "iw2.0" ] && [ "${engine}" != "iw3.0" ] && [ "${engine}" != "quake" ] && [ "${shortname}" != "samp" ] && [ "${shortname}" != "ut2k4" ]; then
 		currentopt+=("${cmd_update[@]}" "${cmd_check_update[@]}")
-		# force update for SteamCMD or Multi Theft Auto only.
-		if [ "${appid}" ] || [ "${shortname}" == "mta" ]; then
+		# force update for SteamCMD, Multi Theft Auto or Hytale only.
+		if [ "${appid}" ] || [ "${shortname}" == "mta" ] || [ "${shortname}" == "hyt" ]; then
 			currentopt+=("${cmd_force_update[@]}")
 		fi
 	fi
@@ -137,7 +137,7 @@ if [ "${shortname}" == "squad" ]; then
 fi
 
 ## Mods commands.
-if [ "${engine}" == "source" ] || [ "${shortname}" == "rust" ] || [ "${shortname}" == "hq" ] || [ "${shortname}" == "sdtd" ] || [ "${shortname}" == "cs" ] || [ "${shortname}" == "dod" ] || [ "${shortname}" == "tfc" ] || [ "${shortname}" == "ns" ] || [ "${shortname}" == "ts" ] || [ "${shortname}" == "hldm" ] || [ "${shortname}" == "vh" ]; then
+if [ "${engine}" == "source" ] || [ "${shortname}" == "cs2" ] || [ "${shortname}" == "rust" ] || [ "${shortname}" == "hq" ] || [ "${shortname}" == "sdtd" ] || [ "${shortname}" == "cs" ] || [ "${shortname}" == "dod" ] || [ "${shortname}" == "tfc" ] || [ "${shortname}" == "ns" ] || [ "${shortname}" == "ts" ] || [ "${shortname}" == "hldm" ] || [ "${shortname}" == "vh" ]; then
 	currentopt+=("${cmd_mods_install[@]}" "${cmd_mods_remove[@]}" "${cmd_mods_update[@]}")
 fi
 

+ 3 - 1
lgsm/modules/core_messages.sh

@@ -222,7 +222,9 @@ fn_print_start_nl() {
 # Separator is different for details.
 fn_messages_separator() {
 	if [ "${commandname}" == "DETAILS" ]; then
-		printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' =
+		# Fall back to 80 columns when there is no terminal (e.g. cron or Docker); bash 5.3+ rejects an empty printf width.
+		local columns="${COLUMNS:-$(tput cols 2> /dev/null)}"
+		printf '%*s\n' "${columns:-80}" '' | tr ' ' =
 	else
 		echo -e "${bold}=================================${default}"
 		fn_sleep_time

+ 6 - 1
lgsm/modules/core_modules.sh

@@ -8,7 +8,7 @@
 
 moduleselfname="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")"
 
-modulesversion="v26.2.0"
+modulesversion="v26.3.0"
 
 # Core
 
@@ -680,6 +680,11 @@ update_fctr.sh() {
 	fn_fetch_module
 }
 
+update_hyt.sh() {
+	modulefile="${FUNCNAME[0]}"
+	fn_fetch_module
+}
+
 update_jk2.sh() {
 	modulefile="${FUNCNAME[0]}"
 	fn_fetch_module

+ 22 - 3
lgsm/modules/fix_csgo.sh

@@ -7,11 +7,30 @@
 
 moduleselfname="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")"
 
+# Server files are downloaded using App ID 740 (appid), but the App ID written to steam_appid.txt and steam.inf
+# (clientappid) decides which client can connect: 4465480 for standalone CS:GO, 730 for the CS2 csgo_legacy branch.
+# SteamCMD update/validate can revert these files, so this runs before every start.
+
 # Fixes: server not always creating steam_appid.txt file.
-if [ ! -f "${serverfiles}/steam_appid.txt" ]; then
-	fixname="730 steam_appid.txt"
+# The server writes steam_appid.txt with a trailing null byte, which is removed before comparing.
+if [ -n "${clientappid}" ] && { [ ! -f "${serverfiles}/steam_appid.txt" ] || [ "$(tr -d '\0' < "${serverfiles}/steam_appid.txt")" != "${clientappid}" ]; }; then
+	fixname="${clientappid} steam_appid.txt"
+	fn_fix_msg_start
+	if echo -n "${clientappid}" > "${serverfiles}/steam_appid.txt"; then
+		exitcode=0
+	else
+		exitcode=1
+	fi
+	fn_fix_msg_end
+fi
+
+# Fixes: steam.inf not using the client App ID.
+# steam.inf uses CRLF line endings, the whole value is replaced while keeping the line ending.
+if [ -n "${clientappid}" ] && [ -f "${systemdir}/steam.inf" ] && grep -q "^appID=" "${systemdir}/steam.inf" && ! grep -qE "^appID=${clientappid}[[:cntrl:]]?$" "${systemdir}/steam.inf"; then
+	fixname="${clientappid} steam.inf"
 	fn_fix_msg_start
-	echo -n "730" >> "${serverfiles}/steam_appid.txt"
+	sed -i "s/^appID=[^[:cntrl:]]*/appID=${clientappid}/" "${systemdir}/steam.inf"
+	exitcode=$?
 	fn_fix_msg_end
 fi
 

+ 54 - 4
lgsm/modules/info_game.sh

@@ -757,8 +757,12 @@ fn_info_game_ts3() {
 		fn_info_game_ini "httpqueryport" "query_http_port"
 		fn_info_game_ini "httpsqueryport" "query_https_port"
 		fn_info_game_ini "queryport" "query_port"
+		fn_info_game_ini "configqueryip" "query_ip"
 		fn_info_game_ini "sshqueryport" "query_ssh_port"
 	fi
+	# query_ip can list several addresses (e.g. "0.0.0.0, ::"), use the first.
+	configqueryip="${configqueryip%%,*}"
+	configqueryip="${configqueryip// /}"
 	configip="${configip:-"0.0.0.0"}"
 	dbplugin="${dbplugin:-"NOT SET"}"
 	fileport="${fileport:-"0"}"
@@ -1350,6 +1354,27 @@ fn_info_game_hz() {
 	servername="${servername:-"NOT SET"}"
 }
 
+# Config Type: json
+# Parameters: true
+# Comment:
+# Example: "ServerName": "SERVERNAME"
+# Filetype: json
+fn_info_game_hyt() {
+	# config.json is created by the server on first start.
+	if [ -f "${servercfgfullpath}" ]; then
+		fn_info_game_json "maxplayers" ".MaxPlayers // empty"
+		fn_info_game_json "servername" ".ServerName // empty"
+		fn_info_game_json "serverpassword" ".Password // empty"
+	fi
+	configtype="json"
+	configip="${ip:-"0.0.0.0"}"
+	maxplayers="${maxplayers:-"0"}"
+	port="${port:-"5520"}"
+	queryport="${port}"
+	servername="${servername:-"NOT SET"}"
+	serverpassword="${serverpassword:-"NOT SET"}"
+}
+
 # Config Type: parameters
 # Parameters: true
 # Comment:
@@ -1799,6 +1824,28 @@ fn_info_game_ro() {
 	unreal2queryport="$((port + 1))" # Unreal2 Query Port
 }
 
+# Config Type: ini
+# Parameters: true
+# Comment: ; or #
+# Example: ServerName=SERVERNAME
+# Filetype: ini
+fn_info_game_rsdw() {
+	if [ -f "${servercfgfullpath}" ]; then
+		fn_info_game_ini "adminpassword" "AdminPassword"
+		fn_info_game_ini "ownerid" "OwnerId"
+		fn_info_game_ini "servername" "ServerName"
+		fn_info_game_ini "serverpassword" "WorldPassword"
+		fn_info_game_ini "worldname" "DefaultWorldName"
+	fi
+	adminpassword="${adminpassword:-"NOT SET"}"
+	ownerid="${ownerid:-"NOT SET"}"
+	port="${port:-"0"}"
+	queryport="${port:-"0"}"
+	servername="${servername:-"NOT SET"}"
+	serverpassword="${serverpassword:-"NOT SET"}"
+	worldname="${worldname:-"NOT SET"}"
+}
+
 # Config Type: QuakeC
 # Comment: // or /* */
 # Example: set sv_hostname "SERVERNAME"
@@ -1960,7 +2007,7 @@ fn_info_game_sdtd() {
 		fn_info_game_xml "serverpassword" "/ServerSettings/property[@name='ServerPassword']/@value"
 		fn_info_game_xml "port" "/ServerSettings/property[@name='ServerPort']/@value"
 		fn_info_game_xml "telnetenabled" "/ServerSettings/property[@name='TelnetEnabled']/@value"
-		fn_info_game_xml "telnetpass" "/ServerSettings/property[@name='TelnetPassword']/@value"
+		fn_info_game_xml "telnetpassword" "/ServerSettings/property[@name='TelnetPassword']/@value"
 		fn_info_game_xml "telnetport" "/ServerSettings/property[@name='TelnetPort']/@value"
 		fn_info_game_xml "worldname" "/ServerSettings/property[@name='GameWorld']/@value"
 	fi
@@ -1980,7 +2027,7 @@ fn_info_game_sdtd() {
 	if [ -z "${telnetpassword}" ]; then
 		telnetip="127.0.0.1"
 	fi
-	telnetpass="${telnetpass:-"NOT SET"}"
+	telnetpassword="${telnetpassword:-"NOT SET"}"
 	telnetport="${telnetport:-"0"}"
 	worldname="${worldname:-"NOT SET"}"
 
@@ -1991,8 +2038,7 @@ fn_info_game_sf() {
 	# Parameters
 	servername="${servername:-"NOT SET"}"
 	port="${port:-"0"}"
-	queryport="${queryport:-"0"}"
-	beaconport="${beaconport:-"0"}"
+	queryport="${port}"
 	reliableport="${reliableport:-"0"}"
 }
 
@@ -2375,6 +2421,8 @@ elif [ "${shortname}" == "hw" ]; then
 	fn_info_game_hw
 elif [ "${shortname}" == "hz" ]; then
 	fn_info_game_hz
+elif [ "${shortname}" == "hyt" ]; then
+	fn_info_game_hyt
 elif [ "${shortname}" == "inss" ]; then
 	fn_info_game_inss
 elif [ "${shortname}" == "jc2" ]; then
@@ -2427,6 +2475,8 @@ elif [ "${shortname}" == "qw" ]; then
 	fn_info_game_qw
 elif [ "${shortname}" == "ro" ]; then
 	fn_info_game_ro
+elif [ "${shortname}" == "rsdw" ]; then
+	fn_info_game_rsdw
 elif [ "${shortname}" == "rtcw" ]; then
 	fn_info_game_rtcw
 elif [ "${shortname}" == "rust" ]; then

+ 28 - 4
lgsm/modules/info_messages.sh

@@ -730,7 +730,7 @@ fn_info_messages_ports_edit() {
 		fi
 	done
 	# engines/games that require editing the start parameters.
-	local ports_edit_array=("av" "ck" "col" "cs2" "fctr" "goldsrc" "hcu" "hw" "iw3.0" "ioquake3" "pw" "qfusion" "rust" "scpsl" "scpslsm" "sf" "sol" "spark" "source" "unreal4" "arma3" "dayz" "unt" "vh")
+	local ports_edit_array=("av" "ck" "col" "cs2" "fctr" "goldsrc" "hcu" "hw" "hyt" "iw3.0" "ioquake3" "pw" "qfusion" "rsdw" "rust" "scpsl" "scpslsm" "sf" "sol" "spark" "source" "unreal4" "arma3" "dayz" "unt" "vh")
 	for port_edit in "${ports_edit_array[@]}"; do
 		if [ "${engine}" == "${port_edit}" ] || [ "${gamename}" == "${port_edit}" ] || [ "${shortname}" == "${port_edit}" ]; then
 			startparameterslocation="${configdirserver}"
@@ -754,7 +754,7 @@ fn_info_messages_ports() {
 		portcommand="ss -tuplwn | grep enfMain"
 	elif [ "${shortname}" == "q4" ]; then
 		portcommand="ss -tuplwn | grep q4ded.x86"
-	elif [ "${shortname}" == "mc" ] || [ "${shortname}" == "nec" ] || [ "${shortname}" == "pmc" ] || [ "${shortname}" == "vpmc" ] || [ "${shortname}" == "wmc" ]; then
+	elif [ "${shortname}" == "hyt" ] || [ "${shortname}" == "mc" ] || [ "${shortname}" == "nec" ] || [ "${shortname}" == "pmc" ] || [ "${shortname}" == "vpmc" ] || [ "${shortname}" == "wmc" ]; then
 		portcommand="ss -tuplwn | grep java"
 	elif [ "${shortname}" == "terraria" ]; then
 		portcommand="ss -tuplwn | grep Main"
@@ -1131,6 +1131,13 @@ fn_info_messages_hz() {
 	} | column -s $'\t' -t
 }
 
+fn_info_messages_hyt() {
+	{
+		fn_port "header"
+		fn_port "Game" port udp
+	} | column -s $'\t' -t
+}
+
 fn_info_messages_ins() {
 	{
 		fn_port "header"
@@ -1412,6 +1419,20 @@ fn_info_messages_ro() {
 	} | column -s $'\t' -t
 }
 
+fn_info_messages_rsdw() {
+	{
+		fn_port "header"
+		fn_port "Game" port udp
+	} | column -s $'\t' -t
+	echo -e ""
+	echo -e "${bold}${lightgreen}${gamename} Game Settings${default}"
+	fn_messages_separator
+	{
+		echo -e "${lightblue}World name:\t${default}${worldname}"
+		echo -e "${lightblue}Owner ID:\t${default}${ownerid}"
+	} | column -s $'\t' -t
+}
+
 fn_info_messages_rtcw() {
 	{
 		fn_port "header"
@@ -1502,8 +1523,7 @@ fn_info_messages_sf() {
 	{
 		fn_port "header"
 		fn_port "Game" port udp
-		fn_port "Query" queryport udp
-		fn_port "Beacon" beaconport udp
+		fn_port "Server API" port tcp
 		fn_port "ReliableMessaging" reliableport tcp
 	} | column -s $'\t' -t
 }
@@ -1860,6 +1880,8 @@ fn_info_messages_select_engine() {
 		fn_info_messages_hw
 	elif [ "${shortname}" == "hz" ]; then
 		fn_info_messages_hz
+	elif [ "${shortname}" == "hyt" ]; then
+		fn_info_messages_hyt
 	elif [ "${shortname}" == "ins" ]; then
 		fn_info_messages_ins
 	elif [ "${shortname}" == "inss" ]; then
@@ -1914,6 +1936,8 @@ fn_info_messages_select_engine() {
 		fn_info_messages_qw
 	elif [ "${shortname}" == "ro" ]; then
 		fn_info_messages_ro
+	elif [ "${shortname}" == "rsdw" ]; then
+		fn_info_messages_rsdw
 	elif [ "${shortname}" == "rtcw" ]; then
 		fn_info_messages_rtcw
 	elif [ "${shortname}" == "samp" ]; then

+ 8 - 0
lgsm/modules/install_config.sh

@@ -520,6 +520,9 @@ elif [ "${shortname}" == "hl2dm" ]; then
 elif [ "${shortname}" == "hz" ]; then
 	# Config is generated on first run
 	:
+elif [ "${shortname}" == "hyt" ]; then
+	# Config is generated on first run
+	:
 elif [ "${shortname}" == "ins" ]; then
 	array_configs+=(server.cfg)
 	fn_default_config_remote
@@ -668,6 +671,11 @@ elif [ "${shortname}" == "ricochet" ]; then
 	fn_default_config_remote
 	fn_set_config_vars
 	fn_list_config_locations
+elif [ "${shortname}" == "rsdw" ]; then
+	array_configs+=(DedicatedServer.ini)
+	fn_default_config_remote
+	fn_set_config_vars
+	fn_list_config_locations
 elif [ "${shortname}" == "rtcw" ]; then
 	array_configs+=(server.cfg)
 	fn_default_config_remote

+ 3 - 1
lgsm/modules/install_factorio_save.sh

@@ -11,4 +11,6 @@ echo -e ""
 echo -e "${bold}${lightyellow}Creating initial Factorio savefile${default}"
 fn_messages_separator
 check_glibc.sh
-"${executabledir}"/factorio --create "${serverfiles}/save1"
+# Create the save in the saves directory so --start-server-load-latest can find it.
+mkdir -p "${serverfiles}/saves"
+"${executabledir}"/factorio --create "${serverfiles}/saves/save1"

+ 4 - 0
lgsm/modules/install_gslt.sh

@@ -17,6 +17,10 @@ else
 	echo -e "GSLT is an optional feature for ${gamename} server."
 	fn_script_log_info "GSLT is an optional feature for ${gamename} server"
 fi
+if [ "${shortname}" == "csgo" ] && [ -n "${clientappid}" ]; then
+	echo -e "The GSLT must be created using the App ID set in clientappid (${clientappid})."
+	fn_script_log_info "The GSLT must be created using the App ID set in clientappid (${clientappid})"
+fi
 
 echo -e ""
 echo -e "More info: ${italic}https://docs.linuxgsm.com/steamcmd/gslt${default}"

+ 3 - 0
lgsm/modules/install_server_files.sh

@@ -220,6 +220,7 @@ fn_install_server_files() {
 	else
 		fn_print_fail_nl "Installing ${gamename} Server failed, missing default configuration"
 		fn_script_log_fail "Installing ${gamename} Server failed, missing default configuration"
+		core_exit.sh
 	fi
 	fn_fetch_file "${remote_fileurl}" "" "" "" "${local_filedir}" "${local_filename}" "${chmodx}" "${run}" "${forcedl}" "${md5}"
 	fn_dl_extract "${local_filedir}" "${local_filename}" "${serverfiles}"
@@ -256,6 +257,8 @@ elif [ "${shortname}" == "mta" ]; then
 elif [ "${shortname}" == "fctr" ]; then
 	update_fctr.sh
 	install_factorio_save.sh
+elif [ "${shortname}" == "hyt" ]; then
+	update_hyt.sh
 elif [ "${shortname}" == "jk2" ]; then
 	update_jk2.sh
 elif [ "${shortname}" == "vints" ]; then

+ 108 - 3
lgsm/modules/mods_core.sh

@@ -29,17 +29,54 @@ fn_mod_install_files() {
 		mkdir -p "${extractdest}"
 	fi
 	fn_dl_extract "${modstmpdir}" "${modfilename}" "${extractdest}"
-	# If modsubdirs names a specific subfolder, use its contents as the install root.
+	# If modsubdirs names a specific subfolder (or path to one), use its contents as the install root.
 	if [ "${modsubdirs}" != "0" ] && [ -d "${extractdest}/${modsubdirs}" ]; then
 		local tmpsubdir
 		tmpsubdir=$(mktemp -d)
 		mv "${extractdest}/${modsubdirs}" "${tmpsubdir}/"
 		rm -rf "${extractdest}"
-		mv "${tmpsubdir}/${modsubdirs}" "${extractdest}"
+		mv "${tmpsubdir}/$(basename "${modsubdirs}")" "${extractdest}"
 		rm -rf "${tmpsubdir}"
 	fi
 }
 
+# Compile a SourceMod plugin that is only distributed as source, using the installed SourceMod compiler.
+# The compiled plugin is added to the extracted mod files so it is installed, updated and removed with the mod.
+fn_mod_compile_sourcemod_plugin() {
+	local pluginname="${1}"
+	local spcompdir="${modinstalldir}/addons/sourcemod/scripting"
+	# Prefer the 64-bit compiler; the 32-bit spcomp needs i386 libraries.
+	local spcomp="${spcompdir}/spcomp64"
+	if [ ! -x "${spcomp}" ]; then
+		spcomp="${spcompdir}/spcomp"
+	fi
+	echo -en "compiling ${pluginname}.sp..."
+	fn_sleep_time
+	if [ ! -x "${spcomp}" ]; then
+		fn_print_fail_eol_nl
+		fn_script_log_fail "Compiling ${pluginname}.sp: SourceMod compiler not found in ${spcompdir}"
+		echo -e "* SourceMod compiler not found in ${spcompdir}"
+		exitcode=1
+		core_exit.sh
+	fi
+	mkdir -p "${extractdest}/addons/sourcemod/plugins"
+	spcompoutput=$("${spcomp}" -i"${spcompdir}/include" "${extractdest}/addons/sourcemod/scripting/${pluginname}.sp" -o"${extractdest}/addons/sourcemod/plugins/${pluginname}.smx" 2>&1)
+	exitcode=$?
+	if [ "${exitcode}" -ne 0 ] || [ ! -s "${extractdest}/addons/sourcemod/plugins/${pluginname}.smx" ]; then
+		fn_print_fail_eol_nl
+		fn_script_log_fail "Compiling ${pluginname}.sp"
+		if [ -f "${lgsmlog}" ]; then
+			echo -e "${spcompoutput}" >> "${lgsmlog}"
+		fi
+		echo -e "${spcompoutput}"
+		exitcode=1
+		core_exit.sh
+	else
+		fn_print_ok_eol_nl
+		fn_script_log_pass "Compiling ${pluginname}.sp"
+	fi
+}
+
 # Convert mod files to lowercase if needed.
 fn_mod_lowercase() {
 	# Checking lowercase settings from mods array definition
@@ -158,8 +195,13 @@ fn_mod_tidy_files_list() {
 		sed -i "/^addons\/metamod\/sourcemod.vdf$/d" "${modsdir}/${modcommand}-files.txt"
 	fi
 
+	# Keep Metamod:Source when removing CounterStrikeSharp, but remove its own VDF.
+	if [ "${modcommand}" == "counterstrikesharp" ]; then
+		sed -i "/^addons\/metamod$/d" "${modsdir}/${modcommand}-files.txt"
+	fi
+
 	# Remove common paths from deletion list (Add your sourcemod mod here)
-	if [ "${modcommand}" == "gokz" ] || [ "${modcommand}" == "ttt" ] || [ "${modcommand}" == "steamworks" ] || [ "${modcommand}" == "get5" ]; then
+	if [ "${modcommand}" == "gokz" ] || [ "${modcommand}" == "ttt" ] || [ "${modcommand}" == "steamworks" ] || [ "${modcommand}" == "get5" ] || [ "${modcommand}" == "nolobbyreservation" ]; then
 		sed -i "/^addons\/sourcemod$/d" "${modsdir}/${modcommand}-files.txt"
 		sed -i "/^addons\/sourcemod\/configs$/d" "${modsdir}/${modcommand}-files.txt"
 		sed -i "/^addons\/sourcemod\/extensions$/d" "${modsdir}/${modcommand}-files.txt"
@@ -197,6 +239,9 @@ fn_mod_tidy_files_list() {
 		sed -i "/^addons\/sourcemod\/scripting\/include\/dhooks.inc$/d" "${modsdir}/${modcommand}-files.txt"
 		sed -i "/^addons\/sourcemod\/scripting\/include\/updater.inc$/d" "${modsdir}/${modcommand}-files.txt"
 	fi
+	if [ "${modcommand}" == "nolobbyreservation" ]; then
+		sed -i "/^addons\/sourcemod\/gamedata$/d" "${modsdir}/${modcommand}-files.txt"
+	fi
 }
 
 ## Information Gathering.
@@ -693,6 +738,66 @@ fn_mod_remove_liblist_gam_file() {
 	fi
 }
 
+# modifiers for gameinfo.gi to add/remove the Metamod:Source search path
+# Counter-Strike 2 only: the csgo paths below are specific to that game.
+fn_mod_install_gameinfo_gi_file() {
+	local gameinfofile="${modinstalldir}/gameinfo.gi"
+	local metamodpattern='^[[:space:]]*"?Game"?[[:space:]]+"?csgo/addons/metamod"?[[:space:]]*(//.*)?$'
+	local lowviolencepattern='^[[:space:]]*"?Game_LowViolence"?[[:space:]]+"?csgo_lv"?[[:space:]]*(//.*)?$'
+
+	if [ -f "${gameinfofile}" ]; then
+		# the search path survives a mod update, only add it when it is missing
+		if grep -Eq "${metamodpattern}" "${gameinfofile}"; then
+			fn_script_log_info "Metamod:Source search path already set in ${gameinfofile}"
+		else
+			# insert the search path below Game_LowViolence as documented by Metamod:Source
+			logentry="line (Game csgo/addons/metamod) inserted into ${gameinfofile}"
+			echo -en "adding Metamod:Source search path in gameinfo.gi..."
+			sed -i -E "\\@${lowviolencepattern}@a\\\\t\\t\\tGame\\tcsgo/addons/metamod" "${gameinfofile}"
+			grep -Eq "${metamodpattern}" "${gameinfofile}"
+			exitcode=$?
+			# if the insert didn't happen, error out
+			if [ "${exitcode}" -ne 0 ]; then
+				fn_script_log_fail "${logentry}"
+				fn_print_fail_eol_nl
+				core_exit.sh
+			else
+				fn_script_log_pass "${logentry}"
+				fn_print_ok_eol_nl
+			fi
+		fi
+	else
+		fn_script_log_fail "Unable to find ${gameinfofile}"
+		fn_print_fail_nl "Unable to find ${gameinfofile}"
+		core_exit.sh
+	fi
+}
+
+# Unlike the install counterpart this does not call core_exit.sh on failure:
+# command_mods_remove.sh ends with fn_script_log, which leaves exitcode alone,
+# while command_mods_install.sh ends with fn_script_log_pass, which resets it to 0.
+fn_mod_remove_gameinfo_gi_file() {
+	local gameinfofile="${modinstalldir}/gameinfo.gi"
+	local metamodpattern='^[[:space:]]*"?Game"?[[:space:]]+"?csgo/addons/metamod"?[[:space:]]*(//.*)?$'
+
+	if [ -f "${gameinfofile}" ]; then
+		# is the search path found? If so remove it and keep the rest of the file
+		if grep -Eq "${metamodpattern}" "${gameinfofile}"; then
+			logentry="line (Game csgo/addons/metamod) removed from ${gameinfofile}"
+			echo -en "removing Metamod:Source search path in gameinfo.gi..."
+			# delete the line we inserted
+			sed -i -E "\\@${metamodpattern}@d" "${gameinfofile}"
+			if grep -Eq "${metamodpattern}" "${gameinfofile}"; then
+				fn_script_log_fail "${logentry}"
+				fn_print_fail_eol_nl
+			else
+				fn_script_log_pass "${logentry}"
+				fn_print_ok_eol_nl
+			fi
+		fi
+	fi
+}
+
 fn_mod_install_amxmodx_file() {
 	# does plugins.ini exist?
 	if [ -f "${modinstalldir}/addons/metamod/plugins.ini" ]; then

+ 74 - 29
lgsm/modules/mods_list.sh

@@ -57,18 +57,37 @@ amxxtsurl="${amxxtsdownloadurl}"
 # Metamod:Source
 metamodsourceversion="1.12"
 metamodsourcescrapeurl="https://mms.alliedmods.net/mmsdrop/${metamodsourceversion}/mmsource-latest-linux"
-metamodsourcelatestfile=$(wget "${metamodsourcescrapeurl}" -q -O -)
+if [ "${engine}" == "source" ]; then
+	metamodsourcelatestfile=$(wget "${metamodsourcescrapeurl}" -q -O -)
+fi
 metamodsourcedownloadurl="https://www.metamodsource.net/latest.php?os=linux&version=${metamodsourceversion}"
 metamodsourceurl="${metamodsourcedownloadurl}"
+# Metamod:Source 2.0
+metamodsource2version="2.0"
+metamodsource2scrapeurl="https://mms.alliedmods.net/mmsdrop/${metamodsource2version}/mmsource-latest-linux"
+if [ "${shortname}" == "cs2" ]; then
+	metamodsource2latestfile=$(wget "${metamodsource2scrapeurl}" -q -O -)
+fi
+metamodsource2downloadurl="https://www.metamodsource.net/latest.php?os=linux&version=${metamodsource2version}"
+metamodsource2url="${metamodsource2downloadurl}"
 # Sourcemod
-sourcemodversion="1.13"
+# Use the compatible stable SourceMod branch for Counter-Strike: Global Offensive.
+if [ "${shortname}" == "csgo" ]; then
+	sourcemodversion="1.12"
+else
+	sourcemodversion="1.13"
+fi
 sourcemodscrapeurl="https://sm.alliedmods.net/smdrop/${sourcemodversion}/sourcemod-latest-linux"
-sourcemodlatestfile=$(wget "${sourcemodscrapeurl}" -q -O -)
+if [ "${engine}" == "source" ]; then
+	sourcemodlatestfile=$(wget "${sourcemodscrapeurl}" -q -O -)
+fi
 sourcemoddownloadurl="https://www.sourcemod.net/latest.php?os=linux&version=${sourcemodversion}"
 sourcemodurl="${sourcemoddownloadurl}"
 # Steamworks
 steamworksscrapeurl="https://users.alliedmods.net/~kyles/builds/SteamWorks"
-steamworkslatestfile=$(curl --connect-timeout 3 -sL ${steamworksscrapeurl} | grep -m 1 linux | cut -d '"' -f 4)
+if [ "${shortname}" == "csgo" ]; then
+	steamworkslatestfile=$(curl --connect-timeout 3 -sL ${steamworksscrapeurl} | grep -m 1 linux | cut -d '"' -f 4)
+fi
 steamworksdownloadurl="${steamworksscrapeurl}/${steamworkslatestfile}"
 steamworksurl="${steamworksdownloadurl}"
 # Stripper:Source
@@ -77,35 +96,54 @@ stripperlatestfile="stripper-${stripperversion}-linux.tar.gz"
 stripperdownloadurl="http://www.bailopan.net/stripper/snapshots/1.2/${stripperlatestfile}"
 stripperurl="${stripperdownloadurl}"
 
+# CS2 Mods
+if [ "${shortname}" == "cs2" ]; then
+	counterstrikesharprelease=$(curl --connect-timeout 3 -sL https://api.github.com/repos/roflmuffin/CounterStrikeSharp/releases/latest)
+	counterstrikesharplastbuild=$(printf '%s\n' "${counterstrikesharprelease}" | jq '.assets[]? | select(.name | startswith("counterstrikesharp-with-runtime-linux-") and endswith(".zip"))' 2> /dev/null)
+	counterstrikesharplatestfile=$(printf '%s\n' "${counterstrikesharplastbuild}" | jq -r '.name')
+	counterstrikesharplatestlink=$(printf '%s\n' "${counterstrikesharplastbuild}" | jq -r '.browser_download_url')
+fi
+
 # CS:GO Mods
-get5lastbuild=$(curl --connect-timeout 3 -sL https://api.github.com/repos/splewis/get5/releases/latest | jq '.assets[] |select(.browser_download_url | endswith(".tar.gz"))')
-get5latestfile=$(echo -e "${get5lastbuild}" | jq -r '.name')
-get5latestfilelink=$(echo -e "${get5lastbuild}" | jq -r '.browser_download_url')
-csgopracticelatest=$(curl --connect-timeout 3 -sL https://api.github.com/repos/splewis/csgo-practice-mode/releases/latest | jq '.assets[]')
-csgopracticelatestfile=$(echo -e "${csgopracticelatest}" | jq -r '.name')
-csgopracticelatestlink=$(echo -e "${csgopracticelatest}" | jq -r '.browser_download_url')
-csgopuglatest=$(curl --connect-timeout 3 -sL https://api.github.com/repos/splewis/csgo-pug-setup/releases/latest | jq '.assets[]')
-csgopuglatestfile=$(echo -e "${csgopuglatest}" | jq -r '.name')
-csgopuglatestlink=$(echo -e "${csgopuglatest}" | jq -r '.browser_download_url')
-gokzlatestversion=$(curl --connect-timeout 3 -s https://api.github.com/repos/KZGlobalTeam/gokz/releases/latest | grep "tag_name" | cut -d : -f 2,3 | sed -E 's/.*"([^"]+)".*/\1/')
-gokzlatestfile="GOKZ-v${gokzlatestversion}.zip"
-gokzlatestlink="https://github.com/KZGlobalTeam/gokz/releases/download/${gokzlatestversion}/${gokzlatestfile}"
-movementapilatestversion=$(curl --connect-timeout 3 -s https://api.github.com/repos/danzayau/MovementAPI/releases/latest | grep "tag_name" | cut -d : -f 2,3 | sed -E 's/.*"([^"]+)".*/\1/')
-movementapilatestfile="MovementAPI-v${movementapilatestversion}.zip"
-movementapilatestlink="https://github.com/danzayau/MovementAPI/releases/download/${movementapilatestversion}/${movementapilatestfile}"
+if [ "${shortname}" == "csgo" ]; then
+	get5lastbuild=$(curl --connect-timeout 3 -sL https://api.github.com/repos/splewis/get5/releases/latest | jq '.assets[] |select(.browser_download_url | endswith(".tar.gz"))')
+	get5latestfile=$(echo -e "${get5lastbuild}" | jq -r '.name')
+	get5latestfilelink=$(echo -e "${get5lastbuild}" | jq -r '.browser_download_url')
+	csgopracticelatest=$(curl --connect-timeout 3 -sL https://api.github.com/repos/splewis/csgo-practice-mode/releases/latest | jq '.assets[]')
+	csgopracticelatestfile=$(echo -e "${csgopracticelatest}" | jq -r '.name')
+	csgopracticelatestlink=$(echo -e "${csgopracticelatest}" | jq -r '.browser_download_url')
+	csgopuglatest=$(curl --connect-timeout 3 -sL https://api.github.com/repos/splewis/csgo-pug-setup/releases/latest | jq '.assets[]')
+	csgopuglatestfile=$(echo -e "${csgopuglatest}" | jq -r '.name')
+	csgopuglatestlink=$(echo -e "${csgopuglatest}" | jq -r '.browser_download_url')
+	gokzlatestversion=$(curl --connect-timeout 3 -s https://api.github.com/repos/KZGlobalTeam/gokz/releases/latest | grep "tag_name" | cut -d : -f 2,3 | sed -E 's/.*"([^"]+)".*/\1/')
+	gokzlatestfile="GOKZ-v${gokzlatestversion}.zip"
+	gokzlatestlink="https://github.com/KZGlobalTeam/gokz/releases/download/${gokzlatestversion}/${gokzlatestfile}"
+	movementapilatestversion=$(curl --connect-timeout 3 -s https://api.github.com/repos/danzayau/MovementAPI/releases/latest | grep "tag_name" | cut -d : -f 2,3 | sed -E 's/.*"([^"]+)".*/\1/')
+	movementapilatestfile="MovementAPI-v${movementapilatestversion}.zip"
+	movementapilatestlink="https://github.com/danzayau/MovementAPI/releases/download/${movementapilatestversion}/${movementapilatestfile}"
+fi
 
 # Rust
 carbonrustapilatestfile="Carbon.Linux.Release.tar.gz"
-carbonrustlatestlink=$(curl --connect-timeout 3 -sL https://api.github.com/repos/CarbonCommunity/Carbon.Core/releases/tags/production_build | jq -r '.assets[]|select(.name == "Carbon.Linux.Release.tar.gz") | .browser_download_url')
+if [ "${shortname}" == "rust" ]; then
+	carbonrustlatestlink=$(curl --connect-timeout 3 -sL https://api.github.com/repos/CarbonCommunity/Carbon.Core/releases/tags/production_build | jq -r '.assets[]|select(.name == "Carbon.Linux.Release.tar.gz") | .browser_download_url')
+fi
 
 # Oxide
-oxiderustlatestlink=$(curl --connect-timeout 3 -sL https://api.github.com/repos/OxideMod/Oxide.Rust/releases/latest | jq -r '.assets[]|select(.browser_download_url | contains("linux")) | .browser_download_url')
-oxidehurtworldlatestlink=$(curl --connect-timeout 3 -sL https://api.github.com/repos/OxideMod/Oxide.Hurtworld/releases/latest | jq -r '.assets[].browser_download_url')
-oxidesdtdlatestlink=$(curl --connect-timeout 3 -sL https://api.github.com/repos/OxideMod/Oxide.SevenDaysToDie/releases/latest | jq -r '.assets[]|select(.browser_download_url | contains("linux")) | .browser_download_url')
-# Valheim Plus
-valheimpluslatestlink=$(curl --connect-timeout 3 -sL https://api.github.com/repos/Grantapher/ValheimPlus/releases/latest | jq -r '.assets[]|select(.browser_download_url | contains("UnixServer.tar.gz")) | .browser_download_url')
-# Valheim BepInEx
-bepinexvhlatestlink=$(curl --connect-timeout 3 -sL "https://thunderstore.io/api/experimental/package/denikson/BepInExPack_Valheim/" -H "accept: application/json" | jq -r '.latest.download_url')
+if [ "${shortname}" == "rust" ]; then
+	oxiderustlatestlink=$(curl --connect-timeout 3 -sL https://api.github.com/repos/OxideMod/Oxide.Rust/releases/latest | jq -r '.assets[]|select(.browser_download_url | contains("linux")) | .browser_download_url')
+elif [ "${shortname}" == "hw" ]; then
+	oxidehurtworldlatestlink=$(curl --connect-timeout 3 -sL https://api.github.com/repos/OxideMod/Oxide.Hurtworld/releases/latest | jq -r '.assets[].browser_download_url')
+elif [ "${shortname}" == "sdtd" ]; then
+	oxidesdtdlatestlink=$(curl --connect-timeout 3 -sL https://api.github.com/repos/OxideMod/Oxide.SevenDaysToDie/releases/latest | jq -r '.assets[]|select(.browser_download_url | contains("linux")) | .browser_download_url')
+fi
+
+if [ "${shortname}" == "vh" ]; then
+	# Valheim Plus
+	valheimpluslatestlink=$(curl --connect-timeout 3 -sL https://api.github.com/repos/Grantapher/ValheimPlus/releases/latest | jq -r '.assets[]|select(.browser_download_url | contains("UnixServer.tar.gz")) | .browser_download_url')
+	# Valheim BepInEx
+	bepinexvhlatestlink=$(curl --connect-timeout 3 -sL "https://thunderstore.io/api/experimental/package/denikson/BepInExPack_Valheim/" -H "accept: application/json" | jq -r '.latest.download_url')
+fi
 
 # Define mods information (required)
 
@@ -122,7 +160,7 @@ modseparator="MOD"
 # [2] 	| "Pretty Name": the common name people use to call the mod that will be displayed to the user
 # [3] 	| "URL": link to the mod archive file; can be a variable previously defined while scraping a URL
 # [4] 	| "filename": the output filename
-# [5]	| "modsubdirs": name of a subfolder within the archive to extract as the install root (use "0" to extract all contents)
+# [5]	| "modsubdirs": name of (or path to) a subfolder within the archive to extract as the install root (use "0" to extract all contents)
 # [6]	| "LowercaseOn/Off": LowercaseOff or LowercaseOn: enable/disable converting extracted files and directories to lowercase (some games require it)
 # [7] 	| "modinstalldir": the directory in which to install the mode (use LGSM dir variables such as ${systemdir})
 # [8]	| "/files/to/keep;", files & directories that should not be overwritten upon update, separated and ended with a semicolon; you can also use "OVERWRITE" value to ignore the value or "NOUPDATE" to disallow updating; for files to keep upon uninstall, see fn_mod_tidy_files_list from mods_core.sh
@@ -157,7 +195,14 @@ mod_info_sourcemod=(MOD "sourcemod" "SourceMod" "${sourcemodurl}" "${sourcemodla
 mod_info_steamworks=(MOD "steamworks" "SteamWorks" "${steamworksurl}" "${steamworkslatestfile}" "0" "LowercaseOff" "${systemdir}" "OVERWRITE" "ENGINES" "Counter-Strike: Global Offensive;" "NOTGAMES" "https://github.com/KyleSanderson/SteamWorks" "Exposing SteamWorks functions to SourcePawn")
 mod_info_stripper=(MOD "stripper" "Stripper Source" "${stripperurl}" "${stripperlatestfile}" "0" "LowercaseOff" "${systemdir}" "addons/stripper/maps;" "ENGINES" "Counter-Strike: Global Offensive;Counter-Strike: Source;Day of Defeat: Source;Half Life: Deathmatch;Half Life 2: Deathmatch;Insurgency;Left 4 Dead;Left 4 Dead 2;Nuclear Dawn;Team Fortress 2;" "NOTGAMES" "http://www.bailopan.net/stripper/" "Add or remove objects from map (requires MetaMod)")
 
+# CS2 Mods
+mod_info_metamodsource2=(MOD "metamodsource2" "Metamod:Source 2.0" "${metamodsource2url}" "${metamodsource2latestfile}" "0" "LowercaseOff" "${systemdir}" "addons/metamod/metaplugins.ini;" "ENGINES" "Counter-Strike 2;" "NOTGAMES" "https://www.metamodsource.net" "Plugins Framework (Dev Build)")
+mod_info_counterstrikesharp=(MOD "counterstrikesharp" "CounterStrikeSharp" "${counterstrikesharplatestlink}" "${counterstrikesharplatestfile}" "0" "LowercaseOff" "${systemdir}" "OVERWRITE" "ENGINES" "Counter-Strike 2;" "NOTGAMES" "https://docs.cssharp.dev/" "Write server plugins in C# (requires Metamod:Source 2.0)")
+
 # CS:GO Mods
+# NoLobbyReservation is pinned to a reviewed commit: it has no releases and is compiled from source on install.
+nolobbyreservationcommit="003ab19fcf5e6eb67d21794218a4dc387d2923ca"
+mod_info_nolobbyreservation=(MOD "nolobbyreservation" "NoLobbyReservation" "https://github.com/eldoradoel/NoLobbyReservation/archive/${nolobbyreservationcommit}.zip" "NoLobbyReservation-${nolobbyreservationcommit}.zip" "NoLobbyReservation-${nolobbyreservationcommit}/csgo" "LowercaseOff" "${systemdir}" "OVERWRITE" "ENGINES" "Counter-Strike: Global Offensive;" "NOTGAMES" "https://github.com/eldoradoel/NoLobbyReservation" "Allows players to connect without a Steam lobby reservation - Required for standalone CS:GO clients to connect (requires SourceMod and MetaMod)")
 mod_info_gokz=(MOD "gokz" "GOKZ" "${gokzlatestlink}" "${gokzlatestfile}" "0" "LowercaseOff" "${systemdir}" "cfg;addons/sourcemod/configs;" "ENGINES" "Counter-Strike: Global Offensive;" "NOTGAMES" "https://github.com/KZGlobalTeam/gokz" "GOKZ ${gokzlatestversion} - Implements the KZ game mode (requires SourceMod and MetaMod)")
 mod_info_ttt=(MOD "ttt" "Trouble in Terrorist Town" "https://csgottt.com/downloads/ttt-latest-dev-${sourcemodversion}.zip" "ttt-latest.zip" "0" "LowercaseOff" "${systemdir}" "cfg;addons/sourcemod/configs;" "ENGINES" "Counter-Strike: Global Offensive;" "NOTGAMES" "https://github.com/Bara/TroubleinTerroristTown" "Implements the TTT game mode (requires SourceMod and MetaMod)")
 mod_info_get5=(MOD "get5" "Get 5" "${get5latestfilelink}" "${get5latestfile}" "0" "LowercaseOff" "${systemdir}" "cfg;addons/sourcemod/configs;" "ENGINES" "Counter-Strike: Global Offensive;" "NOTGAMES" "https://github.com/splewis/get5" "Plugin for competitive matches/scrims (requires SourceMod and MetaMod)")
@@ -211,4 +256,4 @@ mod_info_valheimplus=(MOD "valheimplus" "Valheim PLUS" "${valheimpluslatestlink}
 mod_info_bepinexvh=(MOD "bepinexvh" "BepInEx Valheim" "${bepinexvhlatestlink}" "denikson-BepInExPack_Valheim.zip" "BepInExPack_Valheim" "LowercaseOff" "${systemdir}" "OVERWRITE" "ENGINES" "Valheim;" "NOTGAMES" "https://valheim.thunderstore.io/package/denikson/BepInExPack_Valheim/" "Unity / XNA game patcher and plugin framework")
 
 # REQUIRED: Set all mods info into the global array
-mods_global_array=("${mod_info_metamod[@]}" "${mod_info_base_amxx[@]}" "${mod_info_cs_amxx[@]}" "${mod_info_dod_amxx[@]}" "${mod_info_tfc_amxx[@]}" "${mod_info_ns_amxx[@]}" "${mod_info_ts_amxx[@]}" "${mod_info_metamodsource[@]}" "${mod_info_sourcemod[@]}" "${mod_info_steamworks[@]}" "${mod_info_gokz[@]}" "${mod_info_ttt[@]}" "${mod_info_get5[@]}" "${mod_info_prac[@]}" "${mod_info_pug[@]}" "${mod_info_dhook[@]}" "${mod_info_movement[@]}" "${mod_info_cleaner[@]}" "${mod_info_ulib[@]}" "${mod_info_ulx[@]}" "${mod_info_utime[@]}" "${mod_info_uclip[@]}" "${mod_info_acf[@]}" "${mod_info_acf_missiles[@]}" "${mod_info_acf_sweps[@]}" "${mod_info_advdupe2[@]}" "${mod_info_pac3[@]}" "${mod_info_wiremod[@]}" "${mod_info_wiremodextras[@]}" "${mod_info_darkrp[@]}" "${mod_info_darkrpmodification[@]}" "${mod_info_rustcarbon[@]}" "${mod_info_rustoxide[@]}" "${mod_info_hwoxide[@]}" "${mod_info_sdtdoxide[@]}" "${mod_info_advduplicator[@]}" "${mod_info_trackassemblytool[@]}" "${mod_info_physpropertiesadv[@]}" "${mod_info_controlsystemse2[@]}" "${mod_info_e2pistontiming[@]}" "${mod_info_propcannontool[@]}" "${mod_info_gearassemblytool[@]}" "${mod_info_spinnertool[@]}" "${mod_info_surfacefrictiontool[@]}" "${mod_info_magneticdipole[@]}" "${mod_info_environmentorganizer[@]}" "${mod_info_precision_alignment[@]}" "${mod_info_improved_stacker[@]}" "${mod_info_improved_weight[@]}" "${mod_info_improved_antinoclip[@]}" "${mod_info_laserstool[@]}" "${mod_info_valheimplus[@]}" "${mod_info_bepinexvh[@]}")
+mods_global_array=("${mod_info_metamod[@]}" "${mod_info_base_amxx[@]}" "${mod_info_cs_amxx[@]}" "${mod_info_dod_amxx[@]}" "${mod_info_tfc_amxx[@]}" "${mod_info_ns_amxx[@]}" "${mod_info_ts_amxx[@]}" "${mod_info_metamodsource[@]}" "${mod_info_metamodsource2[@]}" "${mod_info_counterstrikesharp[@]}" "${mod_info_sourcemod[@]}" "${mod_info_nolobbyreservation[@]}" "${mod_info_steamworks[@]}" "${mod_info_gokz[@]}" "${mod_info_ttt[@]}" "${mod_info_get5[@]}" "${mod_info_prac[@]}" "${mod_info_pug[@]}" "${mod_info_dhook[@]}" "${mod_info_movement[@]}" "${mod_info_cleaner[@]}" "${mod_info_ulib[@]}" "${mod_info_ulx[@]}" "${mod_info_utime[@]}" "${mod_info_uclip[@]}" "${mod_info_acf[@]}" "${mod_info_acf_missiles[@]}" "${mod_info_acf_sweps[@]}" "${mod_info_advdupe2[@]}" "${mod_info_pac3[@]}" "${mod_info_wiremod[@]}" "${mod_info_wiremodextras[@]}" "${mod_info_darkrp[@]}" "${mod_info_darkrpmodification[@]}" "${mod_info_rustcarbon[@]}" "${mod_info_rustoxide[@]}" "${mod_info_hwoxide[@]}" "${mod_info_sdtdoxide[@]}" "${mod_info_advduplicator[@]}" "${mod_info_trackassemblytool[@]}" "${mod_info_physpropertiesadv[@]}" "${mod_info_controlsystemse2[@]}" "${mod_info_e2pistontiming[@]}" "${mod_info_propcannontool[@]}" "${mod_info_gearassemblytool[@]}" "${mod_info_spinnertool[@]}" "${mod_info_surfacefrictiontool[@]}" "${mod_info_magneticdipole[@]}" "${mod_info_environmentorganizer[@]}" "${mod_info_precision_alignment[@]}" "${mod_info_improved_stacker[@]}" "${mod_info_improved_weight[@]}" "${mod_info_improved_antinoclip[@]}" "${mod_info_laserstool[@]}" "${mod_info_valheimplus[@]}" "${mod_info_bepinexvh[@]}")

+ 1 - 3
lgsm/modules/query_gsquery.py

@@ -1,5 +1,4 @@
 #! /usr/bin/env python3
-# -*- coding: utf-8 -*-
 # LinuxGSM query_gsquery.py module
 # Author: Daniel Gibbs
 # Contributors: https://linuxgsm.com/contrib
@@ -65,7 +64,6 @@ class GSQuery:
         """Create a query instance from parsed CLI args."""
 
         self.argument = arguments
-        #
         if self.argument.engine in self.sourcequery:
             self.query_prompt_string = b"\xff\xff\xff\xffTSource Engine Query\0"
         elif self.argument.engine in self.idtech2query:
@@ -135,7 +133,7 @@ class GSQuery:
         # Receive.
         try:
             self.response = connection.recv(self.default_buffer_length)
-        except socket.error:
+        except OSError:
             self.fatal_error("Unable to receive", 2)
         connection.close()
         # Response.

+ 307 - 0
lgsm/modules/update_bb.sh

@@ -0,0 +1,307 @@
+#!/bin/bash
+# LinuxGSM update_hyt.sh module
+# Author: Andrew dos Santos
+# Contributors: https://linuxgsm.com/contrib
+# Website: https://linuxgsm.com
+# Description: Handles updating of Hytale servers.
+
+moduleselfname="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")"
+
+fn_update_hyt_downloader_arch() {
+	case "${arch}" in
+		x86_64 | amd64)
+			hytaledownloaderfile="hytale-downloader-linux-amd64"
+			;;
+		aarch64 | arm64)
+			fn_print_failure "Hytale supports arm64 servers, but the official downloader archive currently used by LinuxGSM does not include a Linux arm64 downloader."
+			fn_script_log_fail "Unsupported Hytale downloader architecture: ${arch}"
+			core_exit.sh
+			;;
+		*)
+			fn_print_failure "Unknown or unsupported hytale-downloader architecture: ${arch}"
+			fn_script_log_fail "Unknown or unsupported hytale-downloader architecture: ${arch}"
+			core_exit.sh
+			;;
+	esac
+	hytaledownloader="${serverfiles}/${hytaledownloaderfile}"
+}
+
+fn_update_downloader() {
+	fn_update_hyt_downloader_arch
+	if [ ! -f "${hytaledownloader}" ] || [ "${forceupdate}" == "1" ]; then
+		fn_fetch_file "https://downloader.hytale.com/hytale-downloader.zip" "" "" "" "${tmpdir}" "hytale-downloader.zip" "nochmodx" "norun" "forcedl" "nohash"
+		echo -en "extracting hytale-downloader..."
+		unzip -oq "${tmpdir}/hytale-downloader.zip" "${hytaledownloaderfile}" "QUICKSTART.md" -d "${serverfiles}"
+		exitcode=$?
+		if [ "${exitcode}" -ne 0 ]; then
+			fn_print_fail_eol_nl
+			fn_script_log_fail "Extracting hytale-downloader"
+			core_exit.sh
+		else
+			chmod +x "${hytaledownloader}"
+			fn_print_ok_eol_nl
+			fn_script_log_pass "Extracting hytale-downloader"
+		fi
+	fi
+}
+
+fn_update_hyt_patchline_args() {
+	hytalepatchline="${hytalepatchline:-release}"
+	hytalepatchlineargs=()
+	if [ "${hytalepatchline}" != "release" ]; then
+		hytalepatchlineargs=(-patchline "${hytalepatchline}")
+	fi
+}
+
+# The downloader needs a Hytale account login (OAuth device code) the first time it runs, and again
+# if the saved credentials stop working. It waits for the login with no time limit, so it must
+# never run unattended without credentials, otherwise cron jobs hang.
+# Install always shows the login and waits: someone is watching, either in a terminal or in the
+# container logs (e.g. docker logs) during the first install.
+fn_update_hyt_auth() {
+	hytalecredentials="${serverfiles}/.hytale-downloader-credentials.json"
+	if [ "${firstcommandname}" == "INSTALL" ] || { [ -t 0 ] && [ -t 1 ]; }; then
+		hytaledownloadertimeout="900"
+		if [ ! -f "${hytalecredentials}" ]; then
+			echo -e ""
+			echo -e "${bold}${lightyellow}Hytale account login required${default}"
+			echo -e "The Hytale downloader needs you to log in with a Hytale account that owns the game."
+			echo -e "Open the URL shown below in a browser and approve the code within 15 minutes. This is only needed once."
+			echo -e "Running in Docker? The URL is also shown in the container logs (docker logs)."
+			echo -e ""
+			fn_script_log_info "Hytale downloader login required"
+		fi
+	else
+		hytaledownloadertimeout="300"
+		if [ ! -f "${hytalecredentials}" ]; then
+			fn_print_failure_nl "Hytale downloader is not logged in"
+			echo -e "* Run ./${selfname} update in a terminal and approve the Hytale account login once."
+			fn_script_log_fail "Hytale downloader is not logged in: ${hytalecredentials} not found"
+			core_exit.sh
+		fi
+	fi
+}
+
+# Report a downloader timeout, which usually means it was waiting for a login.
+fn_update_hyt_timeout_check() {
+	if [ "${exitcode}" -eq 124 ]; then
+		fn_print_failure_nl "Hytale downloader timed out, it is probably waiting for a Hytale account login"
+		echo -e "* The saved credentials may have expired. Run ./${selfname} update in a terminal to log in again."
+		fn_script_log_fail "Hytale downloader timed out after ${hytaledownloadertimeout}s"
+		core_exit.sh
+	fi
+}
+
+fn_update_extract() {
+	extractdir="${tmpdir}/hytale-game"
+	rm -rf "${extractdir:?}"
+	mkdir -p "${extractdir}"
+	echo -en "extracting ${remotebuildfilename}..."
+	unzip -oq "${tmpdir}/${remotebuildfilename}" -d "${extractdir}"
+	exitcode=$?
+	if [ "${exitcode}" -ne 0 ]; then
+		fn_print_fail_eol_nl
+		fn_script_log_fail "Extracting ${remotebuildfilename}"
+		core_exit.sh
+	fi
+
+	if [ -d "${extractdir}/game/Server" ]; then
+		extractedroot="${extractdir}/game"
+	else
+		extractedroot="${extractdir}"
+	fi
+
+	mkdir -p "${serverfiles}"
+	if [ "${firstcommandname}" == "INSTALL" ]; then
+		cp -a "${extractedroot}/." "${serverfiles}/"
+	else
+		for item in "${extractedroot}"/*; do
+			[ -e "${item}" ] || continue
+			itemname="$(basename "${item}")"
+			case "${itemname}" in
+				Server | .hytale-downloader-credentials.json)
+					continue
+					;;
+				jvm.options)
+					if [ -f "${serverfiles}/jvm.options" ]; then
+						continue
+					fi
+					;;
+			esac
+			cp -a "${item}" "${serverfiles}/"
+		done
+		mkdir -p "${serverfiles}/Server"
+		for item in "${extractedroot}/Server"/*; do
+			[ -e "${item}" ] || continue
+			itemname="$(basename "${item}")"
+			case "${itemname}" in
+				backups | bans.json | config.json | logs | mods | permissions.json | universe | whitelist.json)
+					continue
+					;;
+			esac
+			cp -a "${item}" "${serverfiles}/Server/"
+		done
+	fi
+	[ -f "${serverfiles}/start.sh" ] && chmod +x "${serverfiles}/start.sh"
+	fn_print_ok_eol_nl
+	fn_script_log_pass "Extracting ${remotebuildfilename}"
+	rm -rf "${extractdir:?}"
+}
+
+fn_update_dl() {
+	fn_update_downloader
+	fn_update_hyt_patchline_args
+	remotebuildfilename="hytale-server-${remotebuild:-latest}.zip"
+	echo -e "downloading file [ ${italic}${remotebuildfilename}${default} ]"
+	fn_sleep_time
+	cd "${serverfiles}" || exit
+	fn_update_hyt_auth
+	timeout --foreground "${hytaledownloadertimeout}" "${hytaledownloader}" -download-path "${tmpdir}/${remotebuildfilename}" "${hytalepatchlineargs[@]}" -skip-update-check
+	exitcode=$?
+	fn_update_hyt_timeout_check
+	if [ "${exitcode}" -ne 0 ]; then
+		fn_print_failure_nl "Downloading ${remotebuildfilename}"
+		fn_script_log_fail "Downloading ${remotebuildfilename}"
+		core_exit.sh
+	fi
+	fn_update_extract
+	if [ -n "${remotebuild}" ]; then
+		echo "${remotebuild}" > "${localbuildfile}"
+	fi
+	fn_clear_tmp
+}
+
+fn_update_hyt_check_localbuild_tracking() {
+	if [ -f "${localbuildfile}" ] && [ -f "${serverfiles}/Server/HytaleServer.jar" ] && [ "${serverfiles}/Server/HytaleServer.jar" -nt "${localbuildfile}" ]; then
+		fn_print_warn_nl "Hytale server files are newer than LinuxGSM build tracking"
+		echo -e "* Hytale may have updated itself outside LinuxGSM."
+		echo -e "* Run ./${selfname} update to refresh LinuxGSM version tracking."
+		fn_script_log_warn "Hytale server files are newer than LinuxGSM build tracking"
+	fi
+}
+
+fn_update_localbuild() {
+	fn_print_dots "Checking local build: ${remotelocation}"
+	if [ -f "${localbuildfile}" ]; then
+		localbuild="$(cat "${localbuildfile}")"
+	fi
+	if [ -z "${localbuild}" ]; then
+		fn_print_error "Checking local build: ${remotelocation}: missing local build info"
+		fn_script_log_error "Missing local build info"
+		fn_script_log_error "Set localbuild to 0"
+		localbuild="0"
+	else
+		fn_print_ok "Checking local build: ${remotelocation}"
+		fn_script_log_pass "Checking local build"
+		fn_update_hyt_check_localbuild_tracking
+	fi
+}
+
+fn_update_remotebuild() {
+	fn_update_downloader
+	fn_update_hyt_patchline_args
+	cd "${serverfiles}" || exit
+	remotebuildresponsefile="${tmpdir}/hytale-print-version-response.txt"
+	rm -f "${remotebuildresponsefile}"
+	fn_update_hyt_auth
+	timeout --foreground "${hytaledownloadertimeout}" "${hytaledownloader}" -print-version "${hytalepatchlineargs[@]}" -skip-update-check 2>&1 | tee "${remotebuildresponsefile}"
+	exitcode=${PIPESTATUS[0]}
+	fn_update_hyt_timeout_check
+	remotebuildresponse="$(cat "${remotebuildresponsefile}")"
+	if [ -f "${lgsmlog}" ]; then
+		cat "${remotebuildresponsefile}" >> "${lgsmlog}"
+	fi
+	remotebuild=$(echo "${remotebuildresponse}" | grep -Eo "[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[A-Za-z0-9]+" | tail -1)
+	if [ -z "${remotebuild}" ]; then
+		remotebuild=$(echo "${remotebuildresponse}" | tail -1)
+	fi
+
+	if [ "${firstcommandname}" != "INSTALL" ]; then
+		fn_print_dots "Checking remote build: ${remotelocation}"
+		if [ "${exitcode}" -ne 0 ] || [ -z "${remotebuild}" ] || [ "${remotebuild}" == "null" ]; then
+			fn_print_fail "Checking remote build: ${remotelocation}"
+			fn_script_log_fail "Checking remote build"
+			core_exit.sh
+		else
+			fn_print_ok "Checking remote build: ${remotelocation}"
+			fn_script_log_pass "Checking remote build"
+		fi
+	else
+		if [ "${exitcode}" -ne 0 ] || [ -z "${remotebuild}" ] || [ "${remotebuild}" == "null" ]; then
+			fn_print_failure "Unable to get remote build"
+			fn_script_log_fail "Unable to get remote build"
+			core_exit.sh
+		fi
+	fi
+}
+
+fn_update_compare() {
+	fn_print_dots "Checking for update: ${remotelocation}"
+	if [ "${localbuild}" != "${remotebuild}" ] || [ "${forceupdate}" == "1" ]; then
+		date '+%s' > "${lockdir:?}/update.lock"
+		fn_print_ok_nl "Checking for update: ${remotelocation}"
+		fn_print "\n"
+		fn_print_nl "${bold}${underline}Update${default} available"
+		fn_print_nl "* Local build: ${red}${localbuild}${default}"
+		fn_print_nl "* Remote build: ${green}${remotebuild}${default}"
+		fn_print_nl "* Patchline: ${hytalepatchline}"
+		fn_print "\n"
+		fn_script_log_info "Update available"
+		fn_script_log_info "Local build: ${localbuild}"
+		fn_script_log_info "Remote build: ${remotebuild}"
+		fn_script_log_info "Patchline: ${hytalepatchline}"
+
+		if [ "${commandname}" == "UPDATE" ]; then
+			date +%s > "${lockdir:?}/last-updated.lock"
+			unset updateonstart
+			check_status.sh
+			if [ "${status}" == "0" ]; then
+				fn_update_dl
+			else
+				fn_print_restart_warning
+				exitbypass=1
+				command_stop.sh
+				fn_firstcommand_reset
+				exitbypass=1
+				fn_update_dl
+				exitbypass=1
+				command_start.sh
+				fn_firstcommand_reset
+			fi
+			unset exitbypass
+			alert="update"
+		elif [ "${commandname}" == "CHECK-UPDATE" ]; then
+			alert="check-update"
+		fi
+		alert.sh
+	else
+		fn_print_ok_nl "Checking for update: ${remotelocation}"
+		fn_print "\n"
+		fn_print_nl "${bold}${underline}No update${default} available"
+		fn_print_nl "* Local build: ${green}${localbuild}${default}"
+		fn_print_nl "* Remote build: ${green}${remotebuild}${default}"
+		fn_print_nl "* Patchline: ${hytalepatchline}"
+		fn_print "\n"
+		fn_script_log_info "No update available"
+		fn_script_log_info "Local build: ${localbuild}"
+		fn_script_log_info "Remote build: ${remotebuild}"
+		fn_script_log_info "Patchline: ${hytalepatchline}"
+	fi
+}
+
+info_distro.sh
+
+localbuildfile="${datadir}/${selfname}-hytale-version"
+remotelocation="hytale.com"
+
+if [ "${firstcommandname}" == "INSTALL" ]; then
+	fn_update_remotebuild
+	fn_update_dl
+else
+	fn_print_dots "Checking for update"
+	fn_print_dots "Checking for update: ${remotelocation}"
+	fn_script_log_info "Checking for update: ${remotelocation}"
+	fn_update_localbuild
+	fn_update_remotebuild
+	fn_update_compare
+fi

+ 34 - 14
linuxgsm.sh

@@ -24,7 +24,7 @@ if [ -f ".dev-debug" ]; then
 	set -x
 fi
 
-version="v26.2.0"
+version="v26.3.0"
 shortname="core"
 gameservername="core"
 commandname="CORE"
@@ -70,17 +70,32 @@ core_modules.sh() {
 }
 
 # Bootstrap
+# Exits during bootstrap. core_exit.sh is only usable once core_modules.sh (which defines it)
+# and core_dl.sh (fn_fetch_module, which it uses) are loaded, so fall back to a plain exit.
+fn_bootstrap_exit() {
+	if type core_exit.sh > /dev/null 2>&1 && type fn_fetch_module > /dev/null 2>&1; then
+		core_exit.sh
+	else
+		exit "${1:-1}"
+	fi
+}
+
 # Fetches the core modules required before passed off to core_dl.sh.
 fn_bootstrap_fetch_trap() {
 	echo -e ""
-	echo -en "downloading ${local_filename}"
-	fn_print_canceled_eol_nl
-	fn_script_log_info "Downloading ${local_filename}...CANCELED"
 	rm -f "${local_filedir:?}/${local_filename}"
-	echo -en "downloading ${local_filename}"
-	fn_print_removed_eol_nl
-	fn_script_log_info "Downloading ${local_filename}...REMOVED"
-	core_exit.sh
+	# Message and logging functions come from core_messages.sh, which may not be loaded yet.
+	if type fn_print_canceled_eol_nl > /dev/null 2>&1; then
+		echo -en "downloading ${local_filename}"
+		fn_print_canceled_eol_nl
+		echo -en "downloading ${local_filename}"
+		fn_print_removed_eol_nl
+		fn_script_log_info "Downloading ${local_filename}...CANCELED"
+		fn_script_log_info "Downloading ${local_filename}...REMOVED"
+	else
+		echo -e "downloading ${local_filename} ... CANCELED (removed)"
+	fi
+	fn_bootstrap_exit 130
 }
 
 # Fetches modules from the Git repo during first download.
@@ -175,14 +190,15 @@ fn_bootstrap_fetch_file() {
 			if [ "${exitcode}" -ne 0 ]; then
 				if [ ${counter} -ge 2 ]; then
 					echo -e " ... FAIL"
-					if [ -f "${lgsmlog}" ]; then
+					# Logging functions come from core_messages.sh, which may not be loaded yet during bootstrap.
+					if [ -f "${lgsmlog}" ] && type fn_script_log_fail > /dev/null 2>&1; then
 						fn_script_log_fail "Downloading ${local_filename}..."
 						fn_script_log_fail "${fileurl}"
 					fi
-					core_exit.sh
+					fn_bootstrap_exit 1
 				else
 					echo -e " ... ERROR"
-					if [ -f "${lgsmlog}" ]; then
+					if [ -f "${lgsmlog}" ] && type fn_script_log_error > /dev/null 2>&1; then
 						fn_script_log_error "Downloading ${local_filename}..."
 						fn_script_log_error "${fileurl}"
 					fi
@@ -191,7 +207,7 @@ fn_bootstrap_fetch_file() {
 				echo -en " ... OK"
 				sleep "0.1"
 				echo -e "\033\\r"
-				if [ -f "${lgsmlog}" ]; then
+				if [ -f "${lgsmlog}" ] && type fn_script_log_pass > /dev/null 2>&1; then
 					fn_script_log_pass "Downloading ${local_filename}..."
 				fi
 
@@ -243,13 +259,17 @@ fn_bootstrap_fetch_file_github() {
 # Installer menu.
 
 fn_print_center() {
-	columns=$(tput cols)
+	# Fall back to 80 columns when there is no terminal.
+	columns="${COLUMNS:-$(tput cols 2> /dev/null)}"
+	columns="${columns:-80}"
 	line="$*"
 	printf "%*s\n" $(((${#line} + columns) / 2)) "${line}"
 }
 
 fn_print_horizontal() {
-	printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' "="
+	# Fall back to 80 columns when there is no terminal; bash 5.3+ rejects an empty printf width.
+	local columns="${COLUMNS:-$(tput cols 2> /dev/null)}"
+	printf '%*s\n' "${columns:-80}" '' | tr ' ' "="
 }
 
 # Bash menu.