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
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:
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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."
|
||||
Reference in New Issue
Block a user