azerothcore-playerbots-inst.../install.sh
AzerothCore Installer 67b9b2dde5 Initial commit: full unattended AzerothCore + Playerbots installer
- Builds mod-playerbots/azerothcore-wotlk (Playerbot branch) from source
- Optional modules: mod-ah-bot, mod-ollama-chat, mod-quest-loot-party,
  mod-breaking-news-override
- Interactive prompts for all secrets/settings, then fully unattended
- systemd services, ufw rules, MySQL tuning, client data via bin/acore
- Automated GM account creation via expect-driven console session
- Resumable via a state-marker directory if a step fails partway through
- Generates restart-servers.sh and soap-cmd.sh helper scripts
- Writes a chmod 600 summary file with all credentials/next steps
2026-08-21 14:54:40 +00:00

933 lines
37 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# ==============================================================================
# AzerothCore + Playerbots — Full Automated Installer
# ==============================================================================
#
# Installs, from a clean Ubuntu Server VM, all the way to running services:
#
# - AzerothCore (Playerbot fork: mod-playerbots/azerothcore-wotlk)
# - mod-playerbots (bot army)
# - mod-ah-bot (auction house bot) [optional]
# - mod-ollama-chat (LLM-powered bot chat) [optional]
# - mod-quest-loot-party (shared quest item loot) [optional]
# - mod-breaking-news-override (character-select notice) [optional]
#
# It asks for every password/setting it needs up front, then runs
# unattended. When finished it writes a summary file to your home
# directory with everything you'll need to connect and manage the
# server (chmod 600 — it contains real passwords).
#
# USAGE:
# 1. Run this on a CLEAN Ubuntu Server 24.04 VM as a regular sudo user
# (not root).
# 2. Run it inside `screen` or `tmux` — the build step alone can take
# 30-90+ minutes, and this script does NOT survive an SSH disconnect
# on its own:
# screen -S install
# chmod +x install.sh
# ./install.sh
# 3. If it stops partway (build error, disconnect, etc.), just run it
# again. Every step records completion in a state directory and is
# skipped on re-run once done — see "Resuming" in the README.
#
# This script is intentionally verbose about what it's doing and why.
# Read the comments if something goes wrong — they explain several
# non-obvious gotchas this project's tooling has (inconsistent config
# filenames between modules, a cmake flag that silently skips the
# database-import tool, etc).
#
# ==============================================================================
set -euo pipefail
# ------------------------------------------------------------------------------
# Globals
# ------------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_DIR="${INSTALL_DIR:-$HOME/azeroth-server}"
STATE_DIR="$HOME/.azerothcore-installer-state"
SUMMARY_FILE="$HOME/azerothcore-server-info.txt"
CORE_REPO="https://github.com/mod-playerbots/azerothcore-wotlk.git"
CORE_BRANCH="Playerbot"
PLAYERBOTS_MODULE_REPO="https://github.com/mod-playerbots/mod-playerbots.git"
PLAYERBOTS_MODULE_BRANCH="master"
AHBOT_MODULE_REPO="https://github.com/azerothcore/mod-ah-bot.git"
OLLAMA_MODULE_REPO="https://github.com/DustinHendrickson/mod-ollama-chat.git"
QUESTPARTY_MODULE_REPO="https://github.com/pangolp/mod-quest-loot-party.git"
BREAKINGNEWS_MODULE_REPO="https://github.com/azerothcore/mod-breaking-news-override.git"
mkdir -p "$STATE_DIR"
# ------------------------------------------------------------------------------
# Logging helpers
# ------------------------------------------------------------------------------
log() { echo -e "\n\033[1;32m==> $1\033[0m\n"; }
info() { echo -e "\033[0;36m $1\033[0m"; }
warn() { echo -e "\n\033[1;33m!! $1\033[0m\n"; }
fail() { echo -e "\n\033[1;31mXX $1\033[0m\n" >&2; exit 1; }
# ------------------------------------------------------------------------------
# State tracking — lets the script be re-run safely after a partial failure.
# Each completed step writes a marker file; steps check for their marker
# before doing (potentially slow / non-idempotent) work.
# ------------------------------------------------------------------------------
step_done() { [ -f "$STATE_DIR/$1" ]; }
mark_done() { touch "$STATE_DIR/$1"; }
run_step() {
local step_name="$1"
local step_fn="$2"
if step_done "$step_name"; then
info "Skipping '$step_name' (already completed — remove $STATE_DIR/$step_name to redo it)"
else
log "Step: $step_name"
"$step_fn"
mark_done "$step_name"
fi
}
# ------------------------------------------------------------------------------
# Prompt helpers
# ------------------------------------------------------------------------------
# prompt_text <var_name> <question> <default>
prompt_text() {
local __var="$1" __q="$2" __default="${3:-}"
local __answer
if [ -n "$__default" ]; then
read -r -p "$__q [$__default]: " __answer
__answer="${__answer:-$__default}"
else
read -r -p "$__q: " __answer
fi
printf -v "$__var" '%s' "$__answer"
}
# prompt_yes_no <question> <default: y|n> -- returns 0 for yes, 1 for no
prompt_yes_no() {
local __q="$1" __default="${2:-y}"
local __suffix="[y/N]"
[ "$__default" = "y" ] && __suffix="[Y/n]"
local __answer
read -r -p "$__q $__suffix: " __answer
__answer="${__answer:-$__default}"
[[ "$__answer" =~ ^[Yy] ]]
}
# prompt_password_confirm <var_name> <question>
# Prompts twice (hidden input) and insists they match before returning.
prompt_password_confirm() {
local __var="$1" __q="$2"
local __p1 __p2
while true; do
read -r -s -p "$__q: " __p1; echo
read -r -s -p "Confirm: " __p2; echo
if [ "$__p1" != "$__p2" ]; then
echo "Passwords did not match — try again."
continue
fi
if [ -z "$__p1" ]; then
echo "Password cannot be empty — try again."
continue
fi
printf -v "$__var" '%s' "$__p1"
break
done
}
# ------------------------------------------------------------------------------
# Preflight checks
# ------------------------------------------------------------------------------
preflight_checks() {
if [ "$EUID" -eq 0 ]; then
fail "Do not run this script as root. Run as a regular sudo-capable user."
fi
if ! sudo -v; then
fail "This user needs sudo access."
fi
if ! grep -qi "ubuntu" /etc/os-release 2>/dev/null; then
warn "This doesn't look like Ubuntu. The script assumes apt/systemd/ufw are available — proceed at your own risk."
fi
local ram_gb
ram_gb=$(awk '/MemTotal/ {printf "%.0f", $2/1024/1024}' /proc/meminfo)
if [ "$ram_gb" -lt 8 ]; then
warn "Detected ~${ram_gb}GB RAM. AzerothCore + Playerbots wants 8GB minimum, 16GB comfortable. Proceeding, but the build or a large bot population may struggle."
fi
local disk_free_gb
disk_free_gb=$(df --output=avail -BG "$HOME" | tail -1 | tr -dc '0-9')
if [ "$disk_free_gb" -lt 40 ]; then
warn "Detected ~${disk_free_gb}GB free disk under \$HOME. 40GB+ is recommended (client data + DB growth)."
fi
}
# ------------------------------------------------------------------------------
# Configuration collection — everything the script needs to know is
# gathered up front so the rest of the run is unattended.
# ------------------------------------------------------------------------------
collect_config() {
echo "=============================================================="
echo " AzerothCore + Playerbots installer — configuration"
echo "=============================================================="
echo "This asks a series of questions, then runs unattended."
echo "Press Enter to accept a default shown in [brackets]."
echo
# --- Database ---
prompt_password_confirm ACORE_DB_PASSWORD "MySQL password to set for the 'acore' database user"
# --- Build sizing ---
local nproc_val
nproc_val=$(nproc)
local default_threads=$(( nproc_val > 2 ? nproc_val - 2 : 1 ))
prompt_text BUILD_THREADS "Parallel build threads (compile job count)" "$default_threads"
prompt_text WORLD_UPDATE_THREADS "worldserver MapUpdate.Threads" "$default_threads"
# --- Realm ---
prompt_text REALM_NAME "Realm name (shown in the client realm list)" "AzerothCore"
# --- Playerbots ---
if prompt_yes_no "Install mod-playerbots (bot army)?" "y"; then
INSTALL_PLAYERBOTS="1"
prompt_text MIN_RANDOM_BOTS "Minimum random bots" "50"
prompt_text MAX_RANDOM_BOTS "Maximum random bots" "200"
else
INSTALL_PLAYERBOTS="0"
MIN_RANDOM_BOTS="0"
MAX_RANDOM_BOTS="0"
fi
# --- AH Bot ---
if prompt_yes_no "Install mod-ah-bot (auction house bot)?" "y"; then
INSTALL_AHBOT="1"
else
INSTALL_AHBOT="0"
fi
# --- Ollama chat ---
if prompt_yes_no "Install mod-ollama-chat (LLM-powered bot chat via Ollama)?" "n"; then
INSTALL_OLLAMA="1"
echo
echo " mod-ollama-chat needs a running Ollama server. It can be:"
echo " - on THIS machine (the script will install Ollama + pull a model)"
echo " - on ANOTHER machine on your network (you just give the address)"
if prompt_yes_no " Install Ollama locally on this VM?" "n"; then
OLLAMA_LOCAL="1"
prompt_text OLLAMA_MODEL "Ollama model to pull (small models recommended on a shared VM)" "llama3.2:1b"
OLLAMA_ENDPOINT="http://localhost:11434"
else
OLLAMA_LOCAL="0"
prompt_text OLLAMA_HOST "IP address of the existing Ollama server" ""
prompt_text OLLAMA_PORT "Ollama port" "11434"
OLLAMA_ENDPOINT="http://${OLLAMA_HOST}:${OLLAMA_PORT}"
prompt_text OLLAMA_MODEL "Exact model name/tag to use (must already be pulled on that server — check with: curl http://${OLLAMA_HOST}:${OLLAMA_PORT}/api/tags)" ""
fi
else
INSTALL_OLLAMA="0"
OLLAMA_LOCAL="0"
OLLAMA_ENDPOINT=""
OLLAMA_MODEL=""
fi
# --- Quest Loot Party ---
if prompt_yes_no "Install mod-quest-loot-party (shared quest-item loot for groups)?" "y"; then
INSTALL_QUESTPARTY="1"
else
INSTALL_QUESTPARTY="0"
fi
# --- Breaking News ---
if prompt_yes_no "Install mod-breaking-news-override (character-select announcement panel)?" "y"; then
INSTALL_BREAKINGNEWS="1"
prompt_text BREAKINGNEWS_TITLE "Title for the announcement panel" "$REALM_NAME"
else
INSTALL_BREAKINGNEWS="0"
BREAKINGNEWS_TITLE=""
fi
# --- GM account ---
echo
echo " A GM (admin) account will be created for you once the server is up."
prompt_text GM_ACCOUNT_NAME "GM account username" "admin"
prompt_password_confirm GM_ACCOUNT_PASSWORD "GM account password"
# --- Firewall ---
echo
if prompt_yes_no "Configure ufw firewall for game ports (3724, 8085)?" "y"; then
CONFIGURE_FIREWALL="1"
else
CONFIGURE_FIREWALL="0"
fi
echo
echo "=============================================================="
echo " Configuration collected. Starting unattended install."
echo " Safe to detach/reattach your screen/tmux session from here."
echo "=============================================================="
sleep 2
}
# ------------------------------------------------------------------------------
# Step: system dependencies
# ------------------------------------------------------------------------------
step_install_dependencies() {
sudo apt update && sudo apt upgrade -y
sudo apt install -y git cmake make gcc g++ clang \
libssl-dev libbz2-dev libreadline-dev libncurses-dev \
libboost-all-dev libmysqlclient-dev mysql-server \
unzip curl screen lsb-release gnupg wget expect
}
# ------------------------------------------------------------------------------
# Step: MySQL user setup
# ------------------------------------------------------------------------------
step_setup_mysql() {
sudo systemctl enable --now mysql
sudo mysql -u root <<SQL
CREATE USER IF NOT EXISTS 'acore'@'localhost' IDENTIFIED BY '${ACORE_DB_PASSWORD}';
GRANT ALL PRIVILEGES ON *.* TO 'acore'@'localhost' WITH GRANT OPTION;
FLUSH PRIVILEGES;
SQL
}
# ------------------------------------------------------------------------------
# Step: clone core + selected modules
# ------------------------------------------------------------------------------
step_clone_source() {
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"
if [ ! -d "azerothcore-wotlk" ]; then
git clone "$CORE_REPO" --branch="$CORE_BRANCH" azerothcore-wotlk
fi
cd "$INSTALL_DIR/azerothcore-wotlk/modules"
if [ "$INSTALL_PLAYERBOTS" = "1" ] && [ ! -d "mod-playerbots" ]; then
git clone "$PLAYERBOTS_MODULE_REPO" --branch="$PLAYERBOTS_MODULE_BRANCH" mod-playerbots
fi
if [ "$INSTALL_AHBOT" = "1" ] && [ ! -d "mod-ah-bot" ]; then
git clone "$AHBOT_MODULE_REPO" mod-ah-bot
fi
if [ "$INSTALL_OLLAMA" = "1" ] && [ ! -d "mod-ollama-chat" ]; then
git clone "$OLLAMA_MODULE_REPO" mod-ollama-chat
fi
if [ "$INSTALL_QUESTPARTY" = "1" ] && [ ! -d "mod-quest-loot-party" ]; then
git clone "$QUESTPARTY_MODULE_REPO" mod-quest-loot-party
fi
if [ "$INSTALL_BREAKINGNEWS" = "1" ] && [ ! -d "mod-breaking-news-override" ]; then
git clone "$BREAKINGNEWS_MODULE_REPO" mod-breaking-news-override
fi
}
# ------------------------------------------------------------------------------
# Step: build
#
# NOTE on -DTOOLS_BUILD: this MUST be "db-only" (or "all"), never "none".
# AzerothCore's dbimport tool — which creates/updates the core databases —
# is itself considered a "tool" by this flag. Setting it to "none" silently
# skips building dbimport, its config template never gets installed, and
# the DB-import step several stages later fails with a confusing
# "file not found" error that gives no hint the cause was a build flag.
# "db-only" builds dbimport without also building the (slow, client-data-
# extracting) mapextractor/vmap/mmap tools we don't need — we get client
# data via `bin/acore client-data` instead, which downloads pre-extracted
# data from the community-maintained wowgaming/client-data repo.
# ------------------------------------------------------------------------------
step_build() {
cd "$INSTALL_DIR/azerothcore-wotlk"
mkdir -p build && cd build
cmake ../ \
-DCMAKE_INSTALL_PREFIX="$INSTALL_DIR/azerothcore-wotlk/env/dist" \
-DCMAKE_C_COMPILER=/usr/bin/clang \
-DCMAKE_CXX_COMPILER=/usr/bin/clang++ \
-DCONF_DIR="$INSTALL_DIR/azerothcore-wotlk/env/dist/etc" \
-DWITH_WARNINGS=0 \
-DTOOLS_BUILD=db-only \
-DSCRIPTS=static \
-DMODULES=static
make -j"$BUILD_THREADS"
make install
local bin_dir="$INSTALL_DIR/azerothcore-wotlk/env/dist/bin"
[ -f "$bin_dir/worldserver" ] || fail "worldserver binary missing after build — check the make output above."
[ -f "$bin_dir/dbimport" ] || fail "dbimport binary missing after build — TOOLS_BUILD may not have taken effect."
}
# ------------------------------------------------------------------------------
# Step: import core databases
# ------------------------------------------------------------------------------
step_import_databases() {
local etc_dir="$INSTALL_DIR/azerothcore-wotlk/env/dist/etc"
local bin_dir="$INSTALL_DIR/azerothcore-wotlk/env/dist/bin"
cd "$etc_dir"
[ -f dbimport.conf ] || cp dbimport.conf.dist dbimport.conf
sed -i "s#^LoginDatabaseInfo.*#LoginDatabaseInfo = \"127.0.0.1;3306;acore;${ACORE_DB_PASSWORD};acore_auth\"#" dbimport.conf
sed -i "s#^WorldDatabaseInfo.*#WorldDatabaseInfo = \"127.0.0.1;3306;acore;${ACORE_DB_PASSWORD};acore_world\"#" dbimport.conf
sed -i "s#^CharacterDatabaseInfo.*#CharacterDatabaseInfo = \"127.0.0.1;3306;acore;${ACORE_DB_PASSWORD};acore_characters\"#" dbimport.conf
cd "$bin_dir"
./dbimport
# Playerbots gets its own database. Its tables are populated by the
# normal auto-update system the first time worldserver starts with the
# module compiled in — deliberately NOT imported by hand here. Manually
# pre-importing module SQL is what causes "table already exists" /
# "table doesn't exist" auto-updater desyncs later — let the updater
# own its own tracking table from a clean database.
if [ "$INSTALL_PLAYERBOTS" = "1" ]; then
sudo mysql -u root <<SQL
CREATE DATABASE IF NOT EXISTS acore_playerbots;
GRANT ALL PRIVILEGES ON acore_playerbots.* TO 'acore'@'localhost';
FLUSH PRIVILEGES;
SQL
fi
}
# ------------------------------------------------------------------------------
# Step: configure authserver / worldserver
# ------------------------------------------------------------------------------
step_configure_core() {
local etc_dir="$INSTALL_DIR/azerothcore-wotlk/env/dist/etc"
cd "$etc_dir"
[ -f authserver.conf ] || cp authserver.conf.dist authserver.conf
[ -f worldserver.conf ] || cp worldserver.conf.dist worldserver.conf
sed -i "s#^LoginDatabaseInfo.*#LoginDatabaseInfo = \"127.0.0.1;3306;acore;${ACORE_DB_PASSWORD};acore_auth\"#" authserver.conf
sed -i "s#^LoginDatabaseInfo.*#LoginDatabaseInfo = \"127.0.0.1;3306;acore;${ACORE_DB_PASSWORD};acore_auth\"#" worldserver.conf
sed -i "s#^WorldDatabaseInfo.*#WorldDatabaseInfo = \"127.0.0.1;3306;acore;${ACORE_DB_PASSWORD};acore_world\"#" worldserver.conf
sed -i "s#^CharacterDatabaseInfo.*#CharacterDatabaseInfo = \"127.0.0.1;3306;acore;${ACORE_DB_PASSWORD};acore_characters\"#" worldserver.conf
sed -i "s#^MapUpdate.Threads.*#MapUpdate.Threads = ${WORLD_UPDATE_THREADS}#" worldserver.conf
}
# ------------------------------------------------------------------------------
# Step: configure modules
#
# NOTE on filenames: AzerothCore modules are NOT consistent about whether
# their installed .conf.dist uses hyphens or underscores, or matches the
# repo name at all. These exact filenames below were confirmed by hand
# against real installs — don't "clean them up" to match the repo names,
# they're deliberately exact.
# ------------------------------------------------------------------------------
step_configure_modules() {
local mod_dir="$INSTALL_DIR/azerothcore-wotlk/env/dist/etc/modules"
cd "$mod_dir"
if [ "$INSTALL_PLAYERBOTS" = "1" ]; then
[ -f playerbots.conf ] || cp playerbots.conf.dist playerbots.conf
sed -i "s#^PlayerbotsDatabaseInfo.*#PlayerbotsDatabaseInfo = \"127.0.0.1;3306;acore;${ACORE_DB_PASSWORD};acore_playerbots\"#" playerbots.conf
cat >> playerbots.conf <<EOF
# --- Set by installer ---
AiPlayerbot.RandomBotAutologin = 1
AiPlayerbot.MinRandomBots = ${MIN_RANDOM_BOTS}
AiPlayerbot.MaxRandomBots = ${MAX_RANDOM_BOTS}
EOF
# mod-ollama-chat's README asks for Playerbots' own chat/emote/greet
# systems to be quieted down so they don't fight with LLM-generated
# chat. Only applied when both modules are being installed together.
if [ "$INSTALL_OLLAMA" = "1" ]; then
cat >> playerbots.conf <<EOF
# --- mod-ollama-chat compatibility (set by installer) ---
AiPlayerbot.EnableBroadcasts = 0
AiPlayerbot.RandomBotTalk = 0
AiPlayerbot.RandomBotEmote = 0
AiPlayerbot.RandomBotSuggestDungeons = 0
AiPlayerbot.EnableGreet = 0
AiPlayerbot.GuildFeedback = 0
AiPlayerbot.RandomBotSayWithoutMaster = 0
EOF
fi
fi
if [ "$INSTALL_AHBOT" = "1" ]; then
[ -f mod_ahbot.conf ] || cp mod_ahbot.conf.dist mod_ahbot.conf
# AHBot needs a dedicated account+character to act as the buyer/
# seller. That account is created later (create_ahbot_account),
# but the character itself requires a one-time client login to
# exist — this is a genuine manual step, documented in the
# summary file, not something this script can complete unattended.
fi
if [ "$INSTALL_OLLAMA" = "1" ]; then
[ -f mod_ollama_chat.conf ] || cp mod_ollama_chat.conf.dist mod_ollama_chat.conf
sed -i "s#^OllamaChat.ApiEndpoint.*#OllamaChat.ApiEndpoint = ${OLLAMA_ENDPOINT}#" mod_ollama_chat.conf
sed -i "s#^OllamaChat.Model.*#OllamaChat.Model = ${OLLAMA_MODEL}#" mod_ollama_chat.conf
fi
if [ "$INSTALL_QUESTPARTY" = "1" ]; then
[ -f mod-quest-loot-party.conf ] || cp mod-quest-loot-party.conf.dist mod-quest-loot-party.conf
sed -i "s#^QuestParty.Enable.*#QuestParty.Enable = true#" mod-quest-loot-party.conf
sed -i "s#^QuestParty.Message.*#QuestParty.Message = true#" mod-quest-loot-party.conf
fi
if [ "$INSTALL_BREAKINGNEWS" = "1" ]; then
[ -f breakingnews.conf ] || cp breakingnews.conf.dist breakingnews.conf
sed -i "s#^BreakingNews.Enable.*#BreakingNews.Enable = 1#" breakingnews.conf
sed -i "s#^BreakingNews.Title.*#BreakingNews.Title = \"${BREAKINGNEWS_TITLE}\"#" breakingnews.conf
sed -i "s#^BreakingNews.HtmlPath.*#BreakingNews.HtmlPath = \"./breakingnews.html\"#" breakingnews.conf
local bin_dir="$INSTALL_DIR/azerothcore-wotlk/env/dist/bin"
if [ ! -f "$bin_dir/breakingnews.html" ]; then
cat > "$bin_dir/breakingnews.html" <<EOF
<html>
<body>
<p>Welcome to ${BREAKINGNEWS_TITLE}!</p>
<br/>
<p>Edit $bin_dir/breakingnews.html to customize this message, then restart worldserver.</p>
</body>
</html>
EOF
fi
fi
}
# ------------------------------------------------------------------------------
# Step: install Ollama locally (only if the user chose that option)
# ------------------------------------------------------------------------------
step_install_ollama_local() {
[ "$INSTALL_OLLAMA" = "1" ] && [ "$OLLAMA_LOCAL" = "1" ] || return 0
curl -fsSL https://ollama.com/install.sh | sh
sudo systemctl enable --now ollama
sleep 3
ollama pull "$OLLAMA_MODEL"
}
# ------------------------------------------------------------------------------
# Step: MySQL tuning
# ------------------------------------------------------------------------------
step_tune_mysql() {
local mysql_cnf="/etc/mysql/mysql.conf.d/mysqld.cnf"
local ram_gb
ram_gb=$(awk '/MemTotal/ {printf "%.0f", $2/1024/1024}' /proc/meminfo)
local buffer_pool_gb=$(( ram_gb / 2 ))
[ "$buffer_pool_gb" -lt 1 ] && buffer_pool_gb=1
if ! grep -q "# --- AzerothCore installer tuning ---" "$mysql_cnf" 2>/dev/null; then
sudo tee -a "$mysql_cnf" > /dev/null <<EOF
# --- AzerothCore installer tuning ---
[mysqld]
skip-log-bin
innodb_buffer_pool_size = ${buffer_pool_gb}G
innodb_io_capacity = 500
innodb_io_capacity_max = 2500
transaction_isolation = READ-COMMITTED
EOF
sudo systemctl restart mysql
sleep 3
fi
}
# ------------------------------------------------------------------------------
# Step: firewall
# ------------------------------------------------------------------------------
step_configure_firewall() {
[ "$CONFIGURE_FIREWALL" = "1" ] || return 0
command -v ufw >/dev/null 2>&1 || { warn "ufw not found, skipping firewall configuration."; return 0; }
sudo ufw allow 22/tcp comment 'SSH'
sudo ufw allow 3724/tcp comment 'AzerothCore auth'
sudo ufw allow 8085/tcp comment 'AzerothCore world'
sudo ufw --force enable
}
# ------------------------------------------------------------------------------
# Step: client data
# ------------------------------------------------------------------------------
step_client_data() {
cd "$INSTALL_DIR/azerothcore-wotlk"
local bin_dir="env/dist/bin"
if [ -f "$bin_dir/maps" ] || [ -d "$bin_dir/maps" ]; then
return 0
fi
if [ -x "bin/acore" ]; then
bin/acore client-data || warn "bin/acore client-data failed. Run it manually later: cd $INSTALL_DIR/azerothcore-wotlk && bin/acore client-data"
else
warn "bin/acore not found — fetch client data manually. See: https://github.com/wowgaming/client-data"
fi
}
# ------------------------------------------------------------------------------
# Step: systemd services
# ------------------------------------------------------------------------------
step_systemd_services() {
local bin_dir="$INSTALL_DIR/azerothcore-wotlk/env/dist/bin"
local user
user="$(whoami)"
sudo tee /etc/systemd/system/azerothcore-authserver.service > /dev/null <<EOF
[Unit]
Description=AzerothCore Auth Server
After=network.target mysql.service
Wants=mysql.service
[Service]
Type=simple
User=${user}
WorkingDirectory=${bin_dir}
ExecStart=${bin_dir}/authserver
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF
sudo tee /etc/systemd/system/azerothcore-worldserver.service > /dev/null <<EOF
[Unit]
Description=AzerothCore World Server
After=network.target mysql.service azerothcore-authserver.service
Wants=mysql.service
Requires=azerothcore-authserver.service
[Service]
Type=simple
User=${user}
WorkingDirectory=${bin_dir}
ExecStart=${bin_dir}/worldserver
Restart=on-failure
RestartSec=10
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable azerothcore-authserver
sudo systemctl enable azerothcore-worldserver
}
# ------------------------------------------------------------------------------
# Step: start services and wait for worldserver to come fully up
# ------------------------------------------------------------------------------
step_start_services() {
sudo systemctl restart azerothcore-authserver
sudo systemctl restart azerothcore-worldserver
log "Waiting for worldserver to finish loading (this can take a few minutes on first boot)..."
local waited=0
local max_wait=900 # 15 minutes ceiling
while [ "$waited" -lt "$max_wait" ]; do
if sudo journalctl -u azerothcore-worldserver --no-pager 2>/dev/null | grep -q "Update time diff"; then
info "worldserver is up and running its main loop."
return 0
fi
if sudo systemctl is-failed --quiet azerothcore-worldserver; then
fail "worldserver service failed to start. Check: sudo journalctl -u azerothcore-worldserver -n 100 --no-pager"
fi
sleep 5
waited=$((waited + 5))
done
warn "worldserver did not report a fully-loaded state within ${max_wait}s. It may still be starting — check: sudo journalctl -u azerothcore-worldserver -f"
}
# ------------------------------------------------------------------------------
# Step: create the GM account
#
# This is the one step that genuinely can't be done with a simple SQL
# INSERT — AzerothCore stores account credentials as an SRP6 salt+verifier
# pair, not a plain hash, so account creation has to go through the
# worldserver console command itself. Since the server is already running
# as a systemd service (no attached console), this step briefly stops the
# service, runs worldserver in the foreground under `expect` so we can
# type the account-creation commands the moment the console is ready, then
# hands control back to systemd.
#
# If this step fails for any reason, it does NOT fail the whole install —
# it warns and leaves clear manual instructions in the summary file,
# since account creation only takes a few seconds to do by hand.
# ------------------------------------------------------------------------------
step_create_gm_account() {
local bin_dir="$INSTALL_DIR/azerothcore-wotlk/env/dist/bin"
sudo systemctl stop azerothcore-worldserver
sleep 2
local expect_script
expect_script=$(mktemp)
cat > "$expect_script" <<EXPECT_EOF
set timeout 600
cd "${bin_dir}"
spawn ./worldserver
expect {
"<Ctrl-C> to stop" { }
timeout { puts "TIMEOUT_WAITING_FOR_READY"; exit 1 }
}
# Give the command processor a moment to fully attach after the banner.
sleep 15
send "account create ${GM_ACCOUNT_NAME} ${GM_ACCOUNT_PASSWORD}\r"
sleep 3
send "account set gmlevel ${GM_ACCOUNT_NAME} 3 -1\r"
sleep 3
send "\003"
expect eof
EXPECT_EOF
if expect "$expect_script"; then
GM_ACCOUNT_CREATED="1"
else
GM_ACCOUNT_CREATED="0"
warn "Automatic GM account creation did not complete cleanly. You can create it by hand — instructions are in $SUMMARY_FILE."
fi
rm -f "$expect_script"
sudo systemctl start azerothcore-worldserver
}
# ------------------------------------------------------------------------------
# Step: generate helper scripts (restart, SOAP)
# ------------------------------------------------------------------------------
step_generate_helper_scripts() {
cat > "$INSTALL_DIR/restart-servers.sh" <<'EOF'
#!/usr/bin/env bash
# Restart AzerothCore services after a config change.
# Usage: ./restart-servers.sh [auth|world|both]
set -e
TARGET="${1:-both}"
case "$TARGET" in
auth) sudo systemctl restart azerothcore-authserver ;;
world) sudo systemctl restart azerothcore-worldserver ;;
both) sudo systemctl restart azerothcore-authserver
sudo systemctl restart azerothcore-worldserver ;;
*) echo "Usage: $0 [auth|world|both]"; exit 1 ;;
esac
sleep 3
echo "--- Status ---"
sudo systemctl status azerothcore-authserver --no-pager | head -5
echo ""
sudo systemctl status azerothcore-worldserver --no-pager | head -5
echo ""
echo "Tailing worldserver logs (Ctrl+C to stop watching, server keeps running)..."
sudo journalctl -u azerothcore-worldserver -f
EOF
chmod +x "$INSTALL_DIR/restart-servers.sh"
if [ "$SOAP_ENABLED" = "1" ]; then
cat > "$INSTALL_DIR/soap-cmd.sh" <<EOF
#!/usr/bin/env bash
# Send a single worldserver console command via SOAP.
# Usage: ./soap-cmd.sh "server info"
SOAP_USER="${GM_ACCOUNT_NAME}"
SOAP_PASS="${GM_ACCOUNT_PASSWORD}"
SOAP_HOST="127.0.0.1"
SOAP_PORT="7878"
CMD="\$1"
if [ -z "\$CMD" ]; then
echo "Usage: \$0 \"<worldserver command>\""
exit 1
fi
curl -s -u "\${SOAP_USER}:\${SOAP_PASS}" \\
-H "Content-Type: text/xml" \\
-d "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV=\\"http://schemas.xmlsoap.org/soap/envelope/\\">
<SOAP-ENV:Body>
<ns1:executeCommand xmlns:ns1=\\"urn:AC\\">
<command>\${CMD}</command>
</ns1:executeCommand>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>" \\
"http://\${SOAP_HOST}:\${SOAP_PORT}/"
echo ""
EOF
chmod 700 "$INSTALL_DIR/soap-cmd.sh"
fi
}
# ------------------------------------------------------------------------------
# Step: write the summary file
# ------------------------------------------------------------------------------
step_write_summary() {
local etc_dir="$INSTALL_DIR/azerothcore-wotlk/env/dist/etc"
local public_ip
public_ip=$(curl -s ifconfig.me 2>/dev/null || echo "<could not detect — run: curl ifconfig.me>")
local local_ip
local_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
{
echo "=============================================================="
echo " AzerothCore + Playerbots — Install Summary"
echo " Generated: $(date)"
echo "=============================================================="
echo
echo "KEEP THIS FILE PRIVATE — it contains real passwords."
echo " chmod 600 $SUMMARY_FILE (already applied)"
echo
echo "--- Server ---"
echo "Install directory: $INSTALL_DIR/azerothcore-wotlk"
echo "Local IP: ${local_ip:-unknown}"
echo "Public IP (best guess): $public_ip"
echo "Realm name: $REALM_NAME"
echo
echo "--- MySQL ---"
echo "User: acore"
echo "Password: $ACORE_DB_PASSWORD"
echo "Databases: acore_auth, acore_characters, acore_world"
[ "$INSTALL_PLAYERBOTS" = "1" ] && echo " acore_playerbots"
echo
echo "--- GM account ---"
echo "Username: $GM_ACCOUNT_NAME"
echo "Password: $GM_ACCOUNT_PASSWORD"
echo "GM level: 3"
if [ "${GM_ACCOUNT_CREATED:-0}" = "0" ]; then
echo
echo " !! Automatic account creation did NOT complete. Create it by hand:"
echo " sudo systemctl stop azerothcore-worldserver"
echo " cd $INSTALL_DIR/azerothcore-wotlk/env/dist/bin && ./worldserver"
echo " (wait for it to fully load, then type:)"
echo " account create $GM_ACCOUNT_NAME $GM_ACCOUNT_PASSWORD"
echo " account set gmlevel $GM_ACCOUNT_NAME 3 -1"
echo " (Ctrl+C, then:)"
echo " sudo systemctl start azerothcore-worldserver"
fi
echo
echo "--- Client setup ---"
echo "In realmlist.wtf: set realmlist ${public_ip}"
echo " (use the local IP instead if connecting from the same LAN)"
echo
echo "--- Realm DB row (fix if the address above is wrong for your network) ---"
echo " mysql -u acore -p acore_auth -e \"UPDATE realmlist SET address = 'YOUR_IP' WHERE id = 1;\""
echo
echo "--- Services ---"
echo " sudo systemctl status azerothcore-authserver"
echo " sudo systemctl status azerothcore-worldserver"
echo " sudo journalctl -u azerothcore-worldserver -f"
echo " $INSTALL_DIR/restart-servers.sh [auth|world|both]"
echo
echo "--- Modules installed ---"
[ "$INSTALL_PLAYERBOTS" = "1" ] && echo " - Playerbots (min $MIN_RANDOM_BOTS / max $MAX_RANDOM_BOTS bots)"
[ "$INSTALL_AHBOT" = "1" ] && echo " - AH Bot"
[ "$INSTALL_OLLAMA" = "1" ] && echo " - Ollama Chat (endpoint: $OLLAMA_ENDPOINT, model: $OLLAMA_MODEL)"
[ "$INSTALL_QUESTPARTY" = "1" ] && echo " - Quest Loot Party"
[ "$INSTALL_BREAKINGNEWS" = "1" ] && echo " - Breaking News Override (edit: $INSTALL_DIR/azerothcore-wotlk/env/dist/bin/breakingnews.html)"
echo
if [ "$INSTALL_AHBOT" = "1" ]; then
echo "--- AH Bot: manual follow-up required ---"
echo " 1. Create a dedicated account for the bot to use:"
echo " (via SOAP, once GM account works) ./soap-cmd.sh \"account create ahbot <a password>\""
echo " 2. Log into the client ONCE with that account and create a throwaway"
echo " character — AH Bot needs a real character to act through, and this"
echo " step genuinely requires a client login (can't be scripted headlessly)."
echo " 3. Find its account ID:"
echo " mysql -u acore -p acore_auth -e \"SELECT id, username FROM account WHERE username='AHBOT';\""
echo " 4. Put that ID into:"
echo " $etc_dir/modules/mod_ahbot.conf"
echo " 5. Give the character starting gold (money is stored in copper, 1g = 10000):"
echo " mysql -u acore -p acore_characters -e \"UPDATE characters SET money = 10000000 WHERE account = <ID>;\""
echo " 6. $INSTALL_DIR/restart-servers.sh world"
echo
fi
echo "--- Config file locations ---"
echo " $etc_dir/authserver.conf"
echo " $etc_dir/worldserver.conf"
echo " $etc_dir/modules/ (per-module .conf files)"
echo
echo "--- Client data ---"
echo " Downloaded via 'bin/acore client-data' into env/dist/bin/"
echo " (maps, vmaps, mmaps, dbc — pre-extracted, sourced from"
echo " github.com/wowgaming/client-data)"
echo
if [ "$SOAP_ENABLED" = "1" ]; then
echo "--- SOAP remote admin ---"
echo " Disabled by default in worldserver.conf (SOAP.Enabled = 0)."
echo " To enable: set SOAP.Enabled = 1 and SOAP.IP as needed in worldserver.conf,"
echo " restart worldserver, then use: $INSTALL_DIR/soap-cmd.sh \"<command>\""
echo " soap-cmd.sh contains your GM password in plaintext — keep it private"
echo " (already chmod 700)."
echo
fi
echo "=============================================================="
} > "$SUMMARY_FILE"
chmod 600 "$SUMMARY_FILE"
}
# ------------------------------------------------------------------------------
# main
# ------------------------------------------------------------------------------
main() {
SOAP_ENABLED="1" # helper script is always generated; SOAP itself stays
# disabled in worldserver.conf until the user opts in
# (see summary file) — safer default than exposing
# an admin interface automatically.
preflight_checks
# Configuration is re-collected on every run (it only takes a minute
# to answer) since shell variables don't persist between invocations.
# Completed install STEPS are still skipped via the state directory
# markers below, regardless of re-answering these questions the same
# way — so re-running after a partial failure is still fast.
collect_config
run_step "dependencies" step_install_dependencies
run_step "mysql_user" step_setup_mysql
run_step "clone_source" step_clone_source
run_step "build" step_build
run_step "import_databases" step_import_databases
run_step "configure_core" step_configure_core
run_step "configure_modules" step_configure_modules
run_step "install_ollama_local" step_install_ollama_local
run_step "tune_mysql" step_tune_mysql
run_step "firewall" step_configure_firewall
run_step "client_data" step_client_data
run_step "systemd_services" step_systemd_services
run_step "start_services" step_start_services
run_step "gm_account" step_create_gm_account
run_step "helper_scripts" step_generate_helper_scripts
step_write_summary # always regenerate — cheap, and reflects latest state
log "Install complete."
echo "Summary written to: $SUMMARY_FILE (chmod 600 — contains real passwords)"
echo
echo "Quick checks:"
echo " sudo systemctl status azerothcore-worldserver"
echo " sudo journalctl -u azerothcore-worldserver -f"
echo
echo "Read $SUMMARY_FILE for connection details, the GM account, and any"
echo "manual follow-up steps (AH Bot needs one)."
}
main "$@"