- 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
933 lines
37 KiB
Bash
Executable file
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 "$@"
|