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,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`.
|
||||
@@ -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"
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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."
|
||||
@@ -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"
|
||||
@@ -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