diff --git a/.gitignore b/.gitignore index f048aa9..ad709f9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,10 @@ local-storage/ images/ node_modules/ .mcp*/ +scripts/__pycache__/ .env package-lock.json package.json -.modules_state \ No newline at end of file +.modules_state +.modules-meta +todo.md \ No newline at end of file diff --git a/README.md b/README.md index dcf78a5..dcbd2bf 100644 --- a/README.md +++ b/README.md @@ -548,6 +548,11 @@ Initializes or updates AzerothCore source repositories for compilation. #### `scripts/manage-modules.sh` - Module Management Container Internal script that manages module lifecycle within the ac-modules container. +#### `config/modules.json` & `scripts/modules.py` +- Declarative manifest describing every supported module (repo, type, hooks, dependencies). +- `scripts/modules.py` reads the manifest and `.env`, generating `modules.env`, rebuild metadata, and shell-ready module maps. +- Build and deploy scripts source `modules.env`, while `manage-modules.sh` consumes the manifest at runtimeβ€”no more duplicated module lists. + #### `scripts/manage-modules-sql.sh` - Module Database Integration Executes module-specific SQL scripts for database schema updates. diff --git a/build.sh b/build.sh index 639dcb5..013c8f7 100755 --- a/build.sh +++ b/build.sh @@ -67,6 +67,7 @@ require_cmd(){ } require_cmd docker +require_cmd python3 read_env(){ local key="$1" default="${2:-}" @@ -80,28 +81,47 @@ read_env(){ echo "$value" } -# Module detection logic (extracted from deploy.sh) -COMPILE_MODULE_VARS=( - 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="$ROOT_DIR/scripts/modules.py" +MODULE_STATE_INITIALIZED=0 +declare -a MODULES_COMPILE_LIST=() + +resolve_local_storage_path(){ + local local_root + local_root="$(read_env STORAGE_PATH_LOCAL "./local-storage")" + if [[ "$local_root" != /* ]]; then + local_root="${local_root#./}" + local_root="$ROOT_DIR/$local_root" + fi + echo "${local_root%/}" +} + +generate_module_state(){ + local storage_root + storage_root="$(resolve_local_storage_path)" + local output_dir="${storage_root}/modules" + if ! python3 "$MODULE_HELPER" --env-path "$ENV_PATH" --manifest "$ROOT_DIR/config/modules.json" generate --output-dir "$output_dir"; then + err "Module manifest validation failed. See errors above." + exit 1 + fi + if [ ! -f "${output_dir}/modules.env" ]; then + err "modules.env not produced by helper at ${output_dir}/modules.env" + exit 1 + fi + # shellcheck disable=SC1090 + source "${output_dir}/modules.env" + MODULE_STATE_INITIALIZED=1 + MODULES_COMPILE_LIST=() + 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 +} requires_playerbot_source(){ - if [ "$(read_env MODULE_PLAYERBOTS "0")" = "1" ]; then - return 0 + if [ "$MODULE_STATE_INITIALIZED" -ne 1 ]; then + generate_module_state fi - local var - for var in "${COMPILE_MODULE_VARS[@]}"; do - if [ "$(read_env "$var" "0")" = "1" ]; then - return 0 - fi - done - return 1 + [ "${MODULES_REQUIRES_PLAYERBOT_SOURCE:-0}" = "1" ] } ensure_source_repo(){ @@ -197,14 +217,14 @@ detect_rebuild_reasons(){ fi # Check if any C++ modules are enabled but modules-latest images don't exist + if [ "$MODULE_STATE_INITIALIZED" -ne 1 ]; then + generate_module_state + fi + local any_cxx_modules=0 - local var - for var in "${COMPILE_MODULE_VARS[@]}"; do - if [ "$(read_env "$var" "0")" = "1" ]; then - any_cxx_modules=1 - break - fi - done + if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then + any_cxx_modules=1 + fi if [ "$any_cxx_modules" = "1" ]; then local authserver_modules_image @@ -296,11 +316,7 @@ confirm_build(){ # Module staging logic (extracted from setup.sh) sync_modules(){ local storage_path - storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")" - if [[ "$storage_path" != /* ]]; then - storage_path="${storage_path#./}" - storage_path="$ROOT_DIR/$storage_path" - fi + storage_path="$(resolve_local_storage_path)" mkdir -p "$storage_path/modules" info "Using local module staging at $storage_path/modules" @@ -323,10 +339,10 @@ resolve_project_name(){ stage_modules(){ local src_path="$1" local storage_path - storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")" - if [[ "$storage_path" != /* ]]; then - storage_path="${storage_path#./}" - storage_path="$ROOT_DIR/$storage_path" + storage_path="$(resolve_local_storage_path)" + + if [ -z "${MODULES_ENABLED:-}" ]; then + generate_module_state fi info "Staging modules to source directory: $src_path/modules" @@ -340,28 +356,17 @@ stage_modules(){ local local_modules_dir="${src_path}/modules" mkdir -p "$local_modules_dir" - # Export module variables for the script - local module_vars=( - MODULE_PLAYERBOTS 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 - ) - - local module_export_var - for module_export_var in "${module_vars[@]}"; do - local module_value - module_value="$(read_env "$module_export_var" "0")" - export "${module_export_var}=${module_value:-0}" - done - local staging_modules_dir="${storage_path}/modules" export MODULES_HOST_DIR="$staging_modules_dir" + local env_target_dir="$src_path/env/dist/etc" + mkdir -p "$env_target_dir" + export MODULES_ENV_TARGET_DIR="$env_target_dir" + + local lua_target_dir="$src_path/lua_scripts" + mkdir -p "$lua_target_dir" + export MODULES_LUA_TARGET_DIR="$lua_target_dir" + # Set up local storage path for build sentinel tracking local local_storage_path local_storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")" @@ -388,6 +393,7 @@ stage_modules(){ # Run module staging script in local modules directory export MODULES_LOCAL_RUN=1 + export MODULES_SKIP_SQL=1 if [ -n "$staging_modules_dir" ]; then mkdir -p "$staging_modules_dir" rm -f "$staging_modules_dir/.modules_state" "$staging_modules_dir/.requires_rebuild" 2>/dev/null || true @@ -404,11 +410,19 @@ stage_modules(){ rsync -a --delete \ --exclude '.modules_state' \ --exclude '.requires_rebuild' \ + --exclude 'modules.env' \ + --exclude 'modules-state.json' \ + --exclude 'modules-compile.txt' \ + --exclude 'modules-enabled.txt' \ "$local_modules_dir"/ "$staging_modules_dir"/ else find "$staging_modules_dir" -mindepth 1 -maxdepth 1 \ ! -name '.modules_state' \ ! -name '.requires_rebuild' \ + ! -name 'modules.env' \ + ! -name 'modules-state.json' \ + ! -name 'modules-compile.txt' \ + ! -name 'modules-enabled.txt' \ -exec rm -rf {} + 2>/dev/null || true (cd "$local_modules_dir" && tar cf - --exclude='.modules_state' --exclude='.requires_rebuild' .) | (cd "$staging_modules_dir" && tar xf -) fi @@ -420,6 +434,7 @@ stage_modules(){ # Cleanup export GIT_CONFIG_GLOBAL="$prev_git_config_global" unset MODULES_LOCAL_RUN + unset MODULES_SKIP_SQL unset MODULES_HOST_DIR [ -n "$git_temp_config" ] && [ -f "$git_temp_config" ] && rm -f "$git_temp_config" } @@ -492,6 +507,9 @@ main(){ local src_dir local rebuild_reasons + info "Preparing module manifest metadata" + generate_module_state + info "Step 1/6: Setting up source repository" src_dir="$(ensure_source_repo)" diff --git a/config/modules.json b/config/modules.json new file mode 100644 index 0000000..4aef2d9 --- /dev/null +++ b/config/modules.json @@ -0,0 +1,497 @@ +{ + "modules": [ + { + "key": "MODULE_PLAYERBOTS", + "name": "mod-playerbots", + "repo": "https://github.com/mod-playerbots/mod-playerbots.git", + "needs_build": false, + "type": "data", + "notes": "Installs SQL/config assets; core functionality is built into playerbot images", + "post_install_hooks": [], + "config_cleanup": [ + "playerbots.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_AOE_LOOT", + "name": "mod-aoe-loot", + "repo": "https://github.com/azerothcore/mod-aoe-loot.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "mod_aoe_loot.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_LEARN_SPELLS", + "name": "mod-learn-spells", + "repo": "https://github.com/azerothcore/mod-learn-spells.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "mod_learnspells.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_FIREWORKS", + "name": "mod-fireworks-on-level", + "repo": "https://github.com/azerothcore/mod-fireworks-on-level.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "mod_fireworks.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_INDIVIDUAL_PROGRESSION", + "name": "mod-individual-progression", + "repo": "https://github.com/ZhengPeiRu21/mod-individual-progression.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "individual_progression.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_AHBOT", + "name": "mod-ahbot", + "repo": "https://github.com/azerothcore/mod-ahbot.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "mod_ahbot.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_AUTOBALANCE", + "name": "mod-autobalance", + "repo": "https://github.com/azerothcore/mod-autobalance.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "AutoBalance.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_TRANSMOG", + "name": "mod-transmog", + "repo": "https://github.com/azerothcore/mod-transmog.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "transmog.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_NPC_BUFFER", + "name": "mod-npc-buffer", + "repo": "https://github.com/azerothcore/mod-npc-buffer.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "npc_buffer.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_DYNAMIC_XP", + "name": "mod-dynamic-xp", + "repo": "https://github.com/azerothcore/mod-dynamic-xp.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "Individual-XP.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_SOLO_LFG", + "name": "mod-solo-lfg", + "repo": "https://github.com/azerothcore/mod-solo-lfg.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "SoloLfg.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_1V1_ARENA", + "name": "mod-1v1-arena", + "repo": "https://github.com/azerothcore/mod-1v1-arena.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "1v1arena.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_PHASED_DUELS", + "name": "mod-phased-duels", + "repo": "https://github.com/azerothcore/mod-phased-duels.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "phasedduels.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_BREAKING_NEWS", + "name": "mod-breaking-news-override", + "repo": "https://github.com/azerothcore/mod-breaking-news-override.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "breaking_news.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_BOSS_ANNOUNCER", + "name": "mod-boss-announcer", + "repo": "https://github.com/azerothcore/mod-boss-announcer.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "boss_announcer.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_ACCOUNT_ACHIEVEMENTS", + "name": "mod-account-achievements", + "repo": "https://github.com/azerothcore/mod-account-achievements.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "account_achievements.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_AUTO_REVIVE", + "name": "mod-auto-revive", + "repo": "https://github.com/azerothcore/mod-auto-revive.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "AutoRevive.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_GAIN_HONOR_GUARD", + "name": "mod-gain-honor-guard", + "repo": "https://github.com/azerothcore/mod-gain-honor-guard.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "GainHonorGuard.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_ELUNA", + "name": "mod-ale", + "repo": "https://github.com/azerothcore/mod-ale.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [ + "mod_ale_move_path_patch" + ], + "config_cleanup": [ + "mod_eluna.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_TIME_IS_TIME", + "name": "mod-TimeIsTime", + "repo": "https://github.com/dunjeon/mod-TimeIsTime.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "mod-time_is_time.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_POCKET_PORTAL", + "name": "mod-pocket-portal", + "repo": "https://github.com/azerothcore/mod-pocket-portal.git", + "needs_build": true, + "type": "cpp", + "status": "blocked", + "block_reason": "Requires C++20 std::format support patch before enabling", + "post_install_hooks": [], + "config_cleanup": [ + "pocketportal.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_RANDOM_ENCHANTS", + "name": "mod-random-enchants", + "repo": "https://github.com/azerothcore/mod-random-enchants.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "RandomEnchants.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_SOLOCRAFT", + "name": "mod-solocraft", + "repo": "https://github.com/azerothcore/mod-solocraft.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "Solocraft.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_PVP_TITLES", + "name": "mod-pvp-titles", + "repo": "https://github.com/azerothcore/mod-pvp-titles.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "depends_on": [ + "MODULE_ELUNA" + ], + "config_cleanup": [ + "mod_pvptitles.conf*" + ] + }, + { + "key": "MODULE_NPC_BEASTMASTER", + "name": "mod-npc-beastmaster", + "repo": "https://github.com/azerothcore/mod-npc-beastmaster.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "npc_beastmaster.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_NPC_ENCHANTER", + "name": "mod-npc-enchanter", + "repo": "https://github.com/azerothcore/mod-npc-enchanter.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "npc_enchanter.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_INSTANCE_RESET", + "name": "mod-instance-reset", + "repo": "https://github.com/azerothcore/mod-instance-reset.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "instance-reset.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_LEVEL_GRANT", + "name": "mod-quest-count-level", + "repo": "https://github.com/michaeldelago/mod-quest-count-level.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "levelGrant.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_ARAC", + "name": "mod-arac", + "repo": "https://github.com/heyitsbench/mod-arac.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "config_cleanup": [ + "arac.conf*" + ], + "depends_on": [] + }, + { + "key": "MODULE_ASSISTANT", + "name": "mod-assistant", + "repo": "https://github.com/noisiver/mod-assistant.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "depends_on": [] + }, + { + "key": "MODULE_REAGENT_BANK", + "name": "mod-reagent-bank", + "repo": "https://github.com/ZhengPeiRu21/mod-reagent-bank.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "depends_on": [] + }, + { + "key": "MODULE_BLACK_MARKET_AUCTION_HOUSE", + "name": "mod-black-market", + "repo": "https://github.com/Youpeoples/Black-Market-Auction-House.git", + "needs_build": false, + "type": "lua", + "requires": [ + "MODULE_ELUNA" + ], + "post_install_hooks": [ + "black_market_copy_lua" + ], + "depends_on": [ + "MODULE_ELUNA" + ] + }, + { + "key": "MODULE_CHALLENGE_MODES", + "name": "mod-challenge-modes", + "repo": "https://github.com/ZhengPeiRu21/mod-challenge-modes.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "depends_on": [] + }, + { + "key": "MODULE_OLLAMA_CHAT", + "name": "mod-ollama-chat", + "repo": "https://github.com/DustinHendrickson/mod-ollama-chat.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "depends_on": [] + }, + { + "key": "MODULE_PLAYER_BOT_LEVEL_BRACKETS", + "name": "mod-player-bot-level-brackets", + "repo": "https://github.com/DustinHendrickson/mod-player-bot-level-brackets.git", + "needs_build": true, + "type": "cpp", + "requires": [ + "MODULE_PLAYERBOTS" + ], + "post_install_hooks": [], + "depends_on": [] + }, + { + "key": "MODULE_STATBOOSTER", + "name": "StatBooster", + "repo": "https://github.com/AnchyDev/StatBooster.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "depends_on": [] + }, + { + "key": "MODULE_DUNGEON_RESPAWN", + "name": "DungeonRespawn", + "repo": "https://github.com/AnchyDev/DungeonRespawn.git", + "needs_build": true, + "type": "cpp", + "status": "blocked", + "block_reason": "Upstream override signature mismatch (OnBeforeTeleport); awaiting fix", + "post_install_hooks": [], + "depends_on": [] + }, + { + "key": "MODULE_SKELETON_MODULE", + "name": "skeleton-module", + "repo": "https://github.com/azerothcore/skeleton-module.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "depends_on": [] + }, + { + "key": "MODULE_BG_SLAVERYVALLEY", + "name": "mod-bg-slaveryvalley", + "repo": "https://github.com/Helias/mod-bg-slaveryvalley.git", + "needs_build": true, + "type": "cpp", + "depends_on": [ + "MODULE_ELUNA" + ], + "post_install_hooks": [] + }, + { + "key": "MODULE_AZEROTHSHARD", + "name": "mod-azerothshard", + "repo": "https://github.com/azerothcore/mod-azerothshard.git", + "needs_build": true, + "type": "cpp", + "post_install_hooks": [], + "depends_on": [] + }, + { + "key": "MODULE_WORGOBLIN", + "name": "mod-worgoblin", + "repo": "https://github.com/heyitsbench/mod-worgoblin.git", + "needs_build": true, + "type": "cpp", + "depends_on": [ + "MODULE_ELUNA" + ], + "post_install_hooks": [] + }, + { + "key": "MODULE_ELUNA_TS", + "name": "eluna-ts", + "repo": "https://github.com/azerothcore/eluna-ts.git", + "needs_build": false, + "type": "tool", + "requires": [ + "MODULE_ELUNA" + ], + "post_install_hooks": [], + "depends_on": [ + "MODULE_ELUNA" + ] + } + ] +} diff --git a/deploy.sh b/deploy.sh index 117b011..db0cb49 100755 --- a/deploy.sh +++ b/deploy.sh @@ -26,15 +26,9 @@ REMOTE_PROJECT_DIR="" REMOTE_SKIP_STORAGE=0 REMOTE_ARGS_PROVIDED=0 -COMPILE_MODULE_VARS=( - 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_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_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_HELPER="$ROOT_DIR/scripts/modules.py" +MODULE_STATE_INITIALIZED=0 +declare -a MODULES_COMPILE_LIST=() BLUE='\033[0;34m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' info(){ printf '%b\n' "${BLUE}ℹ️ $*${NC}"; } @@ -248,6 +242,7 @@ require_cmd(){ } require_cmd docker +require_cmd python3 if [ "$REMOTE_MODE" -eq 1 ]; then if [ -z "$REMOTE_HOST" ]; then @@ -283,6 +278,43 @@ read_env(){ echo "$value" } +resolve_local_storage_path(){ + local path + path="$(read_env STORAGE_PATH_LOCAL "./local-storage")" + if [[ "$path" != /* ]]; then + path="${path#./}" + path="$ROOT_DIR/$path" + fi + echo "${path%/}" +} + +ensure_module_state(){ + if [ "$MODULE_STATE_INITIALIZED" -eq 1 ]; then + return + fi + + local storage_root + storage_root="$(resolve_local_storage_path)" + local output_dir="${storage_root}/modules" + + if ! python3 "$MODULE_HELPER" --env-path "$ENV_PATH" --manifest "$ROOT_DIR/config/modules.json" generate --output-dir "$output_dir"; then + err "Module manifest validation failed. See errors above." + fi + + if [ ! -f "$output_dir/modules.env" ]; then + err "modules.env not produced at $output_dir/modules.env" + fi + + # shellcheck disable=SC1090 + source "$output_dir/modules.env" + MODULE_STATE_INITIALIZED=1 + MODULES_COMPILE_LIST=() + 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 +} + resolve_project_name(){ local raw_name="$(read_env COMPOSE_PROJECT_NAME "acore-compose")" local sanitized @@ -327,14 +359,12 @@ detect_build_needed(){ fi # Check if any C++ modules are enabled but modules-latest images don't exist + ensure_module_state + local any_cxx_modules=0 - local var - for var in "${COMPILE_MODULE_VARS[@]}"; do - if [ "$(read_env "$var" "0")" = "1" ]; then - any_cxx_modules=1 - break - fi - done + if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then + any_cxx_modules=1 + fi if [ "$any_cxx_modules" = "1" ]; then local authserver_modules_image @@ -473,13 +503,11 @@ determine_profile(){ return fi - local var - for var in "${COMPILE_MODULE_VARS[@]}"; do - if [ "$(read_env "$var" "0")" = "1" ]; then - echo "modules" - return - fi - done + ensure_module_state + if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then + echo "modules" + return + fi echo "standard" } diff --git a/docker-compose.yml b/docker-compose.yml index 9da2457..f6f1957 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -590,7 +590,9 @@ services: - ${STORAGE_PATH}/modules:/modules - ${STORAGE_PATH}/config:/azerothcore/env/dist/etc - ./scripts:/tmp/scripts:ro + - ./config:/tmp/config:ro environment: + - MODULES_MANIFEST_PATH=/tmp/config/modules.json - MODULE_PLAYERBOTS=${MODULE_PLAYERBOTS} - MODULE_AOE_LOOT=${MODULE_AOE_LOOT} - MODULE_LEARN_SPELLS=${MODULE_LEARN_SPELLS} @@ -634,6 +636,8 @@ services: - MODULE_WORGOBLIN=${MODULE_WORGOBLIN} - MODULE_ELUNA_TS=${MODULE_ELUNA_TS} - CONTAINER_MYSQL=${CONTAINER_MYSQL} + - MYSQL_PORT=${MYSQL_PORT} + - MYSQL_USER=${MYSQL_USER} - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - DB_AUTH_NAME=${DB_AUTH_NAME} - DB_WORLD_NAME=${DB_WORLD_NAME} @@ -646,7 +650,7 @@ services: command: - -c - | - apk add --no-cache curl bash git + apk add --no-cache curl bash git python3 (chmod +x /tmp/scripts/manage-modules.sh /tmp/scripts/manage-modules-sql.sh 2>/dev/null || true) && /tmp/scripts/manage-modules.sh # Fix permissions after module operations chown -R ${CONTAINER_USER} /modules /azerothcore/env/dist/etc 2>/dev/null || true diff --git a/modules-state.json b/modules-state.json new file mode 100644 index 0000000..e500295 --- /dev/null +++ b/modules-state.json @@ -0,0 +1,1085 @@ +{ + "compile_modules": [ + "mod-aoe-loot", + "mod-learn-spells", + "mod-fireworks-on-level", + "mod-individual-progression", + "mod-ahbot", + "mod-autobalance", + "mod-transmog", + "mod-npc-buffer", + "mod-dynamic-xp", + "mod-solo-lfg", + "mod-1v1-arena", + "mod-phased-duels", + "mod-breaking-news-override", + "mod-boss-announcer", + "mod-account-achievements", + "mod-auto-revive", + "mod-gain-honor-guard", + "mod-ale", + "mod-TimeIsTime", + "mod-random-enchants", + "mod-solocraft", + "mod-pvp-titles", + "mod-npc-beastmaster", + "mod-npc-enchanter", + "mod-instance-reset", + "mod-quest-count-level", + "mod-arac", + "mod-assistant", + "mod-reagent-bank", + "mod-challenge-modes", + "mod-ollama-chat", + "mod-player-bot-level-brackets", + "StatBooster", + "skeleton-module", + "mod-bg-slaveryvalley", + "mod-azerothshard", + "mod-worgoblin" + ], + "enabled_modules": [ + "mod-playerbots", + "mod-aoe-loot", + "mod-learn-spells", + "mod-fireworks-on-level", + "mod-individual-progression", + "mod-ahbot", + "mod-autobalance", + "mod-transmog", + "mod-npc-buffer", + "mod-dynamic-xp", + "mod-solo-lfg", + "mod-1v1-arena", + "mod-phased-duels", + "mod-breaking-news-override", + "mod-boss-announcer", + "mod-account-achievements", + "mod-auto-revive", + "mod-gain-honor-guard", + "mod-ale", + "mod-TimeIsTime", + "mod-random-enchants", + "mod-solocraft", + "mod-pvp-titles", + "mod-npc-beastmaster", + "mod-npc-enchanter", + "mod-instance-reset", + "mod-quest-count-level", + "mod-arac", + "mod-assistant", + "mod-reagent-bank", + "mod-black-market", + "mod-challenge-modes", + "mod-ollama-chat", + "mod-player-bot-level-brackets", + "StatBooster", + "skeleton-module", + "mod-bg-slaveryvalley", + "mod-azerothshard", + "mod-worgoblin", + "eluna-ts" + ], + "env_path": "/home/upb/src/acore-compose/.env", + "errors": [], + "generated_at": "2025-10-31T20:30:32.837900+00:00", + "manifest_path": "/home/upb/src/acore-compose/config/modules.json", + "modules": [ + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "playerbots.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_PLAYERBOTS", + "module_type": "data", + "name": "mod-playerbots", + "needs_build": false, + "notes": "Installs SQL/config assets; core functionality is built into playerbot images", + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/mod-playerbots/mod-playerbots.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "mod_aoe_loot.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_AOE_LOOT", + "module_type": "cpp", + "name": "mod-aoe-loot", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-aoe-loot.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "mod_learnspells.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_LEARN_SPELLS", + "module_type": "cpp", + "name": "mod-learn-spells", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-learn-spells.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "mod_fireworks.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_FIREWORKS", + "module_type": "cpp", + "name": "mod-fireworks-on-level", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-fireworks-on-level.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "individual_progression.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_INDIVIDUAL_PROGRESSION", + "module_type": "cpp", + "name": "mod-individual-progression", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/ZhengPeiRu21/mod-individual-progression.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "mod_ahbot.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_AHBOT", + "module_type": "cpp", + "name": "mod-ahbot", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-ahbot.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "AutoBalance.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_AUTOBALANCE", + "module_type": "cpp", + "name": "mod-autobalance", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-autobalance.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "transmog.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_TRANSMOG", + "module_type": "cpp", + "name": "mod-transmog", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-transmog.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "npc_buffer.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_NPC_BUFFER", + "module_type": "cpp", + "name": "mod-npc-buffer", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-npc-buffer.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "Individual-XP.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_DYNAMIC_XP", + "module_type": "cpp", + "name": "mod-dynamic-xp", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-dynamic-xp.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "SoloLfg.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_SOLO_LFG", + "module_type": "cpp", + "name": "mod-solo-lfg", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-solo-lfg.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "1v1arena.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_1V1_ARENA", + "module_type": "cpp", + "name": "mod-1v1-arena", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-1v1-arena.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "phasedduels.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_PHASED_DUELS", + "module_type": "cpp", + "name": "mod-phased-duels", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-phased-duels.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "breaking_news.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_BREAKING_NEWS", + "module_type": "cpp", + "name": "mod-breaking-news-override", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-breaking-news-override.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "boss_announcer.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_BOSS_ANNOUNCER", + "module_type": "cpp", + "name": "mod-boss-announcer", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-boss-announcer.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "account_achievements.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_ACCOUNT_ACHIEVEMENTS", + "module_type": "cpp", + "name": "mod-account-achievements", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-account-achievements.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "AutoRevive.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_AUTO_REVIVE", + "module_type": "cpp", + "name": "mod-auto-revive", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-auto-revive.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "GainHonorGuard.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_GAIN_HONOR_GUARD", + "module_type": "cpp", + "name": "mod-gain-honor-guard", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-gain-honor-guard.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "mod_eluna.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_ELUNA", + "module_type": "cpp", + "name": "mod-ale", + "needs_build": true, + "notes": null, + "post_install_hooks": [ + "mod_ale_move_path_patch" + ], + "ref": null, + "repo": "https://github.com/azerothcore/mod-ale.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "mod-time_is_time.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_TIME_IS_TIME", + "module_type": "cpp", + "name": "mod-TimeIsTime", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/dunjeon/mod-TimeIsTime.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": "Requires C++20 std::format support patch before enabling", + "blocked": true, + "config_cleanup": [ + "pocketportal.conf*" + ], + "dependency_issues": [], + "enabled_effective": false, + "enabled_raw": false, + "errors": [], + "key": "MODULE_POCKET_PORTAL", + "module_type": "cpp", + "name": "mod-pocket-portal", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-pocket-portal.git", + "requires": [], + "sql": null, + "status": "blocked", + "value": "0", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "RandomEnchants.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_RANDOM_ENCHANTS", + "module_type": "cpp", + "name": "mod-random-enchants", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-random-enchants.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "Solocraft.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_SOLOCRAFT", + "module_type": "cpp", + "name": "mod-solocraft", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-solocraft.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "mod_pvptitles.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_PVP_TITLES", + "module_type": "cpp", + "name": "mod-pvp-titles", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-pvp-titles.git", + "requires": [ + "MODULE_ELUNA" + ], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "npc_beastmaster.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_NPC_BEASTMASTER", + "module_type": "cpp", + "name": "mod-npc-beastmaster", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-npc-beastmaster.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "npc_enchanter.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_NPC_ENCHANTER", + "module_type": "cpp", + "name": "mod-npc-enchanter", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-npc-enchanter.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "instance-reset.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_INSTANCE_RESET", + "module_type": "cpp", + "name": "mod-instance-reset", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-instance-reset.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "levelGrant.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_LEVEL_GRANT", + "module_type": "cpp", + "name": "mod-quest-count-level", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/michaeldelago/mod-quest-count-level.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [ + "arac.conf*" + ], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_ARAC", + "module_type": "cpp", + "name": "mod-arac", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/heyitsbench/mod-arac.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_ASSISTANT", + "module_type": "cpp", + "name": "mod-assistant", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/noisiver/mod-assistant.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_REAGENT_BANK", + "module_type": "cpp", + "name": "mod-reagent-bank", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/ZhengPeiRu21/mod-reagent-bank.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_BLACK_MARKET_AUCTION_HOUSE", + "module_type": "lua", + "name": "mod-black-market", + "needs_build": false, + "notes": null, + "post_install_hooks": [ + "black_market_copy_lua" + ], + "ref": null, + "repo": "https://github.com/Youpeoples/Black-Market-Auction-House.git", + "requires": [ + "MODULE_ELUNA" + ], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_CHALLENGE_MODES", + "module_type": "cpp", + "name": "mod-challenge-modes", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/ZhengPeiRu21/mod-challenge-modes.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_OLLAMA_CHAT", + "module_type": "cpp", + "name": "mod-ollama-chat", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/DustinHendrickson/mod-ollama-chat.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_PLAYER_BOT_LEVEL_BRACKETS", + "module_type": "cpp", + "name": "mod-player-bot-level-brackets", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/DustinHendrickson/mod-player-bot-level-brackets.git", + "requires": [ + "MODULE_PLAYERBOTS" + ], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_STATBOOSTER", + "module_type": "cpp", + "name": "StatBooster", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/AnchyDev/StatBooster.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": "Upstream override signature mismatch (OnBeforeTeleport); awaiting fix", + "blocked": true, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": false, + "enabled_raw": false, + "errors": [], + "key": "MODULE_DUNGEON_RESPAWN", + "module_type": "cpp", + "name": "DungeonRespawn", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/AnchyDev/DungeonRespawn.git", + "requires": [], + "sql": null, + "status": "blocked", + "value": "0", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_SKELETON_MODULE", + "module_type": "cpp", + "name": "skeleton-module", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/skeleton-module.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_BG_SLAVERYVALLEY", + "module_type": "cpp", + "name": "mod-bg-slaveryvalley", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/Helias/mod-bg-slaveryvalley.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_AZEROTHSHARD", + "module_type": "cpp", + "name": "mod-azerothshard", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/mod-azerothshard.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_WORGOBLIN", + "module_type": "cpp", + "name": "mod-worgoblin", + "needs_build": true, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/heyitsbench/mod-worgoblin.git", + "requires": [], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + }, + { + "block_reason": null, + "blocked": false, + "config_cleanup": [], + "dependency_issues": [], + "enabled_effective": true, + "enabled_raw": true, + "errors": [], + "key": "MODULE_ELUNA_TS", + "module_type": "tool", + "name": "eluna-ts", + "needs_build": false, + "notes": null, + "post_install_hooks": [], + "ref": null, + "repo": "https://github.com/azerothcore/eluna-ts.git", + "requires": [ + "MODULE_ELUNA" + ], + "sql": null, + "status": "active", + "value": "1", + "warnings": [] + } + ], + "requires_playerbot_source": true, + "warnings": [] +} diff --git a/modules.env b/modules.env new file mode 100644 index 0000000..085bb21 --- /dev/null +++ b/modules.env @@ -0,0 +1,51 @@ +# Autogenerated by scripts/modules.py +# Generated at 2025-10-31T20:30:32.837900+00:00 +export MODULES_MANIFEST="/home/upb/src/acore-compose/config/modules.json" +export MODULES_ENV_PATH="/home/upb/src/acore-compose/.env" +export MODULE_PLAYERBOTS=1 +export MODULE_AOE_LOOT=1 +export MODULE_LEARN_SPELLS=1 +export MODULE_FIREWORKS=1 +export MODULE_INDIVIDUAL_PROGRESSION=1 +export MODULE_AHBOT=1 +export MODULE_AUTOBALANCE=1 +export MODULE_TRANSMOG=1 +export MODULE_NPC_BUFFER=1 +export MODULE_DYNAMIC_XP=1 +export MODULE_SOLO_LFG=1 +export MODULE_1V1_ARENA=1 +export MODULE_PHASED_DUELS=1 +export MODULE_BREAKING_NEWS=1 +export MODULE_BOSS_ANNOUNCER=1 +export MODULE_ACCOUNT_ACHIEVEMENTS=1 +export MODULE_AUTO_REVIVE=1 +export MODULE_GAIN_HONOR_GUARD=1 +export MODULE_ELUNA=1 +export MODULE_TIME_IS_TIME=1 +export MODULE_POCKET_PORTAL=0 +export MODULE_RANDOM_ENCHANTS=1 +export MODULE_SOLOCRAFT=1 +export MODULE_PVP_TITLES=1 +export MODULE_NPC_BEASTMASTER=1 +export MODULE_NPC_ENCHANTER=1 +export MODULE_INSTANCE_RESET=1 +export MODULE_LEVEL_GRANT=1 +export MODULE_ARAC=1 +export MODULE_ASSISTANT=1 +export MODULE_REAGENT_BANK=1 +export MODULE_BLACK_MARKET_AUCTION_HOUSE=1 +export MODULE_CHALLENGE_MODES=1 +export MODULE_OLLAMA_CHAT=1 +export MODULE_PLAYER_BOT_LEVEL_BRACKETS=1 +export MODULE_STATBOOSTER=1 +export MODULE_DUNGEON_RESPAWN=0 +export MODULE_SKELETON_MODULE=1 +export MODULE_BG_SLAVERYVALLEY=1 +export MODULE_AZEROTHSHARD=1 +export MODULE_WORGOBLIN=1 +export MODULE_ELUNA_TS=1 +export MODULES_ENABLED="mod-playerbots mod-aoe-loot mod-learn-spells mod-fireworks-on-level mod-individual-progression mod-ahbot mod-autobalance mod-transmog mod-npc-buffer mod-dynamic-xp mod-solo-lfg mod-1v1-arena mod-phased-duels mod-breaking-news-override mod-boss-announcer mod-account-achievements mod-auto-revive mod-gain-honor-guard mod-ale mod-TimeIsTime mod-random-enchants mod-solocraft mod-pvp-titles mod-npc-beastmaster mod-npc-enchanter mod-instance-reset mod-quest-count-level mod-arac mod-assistant mod-reagent-bank mod-black-market mod-challenge-modes mod-ollama-chat mod-player-bot-level-brackets StatBooster skeleton-module mod-bg-slaveryvalley mod-azerothshard mod-worgoblin eluna-ts" +export MODULES_COMPILE="mod-aoe-loot mod-learn-spells mod-fireworks-on-level mod-individual-progression mod-ahbot mod-autobalance mod-transmog mod-npc-buffer mod-dynamic-xp mod-solo-lfg mod-1v1-arena mod-phased-duels mod-breaking-news-override mod-boss-announcer mod-account-achievements mod-auto-revive mod-gain-honor-guard mod-ale mod-TimeIsTime mod-random-enchants mod-solocraft mod-pvp-titles mod-npc-beastmaster mod-npc-enchanter mod-instance-reset mod-quest-count-level mod-arac mod-assistant mod-reagent-bank mod-challenge-modes mod-ollama-chat mod-player-bot-level-brackets StatBooster skeleton-module mod-bg-slaveryvalley mod-azerothshard mod-worgoblin" +export MODULES_REQUIRES_PLAYERBOT_SOURCE=1 +export MODULES_WARNING_COUNT=0 +export MODULES_ERROR_COUNT=0 diff --git a/scripts/check_module_staging.py b/scripts/check_module_staging.py new file mode 100755 index 0000000..576d55d --- /dev/null +++ b/scripts/check_module_staging.py @@ -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()) diff --git a/scripts/manage-modules-sql.sh b/scripts/manage-modules-sql.sh index fd0189d..ba56eac 100755 --- a/scripts/manage-modules-sql.sh +++ b/scripts/manage-modules-sql.sh @@ -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'(?/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 } diff --git a/scripts/manage-modules.sh b/scripts/manage-modules.sh index 235a395..658ed37 100755 --- a/scripts/manage-modules.sh +++ b/scripts/manage-modules.sh @@ -1,912 +1,439 @@ #!/bin/bash -# ac-compose -set -e + +# Manifest-driven module management. Stages repositories, applies module +# metadata hooks, manages configuration files, and flags rebuild requirements. + +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" - -echo 'Setting up git user' -git config --global user.name "${GIT_USERNAME:-ac-compose}" -git config --global user.email "${GIT_EMAIL:-noreply@azerothcore.org}" -# PAT not needed for public repositories - -echo 'Initializing module management...' -if [ "$MODULES_LOCAL_RUN" != "1" ]; then - cd /modules -fi - -echo 'Cleaning up disabled modules...' - -# Playerbots are integrated into the source - no separate module to remove - -if [ "$MODULE_AOE_LOOT" != "1" ] && [ -d "mod-aoe-loot" ]; then - echo 'Removing mod-aoe-loot (disabled)...' - rm -rf mod-aoe-loot -fi - -if [ "$MODULE_LEARN_SPELLS" != "1" ] && [ -d "mod-learn-spells" ]; then - echo 'Removing mod-learn-spells (disabled)...' - rm -rf mod-learn-spells -fi - -if [ "$MODULE_FIREWORKS" != "1" ] && [ -d "mod-fireworks-on-level" ]; then - echo 'Removing mod-fireworks-on-level (disabled)...' - rm -rf mod-fireworks-on-level -fi - -if [ "$MODULE_INDIVIDUAL_PROGRESSION" != "1" ] && [ -d "mod-individual-progression" ]; then - echo 'Removing mod-individual-progression (disabled)...' - rm -rf mod-individual-progression -fi - -if [ "$MODULE_AHBOT" != "1" ] && [ -d "mod-ahbot" ]; then - echo 'Removing mod-ahbot (disabled)...' - rm -rf mod-ahbot -fi - -if [ "$MODULE_AUTOBALANCE" != "1" ] && [ -d "mod-autobalance" ]; then - echo 'Removing mod-autobalance (disabled)...' - rm -rf mod-autobalance -fi - -if [ "$MODULE_TRANSMOG" != "1" ] && [ -d "mod-transmog" ]; then - echo 'Removing mod-transmog (disabled)...' - rm -rf mod-transmog -fi - -if [ "$MODULE_NPC_BUFFER" != "1" ] && [ -d "mod-npc-buffer" ]; then - echo 'Removing mod-npc-buffer (disabled)...' - rm -rf mod-npc-buffer -fi - -if [ "$MODULE_DYNAMIC_XP" != "1" ] && [ -d "mod-dynamic-xp" ]; then - echo 'Removing mod-dynamic-xp (disabled)...' - rm -rf mod-dynamic-xp -fi - -if [ "$MODULE_SOLO_LFG" != "1" ] && [ -d "mod-solo-lfg" ]; then - echo 'Removing mod-solo-lfg (disabled)...' - rm -rf mod-solo-lfg -fi - -if [ "$MODULE_1V1_ARENA" != "1" ] && [ -d "mod-1v1-arena" ]; then - echo 'Removing mod-1v1-arena (disabled)...' - rm -rf mod-1v1-arena -fi - -if [ "$MODULE_PHASED_DUELS" != "1" ] && [ -d "mod-phased-duels" ]; then - echo 'Removing mod-phased-duels (disabled)...' - rm -rf mod-phased-duels -fi - -if [ "$MODULE_BREAKING_NEWS" != "1" ] && [ -d "mod-breaking-news-override" ]; then - echo 'Removing mod-breaking-news-override (disabled)...' - rm -rf mod-breaking-news-override -fi - -if [ "$MODULE_BOSS_ANNOUNCER" != "1" ] && [ -d "mod-boss-announcer" ]; then - echo 'Removing mod-boss-announcer (disabled)...' - rm -rf mod-boss-announcer -fi - -if [ "$MODULE_ACCOUNT_ACHIEVEMENTS" != "1" ] && [ -d "mod-account-achievements" ]; then - echo 'Removing mod-account-achievements (disabled)...' - rm -rf mod-account-achievements -fi - -if [ "$MODULE_AUTO_REVIVE" != "1" ] && [ -d "mod-auto-revive" ]; then - echo 'Removing mod-auto-revive (disabled)...' - rm -rf mod-auto-revive -fi - -if [ "$MODULE_GAIN_HONOR_GUARD" != "1" ] && [ -d "mod-gain-honor-guard" ]; then - echo 'Removing mod-gain-honor-guard (disabled)...' - rm -rf mod-gain-honor-guard -fi - -if [ "$MODULE_ELUNA" != "1" ] && [ -d "mod-ale" ]; then - echo 'Removing mod-ale (disabled)...' - rm -rf mod-ale -fi -if [ "$MODULE_ELUNA" = "1" ] && [ -d "mod-eluna" ]; then - echo 'Removing legacy mod-eluna directory (superseded by mod-ale)...' - rm -rf mod-eluna -fi -if [ "$MODULE_ARAC" != "1" ] && [ -d "mod-arac" ]; then - echo 'Removing mod-arac (disabled)...' - rm -rf mod-arac -fi - -if [ "$MODULE_TIME_IS_TIME" != "1" ] && [ -d "mod-TimeIsTime" ]; then - echo 'Removing mod-TimeIsTime (disabled)...' - rm -rf mod-TimeIsTime -fi - -if [ "$MODULE_POCKET_PORTAL" = "1" ]; then - echo '⚠️ mod-pocket-portal is temporarily disabled (requires C++20 ). Skipping until patched.' - echo ' Apply the std::format -> SendSystemMessage fix before re-enabling this module.' - MODULE_POCKET_PORTAL=0 -fi - -if [ "$MODULE_POCKET_PORTAL" != "1" ] && [ -d "mod-pocket-portal" ]; then - echo 'Removing mod-pocket-portal (disabled)...' - rm -rf mod-pocket-portal -fi - -if [ "$MODULE_RANDOM_ENCHANTS" != "1" ] && [ -d "mod-random-enchants" ]; then - echo 'Removing mod-random-enchants (disabled)...' - rm -rf mod-random-enchants -fi - -if [ "$MODULE_SOLOCRAFT" != "1" ] && [ -d "mod-solocraft" ]; then - echo 'Removing mod-solocraft (disabled)...' - rm -rf mod-solocraft -fi - -if [ "$MODULE_PVP_TITLES" != "1" ] && [ -d "mod-pvp-titles" ]; then - echo 'Removing mod-pvp-titles (disabled)...' - rm -rf mod-pvp-titles -fi - -if [ "$MODULE_NPC_BEASTMASTER" != "1" ] && [ -d "mod-npc-beastmaster" ]; then - echo 'Removing mod-npc-beastmaster (disabled)...' - rm -rf mod-npc-beastmaster -fi - -if [ "$MODULE_NPC_ENCHANTER" != "1" ] && [ -d "mod-npc-enchanter" ]; then - echo 'Removing mod-npc-enchanter (disabled)...' - rm -rf mod-npc-enchanter -fi - -if [ "$MODULE_INSTANCE_RESET" != "1" ] && [ -d "mod-instance-reset" ]; then - echo 'Removing mod-instance-reset (disabled)...' - rm -rf mod-instance-reset -fi - -if [ "$MODULE_LEVEL_GRANT" != "1" ] && [ -d "mod-quest-count-level" ]; then - echo 'Removing mod-quest-count-level (disabled)...' - rm -rf mod-quest-count-level -fi -if [ "$MODULE_ASSISTANT" != "1" ] && [ -d "mod-assistant" ]; then - echo 'Removing mod-assistant (disabled)...' - rm -rf mod-assistant -fi -if [ "$MODULE_REAGENT_BANK" != "1" ] && [ -d "mod-reagent-bank" ]; then - echo 'Removing mod-reagent-bank (disabled)...' - rm -rf mod-reagent-bank -fi -if [ "$MODULE_BLACK_MARKET_AUCTION_HOUSE" != "1" ] && [ -d "mod-black-market" ]; then - echo 'Removing mod-black-market (disabled)...' - rm -rf mod-black-market -fi - -if [ "$MODULE_CHALLENGE_MODES" != "1" ] && [ -d "mod-challenge-modes" ]; then - echo 'Removing mod-challenge-modes (disabled)...' - rm -rf mod-challenge-modes -fi - -if [ "$MODULE_OLLAMA_CHAT" != "1" ] && [ -d "mod-ollama-chat" ]; then - echo 'Removing mod-ollama-chat (disabled)...' - rm -rf mod-ollama-chat -fi - -if [ "$MODULE_PLAYER_BOT_LEVEL_BRACKETS" != "1" ] && [ -d "mod-player-bot-level-brackets" ]; then - echo 'Removing mod-player-bot-level-brackets (disabled)...' - rm -rf mod-player-bot-level-brackets -fi - -if [ "$MODULE_STATBOOSTER" != "1" ] && [ -d "StatBooster" ]; then - echo 'Removing StatBooster (disabled)...' - rm -rf StatBooster -fi - -if [ "$MODULE_DUNGEON_RESPAWN" != "1" ] && [ -d "DungeonRespawn" ]; then - echo 'Removing DungeonRespawn (disabled)...' - rm -rf DungeonRespawn -fi - -if [ "$MODULE_SKELETON_MODULE" != "1" ] && [ -d "skeleton-module" ]; then - echo 'Removing skeleton-module (disabled)...' - rm -rf skeleton-module -fi - -if [ "$MODULE_BG_SLAVERYVALLEY" != "1" ] && [ -d "mod-bg-slaveryvalley" ]; then - echo 'Removing mod-bg-slaveryvalley (disabled)...' - rm -rf mod-bg-slaveryvalley -fi - -if [ "$MODULE_AZEROTHSHARD" != "1" ] && [ -d "mod-azerothshard" ]; then - echo 'Removing mod-azerothshard (disabled)...' - rm -rf mod-azerothshard -fi - -if [ "$MODULE_WORGOBLIN" != "1" ] && [ -d "mod-worgoblin" ]; then - echo 'Removing mod-worgoblin (disabled)...' - rm -rf mod-worgoblin -fi - -if [ "$MODULE_ELUNA_TS" != "1" ] && [ -d "eluna-ts" ]; then - echo 'Removing eluna-ts (disabled)...' - rm -rf eluna-ts -fi - -echo 'Installing enabled modules...' - -# Playerbots handling - integrated into uprightbass360 AzerothCore fork -if [ "$MODULE_PLAYERBOTS" = "1" ] && [ ! -d "mod-playerbots" ]; then - echo 'πŸ€– Installing mod-playerbots module (required for uprightbass360 images)...' - echo ' πŸ“– Project: https://github.com/mod-playerbots/mod-playerbots' - echo ' ℹ️ Core C++ functionality built into uprightbass360/azerothcore-wotlk-playerbots images' - echo ' πŸ—„οΈ Installing SQL schemas, configurations, and database structure' - echo ' πŸ”§ Module provides: DBC tables, bot names, AI configs, travel nodes, etc.' - echo ' πŸ“‹ POST-INSTALL: Requires manual account/character configuration' - - # Clone the official mod-playerbots module for SQL and configuration files - git clone https://github.com/mod-playerbots/mod-playerbots.git mod-playerbots - - echo ' βœ… mod-playerbots module installed successfully' - echo ' πŸ“Š Module contains SQL for 3 databases: world, characters, playerbots' -fi - -# Install AOE Loot if enabled -if [ "$MODULE_AOE_LOOT" = "1" ] && [ ! -d "mod-aoe-loot" ]; then - echo 'πŸ’° Installing mod-aoe-loot...' - echo ' πŸ“– Project: https://github.com/azerothcore/mod-aoe-loot' - echo ' ℹ️ Allows looting multiple corpses with one action' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/azerothcore/mod-aoe-loot.git mod-aoe-loot -fi - -# Install Learn Spells if enabled -if [ "$MODULE_LEARN_SPELLS" = "1" ] && [ ! -d "mod-learn-spells" ]; then - echo 'πŸ“š Installing mod-learn-spells...' - echo ' πŸ“– Project: https://github.com/azerothcore/mod-learn-spells' - echo ' ℹ️ Automatically teaches class spells on level up' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/azerothcore/mod-learn-spells.git mod-learn-spells -fi - -# Install Fireworks on Level if enabled -if [ "$MODULE_FIREWORKS" = "1" ] && [ ! -d "mod-fireworks-on-level" ]; then - echo 'πŸŽ† Installing mod-fireworks-on-level...' - echo ' πŸ“– Project: https://github.com/azerothcore/mod-fireworks-on-level' - echo ' ℹ️ Displays fireworks when players level up' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/azerothcore/mod-fireworks-on-level.git mod-fireworks-on-level -fi - -# Install Individual Progression if enabled -if [ "$MODULE_INDIVIDUAL_PROGRESSION" = "1" ] && [ ! -d "mod-individual-progression" ]; then - echo '⏳ Installing mod-individual-progression...' - echo ' πŸ“– Project: https://github.com/ZhengPeiRu21/mod-individual-progression' - echo ' ℹ️ Simulates authentic Vanillaβ†’TBCβ†’WotLK progression per player' - echo ' βœ… AUTO-CONFIG: Automatically sets EnablePlayerSettings=1 and DBC.EnforceItemAttributes=0' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - echo ' πŸ“ Optional client files available in optional/ directory' - git clone https://github.com/ZhengPeiRu21/mod-individual-progression.git mod-individual-progression -fi - -# Quality of Life Modules -if [ "$MODULE_AHBOT" = "1" ] && [ ! -d "mod-ahbot" ]; then - echo 'πŸͺ Installing mod-ahbot...' - echo ' πŸ“– Project: https://github.com/azerothcore/mod-ahbot' - echo ' ℹ️ Auction house bot that buys and sells items automatically' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - echo ' πŸ“‹ POST-INSTALL: Requires manual account/character setup in mod_ahbot.conf' - git clone https://github.com/azerothcore/mod-ahbot.git mod-ahbot -fi - -if [ "$MODULE_AUTOBALANCE" = "1" ] && [ ! -d "mod-autobalance" ]; then - echo 'βš–οΈ Installing mod-autobalance...' - echo ' πŸ“– Project: https://github.com/azerothcore/mod-autobalance' - echo ' ℹ️ Automatically adjusts dungeon difficulty based on party size' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/azerothcore/mod-autobalance.git mod-autobalance -fi - -if [ "$MODULE_TRANSMOG" = "1" ] && [ ! -d "mod-transmog" ]; then - echo '🎭 Installing mod-transmog...' - echo ' πŸ“– Project: https://github.com/azerothcore/mod-transmog' - echo ' ℹ️ Allows appearance customization of equipment' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/azerothcore/mod-transmog.git mod-transmog -fi - -if [ "$MODULE_NPC_BUFFER" = "1" ] && [ ! -d "mod-npc-buffer" ]; then - echo 'Installing mod-npc-buffer...' - git clone https://github.com/azerothcore/mod-npc-buffer.git mod-npc-buffer -fi - -# Gameplay Enhancement Modules -if [ "$MODULE_DYNAMIC_XP" = "1" ] && [ ! -d "mod-dynamic-xp" ]; then - echo 'Installing mod-dynamic-xp...' - git clone https://github.com/azerothcore/mod-dynamic-xp.git mod-dynamic-xp -fi - -if [ "$MODULE_SOLO_LFG" = "1" ] && [ ! -d "mod-solo-lfg" ]; then - echo 'πŸ” Installing mod-solo-lfg...' - echo ' πŸ“– Project: https://github.com/azerothcore/mod-solo-lfg' - echo ' ℹ️ Allows dungeon finder for solo players and small groups' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - echo ' πŸ’‘ Pairs perfectly with mod-solocraft and mod-autobalance' - git clone https://github.com/azerothcore/mod-solo-lfg.git mod-solo-lfg -fi - -if [ "$MODULE_1V1_ARENA" = "1" ] && [ ! -d "mod-1v1-arena" ]; then - echo 'Installing mod-1v1-arena...' - git clone https://github.com/azerothcore/mod-1v1-arena.git mod-1v1-arena -fi - -if [ "$MODULE_PHASED_DUELS" = "1" ] && [ ! -d "mod-phased-duels" ]; then - echo 'Installing mod-phased-duels...' - git clone https://github.com/azerothcore/mod-phased-duels.git mod-phased-duels -fi - -# Server Management Modules -if [ "$MODULE_BREAKING_NEWS" = "1" ] && [ ! -d "mod-breaking-news-override" ]; then - echo 'πŸ“° Installing mod-breaking-news-override...' - echo ' πŸ“– Project: https://github.com/azerothcore/mod-breaking-news-override' - echo ' ℹ️ Displays custom breaking news on character selection screen' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - echo ' πŸ“‹ POST-INSTALL: Requires custom HTML file creation and path configuration' - git clone https://github.com/azerothcore/mod-breaking-news-override.git mod-breaking-news-override -fi - -if [ "$MODULE_BOSS_ANNOUNCER" = "1" ] && [ ! -d "mod-boss-announcer" ]; then - echo 'Installing mod-boss-announcer...' - git clone https://github.com/azerothcore/mod-boss-announcer.git mod-boss-announcer -fi - -if [ "$MODULE_ACCOUNT_ACHIEVEMENTS" = "1" ] && [ ! -d "mod-account-achievements" ]; then - echo 'Installing mod-account-achievements...' - git clone https://github.com/azerothcore/mod-account-achievements.git mod-account-achievements -fi - -# Additional Modules Found in Config -if [ "$MODULE_AUTO_REVIVE" = "1" ] && [ ! -d "mod-auto-revive" ]; then - echo 'Installing mod-auto-revive...' - git clone https://github.com/azerothcore/mod-auto-revive.git mod-auto-revive -fi - -if [ "$MODULE_GAIN_HONOR_GUARD" = "1" ] && [ ! -d "mod-gain-honor-guard" ]; then - echo 'Installing mod-gain-honor-guard...' - git clone https://github.com/azerothcore/mod-gain-honor-guard.git mod-gain-honor-guard -fi - -if [ "$MODULE_ELUNA" = "1" ] && [ ! -d "mod-ale" ]; then - echo 'πŸ–₯️ Installing mod-ale (AzerothCore Lua Engine)...' - echo ' πŸ“– Project: https://github.com/azerothcore/mod-ale' - echo ' ℹ️ Next-generation Lua scripting engine for AzerothCore' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/azerothcore/mod-ale.git mod-ale -fi - -if [ -d "mod-ale" ]; then - creature_methods_file="mod-ale/src/LuaEngine/methods/CreatureMethods.h" - if grep -q 'MoveWaypoint(creature->GetWaypointPath(), true);' "$creature_methods_file" 2>/dev/null; then - if sed -i 's/MoveWaypoint(creature->GetWaypointPath(), true);/MovePath(creature->GetWaypointPath(), true);/' "$creature_methods_file"; then - echo ' βœ… Applied mod-ale MovePath compatibility fix (upstream issue #336)' +MODULE_HELPER="$SCRIPT_DIR/modules.py" +DEFAULT_ENV_PATH="$PROJECT_ROOT/.env" +ENV_PATH="${MODULES_ENV_PATH:-$DEFAULT_ENV_PATH}" + +BLUE='\033[0;34m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' +PLAYERBOTS_DB_UPDATE_LOGGED=0 +info(){ printf '%b\n' "${BLUE}ℹ️ $*${NC}"; } +ok(){ printf '%b\n' "${GREEN}βœ… $*${NC}"; } +warn(){ printf '%b\n' "${YELLOW}⚠️ $*${NC}"; } +err(){ printf '%b\n' "${RED}❌ $*${NC}"; exit 1; } + +ensure_python(){ + if ! command -v python3 >/dev/null 2>&1; then + err "python3 is required but not installed in PATH" + fi +} + +resolve_manifest_path(){ + if [ -n "${MODULES_MANIFEST_PATH:-}" ] && [ -f "${MODULES_MANIFEST_PATH}" ]; then + echo "${MODULES_MANIFEST_PATH}" + return + fi + local candidate + candidate="$PROJECT_ROOT/config/modules.json" + if [ -f "$candidate" ]; then + echo "$candidate" + return + fi + candidate="$SCRIPT_DIR/../config/modules.json" + if [ -f "$candidate" ]; then + echo "$candidate" + return + fi + candidate="/tmp/config/modules.json" + if [ -f "$candidate" ]; then + echo "$candidate" + return + fi + err "Unable to locate module manifest (set MODULES_MANIFEST_PATH or ensure config/modules.json exists)" +} + +setup_git_config(){ + info "Configuring git identity" + git config --global user.name "${GIT_USERNAME:-ac-compose}" >/dev/null 2>&1 || true + git config --global user.email "${GIT_EMAIL:-noreply@azerothcore.org}" >/dev/null 2>&1 || true +} + +generate_module_state(){ + mkdir -p "$STATE_DIR" + if ! python3 "$MODULE_HELPER" --env-path "$ENV_PATH" --manifest "$MANIFEST_PATH" generate --output-dir "$STATE_DIR"; then + err "Module manifest validation failed" + fi + local env_file="$STATE_DIR/modules.env" + if [ ! -f "$env_file" ]; then + err "modules.env not produced at $env_file" + fi + # shellcheck disable=SC1090 + source "$env_file" + if ! MODULE_SHELL_STATE="$(python3 "$MODULE_HELPER" --env-path "$ENV_PATH" --manifest "$MANIFEST_PATH" dump --format shell)"; then + err "Unable to load manifest metadata" + fi + local eval_script + eval_script="$(echo "$MODULE_SHELL_STATE" | sed 's/^declare -A /declare -gA /')" + eval "$eval_script" + 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 +} + +remove_disabled_modules(){ + for key in "${MODULE_KEYS[@]}"; do + local dir + dir="${MODULE_NAME[$key]:-}" + [ -n "$dir" ] || continue + if [ "${MODULE_ENABLED[$key]:-0}" != "1" ] && [ -d "$dir" ]; then + info "Removing ${dir} (disabled)" + rm -rf "$dir" + fi + done +} + +run_post_install_hooks(){ + local key="$1" + local dir="$2" + local hooks_csv="${MODULE_POST_INSTALL[$key]:-}" + IFS=',' read -r -a hooks <<< "$hooks_csv" + for hook in "${hooks[@]}"; do + [ -n "$hook" ] || continue + case "$hook" in + mod_ale_move_path_patch) + apply_mod_ale_patch "$dir" + ;; + black_market_copy_lua) + copy_black_market_lua "$dir" + ;; + *) + warn "Unknown post-install hook '$hook' for ${MODULE_NAME[$key]:-}" + ;; + esac + done +} + +install_enabled_modules(){ + for key in "${MODULE_KEYS[@]}"; do + if [ "${MODULE_ENABLED[$key]:-0}" != "1" ]; then + continue + fi + local dir repo ref + dir="${MODULE_NAME[$key]:-}" + repo="${MODULE_REPO[$key]:-}" + ref="${MODULE_REF[$key]:-}" + if [ -z "$dir" ] || [ -z "$repo" ]; then + warn "Missing repository metadata for $key" + continue + fi + if [ -d "$dir/.git" ]; then + info "$dir already present; skipping clone" + elif [ -d "$dir" ]; then + warn "$dir exists but is not a git repository; leaving in place" else - echo ' ⚠️ Failed to adjust mod-ale MoveWaypoint call' + info "Cloning ${dir} from ${repo}" + if ! git clone "$repo" "$dir"; then + err "Failed to clone $repo" + fi + if [ -n "$ref" ]; then + (cd "$dir" && git checkout "$ref") || warn "Unable to checkout ref $ref for $dir" + fi + fi + run_post_install_hooks "$key" "$dir" + done +} + +apply_mod_ale_patch(){ + local module_dir="$1" + local target_file="$module_dir/src/LuaEngine/methods/CreatureMethods.h" + if [ ! -f "$target_file" ]; then + warn "mod-ale file missing for MovePath patch ($target_file)" + return + fi + if grep -q 'MoveWaypoint(creature->GetWaypointPath(), true);' "$target_file"; then + if sed -i 's/MoveWaypoint(creature->GetWaypointPath(), true);/MovePath(creature->GetWaypointPath(), FORCED_MOVEMENT_RUN);/' "$target_file"; then + ok "Applied mod-ale MovePath compatibility fix" + else + warn "Failed to adjust mod-ale MovePath call" fi else - echo ' ℹ️ mod-ale MovePath compatibility fix already present' + info "mod-ale MovePath compatibility fix already present" fi -fi +} -if [ "$MODULE_ARAC" = "1" ] && [ ! -d "mod-arac" ]; then - echo '🌈 Installing mod-arac...' - echo ' πŸ“– Project: https://github.com/heyitsbench/mod-arac' - echo ' ℹ️ All Races All Classes - Removes class restrictions' - echo ' 🚨 CRITICAL: Requires DBC file updates and client patch!' - echo ' πŸ“‹ POST-INSTALL: Apply Patch-A.MPQ to client WoW/Data/ directory' - git clone https://github.com/heyitsbench/mod-arac.git mod-arac -fi - -if [ "$MODULE_TIME_IS_TIME" = "1" ] && [ ! -d "mod-TimeIsTime" ]; then - echo 'Installing mod-TimeIsTime...' - git clone https://github.com/dunjeon/mod-TimeIsTime.git mod-TimeIsTime -fi - -if [ "$MODULE_RANDOM_ENCHANTS" = "1" ] && [ ! -d "mod-random-enchants" ]; then - echo 'Installing mod-random-enchants...' - git clone https://github.com/azerothcore/mod-random-enchants.git mod-random-enchants -fi - -if [ "$MODULE_SOLOCRAFT" = "1" ] && [ ! -d "mod-solocraft" ]; then - echo '🎯 Installing mod-solocraft...' - echo ' πŸ“– Project: https://github.com/azerothcore/mod-solocraft' - echo ' ℹ️ Scales dungeon/raid difficulty for solo players' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - echo ' πŸ’‘ Works well with mod-autobalance and mod-solo-lfg' - git clone https://github.com/azerothcore/mod-solocraft.git mod-solocraft -fi - -if [ "$MODULE_PVP_TITLES" = "1" ] && [ ! -d "mod-pvp-titles" ]; then - echo 'Installing mod-pvp-titles...' - git clone https://github.com/azerothcore/mod-pvp-titles.git mod-pvp-titles -fi - -if [ "$MODULE_NPC_BEASTMASTER" = "1" ] && [ ! -d "mod-npc-beastmaster" ]; then - echo 'Installing mod-npc-beastmaster...' - git clone https://github.com/azerothcore/mod-npc-beastmaster.git mod-npc-beastmaster -fi - -if [ "$MODULE_NPC_ENCHANTER" = "1" ] && [ ! -d "mod-npc-enchanter" ]; then - echo '✨ Installing mod-npc-enchanter...' - echo ' πŸ“– Project: https://github.com/azerothcore/mod-npc-enchanter' - echo ' ℹ️ NPC that provides enchanting services' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/azerothcore/mod-npc-enchanter.git mod-npc-enchanter -fi - -if [ "$MODULE_INSTANCE_RESET" = "1" ] && [ ! -d "mod-instance-reset" ]; then - echo 'Installing mod-instance-reset...' - git clone https://github.com/azerothcore/mod-instance-reset.git mod-instance-reset -fi - -if [ "$MODULE_LEVEL_GRANT" = "1" ] && [ ! -d "mod-quest-count-level" ]; then - echo 'Installing mod-quest-count-level...' - git clone https://github.com/michaeldelago/mod-quest-count-level.git mod-quest-count-level -fi -if [ "$MODULE_ASSISTANT" = "1" ] && [ ! -d "mod-assistant" ]; then - echo 'πŸ€– Installing mod-assistant...' - echo ' πŸ“– Project: https://github.com/noisiver/mod-assistant' - echo ' ℹ️ NPC (ID: 9000000) providing heirlooms, glyphs, gems, profession services' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/noisiver/mod-assistant.git mod-assistant -fi -if [ "$MODULE_REAGENT_BANK" = "1" ] && [ ! -d "mod-reagent-bank" ]; then - echo '🏦 Installing mod-reagent-bank...' - echo ' πŸ“– Project: https://github.com/ZhengPeiRu21/mod-reagent-bank' - echo ' ℹ️ Reagent banker NPC for storing crafting materials, frees bag space' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/ZhengPeiRu21/mod-reagent-bank.git mod-reagent-bank -fi -if [ "$MODULE_BLACK_MARKET_AUCTION_HOUSE" = "1" ] && [ ! -d "mod-black-market" ]; then - echo 'πŸ΄β€β˜ οΈ Installing mod-black-market...' - echo ' πŸ“– Project: https://github.com/Youpeoples/Black-Market-Auction-House' - echo ' ℹ️ MoP Black Market Auction House backported using Eluna Lua engine' - echo ' ⚠️ SPECIAL MODULE: Uses Lua scripts, not C++ compilation' - echo ' πŸ”§ REQUIRES: mod-ale must be enabled and functional' - git clone https://github.com/Youpeoples/Black-Market-Auction-House.git mod-black-market - - # Special handling: Copy Lua scripts to lua_scripts directory - if [ "$MODULE_ELUNA" = "1" ] && [ -d "mod-black-market/Server Files/lua_scripts" ]; then - echo ' πŸ”§ Integrating Black Market Lua scripts with mod-ale...' - if mkdir -p /azerothcore/lua_scripts 2>/dev/null; then - if cp -r "mod-black-market/Server Files/lua_scripts/." /azerothcore/lua_scripts/ 2>/dev/null; then - echo ' βœ… Black Market Lua scripts copied to /azerothcore/lua_scripts directory' - ls -la /azerothcore/lua_scripts/ | grep -E "\.lua$" || echo " ℹ️ No .lua files found after copy" - else - echo ' ⚠️ WARNING: Failed to copy Lua scripts into /azerothcore/lua_scripts; continuing' - fi - elif [ -n "${MODULES_HOST_DIR:-}" ]; then - host_lua_dir="${MODULES_HOST_DIR%/}/lua_scripts" - if mkdir -p "$host_lua_dir" && cp -r "mod-black-market/Server Files/lua_scripts/." "$host_lua_dir/" 2>/dev/null; then - echo " βœ… Black Market Lua scripts staged to $host_lua_dir" - else - echo " ⚠️ WARNING: Unable to stage Lua scripts to $host_lua_dir; continuing" - fi +copy_black_market_lua(){ + local module_dir="$1" + local source_dir="$module_dir/Server Files/lua_scripts" + if [ ! -d "$source_dir" ]; then + warn "Black Market Lua scripts not found at '$source_dir'" + return + fi + local target="${MODULES_LUA_TARGET_DIR:-}" + if [ -z "$target" ]; then + if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then + target="${MODULES_ROOT}/lua_scripts" else - echo ' ⚠️ WARNING: No writable target for Lua scripts; continuing without copy' + target="/azerothcore/lua_scripts" fi + fi + if mkdir -p "$target" 2>/dev/null && cp -r "$source_dir/." "$target/" 2>/dev/null; then + ok "Black Market Lua scripts copied to $target" + return + fi + if [ -n "${MODULES_HOST_DIR:-}" ]; then + target="${MODULES_HOST_DIR%/}/lua_scripts" + if mkdir -p "$target" 2>/dev/null && cp -r "$source_dir/." "$target/" 2>/dev/null; then + ok "Black Market Lua scripts staged to $target" + return + fi + fi + warn "Unable to copy Black Market Lua scripts to a writable location" +} + +update_playerbots_db_info(){ + local target="$1" + if [ ! -f "$target" ]; then + return 0 + fi + + local host="${CONTAINER_MYSQL:-${MYSQL_HOST:-127.0.0.1}}" + local port="${MYSQL_PORT:-3306}" + local user="${MYSQL_USER:-root}" + local pass="${MYSQL_ROOT_PASSWORD:-acore}" + local db="${DB_PLAYERBOTS_NAME:-acore_playerbots}" + local value="${host};${port};${user};${pass};${db}" + + if grep -qE '^[[:space:]]*PlayerbotsDatabaseInfo[[:space:]]*=' "$target"; then + sed -i "s|^[[:space:]]*PlayerbotsDatabaseInfo[[:space:]]*=.*|PlayerbotsDatabaseInfo = \"${value}\"|" "$target" || return else - echo ' ⚠️ WARNING: mod-ale not enabled - Black Market will not function' + printf '\nPlayerbotsDatabaseInfo = "%s"\n' "$value" >> "$target" || return fi -fi -# Featured catalogue additions -if [ "$MODULE_CHALLENGE_MODES" = "1" ] && [ ! -d "mod-challenge-modes" ]; then - echo '🏁 Installing mod-challenge-modes...' - echo ' πŸ“– Project: https://github.com/ZhengPeiRu21/mod-challenge-modes' - echo ' ℹ️ Adds timed dungeon challenge runs with scaling modifiers' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/ZhengPeiRu21/mod-challenge-modes.git mod-challenge-modes -fi - -if [ "$MODULE_OLLAMA_CHAT" = "1" ] && [ ! -d "mod-ollama-chat" ]; then - echo 'πŸ—£οΈ Installing mod-ollama-chat...' - echo ' πŸ“– Project: https://github.com/DustinHendrickson/mod-ollama-chat' - echo ' ℹ️ Integrates playerbots with external LLM dialogue via the Ollama API' - echo ' ⚠️ Requires MODULE_PLAYERBOTS=1 and OLLAMA service configuration' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/DustinHendrickson/mod-ollama-chat.git mod-ollama-chat - if [ "$MODULE_PLAYERBOTS" != "1" ]; then - echo ' ⚠️ WARNING: Playerbots are disabled; enable MODULE_PLAYERBOTS for mod-ollama-chat to function.' + if [ "$PLAYERBOTS_DB_UPDATE_LOGGED" = "0" ]; then + info "Updated PlayerbotsDatabaseInfo to use host ${host}:${port}" + PLAYERBOTS_DB_UPDATE_LOGGED=1 fi -fi -if [ "$MODULE_PLAYER_BOT_LEVEL_BRACKETS" = "1" ] && [ ! -d "mod-player-bot-level-brackets" ]; then - echo 'πŸ“Š Installing mod-player-bot-level-brackets...' - echo ' πŸ“– Project: https://github.com/DustinHendrickson/mod-player-bot-level-brackets' - echo ' ℹ️ Keeps playerbot populations balanced across configurable level ranges' - echo ' ⚠️ Requires MODULE_PLAYERBOTS=1' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/DustinHendrickson/mod-player-bot-level-brackets.git mod-player-bot-level-brackets - if [ "$MODULE_PLAYERBOTS" != "1" ]; then - echo ' ⚠️ WARNING: Playerbots are disabled; Level Brackets will be inert until MODULE_PLAYERBOTS=1.' + return 0 +} + +manage_configuration_files(){ + echo 'Managing configuration files...' + + local env_target="${MODULES_ENV_TARGET_DIR:-}" + if [ -z "$env_target" ]; then + if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then + env_target="${MODULES_ROOT}/env/dist/etc" + else + env_target="/azerothcore/env/dist/etc" + fi fi -fi -if [ "$MODULE_BG_SLAVERYVALLEY" = "1" ] && [ ! -d "mod-bg-slaveryvalley" ]; then - echo 'βš”οΈ Installing mod-bg-slaveryvalley...' - echo ' πŸ“– Project: https://github.com/Helias/mod-bg-slaveryvalley' - echo ' ℹ️ Introduces the custom Slavery Valley battleground' - echo ' ⚠️ Requires custom DBC/client patch assets for battleground entries' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/Helias/mod-bg-slaveryvalley.git mod-bg-slaveryvalley -fi + mkdir -p "$env_target" -if [ "$MODULE_AZEROTHSHARD" = "1" ] && [ ! -d "mod-azerothshard" ]; then - echo '🧩 Installing mod-azerothshard...' - echo ' πŸ“– Project: https://github.com/azerothcore/mod-azerothshard' - echo ' ℹ️ Bundle of AzerothShard quality-of-life tweaks and scripts' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/azerothcore/mod-azerothshard.git mod-azerothshard -fi + local key patterns_csv enabled pattern + for key in "${MODULE_KEYS[@]}"; do + enabled="${MODULE_ENABLED[$key]:-0}" + patterns_csv="${MODULE_CONFIG_CLEANUP[$key]:-}" + IFS=',' read -r -a patterns <<< "$patterns_csv" + if [ "${#patterns[@]}" -eq 1 ] && [ -z "${patterns[0]}" ]; then + unset patterns + continue + fi + for pattern in "${patterns[@]}"; do + [ -n "$pattern" ] || continue + if [ "$enabled" != "1" ]; then + rm -f "$env_target"/$pattern 2>/dev/null || true + fi + done + unset patterns + done -if [ "$MODULE_WORGOBLIN" = "1" ] && [ ! -d "mod-worgoblin" ]; then - echo '🐺 Installing mod-worgoblin...' - echo ' πŸ“– Project: https://github.com/heyitsbench/mod-worgoblin' - echo ' ℹ️ Enables Worgen and Goblin as playable races' - echo ' ⚠️ Requires Patch-W.MPQ (or equivalent) and DBC/DB updates' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/heyitsbench/mod-worgoblin.git mod-worgoblin -fi + local module_dir + for key in "${MODULE_KEYS[@]}"; do + module_dir="${MODULE_NAME[$key]:-}" + [ -n "$module_dir" ] || continue + [ -d "$module_dir" ] || continue + find "$module_dir" -name "*.conf.dist" -exec cp {} "$env_target"/ \; 2>/dev/null || true + done -if [ "$MODULE_STATBOOSTER" = "1" ] && [ ! -d "StatBooster" ]; then - echo 'πŸ“ˆ Installing StatBooster...' - echo ' πŸ“– Project: https://github.com/AnchyDev/StatBooster' - echo ' ℹ️ Random enchant upgrade system for AzerothCore' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/AnchyDev/StatBooster.git StatBooster -fi + local modules_conf_dir="${env_target%/}/modules" + mkdir -p "$modules_conf_dir" + rm -f "$modules_conf_dir"/*.conf "$modules_conf_dir"/*.conf.dist 2>/dev/null || true + for key in "${MODULE_KEYS[@]}"; do + module_dir="${MODULE_NAME[$key]:-}" + [ -n "$module_dir" ] || continue + [ -d "$module_dir" ] || continue + while IFS= read -r conf_file; do + [ -n "$conf_file" ] || continue + base_name="$(basename "$conf_file")" + dest_name="${base_name%.dist}" + cp "$conf_file" "$modules_conf_dir/$dest_name" + done < <(find "$module_dir" -path "*/conf/*" -type f \( -name "*.conf" -o -name "*.conf.dist" \) 2>/dev/null) + done -if [ "$MODULE_DUNGEON_RESPAWN" = "1" ]; then - echo '⚠️ DungeonRespawn is temporarily disabled (compilation incompatibility). Skipping until patched.' - echo ' πŸ“– Project: https://github.com/AnchyDev/DungeonRespawn' - echo ' ❌ Issue: OnBeforeTeleport function incorrectly marked as override' - echo ' πŸ”§ Apply compilation fix before re-enabling this module.' - MODULE_DUNGEON_RESPAWN=0 -fi - -if [ "$MODULE_DUNGEON_RESPAWN" = "1" ] && [ ! -d "DungeonRespawn" ]; then - echo 'πŸšͺ Installing DungeonRespawn...' - echo ' πŸ“– Project: https://github.com/AnchyDev/DungeonRespawn' - echo ' ℹ️ Teleports players back to the dungeon entrance after death' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/AnchyDev/DungeonRespawn.git DungeonRespawn -fi - -if [ "$MODULE_SKELETON_MODULE" = "1" ] && [ ! -d "skeleton-module" ]; then - echo '🦴 Installing skeleton-module...' - echo ' πŸ“– Project: https://github.com/azerothcore/skeleton-module' - echo ' ℹ️ Blank starter module for rapid prototyping' - echo ' πŸ”§ REBUILD REQUIRED: Container must be rebuilt with source-based compilation' - git clone https://github.com/azerothcore/skeleton-module.git skeleton-module -fi - -if [ "$MODULE_ELUNA_TS" = "1" ] && [ ! -d "eluna-ts" ]; then - echo 'πŸ§ͺ Installing eluna-ts...' - echo ' πŸ“– Project: https://github.com/azerothcore/eluna-ts' - echo ' ℹ️ Provides a TypeScript toolchain that transpiles to Eluna Lua scripts' - echo ' πŸ”§ OPTIONAL: Run npm install && npm run build inside eluna-ts for transpilation features' - git clone https://github.com/azerothcore/eluna-ts.git eluna-ts -fi - -echo 'Managing configuration files...' - -# Remove configuration files for disabled modules -if [ "$MODULE_PLAYERBOTS" != "1" ]; then - rm -f /azerothcore/env/dist/etc/playerbots.conf* -fi - -if [ "$MODULE_AOE_LOOT" != "1" ]; then - rm -f /azerothcore/env/dist/etc/mod_aoe_loot.conf* -fi - -if [ "$MODULE_LEARN_SPELLS" != "1" ]; then - rm -f /azerothcore/env/dist/etc/mod_learnspells.conf* -fi - -if [ "$MODULE_FIREWORKS" != "1" ]; then - rm -f /azerothcore/env/dist/etc/mod_fireworks.conf* -fi - -if [ "$MODULE_INDIVIDUAL_PROGRESSION" != "1" ]; then - rm -f /azerothcore/env/dist/etc/individual_progression.conf* -fi - -if [ "$MODULE_AHBOT" != "1" ]; then - rm -f /azerothcore/env/dist/etc/mod_ahbot.conf* -fi - -if [ "$MODULE_AUTOBALANCE" != "1" ]; then - rm -f /azerothcore/env/dist/etc/AutoBalance.conf* -fi - -if [ "$MODULE_TRANSMOG" != "1" ]; then - rm -f /azerothcore/env/dist/etc/transmog.conf* -fi - -if [ "$MODULE_NPC_BUFFER" != "1" ]; then - rm -f /azerothcore/env/dist/etc/npc_buffer.conf* -fi - -if [ "$MODULE_DYNAMIC_XP" != "1" ]; then - rm -f /azerothcore/env/dist/etc/Individual-XP.conf* -fi - -if [ "$MODULE_SOLO_LFG" != "1" ]; then - rm -f /azerothcore/env/dist/etc/SoloLfg.conf* -fi - -if [ "$MODULE_1V1_ARENA" != "1" ]; then - rm -f /azerothcore/env/dist/etc/1v1arena.conf* -fi - -if [ "$MODULE_PHASED_DUELS" != "1" ]; then - rm -f /azerothcore/env/dist/etc/phasedduels.conf* -fi - -if [ "$MODULE_BREAKING_NEWS" != "1" ]; then - rm -f /azerothcore/env/dist/etc/breaking_news.conf* -fi - -if [ "$MODULE_BOSS_ANNOUNCER" != "1" ]; then - rm -f /azerothcore/env/dist/etc/boss_announcer.conf* -fi - -if [ "$MODULE_ACCOUNT_ACHIEVEMENTS" != "1" ]; then - rm -f /azerothcore/env/dist/etc/account_achievements.conf* -fi - -if [ "$MODULE_AUTO_REVIVE" != "1" ]; then - rm -f /azerothcore/env/dist/etc/AutoRevive.conf* -fi - -if [ "$MODULE_GAIN_HONOR_GUARD" != "1" ]; then - rm -f /azerothcore/env/dist/etc/GainHonorGuard.conf* -fi - -if [ "$MODULE_ELUNA" != "1" ]; then - rm -f /azerothcore/env/dist/etc/mod_eluna.conf* -fi -if [ "$MODULE_ARAC" != "1" ]; then - rm -f /azerothcore/env/dist/etc/arac.conf* -fi - -if [ "$MODULE_TIME_IS_TIME" != "1" ]; then - rm -f /azerothcore/env/dist/etc/mod-time_is_time.conf* -fi - -if [ "$MODULE_POCKET_PORTAL" != "1" ]; then - rm -f /azerothcore/env/dist/etc/pocketportal.conf* -fi - -if [ "$MODULE_RANDOM_ENCHANTS" != "1" ]; then - rm -f /azerothcore/env/dist/etc/RandomEnchants.conf* -fi - -if [ "$MODULE_SOLOCRAFT" != "1" ]; then - rm -f /azerothcore/env/dist/etc/Solocraft.conf* -fi - -if [ "$MODULE_PVP_TITLES" != "1" ]; then - rm -f /azerothcore/env/dist/etc/mod_pvptitles.conf* -fi - -if [ "$MODULE_NPC_BEASTMASTER" != "1" ]; then - rm -f /azerothcore/env/dist/etc/npc_beastmaster.conf* -fi - -if [ "$MODULE_NPC_ENCHANTER" != "1" ]; then - rm -f /azerothcore/env/dist/etc/npc_enchanter.conf* -fi - -if [ "$MODULE_INSTANCE_RESET" != "1" ]; then - rm -f /azerothcore/env/dist/etc/instance-reset.conf* -fi - -if [ "$MODULE_LEVEL_GRANT" != "1" ]; then - rm -f /azerothcore/env/dist/etc/levelGrant.conf* -fi - -# Install configuration files for enabled modules -for module_dir in mod-*; do - if [ -d "$module_dir" ]; then - echo "Installing config files for $module_dir..." - find "$module_dir" -name "*.conf.dist" -exec cp {} /azerothcore/env/dist/etc/ \; 2>/dev/null || true + local playerbots_enabled="${MODULE_PLAYERBOTS:-0}" + if [ "${MODULE_ENABLED[MODULE_PLAYERBOTS]:-0}" = "1" ]; then + playerbots_enabled=1 fi -done -# Populate module-specific configuration directory (/etc/modules) -MODULES_CONF_DIR="/azerothcore/env/dist/etc/modules" -mkdir -p "$MODULES_CONF_DIR" -rm -f "$MODULES_CONF_DIR"/*.conf "$MODULES_CONF_DIR"/*.conf.dist 2>/dev/null || true -for module_dir in mod-*; do - [ -d "$module_dir" ] || continue - while IFS= read -r conf_file; do - [ -n "$conf_file" ] || continue - base_name="$(basename "$conf_file")" - dest_name="${base_name%.dist}" - cp "$conf_file" "$MODULES_CONF_DIR/$dest_name" - done < <(find "$module_dir" -path "*/conf/*" -type f \( -name "*.conf" -o -name "*.conf.dist" \) 2>/dev/null) -done + if [ "$playerbots_enabled" = "1" ]; then + update_playerbots_db_info "$env_target/playerbots.conf" + update_playerbots_db_info "$env_target/playerbots.conf.dist" + update_playerbots_db_info "$modules_conf_dir/playerbots.conf" + update_playerbots_db_info "$modules_conf_dir/playerbots.conf.dist" + fi -if [ "$MODULE_AUTOBALANCE" = "1" ]; then - if [ -f "/azerothcore/env/dist/etc/AutoBalance.conf.dist" ]; then + if [ "${MODULE_AUTOBALANCE:-0}" = "1" ] && [ -f "$env_target/AutoBalance.conf.dist" ]; then sed -i 's/^AutoBalance\.LevelScaling\.EndGameBoost.*/AutoBalance.LevelScaling.EndGameBoost = false # disabled pending proper implementation/' \ - /azerothcore/env/dist/etc/AutoBalance.conf.dist || true + "$env_target/AutoBalance.conf.dist" || true fi -fi +} -# Load SQL runner if present -if [ -f "/scripts/manage-modules-sql.sh" ]; then - . /scripts/manage-modules-sql.sh -elif [ -f "/tmp/scripts/manage-modules-sql.sh" ]; then - . /tmp/scripts/manage-modules-sql.sh -else - echo "⚠️ SQL helper not found, skipping module SQL execution" - echo "If you are seeing this during build this is normal" +load_sql_helper(){ + local helper_paths=( + "/scripts/manage-modules-sql.sh" + "/tmp/scripts/manage-modules-sql.sh" + ) -fi + if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then + helper_paths+=("$SCRIPT_DIR/manage-modules-sql.sh") + fi -# Execute SQLs for enabled modules (via helper) -SQL_EXECUTION_FAILED=0 -if declare -f execute_module_sql_scripts >/dev/null 2>&1; then - echo 'Executing module SQL scripts...' - if execute_module_sql_scripts; then - echo 'SQL execution complete.' + local helper_path="" + for helper_path in "${helper_paths[@]}"; do + if [ -f "$helper_path" ]; then + # shellcheck disable=SC1090 + . "$helper_path" + SQL_HELPER_PATH="$helper_path" + return 0 + fi + done + + err "SQL helper not found; expected manage-modules-sql.sh to be available" +} + +execute_module_sql(){ + SQL_EXECUTION_FAILED=0 + if declare -f execute_module_sql_scripts >/dev/null 2>&1; then + echo 'Executing module SQL scripts...' + if execute_module_sql_scripts; then + echo 'SQL execution complete.' + else + echo '⚠️ Module SQL scripts reported errors' + SQL_EXECUTION_FAILED=1 + fi else - echo '⚠️ Module SQL scripts reported errors' - SQL_EXECUTION_FAILED=1 + info "SQL helper did not expose execute_module_sql_scripts; skipping module SQL execution" fi -fi +} -# Module state tracking and rebuild logic -echo 'Checking for module changes that require rebuild...' +track_module_state(){ + echo 'Checking for module changes that require rebuild...' -if [ "$MODULES_LOCAL_RUN" = "1" ]; then - MODULES_STATE_FILE="./.modules_state" -else - MODULES_STATE_FILE="/modules/.modules_state" -fi -CURRENT_STATE="" -REBUILD_REQUIRED=0 + local modules_state_file + if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then + modules_state_file="./.modules_state" + else + modules_state_file="/modules/.modules_state" + fi -# Create current module state hash -for module_var in MODULE_PLAYERBOTS 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; do - eval "value=\$$module_var" - value="${value:-0}" - CURRENT_STATE="$CURRENT_STATE$module_var=$value|" -done + local current_state="" + for key in "${MODULE_KEYS[@]}"; do + current_state+="${key}=${MODULE_ENABLED[$key]:-0}|" + done -# Check if state has changed -if [ -f "$MODULES_STATE_FILE" ]; then - PREVIOUS_STATE=$(cat "$MODULES_STATE_FILE") - if [ "$CURRENT_STATE" != "$PREVIOUS_STATE" ]; then - echo "πŸ”„ Module configuration has changed - rebuild required" - REBUILD_REQUIRED=1 + local previous_state="" + if [ -f "$modules_state_file" ]; then + previous_state="$(cat "$modules_state_file")" + fi + + local rebuild_required=0 + if [ "$current_state" != "$previous_state" ]; then + if [ -n "$previous_state" ]; then + echo "πŸ”„ Module configuration has changed - rebuild required" + else + echo "πŸ“ First run - establishing module state baseline" + fi + rebuild_required=1 else echo "βœ… No module changes detected" fi -else - echo "πŸ“ First run - establishing module state baseline" - REBUILD_REQUIRED=1 -fi -# Save current state -echo "$CURRENT_STATE" > "$MODULES_STATE_FILE" + echo "$current_state" > "$modules_state_file" -# Check if any C++ modules are enabled (modules requiring source compilation) -# NOTE: mod-playerbots uses pre-built images and doesn't require rebuild -ENABLED_MODULES="" -[ "$MODULE_AOE_LOOT" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-aoe-loot" -[ "$MODULE_LEARN_SPELLS" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-learn-spells" -[ "$MODULE_FIREWORKS" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-fireworks-on-level" -[ "$MODULE_INDIVIDUAL_PROGRESSION" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-individual-progression" -[ "$MODULE_AHBOT" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-ahbot" -[ "$MODULE_AUTOBALANCE" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-autobalance" -[ "$MODULE_TRANSMOG" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-transmog" -[ "$MODULE_NPC_BUFFER" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-npc-buffer" -[ "$MODULE_DYNAMIC_XP" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-dynamic-xp" -[ "$MODULE_SOLO_LFG" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-solo-lfg" -[ "$MODULE_1V1_ARENA" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-1v1-arena" -[ "$MODULE_PHASED_DUELS" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-phased-duels" -[ "$MODULE_BREAKING_NEWS" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-breaking-news-override" -[ "$MODULE_BOSS_ANNOUNCER" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-boss-announcer" -[ "$MODULE_ACCOUNT_ACHIEVEMENTS" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-account-achievements" -[ "$MODULE_AUTO_REVIVE" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-auto-revive" -[ "$MODULE_GAIN_HONOR_GUARD" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-gain-honor-guard" -[ "$MODULE_ELUNA" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-ale" -[ "$MODULE_TIME_IS_TIME" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-time-is-time" -[ "$MODULE_RANDOM_ENCHANTS" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-random-enchants" -[ "$MODULE_SOLOCRAFT" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-solocraft" -[ "$MODULE_PVP_TITLES" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-pvp-titles" -[ "$MODULE_NPC_BEASTMASTER" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-npc-beastmaster" -[ "$MODULE_NPC_ENCHANTER" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-npc-enchanter" -[ "$MODULE_INSTANCE_RESET" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-instance-reset" -[ "$MODULE_LEVEL_GRANT" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-quest-count-level" -[ "$MODULE_ARAC" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-arac" -[ "$MODULE_ASSISTANT" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-assistant" -[ "$MODULE_REAGENT_BANK" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-reagent-bank" -[ "$MODULE_CHALLENGE_MODES" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-challenge-modes" -[ "$MODULE_OLLAMA_CHAT" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-ollama-chat" -[ "$MODULE_PLAYER_BOT_LEVEL_BRACKETS" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-player-bot-level-brackets" -[ "$MODULE_STATBOOSTER" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES StatBooster" -[ "$MODULE_DUNGEON_RESPAWN" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES DungeonRespawn" -[ "$MODULE_SKELETON_MODULE" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES skeleton-module" -[ "$MODULE_BG_SLAVERYVALLEY" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-bg-slaveryvalley" -[ "$MODULE_AZEROTHSHARD" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-azerothshard" -[ "$MODULE_WORGOBLIN" = "1" ] && ENABLED_MODULES="$ENABLED_MODULES mod-worgoblin" + if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then + echo "πŸ”§ Detected ${#MODULES_COMPILE_LIST[@]} enabled C++ modules requiring compilation:" + for mod in "${MODULES_COMPILE_LIST[@]}"; do + echo " β€’ $mod" + done + else + echo "βœ… No C++ modules enabled - pre-built containers can be used" + fi -if [ -n "$ENABLED_MODULES" ]; then - ENABLED_COUNT=$(echo $ENABLED_MODULES | wc -w) - echo "πŸ”§ Detected $ENABLED_COUNT enabled C++ modules requiring compilation:" - for mod in $ENABLED_MODULES; do - echo " β€’ $mod" - done - - if [ "$REBUILD_REQUIRED" = "1" ]; then - if [ "$RUN_REBUILD_NOW" = "0" ]; then - echo "" - echo "🚨 REBUILD REQUIRED 🚨" - echo "Module configuration has changed. To integrate C++ modules into AzerothCore:" - echo "" - echo "1. Stop current services:" - echo " docker compose down" - echo "" - echo "2. Build with source-based compilation (external process)" - echo " ./scripts/rebuild-with-modules.sh (if available)" - echo "" - echo "πŸ“‹ NOTE: Source-based build will compile AzerothCore with all enabled modules" - echo "⏱️ Expected build time: 15-45 minutes depending on system performance" - echo "" + local rebuild_sentinel + if [ "${MODULES_LOCAL_RUN:-0}" = "1" ]; then + if [ -n "${LOCAL_STORAGE_SENTINEL_PATH:-}" ]; then + rebuild_sentinel="${LOCAL_STORAGE_SENTINEL_PATH}" else - echo "βœ… Ready to compile modules" + rebuild_sentinel="./.requires_rebuild" + fi + else + rebuild_sentinel="/modules/.requires_rebuild" + fi + + local host_rebuild_sentinel="" + if [ -n "${MODULES_HOST_DIR:-}" ]; then + host_rebuild_sentinel="${MODULES_HOST_DIR%/}/.requires_rebuild" + fi + + if [ "$rebuild_required" = "1" ] && [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then + printf '%s\n' "${MODULES_COMPILE_LIST[@]}" > "$rebuild_sentinel" + if [ -n "$host_rebuild_sentinel" ]; then + printf '%s\n' "${MODULES_COMPILE_LIST[@]}" > "$host_rebuild_sentinel" 2>/dev/null || true + fi + echo "🚨 Module changes detected; run ./scripts/rebuild-with-modules.sh to rebuild source images." + else + rm -f "$rebuild_sentinel" 2>/dev/null || true + if [ -n "$host_rebuild_sentinel" ]; then + rm -f "$host_rebuild_sentinel" 2>/dev/null || true fi fi -else - echo "βœ… No C++ modules enabled - pre-built containers can be used" -fi +} -echo 'Module management complete.' +main(){ + ensure_python -if [ "$MODULES_LOCAL_RUN" = "1" ]; then - # When running locally, use local-storage for build state tracking - local_storage_path="${LOCAL_STORAGE_SENTINEL_PATH:-}" - if [ -n "$local_storage_path" ]; then - REBUILD_SENTINEL="$local_storage_path" + if [ "${MODULES_LOCAL_RUN:-0}" != "1" ]; then + cd /modules || err "Modules directory /modules not found" + fi + MODULES_ROOT="$(pwd)" + + MANIFEST_PATH="$(resolve_manifest_path)" + STATE_DIR="${MODULES_HOST_DIR:-$MODULES_ROOT}" + + setup_git_config + generate_module_state + remove_disabled_modules + install_enabled_modules + manage_configuration_files + info "SQL execution gate: MODULES_SKIP_SQL=${MODULES_SKIP_SQL:-0}" + if [ "${MODULES_SKIP_SQL:-0}" = "1" ]; then + info "Skipping module SQL execution (MODULES_SKIP_SQL=1)" else - # Fallback to current directory if no path provided (legacy behavior) - REBUILD_SENTINEL="./.requires_rebuild" + info "Initiating module SQL helper" + load_sql_helper + info "SQL helper loaded from ${SQL_HELPER_PATH:-unknown}" + execute_module_sql fi -else - REBUILD_SENTINEL="/modules/.requires_rebuild" -fi -HOST_REBUILD_SENTINEL="${MODULES_HOST_DIR:-}" -if [ -n "$HOST_REBUILD_SENTINEL" ]; then - HOST_REBUILD_SENTINEL="${HOST_REBUILD_SENTINEL%/}/.requires_rebuild" -fi -if [ "$SQL_EXECUTION_FAILED" = "1" ]; then - echo "⚠️ SQL execution encountered issues; review logs above." -fi + track_module_state -if [ "$REBUILD_REQUIRED" = "1" ] && [ -n "$ENABLED_MODULES" ]; then - echo "$ENABLED_MODULES" > "$REBUILD_SENTINEL" - if [ -n "$HOST_REBUILD_SENTINEL" ]; then - echo "$ENABLED_MODULES" > "$HOST_REBUILD_SENTINEL" 2>/dev/null || true + if [ "${SQL_EXECUTION_FAILED:-0}" = "1" ]; then + warn "Module SQL execution reported issues; review logs above." fi -else - rm -f "$REBUILD_SENTINEL" 2>/dev/null || true - if [ -n "$HOST_REBUILD_SENTINEL" ]; then - rm -f "$HOST_REBUILD_SENTINEL" 2>/dev/null || true - fi -fi -# Optional: keep container alive for inspection in CI/debug contexts -if [ "${MODULES_DEBUG_KEEPALIVE:-0}" = "1" ]; then - tail -f /dev/null -fi + echo 'Module management complete.' + + if [ "${MODULES_DEBUG_KEEPALIVE:-0}" = "1" ]; then + tail -f /dev/null + fi +} + +main "$@" diff --git a/scripts/modules.py b/scripts/modules.py new file mode 100755 index 0000000..07518f9 --- /dev/null +++ b/scripts/modules.py @@ -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()) diff --git a/scripts/rebuild-with-modules.sh b/scripts/rebuild-with-modules.sh index 849baa7..d46026d 100755 --- a/scripts/rebuild-with-modules.sh +++ b/scripts/rebuild-with-modules.sh @@ -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 diff --git a/scripts/sql/custom/characters/2025_10_XX_fix_playerbot_arena.sql b/scripts/sql/custom/characters/2025_10_XX_fix_playerbot_arena.sql index 27d7e85..a218744 100644 --- a/scripts/sql/custom/characters/2025_10_XX_fix_playerbot_arena.sql +++ b/scripts/sql/custom/characters/2025_10_XX_fix_playerbot_arena.sql @@ -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 ( diff --git a/scripts/stage-modules.sh b/scripts/stage-modules.sh index 636d787..79cbac5 100755 --- a/scripts/stage-modules.sh +++ b/scripts/stage-modules.sh @@ -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) diff --git a/start-containers.sh b/start-containers.sh index 6d38c79..95599b9 100755 --- a/start-containers.sh +++ b/start-containers.sh @@ -1,10 +1,39 @@ #!/bin/bash # Thin wrapper to bring the AzerothCore stack online without triggering rebuilds. -# Reuses deploy.sh so all profile detection and tagging logic stay consistent. +# Picks the right profile automatically (standard/playerbots/modules) and delegates +# to deploy.sh so all staging/health logic stays consistent. set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -exec "${SCRIPT_DIR}/deploy.sh" --skip-rebuild --yes --no-watch +PROFILE="$(python3 - <<'PY' "$ROOT_DIR" +import json, subprocess, sys +from pathlib import Path + +root = Path(sys.argv[1]) +modules_py = root / "scripts" / "modules.py" +env_path = root / ".env" +manifest_path = root / "config" / "modules.json" + +state = json.loads(subprocess.check_output([ + sys.executable, + str(modules_py), + "--env-path", str(env_path), + "--manifest", str(manifest_path), + "dump", "--format", "json", +])) + +enabled = [m for m in state["modules"] if m["enabled"]] +profile = "standard" +if any(m["key"] == "MODULE_PLAYERBOTS" and m["enabled"] for m in enabled): + profile = "playerbots" +elif any(m["needs_build"] and m["enabled"] for m in enabled): + profile = "modules" + +print(profile) +PY +)" + +exec "${ROOT_DIR}/deploy.sh" --profile "$PROFILE" --yes --no-watch