603 lines
20 KiB
Bash
Executable File
603 lines
20 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# terractl - helper for terraria@.service
|
|
#
|
|
# Layout:
|
|
# /var/lib/terraria/dist/ server zips (read-only source)
|
|
# terraria-server-*.zip vanilla dedicated server zips from terraria.org
|
|
# tModLoader*.zip tModLoader release zips (GitHub)
|
|
# /var/lib/terraria/instances/<instance>/
|
|
# instance.conf TYPE (vanilla|tmodloader), ZIP pin, EXTRA_ARGS
|
|
# serverconfig.txt normal Terraria server config (port, world, ...)
|
|
# server/ unpacked server binaries (replaceable)
|
|
# data/ worlds, mods, player data (kept across reinstalls)
|
|
set -euo pipefail
|
|
|
|
# All paths used are absolute. Start from / so nothing depends on the caller's
|
|
# directory (which the 'terraria' user may not be allowed into, e.g. /root).
|
|
cd /
|
|
|
|
# Local overrides (see the comments in that file)
|
|
# shellcheck disable=SC1091
|
|
if [[ -r /etc/default/terractl ]]; then . /etc/default/terractl; fi
|
|
|
|
BASE_DIR="${TERRARIA_BASE:-/var/lib/terraria/instances}"
|
|
DIST_DIR="${TERRARIA_DIST:-/var/lib/terraria/dist}"
|
|
RUN_DIR="/run/terraria"
|
|
SVC_USER="${TERRARIA_USER:-terraria}"
|
|
STOP_TIMEOUT="${TERRARIA_STOP_TIMEOUT:-80}" # keep below TimeoutStopSec
|
|
TERRACTL_VERSION="@VERSION@" # filled in by 'make install' / 'make deb'
|
|
|
|
die() { echo "terractl: $*" >&2; exit 1; }
|
|
log() { echo "terractl: $*" >&2; }
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: terractl <command> <instance> [args]
|
|
terractl --version
|
|
|
|
Admin commands:
|
|
new <name> [vanilla|tmodloader] [port] create an instance (run as root)
|
|
list show instances, type, port, state
|
|
attach <name> interactive console (Ctrl-D to detach)
|
|
cmd <name> <console command...> send a console command (e.g. save, say hi)
|
|
reinstall <name> [--clean] re-unpack server on next start
|
|
(--clean also re-downloads tModLoader's .NET)
|
|
mods <name> [list] show installed mods and whether they're enabled
|
|
mods <name> add <file.tmod|folder>... install mods (and ModConfigs) and enable them
|
|
mods <name> enable|disable <mod>... change which installed mods load
|
|
mods <name> remove <mod>... uninstall mods
|
|
delete <name> [-y] [--no-backup] stop, disable and remove an instance
|
|
(worlds/configs archived to $BASE_DIR/.deleted/)
|
|
|
|
Used by terraria@.service:
|
|
prepare <name> create dirs/configs, unpack server if needed
|
|
run <name> exec the server
|
|
stop <name> <mainpid> graceful shutdown via console "exit"
|
|
EOF
|
|
exit "${1:-1}"
|
|
}
|
|
|
|
valid_name() {
|
|
# Must not start with '.', so '.', '..' and hidden dirs can never be targeted
|
|
[[ ${1:-} =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]] || die "invalid instance name '${1:-}'"
|
|
}
|
|
|
|
inst_dir() { echo "$BASE_DIR/$1"; }
|
|
|
|
load_conf() {
|
|
local dir; dir=$(inst_dir "$1")
|
|
TYPE=vanilla
|
|
ZIP=""
|
|
EXTRA_ARGS=()
|
|
# shellcheck disable=SC1091
|
|
[[ -f $dir/instance.conf ]] && source "$dir/instance.conf"
|
|
case $TYPE in
|
|
vanilla|tmodloader) ;;
|
|
*) die "TYPE in $dir/instance.conf must be 'vanilla' or 'tmodloader' (got '$TYPE')" ;;
|
|
esac
|
|
}
|
|
|
|
write_instance_conf() {
|
|
local name=$1 type=$2 dir; dir=$(inst_dir "$name")
|
|
cat > "$dir/instance.conf" <<EOF
|
|
# Settings for terraria@$name
|
|
# Changing TYPE or ZIP makes the next start re-unpack server/ (data/ is kept).
|
|
|
|
TYPE=$type # vanilla | tmodloader
|
|
|
|
# Pin a specific zip from $DIST_DIR (filename or absolute path).
|
|
# Blank = newest matching zip at install time; it is NOT auto-upgraded later.
|
|
ZIP=
|
|
|
|
# Extra arguments passed to the server binary, as a bash array, e.g.
|
|
# EXTRA_ARGS=(-disableannouncementbox)
|
|
EXTRA_ARGS=()
|
|
EOF
|
|
}
|
|
|
|
next_free_port() {
|
|
local max=7776 p f
|
|
for f in "$BASE_DIR"/*/serverconfig.txt; do
|
|
[[ -f $f ]] || continue
|
|
p=$(sed -n 's/^port=\([0-9][0-9]*\).*/\1/p' "$f" | tail -n1)
|
|
if [[ -n $p && $p -gt $max ]]; then max=$p; fi
|
|
done
|
|
echo $((max + 1))
|
|
}
|
|
|
|
write_server_config() {
|
|
local name=$1 port=${2:-} dir; dir=$(inst_dir "$name")
|
|
[[ -n $port ]] || port=$(next_free_port)
|
|
cat > "$dir/serverconfig.txt" <<EOF
|
|
# Terraria server config for instance '$name'
|
|
# Edit freely, then: systemctl restart terraria@$name
|
|
|
|
world=$dir/data/Worlds/$name.wld
|
|
worldpath=$dir/data/Worlds
|
|
# Created automatically if the world file does not exist. 1=small 2=medium 3=large
|
|
autocreate=2
|
|
worldname=$name
|
|
# 0=classic 1=expert 2=master 3=journey
|
|
difficulty=0
|
|
#seed=
|
|
|
|
port=$port
|
|
maxplayers=8
|
|
password=
|
|
motd=Welcome to $name
|
|
secure=1
|
|
upnp=0
|
|
language=en-US
|
|
banlist=$dir/data/banlist.txt
|
|
EOF
|
|
log "created $dir/serverconfig.txt (port $port)"
|
|
}
|
|
|
|
pick_zip() {
|
|
local pattern f
|
|
if [[ -n $ZIP ]]; then
|
|
if [[ $ZIP == /* ]]; then echo "$ZIP"; else echo "$DIST_DIR/$ZIP"; fi
|
|
return
|
|
fi
|
|
case $TYPE in
|
|
vanilla) pattern='terraria-server-*.zip' ;;
|
|
tmodloader) pattern='tModLoader*.zip' ;;
|
|
esac
|
|
f=$(find "$DIST_DIR" -maxdepth 1 -type f -name "$pattern" -printf '%f\n' | sort -V | tail -n1)
|
|
[[ -n $f ]] || die "no '$pattern' found in $DIST_DIR"
|
|
echo "$DIST_DIR/$f"
|
|
}
|
|
|
|
# Global (not local) so the EXIT trap can still see them after functions return
|
|
EXTRACT_TMP=""
|
|
ATTACH_PID=""
|
|
cleanup() {
|
|
if [[ -n $EXTRACT_TMP && -d $EXTRACT_TMP ]]; then rm -rf "$EXTRACT_TMP"; fi
|
|
if [[ -n $ATTACH_PID ]]; then kill "$ATTACH_PID" 2>/dev/null || true; fi
|
|
return 0
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
install_server() {
|
|
local name=$1 dir marker zip src have_type="" have_zip=""
|
|
dir=$(inst_dir "$name")
|
|
marker="$dir/server/.installed"
|
|
|
|
# Already installed with the right type (and pinned zip, if any)? Nothing to do.
|
|
if [[ -f $marker ]]; then
|
|
read -r have_type have_zip < "$marker" || true
|
|
if [[ $have_type == "$TYPE" ]] && [[ -z $ZIP || $have_zip == "$(basename "$ZIP")" ]]; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
command -v unzip >/dev/null || die "unzip is not installed"
|
|
zip=$(pick_zip)
|
|
[[ -r $zip ]] || die "cannot read $zip"
|
|
log "installing $TYPE from $(basename "$zip") into $dir/server"
|
|
|
|
EXTRACT_TMP=$(mktemp -d "$dir/.extract.XXXXXX")
|
|
unzip -o -q "$zip" -d "$EXTRACT_TMP"
|
|
|
|
case $TYPE in
|
|
vanilla)
|
|
# terraria.org zips contain <version>/{Linux,Windows,Mac}; we only want Linux
|
|
src=$(find "$EXTRACT_TMP" -type f -name TerrariaServer.bin.x86_64 -path '*/Linux/*' -printf '%h\n' -quit)
|
|
[[ -n $src ]] || die "Linux/TerrariaServer.bin.x86_64 not found in $zip"
|
|
chmod +x "$src/TerrariaServer.bin.x86_64"
|
|
[[ -f $src/TerrariaServer ]] && chmod +x "$src/TerrariaServer"
|
|
;;
|
|
tmodloader)
|
|
src=$(find "$EXTRACT_TMP" -type f -name start-tModLoaderServer.sh -printf '%h\n' -quit)
|
|
[[ -n $src ]] || die "start-tModLoaderServer.sh not found in $zip"
|
|
find "$src" -type f -name '*.sh' -exec chmod +x {} +
|
|
;;
|
|
esac
|
|
|
|
# tModLoader releases don't include .NET; it downloads it into server/dotnet on
|
|
# first start. Carry that (and the logs) over so an upgrade or reinstall doesn't
|
|
# download it again. tModLoader checks the .NET version itself on every start
|
|
# and installs a different one if the new release needs it.
|
|
if [[ $TYPE == tmodloader && -d $dir/server ]]; then
|
|
local keep
|
|
for keep in dotnet tModLoader-Logs; do
|
|
if [[ -d $dir/server/$keep && ! -e $src/$keep ]]; then
|
|
mv "$dir/server/$keep" "$src/$keep"
|
|
log "kept existing server/$keep"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
rm -rf "$dir/server"
|
|
mv "$src" "$dir/server"
|
|
echo "$TYPE $(basename "$zip")" > "$dir/server/.installed"
|
|
cleanup
|
|
log "install complete"
|
|
}
|
|
|
|
cmd_prepare() {
|
|
local name=$1 dir; dir=$(inst_dir "$name")
|
|
mkdir -p "$dir/data/Worlds"
|
|
[[ -f $dir/instance.conf ]] || write_instance_conf "$name" vanilla
|
|
load_conf "$name"
|
|
if [[ $TYPE == tmodloader ]]; then mkdir -p "$dir/data/Mods"; fi
|
|
[[ -f $dir/serverconfig.txt ]] || write_server_config "$name"
|
|
install_server "$name"
|
|
}
|
|
|
|
cmd_run() {
|
|
local name=$1 dir; dir=$(inst_dir "$name")
|
|
load_conf "$name"
|
|
[[ -f $dir/server/.installed ]] || die "server not installed; run 'prepare' first"
|
|
# Per-instance HOME so nothing is shared through ~/.local or ~/.dotnet
|
|
export HOME="$dir"
|
|
cd "$dir/server"
|
|
case $TYPE in
|
|
vanilla)
|
|
exec ./TerrariaServer.bin.x86_64 -config "$dir/serverconfig.txt" "${EXTRA_ARGS[@]}"
|
|
;;
|
|
tmodloader)
|
|
# -nosteam skips the interactive "use steam server?" prompt.
|
|
# -tmlsavedirectory keeps worlds, Mods/ and configs under data/.
|
|
# First start downloads a matching .NET runtime into server/dotnet.
|
|
exec ./start-tModLoaderServer.sh -nosteam \
|
|
-config "$dir/serverconfig.txt" \
|
|
-tmlsavedirectory "$dir/data" \
|
|
"${EXTRA_ARGS[@]}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cmd_stop() {
|
|
local name=$1 pid=${2:-} fifo="$RUN_DIR/$1.stdin" i
|
|
if [[ -z $pid ]] || ! kill -0 "$pid" 2>/dev/null; then exit 0; fi
|
|
if [[ -p $fifo ]]; then
|
|
echo exit > "$fifo"
|
|
else
|
|
log "console FIFO missing; systemd will send SIGTERM"
|
|
exit 0
|
|
fi
|
|
for ((i = 0; i < STOP_TIMEOUT; i++)); do
|
|
kill -0 "$pid" 2>/dev/null || exit 0
|
|
sleep 1
|
|
done
|
|
log "server did not exit within ${STOP_TIMEOUT}s; leaving it to systemd"
|
|
}
|
|
|
|
cmd_cmd() {
|
|
local name=$1; shift
|
|
[[ $# -gt 0 ]] || die "no console command given"
|
|
local fifo="$RUN_DIR/$name.stdin"
|
|
[[ -p $fifo ]] || die "$fifo not found - is terraria@$name running?"
|
|
printf '%s\n' "$*" > "$fifo"
|
|
echo "sent. Output: journalctl -u terraria@$name -n 20"
|
|
}
|
|
|
|
cmd_attach() {
|
|
local name=$1 fifo="$RUN_DIR/$1.stdin" line
|
|
[[ -p $fifo ]] || die "$fifo not found - is terraria@$name running?"
|
|
[[ -w $fifo ]] || die "no write access to $fifo (run with sudo or join group '$SVC_USER')"
|
|
|
|
echo "Attached to terraria@$name. Ctrl-D or Ctrl-C detaches; the server keeps running."
|
|
# Server output comes from the journal; -o cat drops the timestamp prefixes.
|
|
journalctl -u "terraria@$name" -f -n 30 -o cat &
|
|
ATTACH_PID=$! # killed by cleanup() however attach ends
|
|
trap 'echo; exit 0' INT
|
|
|
|
# -e gives line editing and history while typing commands
|
|
while IFS= read -r -e line; do
|
|
[[ -z $line ]] && continue
|
|
if [[ $line == exit ]]; then
|
|
echo "(not sent: 'exit' would shut the server down. Use Ctrl-D to detach," \
|
|
"or 'systemctl stop terraria@$name' to stop it.)"
|
|
continue
|
|
fi
|
|
printf '%s\n' "$line" > "$fifo"
|
|
done
|
|
echo
|
|
}
|
|
|
|
cmd_delete() {
|
|
local name=$1; shift
|
|
local assume_yes=0 backup=1 dir archive answer
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-y|--yes) assume_yes=1 ;;
|
|
--no-backup) backup=0 ;;
|
|
*) die "unknown option '$1'" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[[ $EUID -eq 0 ]] || die "delete must be run as root"
|
|
dir=$(inst_dir "$name")
|
|
[[ -d $dir ]] || die "$dir does not exist"
|
|
# Refuse to remove anything that doesn't look like one of our instances
|
|
[[ -f $dir/instance.conf ]] || die "$dir has no instance.conf; not deleting it"
|
|
|
|
if [[ $assume_yes -ne 1 ]]; then
|
|
[[ -t 0 ]] || die "not a terminal; use -y to delete without confirmation"
|
|
echo "This will stop terraria@$name and remove $dir."
|
|
if [[ $backup -eq 1 ]]; then
|
|
echo "Worlds and configs will be archived to $BASE_DIR/.deleted/ first."
|
|
else
|
|
echo "NO BACKUP will be made. Worlds will be lost."
|
|
fi
|
|
read -r -p "Type the instance name to confirm: " answer
|
|
[[ $answer == "$name" ]] || die "confirmation did not match; nothing deleted"
|
|
fi
|
|
|
|
if command -v systemctl >/dev/null; then
|
|
systemctl disable --now "terraria@$name.service" 2>/dev/null || true
|
|
systemctl stop "terraria@$name.socket" 2>/dev/null || true
|
|
systemctl reset-failed "terraria@$name.service" "terraria@$name.socket" 2>/dev/null || true
|
|
fi
|
|
|
|
if [[ $backup -eq 1 ]]; then
|
|
mkdir -p "$BASE_DIR/.deleted"
|
|
chmod 700 "$BASE_DIR/.deleted"
|
|
archive="$BASE_DIR/.deleted/$name-$(date +%Y%m%d-%H%M%S).tar.gz"
|
|
# server/ is excluded: it's just the unpacked zip and can be recreated
|
|
tar -czf "$archive" -C "$BASE_DIR" \
|
|
--exclude="$name/server" --exclude="$name/.extract.*" "$name"
|
|
echo "backup: $archive"
|
|
fi
|
|
|
|
rm -rf -- "$dir"
|
|
echo "deleted instance '$name'"
|
|
}
|
|
|
|
# ---------------------------------------------------------------- mods ----
|
|
# enabled.json is a plain JSON array of mod names, e.g. ["CalamityMod","MagicStorage"].
|
|
# Mod names are identifiers, so simple text handling is enough (no jq needed).
|
|
|
|
fix_owner() { if [[ $EUID -eq 0 ]]; then chown "$SVC_USER:$SVC_USER" "$@"; fi; }
|
|
|
|
read_enabled() {
|
|
local f="$1/enabled.json"
|
|
[[ -f $f ]] || return 0
|
|
grep -o '"[^"]*"' "$f" | tr -d '"' || true
|
|
}
|
|
|
|
write_enabled() { # write_enabled <mods dir> <name>...
|
|
local mdir=$1; shift
|
|
local out="[" sep="" n
|
|
for n in "$@"; do out+="$sep"$'\n'" \"$n\""; sep=","; done
|
|
if [[ $# -gt 0 ]]; then out+=$'\n'; fi
|
|
out+="]"
|
|
printf '%s\n' "$out" > "$mdir/.enabled.json.tmp"
|
|
mv "$mdir/.enabled.json.tmp" "$mdir/enabled.json"
|
|
fix_owner "$mdir/enabled.json"
|
|
}
|
|
|
|
mods_restart_hint() {
|
|
if systemctl is-active --quiet "terraria@$name" 2>/dev/null; then
|
|
echo "Restart to apply: sudo systemctl restart terraria@$name"
|
|
fi
|
|
}
|
|
|
|
mods_list() {
|
|
local enabled f n st
|
|
enabled=$(read_enabled "$mdir")
|
|
local -a files=()
|
|
for f in "$mdir"/*.tmod; do
|
|
if [[ -f $f ]]; then files+=("$f"); fi
|
|
done
|
|
if [[ ${#files[@]} -eq 0 ]]; then
|
|
echo "No mods installed in $mdir"
|
|
echo "Add some with: terractl mods $name add <folder or .tmod files>"
|
|
return 0
|
|
fi
|
|
printf '%-32s %s\n' MOD STATE
|
|
for f in "${files[@]}"; do
|
|
n=$(basename "$f" .tmod)
|
|
if grep -qxF "$n" <<<"$enabled"; then st=enabled; else st=disabled; fi
|
|
printf '%-32s %s\n' "$n" "$st"
|
|
done
|
|
for n in $enabled; do
|
|
if [[ ! -f $mdir/$n.tmod ]]; then printf '%-32s %s\n' "$n" "enabled but MISSING (server will fail to load mods)"; fi
|
|
done
|
|
}
|
|
|
|
mods_add() {
|
|
[[ $# -gt 0 ]] || die "usage: terractl mods $name add <file.tmod|folder>..."
|
|
local src f n found
|
|
local -a files=() cfgs=()
|
|
for src in "$@"; do
|
|
if [[ -f $src ]]; then
|
|
[[ $src == *.tmod ]] || die "$src is not a .tmod file"
|
|
files+=("$src")
|
|
elif [[ -d $src ]]; then
|
|
# Accepts a folder of .tmod files, or an export folder containing Mods/ and ModConfigs/
|
|
found=0
|
|
for f in "$src"/*.tmod "$src"/Mods/*.tmod; do
|
|
if [[ -f $f ]]; then files+=("$f"); found=1; fi
|
|
done
|
|
[[ $found -eq 1 ]] || die "no .tmod files in $src or $src/Mods"
|
|
for f in "$src"/ModConfigs/*.json; do
|
|
if [[ -f $f ]]; then cfgs+=("$f"); fi
|
|
done
|
|
else
|
|
die "$src not found"
|
|
fi
|
|
done
|
|
|
|
local -A pick=()
|
|
for f in "${files[@]}"; do
|
|
n=$(basename "$f" .tmod)
|
|
[[ $n =~ ^[A-Za-z0-9_]+$ ]] || die "unexpected mod file name '$n.tmod'"
|
|
if [[ -n ${pick[$n]:-} ]]; then log "warning: $n given more than once; using $f"; fi
|
|
pick[$n]=$f
|
|
done
|
|
|
|
mkdir -p "$mdir"
|
|
fix_owner "$dir/data" "$mdir"
|
|
for n in "${!pick[@]}"; do
|
|
install -m 0640 "${pick[$n]}" "$mdir/$n.tmod"
|
|
fix_owner "$mdir/$n.tmod"
|
|
echo "installed $n"
|
|
done
|
|
|
|
if [[ ${#cfgs[@]} -gt 0 ]]; then
|
|
mkdir -p "$dir/data/ModConfigs"
|
|
fix_owner "$dir/data/ModConfigs"
|
|
for f in "${cfgs[@]}"; do
|
|
install -m 0640 "$f" "$dir/data/ModConfigs/$(basename "$f")"
|
|
fix_owner "$dir/data/ModConfigs/$(basename "$f")"
|
|
done
|
|
echo "installed ${#cfgs[@]} mod config file(s)"
|
|
fi
|
|
|
|
local -a current=()
|
|
mapfile -t current < <(read_enabled "$mdir")
|
|
# shellcheck disable=SC2046
|
|
write_enabled "$mdir" $(printf '%s\n' "${current[@]}" "${!pick[@]}" | sort -u)
|
|
echo "enabled: ${!pick[*]}"
|
|
echo "Note: dependencies are not checked here; if one is missing, the server log will say which."
|
|
mods_restart_hint
|
|
}
|
|
|
|
mods_set() { # mods_set 1|0 <name>...
|
|
local on=$1; shift
|
|
[[ $# -gt 0 ]] || die "no mod names given"
|
|
local n list
|
|
local -a current=()
|
|
mapfile -t current < <(read_enabled "$mdir")
|
|
if [[ $on -eq 1 ]]; then
|
|
for n in "$@"; do
|
|
[[ -f $mdir/$n.tmod ]] || die "$n is not installed (no $mdir/$n.tmod)"
|
|
done
|
|
list=$(printf '%s\n' "${current[@]}" "$@" | sort -u)
|
|
else
|
|
list=$(printf '%s\n' "${current[@]}" | grep -vxF -f <(printf '%s\n' "$@") || true)
|
|
fi
|
|
mkdir -p "$mdir"
|
|
# shellcheck disable=SC2086
|
|
write_enabled "$mdir" $list
|
|
}
|
|
|
|
mods_remove() {
|
|
[[ $# -gt 0 ]] || die "no mod names given"
|
|
local n
|
|
for n in "$@"; do
|
|
[[ $n =~ ^[A-Za-z0-9_]+$ ]] || die "invalid mod name '$n'"
|
|
if [[ -f $mdir/$n.tmod ]]; then rm -f "$mdir/$n.tmod"; echo "removed $n"; else log "$n was not installed"; fi
|
|
done
|
|
mods_set 0 "$@"
|
|
}
|
|
|
|
cmd_mods() {
|
|
local name=$1; shift
|
|
local action=${1:-list}
|
|
if [[ $# -gt 0 ]]; then shift; fi
|
|
local dir mdir
|
|
dir=$(inst_dir "$name")
|
|
[[ -f $dir/instance.conf ]] || die "no instance '$name' (create it with: terractl new $name tmodloader)"
|
|
load_conf "$name"
|
|
[[ $TYPE == tmodloader ]] || die "'$name' is a $TYPE instance; mods need TYPE=tmodloader"
|
|
mdir="$dir/data/Mods"
|
|
case $action in
|
|
list) mods_list ;;
|
|
add) mods_add "$@" ;;
|
|
enable) mods_set 1 "$@"; echo "enabled: $*"; mods_restart_hint ;;
|
|
disable) mods_set 0 "$@"; echo "disabled: $*"; mods_restart_hint ;;
|
|
remove) mods_remove "$@"; mods_restart_hint ;;
|
|
*) die "unknown mods action '$action' (list, add, enable, disable, remove)" ;;
|
|
esac
|
|
}
|
|
|
|
cmd_new() {
|
|
local name=$1 type=${2:-vanilla} port=${3:-} dir
|
|
[[ $type == vanilla || $type == tmodloader ]] || die "type must be vanilla or tmodloader"
|
|
[[ -z $port || $port =~ ^[0-9]+$ ]] || die "port must be a number"
|
|
dir=$(inst_dir "$name")
|
|
[[ -e $dir ]] && die "$dir already exists"
|
|
mkdir -p "$dir/data/Worlds"
|
|
if [[ $type == tmodloader ]]; then mkdir -p "$dir/data/Mods"; fi
|
|
write_instance_conf "$name" "$type"
|
|
write_server_config "$name" "$port"
|
|
if [[ $EUID -eq 0 ]]; then chown -R "$SVC_USER:$SVC_USER" "$dir"; fi
|
|
cat <<EOF
|
|
Created $type instance '$name' in $dir
|
|
Review $dir/serverconfig.txt, then:
|
|
EOF
|
|
if [[ $type == tmodloader ]]; then
|
|
echo " terractl mods $name add <folder or .tmod files> (before first start)"
|
|
fi
|
|
cat <<EOF
|
|
systemctl enable --now terraria@$name
|
|
terractl attach $name
|
|
EOF
|
|
}
|
|
|
|
cmd_list() {
|
|
local d n port state
|
|
printf '%-20s %-11s %-6s %s\n' INSTANCE TYPE PORT STATE
|
|
for d in "$BASE_DIR"/*/; do
|
|
[[ -f $d/instance.conf ]] || continue
|
|
n=$(basename "$d")
|
|
(
|
|
load_conf "$n"
|
|
port=$(sed -n 's/^port=\([0-9][0-9]*\).*/\1/p' "$d/serverconfig.txt" 2>/dev/null | tail -n1)
|
|
state=$(systemctl is-active "terraria@$n" 2>/dev/null || true)
|
|
printf '%-20s %-11s %-6s %s\n' "$n" "$TYPE" "${port:--}" "$state"
|
|
) 2>/dev/null || printf '%-20s %s\n' "$n" "(cannot read config - try sudo)"
|
|
done
|
|
}
|
|
|
|
cmd_reinstall() {
|
|
local name=$1 clean=0 dir; dir=$(inst_dir "$name")
|
|
shift
|
|
case ${1:-} in
|
|
--clean) clean=1 ;;
|
|
"") ;;
|
|
*) die "unknown option '$1'" ;;
|
|
esac
|
|
[[ -f $dir/instance.conf ]] || die "no instance '$name'"
|
|
rm -f "$dir/server/.installed"
|
|
if [[ $clean -eq 1 ]]; then
|
|
rm -rf "$dir/server/dotnet" "$dir/server/tModLoader-Logs"
|
|
echo "removed the downloaded .NET runtime and logs; .NET will be downloaded again"
|
|
fi
|
|
echo "server/ will be re-unpacked on next start: systemctl restart terraria@$name"
|
|
}
|
|
|
|
[[ $# -ge 1 ]] || usage
|
|
command=$1; shift
|
|
case $command in
|
|
-h|--help|help) usage 0 ;;
|
|
-V|--version|version)
|
|
# The build replaces the placeholder with a version number, which never contains '@'
|
|
if [[ $TERRACTL_VERSION == *@* ]]; then echo "terractl (development copy)"
|
|
else echo "terractl $TERRACTL_VERSION"; fi
|
|
exit 0 ;;
|
|
list) cmd_list; exit ;;
|
|
esac
|
|
[[ $# -ge 1 ]] || usage
|
|
valid_name "$1"
|
|
|
|
# prepare/run create and modify instance files. If invoked as root (e.g. while
|
|
# debugging), switch to the service user so nothing ends up owned by root.
|
|
if [[ $command == prepare || $command == run ]]; then
|
|
if [[ $EUID -eq 0 ]]; then
|
|
command -v runuser >/dev/null || die "runuser not found; run as '$SVC_USER' instead"
|
|
exec runuser -u "$SVC_USER" -- env HOME="$(inst_dir "$1")" \
|
|
"$(readlink -f "$0")" "$command" "$@"
|
|
elif [[ $(id -un) != "$SVC_USER" ]]; then
|
|
die "$command must run as root or '$SVC_USER'"
|
|
fi
|
|
fi
|
|
|
|
case $command in
|
|
prepare) cmd_prepare "$1" ;;
|
|
run) cmd_run "$1" ;;
|
|
stop) cmd_stop "$@" ;;
|
|
cmd) cmd_cmd "$@" ;;
|
|
attach) cmd_attach "$1" ;;
|
|
mods) cmd_mods "$@" ;;
|
|
new) cmd_new "$@" ;;
|
|
reinstall) cmd_reinstall "$@" ;;
|
|
delete) cmd_delete "$@" ;;
|
|
*) usage ;;
|
|
esac
|