#!/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: 6 # 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: 1.0 # 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."