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
This commit is contained in:
AzerothCore Installer 2026-08-21 14:54:40 +00:00
commit 67b9b2dde5
4 changed files with 1168 additions and 0 deletions

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
# Generated at install time — never commit real credentials or install state
azerothcore-server-info.txt
.azerothcore-installer-state/
# OS/editor noise
.DS_Store
*.swp
*~

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

206
README.md Normal file
View file

@ -0,0 +1,206 @@
# AzerothCore + Playerbots Installer
A single script that takes a clean Ubuntu Server VM to a running AzerothCore
3.3.5a (Wrath of the Lich King) private server with the [Playerbots][playerbots]
fork, plus a handful of optional community modules — fully unattended after
you answer a short set of setup questions.
[playerbots]: https://github.com/mod-playerbots/azerothcore-wotlk
## What it installs
- **AzerothCore**, built from the [`mod-playerbots/azerothcore-wotlk`][pb-core]
fork (`Playerbot` branch) — required for Playerbots support; the standard
AzerothCore repo will not compile the module.
- **mod-playerbots** — populates the world with AI-controlled bot characters
you can group with, fight alongside, or just watch go about their business.
- **mod-ah-bot** *(optional)* — keeps the auction house stocked and active.
- **mod-ollama-chat** *(optional)* — gives bots LLM-generated chat via an
[Ollama](https://ollama.com) server, either installed locally by this
script or pointed at one you already run elsewhere on your network.
- **mod-quest-loot-party** *(optional)* — lets every eligible party member
receive a quest item drop, not just whoever loots first.
- **mod-breaking-news-override** *(optional)* — an announcement panel on the
character-select screen, driven by a small HTML file you can edit later.
It also sets up:
- `authserver` / `worldserver` as **systemd services** (auto-start on boot,
auto-restart on crash)
- Basic **MySQL tuning** sized to the VM's RAM
- **ufw firewall rules** for the game ports (optional, on by default)
- **Client data** (maps/vmaps/mmaps/dbc), downloaded pre-extracted via
AzerothCore's own `bin/acore client-data` command — no WoW client needed
for this step
- A **GM (admin) account**, created automatically
- Helper scripts: `restart-servers.sh` and (if you want it later) `soap-cmd.sh`
for remote console commands
- A **summary file** in your home directory with every credential, path,
and next step you'll need
[pb-core]: https://github.com/mod-playerbots/azerothcore-wotlk
## Requirements
- **Ubuntu Server 24.04** (other Ubuntu versions will likely work; nothing
else is tested)
- A regular user with **sudo** access (not root)
- Recommended VM spec: **8 vCPU / 16GB RAM / 60GB+ disk** — see
[Hardware sizing](#hardware-sizing) below for smaller/larger setups
- Outbound internet access (cloning repos, `apt`, downloading client data)
## Usage
```bash
git clone <this-repo-url>
cd azerothcore-playerbots-installer
chmod +x install.sh
```
**Run it inside `screen` or `tmux`.** The compile step alone can take
3090+ minutes; if your SSH session drops, a bare foreground script dies
with it.
```bash
screen -S install
./install.sh
```
You'll be asked for:
- A MySQL password for the database user the server will use
- Build/thread counts (sensible defaults are suggested based on your CPU)
- Which optional modules to install
- Playerbots population size (min/max bots)
- Ollama connection details, if you want bot chat
- A GM account username/password
- Whether to open the game ports in `ufw`
Then it runs unattended. Detach from `screen` any time with `Ctrl+A`, `D`,
and reattach later with `screen -r install`.
When it finishes, everything you need is in:
```
~/azerothcore-server-info.txt
```
This file is `chmod 600`'d automatically since it contains real passwords.
**Treat it like a password file** — don't copy it somewhere world-readable.
## Resuming after a failure
Every step records its own completion in `~/.azerothcore-installer-state/`.
If the script dies partway through (compiler error, disconnected SSH,
out of disk, etc.), fix the underlying problem and just run it again:
```bash
./install.sh
```
Completed steps are skipped; the run picks up wherever it left off. You'll
be asked the setup questions again each time (they're cheap to answer), but
slow steps like the build or client-data download won't redo unnecessarily.
To force a specific step to redo, delete its marker:
```bash
rm ~/.azerothcore-installer-state/build
./install.sh
```
## Hardware sizing
| Bot count / use case | vCPU | RAM | Disk |
|---|---|---|---|
| Solo / small friends group, ~50-100 bots | 4 | 8GB | 40GB |
| Medium, ~200-300 bots | 6-8 | 16GB | 60GB |
| Large bot population / real player traffic | 8+ | 16-24GB | 80GB+ |
`worldserver`'s main loop is heavily single-core-performance dependent —
a modern CPU with fewer, faster cores tends to outperform more, slower
cores. `MapUpdate.Threads` (set automatically from your detected core
count, adjustable) generally shouldn't exceed cores minus 2.
## After install: things worth doing
**Confirm the realm address is right for how people will connect.** The
summary file fills in your best-guess public IP, but if you're behind a
home router (NAT), players outside your LAN need port forwarding for
3724/8085 on the router itself, and the `realmlist` table's `address`
needs to be the address actually reachable from where they're connecting
from:
```bash
mysql -u acore -p acore_auth -e "UPDATE realmlist SET address = 'YOUR_IP' WHERE id = 1;"
```
**AH Bot needs one manual step.** Because its buyer/seller acts through a
real character, and character creation requires an actual client login,
this can't be scripted headlessly. The summary file has the exact steps —
short version: create an account for it, log into the client once to make
a throwaway character, then point `mod_ahbot.conf` at that account's ID
and give the character some starting gold.
**SOAP remote admin is installed but disabled by default.** The
`soap-cmd.sh` helper script is generated either way, but `SOAP.Enabled`
is left at `0` in `worldserver.conf` — enabling a remote admin interface
by default felt like the wrong call for an unattended script to make on
your behalf. Flip it on in `worldserver.conf` when you actually want it,
and restrict the port at your firewall to trusted IPs rather than opening
it broadly (SOAP auth sends credentials unencrypted over plain HTTP).
**Consider restricting SSH, and MySQL/SOAP if you enable them, to known
IPs** rather than "Anywhere" in `ufw`, especially if this VM has a public
IP. The installer only opens the two game ports by default for this
reason.
## Why some things work the way they do
A few non-obvious lessons are baked into this script, worth knowing if
you're debugging or extending it:
- **`-DTOOLS_BUILD=db-only`, never `none`.** AzerothCore's `dbimport` tool
is bundled under the same cmake flag as the map/vmap/mmap extractors.
Setting it to `none` to skip the (slow, client-required) extractors also
silently skips building `dbimport` itself — which then makes the
database-import step fail with a confusing missing-file error several
steps later, with no obvious link back to the build flag. `db-only`
builds just the database tool; client data comes from `bin/acore
client-data` instead (pre-extracted, no client needed).
- **Module SQL is never imported by hand.** Every module here relies on
AzerothCore's own auto-updater to apply its SQL on first `worldserver`
start, using a `updates` tracking table it manages itself. Manually
running a module's `.sql` files against MySQL — even when a module's own
README suggests it — desyncs that tracking table from actual database
state, which shows up later as baffling "table already exists" or
"table doesn't exist" crashes. Let the updater own its own bookkeeping.
- **Module config filenames are not consistent.** Some use hyphens
(`mod-quest-loot-party.conf`), some underscores (`mod_ahbot.conf`,
`mod_ollama_chat.conf`), one is just lowercase with neither
(`breakingnews.conf`) — and none of these necessarily match the module's
repository name. The filenames in this script were confirmed against
real installs; if a module updates and renames its config, the relevant
step will need updating too.
- **GM account creation needs the console, not raw SQL.** Account
passwords are stored as an SRP6 salt+verifier pair, not a simple hash,
so there's no safe direct-SQL way to create one. This script briefly
stops the systemd-managed `worldserver`, drives an interactive console
session via `expect`, and hands control back to systemd afterward.
## What this script won't do for you
- **Won't source a WoW client.** Not needed anyway — client data comes
pre-extracted via `bin/acore client-data`.
- **Won't create the AH Bot's character** — see above, needs a real
client login.
- **Won't harden the VM beyond basic firewall rules** — no fail2ban,
no automatic security updates, no TLS in front of SOAP. Reasonable
next steps for anything facing the open internet.
- **Won't set up backups.** Worth adding a periodic `mysqldump` of all
databases once the server's running — bot activity and real player
progress both live only in MySQL.
## License
MIT — see [LICENSE](LICENSE).

933
install.sh Executable file
View file

@ -0,0 +1,933 @@
#!/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 "$@"