mirror of
https://github.com/uprightbass360/AzerothCore-RealmMaster.git
synced 2026-01-13 00:58:34 +00:00
adding automatic module inclusion features and bugfixes
This commit is contained in:
76
scripts/check_module_staging.py
Executable file
76
scripts/check_module_staging.py
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_module_state(root: Path) -> dict:
|
||||
env_path = root / ".env"
|
||||
manifest_path = root / "config" / "modules.json"
|
||||
modules_py = root / "scripts" / "modules.py"
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
[
|
||||
sys.executable,
|
||||
str(modules_py),
|
||||
"--env-path",
|
||||
str(env_path),
|
||||
"--manifest",
|
||||
str(manifest_path),
|
||||
"dump",
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print("Unable to load module state:", exc, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
return json.loads(output)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
data = load_module_state(root)
|
||||
|
||||
enabled_modules = [m for m in data["modules"] if m["enabled"]]
|
||||
storage_dir = root / "storage" / "modules"
|
||||
|
||||
local_root = Path(os.environ.get("STORAGE_PATH_LOCAL", "./local-storage"))
|
||||
local_root = (root / local_root).resolve()
|
||||
requires_playerbots = any(m["key"] == "MODULE_PLAYERBOTS" and m["enabled"] for m in enabled_modules)
|
||||
source_dir = local_root / "source"
|
||||
source_dir = source_dir / ("azerothcore-playerbots" if requires_playerbots else "azerothcore") / "modules"
|
||||
|
||||
print(f"📦 Checking module staging in {storage_dir} and {source_dir}")
|
||||
print("Enabled modules:", ", ".join(m["name"] for m in enabled_modules))
|
||||
|
||||
status = 0
|
||||
for module in enabled_modules:
|
||||
dir_name = module["name"]
|
||||
storage_path = storage_dir / dir_name
|
||||
source_path = source_dir / dir_name
|
||||
|
||||
def state(path: Path) -> str:
|
||||
if (path / ".git").is_dir():
|
||||
return "git"
|
||||
if path.is_dir():
|
||||
return "present"
|
||||
return "missing"
|
||||
|
||||
storage_state = state(storage_path)
|
||||
source_state = state(source_path)
|
||||
print(f" - {dir_name} ({module['key']}): storage={storage_state}, source={source_state}")
|
||||
|
||||
if storage_state == "missing" or source_state == "missing":
|
||||
status = 1
|
||||
|
||||
return status
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -6,6 +6,75 @@ trap 'echo " ❌ SQL helper error (line ${LINENO}): ${BASH_COMMAND}" >&2' ERR
|
||||
CUSTOM_SQL_ROOT="/tmp/scripts/sql/custom"
|
||||
ALT_CUSTOM_SQL_ROOT="/scripts/sql/custom"
|
||||
|
||||
SQL_SUCCESS_LOG=()
|
||||
SQL_FAILURE_LOG=()
|
||||
TEMP_SQL_FILES=()
|
||||
|
||||
render_sql_file_for_execution(){
|
||||
local src="$1"
|
||||
local pb_db="${DB_PLAYERBOTS_NAME:-acore_playerbots}"
|
||||
local rendered="$src"
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
local temp
|
||||
temp="$(mktemp)"
|
||||
local result
|
||||
result="$(python3 - "$src" "$temp" "$pb_db" <<'PY'
|
||||
import sys, pathlib, re
|
||||
src, dest, pb_db = sys.argv[1:]
|
||||
text = pathlib.Path(src).read_text()
|
||||
original = text
|
||||
text = text.replace("{{PLAYERBOTS_DB}}", pb_db)
|
||||
pattern = re.compile(r'(?<![.`])\bplayerbots\b')
|
||||
text = pattern.sub(f'`{pb_db}`.playerbots', text)
|
||||
pathlib.Path(dest).write_text(text)
|
||||
print("changed" if text != original else "unchanged", end="")
|
||||
PY
|
||||
)"
|
||||
if [ "$result" = "changed" ]; then
|
||||
rendered="$temp"
|
||||
TEMP_SQL_FILES+=("$temp")
|
||||
else
|
||||
rm -f "$temp"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$rendered"
|
||||
}
|
||||
|
||||
log_sql_success(){
|
||||
local target_db="$1"
|
||||
local sql_file="$2"
|
||||
SQL_SUCCESS_LOG+=("${target_db}::${sql_file}")
|
||||
}
|
||||
|
||||
log_sql_failure(){
|
||||
local target_db="$1"
|
||||
local sql_file="$2"
|
||||
SQL_FAILURE_LOG+=("${target_db}::${sql_file}")
|
||||
}
|
||||
|
||||
mysql_exec(){
|
||||
local mysql_port="${MYSQL_PORT:-3306}"
|
||||
if command -v mariadb >/dev/null 2>&1; then
|
||||
mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P "$mysql_port" -u root -p"${MYSQL_ROOT_PASSWORD}" "$@"
|
||||
return
|
||||
fi
|
||||
if command -v mysql >/dev/null 2>&1; then
|
||||
mysql --ssl-mode=DISABLED -h "${CONTAINER_MYSQL}" -P "$mysql_port" -u root -p"${MYSQL_ROOT_PASSWORD}" "$@"
|
||||
return
|
||||
fi
|
||||
echo " ❌ Neither mariadb nor mysql client is available for SQL execution" >&2
|
||||
return 127
|
||||
}
|
||||
|
||||
playerbots_table_exists(){
|
||||
local pb_db="${DB_PLAYERBOTS_NAME:-acore_playerbots}"
|
||||
local count
|
||||
count="$(mysql_exec -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${pb_db}' AND table_name='playerbots';" 2>/dev/null || echo 0)"
|
||||
[ "${count}" != "0" ]
|
||||
}
|
||||
|
||||
run_custom_sql_group(){
|
||||
local subdir="$1" target_db="$2" label="$3"
|
||||
local dir="${CUSTOM_SQL_ROOT}/${subdir}"
|
||||
@@ -13,25 +82,76 @@ run_custom_sql_group(){
|
||||
dir="${ALT_CUSTOM_SQL_ROOT}/${subdir}"
|
||||
fi
|
||||
[ -d "$dir" ] || return 0
|
||||
LC_ALL=C find "$dir" -type f -name "*.sql" | sort | while read -r sql_file; do
|
||||
while IFS= read -r sql_file; do
|
||||
local base_name
|
||||
base_name="$(basename "$sql_file")"
|
||||
local rendered
|
||||
rendered="$(render_sql_file_for_execution "$sql_file")"
|
||||
if grep -q '\bplayerbots\b' "$rendered"; then
|
||||
if ! playerbots_table_exists; then
|
||||
echo " Skipping ${label}: ${base_name} (playerbots table missing)"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
echo " Executing ${label}: ${base_name}"
|
||||
if mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" "${target_db}" < "$sql_file" >/dev/null 2>&1; then
|
||||
local sql_output
|
||||
sql_output="$(mktemp)"
|
||||
if mysql_exec "${target_db}" < "$rendered" >"$sql_output" 2>&1; then
|
||||
echo " ✅ Successfully executed ${base_name}"
|
||||
log_sql_success "$target_db" "$sql_file"
|
||||
else
|
||||
echo " ❌ Failed to execute $sql_file"
|
||||
sed 's/^/ /' "$sql_output"
|
||||
log_sql_failure "$target_db" "$sql_file"
|
||||
fi
|
||||
done || true
|
||||
rm -f "$sql_output"
|
||||
done < <(LC_ALL=C find "$dir" -type f -name "*.sql" | sort) || true
|
||||
}
|
||||
|
||||
# Function to execute SQL files for a module
|
||||
execute_module_sql() {
|
||||
local module_dir="$1"
|
||||
local module_name="$2"
|
||||
module_sql_run_module(){
|
||||
local module_key="$1"
|
||||
local module_dir="$2"
|
||||
local module_name="${MODULE_NAME[$module_key]:-}"
|
||||
if [ -z "$module_name" ]; then
|
||||
module_name="$module_dir"
|
||||
fi
|
||||
local world_db="${DB_WORLD_NAME:-acore_world}"
|
||||
local auth_db="${DB_AUTH_NAME:-acore_auth}"
|
||||
local characters_db="${DB_CHARACTERS_NAME:-acore_characters}"
|
||||
local playerbots_db="${DB_PLAYERBOTS_NAME:-acore_playerbots}"
|
||||
local character_set="${MYSQL_CHARACTER_SET:-utf8mb4}"
|
||||
local collation="${MYSQL_COLLATION:-utf8mb4_unicode_ci}"
|
||||
execute_sql_file_in_db(){
|
||||
local target_db="$1"
|
||||
local sql_file="$2"
|
||||
local label="$3"
|
||||
local rendered
|
||||
rendered="$(render_sql_file_for_execution "$sql_file")"
|
||||
|
||||
if grep -q '\bplayerbots\b' "$rendered"; then
|
||||
if ! playerbots_table_exists; then
|
||||
echo " Skipping ${label}: ${base_name} (playerbots table missing)"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
local base_name
|
||||
base_name="$(basename "$sql_file")"
|
||||
echo " Executing ${label}: ${base_name}"
|
||||
local sql_output
|
||||
sql_output="$(mktemp)"
|
||||
if mysql_exec "${target_db}" < "$rendered" >"$sql_output" 2>&1; then
|
||||
echo " ✅ Successfully executed ${base_name}"
|
||||
log_sql_success "$target_db" "$sql_file"
|
||||
else
|
||||
echo " ❌ Failed to execute $sql_file"
|
||||
sed 's/^/ /' "$sql_output"
|
||||
log_sql_failure "$target_db" "$sql_file"
|
||||
fi
|
||||
rm -f "$sql_output"
|
||||
}
|
||||
|
||||
local run_sorted_sql
|
||||
|
||||
run_sorted_sql() {
|
||||
@@ -40,27 +160,22 @@ execute_module_sql() {
|
||||
local label="$3"
|
||||
local skip_regex="${4:-}"
|
||||
[ -d "$dir" ] || return
|
||||
LC_ALL=C find "$dir" -type f -name "*.sql" | sort | while read -r sql_file; do
|
||||
while IFS= read -r sql_file; do
|
||||
local base_name
|
||||
base_name="$(basename "$sql_file")"
|
||||
if [ -n "$skip_regex" ] && [[ "$base_name" =~ $skip_regex ]]; then
|
||||
echo " Skipping ${label}: ${base_name}"
|
||||
continue
|
||||
fi
|
||||
echo " Executing ${label}: ${base_name}"
|
||||
if mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" "${target_db}" < "$sql_file" >/dev/null 2>&1; then
|
||||
echo " ✅ Successfully executed ${base_name}"
|
||||
else
|
||||
echo " ❌ Failed to execute $sql_file"
|
||||
fi
|
||||
done || true
|
||||
execute_sql_file_in_db "$target_db" "$sql_file" "$label"
|
||||
done < <(LC_ALL=C find "$dir" -type f -name "*.sql" | sort) || true
|
||||
}
|
||||
|
||||
echo "Processing SQL scripts for $module_name..."
|
||||
|
||||
if [ "$module_name" = "Playerbots" ]; then
|
||||
if [ "$module_key" = "MODULE_PLAYERBOTS" ]; then
|
||||
echo " Ensuring database ${playerbots_db} exists..."
|
||||
if mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" -e "CREATE DATABASE IF NOT EXISTS \`${playerbots_db}\` CHARACTER SET ${character_set} COLLATE ${collation};" >/dev/null 2>&1; then
|
||||
if mysql_exec -e "CREATE DATABASE IF NOT EXISTS \`${playerbots_db}\` CHARACTER SET ${character_set} COLLATE ${collation};" >/dev/null 2>&1; then
|
||||
echo " ✅ Playerbots database ready"
|
||||
else
|
||||
echo " ❌ Failed to ensure playerbots database"
|
||||
@@ -71,45 +186,30 @@ execute_module_sql() {
|
||||
if [ -d "$module_dir/data/sql" ]; then
|
||||
# Execute world database scripts
|
||||
if [ -d "$module_dir/data/sql/world" ]; then
|
||||
find "$module_dir/data/sql/world" -name "*.sql" -type f | while read sql_file; do
|
||||
echo " Executing world SQL: $(basename "$sql_file")"
|
||||
if mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" "${DB_WORLD_NAME}" < "$sql_file" >/dev/null 2>&1; then
|
||||
echo " ✅ Successfully executed $(basename "$sql_file")"
|
||||
else
|
||||
echo " ❌ Failed to execute $sql_file"
|
||||
fi
|
||||
done
|
||||
while IFS= read -r sql_file; do
|
||||
execute_sql_file_in_db "$world_db" "$sql_file" "world SQL"
|
||||
done < <(find "$module_dir/data/sql/world" -type f -name "*.sql") || true
|
||||
fi
|
||||
run_sorted_sql "$module_dir/data/sql/db-world" "${DB_WORLD_NAME}" "world SQL"
|
||||
run_sorted_sql "$module_dir/data/sql/db-world" "${world_db}" "world SQL"
|
||||
|
||||
# Execute auth database scripts
|
||||
if [ -d "$module_dir/data/sql/auth" ]; then
|
||||
find "$module_dir/data/sql/auth" -name "*.sql" -type f | while read sql_file; do
|
||||
echo " Executing auth SQL: $(basename "$sql_file")"
|
||||
if mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" "${DB_AUTH_NAME}" < "$sql_file" >/dev/null 2>&1; then
|
||||
echo " ✅ Successfully executed $(basename "$sql_file")"
|
||||
else
|
||||
echo " ❌ Failed to execute $sql_file"
|
||||
fi
|
||||
done
|
||||
while IFS= read -r sql_file; do
|
||||
execute_sql_file_in_db "$auth_db" "$sql_file" "auth SQL"
|
||||
done < <(find "$module_dir/data/sql/auth" -type f -name "*.sql") || true
|
||||
fi
|
||||
run_sorted_sql "$module_dir/data/sql/db-auth" "${DB_AUTH_NAME}" "auth SQL"
|
||||
run_sorted_sql "$module_dir/data/sql/db-auth" "${auth_db}" "auth SQL"
|
||||
|
||||
# Execute character database scripts
|
||||
if [ -d "$module_dir/data/sql/characters" ]; then
|
||||
find "$module_dir/data/sql/characters" -name "*.sql" -type f | while read sql_file; do
|
||||
echo " Executing characters SQL: $(basename "$sql_file")"
|
||||
if mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" "${DB_CHARACTERS_NAME}" < "$sql_file" >/dev/null 2>&1; then
|
||||
echo " ✅ Successfully executed $(basename "$sql_file")"
|
||||
else
|
||||
echo " ❌ Failed to execute $sql_file"
|
||||
fi
|
||||
done
|
||||
while IFS= read -r sql_file; do
|
||||
execute_sql_file_in_db "$characters_db" "$sql_file" "characters SQL"
|
||||
done < <(find "$module_dir/data/sql/characters" -type f -name "*.sql") || true
|
||||
fi
|
||||
run_sorted_sql "$module_dir/data/sql/db-characters" "${DB_CHARACTERS_NAME}" "characters SQL"
|
||||
run_sorted_sql "$module_dir/data/sql/db-characters" "${characters_db}" "characters SQL"
|
||||
|
||||
# Execute playerbots database scripts
|
||||
if [ "$module_name" = "Playerbots" ] && [ -d "$module_dir/data/sql/playerbots" ]; then
|
||||
if [ "$module_key" = "MODULE_PLAYERBOTS" ] && [ -d "$module_dir/data/sql/playerbots" ]; then
|
||||
local pb_root="$module_dir/data/sql/playerbots"
|
||||
run_sorted_sql "$pb_root/base" "$playerbots_db" "playerbots SQL"
|
||||
run_sorted_sql "$pb_root/custom" "$playerbots_db" "playerbots SQL"
|
||||
@@ -119,18 +219,16 @@ execute_module_sql() {
|
||||
fi
|
||||
|
||||
# Execute base SQL files (common pattern)
|
||||
find "$module_dir/data/sql" -maxdepth 1 -name "*.sql" -type f | while read sql_file; do
|
||||
echo " Executing base SQL: $(basename "$sql_file")"
|
||||
mysql -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" "${DB_WORLD_NAME}" < "$sql_file" 2>/dev/null || echo " Warning: Failed to execute $sql_file"
|
||||
done
|
||||
while IFS= read -r sql_file; do
|
||||
execute_sql_file_in_db "$world_db" "$sql_file" "base SQL"
|
||||
done < <(find "$module_dir/data/sql" -maxdepth 1 -type f -name "*.sql") || true
|
||||
fi
|
||||
|
||||
# Look for SQL files in other common locations
|
||||
if [ -d "$module_dir/sql" ]; then
|
||||
find "$module_dir/sql" -name "*.sql" -type f | while read sql_file; do
|
||||
echo " Executing SQL: $(basename "$sql_file")"
|
||||
mysql -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" "${DB_WORLD_NAME}" < "$sql_file" 2>/dev/null || echo " Warning: Failed to execute $sql_file"
|
||||
done
|
||||
while IFS= read -r sql_file; do
|
||||
execute_sql_file_in_db "$world_db" "$sql_file" "module SQL"
|
||||
done < <(find "$module_dir/sql" -type f -name "*.sql") || true
|
||||
fi
|
||||
|
||||
return 0
|
||||
@@ -144,76 +242,60 @@ execute_module_sql_scripts() {
|
||||
apk add --no-cache mariadb-client >/dev/null 2>&1 || echo "Warning: Could not install MariaDB client"
|
||||
}
|
||||
|
||||
# Iterate modules from staging directory to catch new modules automatically
|
||||
for module_dir in */; do
|
||||
[[ -d "$module_dir" ]] || continue
|
||||
[[ "$module_dir" == "." || "$module_dir" == ".." ]] && continue
|
||||
module_dir="${module_dir%/}"
|
||||
# Only process directories that follow mod-* convention or known module names
|
||||
if [[ "$module_dir" != mod-* && "$module_dir" != StatBooster && "$module_dir" != DungeonRespawn && "$module_dir" != eluna-ts ]]; then
|
||||
SQL_SUCCESS_LOG=()
|
||||
SQL_FAILURE_LOG=()
|
||||
|
||||
# Iterate modules from manifest metadata
|
||||
local key module_dir enabled
|
||||
local world_db="${DB_WORLD_NAME:-acore_world}"
|
||||
local auth_db="${DB_AUTH_NAME:-acore_auth}"
|
||||
local characters_db="${DB_CHARACTERS_NAME:-acore_characters}"
|
||||
for key in "${MODULE_KEYS[@]}"; do
|
||||
module_dir="${MODULE_NAME[$key]:-}"
|
||||
[ -n "$module_dir" ] || continue
|
||||
[ -d "$module_dir" ] || continue
|
||||
|
||||
enabled="${MODULE_ENABLED[$key]:-0}"
|
||||
if [ "$enabled" != "1" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local enabled=0
|
||||
case "$module_dir" in
|
||||
mod-playerbots) enabled="$MODULE_PLAYERBOTS" ;;
|
||||
mod-aoe-loot) enabled="$MODULE_AOE_LOOT" ;;
|
||||
mod-learn-spells) enabled="$MODULE_LEARN_SPELLS" ;;
|
||||
mod-fireworks-on-level) enabled="$MODULE_FIREWORKS" ;;
|
||||
mod-individual-progression) enabled="$MODULE_INDIVIDUAL_PROGRESSION" ;;
|
||||
mod-ahbot) enabled="$MODULE_AHBOT" ;;
|
||||
mod-autobalance) enabled="$MODULE_AUTOBALANCE" ;;
|
||||
mod-transmog) enabled="$MODULE_TRANSMOG" ;;
|
||||
mod-npc-buffer) enabled="$MODULE_NPC_BUFFER" ;;
|
||||
mod-dynamic-xp) enabled="$MODULE_DYNAMIC_XP" ;;
|
||||
mod-solo-lfg) enabled="$MODULE_SOLO_LFG" ;;
|
||||
mod-1v1-arena) enabled="$MODULE_1V1_ARENA" ;;
|
||||
mod-phased-duels) enabled="$MODULE_PHASED_DUELS" ;;
|
||||
mod-breaking-news-override) enabled="$MODULE_BREAKING_NEWS" ;;
|
||||
mod-boss-announcer) enabled="$MODULE_BOSS_ANNOUNCER" ;;
|
||||
mod-account-achievements) enabled="$MODULE_ACCOUNT_ACHIEVEMENTS" ;;
|
||||
mod-auto-revive) enabled="$MODULE_AUTO_REVIVE" ;;
|
||||
mod-gain-honor-guard) enabled="$MODULE_GAIN_HONOR_GUARD" ;;
|
||||
mod-ale) enabled="$MODULE_ELUNA" ;;
|
||||
mod-TimeIsTime) enabled="$MODULE_TIME_IS_TIME" ;;
|
||||
mod-pocket-portal) enabled="$MODULE_POCKET_PORTAL" ;;
|
||||
mod-random-enchants) enabled="$MODULE_RANDOM_ENCHANTS" ;;
|
||||
mod-solocraft) enabled="$MODULE_SOLOCRAFT" ;;
|
||||
mod-pvp-titles) enabled="$MODULE_PVP_TITLES" ;;
|
||||
mod-npc-beastmaster) enabled="$MODULE_NPC_BEASTMASTER" ;;
|
||||
mod-npc-enchanter) enabled="$MODULE_NPC_ENCHANTER" ;;
|
||||
mod-instance-reset) enabled="$MODULE_INSTANCE_RESET" ;;
|
||||
mod-quest-count-level) enabled="$MODULE_LEVEL_GRANT" ;;
|
||||
mod-arac) enabled="$MODULE_ARAC" ;;
|
||||
mod-assistant) enabled="$MODULE_ASSISTANT" ;;
|
||||
mod-reagent-bank) enabled="$MODULE_REAGENT_BANK" ;;
|
||||
mod-black-market) enabled="$MODULE_BLACK_MARKET_AUCTION_HOUSE" ;;
|
||||
mod-challenge-modes) enabled="$MODULE_CHALLENGE_MODES" ;;
|
||||
mod-ollama-chat) enabled="$MODULE_OLLAMA_CHAT" ;;
|
||||
mod-player-bot-level-brackets) enabled="$MODULE_PLAYER_BOT_LEVEL_BRACKETS" ;;
|
||||
StatBooster) enabled="$MODULE_STATBOOSTER" ;;
|
||||
DungeonRespawn) enabled="$MODULE_DUNGEON_RESPAWN" ;;
|
||||
skeleton-module) enabled="$MODULE_SKELETON_MODULE" ;;
|
||||
mod-bg-slaveryvalley) enabled="$MODULE_BG_SLAVERYVALLEY" ;;
|
||||
mod-azerothshard) enabled="$MODULE_AZEROTHSHARD" ;;
|
||||
mod-worgoblin) enabled="$MODULE_WORGOBLIN" ;;
|
||||
eluna-ts) enabled="$MODULE_ELUNA_TS" ;;
|
||||
*) enabled=1 ;; # Default to enabled for unknown module directories
|
||||
esac
|
||||
|
||||
if [ "${enabled:-0}" = "1" ]; then
|
||||
# Skip modules explicitly disabled for SQL
|
||||
if [ "$module_dir" = "mod-pocket-portal" ]; then
|
||||
echo '⚠️ Skipping mod-pocket-portal SQL: module disabled until C++20 patch is applied.'
|
||||
continue
|
||||
fi
|
||||
execute_module_sql "$module_dir" "$module_dir"
|
||||
if [ "$module_dir" = "mod-pocket-portal" ]; then
|
||||
echo '⚠️ Skipping mod-pocket-portal SQL: module disabled until C++20 patch is applied.'
|
||||
continue
|
||||
fi
|
||||
|
||||
module_sql_run_module "$key" "$module_dir"
|
||||
done
|
||||
|
||||
run_custom_sql_group world "${DB_WORLD_NAME}" "custom world SQL"
|
||||
run_custom_sql_group auth "${DB_AUTH_NAME}" "custom auth SQL"
|
||||
run_custom_sql_group characters "${DB_CHARACTERS_NAME}" "custom characters SQL"
|
||||
run_custom_sql_group world "${world_db}" "custom world SQL"
|
||||
run_custom_sql_group auth "${auth_db}" "custom auth SQL"
|
||||
run_custom_sql_group characters "${characters_db}" "custom characters SQL"
|
||||
|
||||
echo "SQL execution summary:"
|
||||
if [ ${#SQL_SUCCESS_LOG[@]} -gt 0 ]; then
|
||||
echo " ✅ Applied:"
|
||||
for entry in "${SQL_SUCCESS_LOG[@]}"; do
|
||||
IFS='::' read -r db file <<< "$entry"
|
||||
echo " • [$db] $file"
|
||||
done
|
||||
else
|
||||
echo " ✅ Applied: none"
|
||||
fi
|
||||
if [ ${#SQL_FAILURE_LOG[@]} -gt 0 ]; then
|
||||
echo " ❌ Failed:"
|
||||
for entry in "${SQL_FAILURE_LOG[@]}"; do
|
||||
IFS='::' read -r db file <<< "$entry"
|
||||
echo " • [$db] $file"
|
||||
done
|
||||
else
|
||||
echo " ❌ Failed: none"
|
||||
fi
|
||||
|
||||
if [ ${#TEMP_SQL_FILES[@]} -gt 0 ]; then
|
||||
rm -f "${TEMP_SQL_FILES[@]}" 2>/dev/null || true
|
||||
TEMP_SQL_FILES=()
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
519
scripts/modules.py
Executable file
519
scripts/modules.py
Executable file
@@ -0,0 +1,519 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Module manifest helper.
|
||||
|
||||
Reads config/modules.json and .env to produce canonical module state that
|
||||
downstream shell scripts can consume for staging, rebuild detection, and
|
||||
dependency validation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
from dataclasses import dataclass, asdict, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Optional, Tuple
|
||||
import shlex
|
||||
|
||||
|
||||
STRICT_TRUE = {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def parse_bool(value: str) -> bool:
|
||||
if value is None:
|
||||
return False
|
||||
return str(value).strip().lower() in STRICT_TRUE
|
||||
|
||||
|
||||
def load_env_file(env_path: Path) -> Dict[str, str]:
|
||||
if not env_path.exists():
|
||||
return {}
|
||||
env: Dict[str, str] = {}
|
||||
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if line.startswith("export "):
|
||||
line = line[len("export ") :].strip()
|
||||
if "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
elif value.startswith("'") and value.endswith("'"):
|
||||
value = value[1:-1]
|
||||
env[key] = value
|
||||
return env
|
||||
|
||||
|
||||
def load_manifest(manifest_path: Path) -> List[Dict[str, object]]:
|
||||
if not manifest_path.exists():
|
||||
raise FileNotFoundError(f"Manifest file not found: {manifest_path}")
|
||||
with manifest_path.open("r", encoding="utf-8") as fh:
|
||||
manifest = json.load(fh)
|
||||
modules = manifest.get("modules")
|
||||
if not isinstance(modules, list):
|
||||
raise ValueError("Manifest must define a top-level 'modules' array")
|
||||
validated: List[Dict[str, object]] = []
|
||||
seen_keys: set[str] = set()
|
||||
for entry in modules:
|
||||
if not isinstance(entry, dict):
|
||||
raise ValueError("Each manifest entry must be an object")
|
||||
key = entry.get("key")
|
||||
name = entry.get("name")
|
||||
repo = entry.get("repo")
|
||||
if not key or not isinstance(key, str):
|
||||
raise ValueError("Manifest entry missing 'key'")
|
||||
if key in seen_keys:
|
||||
raise ValueError(f"Duplicate manifest key detected: {key}")
|
||||
seen_keys.add(key)
|
||||
if not name or not isinstance(name, str):
|
||||
raise ValueError(f"Manifest entry {key} missing 'name'")
|
||||
if not repo or not isinstance(repo, str):
|
||||
raise ValueError(f"Manifest entry {key} missing 'repo'")
|
||||
validated.append(entry)
|
||||
return validated
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModuleState:
|
||||
key: str
|
||||
name: str
|
||||
repo: str
|
||||
needs_build: bool
|
||||
module_type: str
|
||||
requires: List[str] = field(default_factory=list)
|
||||
ref: Optional[str] = None
|
||||
status: str = "active"
|
||||
block_reason: Optional[str] = None
|
||||
post_install_hooks: List[str] = field(default_factory=list)
|
||||
config_cleanup: List[str] = field(default_factory=list)
|
||||
sql: Optional[object] = None
|
||||
notes: Optional[str] = None
|
||||
enabled_raw: bool = False
|
||||
enabled_effective: bool = False
|
||||
value: str = "0"
|
||||
dependency_issues: List[str] = field(default_factory=list)
|
||||
warnings: List[str] = field(default_factory=list)
|
||||
errors: List[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def blocked(self) -> bool:
|
||||
return self.status.lower() == "blocked"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModuleCollectionState:
|
||||
manifest_path: Path
|
||||
env_path: Path
|
||||
modules: List[ModuleState]
|
||||
generated_at: datetime
|
||||
warnings: List[str]
|
||||
errors: List[str]
|
||||
|
||||
def enabled_modules(self) -> List[ModuleState]:
|
||||
return [module for module in self.modules if module.enabled_effective]
|
||||
|
||||
def compile_modules(self) -> List[ModuleState]:
|
||||
return [
|
||||
module
|
||||
for module in self.modules
|
||||
if module.enabled_effective and module.needs_build
|
||||
]
|
||||
|
||||
def requires_playerbot_source(self) -> bool:
|
||||
module_map = {m.key: m for m in self.modules}
|
||||
playerbots_enabled = module_map.get("MODULE_PLAYERBOTS")
|
||||
playerbots = bool(playerbots_enabled and playerbots_enabled.enabled_effective)
|
||||
needs_cpp = any(module.needs_build and module.enabled_effective for module in self.modules)
|
||||
return playerbots or needs_cpp
|
||||
|
||||
|
||||
def build_state(env_path: Path, manifest_path: Path) -> ModuleCollectionState:
|
||||
env_map = load_env_file(env_path)
|
||||
manifest_entries = load_manifest(manifest_path)
|
||||
modules: List[ModuleState] = []
|
||||
errors: List[str] = []
|
||||
warnings: List[str] = []
|
||||
|
||||
# Track which manifest keys appear in .env for coverage validation
|
||||
env_keys_in_manifest: set[str] = set()
|
||||
|
||||
for entry in manifest_entries:
|
||||
key = entry["key"]
|
||||
name = entry["name"]
|
||||
repo = entry["repo"]
|
||||
needs_build = bool(entry.get("needs_build", False))
|
||||
module_type = str(entry.get("type", "cpp"))
|
||||
requires = entry.get("requires") or []
|
||||
if not isinstance(requires, list):
|
||||
raise ValueError(f"Manifest entry {key} has non-list 'requires'")
|
||||
requires = [str(dep) for dep in requires]
|
||||
|
||||
depends_on = entry.get("depends_on") or []
|
||||
if not isinstance(depends_on, list):
|
||||
raise ValueError(f"Manifest entry {key} has non-list 'depends_on'")
|
||||
depends_on = [str(dep) for dep in depends_on]
|
||||
if depends_on:
|
||||
requires = list(dict.fromkeys(requires + depends_on))
|
||||
status = entry.get("status", "active")
|
||||
block_reason = entry.get("block_reason")
|
||||
post_install_hooks = entry.get("post_install_hooks") or []
|
||||
if not isinstance(post_install_hooks, list):
|
||||
raise ValueError(f"Manifest entry {key} has non-list 'post_install_hooks'")
|
||||
post_install_hooks = [str(hook) for hook in post_install_hooks]
|
||||
config_cleanup = entry.get("config_cleanup") or []
|
||||
if not isinstance(config_cleanup, list):
|
||||
raise ValueError(f"Manifest entry {key} has non-list 'config_cleanup'")
|
||||
config_cleanup = [str(pattern) for pattern in config_cleanup]
|
||||
sql = entry.get("sql")
|
||||
ref = entry.get("ref")
|
||||
notes = entry.get("notes")
|
||||
|
||||
raw_value = env_map.get(key, os.environ.get(key, "0"))
|
||||
env_keys_in_manifest.add(key)
|
||||
enabled_raw = parse_bool(raw_value)
|
||||
|
||||
module = ModuleState(
|
||||
key=key,
|
||||
name=name,
|
||||
repo=repo,
|
||||
needs_build=needs_build,
|
||||
module_type=module_type,
|
||||
requires=requires,
|
||||
ref=ref,
|
||||
status=status,
|
||||
block_reason=block_reason,
|
||||
post_install_hooks=post_install_hooks,
|
||||
config_cleanup=config_cleanup,
|
||||
sql=sql,
|
||||
notes=notes,
|
||||
enabled_raw=enabled_raw,
|
||||
)
|
||||
|
||||
if module.blocked and enabled_raw:
|
||||
module.errors.append(
|
||||
f"{module.key} is blocked: {module.block_reason or 'blocked in manifest'}"
|
||||
)
|
||||
|
||||
# Effective enablement respects block status
|
||||
module.enabled_effective = enabled_raw and not module.blocked
|
||||
module.value = "1" if module.enabled_effective else "0"
|
||||
|
||||
modules.append(module)
|
||||
|
||||
module_map: Dict[str, ModuleState] = {module.key: module for module in modules}
|
||||
|
||||
# Dependency validation
|
||||
for module in modules:
|
||||
if not module.enabled_effective:
|
||||
continue
|
||||
missing: List[str] = []
|
||||
for dependency in module.requires:
|
||||
dep_state = module_map.get(dependency)
|
||||
if not dep_state or not dep_state.enabled_effective:
|
||||
missing.append(dependency)
|
||||
if missing:
|
||||
plural = "modules" if len(missing) > 1 else "module"
|
||||
list_str = ", ".join(missing)
|
||||
message = f"{module.key} requires {plural}: {list_str}"
|
||||
module.errors.append(message)
|
||||
|
||||
# Collect warnings/errors
|
||||
for module in modules:
|
||||
if module.errors:
|
||||
errors.extend(module.errors)
|
||||
if module.warnings:
|
||||
warnings.extend(module.warnings)
|
||||
|
||||
# Warn if .env defines modules not in manifest
|
||||
extra_env_modules = [
|
||||
key for key in env_map.keys() if key.startswith("MODULE_") and key not in module_map
|
||||
]
|
||||
for unknown_key in extra_env_modules:
|
||||
warnings.append(f".env defines {unknown_key} but it is missing from the manifest")
|
||||
|
||||
# Warn if manifest entry lacks .env toggle
|
||||
for module in modules:
|
||||
if module.key not in env_map and module.key not in os.environ:
|
||||
warnings.append(
|
||||
f"Manifest includes {module.key} but .env does not define it (defaulting to 0)"
|
||||
)
|
||||
|
||||
return ModuleCollectionState(
|
||||
manifest_path=manifest_path,
|
||||
env_path=env_path,
|
||||
modules=modules,
|
||||
generated_at=datetime.now(timezone.utc),
|
||||
warnings=warnings,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
def write_outputs(state: ModuleCollectionState, output_dir: Path) -> None:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
env_lines: List[str] = [
|
||||
"# Autogenerated by scripts/modules.py",
|
||||
f"# Generated at {state.generated_at.isoformat()}",
|
||||
f'export MODULES_MANIFEST="{state.manifest_path}"',
|
||||
f'export MODULES_ENV_PATH="{state.env_path}"',
|
||||
]
|
||||
|
||||
enabled_names: List[str] = []
|
||||
compile_names: List[str] = []
|
||||
|
||||
for module in state.modules:
|
||||
env_lines.append(f"export {module.key}={module.value}")
|
||||
if module.enabled_effective:
|
||||
enabled_names.append(module.name)
|
||||
if module.enabled_effective and module.needs_build:
|
||||
compile_names.append(module.name)
|
||||
|
||||
env_lines.append(f'export MODULES_ENABLED="{ " ".join(enabled_names) }"'.rstrip())
|
||||
env_lines.append(f'export MODULES_COMPILE="{ " ".join(compile_names) }"'.rstrip())
|
||||
env_lines.append(
|
||||
f"export MODULES_REQUIRES_PLAYERBOT_SOURCE="
|
||||
f'{"1" if state.requires_playerbot_source() else "0"}'
|
||||
)
|
||||
env_lines.append(f"export MODULES_WARNING_COUNT={len(state.warnings)}")
|
||||
env_lines.append(f"export MODULES_ERROR_COUNT={len(state.errors)}")
|
||||
|
||||
modules_env_path = output_dir / "modules.env"
|
||||
modules_env_path.write_text("\n".join(env_lines) + "\n", encoding="utf-8")
|
||||
|
||||
state_payload = {
|
||||
"generated_at": state.generated_at.isoformat(),
|
||||
"manifest_path": str(state.manifest_path),
|
||||
"env_path": str(state.env_path),
|
||||
"warnings": state.warnings,
|
||||
"errors": state.errors,
|
||||
"modules": [
|
||||
{
|
||||
**asdict(module),
|
||||
"enabled_raw": module.enabled_raw,
|
||||
"enabled_effective": module.enabled_effective,
|
||||
"blocked": module.blocked,
|
||||
}
|
||||
for module in state.modules
|
||||
],
|
||||
"enabled_modules": [module.name for module in state.enabled_modules()],
|
||||
"compile_modules": [module.name for module in state.compile_modules()],
|
||||
"requires_playerbot_source": state.requires_playerbot_source(),
|
||||
}
|
||||
|
||||
modules_state_path = output_dir / "modules-state.json"
|
||||
modules_state_path.write_text(
|
||||
json.dumps(state_payload, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
meta_dir = output_dir / ".modules-meta"
|
||||
meta_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
compile_list_path = meta_dir / "modules-compile.txt"
|
||||
compile_list_path.write_text(
|
||||
"\n".join(state_payload["compile_modules"]) + ("\n" if compile_names else ""),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
enabled_list_path = meta_dir / "modules-enabled.txt"
|
||||
enabled_list_path.write_text(
|
||||
"\n".join(state_payload["enabled_modules"]) + ("\n" if enabled_names else ""),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def print_list(state: ModuleCollectionState, selector: str) -> None:
|
||||
if selector == "compile":
|
||||
items = [module.name for module in state.compile_modules()]
|
||||
elif selector == "enabled":
|
||||
items = [module.name for module in state.enabled_modules()]
|
||||
elif selector == "keys":
|
||||
items = [module.key for module in state.enabled_modules()]
|
||||
else:
|
||||
raise ValueError(f"Unknown list selector: {selector}")
|
||||
for item in items:
|
||||
print(item)
|
||||
|
||||
|
||||
def print_requires_playerbot(state: ModuleCollectionState) -> None:
|
||||
print("1" if state.requires_playerbot_source() else "0")
|
||||
|
||||
|
||||
def print_state(state: ModuleCollectionState, fmt: str) -> None:
|
||||
payload = {
|
||||
"generated_at": state.generated_at.isoformat(),
|
||||
"warnings": state.warnings,
|
||||
"errors": state.errors,
|
||||
"modules": [
|
||||
{
|
||||
"key": module.key,
|
||||
"name": module.name,
|
||||
"enabled": module.enabled_effective,
|
||||
"needs_build": module.needs_build,
|
||||
"requires": module.requires,
|
||||
"blocked": module.blocked,
|
||||
"dependency_issues": module.dependency_issues,
|
||||
"post_install_hooks": module.post_install_hooks,
|
||||
"config_cleanup": module.config_cleanup,
|
||||
}
|
||||
for module in state.modules
|
||||
],
|
||||
"enabled_modules": [module.name for module in state.enabled_modules()],
|
||||
"compile_modules": [module.name for module in state.compile_modules()],
|
||||
"requires_playerbot_source": state.requires_playerbot_source(),
|
||||
}
|
||||
if fmt == "json":
|
||||
json.dump(payload, sys.stdout, indent=2, sort_keys=True)
|
||||
sys.stdout.write("\n")
|
||||
elif fmt == "shell":
|
||||
keys = [module.key for module in state.modules]
|
||||
quoted_keys = " ".join(shlex.quote(key) for key in keys)
|
||||
print(f"MODULE_KEYS=({quoted_keys})")
|
||||
print(
|
||||
"declare -A MODULE_NAME MODULE_REPO MODULE_REF MODULE_TYPE MODULE_ENABLED "
|
||||
"MODULE_NEEDS_BUILD MODULE_BLOCKED MODULE_POST_INSTALL MODULE_REQUIRES "
|
||||
"MODULE_CONFIG_CLEANUP "
|
||||
"MODULE_NOTES MODULE_STATUS MODULE_BLOCK_REASON"
|
||||
)
|
||||
for module in state.modules:
|
||||
key = module.key
|
||||
post_install = ",".join(module.post_install_hooks)
|
||||
dependencies = ",".join(module.requires)
|
||||
block_reason = module.block_reason or ""
|
||||
ref = module.ref or ""
|
||||
notes = module.notes or ""
|
||||
config_cleanup = ",".join(module.config_cleanup)
|
||||
print(f"MODULE_NAME[{key}]={shlex.quote(module.name)}")
|
||||
print(f"MODULE_REPO[{key}]={shlex.quote(module.repo)}")
|
||||
print(f"MODULE_REF[{key}]={shlex.quote(ref)}")
|
||||
print(f"MODULE_TYPE[{key}]={shlex.quote(module.module_type)}")
|
||||
print(f"MODULE_ENABLED[{key}]={1 if module.enabled_effective else 0}")
|
||||
print(f"MODULE_NEEDS_BUILD[{key}]={1 if module.needs_build else 0}")
|
||||
print(f"MODULE_BLOCKED[{key}]={1 if module.blocked else 0}")
|
||||
print(f"MODULE_POST_INSTALL[{key}]={shlex.quote(post_install)}")
|
||||
print(f"MODULE_REQUIRES[{key}]={shlex.quote(dependencies)}")
|
||||
print(f"MODULE_CONFIG_CLEANUP[{key}]={shlex.quote(config_cleanup)}")
|
||||
print(f"MODULE_NOTES[{key}]={shlex.quote(notes)}")
|
||||
print(f"MODULE_STATUS[{key}]={shlex.quote(module.status)}")
|
||||
print(f"MODULE_BLOCK_REASON[{key}]={shlex.quote(block_reason)}")
|
||||
else:
|
||||
raise ValueError(f"Unsupported format: {fmt}")
|
||||
|
||||
|
||||
def handle_generate(args: argparse.Namespace) -> int:
|
||||
env_path = Path(args.env_path).resolve()
|
||||
manifest_path = Path(args.manifest).resolve()
|
||||
output_dir = Path(args.output_dir).resolve()
|
||||
state = build_state(env_path, manifest_path)
|
||||
write_outputs(state, output_dir)
|
||||
|
||||
if state.warnings:
|
||||
warning_block = "\n".join(f"- {warning}" for warning in state.warnings)
|
||||
print(
|
||||
textwrap.dedent(
|
||||
f"""\
|
||||
⚠️ Module manifest warnings detected:
|
||||
{warning_block}
|
||||
"""
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
if state.errors:
|
||||
error_block = "\n".join(f"- {error}" for error in state.errors)
|
||||
print(
|
||||
textwrap.dedent(
|
||||
f"""\
|
||||
❌ Module manifest errors detected:
|
||||
{error_block}
|
||||
"""
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def configure_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="Module manifest helper")
|
||||
parser.add_argument(
|
||||
"--env-path",
|
||||
default=".env",
|
||||
help="Path to .env file (default: .env)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest",
|
||||
default="config/modules.json",
|
||||
help="Path to module manifest (default: config/modules.json)",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
generate_parser = subparsers.add_parser("generate", help="Generate module state files")
|
||||
generate_parser.add_argument(
|
||||
"--output-dir",
|
||||
default="local-storage/modules",
|
||||
help="Directory for generated module artifacts (default: local-storage/modules)",
|
||||
)
|
||||
generate_parser.set_defaults(func=handle_generate)
|
||||
|
||||
list_parser = subparsers.add_parser("list", help="Print module lists")
|
||||
list_parser.add_argument(
|
||||
"--type",
|
||||
choices=["compile", "enabled", "keys"],
|
||||
default="compile",
|
||||
help="List selector (default: compile)",
|
||||
)
|
||||
|
||||
def handle_list(args: argparse.Namespace) -> int:
|
||||
state = build_state(Path(args.env_path).resolve(), Path(args.manifest).resolve())
|
||||
print_list(state, args.type)
|
||||
return 1 if state.errors else 0
|
||||
|
||||
list_parser.set_defaults(func=handle_list)
|
||||
|
||||
rps_parser = subparsers.add_parser(
|
||||
"requires-playerbot", help="Print 1 if playerbot source is required else 0"
|
||||
)
|
||||
|
||||
def handle_requires_playerbot(args: argparse.Namespace) -> int:
|
||||
state = build_state(Path(args.env_path).resolve(), Path(args.manifest).resolve())
|
||||
print_requires_playerbot(state)
|
||||
return 1 if state.errors else 0
|
||||
|
||||
rps_parser.set_defaults(func=handle_requires_playerbot)
|
||||
|
||||
dump_parser = subparsers.add_parser("dump", help="Dump module state (JSON format)")
|
||||
dump_parser.add_argument(
|
||||
"--format",
|
||||
choices=["json", "shell"],
|
||||
default="json",
|
||||
help="Output format (default: json)",
|
||||
)
|
||||
|
||||
def handle_dump(args: argparse.Namespace) -> int:
|
||||
state = build_state(Path(args.env_path).resolve(), Path(args.manifest).resolve())
|
||||
print_state(state, args.format)
|
||||
return 1 if state.errors else 0
|
||||
|
||||
dump_parser.set_defaults(func=handle_dump)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Optional[Iterable[str]] = None) -> int:
|
||||
parser = configure_parser()
|
||||
args = parser.parse_args(argv)
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -84,29 +84,50 @@ ASSUME_YES=0
|
||||
SOURCE_OVERRIDE=""
|
||||
SKIP_STOP=0
|
||||
|
||||
COMPILE_MODULE_KEYS=(
|
||||
MODULE_AOE_LOOT MODULE_LEARN_SPELLS MODULE_FIREWORKS MODULE_INDIVIDUAL_PROGRESSION MODULE_AHBOT MODULE_AUTOBALANCE
|
||||
MODULE_TRANSMOG MODULE_NPC_BUFFER MODULE_DYNAMIC_XP MODULE_SOLO_LFG MODULE_1V1_ARENA MODULE_PHASED_DUELS
|
||||
MODULE_BREAKING_NEWS MODULE_BOSS_ANNOUNCER MODULE_ACCOUNT_ACHIEVEMENTS MODULE_AUTO_REVIVE MODULE_GAIN_HONOR_GUARD
|
||||
MODULE_ELUNA MODULE_TIME_IS_TIME MODULE_POCKET_PORTAL MODULE_RANDOM_ENCHANTS MODULE_SOLOCRAFT MODULE_PVP_TITLES MODULE_NPC_BEASTMASTER
|
||||
MODULE_NPC_ENCHANTER MODULE_INSTANCE_RESET MODULE_LEVEL_GRANT MODULE_ARAC MODULE_ASSISTANT MODULE_REAGENT_BANK
|
||||
MODULE_BLACK_MARKET_AUCTION_HOUSE MODULE_CHALLENGE_MODES MODULE_OLLAMA_CHAT MODULE_PLAYER_BOT_LEVEL_BRACKETS MODULE_STATBOOSTER MODULE_DUNGEON_RESPAWN
|
||||
MODULE_SKELETON_MODULE MODULE_BG_SLAVERYVALLEY MODULE_AZEROTHSHARD MODULE_WORGOBLIN MODULE_ELUNA_TS
|
||||
)
|
||||
MODULE_HELPER="$PROJECT_DIR/scripts/modules.py"
|
||||
MODULE_STATE_DIR=""
|
||||
declare -a MODULES_COMPILE_LIST=()
|
||||
|
||||
resolve_local_storage_path(){
|
||||
local path
|
||||
path="$(read_env STORAGE_PATH_LOCAL "./local-storage")"
|
||||
if [[ "$path" != /* ]]; then
|
||||
path="${path#./}"
|
||||
path="$PROJECT_DIR/$path"
|
||||
fi
|
||||
echo "${path%/}"
|
||||
}
|
||||
|
||||
ensure_module_state(){
|
||||
if [ -n "$MODULE_STATE_DIR" ]; then
|
||||
return 0
|
||||
fi
|
||||
local storage_root
|
||||
storage_root="$(resolve_local_storage_path)"
|
||||
MODULE_STATE_DIR="${storage_root}/modules"
|
||||
if ! python3 "$MODULE_HELPER" --env-path "$ENV_FILE" --manifest "$PROJECT_DIR/config/modules.json" generate --output-dir "$MODULE_STATE_DIR"; then
|
||||
echo "❌ Module manifest validation failed. See details above."
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$MODULE_STATE_DIR/modules.env" ]; then
|
||||
echo "❌ modules.env not produced at $MODULE_STATE_DIR/modules.env"
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1090
|
||||
source "$MODULE_STATE_DIR/modules.env"
|
||||
IFS=' ' read -r -a MODULES_COMPILE_LIST <<< "${MODULES_COMPILE:-}"
|
||||
if [ "${#MODULES_COMPILE_LIST[@]}" -eq 1 ] && [ -z "${MODULES_COMPILE_LIST[0]}" ]; then
|
||||
MODULES_COMPILE_LIST=()
|
||||
fi
|
||||
}
|
||||
|
||||
modules_require_playerbot_source(){
|
||||
if [ "$(read_env MODULE_PLAYERBOTS "0")" = "1" ]; then
|
||||
ensure_module_state
|
||||
if [ "${MODULES_REQUIRES_PLAYERBOT_SOURCE:-0}" = "1" ]; then
|
||||
echo 1
|
||||
return
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
local key
|
||||
for key in "${COMPILE_MODULE_KEYS[@]}"; do
|
||||
if [ "$(read_env "$key" "0")" = "1" ]; then
|
||||
echo 1
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo 0
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -124,6 +145,11 @@ if ! command -v docker >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "❌ python3 not found in PATH."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STORAGE_PATH="$(read_env STORAGE_PATH "./storage")"
|
||||
if [[ "$STORAGE_PATH" != /* ]]; then
|
||||
STORAGE_PATH="$PROJECT_DIR/${STORAGE_PATH#./}"
|
||||
@@ -191,65 +217,16 @@ if [ ! -f "$SOURCE_COMPOSE" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
declare -A MODULE_REPO_MAP=(
|
||||
[MODULE_AOE_LOOT]=mod-aoe-loot
|
||||
[MODULE_LEARN_SPELLS]=mod-learn-spells
|
||||
[MODULE_FIREWORKS]=mod-fireworks-on-level
|
||||
[MODULE_INDIVIDUAL_PROGRESSION]=mod-individual-progression
|
||||
[MODULE_AHBOT]=mod-ahbot
|
||||
[MODULE_AUTOBALANCE]=mod-autobalance
|
||||
[MODULE_TRANSMOG]=mod-transmog
|
||||
[MODULE_NPC_BUFFER]=mod-npc-buffer
|
||||
[MODULE_DYNAMIC_XP]=mod-dynamic-xp
|
||||
[MODULE_SOLO_LFG]=mod-solo-lfg
|
||||
[MODULE_1V1_ARENA]=mod-1v1-arena
|
||||
[MODULE_PHASED_DUELS]=mod-phased-duels
|
||||
[MODULE_BREAKING_NEWS]=mod-breaking-news-override
|
||||
[MODULE_BOSS_ANNOUNCER]=mod-boss-announcer
|
||||
[MODULE_ACCOUNT_ACHIEVEMENTS]=mod-account-achievements
|
||||
[MODULE_AUTO_REVIVE]=mod-auto-revive
|
||||
[MODULE_GAIN_HONOR_GUARD]=mod-gain-honor-guard
|
||||
[MODULE_ELUNA]=mod-ale
|
||||
[MODULE_TIME_IS_TIME]=mod-TimeIsTime
|
||||
[MODULE_POCKET_PORTAL]=mod-pocket-portal
|
||||
[MODULE_RANDOM_ENCHANTS]=mod-random-enchants
|
||||
[MODULE_SOLOCRAFT]=mod-solocraft
|
||||
[MODULE_PVP_TITLES]=mod-pvp-titles
|
||||
[MODULE_NPC_BEASTMASTER]=mod-npc-beastmaster
|
||||
[MODULE_NPC_ENCHANTER]=mod-npc-enchanter
|
||||
[MODULE_INSTANCE_RESET]=mod-instance-reset
|
||||
[MODULE_LEVEL_GRANT]=mod-quest-count-level
|
||||
[MODULE_ARAC]=mod-arac
|
||||
[MODULE_ASSISTANT]=mod-assistant
|
||||
[MODULE_REAGENT_BANK]=mod-reagent-bank
|
||||
[MODULE_BLACK_MARKET_AUCTION_HOUSE]=mod-black-market
|
||||
[MODULE_CHALLENGE_MODES]=mod-challenge-modes
|
||||
[MODULE_OLLAMA_CHAT]=mod-ollama-chat
|
||||
[MODULE_PLAYER_BOT_LEVEL_BRACKETS]=mod-player-bot-level-brackets
|
||||
[MODULE_STATBOOSTER]=StatBooster
|
||||
[MODULE_DUNGEON_RESPAWN]=DungeonRespawn
|
||||
[MODULE_SKELETON_MODULE]=skeleton-module
|
||||
[MODULE_BG_SLAVERYVALLEY]=mod-bg-slaveryvalley
|
||||
[MODULE_AZEROTHSHARD]=mod-azerothshard
|
||||
[MODULE_WORGOBLIN]=mod-worgoblin
|
||||
[MODULE_ELUNA_TS]=eluna-ts
|
||||
)
|
||||
ensure_module_state
|
||||
|
||||
compile_modules=()
|
||||
for key in "${!MODULE_REPO_MAP[@]}"; do
|
||||
if [ "$(read_env "$key" "0")" = "1" ]; then
|
||||
compile_modules+=("${MODULE_REPO_MAP[$key]}")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#compile_modules[@]} -eq 0 ]; then
|
||||
if [ ${#MODULES_COMPILE_LIST[@]} -eq 0 ]; then
|
||||
echo "✅ No C++ modules enabled that require a source rebuild."
|
||||
rm -f "$SENTINEL_FILE" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🔧 Modules requiring compilation:"
|
||||
for mod in "${compile_modules[@]}"; do
|
||||
for mod in "${MODULES_COMPILE_LIST[@]}"; do
|
||||
echo " • $mod"
|
||||
done
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ WHERE guid IN (
|
||||
AND c.deleteInfos_Account IS NULL
|
||||
AND c.name IN (
|
||||
SELECT p.name
|
||||
FROM playerbots p
|
||||
FROM `{{PLAYERBOTS_DB}}`.playerbots p
|
||||
WHERE p.bot = 1
|
||||
)
|
||||
AND EXISTS (
|
||||
|
||||
@@ -97,6 +97,37 @@ read_env(){
|
||||
echo "$value"
|
||||
}
|
||||
|
||||
canonical_path(){
|
||||
local path="$1"
|
||||
if command -v realpath >/dev/null 2>&1; then
|
||||
realpath -m "$path"
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
python3 - "$path" <<'PY'
|
||||
import os, sys
|
||||
print(os.path.normpath(sys.argv[1]))
|
||||
PY
|
||||
else
|
||||
local normalized="$path"
|
||||
# Strip leading "./" portions so relative paths are clean
|
||||
while [[ "$normalized" == ./* ]]; do
|
||||
normalized="${normalized:2}"
|
||||
done
|
||||
# Collapse any embedded "/./" segments that appear in absolute paths
|
||||
while [[ "$normalized" == *"/./"* ]]; do
|
||||
normalized="${normalized//\/\.\//\/}"
|
||||
done
|
||||
# Replace duplicate slashes with a single slash for readability
|
||||
while [[ "$normalized" == *"//"* ]]; do
|
||||
normalized="${normalized//\/\//\/}"
|
||||
done
|
||||
# Preserve absolute path prefix if original started with '/'
|
||||
if [[ "$path" == /* && "$normalized" != /* ]]; then
|
||||
normalized="/${normalized}"
|
||||
fi
|
||||
echo "$normalized"
|
||||
fi
|
||||
}
|
||||
|
||||
confirm(){
|
||||
local prompt="$1" default="$2" reply
|
||||
if [ "$ASSUME_YES" = "1" ]; then
|
||||
@@ -141,15 +172,15 @@ STORAGE_PATH="$(read_env STORAGE_PATH "./storage")"
|
||||
if [[ "$STORAGE_PATH" != /* ]]; then
|
||||
STORAGE_PATH="$PROJECT_DIR/$STORAGE_PATH"
|
||||
fi
|
||||
STORAGE_PATH="$(canonical_path "$STORAGE_PATH")"
|
||||
MODULES_DIR="$STORAGE_PATH/modules"
|
||||
|
||||
# Build sentinel is in local storage, deployment modules are in shared storage
|
||||
LOCAL_STORAGE_PATH="$(read_env STORAGE_PATH_LOCAL "./local-storage")"
|
||||
if [[ "$LOCAL_STORAGE_PATH" != /* ]]; then
|
||||
# Remove leading ./ if present
|
||||
LOCAL_STORAGE_PATH="${LOCAL_STORAGE_PATH#./}"
|
||||
LOCAL_STORAGE_PATH="$PROJECT_DIR/$LOCAL_STORAGE_PATH"
|
||||
fi
|
||||
LOCAL_STORAGE_PATH="$(canonical_path "$LOCAL_STORAGE_PATH")"
|
||||
SENTINEL_FILE="$LOCAL_STORAGE_PATH/modules/.requires_rebuild"
|
||||
|
||||
# Define module mappings (from rebuild-with-modules.sh)
|
||||
|
||||
Reference in New Issue
Block a user