mod-playerbots/extract_ac_client_data.sh
2026-05-14 19:48:38 +02:00

352 lines
16 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# =============================================================================
# AzerothCore client-data extraction for mod-playerbots.
# Run from anywhere — output lands in $SERVER_DATA_DIR.
# =============================================================================
set -euo pipefail
# ─── PATHS ──────────────────────────────────────────────────────────────────
WOW_CLIENT_DATA="/home/dev/wow_client_data"
TOOLS_DIR="/home/dev/azerothcore_installer/_server/azerothcore/env/dist/bin"
SERVER_DATA_DIR="$TOOLS_DIR"
# ─── TOGGLES ────────────────────────────────────────────────────────────────
EXTRACT_DBC_AND_MAPS=true
EXTRACT_VMAPS=true
EXTRACT_MMAPS=true
MMAP_THREADS=0 # 0 = auto-detect (each thread uses 1-2 GB RAM)
MMAP_SINGLE_MAP="" # e.g. "489" for Warsong Gulch only
# Verbatim copy of azerothcore-wotlk/master:src/tools/mmaps_generator/mmaps-config.yaml
# (also the same config the mod-playerbots fork ships).
MMAPS_CONFIG_YAML=$(cat <<'YAML_EOF'
mmapsConfig:
skipLiquid: false
skipContinents: false
skipJunkMaps: true
skipBattlegrounds: false
# Path to the directory containing navigation data files.
# This directory should contain the "maps" and "vmaps" folders,
# and is also where the "mmaps" folder will be created or located.
dataDir: "./"
meshSettings:
# Here we have global config for recast navigation.
# It's possible to override these data on map or tile level (see mapsOverrides).
# Maximum slope angle (in degrees) NPCs can walk on.
# Surfaces steeper than this will be considered unwalkable.
walkableSlopeAngle: 50
# --- Cell Size Calculation ---
# Many parameters below are defined in "cell units".
# In RecastDemo, you often work with world units instead of cell units.
# The actual generator uses (src/tools/mmaps_generator/Config.cpp:28):
#
# cellSize = MMAP::GRID_SIZE / vertexPerMapEdge
#
# Where:
# MMAP::GRID_SIZE = 533.3333f (the size of one map tile in world units)
# vertexPerMapEdge = number of vertices along one edge of the full map grid
#
# Example (AC stock):
# vertexPerMapEdge = 2000 → cellSize ≈ 533.3333 / 2000 ≈ 0.2667 yd
#
# IMPORTANT: when changing vertexPerMapEdge, the per-cell parameters
# below (walkableHeight, walkableClimb, walkableRadius) must be re-scaled
# to preserve their world-unit semantics. Doubling vertexPerMapEdge
# halves cellSize, so cell counts must double to keep the same yd value.
#
# To convert a value from cell units to world units (e.g., walkableClimb),
# multiply by cellSize. For example, a walkableClimb of 6 at 2000 resolution:
# 6 × 0.2667 ≈ 1.60 yd
# Minimum ceiling height (in cell units) NPCs need to pass under an obstacle.
# Controls how much vertical clearance is required.
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
# 6 cells × 0.2667 yd ≈ 1.60 yd — matches WoW player capsule height at
# 2000 resolution (AC stock). Preserves the 1.60 yd world-unit
# ceiling-clearance requirement.
walkableHeight: 6
# Maximum height difference (in cell units) NPCs can step up or down.
# Higher values allow walking over fences, ledges, or steps.
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
#
# Vanilla WotLK uses 6, which allows creatures to "jump" over fences.
# Classic WotLK uses 4, which forces creatures to walk around fences.
# 6 cells × 0.2667 yd ≈ 1.60 yd — Vanilla-WotLK step semantics at
# 2000 resolution. Preserves the 1.60 yd world-unit step. The mmap
# is shared with every creature, NPC patrol, escort, and quest mob;
# tightening below stock breaks patrols that cross 1.5y ledges.
walkableClimb: 4
# Minimum distance (in cell units) around walkable surfaces.
# Helps prevent NPCs from clipping into walls and narrow gaps.
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
# 2 cells × 0.2667 yd ≈ 0.53 yd — AC stock world-unit buffer at
# 2000 resolution. Tested wider (= 0.71y world units): erodes polys
# near mountains/cliffs so pathfinder routes through surviving
# (higher/worse) polys → bot climbs mountains.
walkableRadius: 2
# Number of vertices along one edge of the entire map's navmesh grid.
# Higher values increase mesh resolution but also CPU/memory usage.
# 2000 = AC stock baseline. cellSize ≈ 0.2667 yd.
vertexPerMapEdge: 2000
# Number of vertices along one edge of each tile chunk.
# Must divide vertexPerMapEdge evenly — the generator uses integer
# division: tilesPerMapEdge = vertexPerMap / vertexPerTile
# (src/tools/mmaps_generator/Config.cpp:144).
# A higher vertex count per tile means fewer total tiles,
# reducing runtime work to load, unload, and manage tiles.
# 80 = AC stock baseline. 2000 / 80 = 25 tiles per map edge, 625
# tiles per map (~21y per tile). Lots of small tiles, low per-tile
# RAM, more seams to stitch across.
vertexPerTileEdge: 80
# Tolerance for how much a polygon can deviate from the original geometry when simplified.
# Higher values produce simpler (faster) meshes but can reduce accuracy.
# 0.8 (vs the AC stock 1.8 and recast canonical 1.3) keeps polygon
# edges close to real terrain. Targets "merged step into ramp"
# simplification artifacts that produce corner-cuts and false NOPATH.
maxSimplificationError: 0.8
# You can override any global parameter for a specific map by specifying its map ID.
# Inside each map override, you can also override parameters per individual tile,
# identified by a string "tileX,tileY" (coordinates).
#
# Overrides cascade: global settings → map overrides → tile overrides.
# For example:
#
# mapsOverrides:
# "0": # Map ID 0 overrides
# walkableRadius: 5 # Override global climb height for entire map 0
#
# tilesOverrides:
# "50,70": # Tile at coordinates (50,70) on map 0
# walkableSlopeAngle: 70 # Override slope angle locally just here
# walkableClimb: 4 # Also override climb height for this tile only
#
# "51,71":
# walkableClimb: 3 # Override climb height for tile (51,71)
#
# "48,32":
# walkableClimb: 1 # Even smaller climb for tile (48,32)
#
# "1": # Map ID 1 overrides example
# walkableHeight: 8 # Increase clearance for whole map 1
#
# tilesOverrides:
# "100,100":
# maxSimplificationError: 2.5 # Looser mesh simplification for this tile only
#
# "101,101":
# walkableRadius: 1 # Smaller NPC radius here for tight corridors
#
# This approach allows very fine-grained control of navigation mesh parameters
# on a per-map and per-tile basis, optimizing pathfinding quality and performance.
#
# All parameters defined globally are eligible for override.
# Just specify the parameter name and new value in the override section.
mapsOverrides:
"562": # Blade's Edge Arena
walkableRadius: 0 # This allows walking on the ropes to the pillars
"48": # Blackfathom Deeps
cellSizeVertical: 0.5334 # ch*2 = 0.2667 * 2 ≈ 0.5334. Reduce the chance to have underground levels.
"529": # Arathi Basin
tilesOverrides:
"30,29": # Lumber Mill
# Make sure that Fear will not drop players rom cliff -
# https://github.com/azerothcore/azerothcore-wotlk/pull/22462#issuecomment-3067024680
walkableSlopeAngle: 45
"530": # Outland
tilesOverrides:
"32,30": # Dark portal
walkableSlopeAngle: 45 # https://github.com/chromiecraft/chromiecraft/issues/8404#issuecomment-3476012660
# debugOutput generates debug files in the `meshes` directory for use with RecastDemo.
# This is useful for inspecting and debugging mmap generation visually.
#
# My workflow:
# 1. Install RecastDemo. I'm building it from the source of this fork: https://github.com/jackpoz/recastnavigation
# 2. In-game, move your character to the area you want to debug.
# 3. Type `.mmap loc` in chat. This will output:
# - The current tile file name (e.g., `04832.mmtile`)
# - The Recast config values used to generate that tile
# 4. Enable `debugOutput` and regenerate mmaps (preferably just the tile from step 3).
# - To regenerate only one tile, delete it from the `mmaps` folder.
# 5. After generation, you will find debug files in the `meshes` folder, including an OBJ file (e.g., `map0004832.obj`)
# 6. Copy these debug files to the `Meshes` folder used by RecastDemo.
# - RecastDemo expects this folder to be in the same directory as its executable.
# 7. In RecastDemo:
# - Click "Input Mesh" and select the `.obj` file
# - Choose "Solo Mesh" in the Sample selector
# 8. (Optional) Reuse the Recast config values from step 3:
# - `cellSizeHorizontal` → "Cell Size"
# - `walkableSlopeAngle` → "Max Slope"
# - `walkableClimb` → "Max Climb"
# - and so on
# 9. Scroll to the bottom of RecastDemo UI and press "Build" to generate the navigation mesh
debugOutput: false
YAML_EOF
)
# =============================================================================
# ─── DO NOT EDIT BELOW ──────────────────────────────────────────────────────
# =============================================================================
[ -n "$SERVER_DATA_DIR" ] || { echo "SERVER_DATA_DIR is not set"; exit 1; }
[ -n "$WOW_CLIENT_DATA" ] || { echo "WOW_CLIENT_DATA is not set"; exit 1; }
mkdir -p "$SERVER_DATA_DIR"
cd "$SERVER_DATA_DIR"
# ─── SAFETY: source MPQs are READ-ONLY to this script ──────────────────────
# Resolve both paths to canonical form and refuse to run if the output dir
# is inside the source. Combined with safe_rm() below, this script cannot
# touch any file inside WOW_CLIENT_DATA.
SERVER_DATA_DIR_REAL="$(cd "$SERVER_DATA_DIR" && pwd -P)"
WOW_CLIENT_DATA_REAL="$(cd "$WOW_CLIENT_DATA" && pwd -P 2>/dev/null || echo "$WOW_CLIENT_DATA")"
case "$SERVER_DATA_DIR_REAL/" in
"$WOW_CLIENT_DATA_REAL"/|"$WOW_CLIENT_DATA_REAL"/*)
echo "ERROR: SERVER_DATA_DIR ($SERVER_DATA_DIR_REAL) is inside WOW_CLIENT_DATA — refusing." >&2
exit 1
;;
esac
# Refuses to remove anything outside SERVER_DATA_DIR. Resolves the parent
# to absolute path so a symlink inside cwd can't trick us into traversing
# into the source. Use this for every cleanup in this script.
safe_rm() {
local target="$1"
local parent_abs base
parent_abs="$(cd "$(dirname -- "$target")" 2>/dev/null && pwd -P)" || return 0
base="$(basename -- "$target")"
local abs="$parent_abs/$base"
case "$abs/" in
"$SERVER_DATA_DIR_REAL"/|"$SERVER_DATA_DIR_REAL"/*) ;;
*)
echo "REFUSING to rm path outside SERVER_DATA_DIR: $target$abs" >&2
exit 1 ;;
esac
rm -rf -- "$target"
}
[ "$MMAP_THREADS" -eq 0 ] && MMAP_THREADS=$(nproc 2>/dev/null || echo 4)
echo "Working dir : $(pwd)"
echo "Tools dir : $TOOLS_DIR"
echo "Threads : $MMAP_THREADS"
echo "Steps : maps=$EXTRACT_DBC_AND_MAPS vmaps=$EXTRACT_VMAPS mmaps=$EXTRACT_MMAPS"
echo
# ─── Symlink Data/ → MPQ source (only when extracting from client) ──────────
if [ "$EXTRACT_DBC_AND_MAPS" = true ] || [ "$EXTRACT_VMAPS" = true ]; then
has_mpqs() { find "$1" -maxdepth 1 -iname "*.mpq" -print -quit 2>/dev/null | grep -q .; }
if has_mpqs "$WOW_CLIENT_DATA"; then
MPQ_DIR="$WOW_CLIENT_DATA"
elif has_mpqs "$WOW_CLIENT_DATA/Data"; then
MPQ_DIR="$WOW_CLIENT_DATA/Data"
else
echo "ERROR: no .mpq files in $WOW_CLIENT_DATA" >&2
exit 1
fi
MPQ_DIR="$(cd "$MPQ_DIR" && pwd)"
# Symlink only — refuse to clobber an existing real directory.
if [ -e Data ] && [ ! -L Data ]; then
echo "ERROR: Data/ exists in $(pwd) but is not a symlink" >&2
exit 1
fi
ln -sfn "$MPQ_DIR" Data
echo "Data/ → $MPQ_DIR"
fi
# ─── STEP 1: DBCs + Maps ────────────────────────────────────────────────────
if [ "$EXTRACT_DBC_AND_MAPS" = true ]; then
echo
echo "[1/3] Extracting DBCs + Maps..."
# Clean slate — map_extractor refuses to run if these dirs already exist.
safe_rm dbc
safe_rm maps
safe_rm Cameras
# -e 7 = bitfield MAP(1)|DBC(2)|CAMERA(4) — extract everything.
# The old "-e 2" was DBC-only and skipped maps + cameras entirely.
"$TOOLS_DIR/map_extractor" -e 7 -f 0
if [ ! -d maps ] || [ -z "$(ls -A maps 2>/dev/null)" ]; then
echo "ERROR: map_extractor finished but maps/ is empty — check its output above" >&2
exit 1
fi
fi
# ─── STEP 2: VMaps ──────────────────────────────────────────────────────────
if [ "$EXTRACT_VMAPS" = true ]; then
echo
echo "[2/3] Extracting VMaps..."
# Clean slate — vmap4_extractor refuses to run if these dirs already exist.
safe_rm Buildings
safe_rm vmaps
"$TOOLS_DIR/vmap4_extractor" -l -d ./Data
mkdir -p vmaps
"$TOOLS_DIR/vmap4_assembler" Buildings vmaps
safe_rm Buildings
if [ ! -d vmaps ] || [ -z "$(ls -A vmaps 2>/dev/null)" ]; then
echo "ERROR: vmap4_assembler finished but vmaps/ is empty — check output above" >&2
exit 1
fi
fi
# ─── STEP 3: MMaps ──────────────────────────────────────────────────────────
if [ "$EXTRACT_MMAPS" = true ]; then
if [ ! -d maps ]; then
echo "ERROR: maps/ missing in $(pwd) — run with EXTRACT_DBC_AND_MAPS=true once" >&2
exit 1
fi
if [ ! -d vmaps ]; then
echo "ERROR: vmaps/ missing in $(pwd) — run with EXTRACT_VMAPS=true once" >&2
exit 1
fi
echo
echo "[3/3] Generating MMaps... (do not interrupt)"
printf '%s\n' "$MMAPS_CONFIG_YAML" > mmaps-config.yaml
# Wipe any existing tiles before regenerating. Mixed tiles from
# previous runs (different cellSize / verticesPerTileEdge / etc.)
# would otherwise be silently kept and mixed with new ones,
# producing a corrupt navmesh. Clean slate every mmap run.
safe_rm mmaps
mkdir -p mmaps
# Workaround: some mmaps_generator builds write a few tiles to /mmaps
# via an absolute path. Pre-create it so the writes don't fail, then
# fold the strays into our local mmaps/ at the end.
sudo rm -rf /mmaps
sudo mkdir -p /mmaps && sudo chmod 777 /mmaps
CMD=("$TOOLS_DIR/mmaps_generator" --config mmaps-config.yaml --threads "$MMAP_THREADS")
[ -n "$MMAP_SINGLE_MAP" ] && CMD+=("$MMAP_SINGLE_MAP")
START=$(date +%s)
"${CMD[@]}"
ELAPSED=$(( $(date +%s) - START ))
if compgen -G "/mmaps/*.mmtile" >/dev/null; then
cp /mmaps/*.mmtile mmaps/ && rm -f /mmaps/*.mmtile
fi
echo
echo "MMap done in $((ELAPSED / 60))m $((ELAPSED % 60))s"
echo "Tiles: $(ls mmaps/*.mmtile 2>/dev/null | wc -l)"
fi
echo
echo "Done. Restart worldserver to pick up changes."