mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
352 lines
16 KiB
Bash
352 lines
16 KiB
Bash
#!/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: 60
|
||
|
||
# --- 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."
|