Files
AzerothAI/deploy/playable/enable-autonomous-world.sh
T
Richard Clark 5f0fc86cf9
Core tests / core (push) Successful in 9m57s
Full verification / application (push) Failing after 5m16s
Full verification / local-brain (push) Failing after 6m15s
Realm simulation / simulated-live-world (push) Failing after 5m32s
WoW integration lab / real-worldserver (push) Canceled after 17m23s
Import verified AzerothAI snapshot from GitHub
Source branch: feature/ai-brain-router
Source commit: c2a09e0460465201a58aaab56c71424e9a96405e
2026-09-22 13:12:54 -04:00

411 lines
17 KiB
Bash

#!/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"