#!/usr/bin/env bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" BOT_COUNT="${AZEROTHAI_RANDOM_BOTS:-10}" BOT_ACCOUNT_COUNT="${AZEROTHAI_RANDOM_BOT_ACCOUNTS:-30}" if [[ ! "$BOT_COUNT" =~ ^[0-9]+$ ]] || (( BOT_COUNT < 1 || BOT_COUNT > 60 )); then echo "AZEROTHAI_RANDOM_BOTS must be an integer from 1 to 60." >&2 exit 2 fi if [[ ! "$BOT_ACCOUNT_COUNT" =~ ^[0-9]+$ ]] || (( BOT_ACCOUNT_COUNT < 1 || BOT_ACCOUNT_COUNT > 100 )); then echo "AZEROTHAI_RANDOM_BOT_ACCOUNTS must be an integer from 1 to 100." >&2 exit 2 fi find_lab_root(){ if [[ -n "${AZEROTHAI_PLAYABLE_ROOT:-}" && -d "$AZEROTHAI_PLAYABLE_ROOT/azerothcore" ]]; then printf '%s' "$AZEROTHAI_PLAYABLE_ROOT"; return fi for root in "$HOME/azerothai-pc-lab" "$HOME/azerothai-playable"; do if [[ -d "$root/azerothcore" ]]; then printf '%s' "$root"; return; fi done } LAB_ROOT="$(find_lab_root || true)" if [[ -z "$LAB_ROOT" ]]; then echo "Could not find the playable AzerothCore lab. Set AZEROTHAI_PLAYABLE_ROOT first." >&2 exit 3 fi ACORE_ROOT="$LAB_ROOT/azerothcore" RUNTIME_ENV="$LAB_ROOT/runtime.env" DEV_VARS="$REPO_ROOT/.dev.vars" PLAYERBOTS_CONF="" need(){ command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }; } need docker need curl need jq need openssl need python3 need pnpm cd "$ACORE_ROOT" docker compose version >/dev/null if [[ ! -f "$RUNTIME_ENV" ]]; then echo "Missing $RUNTIME_ENV. Run the playable bootstrap first." >&2 exit 4 fi # shellcheck disable=SC1090 source "$RUNTIME_ENV" : "${ALE_BRIDGE_TOKEN:?Missing ALE_BRIDGE_TOKEN}" : "${ALE_OPERATOR_TOKEN:?Missing ALE_OPERATOR_TOKEN}" : "${DOCKER_DB_ROOT_PASSWORD:?Missing DOCKER_DB_ROOT_PASSWORD}" echo "==> Running fail-fast preflight" AZEROTHAI_PLAYABLE_ROOT="$LAB_ROOT" bash "$REPO_ROOT/deploy/playable/preflight.sh" if [[ -z "${AUTONOMY_TOKEN:-}" ]]; then AUTONOMY_TOKEN="$(openssl rand -hex 32)" printf '\nAUTONOMY_TOKEN=%s\n' "$AUTONOMY_TOKEN" >> "$RUNTIME_ENV" chmod 600 "$RUNTIME_ENV" fi echo "==> Updating AzerothAI private runtime token" python3 - "$DEV_VARS" "$AUTONOMY_TOKEN" <<'PY' from pathlib import Path import sys p=Path(sys.argv[1]); token=sys.argv[2] lines=p.read_text().splitlines() if p.exists() else [] out=[]; seen=False for line in lines: if line.startswith("AUTONOMY_TOKEN="): out.append("AUTONOMY_TOKEN="+token); seen=True else: out.append(line) if not seen: out.append("AUTONOMY_TOKEN="+token) p.write_text("\n".join(out).rstrip()+"\n") PY chmod 600 "$DEV_VARS" echo "==> Installing latest AzerothAI ALE bridge" mkdir -p "$ACORE_ROOT/azerothai-lua" cp "$REPO_ROOT/ale/azerothai_bridge.lua" "$ACORE_ROOT/azerothai-lua/azerothai_bridge.lua" python3 - "$ACORE_ROOT/azerothai-lua/azerothai_bridge.lua" <<'PY' from pathlib import Path import sys p=Path(sys.argv[1]) s=p.read_text() s=s.replace('local POLL_MS = tonumber(GetConfigValue("AzerothAI.PollMs")) or 750','local POLL_MS = 750') s=s.replace('local PROTECTED = GetConfigValue("AzerothAI.ProtectedCharacter")','local PROTECTED = "Zaku"') p.write_text(s) PY echo "==> Enabling native Playerbots that stay alive without a human online" python3 - "$ACORE_ROOT/docker-compose.override.yml" "$BOT_COUNT" "$BOT_ACCOUNT_COUNT" <<'PY' from pathlib import Path import re,sys p=Path(sys.argv[1]); count=sys.argv[2]; accounts=sys.argv[3] s=p.read_text() replacements={ "AC_AI_PLAYERBOT_ENABLED":'"1"', "AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN":'"1"', "AC_AI_PLAYERBOT_MIN_RANDOM_BOTS":f'"{count}"', "AC_AI_PLAYERBOT_MAX_RANDOM_BOTS":f'"{count}"', "AC_AI_PLAYERBOT_RANDOM_BOT_ACCOUNT_COUNT":f'"{accounts}"', "AC_AI_PLAYERBOT_DISABLED_WITHOUT_REAL_PLAYER":'"0"', "AC_AI_PLAYERBOT_BOT_ACTIVE_ALONE":'"100"', "AC_AI_PLAYERBOT_BOT_ACTIVE_ALONE_SMART_SCALE":'"0"', "AC_AI_PLAYERBOT_RANDOM_BOTS_PER_INTERVAL":'"12"', "AC_AI_PLAYERBOT_AUTO_DO_QUESTS":'"1"', "AC_AI_PLAYERBOT_ENABLE_NEW_RPG_STRATEGY":'"1"', "AC_AI_PLAYERBOT_MIN_RANDOM_BOT_RANDOMIZE_TIME":'"31104000"', "AC_AI_PLAYERBOT_MAX_RANDOM_BOT_RANDOMIZE_TIME":'"31104000"', "AC_AI_PLAYERBOT_MIN_RANDOM_BOT_TELEPORT_INTERVAL":'"31104000"', "AC_AI_PLAYERBOT_MAX_RANDOM_BOT_TELEPORT_INTERVAL":'"31104000"', "AC_AI_PLAYERBOT_PERMANENTLY_IN_WORLD_TIME":'"31104000"', "AC_AI_PLAYERBOT_RANDOM_BOT_TALK":'"0"', "AC_AI_PLAYERBOT_RANDOM_BOT_EMOTE":'"0"', "AC_AI_PLAYERBOT_RANDOM_BOT_SAY_WITHOUT_MASTER":'"0"', "AC_AI_PLAYERBOT_COMMAND_PREFIX":'"!"', "AC_AI_PLAYERBOT_RANDOM_BOT_JOIN_LFG":'"1"', } lines=s.splitlines() present=set() out=[] for line in lines: m=re.match(r'(\s*)(AC_[A-Z0-9_]+):\s*.*$',line) if m and m.group(2) in replacements: key=m.group(2); out.append(f'{m.group(1)}{key}: {replacements[key]}'); present.add(key) else: out.append(line) # Add missing settings to the ac-worldserver environment block directly after the known autologin line. for key,value in replacements.items(): if key not in present: inserted=False for i,line in enumerate(out): if 'AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN:' in line: indent=line[:len(line)-len(line.lstrip())] out.insert(i+1,f'{indent}{key}: {value}') inserted=True break if not inserted: raise SystemExit(f'Could not locate ac-worldserver Playerbot environment block for {key}') p.write_text("\n".join(out)+"\n") PY # AzerothCore itself maps AC_* environment variables to config keys. If a # writable module config already exists, mirror the same settings there for # operator readability, but do not require a particular module-config layout. for candidate in "$LAB_ROOT/etc/modules/playerbots.conf" "$LAB_ROOT/etc/playerbots.conf"; do if [[ -f "$candidate" ]]; then PLAYERBOTS_CONF="$candidate"; break; fi done if [[ -n "$PLAYERBOTS_CONF" ]]; then echo "==> Mirroring autonomous settings into $PLAYERBOTS_CONF" python3 - "$PLAYERBOTS_CONF" "$BOT_COUNT" "$BOT_ACCOUNT_COUNT" <<'PY' from pathlib import Path import re,sys p=Path(sys.argv[1]); count=sys.argv[2]; accounts=sys.argv[3] updates={ "AiPlayerbot.Enabled":"1", "AiPlayerbot.RandomBotAutologin":"1", "AiPlayerbot.MinRandomBots":count, "AiPlayerbot.MaxRandomBots":count, "AiPlayerbot.RandomBotAccountCount":accounts, "AiPlayerbot.DisabledWithoutRealPlayer":"0", "AiPlayerbot.BotActiveAlone":"100", "AiPlayerbot.botActiveAloneSmartScale":"0", "AiPlayerbot.RandomBotsPerInterval":"12", "AiPlayerbot.AutoDoQuests":"1", "AiPlayerbot.EnableNewRpgStrategy":"1", "AiPlayerbot.MinRandomBotRandomizeTime":"31104000", "AiPlayerbot.MaxRandomBotRandomizeTime":"31104000", "AiPlayerbot.MinRandomBotTeleportInterval":"31104000", "AiPlayerbot.MaxRandomBotTeleportInterval":"31104000", "AiPlayerbot.PermanentlyInWorldTime":"31104000", "AiPlayerbot.RandomBotTalk":"0", "AiPlayerbot.RandomBotEmote":"0", "AiPlayerbot.RandomBotSayWithoutMaster":"0", "AiPlayerbot.CommandPrefix":"!", "AiPlayerbot.RandomBotJoinLfg":"1", "AiPlayerbot.EnablePeriodicOnlineOffline":"0", } lines=p.read_text().splitlines(); seen=set(); out=[] for line in lines: m=re.match(r'\s*([A-Za-z0-9_.]+)\s*=.*$',line) if m and m.group(1) in updates: key=m.group(1); out.append(f'{key} = {updates[key]}'); seen.add(key) else: out.append(line) for key,value in updates.items(): if key not in seen: out.append(f'{key} = {value}') p.write_text("\n".join(out)+"\n") PY else echo "==> No writable playerbots.conf found; using AzerothCore AC_* environment configuration" fi echo "==> Rebuilding/restarting AzerothAI with live autonomy endpoint" cd "$REPO_ROOT" pnpm build if [[ -f "$LAB_ROOT/azerothai.pid" ]]; then pid="$(cat "$LAB_ROOT/azerothai.pid" 2>/dev/null || true)" [[ -n "$pid" ]] && kill "$pid" 2>/dev/null || true fi pkill -f "wrangler.*8787" 2>/dev/null || true pkill -f "pnpm.*start:lan" 2>/dev/null || true sleep 2 nohup pnpm start:lan >"$LAB_ROOT/azerothai.log" 2>&1 & echo $! > "$LAB_ROOT/azerothai.pid" for _ in {1..90}; do curl -fsS http://127.0.0.1:8787/api/health >/dev/null 2>&1 && break sleep 1 done curl -fsS http://127.0.0.1:8787/api/health >/tmp/azerothai-autonomy-health.json echo "==> Starting durable ALE database relay" bash "$REPO_ROOT/deploy/playable/install-ale-relay-service.sh" if jq -e '.llmConfigured == true' /tmp/azerothai-autonomy-health.json >/dev/null 2>&1; then echo "==> Selecting Qwen/Ollama for foreground and hero high-level intentions" curl -fsS http://127.0.0.1:8787/api/state \ | jq '{type:"settings",connector:"azerothcore",provider:"ollama",model:(.settings.brains.residentModel // "qwen3.5:4b"),brains:(.settings.brains + {backgroundUseLLM:true}),hardcore:.settings.hardcore,population:.settings.population}' \ >/tmp/azerothai-autonomy-settings.json curl -fsS -X POST -H "Content-Type: application/json" --data-binary @/tmp/azerothai-autonomy-settings.json \ http://127.0.0.1:8787/api/actions >/dev/null fi echo "==> Restarting worldserver with autonomous Playerbot configuration" cd "$ACORE_ROOT" docker compose up -d --no-deps --force-recreate ac-worldserver for _ in {1..180}; do if curl -fsS http://127.0.0.1:8787/api/world-monitor 2>/dev/null | jq -e '.ale.bridgeReady == true' >/dev/null 2>&1; then break; fi sleep 2 done rndbot_accounts(){ docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse "SELECT COUNT(*) FROM acore_auth.account WHERE UPPER(username) LIKE 'RNDBOT%'" 2>/dev/null || echo 0 } RND_ACCOUNTS="$(rndbot_accounts)" if [[ "$RND_ACCOUNTS" =~ ^[0-9]+$ ]] && (( RND_ACCOUNTS == 0 )); then echo "==> Initializing the native random Playerbot pool for the first time" echo " This creates a persistent character pool; later runs preserve it." curl -fsS -X POST \ -H "Authorization: Bearer $ALE_OPERATOR_TOKEN" \ -H "Content-Type: application/json" \ --data '{"kind":"command","command":"playerbots rndbot init"}' \ http://127.0.0.1:8787/api/ale/operator >/tmp/azerothai-rndbot-init.json for _ in {1..240}; do RND_ACCOUNTS="$(rndbot_accounts)" [[ "$RND_ACCOUNTS" =~ ^[0-9]+$ ]] && (( RND_ACCOUNTS >= BOT_ACCOUNT_COUNT )) && break sleep 2 done if ! [[ "$RND_ACCOUNTS" =~ ^[0-9]+$ ]] || (( RND_ACCOUNTS == 0 )); then echo "Random bot initialization was queued but no RNDBOT accounts appeared yet." >&2 echo "Check: cd $ACORE_ROOT && docker compose logs --tail=200 ac-worldserver" >&2 exit 6 fi echo "Random bot accounts created: $RND_ACCOUNTS" fi echo "==> Teaching the ALE bridge which accounts are native Playerbots" BOT_ACCOUNT_IDS="$(docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse "SELECT id FROM acore_auth.account WHERE UPPER(username) LIKE 'RNDBOT%';" 2>/dev/null | tr '\n' ' ' | xargs || true)" if [[ -z "$BOT_ACCOUNT_IDS" ]]; then echo "Could not read RNDBOT account IDs for ALE bot classification." >&2 exit 6 fi python3 - "$ACORE_ROOT/azerothai-lua/azerothai_bridge.lua" "$BOT_ACCOUNT_IDS" <<'PY' from pathlib import Path import sys p=Path(sys.argv[1]) ids=[x for x in sys.argv[2].split() if x.isdigit()] table=", ".join(f"[{x}]=true" for x in ids) s=p.read_text() old="local KNOWN_BOT_ACCOUNTS = {}" new=f"local KNOWN_BOT_ACCOUNTS = {{{table}}}" if old not in s: raise SystemExit("ALE bridge bot-account marker not found") p.write_text(s.replace(old,new,1)) PY # Give Zaku a physical Undead Warlock body from the persistent random-bot pool. # Database identity work happens only while worldserver is stopped. EXISTING_ZAKU="$(docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse "SELECT CONCAT(c.guid,'|',c.account) FROM acore_characters.characters c WHERE LOWER(c.name)='zaku' LIMIT 1" 2>/dev/null || true)" if [[ -z "$EXISTING_ZAKU" ]]; then ZAKU_ROW="$(docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse "SELECT CONCAT(c.guid,'|',c.account,'|',c.name) FROM acore_characters.characters c JOIN acore_auth.account a ON a.id=c.account WHERE UPPER(a.username) LIKE 'RNDBOT%' AND c.race=5 AND c.class=9 ORDER BY c.guid LIMIT 1" 2>/dev/null || true)" if [[ -n "$ZAKU_ROW" ]]; then IFS='|' read -r ZAKU_GUID ZAKU_ACCOUNT ZAKU_OLD_NAME <<<"$ZAKU_ROW" echo "==> Binding persistent hero Zaku to a real Undead Warlock Playerbot" docker compose stop ac-worldserver >/dev/null docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" acore_characters -e "UPDATE characters SET name='Zaku' WHERE guid=$ZAKU_GUID;" docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" acore_playerbots -e "INSERT INTO playerbots_account_type (account_id,account_type) VALUES ($ZAKU_ACCOUNT,1) ON DUPLICATE KEY UPDATE account_type=1, assignment_date=CURRENT_TIMESTAMP;" echo "Zaku body: guid $ZAKU_GUID (renamed from $ZAKU_OLD_NAME)" else echo "No Undead Warlock candidate was found in the Playerbot pool; Zaku remains unbound until one exists." >&2 fi else echo "Zaku character already exists; preserving it." fi echo "==> Restarting worldserver so the autonomous pool can login" docker compose up -d --no-deps --force-recreate ac-worldserver echo "==> Waiting for autonomous Playerbots to be physically online" RND_ONLINE=0 for _ in {1..180}; do RND_ONLINE="$(docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse "SELECT COUNT(*) FROM acore_characters.characters c JOIN acore_auth.account a ON a.id=c.account WHERE c.online=1 AND UPPER(a.username) LIKE 'RNDBOT%'" 2>/dev/null || echo 0)" [[ "$RND_ONLINE" =~ ^[0-9]+$ ]] && (( RND_ONLINE > 0 )) && break sleep 2 done if ! [[ "$RND_ONLINE" =~ ^[0-9]+$ ]] || (( RND_ONLINE == 0 )); then echo "No autonomous Playerbots are physically online after restart." >&2 echo "Check: cd $ACORE_ROOT && docker compose logs --tail=250 ac-worldserver" >&2 exit 7 fi echo "Physical Playerbots online: $RND_ONLINE" echo "==> Starting persistent AzerothAI high-level autonomy service" bash "$REPO_ROOT/deploy/playable/install-autonomy-service.sh" echo "==> Waiting for authoritative Playerbot snapshots" OBSERVED=0 MANAGED=0 for _ in {1..180}; do data="$(curl -fsS http://127.0.0.1:8787/api/world-monitor 2>/dev/null || true)" if [[ -n "$data" ]]; then OBSERVED="$(printf '%s' "$data" | jq -r '.playerbots.observedBots // 0' 2>/dev/null || echo 0)" fi if [[ "$OBSERVED" =~ ^[0-9]+$ ]] && (( OBSERVED > 0 )); then sleep 2 MANAGED="$(curl -fsS http://127.0.0.1:8787/api/state | jq '[.bots[] | select(.autonomy.managed == true)] | length' 2>/dev/null || echo 0)" break fi sleep 2 done if ! [[ "$OBSERVED" =~ ^[0-9]+$ ]] || (( OBSERVED == 0 )); then echo "Playerbots are online, but AzerothAI received no authoritative Playerbot snapshots." >&2 echo "Check ALE bridge/playerbot bindings in the worldserver log." >&2 exit 8 fi if ! [[ "$MANAGED" =~ ^[0-9]+$ ]] || (( MANAGED == 0 )); then echo "AzerothAI received Playerbot events but did not adopt any managed live residents." >&2 exit 9 fi echo "==> Verifying ALE command round trip through the durable relay" VERIFY_RESPONSE="$(curl -fsS -X POST \ -H "Authorization: Bearer $ALE_OPERATOR_TOKEN" \ -H "Content-Type: application/json" \ --data '{"kind":"global","method":"GetPlayerCount","args":[]}' \ http://127.0.0.1:8787/api/ale/operator)" VERIFY_COMMAND_ID="$(printf '%s' "$VERIFY_RESPONSE" | jq -r '.command.id // empty')" if [[ -z "$VERIFY_COMMAND_ID" ]]; then echo "Could not queue ALE verification command." >&2 printf '%s\n' "$VERIFY_RESPONSE" >&2 exit 10 fi VERIFY_STATUS="" for _ in {1..120}; do VERIFY_STATUS="$(curl -fsS -H "Authorization: Bearer $ALE_OPERATOR_TOKEN" http://127.0.0.1:8787/api/ale/status \ | jq -r --arg id "$VERIFY_COMMAND_ID" '.commands[]? | select(.id==$id) | .status' | head -n1)" if [[ "$VERIFY_STATUS" == "completed" ]]; then break; fi if [[ "$VERIFY_STATUS" == "failed" ]]; then echo "ALE verification command failed." >&2 curl -fsS -H "Authorization: Bearer $ALE_OPERATOR_TOKEN" http://127.0.0.1:8787/api/ale/status \ | jq -r --arg id "$VERIFY_COMMAND_ID" '.commands[]? | select(.id==$id)' >&2 || true exit 10 fi sleep 1 done if [[ "$VERIFY_STATUS" != "completed" ]]; then echo "ALE command round trip did not complete within 120 seconds." >&2 echo "Relay service:" >&2 sudo systemctl status azerothai-ale-relay.service --no-pager -l >&2 || true echo "Relay log:" >&2 tail -80 "$LAB_ROOT/ale-relay.log" >&2 || true echo "Spool counts:" >&2 docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse \ "SELECT 'commands',COUNT(*) FROM acore_auth.azerothai_command_inbox WHERE consumed_at IS NULL UNION ALL SELECT 'events',COUNT(*) FROM acore_auth.azerothai_event_outbox WHERE delivered_at IS NULL UNION ALL SELECT 'results',COUNT(*) FROM acore_auth.azerothai_result_outbox WHERE delivered_at IS NULL" >&2 || true exit 10 fi echo echo "============================================================" echo " AUTONOMOUS AZEROTHAI WORLD ENABLED" echo "============================================================" echo "Target native Playerbots : $BOT_COUNT" echo "Physical bots online : $RND_ONLINE" echo "RNDBOT accounts : $RND_ACCOUNTS" echo "Authoritative bot events : $OBSERVED" echo "Managed AzerothAI lives : $MANAGED" echo "ALE command round trip : completed" echo "Autonomy worker PID : $(cat "$LAB_ROOT/autonomy.pid")" if sudo systemctl is-active --quiet azerothai-ale-relay.service 2>/dev/null; then echo "ALE transport : durable DB relay active" else echo "ALE transport : relay NOT active" fi echo echo "The native Playerbot AI now keeps playing with no human logged in." echo "AzerothAI adds periodic identity/personality/memory-driven high-level intentions" echo "only for Playerbots that are authoritatively observed in the live realm." echo echo "Watch:" echo " tail -f $LAB_ROOT/autonomy.log" echo " bash $REPO_ROOT/deploy/playable/status.sh"