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
+160
View File
@@ -0,0 +1,160 @@
# Temporary Windows PC + Ubuntu VM Lab
This is the short-lived lab profile for running AzerothAI on a Windows gaming PC until the dedicated mini PC is ready.
## Layout
Windows gaming PC:
- WoW 3.3.5a client
- Ollama
- Qwen3.5 4B on the RTX GPU
- Ubuntu Server VM containing AzerothCore, Playerbots, ALE, MySQL, AzerothAI, and World Monitor
The Windows install stays intact. Do not install Proxmox on the gaming PC for this temporary lab.
## VM settings
Create an Ubuntu Server VM in VMware with:
- 4 vCPUs
- 8 GB RAM
- 90 GB dynamically allocated disk
- NAT networking for the temporary Wi-Fi lab
- no desktop environment required
Ubuntu 24.04 LTS is the reference host, but newer Ubuntu hosts are acceptable because the AzerothCore Docker build itself uses its pinned container base. The bootstrap automatically expands free LVM space and handles the VMware NAT path to Windows-hosted Ollama.
With NAT, Windows can reach the VM directly on VMnet8. Phone/iPad access is optional and can be added later with bridged networking or a tunnel.
If free disk space is tight, 70 GB can work for a short test, but 90 GB leaves more breathing room for AzerothCore build output and extracted client data.
## Step 1 — Windows host
Open PowerShell as Administrator in the AzerothAI checkout.
Read-only check:
powershell -ExecutionPolicy Bypass -File .\deploy\pc-lab\host-setup.ps1
When ready to set up the model host:
powershell -ExecutionPolicy Bypass -File .\deploy\pc-lab\host-setup.ps1 -InstallOllama -StartOllama -OpenFirewall
That checks RAM, free space, CPU virtualization and GPU; installs Ollama through winget if requested; binds Ollama to TCP 11434; pulls qwen3.5:4b; opens TCP 11434 on Windows Private networks only; and prints the Windows LAN IPv4 address.
Keep that IPv4 address.
## Step 2 — Create Ubuntu VM
Install Ubuntu Server normally. For this temporary Wi-Fi setup, use VMware NAT.
After installation:
ip addr
Keep the VM's LAN IPv4 address too.
## Step 3 — Clone AzerothAI in Ubuntu
sudo apt-get update
sudo apt-get install -y git
git clone https://github.com/Endami-backup/AzerothAI.git
cd AzerothAI
git checkout feature/ai-brain-router
bash deploy/pc-lab/vm-bootstrap.sh
The bootstrap first tries to discover the Windows VMnet8/Ollama endpoint automatically. If it cannot, enter the VMware NAT host IPv4 printed by host-setup.ps1.
On the first run the script may install Docker and ask you to log out/back in before rerunning. That is expected.
The bootstrap verifies the VM can reach Qwen on Windows, calls the real playable-realm bootstrap, builds AzerothCore + Playerbots + ALE, imports the realm databases/client data, starts AzerothAI, points AzerothAI at the Windows Ollama host, and prints the WoW realm and dashboard addresses.
## Step 4 — Create your WoW GM account
Inside Ubuntu:
cd ~/azerothai-pc-lab/azerothcore
docker attach ac-worldserver
At the worldserver console:
account create YOURNAME YOURPASSWORD
account set gmlevel YOURNAME 3 -1
Detach without killing worldserver with Ctrl-P, then Ctrl-Q.
Then save those same credentials for AzerothAI's localhost-only SOAP GM channel from the AzerothAI checkout:
bash deploy/playable/set-gm-credentials.sh YOURNAME YOURPASSWORD
Restart the AzerothAI runtime if the helper tells you to.
## Step 5 — Connect WoW from Windows
Edit:
<WoW 3.3.5a folder>\Data\enUS\realmlist.wtf
Use the Ubuntu VM IP:
set realmlist 192.168.x.x
Launch WoW and sign in. The realm name is AzerothAI Lab.
## Step 6 — Open World Monitor
From Windows, iPhone, or iPad on the same network:
http://<ubuntu-vm-ip>:8787
Open World Monitor. It shows Zaku's location/state/home/goal/last decision, active zones, population bands, Hardcore deaths, authoritative ALE events, server/model health, and Population Director activity.
## Step 7 — Test connectivity from Windows
powershell -ExecutionPolicy Bypass -File .\deploy\pc-lab\check-lab.ps1 -VmIp 192.168.x.x
It checks World Monitor on TCP 8787, WoW authserver on 3724, worldserver on 8085, and the AzerothAI health API.
## What still needs real-server proof
The simulated ALE/live-world path is green in CI. This VM gives us the physical environment to validate:
- real Playerbot character GUID binding
- PlayerBotAI execution of AzerothAI intents
- all real damage paths for Zaku's 1 HP clamp
- actual quest/travel/social behavior
- real WoW client login/play
If the live server differs from the simulator, the worldserver is authoritative and we fix the adapter.
## Stop the lab
From Ubuntu:
export AZEROTHAI_PLAYABLE_ROOT="$HOME/azerothai-pc-lab"
bash deploy/playable/stop.sh
The database and realm files remain on disk.
## Thursday migration
Before deleting the VM, create a private migration archive:
cd ~/AzerothAI
export AZEROTHAI_PLAYABLE_ROOT="$HOME/azerothai-pc-lab"
bash deploy/pc-lab/export-for-mini-pc.sh
The archive contains the AzerothCore auth/characters/world databases, AzerothAI local state, generated configuration, and runtime secrets. It is created with owner-only permissions and must be treated as sensitive.
This VM is disposable, but Zaku is not. On the mini PC we should import this state so Zaku remains the same persistent identity rather than being rebuilt as a new character.
## Remote dashboard over Tailscale
If the dashboard HTML loads over the raw Tailscale IP but CSS/JS assets do not, use the built-in proxy helper:
```bash
cd ~/AzerothAI
git pull
bash deploy/pc-lab/tailscale-dashboard.sh
```
It serves built client assets directly with nginx, proxies API/SSR requests back to the AzerothAI runtime on port 8787, and publishes the result privately with Tailscale Serve over HTTPS. Use the `https://...ts.net/` URL it prints instead of raw `http://100.x.x.x:8787`.
+48
View File
@@ -0,0 +1,48 @@
param(
[Parameter(Mandatory=$true)]
[string]$VmIp,
[int]$DashboardPort = 8787,
[int]$AuthPort = 3724,
[int]$WorldPort = 8085
)
$ErrorActionPreference = "Continue"
function Test-Port([string]$HostName,[int]$Port,[string]$Label) {
$result = Test-NetConnection -ComputerName $HostName -Port $Port -WarningAction SilentlyContinue
if ($result.TcpTestSucceeded) {
Write-Host "[OK] $Label ${HostName}:$Port" -ForegroundColor Green
return $true
}
Write-Host "[FAIL] $Label ${HostName}:$Port" -ForegroundColor Red
return $false
}
Write-Host "AzerothAI PC Lab connectivity check" -ForegroundColor Cyan
Write-Host "VM: $VmIp"
Write-Host ""
$ok = $true
$ok = (Test-Port $VmIp $DashboardPort "World Monitor") -and $ok
$ok = (Test-Port $VmIp $AuthPort "WoW authserver") -and $ok
$ok = (Test-Port $VmIp $WorldPort "WoW worldserver") -and $ok
try {
$health = Invoke-RestMethod -Uri "http://${VmIp}:$DashboardPort/api/health" -TimeoutSec 5
Write-Host "[OK] AzerothAI health API" -ForegroundColor Green
$health | ConvertTo-Json -Depth 5
} catch {
Write-Host "[FAIL] AzerothAI health API" -ForegroundColor Red
$ok = $false
}
if (-not $ok) {
Write-Host ""
Write-Host "One or more checks failed. Verify the VM is using Bridged networking and the Ubuntu firewall is not blocking these ports." -ForegroundColor Yellow
exit 1
}
Write-Host ""
Write-Host "All network checks passed." -ForegroundColor Green
Write-Host "World Monitor: http://${VmIp}:$DashboardPort"
Write-Host "WoW realmlist: set realmlist $VmIp"
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
LAB_ROOT="${AZEROTHAI_PLAYABLE_ROOT:-$HOME/azerothai-pc-lab}"
ACORE_ROOT="$LAB_ROOT/azerothcore"
RUNTIME_ENV="$LAB_ROOT/runtime.env"
OUT_DIR="${1:-$HOME}"
STAMP="$(date +%Y%m%d-%H%M%S)"
WORK="$LAB_ROOT/export-$STAMP"
ARCHIVE="$OUT_DIR/azerothai-pc-lab-$STAMP.tar.gz"
mkdir -p "$WORK/mysql" "$WORK/azerothai"
if [[ ! -f "$RUNTIME_ENV" ]]; then
echo "Missing $RUNTIME_ENV. This does not look like an initialized PC lab." >&2
exit 2
fi
# shellcheck disable=SC1090
source "$RUNTIME_ENV"
: "${DOCKER_DB_ROOT_PASSWORD:?Missing database password in runtime.env}"
if ! docker inspect ac-database >/dev/null 2>&1; then
echo "The AzerothCore database container is not present." >&2
exit 3
fi
echo "==> Dumping AzerothCore databases"
for db in acore_auth acore_characters acore_world; do
docker exec ac-database mysqldump -uroot -p"$DOCKER_DB_ROOT_PASSWORD" --single-transaction --routines --events --triggers "$db" > "$WORK/mysql/$db.sql"
done
echo "==> Preserving AzerothAI state and runtime configuration"
cp "$RUNTIME_ENV" "$WORK/runtime.env"
chmod 600 "$WORK/runtime.env"
if [[ -d "$REPO_ROOT/.wrangler/state" ]]; then
tar -C "$REPO_ROOT" -czf "$WORK/azerothai/wrangler-state.tar.gz" .wrangler/state
fi
if [[ -d "$LAB_ROOT/etc" ]]; then
tar -C "$LAB_ROOT" -czf "$WORK/azerothcore-etc.tar.gz" etc
fi
cat > "$WORK/MANIFEST.txt" <<EOF
AzerothAI temporary PC lab migration export
Created: $(date -Is)
Source branch: $(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
Source commit: $(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || echo unknown)
Contains:
- acore_auth SQL
- acore_characters SQL
- acore_world SQL
- AzerothAI local D1/SQLite state, when present
- AzerothCore generated configuration, when present
- runtime.env including bridge/operator/database secrets
Treat this archive as sensitive because it contains credentials.
EOF
echo "==> Creating migration archive"
tar -C "$WORK" -czf "$ARCHIVE" .
chmod 600 "$ARCHIVE"
rm -rf "$WORK"
echo
echo "Migration export created:"
echo " $ARCHIVE"
echo
echo "Keep this file private. It contains realm data and runtime credentials."
+174
View File
@@ -0,0 +1,174 @@
param(
[switch]$InstallOllama,
[switch]$StartOllama,
[switch]$OpenFirewall,
[string]$Model = "qwen3.5:4b"
)
$ErrorActionPreference = "Stop"
function Section([string]$Text) {
Write-Host ""
Write-Host "==> $Text" -ForegroundColor Cyan
}
function Get-LanIPv4 {
$candidates = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object {
$_.IPAddress -notlike "127.*" -and
$_.IPAddress -notlike "169.254.*" -and
$_.InterfaceAlias -notmatch "vEthernet|VirtualBox|VMware|Loopback|Tailscale"
} |
Sort-Object InterfaceMetric
return ($candidates | Select-Object -First 1 -ExpandProperty IPAddress)
}
function Get-VMwareNatInfo {
$addr = Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias "*VMware*VMnet8*" -ErrorAction SilentlyContinue |
Select-Object -First 1
if (-not $addr) { return $null }
$octets = $addr.IPAddress.Split('.')
if ($octets.Count -ne 4) { return $null }
$subnet = "$($octets[0]).$($octets[1]).$($octets[2]).0/$($addr.PrefixLength)"
return [pscustomobject]@{
IP = $addr.IPAddress
PrefixLength = $addr.PrefixLength
Subnet = $subnet
}
}
Section "AzerothAI temporary Windows host check"
$lanIp = Get-LanIPv4
$vmnet8 = Get-VMwareNatInfo
if (-not $lanIp) {
Write-Warning "Could not auto-detect a LAN IPv4 address."
} else {
Write-Host "Windows LAN IPv4: $lanIp"
}
if ($vmnet8) {
Write-Host "VMware NAT host IPv4: $($vmnet8.IP)"
Write-Host "VMware NAT subnet: $($vmnet8.Subnet)"
}
$free = Get-PSDrive -Name C
$freeGB = [math]::Round($free.Free / 1GB, 1)
Write-Host "Free space on C:: $freeGB GB"
if ($freeGB -lt 70) {
Write-Warning "Less than 70 GB is free. The temporary VM may be tight once AzerothCore and client data are built."
}
$ram = Get-CimInstance Win32_ComputerSystem
$totalGB = [math]::Round($ram.TotalPhysicalMemory / 1GB, 1)
Write-Host "Installed RAM: $totalGB GB"
if ($totalGB -lt 14) {
Write-Warning "This PC has less memory than expected for an 8 GB VM plus Windows."
}
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
Write-Host "CPU: $($cpu.Name)"
Write-Host "Logical processors: $($cpu.NumberOfLogicalProcessors)"
$gpu = Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name
Write-Host "GPU(s): $($gpu -join ', ')"
$virt = (Get-CimInstance Win32_Processor | Select-Object -First 1).VirtualizationFirmwareEnabled
Write-Host "Firmware virtualization enabled: $virt"
if (-not $virt) {
Write-Warning "CPU virtualization appears disabled. Enable SVM/AMD-V in BIOS before creating the VM."
}
$ollamaCommand = Get-Command ollama -ErrorAction SilentlyContinue
$ollamaDefault = Join-Path $env:LOCALAPPDATA "Programs\Ollama\ollama.exe"
$ollamaExe = if ($ollamaCommand) { $ollamaCommand.Source } elseif (Test-Path $ollamaDefault) { $ollamaDefault } else { $null }
if ($InstallOllama -and -not $ollamaExe) {
Section "Installing Ollama"
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
throw "winget is not available. Install Ollama manually, then rerun this script."
}
winget install --id Ollama.Ollama -e --accept-source-agreements --accept-package-agreements
if (Test-Path $ollamaDefault) {
$ollamaExe = $ollamaDefault
} else {
$ollamaCommand = Get-Command ollama -ErrorAction SilentlyContinue
if ($ollamaCommand) { $ollamaExe = $ollamaCommand.Source }
}
}
if ($ollamaExe) {
Section "Ollama"
Write-Host "Ollama detected: $ollamaExe"
if ($StartOllama) {
[Environment]::SetEnvironmentVariable("OLLAMA_HOST", "0.0.0.0:11434", "User")
$env:OLLAMA_HOST = "0.0.0.0:11434"
Get-Process -Name "ollama" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
Start-Process -FilePath $ollamaExe -ArgumentList "serve" -WindowStyle Hidden
Start-Sleep -Seconds 3
Write-Host "Pulling $Model ..."
& $ollamaExe pull $Model
try {
Invoke-RestMethod -Uri "http://127.0.0.1:11434/api/tags" -TimeoutSec 5 | Out-Null
Write-Host "Ollama API: OK"
} catch {
Write-Warning "Ollama did not answer on port 11434 yet."
}
} else {
Write-Host "Use -StartOllama when you want this script to expose Ollama to the Ubuntu VM and pull $Model."
}
} else {
Write-Warning "Ollama is not installed. Re-run with -InstallOllama -StartOllama, or install it manually."
}
if ($OpenFirewall) {
Section "Windows firewall"
$profiles = Get-NetConnectionProfile -ErrorAction SilentlyContinue
$activeProfiles = $profiles | Where-Object { $_.IPv4Connectivity -ne "Disconnected" }
foreach ($profile in $activeProfiles) {
Write-Host "Network profile: $($profile.InterfaceAlias) -> $($profile.NetworkCategory)"
if ($profile.NetworkCategory -eq "Public") {
Write-Warning "This active network is Public. The AzerothAI firewall rule is Private-only and will not accept VM traffic until the network is trusted/Private or you deliberately change the rule."
}
}
$ruleName = "AzerothAI Ollama from VM"
$existing = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if ($existing) {
Remove-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
}
if ($vmnet8) {
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Allow -Protocol TCP -LocalPort 11434 -RemoteAddress $vmnet8.Subnet -Profile Any | Out-Null
Write-Host "Opened TCP 11434 only to the VMware NAT subnet $($vmnet8.Subnet)."
} else {
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Allow -Protocol TCP -LocalPort 11434 -Profile Private | Out-Null
Write-Host "VMnet8 was not detected, so TCP 11434 was opened on Private networks only."
}
}
Section "Temporary VM recommendation"
Write-Host "Ubuntu Server 24.04 LTS"
Write-Host "vCPU: 4"
Write-Host "RAM: 8 GB"
Write-Host "Disk: 90 GB dynamically allocated"
Write-Host "Network: NAT recommended for this temporary Wi-Fi lab"
Write-Host ""
Write-Host "After Ubuntu boots, clone AzerothAI and run:"
Write-Host " bash deploy/pc-lab/vm-bootstrap.sh"
Write-Host ""
if ($vmnet8) {
Write-Host "The Ubuntu NAT VM should use this Windows Ollama endpoint:"
Write-Host " http://$($vmnet8.IP):11434"
} elseif ($lanIp) {
Write-Host "Fallback Windows Ollama endpoint:"
Write-Host " http://${lanIp}:11434"
}
Write-Host ""
Write-Host "Nothing in this script installs Proxmox or modifies your Windows boot configuration."
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
LAB_ROOT="${AZEROTHAI_PLAYABLE_ROOT:-$HOME/azerothai-pc-lab}"
DEV_PORT="${AZEROTHAI_REMOTE_UI_DEV_PORT:-5173}"
PROXY_PORT="${AZEROTHAI_TAILNET_PROXY_PORT:-8788}"
APP_PORT="${AZEROTHAI_APP_PORT:-8787}"
PID_FILE="$LAB_ROOT/azerothai-ui-dev.pid"
LOG_FILE="$LAB_ROOT/azerothai-ui-dev.log"
NGINX_SITE="/etc/nginx/sites-available/azerothai-tailnet"
NGINX_LINK="/etc/nginx/sites-enabled/azerothai-tailnet"
need(){ command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }; }
need curl
need tailscale
need pnpm
TAILSCALE_IP="$(tailscale ip -4 2>/dev/null | head -n1 || true)"
if [[ -z "$TAILSCALE_IP" ]]; then
echo "No active Tailscale IPv4 address found." >&2
exit 2
fi
DNS_NAME="$(tailscale status --json 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); print((d.get("Self",{}).get("DNSName") or "").rstrip("."))' 2>/dev/null || true)"
echo "==> Rebuilding live AzerothAI backend with current source"
cd "$REPO_ROOT"
pnpm build
if [[ -f "$LAB_ROOT/azerothai.pid" ]]; then
OLD_APP_PID="$(cat "$LAB_ROOT/azerothai.pid" 2>/dev/null || true)"
if [[ -n "$OLD_APP_PID" ]]; then kill "$OLD_APP_PID" 2>/dev/null || true; fi
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"
echo "==> Waiting for live AzerothAI backend"
for _ in {1..90}; do
if curl -fsS "http://127.0.0.1:$APP_PORT/api/health" >/dev/null 2>&1; then break; fi
sleep 1
done
curl -fsS "http://127.0.0.1:$APP_PORT/api/health" >/dev/null
echo "==> Restarting autonomous high-level service if this lab is configured"
RUNTIME_ENV="$LAB_ROOT/runtime.env"
if [[ -f "$RUNTIME_ENV" ]] && grep -q '^AUTONOMY_TOKEN=' "$RUNTIME_ENV"; then
bash "$REPO_ROOT/deploy/playable/install-autonomy-service.sh"
fi
echo "==> Ensuring durable ALE database relay is active"
if [[ -f "$RUNTIME_ENV" ]] && grep -q '^ALE_BRIDGE_TOKEN=' "$RUNTIME_ENV" && grep -q '^DOCKER_DB_ROOT_PASSWORD=' "$RUNTIME_ENV"; then
bash "$REPO_ROOT/deploy/playable/install-ale-relay-service.sh"
fi
echo "==> Starting hydration-safe dashboard UI"
mkdir -p "$LAB_ROOT"
if [[ -f "$PID_FILE" ]]; then
OLD_PID="$(cat "$PID_FILE" 2>/dev/null || true)"
if [[ -n "$OLD_PID" ]]; then kill "$OLD_PID" 2>/dev/null || true; fi
fi
pkill -f "vinext.*$DEV_PORT" 2>/dev/null || true
cd "$REPO_ROOT"
nohup pnpm dev >"$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
for _ in {1..90}; do
if curl -fsS "http://127.0.0.1:$DEV_PORT/" >/dev/null 2>&1; then
break
fi
sleep 1
done
if ! curl -fsS "http://127.0.0.1:$DEV_PORT/" >/dev/null 2>&1; then
echo "Dashboard UI dev server did not start." >&2
tail -80 "$LOG_FILE" >&2 || true
exit 3
fi
echo "==> Installing nginx if needed"
if ! command -v nginx >/dev/null 2>&1; then
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nginx
fi
echo "==> Wiring UI to the live AzerothAI API"
sudo tee "$NGINX_SITE" >/dev/null <<EOF
server {
listen 127.0.0.1:$PROXY_PORT;
server_name _;
location ^~ /api/ {
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_set_header Host \$http_host;
proxy_set_header X-Forwarded-Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://127.0.0.1:$APP_PORT;
add_header Cache-Control "no-store" always;
}
location / {
proxy_http_version 1.1;
proxy_set_header Host 127.0.0.1:$DEV_PORT;
proxy_set_header X-Forwarded-Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:$DEV_PORT;
add_header Cache-Control "no-store" always;
}
}
EOF
sudo ln -sfn "$NGINX_SITE" "$NGINX_LINK"
sudo nginx -t
sudo systemctl enable --now nginx
sudo systemctl reload nginx
echo "==> Verifying dashboard and live API through proxy"
VERIFY_HOST="${DNS_NAME:-$TAILSCALE_IP}"
curl -fsS -H "Host: $VERIFY_HOST" "http://127.0.0.1:$PROXY_PORT/" >/dev/null
curl -fsS -H "Host: $VERIFY_HOST" "http://127.0.0.1:$PROXY_PORT/api/state" >/dev/null
ORIGIN="https://$VERIFY_HOST"
ACTION_BODY="$(mktemp)"
ACTION_CODE="$(curl -sS -o "$ACTION_BODY" -w '%{http_code}' -X POST -H "Host: $VERIFY_HOST" -H "Origin: $ORIGIN" -H "Content-Type: application/json" --data '{}' "http://127.0.0.1:$PROXY_PORT/api/actions" || true)"
if [[ "$ACTION_CODE" == "403" ]] && grep -q "Cross-origin action rejected" "$ACTION_BODY"; then
echo "Remote action origin check is still rejecting the trusted tailnet host." >&2
cat "$ACTION_BODY" >&2
rm -f "$ACTION_BODY"
exit 47
fi
rm -f "$ACTION_BODY"
echo "Remote action origin check: accepted (HTTP $ACTION_CODE expected for empty test action)"
echo "==> Publishing privately with Tailscale Serve"
sudo tailscale serve --bg "$PROXY_PORT"
STAMP="$(date +%s)"
echo
echo "============================================================"
echo " AzerothAI remote dashboard is ready"
echo "============================================================"
if [[ -n "$DNS_NAME" ]]; then
echo "Open this fresh URL:"
echo " https://$DNS_NAME/?v=$STAMP"
else
echo "Run: tailscale serve status"
fi
echo
echo "UI log: $LOG_FILE"
echo "Live API remains on port $APP_PORT."
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
LAB_ROOT="${AZEROTHAI_PLAYABLE_ROOT:-$HOME/azerothai-pc-lab}"
cd "$REPO_ROOT"
echo "==> Rebuilding AzerothAI so server HTML and client bundles match"
pnpm build
echo "==> Restarting AzerothAI runtime"
if [[ -f "$LAB_ROOT/azerothai.pid" ]]; then
PID="$(cat "$LAB_ROOT/azerothai.pid" 2>/dev/null || true)"
if [[ -n "$PID" ]]; then kill "$PID" 2>/dev/null || true; fi
sleep 1
fi
nohup pnpm start:lan >"$LAB_ROOT/azerothai.log" 2>&1 &
echo $! > "$LAB_ROOT/azerothai.pid"
echo "==> Waiting for dashboard health"
for _ in {1..90}; 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 "==> Rebuilding remote Tailscale dashboard proxy"
bash "$REPO_ROOT/deploy/pc-lab/tailscale-dashboard.sh"
DNS_NAME="$(tailscale status --json 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); print((d.get("Self",{}).get("DNSName") or "").rstrip("."))' 2>/dev/null || true)"
STAMP="$(date +%s)"
echo
echo "Remote dashboard repair complete."
if [[ -n "$DNS_NAME" ]]; then
echo "Open this fresh URL:"
echo " https://$DNS_NAME/?v=$STAMP"
fi
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
STATIC_ROOT="$REPO_ROOT/dist/client"
PUBLIC_ROOT="/var/www/azerothai"
NGINX_SITE="/etc/nginx/sites-available/azerothai-tailnet"
NGINX_LINK="/etc/nginx/sites-enabled/azerothai-tailnet"
LOCAL_PROXY_PORT="${AZEROTHAI_TAILNET_PROXY_PORT:-8788}"
APP_PORT="${AZEROTHAI_APP_PORT:-8787}"
need(){ command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }; }
if [[ ! -d "$STATIC_ROOT" ]]; then
echo "Missing built client assets at $STATIC_ROOT" >&2
echo "Run: cd $REPO_ROOT && pnpm build" >&2
exit 1
fi
need tailscale
need curl
TAILSCALE_IP="$(tailscale ip -4 2>/dev/null | head -n1 || true)"
if [[ -z "$TAILSCALE_IP" ]]; then
echo "No active Tailscale IPv4 address found. Run: sudo tailscale up --hostname=azerothai-lab" >&2
exit 2
fi
echo "==> Checking AzerothAI backend"
curl -fsS "http://127.0.0.1:$APP_PORT/api/health" >/dev/null || {
echo "AzerothAI is not responding on 127.0.0.1:$APP_PORT" >&2
exit 1
}
echo "==> Installing nginx if needed"
if ! command -v nginx >/dev/null 2>&1; then
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nginx
fi
echo "==> Publishing built client assets to an nginx-readable directory"
sudo rm -rf "$PUBLIC_ROOT"
sudo mkdir -p "$PUBLIC_ROOT"
sudo cp -a "$STATIC_ROOT"/. "$PUBLIC_ROOT"/
sudo find "$PUBLIC_ROOT" -type d -exec chmod 755 {} +
sudo find "$PUBLIC_ROOT" -type f -exec chmod 644 {} +
echo "==> Configuring a static-asset-safe reverse proxy"
sudo tee "$NGINX_SITE" >/dev/null <<EOF
server {
listen 127.0.0.1:$LOCAL_PROXY_PORT;
server_name _;
root $PUBLIC_ROOT;
location ~* \\.(?:css|js|mjs|svg|png|ico|woff|woff2)$ {
try_files \$uri @azerothai;
access_log off;
add_header Cache-Control "no-store" always;
}
location / {
add_header Cache-Control "no-store" always;
proxy_http_version 1.1;
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://127.0.0.1:$APP_PORT;
}
location @azerothai {
proxy_http_version 1.1;
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://127.0.0.1:$APP_PORT;
}
}
EOF
sudo ln -sfn "$NGINX_SITE" "$NGINX_LINK"
sudo nginx -t
sudo systemctl enable --now nginx
sudo systemctl reload nginx
echo "==> Verifying proxied page and repairing asset paths if needed"
PAGE="$(curl -fsS -H "Host: $TAILSCALE_IP:$LOCAL_PROXY_PORT" "http://127.0.0.1:$LOCAL_PROXY_PORT/")"
map_asset_path(){
local raw="$1" clean target base match srcdir dstdir
clean="${raw%%\?*}"
[[ "$clean" == /* ]] || return 0
target="$PUBLIC_ROOT$clean"
[[ -f "$target" ]] && return 0
base="$(basename "$clean")"
match="$(find "$PUBLIC_ROOT" "$REPO_ROOT/dist" "$REPO_ROOT/.next" -type f -name "$base" -print -quit 2>/dev/null || true)"
[[ -n "$match" ]] || return 0
dstdir="$(dirname "$target")"
sudo mkdir -p "$dstdir"
sudo cp -a "$match" "$target"
}
while IFS= read -r asset; do
[[ -n "$asset" ]] || continue
map_asset_path "$asset"
done < <(printf '%s' "$PAGE" | grep -oE '(href|src)="[^"]+\.(css|js|mjs|svg|png|ico|woff|woff2)(\?[^"]*)?"' | cut -d'"' -f2 | sort -u || true)
CSS_PATH="$(printf '%s' "$PAGE" | grep -oE 'href="[^"]+\.css[^"]*"' | head -n1 | cut -d'"' -f2 || true)"
if [[ -n "$CSS_PATH" ]]; then
case "$CSS_PATH" in
http://*|https://*) CSS_URL="$CSS_PATH" ;;
/*) CSS_URL="http://127.0.0.1:$LOCAL_PROXY_PORT$CSS_PATH" ;;
*) CSS_URL="http://127.0.0.1:$LOCAL_PROXY_PORT/$CSS_PATH" ;;
esac
if curl -fsS -H "Host: $TAILSCALE_IP:$LOCAL_PROXY_PORT" "$CSS_URL" >/dev/null; then
echo "Stylesheet reachable through proxy: $CSS_PATH"
else
echo "Stylesheet still returned 404: $CSS_PATH" >&2
echo "Built CSS files found:" >&2
find "$PUBLIC_ROOT" -type f -name '*.css' -print | head -20 >&2 || true
exit 44
fi
else
echo "Warning: no stylesheet link found in HTML." >&2
fi
echo "==> Verifying all JavaScript bundles through proxy"
JS_FAILURES=0
while IFS= read -r script; do
[[ -n "$script" ]] || continue
case "$script" in
http://*|https://*) JS_URL="$script" ;;
/*) JS_URL="http://127.0.0.1:$LOCAL_PROXY_PORT$script" ;;
*) JS_URL="http://127.0.0.1:$LOCAL_PROXY_PORT/$script" ;;
esac
CODE="$(curl -sS -o /dev/null -w '%{http_code}' -H "Host: $TAILSCALE_IP:$LOCAL_PROXY_PORT" "$JS_URL" || true)"
echo "$CODE $script"
if [[ "$CODE" != "200" ]]; then JS_FAILURES=$((JS_FAILURES+1)); fi
done < <(printf '%s' "$PAGE" | grep -oE 'src="[^"]+\\.(js|mjs)(\\?[^"]*)?"' | cut -d'"' -f2 | sort -u || true)
STATE_CODE="$(curl -sS -o /dev/null -w '%{http_code}' -H "Host: $TAILSCALE_IP:$LOCAL_PROXY_PORT" "http://127.0.0.1:$LOCAL_PROXY_PORT/api/state" || true)"
echo "$STATE_CODE /api/state"
if [[ "$STATE_CODE" != "200" ]]; then
echo "Dashboard API state is not reachable through the proxy." >&2
exit 45
fi
if [[ "$JS_FAILURES" -ne 0 ]]; then
echo "$JS_FAILURES JavaScript bundle(s) are still unavailable." >&2
exit 46
fi
echo "==> Publishing privately with Tailscale Serve"
sudo tailscale serve --bg "$LOCAL_PROXY_PORT"
DNS_NAME="$(tailscale status --json 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); print((d.get("Self",{}).get("DNSName") or "").rstrip("."))' 2>/dev/null || true)"
echo
echo "============================================================"
echo " AzerothAI private dashboard proxy is ready"
echo "============================================================"
if [[ -n "$DNS_NAME" ]]; then
echo "Open on any device in your tailnet:"
echo " https://$DNS_NAME/"
else
echo "Run: tailscale serve status"
echo "Then open the HTTPS URL it prints."
fi
echo
echo "Direct raw-IP access to :8787 can still be used for diagnostics,"
echo "but the Tailscale Serve HTTPS URL is the recommended remote dashboard."
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
if [[ "$(id -u)" -eq 0 ]]; then
echo "Run this as your normal Ubuntu user, not root." >&2
exit 2
fi
discover_windows_host(){
local src prefix network candidate
src="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}')"
prefix="$(ip -4 -o addr show scope global | awk 'NR==1{print $4}' | cut -d/ -f2)"
if [[ -n "$src" && -n "$prefix" && "$prefix" -ge 24 ]]; then
network="$(echo "$src" | awk -F. '{print $1"."$2"."$3}')"
candidate="$network.1"
if curl -fsS --connect-timeout 2 "http://$candidate:11434/api/tags" >/dev/null 2>&1; then
printf '%s' "$candidate"
return
fi
fi
local gw
gw="$(ip route | awk '/default/ {print $3; exit}')"
if [[ -n "$gw" ]] && curl -fsS --connect-timeout 2 "http://$gw:11434/api/tags" >/dev/null 2>&1; then
printf '%s' "$gw"
fi
}
if [[ -z "${WINDOWS_HOST_IP:-}" ]]; then
WINDOWS_HOST_IP="$(discover_windows_host || true)"
fi
if ! sudo -n true 2>/dev/null; then
echo "This setup needs sudo for Docker/packages. Ubuntu may ask for your password."
fi
echo "==> Installing Ubuntu prerequisites"
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl git jq openssl python3 unzip lvm2
echo "==> Expanding Ubuntu root LVM to use free virtual-disk space"
ROOT_DEV="$(findmnt -n -o SOURCE / || true)"
if [[ "$ROOT_DEV" == /dev/mapper/* ]] && command -v lvs >/dev/null 2>&1; then
FREE_BYTES="$(sudo vgs --noheadings --units b --nosuffix -o vg_free 2>/dev/null | awk '{sum+=$1} END{printf "%.0f",sum}')"
if [[ "${FREE_BYTES:-0}" -gt 1073741824 ]]; then
sudo lvextend -l +100%FREE -r "$ROOT_DEV"
fi
fi
df -h /
if ! command -v node >/dev/null 2>&1 || [[ "$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)" -lt 22 ]]; then
echo "==> Installing Node.js 22"
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs
fi
if ! command -v pnpm >/dev/null 2>&1; then
echo "==> Installing pnpm"
sudo corepack enable
sudo corepack prepare pnpm@11.25.0 --activate
fi
echo "Node: $(node --version)"
echo "pnpm: $(pnpm --version)"
if ! command -v docker >/dev/null 2>&1; then
echo "==> Installing Docker Engine"
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
echo
echo "Docker was installed and your user was added to the docker group."
echo "Log out of Ubuntu and back in, then rerun this script."
exit 20
fi
if ! docker info >/dev/null 2>&1; then
echo "Docker exists but this user cannot access it yet."
echo "Log out of Ubuntu and back in, then rerun this script."
exit 21
fi
echo "==> Checking Windows-hosted Ollama"
OLLAMA_AVAILABLE=0
if [[ -n "${WINDOWS_HOST_IP:-}" ]] && curl -fsS --connect-timeout 4 "http://$WINDOWS_HOST_IP:11434/api/tags" >/tmp/ollama-tags.json; then
OLLAMA_AVAILABLE=1
echo "Ollama reachable at http://$WINDOWS_HOST_IP:11434"
else
echo "Windows Ollama is not reachable yet. Continuing with deterministic AzerothAI decisions."
echo "The game realm can boot first; Qwen can be connected afterward without rebuilding AzerothCore."
fi
echo "==> Bootstrapping playable AzerothAI realm"
export AZEROTHAI_PLAYABLE_ROOT="${AZEROTHAI_PLAYABLE_ROOT:-$HOME/azerothai-pc-lab}"
bash "$REPO_ROOT/deploy/playable/bootstrap.sh"
LAB_ROOT="$AZEROTHAI_PLAYABLE_ROOT"
if [[ "$OLLAMA_AVAILABLE" -eq 1 ]]; then
echo "==> Pointing AzerothAI at Qwen on the Windows RTX host"
DEV_VARS="$REPO_ROOT/.dev.vars"
python3 - "$DEV_VARS" "http://$WINDOWS_HOST_IP:11434" <<'PY'
from pathlib import Path
import sys
p=Path(sys.argv[1])
value=sys.argv[2]
lines=p.read_text().splitlines() if p.exists() else []
out=[]
found=False
for line in lines:
if line.startswith("LLM_URL="):
out.append("LLM_URL="+value)
found=True
else:
out.append(line)
if not found:
out.append("LLM_URL="+value)
p.write_text("\n".join(out).rstrip()+"\n")
PY
chmod 600 "$DEV_VARS"
echo "==> Restarting AzerothAI runtime with host Ollama endpoint"
if [[ -f "$LAB_ROOT/azerothai.pid" ]]; then
pid="$(cat "$LAB_ROOT/azerothai.pid")"
kill "$pid" 2>/dev/null || true
sleep 1
fi
cd "$REPO_ROOT"
nohup pnpm start:lan >"$LAB_ROOT/azerothai.log" 2>&1 &
echo $! > "$LAB_ROOT/azerothai.pid"
for _ in {1..60}; do
if curl -fsS http://127.0.0.1:8787/api/health >/tmp/azerothai-health.json 2>/dev/null; then break; fi
sleep 1
done
fi
VM_IP="$(hostname -I | awk '{print $1}')"
echo
echo "============================================================"
echo " TEMPORARY PC LAB READY"
echo "============================================================"
echo "Ubuntu VM IP : $VM_IP"
echo "Windows host : ${WINDOWS_HOST_IP:-not connected yet}"
if [[ "$OLLAMA_AVAILABLE" -eq 1 ]]; then
echo "Ollama : http://$WINDOWS_HOST_IP:11434"
else
echo "Ollama : not connected yet (realm still usable)"
fi
echo
echo "Next:"
echo " 1. Create your WoW/GM account using the worldserver console."
echo " 2. Put the Ubuntu VM IP into Windows WoW realmlist.wtf."
echo " 3. Open AzerothAI World Monitor from Windows/iPad:"
echo " http://$VM_IP:8787"
echo
echo "Check everything:"
echo " AZEROTHAI_PLAYABLE_ROOT=$AZEROTHAI_PLAYABLE_ROOT bash deploy/playable/status.sh"
+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."