#!/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 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 -- 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 # 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 <> playerbots.conf <> playerbots.conf < "$bin_dir/breakingnews.html" <

Welcome to ${BREAKINGNEWS_TITLE}!


Edit $bin_dir/breakingnews.html to customize this message, then restart worldserver.

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 </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 < /dev/null </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" < 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" <\"" exit 1 fi curl -s -u "\${SOAP_USER}:\${SOAP_PASS}" \\ -H "Content-Type: text/xml" \\ -d " \${CMD} " \\ "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 "") 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 \"" 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 = ;\"" 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 \"\"" 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 "$@"