Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/counterstrikesharp/scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ if [ "$SHOW_ELO_RANKS" = "true" ] || [ "$DISABLE_SERVER_GUIDELINES" = "true" ];
sed -i --follow-symlinks 's/"FollowCS2ServerGuidelines"[[:space:]]*:[[:space:]]*true/"FollowCS2ServerGuidelines": false/' "$core_json"
fi

# Workshop addons are served by AddonsManager, which is a SwiftlyS2 plugin and
# only ships in that image. Said out loud so a missing model is not a mystery.
if [ -n "${WORKSHOP_ADDONS:-}" ]; then
echo "---Workshop Addons: ${WORKSHOP_ADDONS} need AddonsManager, which only the SwiftlyS2 image ships; players will not get them---"
fi

echo "---Check Metamod Install---"
gameinfo_path="${INSTANCE_SERVER_DIR}/game/csgo/gameinfo.gi"
new_line=" Game csgo/addons/metamod"
Expand Down
5 changes: 3 additions & 2 deletions apps/swiftly/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ ENV SOURCEMOD_CSGO_URL=https://sm.alliedmods.net/smdrop/1.13/sourcemod-1.13.0-gi
ENV NO_LOBBY_RESERVATION_URL=https://github.com/nuxencs/NoLobbyReservation/releases/download/v0.0.1/NoLobbyReservation.zip

# The engine natively mounts exactly one addon -- the map -- so the HUD workshop
# item needs this to reach clients at all. Practice servers only; see setup.sh.
# Blank disables the mount: the plugin degrades to centre text without it.
# item needs AddonsManager to reach clients at all, as do the addons registry
# plugins list in workshop_addons; see setup.sh. Blank disables the HUD's mount:
# the practice plugin degrades to centre text without it.
ENV HUD_WORKSHOP_ID="3791548068"

# ARG, not ENV: the download below happens once at build time, so these are set
Expand Down
55 changes: 16 additions & 39 deletions apps/swiftly/scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -206,15 +206,9 @@ if $INSTALL_UTILITY_PRACTICE_PLUGIN = true ; then
echo "---Utility Practice: plugin dir already present, skipping /opt/utility-practice symlink---"
fi

# The HUD's Panorama layouts live in a workshop addon, and CS2 has no
# server-to-client file transfer -- AddonsManager names the id to connecting
# clients and Steam delivers it. Without this the plugin still works, falling
# back to centre text, so a failure here must not stop the server.
#
# AddonsManager reads its config with optional:false and validates it on
# start, so anything that is not a bare numeric id writes JSON that takes the
# plugin down at load instead of degrading -- a pasted quote, a trailing
# comma, a second id. That is a typo, not a reason to lose the server.
# The HUD's Panorama layouts live in a workshop addon, served with the
# plugins' below. Without it the plugin still works, falling back to centre
# text, so a bad id here must not stop the server.
case "${HUD_WORKSHOP_ID}" in
"")
echo "---Utility Practice: HUD_WORKSHOP_ID unset, HUD falls back to centre text---"
Expand All @@ -223,40 +217,23 @@ if $INSTALL_UTILITY_PRACTICE_PLUGIN = true ; then
echo "---Utility Practice: HUD_WORKSHOP_ID '${HUD_WORKSHOP_ID}' is not a workshop id, HUD falls back to centre text---"
;;
*)
ADDONS_MANAGER_PLUGIN_DIR="${INSTANCE_SERVER_DIR}/game/csgo/addons/swiftlys2/plugins/AddonsManager"
if [ ! -e "$ADDONS_MANAGER_PLUGIN_DIR" ]; then
ln -s "/opt/addons-manager/AddonsManager" "$ADDONS_MANAGER_PLUGIN_DIR"
fi

# Rewritten every boot: the id is ours, and a stale copy from an older
# image would silently serve the wrong addon.
#
# RedownloadAddonOnMount matters because this addon changes -- unlike a
# map, which is published once. AddonsManager only checks that an item is
# installed, not that it is current, so without this a server that cached
# an older HUD keeps serving those layouts forever and never picks up a
# republish.
#
# Through materialize_for_write because configs is a symlink onto the
# node-wide volume: written straight through it, one practice server's
# addon id lands in front of every other server on the node -- and
# AddonsManager watches the file, so a running server swaps to it live.
ADDONS_MANAGER_CONFIG="$(materialize_for_write "${INSTANCE_SERVER_DIR}/game/csgo" "addons/swiftlys2/configs/plugins/AddonsManager/config.jsonc")"
cat > "$ADDONS_MANAGER_CONFIG" <<EOF
{
"Main": {
"Addons": [
"${HUD_WORKSHOP_ID}"
],
"RedownloadAddonOnMount": true
}
}
EOF
echo "---Utility Practice: HUD addon ${HUD_WORKSHOP_ID} via AddonsManager---"
PRACTICE_HUD_ADDON="${HUD_WORKSHOP_ID}"
echo "---Utility Practice: HUD addon ${HUD_WORKSHOP_ID}---"
;;
esac
fi

# The practice HUD's addon, and every addon the plugins this server loads list
# in the catalog -- the panel sends those as WORKSHOP_ADDONS. AddonsManager only
# runs when there is something to serve, so a server whose plugins ship no
# addons never makes a player download anything.
WORKSHOP_ADDON_IDS="$(workshop_addon_ids "${PRACTICE_HUD_ADDON:-}" "${WORKSHOP_ADDONS:-}")"
if [ -n "$WORKSHOP_ADDON_IDS" ]; then
# Bare numeric ids, one per line, so splitting them is safe.
# shellcheck disable=SC2086
enable_addons_manager "${INSTANCE_SERVER_DIR}/game/csgo" $WORKSHOP_ADDON_IDS
fi

# A community server's sanctions: it runs no match plugin, so without this a
# mute or gag set in the panel never reaches it.
if $INSTALL_PLAYER_MANAGEMENT_PLUGIN = true ; then
Expand Down
55 changes: 55 additions & 0 deletions shared/scripts/test/util.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,61 @@ assert_equals "$(grep -A2 'ConVars' "$(instance_branchspecific)" | grep -c '"net
"net_p2p_listen_dedicated was not added"
teardown

addons_manager_config() {
printf '%s' "$workdir/instance/game/csgo/addons/swiftlys2/configs/plugins/AddonsManager/config.jsonc"
}

echo "workshop_addon_ids keeps each workshop id once, in the order given"
assert_equals "$(workshop_addon_ids "3791548068" "100,3791548068,,200" "" 2>/dev/null | tr '\n' ' ')" \
"3791548068 100 200 " "the ids were not deduplicated in order"

# AddonsManager validates its config on start and drops all of it over one bad
# entry, so nothing that is not a bare number may reach it.
echo "workshop_addon_ids drops anything that is not a bare workshop id"
assert_equals "$(workshop_addon_ids '100,"200",3 4,*,https://x/?id=5' 2>/dev/null | tr '\n' ' ')" \
"100 " "a malformed id was passed through"
case "$(workshop_addon_ids '"200"' 2>&1 >/dev/null)" in
*"'\"200\"' is not a workshop id"*) ;;
*) fail "a malformed id was dropped without saying so" ;;
esac

echo "enable_addons_manager links AddonsManager and serves exactly the ids given"
setup
mkdir -p "$workdir/addons-manager/AddonsManager"
ADDONS_MANAGER_DIR="$workdir/addons-manager/AddonsManager" \
NODE_PLUGINS_DIR="$workdir/no-node-plugins" CUSTOM_PLUGINS_DIR="$workdir/plugins" \
enable_addons_manager "$workdir/instance/game/csgo" 100 200 > /dev/null 2>&1
assert_equals "$(readlink "$workdir/instance/game/csgo/addons/swiftlys2/plugins/AddonsManager")" \
"$workdir/addons-manager/AddonsManager" "AddonsManager was not linked into the server"
assert_equals "$(jq -c '.Main' "$(addons_manager_config)" 2>/dev/null)" \
'{"Addons":["100","200"],"RedownloadAddonOnMount":true}' "the config does not serve the ids given"
assert_missing "$workdir/plugins/addons/swiftlys2/configs/plugins/AddonsManager" \
"one server's addons were written onto the node volume"
teardown

echo "enable_addons_manager replaces what an earlier boot served"
setup
mkdir -p "$workdir/addons-manager/AddonsManager"
ADDONS_MANAGER_DIR="$workdir/addons-manager/AddonsManager" \
NODE_PLUGINS_DIR="$workdir/no-node-plugins" CUSTOM_PLUGINS_DIR="$workdir/plugins" \
enable_addons_manager "$workdir/instance/game/csgo" 100 200 > /dev/null 2>&1
ADDONS_MANAGER_DIR="$workdir/addons-manager/AddonsManager" \
NODE_PLUGINS_DIR="$workdir/no-node-plugins" CUSTOM_PLUGINS_DIR="$workdir/plugins" \
enable_addons_manager "$workdir/instance/game/csgo" 300 > /dev/null 2>&1
assert_equals "$(jq -c '.Main.Addons' "$(addons_manager_config)" 2>/dev/null)" '["300"]' \
"an earlier boot's addons were still served"
teardown

echo "enable_addons_manager leaves the server alone in an image without AddonsManager"
setup
ADDONS_MANAGER_DIR="$workdir/no-addons-manager" \
enable_addons_manager "$workdir/instance/game/csgo" 100 > /dev/null 2>&1
assert_equals "$?" "0" "a missing AddonsManager should not abort setup"
assert_missing "$workdir/instance/game/csgo/addons/swiftlys2/plugins/AddonsManager" \
"a link to a missing AddonsManager was planted"
assert_missing "$(addons_manager_config)" "a config was written for a plugin that is not there"
teardown

if [ "$failures" -gt 0 ]; then
echo "$failures assertion(s) failed" >&2
exit 1
Expand Down
93 changes: 93 additions & 0 deletions shared/scripts/util.sh
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,99 @@ write_plugin_configs() {
done <<< "$paths"
}

# CS2 has no server-to-client file transfer, so anything a plugin shows or plays
# that the game does not ship -- models, sounds, Panorama layouts -- reaches
# players as a workshop addon, which AddonsManager names to each connecting
# client for Steam to deliver.
#
# Prints each workshop id in its arguments once, in order, one per line. An
# argument may be a comma-separated list. AddonsManager reads its config with
# optional:false and validates it on start, so one entry that is not a bare
# numeric id -- a pasted quote, a URL, a trailing comma -- takes it down along
# with every addon it serves. Those are dropped with a warning instead.
workshop_addon_ids() {
local seen="," list id
local -a ids

for list in "$@"; do
if [ -z "$list" ]; then
continue
fi

IFS="," read -r -a ids <<< "$list"

for id in "${ids[@]}"; do
case "$id" in
"")
continue
;;
*[!0-9]*)
echo "---Workshop Addons: '${id}' is not a workshop id, skipping it---" >&2
continue
;;
esac

case "$seen" in
*",$id,"*) continue ;;
esac

seen="${seen}${id},"
printf '%s\n' "$id"
done
done
}

# Links the AddonsManager the SwiftlyS2 image ships into this server and has it
# serve exactly these workshop ids, which must already be bare numbers.
#
# The config is rewritten every boot: the ids belong to this server, and a
# stale copy from an older boot would silently serve the wrong addons.
#
# RedownloadAddonOnMount because addons change -- unlike a map, which is
# published once. AddonsManager only checks that an item is installed, not that
# it is current, so without it a server that cached an older copy serves it
# forever and never picks up a republish.
#
# Through materialize_for_write because configs is a symlink onto the node-wide
# volume: written straight through it, one server's addons land in front of
# every other server on the node -- and AddonsManager watches the file, so a
# running server swaps to them live.
enable_addons_manager() {
local root="$1"
shift

local source="${ADDONS_MANAGER_DIR:-/opt/addons-manager/AddonsManager}"

if [ ! -d "$source" ]; then
echo "---Workshop Addons: this image has no AddonsManager, players will not get $*---" >&2
return 0
fi

local plugin_dir="$root/addons/swiftlys2/plugins/AddonsManager"

if [ ! -e "$plugin_dir" ]; then
mkdir -p "$(dirname "$plugin_dir")"
ln -s "$source" "$plugin_dir"
fi

local config
config="$(materialize_for_write "$root" "addons/swiftlys2/configs/plugins/AddonsManager/config.jsonc")"

# A link here is a copy shared with other servers; replace it, never write
# through it.
if [ -L "$config" ]; then
rm -f "$config"
fi

if ! jq -n '{Main: {Addons: $ARGS.positional, RedownloadAddonOnMount: true}}' \
--args "$@" > "$config"; then
echo "---Workshop Addons: could not write ${config}---" >&2
return 0
fi

echo "---Workshop Addons: serving $* via AddonsManager---"
}

# Valve keeps the server's SteamAppId 730 in this file, so the relay settings are
# added to it rather than replacing it. The instance copy is a symlink onto the
# node's game files, so the result is moved over the link, never written through.
Expand Down
Loading