Import verified AzerothAI snapshot from GitHub
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

Source branch: feature/ai-brain-router
Source commit: c2a09e0460465201a58aaab56c71424e9a96405e
This commit is contained in:
Richard Clark
2026-09-22 13:12:54 -04:00
commit 5f0fc86cf9
175 changed files with 25807 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
# Playable AzerothAI Realm
This profile turns the current prototype into a realm you can connect a **WoW 3.3.5a client** to.
It uses:
- the `mod-playerbots/azerothcore-wotlk` **Playerbot** branch
- `mod-playerbots`
- `mod-ale`
- AzerothAI's ALE bridge
- MySQL
- AzerothAI's local dashboard/runtime
- optional Ollama + `qwen3.5:4b`
The server exposes only the standard WoW ports needed by the client:
- TCP 3724 — authserver
- TCP 8085 — worldserver
SOAP runs on TCP 7878 but the playable profile binds it to **127.0.0.1 only** for the AzerothAI operator/GM channel.
## Recommended network
For the mini PC, the cleanest private setup is:
```text
Windows WoW PC ─────┐
│ LAN or Tailscale
HP EliteDesk ───────┘
├─ AzerothCore
├─ Playerbots
├─ ALE
├─ AzerothAI
└─ Ollama
```
If Tailscale is installed, the bootstrap automatically prefers the machine's Tailscale IPv4 address. Otherwise it uses the first LAN IPv4 address it can detect.
You can override it:
```bash
REALM_ADDRESS=192.168.1.50 bash deploy/playable/bootstrap.sh
```
## Bootstrap
Run this from the AzerothAI checkout on the Linux host:
```bash
bash deploy/playable/bootstrap.sh
```
The script:
1. builds AzerothAI and initializes its local D1/SQLite state
2. generates private bridge/operator tokens
3. starts AzerothAI on port 8787
4. clones the Playerbots AzerothCore fork
5. installs `mod-playerbots` and `mod-ale`
6. mounts `ale/azerothai_bridge.lua` into the real worldserver
7. builds AzerothCore
8. prepares client data and databases
9. starts authserver/worldserver
10. updates the realm address so another machine can connect
The generated secrets are stored outside the repo at:
```text
~/azerothai-playable/runtime.env
```
with owner-only permissions.
## Create your player/GM account
AzerothCore recommends account creation through the worldserver console rather than manually creating auth rows.
```bash
cd ~/azerothai-playable/azerothcore
docker attach ac-worldserver
```
Then:
```text
account create YOURNAME YOURPASSWORD
account set gmlevel YOURNAME 3 -1
```
Detach from Docker without stopping worldserver:
```text
Ctrl-P, then Ctrl-Q
```
Then save the same GM account for AzerothAI's local SOAP client:
```bash
bash deploy/playable/set-gm-credentials.sh YOURNAME YOURPASSWORD
```
Restart AzerothAI by stopping/re-running the playable profile so the new local credentials are loaded.
## Connect the WoW client
In the 3.3.5a client's:
```text
Data/enUS/realmlist.wtf
```
set:
```text
set realmlist <the address printed by bootstrap>
```
Then launch the client normally and log in with the account you created.
The realm should appear as:
```text
AzerothAI Lab
```
## Dashboard
From another machine on the same LAN/Tailscale network:
```text
http://<server-address>:8787
```
The dashboard and WoW realm are separate surfaces:
- WoW client: live game world
- AzerothAI dashboard: population, identities, memories, operator tools, events, model settings
## AI brain
The realm can run without Ollama; decisions remain deterministic.
When Ollama is available:
```bash
ollama pull qwen3.5:4b
```
Then choose **Ollama** in AzerothAI's Server & Models page.
Routine Qwen decisions use non-thinking mode so the small host spends its generation budget on a fast structured action instead of hidden reasoning.
## Autonomous living world
The playable profile now enables a real unattended Playerbot population by default.
- native Playerbots log in and keep playing even when no human is connected
- bots remain fully active while alone
- periodic artificial rerandomization/teleporting is pushed out to a one-year horizon so normal movement, questing, combat and leveling remain the source of progression
- ALE emits authoritative Playerbot snapshots with character GUID/account bindings
- AzerothAI adopts observed Playerbots as persistent resident identities
- the local autonomy worker periodically turns each resident's goals/personality/memories into high-level PlayerBotAI intentions
- AzerothCore/Playerbots remains authoritative for movement, combat, spells, quest rules, loot and actual outcomes
- Zaku is bound to a physical Undead Warlock Playerbot when a suitable body exists; his plot-armor rule stays runtime-only and is never exposed to his character context
For an already-running realm, apply the living-world upgrade with:
```bash
bash deploy/playable/enable-autonomous-world.sh
```
The default target is 10 simultaneously active Playerbots. Override it before running the helper, for example:
```bash
AZEROTHAI_RANDOM_BOTS=20 bash deploy/playable/enable-autonomous-world.sh
```
The helper does not report success merely because configuration files changed. It waits for real Playerbot accounts, physically-online bot characters, authoritative ALE observations, and at least one adopted AzerothAI resident.
## Useful commands
Check status:
```bash
bash deploy/playable/status.sh
```
Stop the lab while preserving all data:
```bash
bash deploy/playable/stop.sh
```
Restart later by running bootstrap again.
Worldserver logs:
```bash
cd ~/azerothai-playable/azerothcore
docker compose logs -f ac-worldserver
```
AzerothAI log:
```bash
tail -f ~/azerothai-playable/azerothai.log
```
Autonomous high-level decisions:
```bash
tail -f ~/azerothai-playable/autonomy.log
```
If your existing lab was created by the PC-lab helper, the scripts automatically detect `~/azerothai-pc-lab` as well.
## Storage
By default:
```text
~/azerothai-playable/
├── azerothcore/
├── client-data/
├── etc/
├── logs/
├── runtime.env
├── azerothai.log
└── azerothai.pid
```
The project checkout keeps AzerothAI's local D1 state in `.wrangler/state`.
Do not delete these directories if you want the realm and character history to survive restarts.
+333
View File
@@ -0,0 +1,333 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
LAB_ROOT="${AZEROTHAI_PLAYABLE_ROOT:-$HOME/azerothai-playable}"
ACORE_ROOT="$LAB_ROOT/azerothcore"
RUNTIME_ENV="$LAB_ROOT/runtime.env"
DB_PASSWORD="${DOCKER_DB_ROOT_PASSWORD:-azerothai-local}"
AUTONOMOUS_BOT_COUNT="${AZEROTHAI_RANDOM_BOTS:-10}"
need(){ command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }; }
need git
need docker
need openssl
need python3
need curl
need jq
need unzip
docker compose version >/dev/null
detect_address(){
if command -v tailscale >/dev/null 2>&1; then
local ts
ts="$(tailscale ip -4 2>/dev/null | head -n1 || true)"
if [[ -n "$ts" ]]; then printf '%s' "$ts"; return; fi
fi
hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true
}
REALM_ADDRESS="${REALM_ADDRESS:-$(detect_address)}"
if [[ -z "$REALM_ADDRESS" ]]; then
echo "Could not determine a LAN/Tailscale IPv4 address. Re-run with REALM_ADDRESS=10.x.x.x" >&2
exit 1
fi
if [[ ! "$REALM_ADDRESS" =~ ^[A-Za-z0-9.-]+$ ]]; then
echo "REALM_ADDRESS must be an IPv4 address or simple hostname." >&2
exit 1
fi
mkdir -p "$LAB_ROOT"
chmod 700 "$LAB_ROOT"
if [[ -f "$RUNTIME_ENV" ]]; then
# shellcheck disable=SC1090
source "$RUNTIME_ENV"
else
ALE_BRIDGE_TOKEN="$(openssl rand -hex 32)"
ALE_OPERATOR_TOKEN="$(openssl rand -hex 32)"
AUTONOMY_TOKEN="$(openssl rand -hex 32)"
cat > "$RUNTIME_ENV" <<EOF
ALE_BRIDGE_TOKEN=$ALE_BRIDGE_TOKEN
ALE_OPERATOR_TOKEN=$ALE_OPERATOR_TOKEN
AUTONOMY_TOKEN=$AUTONOMY_TOKEN
DOCKER_DB_ROOT_PASSWORD=$DB_PASSWORD
EOF
chmod 600 "$RUNTIME_ENV"
fi
: "${ALE_BRIDGE_TOKEN:?Missing ALE_BRIDGE_TOKEN}"
: "${ALE_OPERATOR_TOKEN:?Missing ALE_OPERATOR_TOKEN}"
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 "==> Preparing AzerothAI dashboard"
cd "$REPO_ROOT"
if command -v corepack >/dev/null 2>&1; then corepack enable >/dev/null 2>&1 || true; fi
need node
need pnpm
pnpm install --frozen-lockfile
pnpm build
node --import ./scripts/sites-env.mjs ./node_modules/wrangler/bin/wrangler.js d1 execute DB --local --config dist/server/wrangler.json --persist-to .wrangler/state --file drizzle/0000_old_deathbird.sql >/dev/null 2>&1 || true
python3 - "$REPO_ROOT/.dev.vars" "$ALE_BRIDGE_TOKEN" "$ALE_OPERATOR_TOKEN" "$AUTONOMY_TOKEN" <<'PY'
from pathlib import Path
import sys
p=Path(sys.argv[1])
updates={
"ALE_BRIDGE_TOKEN":sys.argv[2],
"ALE_OPERATOR_TOKEN":sys.argv[3],
"AUTONOMY_TOKEN":sys.argv[4],
"ALE_RAW_EXEC_ENABLED":"true",
"LLM_URL":"http://127.0.0.1:11434",
"SOAP_URL":"http://127.0.0.1:7878",
}
lines=p.read_text().splitlines() if p.exists() else []
seen=set()
out=[]
for line in lines:
if "=" in line and not line.lstrip().startswith("#"):
key=line.split("=",1)[0].strip()
if key in updates:
out.append(f"{key}={updates[key]}")
seen.add(key)
continue
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).rstrip()+"\n")
PY
chmod 600 "$REPO_ROOT/.dev.vars"
if [[ -f "$LAB_ROOT/azerothai.pid" ]] && kill -0 "$(cat "$LAB_ROOT/azerothai.pid")" 2>/dev/null; then
echo "AzerothAI dashboard already running."
else
nohup pnpm start:lan >"$LAB_ROOT/azerothai.log" 2>&1 &
echo $! > "$LAB_ROOT/azerothai.pid"
fi
for _ in {1..60}; do
if curl -fsS http://127.0.0.1:8787/api/health >/dev/null 2>&1; then break; fi
sleep 1
done
curl -fsS http://127.0.0.1:8787/api/health >/dev/null || {
echo "AzerothAI did not start. See $LAB_ROOT/azerothai.log" >&2
exit 1
}
echo "==> Preparing Playerbots AzerothCore"
if [[ ! -d "$ACORE_ROOT/.git" ]]; then
git clone --depth 1 --branch Playerbot https://github.com/mod-playerbots/azerothcore-wotlk.git "$ACORE_ROOT"
else
git -C "$ACORE_ROOT" fetch --depth 1 origin Playerbot
git -C "$ACORE_ROOT" checkout -f FETCH_HEAD
fi
mkdir -p "$ACORE_ROOT/modules"
if [[ ! -d "$ACORE_ROOT/modules/mod-playerbots/.git" ]]; then
git clone --depth 1 --branch master https://github.com/mod-playerbots/mod-playerbots.git "$ACORE_ROOT/modules/mod-playerbots"
else
git -C "$ACORE_ROOT/modules/mod-playerbots" pull --ff-only
fi
if [[ ! -d "$ACORE_ROOT/modules/mod-ale/.git" ]]; then
git clone --depth 1 https://github.com/azerothcore/mod-ale.git "$ACORE_ROOT/modules/mod-ale"
else
git -C "$ACORE_ROOT/modules/mod-ale" pull --ff-only
fi
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
cat > "$ACORE_ROOT/docker-compose.override.yml" <<YAML
services:
ac-worldserver:
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
AC_SOAP_ENABLED: "1"
AC_SOAP_IP: "0.0.0.0"
AC_ALE_ENABLED: "1"
AC_ALE_TRACEBACK: "1"
AC_ALE_SCRIPTPATH: "lua_scripts"
AC_AI_PLAYERBOT_ENABLED: "1"
AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN: "1"
AC_AI_PLAYERBOT_MIN_RANDOM_BOTS: "$AUTONOMOUS_BOT_COUNT"
AC_AI_PLAYERBOT_MAX_RANDOM_BOTS: "$AUTONOMOUS_BOT_COUNT"
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: "1"
AC_AI_PLAYERBOT_RANDOM_BOT_EMOTE: "1"
AC_AI_PLAYERBOT_RANDOM_BOT_SAY_WITHOUT_MASTER: "1"
AC_AI_PLAYERBOT_RANDOM_BOT_JOIN_LFG: "1"
volumes:
- ./azerothai-lua:/azerothcore/lua_scripts:ro
- ./modules/mod-playerbots:/azerothcore/modules/mod-playerbots:ro
YAML
mkdir -p "$LAB_ROOT/client-data" "$LAB_ROOT/etc" "$LAB_ROOT/logs"
chmod 777 "$LAB_ROOT/client-data" "$LAB_ROOT/etc" "$LAB_ROOT/logs"
cat > "$ACORE_ROOT/.env" <<EOF
DOCKER_DB_ROOT_PASSWORD=$DB_PASSWORD
DOCKER_AUTH_EXTERNAL_PORT=3724
DOCKER_WORLD_EXTERNAL_PORT=8085
DOCKER_SOAP_EXTERNAL_PORT=127.0.0.1:7878
DOCKER_VOL_DATA=$LAB_ROOT/client-data
DOCKER_VOL_ETC=$LAB_ROOT/etc
DOCKER_VOL_LOGS=$LAB_ROOT/logs
EOF
echo "==> Building real WoW server (first build can take a while)"
cd "$ACORE_ROOT"
docker compose build ac-client-data-init ac-db-import ac-authserver ac-worldserver
docker compose up -d ac-database
echo "==> Waiting for database"
for _ in {1..120}; do
if docker exec ac-database mysqladmin ping -uroot -p"$DB_PASSWORD" --silent >/dev/null 2>&1; then break; fi
sleep 2
done
docker exec ac-database mysqladmin ping -uroot -p"$DB_PASSWORD" --silent >/dev/null
echo "==> Starting durable ALE relay before worldserver"
AZEROTHAI_PLAYABLE_ROOT="$LAB_ROOT" bash "$REPO_ROOT/deploy/playable/install-ale-relay-service.sh"
echo "==> Loading AzerothCore client data"
set +e
docker compose up --no-deps ac-client-data-init
CLIENT_DATA_RC=$?
set -e
if [[ "$CLIENT_DATA_RC" -ne 0 || ! -d "$LAB_ROOT/client-data/dbc" || ! -d "$LAB_ROOT/client-data/maps" ]]; then
echo "Official client-data initializer did not complete; applying fallback client data package."
rm -rf "$LAB_ROOT/client-data"/*
ASSET_URL="$(curl -fsSL https://api.github.com/repos/wowgaming/client-data/releases/latest | jq -r '.assets[] | select(.name=="Data.zip" or .name=="data.zip") | .browser_download_url' | head -1)"
if [[ -z "$ASSET_URL" || "$ASSET_URL" == "null" ]]; then
echo "Could not locate fallback client data archive." >&2
exit 1
fi
curl -fL --retry 3 "$ASSET_URL" -o "$LAB_ROOT/client-data.zip"
unzip -q "$LAB_ROOT/client-data.zip" -d "$LAB_ROOT/client-data"
rm -f "$LAB_ROOT/client-data.zip"
fi
test -d "$LAB_ROOT/client-data/dbc"
test -d "$LAB_ROOT/client-data/maps"
echo "==> Importing AzerothCore databases"
docker compose up --no-deps --abort-on-container-exit --exit-code-from ac-db-import ac-db-import
echo "==> Updating realm address"
for _ in {1..180}; do
if docker exec ac-database mysql -uroot -p"$DB_PASSWORD" -Nse "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='acore_auth' AND table_name='realmlist'" 2>/dev/null | grep -q '^1$'; then
break
fi
sleep 2
done
docker exec ac-database mysql -uroot -p"$DB_PASSWORD" -Nse "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='acore_auth' AND table_name='realmlist'" | grep -q '^1$'
docker exec ac-database mysql -uroot -p"$DB_PASSWORD" acore_auth -e "UPDATE realmlist SET name='AzerothAI Lab', address='$REALM_ADDRESS', port=8085 WHERE id=1;"
echo "==> Starting authserver and worldserver"
docker compose up -d --no-deps ac-authserver
docker compose up -d --no-deps ac-worldserver
tcp_open(){
(echo >/dev/tcp/127.0.0.1/"$1") >/dev/null 2>&1
}
echo "==> Waiting for worldserver"
for _ in {1..180}; do
STATE="$(docker inspect --format='{{.State.Status}}' ac-worldserver 2>/dev/null || true)"
if [[ "$STATE" == "exited" || "$STATE" == "dead" ]]; then
docker compose logs --tail=250 ac-worldserver >&2 || true
exit 1
fi
if tcp_open 8085; then
break
fi
sleep 2
done
if ! tcp_open 8085; then
docker compose logs --tail=250 ac-worldserver >&2 || true
echo "Worldserver did not open TCP 8085." >&2
exit 1
fi
echo "==> Waiting for authserver"
for _ in {1..90}; do
if tcp_open 3724; then
break
fi
sleep 2
done
if ! tcp_open 3724; then
docker compose logs --tail=200 ac-authserver >&2 || true
echo "Authserver did not open TCP 3724." >&2
exit 1
fi
echo "==> Waiting for ALE bridge heartbeat"
ALE_READY=0
for _ in {1..90}; do
if curl -fsS http://127.0.0.1:8787/api/world-monitor 2>/dev/null | jq -e '.ale.bridgeReady == true and .ale.activeRecently == true' >/dev/null 2>&1; then
ALE_READY=1
break
fi
sleep 2
done
if [[ "$ALE_READY" -ne 1 ]]; then
echo "Worldserver is running, but AzerothAI has not received a fresh ALE bridge.ready heartbeat." >&2
docker compose logs --tail=250 ac-worldserver >&2 || true
exit 1
fi
echo "==> Enabling persistent autonomous Playerbots and AzerothAI heartbeat"
AZEROTHAI_PLAYABLE_ROOT="$LAB_ROOT" AZEROTHAI_RANDOM_BOTS="$AUTONOMOUS_BOT_COUNT" bash "$REPO_ROOT/deploy/playable/enable-autonomous-world.sh"
echo
echo "============================================================"
echo " AzerothAI playable lab is LIVE"
echo "============================================================"
echo "Realm address : $REALM_ADDRESS"
echo "Dashboard : http://$REALM_ADDRESS:8787"
echo "Auth port : 3724/tcp"
echo "World port : 8085/tcp"
echo
echo "Next, create your account using the official worldserver console:"
echo " cd $ACORE_ROOT"
echo " docker attach ac-worldserver"
echo
echo "Then enter:"
echo " account create <USERNAME> <PASSWORD>"
echo " account set gmlevel <USERNAME> 3 -1"
echo
echo "Detach WITHOUT stopping worldserver: Ctrl-P then Ctrl-Q"
echo "Then connect that GM account to AzerothAI:"
echo " bash $REPO_ROOT/deploy/playable/set-gm-credentials.sh <USERNAME> <PASSWORD>"
echo
echo "In your WoW 3.3.5a client Data/enUS/realmlist.wtf use:"
echo " set realmlist $REALM_ADDRESS"
echo
echo "Logs:"
echo " AzerothAI : $LAB_ROOT/azerothai.log"
echo " WoW : cd $ACORE_ROOT && docker compose logs -f ac-worldserver"
+410
View File
@@ -0,0 +1,410 @@
#!/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"
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
find_lab_root(){
if [[ -n "${AZEROTHAI_PLAYABLE_ROOT:-}" && -d "$AZEROTHAI_PLAYABLE_ROOT" ]]; then
printf '%s' "$AZEROTHAI_PLAYABLE_ROOT"; return
fi
for root in "$HOME/azerothai-pc-lab" "$HOME/azerothai-playable"; do
if [[ -d "$root" ]]; then printf '%s' "$root"; return; fi
done
printf '%s' "$HOME/azerothai-pc-lab"
}
LAB_ROOT="$(find_lab_root)"
RUNTIME_ENV="$LAB_ROOT/runtime.env"
NODE_BIN="$(command -v node)"
RUN_USER="$(id -un)"
RUN_GROUP="$(id -gn)"
SERVICE_NAME="azerothai-ale-relay.service"
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME"
if [[ ! -f "$RUNTIME_ENV" ]]; then
echo "Missing $RUNTIME_ENV" >&2
exit 2
fi
grep -q '^ALE_BRIDGE_TOKEN=' "$RUNTIME_ENV" || { echo "ALE_BRIDGE_TOKEN missing from $RUNTIME_ENV" >&2; exit 3; }
grep -q '^DOCKER_DB_ROOT_PASSWORD=' "$RUNTIME_ENV" || { echo "DOCKER_DB_ROOT_PASSWORD missing from $RUNTIME_ENV" >&2; exit 3; }
# shellcheck disable=SC1090
source "$RUNTIME_ENV"
: "${ALE_BRIDGE_TOKEN:?ALE_BRIDGE_TOKEN missing from runtime environment}"
: "${DOCKER_DB_ROOT_PASSWORD:?DOCKER_DB_ROOT_PASSWORD missing from runtime environment}"
command -v docker >/dev/null 2>&1 || { echo "docker is required" >&2; exit 4; }
mkdir -p "$LAB_ROOT"
touch "$LAB_ROOT/ale-relay.log"
chown "$RUN_USER:$RUN_GROUP" "$LAB_ROOT/ale-relay.log"
echo "==> Installing durable ALE database relay service"
sudo tee "$SERVICE_FILE" >/dev/null <<EOF
[Unit]
Description=AzerothAI durable ALE database relay
After=network-online.target docker.service
Wants=network-online.target docker.service
[Service]
Type=simple
User=$RUN_USER
Group=$RUN_GROUP
WorkingDirectory=$REPO_ROOT
EnvironmentFile=$RUNTIME_ENV
Environment=AZEROTHAI_URL=http://127.0.0.1:8787
Environment=AZEROTHAI_ALE_RELAY_POLL_MS=250
ExecStart=$NODE_BIN $REPO_ROOT/scripts/ale-db-relay.mjs
Restart=always
RestartSec=2
StandardOutput=append:$LAB_ROOT/ale-relay.log
StandardError=append:$LAB_ROOT/ale-relay.log
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable "$SERVICE_NAME" >/dev/null
sudo systemctl restart "$SERVICE_NAME"
for _ in {1..20}; do
if docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='acore_auth' AND table_name IN ('azerothai_command_inbox','azerothai_event_outbox','azerothai_result_outbox')" 2>/dev/null | grep -qx '3'; then
break
fi
sleep 1
done
if ! sudo systemctl is-active --quiet "$SERVICE_NAME"; then
echo "ALE relay service failed to start." >&2
sudo systemctl status "$SERVICE_NAME" --no-pager -l >&2 || true
tail -80 "$LAB_ROOT/ale-relay.log" >&2 || true
exit 5
fi
TABLE_COUNT="$(docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='acore_auth' AND table_name IN ('azerothai_command_inbox','azerothai_event_outbox','azerothai_result_outbox')" 2>/dev/null || echo 0)"
if [[ "$TABLE_COUNT" != "3" ]]; then
echo "ALE relay is running but durable spool tables were not created." >&2
tail -80 "$LAB_ROOT/ale-relay.log" >&2 || true
exit 6
fi
PID="$(sudo systemctl show -p MainPID --value "$SERVICE_NAME")"
echo "ALE relay service: active (PID $PID)"
echo "Durable spool tables: ready"
echo "Transport: AzerothAI D1 <-> host relay <-> acore_auth spool <-> ALE"
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
find_lab_root(){
if [[ -n "${AZEROTHAI_PLAYABLE_ROOT:-}" && -d "$AZEROTHAI_PLAYABLE_ROOT" ]]; then
printf '%s' "$AZEROTHAI_PLAYABLE_ROOT"; return
fi
for root in "$HOME/azerothai-pc-lab" "$HOME/azerothai-playable"; do
if [[ -d "$root" ]]; then printf '%s' "$root"; return; fi
done
printf '%s' "$HOME/azerothai-pc-lab"
}
LAB_ROOT="$(find_lab_root)"
RUNTIME_ENV="$LAB_ROOT/runtime.env"
NODE_BIN="$(command -v node)"
RUN_USER="$(id -un)"
RUN_GROUP="$(id -gn)"
SERVICE_NAME="azerothai-autonomy.service"
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME"
if [[ ! -f "$RUNTIME_ENV" ]]; then
echo "Missing $RUNTIME_ENV" >&2
exit 2
fi
if ! grep -q '^AUTONOMY_TOKEN=' "$RUNTIME_ENV"; then
echo "AUTONOMY_TOKEN is missing from $RUNTIME_ENV" >&2
exit 3
fi
mkdir -p "$LAB_ROOT"
touch "$LAB_ROOT/autonomy.log"
chown "$RUN_USER:$RUN_GROUP" "$LAB_ROOT/autonomy.log"
# Stop any legacy nohup worker before handing ownership to systemd.
if [[ -f "$LAB_ROOT/autonomy.pid" ]]; then
OLD_PID="$(cat "$LAB_ROOT/autonomy.pid" 2>/dev/null || true)"
if [[ -n "$OLD_PID" ]] && kill -0 "$OLD_PID" 2>/dev/null; then
kill "$OLD_PID" 2>/dev/null || true
fi
fi
pkill -f "$REPO_ROOT/scripts/autonomy-worker.mjs" 2>/dev/null || true
echo "==> Installing persistent AzerothAI autonomy service"
sudo tee "$SERVICE_FILE" >/dev/null <<EOF
[Unit]
Description=AzerothAI autonomous high-level Playerbot brain
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=$RUN_USER
Group=$RUN_GROUP
WorkingDirectory=$REPO_ROOT
EnvironmentFile=$RUNTIME_ENV
Environment=AZEROTHAI_URL=http://127.0.0.1:8787
Environment=AUTONOMY_POLL_MS=30000
ExecStart=$NODE_BIN $REPO_ROOT/scripts/autonomy-worker.mjs
Restart=always
RestartSec=5
StandardOutput=append:$LAB_ROOT/autonomy.log
StandardError=append:$LAB_ROOT/autonomy.log
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable "$SERVICE_NAME" >/dev/null
sudo systemctl restart "$SERVICE_NAME"
sleep 1
PID="$(sudo systemctl show -p MainPID --value "$SERVICE_NAME")"
echo "$PID" > "$LAB_ROOT/autonomy.pid"
if ! sudo systemctl is-active --quiet "$SERVICE_NAME"; then
echo "Autonomy service failed to start." >&2
sudo systemctl status "$SERVICE_NAME" --no-pager -l >&2 || true
exit 4
fi
echo "Autonomy service: active (PID $PID)"
echo "It will restart automatically after crashes and VM reboots."
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
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)"
[[ -n "$LAB_ROOT" ]] || { echo "Preflight: playable lab not found." >&2; exit 2; }
ACORE_ROOT="$LAB_ROOT/azerothcore"
RUNTIME_ENV="$LAB_ROOT/runtime.env"
for cmd in bash node docker pnpm curl jq python3; do
command -v "$cmd" >/dev/null 2>&1 || { echo "Preflight: missing $cmd" >&2; exit 3; }
done
echo "==> Preflight: shell syntax"
for script in "$REPO_ROOT/deploy/playable/enable-autonomous-world.sh" "$REPO_ROOT/deploy/playable/install-autonomy-service.sh" "$REPO_ROOT/deploy/playable/install-ale-relay-service.sh" "$REPO_ROOT/deploy/playable/status.sh" "$REPO_ROOT/deploy/playable/stop.sh"; do
bash -n "$script"
done
echo "==> Preflight: Node syntax"
node --check "$REPO_ROOT/scripts/autonomy-worker.mjs" >/dev/null
node --check "$REPO_ROOT/scripts/ale-db-relay.mjs" >/dev/null
echo "==> Preflight: durable bridge source"
grep -q 'AuthDBQuery("SELECT id,source FROM azerothai_command_inbox' "$REPO_ROOT/ale/azerothai_bridge.lua" || {
echo "Preflight: durable DB command poll is missing from ALE bridge." >&2; exit 4;
}
if grep -q 'HttpRequest(' "$REPO_ROOT/ale/azerothai_bridge.lua"; then
echo "Preflight: ALE bridge still contains outbound HttpRequest transport." >&2
exit 4
fi
echo "==> Preflight: AzerothCore compose"
(cd "$ACORE_ROOT" && docker compose config -q)
[[ -f "$RUNTIME_ENV" ]] || { echo "Preflight: missing $RUNTIME_ENV" >&2; exit 5; }
# shellcheck disable=SC1090
source "$RUNTIME_ENV"
: "${ALE_BRIDGE_TOKEN:?Preflight: ALE_BRIDGE_TOKEN missing}"
: "${ALE_OPERATOR_TOKEN:?Preflight: ALE_OPERATOR_TOKEN missing}"
: "${DOCKER_DB_ROOT_PASSWORD:?Preflight: DOCKER_DB_ROOT_PASSWORD missing}"
echo "==> Preflight: database connectivity"
docker inspect ac-database >/dev/null 2>&1 || { echo "Preflight: ac-database container is missing." >&2; exit 6; }
docker exec ac-database mysqladmin ping -uroot -p"$DOCKER_DB_ROOT_PASSWORD" --silent >/dev/null
if [[ -f "$ACORE_ROOT/modules/mod-ale/src/LuaEngine/methods/GlobalMethods.h" ]]; then
echo "==> Preflight: installed ALE database API"
grep -q 'int AuthDBQuery' "$ACORE_ROOT/modules/mod-ale/src/LuaEngine/methods/GlobalMethods.h" || {
echo "Preflight: installed mod-ale lacks AuthDBQuery." >&2; exit 7;
}
grep -q 'int AuthDBExecute' "$ACORE_ROOT/modules/mod-ale/src/LuaEngine/methods/GlobalMethods.h" || {
echo "Preflight: installed mod-ale lacks AuthDBExecute." >&2; exit 7;
}
fi
echo "Preflight passed."
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "$#" -ne 2 ]]; then
echo "Usage: bash deploy/playable/set-gm-credentials.sh <USERNAME> <PASSWORD>" >&2
exit 2
fi
USERNAME="$1"
PASSWORD="$2"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
DEV_VARS="$REPO_ROOT/.dev.vars"
python3 - "$DEV_VARS" "$USERNAME" "$PASSWORD" <<'PY'
from pathlib import Path
import sys
p=Path(sys.argv[1])
updates={"SOAP_USER":sys.argv[2],"SOAP_PASSWORD":sys.argv[3]}
lines=p.read_text().splitlines() if p.exists() else []
out=[]
seen=set()
for line in lines:
if "=" in line and not line.lstrip().startswith("#"):
key=line.split("=",1)[0].strip()
if key in updates:
out.append(f"{key}={updates[key]}")
seen.add(key)
continue
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).rstrip()+"\n")
PY
chmod 600 "$DEV_VARS"
echo "Saved SOAP credentials to the ignored local .dev.vars file."
echo "Restart AzerothAI so the GM console picks them up."
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env bash
set -euo pipefail
find_lab_root(){
if [[ -n "${AZEROTHAI_PLAYABLE_ROOT:-}" && -d "$AZEROTHAI_PLAYABLE_ROOT" ]]; then
printf '%s' "$AZEROTHAI_PLAYABLE_ROOT"; return
fi
for root in "$HOME/azerothai-pc-lab" "$HOME/azerothai-playable"; do
if [[ -d "$root" ]]; then printf '%s' "$root"; return; fi
done
printf '%s' "$HOME/azerothai-playable"
}
LAB_ROOT="$(find_lab_root)"
ACORE_ROOT="$LAB_ROOT/azerothcore"
RUNTIME_ENV="$LAB_ROOT/runtime.env"
if [[ -f "$RUNTIME_ENV" ]]; then
# shellcheck disable=SC1090
source "$RUNTIME_ENV"
fi
echo "AzerothAI playable lab status"
echo "================================"
echo "Lab root : $LAB_ROOT"
if [[ -f "$LAB_ROOT/azerothai.pid" ]] && kill -0 "$(cat "$LAB_ROOT/azerothai.pid")" 2>/dev/null; then
echo "AzerothAI process : running (PID $(cat "$LAB_ROOT/azerothai.pid"))"
else
echo "AzerothAI process : stopped"
fi
if sudo systemctl is-active --quiet azerothai-autonomy.service 2>/dev/null; then
PID="$(sudo systemctl show -p MainPID --value azerothai-autonomy.service)"
echo "Autonomy worker : running via systemd (PID $PID)"
elif [[ -f "$LAB_ROOT/autonomy.pid" ]] && kill -0 "$(cat "$LAB_ROOT/autonomy.pid")" 2>/dev/null; then
echo "Autonomy worker : running legacy PID $(cat "$LAB_ROOT/autonomy.pid")"
else
echo "Autonomy worker : stopped"
fi
if sudo systemctl is-active --quiet azerothai-ale-relay.service 2>/dev/null; then
RELAY_PID="$(sudo systemctl show -p MainPID --value azerothai-ale-relay.service)"
echo "ALE DB relay : running via systemd (PID $RELAY_PID)"
else
echo "ALE DB relay : stopped"
fi
if curl -fsS http://127.0.0.1:8787/api/health >/tmp/azerothai-health.json 2>/dev/null; then
echo "Dashboard health : OK"
if command -v jq >/dev/null 2>&1; then
jq -c . /tmp/azerothai-health.json
else
cat /tmp/azerothai-health.json
fi
echo
else
echo "Dashboard health : unavailable"
fi
if curl -fsS http://127.0.0.1:8787/api/world-monitor >/tmp/azerothai-world-monitor.json 2>/dev/null; then
echo
echo "Live realm monitor:"
if command -v jq >/dev/null 2>&1; then
jq -r '
" ALE bridge ready : " + ((.ale.bridgeReady // false)|tostring),
" ALE active recently : " + ((.ale.activeRecently // false)|tostring),
" Observed bot events : " + ((.playerbots.observedBots // 0)|tostring),
" ALE pending commands : " + ((.commands.pending // 0)|tostring),
" ALE failed commands : " + ((.commands.failed // 0)|tostring)
' /tmp/azerothai-world-monitor.json
else
cat /tmp/azerothai-world-monitor.json
fi
fi
if curl -fsS http://127.0.0.1:8787/api/state >/tmp/azerothai-state.json 2>/dev/null && command -v jq >/dev/null 2>&1; then
MANAGED="$(jq '[.bots[]? | select(.autonomy.managed == true)] | length' /tmp/azerothai-state.json 2>/dev/null || echo 0)"
DUE="$(jq '[.bots[]? | select(.autonomy.managed == true and .status != "dead" and .status != "retired")] | length' /tmp/azerothai-state.json 2>/dev/null || echo 0)"
echo " Managed AzerothAI lives : $MANAGED"
echo " Managed living residents: $DUE"
fi
if [[ -d "$ACORE_ROOT" ]]; then
echo
echo "WoW containers:"
(cd "$ACORE_ROOT" && docker compose ps) || true
if docker inspect ac-database >/dev/null 2>&1 && [[ -n "${DOCKER_DB_ROOT_PASSWORD:-}" ]]; then
echo
echo "Realm:"
docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse "SELECT CONCAT(name,' | ',address,':',port,' | build ',gamebuild) FROM acore_auth.realmlist WHERE id=1" 2>/dev/null || true
RND_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 '?')"
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 '?')"
echo "Random bot accounts: $RND_ACCOUNTS"
echo "Random bots online : $RND_ONLINE"
if docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse "SELECT 1 FROM information_schema.tables WHERE table_schema='acore_auth' AND table_name='azerothai_command_inbox'" 2>/dev/null | grep -q 1; then
SPOOL_COMMANDS="$(docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse "SELECT COUNT(*) FROM acore_auth.azerothai_command_inbox WHERE consumed_at IS NULL" 2>/dev/null || echo '?')"
SPOOL_EVENTS="$(docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse "SELECT COUNT(*) FROM acore_auth.azerothai_event_outbox WHERE delivered_at IS NULL" 2>/dev/null || echo '?')"
SPOOL_RESULTS="$(docker exec ac-database mysql -uroot -p"$DOCKER_DB_ROOT_PASSWORD" -Nse "SELECT COUNT(*) FROM acore_auth.azerothai_result_outbox WHERE delivered_at IS NULL" 2>/dev/null || echo '?')"
echo "ALE spool commands : $SPOOL_COMMANDS"
echo "ALE spool events : $SPOOL_EVENTS"
echo "ALE spool results : $SPOOL_RESULTS"
fi
fi
echo
echo "Listening ports:"
for port in 3724 8085 8787; do
if command -v ss >/dev/null 2>&1 && ss -ltn | awk '{print $4}' | grep -Eq "[:.]$port$"; then
echo " $port/tcp : listening"
else
echo " $port/tcp : not detected"
fi
done
else
echo "WoW server : not bootstrapped"
fi
if [[ -n "${ALE_OPERATOR_TOKEN:-}" ]]; then
echo
if curl -fsS -H "Authorization: Bearer $ALE_OPERATOR_TOKEN" http://127.0.0.1:8787/api/ale/status >/tmp/azerothai-ale-status.json 2>/dev/null; then
echo "ALE control plane : reachable"
python3 - <<'PY'
import json
p='/tmp/azerothai-ale-status.json'
d=json.load(open(p))
print("rawEnabled:",d.get("rawEnabled"))
print("recent commands:",len(d.get("commands",[])))
print("recent events:",len(d.get("events",[])))
for e in d.get("events",[])[:5]:
print(" -",e.get("at"),e.get("kind"))
PY
else
echo "ALE control plane : unavailable"
fi
fi
if [[ -f "$LAB_ROOT/autonomy.log" ]]; then
echo
echo "Recent autonomy:"
tail -n 5 "$LAB_ROOT/autonomy.log" || true
fi
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
find_lab_root(){
if [[ -n "${AZEROTHAI_PLAYABLE_ROOT:-}" && -d "$AZEROTHAI_PLAYABLE_ROOT" ]]; then
printf '%s' "$AZEROTHAI_PLAYABLE_ROOT"; return
fi
for root in "$HOME/azerothai-pc-lab" "$HOME/azerothai-playable"; do
if [[ -d "$root" ]]; then printf '%s' "$root"; return; fi
done
printf '%s' "$HOME/azerothai-playable"
}
LAB_ROOT="$(find_lab_root)"
ACORE_ROOT="$LAB_ROOT/azerothcore"
stop_pid_file(){
local label="$1" file="$2"
if [[ ! -f "$file" ]]; then return; fi
local pid
pid="$(cat "$file" 2>/dev/null || true)"
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
echo "Stopping $label PID $pid..."
kill "$pid" 2>/dev/null || true
for _ in {1..20}; do
if ! kill -0 "$pid" 2>/dev/null; then break; fi
sleep 0.25
done
kill -9 "$pid" 2>/dev/null || true
fi
rm -f "$file"
}
if sudo systemctl list-unit-files azerothai-autonomy.service >/dev/null 2>&1; then
echo "Stopping AzerothAI autonomy service..."
sudo systemctl disable --now azerothai-autonomy.service >/dev/null 2>&1 || true
fi
if sudo systemctl list-unit-files azerothai-ale-relay.service >/dev/null 2>&1; then
echo "Stopping AzerothAI ALE relay service..."
sudo systemctl disable --now azerothai-ale-relay.service >/dev/null 2>&1 || true
fi
stop_pid_file "AzerothAI autonomy worker" "$LAB_ROOT/autonomy.pid"
if [[ -d "$ACORE_ROOT" ]]; then
echo "Stopping AzerothCore containers..."
(cd "$ACORE_ROOT" && docker compose down)
fi
stop_pid_file "AzerothAI" "$LAB_ROOT/azerothai.pid"
echo "Stopped. Database, client data, AzerothAI state, bot identities, memories, and logs were preserved."