adding automatic module inclusion features and bugfixes

This commit is contained in:
uprightbass360
2025-11-01 00:13:34 -04:00
parent 718af64dae
commit d59c785f11
16 changed files with 3064 additions and 1132 deletions

3
.gitignore vendored
View File

@@ -12,7 +12,10 @@ local-storage/
images/
node_modules/
.mcp*/
scripts/__pycache__/
.env
package-lock.json
package.json
.modules_state
.modules-meta
todo.md

View File

@@ -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.

126
build.sh
View File

@@ -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
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
if [ "$MODULE_STATE_INITIALIZED" -ne 1 ]; then
generate_module_state
fi
local any_cxx_modules=0
if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then
any_cxx_modules=1
fi
done
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)"

497
config/modules.json Normal file
View File

@@ -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"
]
}
]
}

View File

@@ -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
if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then
any_cxx_modules=1
break
fi
done
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
ensure_module_state
if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then
echo "modules"
return
fi
done
echo "standard"
}

View File

@@ -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

1085
modules-state.json Normal file

File diff suppressed because it is too large Load Diff

51
modules.env Normal file
View File

@@ -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

76
scripts/check_module_staging.py Executable file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
from pathlib import Path
def load_module_state(root: Path) -> dict:
env_path = root / ".env"
manifest_path = root / "config" / "modules.json"
modules_py = root / "scripts" / "modules.py"
try:
output = subprocess.check_output(
[
sys.executable,
str(modules_py),
"--env-path",
str(env_path),
"--manifest",
str(manifest_path),
"dump",
"--format",
"json",
],
text=True,
)
except subprocess.CalledProcessError as exc:
print("Unable to load module state:", exc, file=sys.stderr)
sys.exit(2)
return json.loads(output)
def main() -> int:
root = Path(__file__).resolve().parents[1]
data = load_module_state(root)
enabled_modules = [m for m in data["modules"] if m["enabled"]]
storage_dir = root / "storage" / "modules"
local_root = Path(os.environ.get("STORAGE_PATH_LOCAL", "./local-storage"))
local_root = (root / local_root).resolve()
requires_playerbots = any(m["key"] == "MODULE_PLAYERBOTS" and m["enabled"] for m in enabled_modules)
source_dir = local_root / "source"
source_dir = source_dir / ("azerothcore-playerbots" if requires_playerbots else "azerothcore") / "modules"
print(f"📦 Checking module staging in {storage_dir} and {source_dir}")
print("Enabled modules:", ", ".join(m["name"] for m in enabled_modules))
status = 0
for module in enabled_modules:
dir_name = module["name"]
storage_path = storage_dir / dir_name
source_path = source_dir / dir_name
def state(path: Path) -> str:
if (path / ".git").is_dir():
return "git"
if path.is_dir():
return "present"
return "missing"
storage_state = state(storage_path)
source_state = state(source_path)
print(f" - {dir_name} ({module['key']}): storage={storage_state}, source={source_state}")
if storage_state == "missing" or source_state == "missing":
status = 1
return status
if __name__ == "__main__":
sys.exit(main())

View File

@@ -6,6 +6,75 @@ trap 'echo " ❌ SQL helper error (line ${LINENO}): ${BASH_COMMAND}" >&2' ERR
CUSTOM_SQL_ROOT="/tmp/scripts/sql/custom"
ALT_CUSTOM_SQL_ROOT="/scripts/sql/custom"
SQL_SUCCESS_LOG=()
SQL_FAILURE_LOG=()
TEMP_SQL_FILES=()
render_sql_file_for_execution(){
local src="$1"
local pb_db="${DB_PLAYERBOTS_NAME:-acore_playerbots}"
local rendered="$src"
if command -v python3 >/dev/null 2>&1; then
local temp
temp="$(mktemp)"
local result
result="$(python3 - "$src" "$temp" "$pb_db" <<'PY'
import sys, pathlib, re
src, dest, pb_db = sys.argv[1:]
text = pathlib.Path(src).read_text()
original = text
text = text.replace("{{PLAYERBOTS_DB}}", pb_db)
pattern = re.compile(r'(?<![.`])\bplayerbots\b')
text = pattern.sub(f'`{pb_db}`.playerbots', text)
pathlib.Path(dest).write_text(text)
print("changed" if text != original else "unchanged", end="")
PY
)"
if [ "$result" = "changed" ]; then
rendered="$temp"
TEMP_SQL_FILES+=("$temp")
else
rm -f "$temp"
fi
fi
echo "$rendered"
}
log_sql_success(){
local target_db="$1"
local sql_file="$2"
SQL_SUCCESS_LOG+=("${target_db}::${sql_file}")
}
log_sql_failure(){
local target_db="$1"
local sql_file="$2"
SQL_FAILURE_LOG+=("${target_db}::${sql_file}")
}
mysql_exec(){
local mysql_port="${MYSQL_PORT:-3306}"
if command -v mariadb >/dev/null 2>&1; then
mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P "$mysql_port" -u root -p"${MYSQL_ROOT_PASSWORD}" "$@"
return
fi
if command -v mysql >/dev/null 2>&1; then
mysql --ssl-mode=DISABLED -h "${CONTAINER_MYSQL}" -P "$mysql_port" -u root -p"${MYSQL_ROOT_PASSWORD}" "$@"
return
fi
echo " ❌ Neither mariadb nor mysql client is available for SQL execution" >&2
return 127
}
playerbots_table_exists(){
local pb_db="${DB_PLAYERBOTS_NAME:-acore_playerbots}"
local count
count="$(mysql_exec -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${pb_db}' AND table_name='playerbots';" 2>/dev/null || echo 0)"
[ "${count}" != "0" ]
}
run_custom_sql_group(){
local subdir="$1" target_db="$2" label="$3"
local dir="${CUSTOM_SQL_ROOT}/${subdir}"
@@ -13,25 +82,76 @@ run_custom_sql_group(){
dir="${ALT_CUSTOM_SQL_ROOT}/${subdir}"
fi
[ -d "$dir" ] || return 0
LC_ALL=C find "$dir" -type f -name "*.sql" | sort | while read -r sql_file; do
while IFS= read -r sql_file; do
local base_name
base_name="$(basename "$sql_file")"
local rendered
rendered="$(render_sql_file_for_execution "$sql_file")"
if grep -q '\bplayerbots\b' "$rendered"; then
if ! playerbots_table_exists; then
echo " Skipping ${label}: ${base_name} (playerbots table missing)"
continue
fi
fi
echo " Executing ${label}: ${base_name}"
if mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" "${target_db}" < "$sql_file" >/dev/null 2>&1; then
local sql_output
sql_output="$(mktemp)"
if mysql_exec "${target_db}" < "$rendered" >"$sql_output" 2>&1; then
echo " ✅ Successfully executed ${base_name}"
log_sql_success "$target_db" "$sql_file"
else
echo " ❌ Failed to execute $sql_file"
sed 's/^/ /' "$sql_output"
log_sql_failure "$target_db" "$sql_file"
fi
done || true
rm -f "$sql_output"
done < <(LC_ALL=C find "$dir" -type f -name "*.sql" | sort) || true
}
# Function to execute SQL files for a module
execute_module_sql() {
local module_dir="$1"
local module_name="$2"
module_sql_run_module(){
local module_key="$1"
local module_dir="$2"
local module_name="${MODULE_NAME[$module_key]:-}"
if [ -z "$module_name" ]; then
module_name="$module_dir"
fi
local world_db="${DB_WORLD_NAME:-acore_world}"
local auth_db="${DB_AUTH_NAME:-acore_auth}"
local characters_db="${DB_CHARACTERS_NAME:-acore_characters}"
local playerbots_db="${DB_PLAYERBOTS_NAME:-acore_playerbots}"
local character_set="${MYSQL_CHARACTER_SET:-utf8mb4}"
local collation="${MYSQL_COLLATION:-utf8mb4_unicode_ci}"
execute_sql_file_in_db(){
local target_db="$1"
local sql_file="$2"
local label="$3"
local rendered
rendered="$(render_sql_file_for_execution "$sql_file")"
if grep -q '\bplayerbots\b' "$rendered"; then
if ! playerbots_table_exists; then
echo " Skipping ${label}: ${base_name} (playerbots table missing)"
return 0
fi
fi
local base_name
base_name="$(basename "$sql_file")"
echo " Executing ${label}: ${base_name}"
local sql_output
sql_output="$(mktemp)"
if mysql_exec "${target_db}" < "$rendered" >"$sql_output" 2>&1; then
echo " ✅ Successfully executed ${base_name}"
log_sql_success "$target_db" "$sql_file"
else
echo " ❌ Failed to execute $sql_file"
sed 's/^/ /' "$sql_output"
log_sql_failure "$target_db" "$sql_file"
fi
rm -f "$sql_output"
}
local run_sorted_sql
run_sorted_sql() {
@@ -40,27 +160,22 @@ execute_module_sql() {
local label="$3"
local skip_regex="${4:-}"
[ -d "$dir" ] || return
LC_ALL=C find "$dir" -type f -name "*.sql" | sort | while read -r sql_file; do
while IFS= read -r sql_file; do
local base_name
base_name="$(basename "$sql_file")"
if [ -n "$skip_regex" ] && [[ "$base_name" =~ $skip_regex ]]; then
echo " Skipping ${label}: ${base_name}"
continue
fi
echo " Executing ${label}: ${base_name}"
if mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" "${target_db}" < "$sql_file" >/dev/null 2>&1; then
echo " ✅ Successfully executed ${base_name}"
else
echo " ❌ Failed to execute $sql_file"
fi
done || true
execute_sql_file_in_db "$target_db" "$sql_file" "$label"
done < <(LC_ALL=C find "$dir" -type f -name "*.sql" | sort) || true
}
echo "Processing SQL scripts for $module_name..."
if [ "$module_name" = "Playerbots" ]; then
if [ "$module_key" = "MODULE_PLAYERBOTS" ]; then
echo " Ensuring database ${playerbots_db} exists..."
if mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" -e "CREATE DATABASE IF NOT EXISTS \`${playerbots_db}\` CHARACTER SET ${character_set} COLLATE ${collation};" >/dev/null 2>&1; then
if mysql_exec -e "CREATE DATABASE IF NOT EXISTS \`${playerbots_db}\` CHARACTER SET ${character_set} COLLATE ${collation};" >/dev/null 2>&1; then
echo " ✅ Playerbots database ready"
else
echo " ❌ Failed to ensure playerbots database"
@@ -71,45 +186,30 @@ execute_module_sql() {
if [ -d "$module_dir/data/sql" ]; then
# Execute world database scripts
if [ -d "$module_dir/data/sql/world" ]; then
find "$module_dir/data/sql/world" -name "*.sql" -type f | while read sql_file; do
echo " Executing world SQL: $(basename "$sql_file")"
if mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" "${DB_WORLD_NAME}" < "$sql_file" >/dev/null 2>&1; then
echo " ✅ Successfully executed $(basename "$sql_file")"
else
echo " ❌ Failed to execute $sql_file"
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
done
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"
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
done
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"
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
done
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"
fi
module_sql_run_module "$key" "$module_dir"
done
run_custom_sql_group world "${DB_WORLD_NAME}" "custom world SQL"
run_custom_sql_group auth "${DB_AUTH_NAME}" "custom auth SQL"
run_custom_sql_group characters "${DB_CHARACTERS_NAME}" "custom characters SQL"
run_custom_sql_group world "${world_db}" "custom world SQL"
run_custom_sql_group auth "${auth_db}" "custom auth SQL"
run_custom_sql_group characters "${characters_db}" "custom characters SQL"
echo "SQL execution summary:"
if [ ${#SQL_SUCCESS_LOG[@]} -gt 0 ]; then
echo " ✅ Applied:"
for entry in "${SQL_SUCCESS_LOG[@]}"; do
IFS='::' read -r db file <<< "$entry"
echo " • [$db] $file"
done
else
echo " ✅ Applied: none"
fi
if [ ${#SQL_FAILURE_LOG[@]} -gt 0 ]; then
echo " ❌ Failed:"
for entry in "${SQL_FAILURE_LOG[@]}"; do
IFS='::' read -r db file <<< "$entry"
echo " • [$db] $file"
done
else
echo " ❌ Failed: none"
fi
if [ ${#TEMP_SQL_FILES[@]} -gt 0 ]; then
rm -f "${TEMP_SQL_FILES[@]}" 2>/dev/null || true
TEMP_SQL_FILES=()
fi
return 0
}

File diff suppressed because it is too large Load Diff

519
scripts/modules.py Executable file
View File

@@ -0,0 +1,519 @@
#!/usr/bin/env python3
"""
Module manifest helper.
Reads config/modules.json and .env to produce canonical module state that
downstream shell scripts can consume for staging, rebuild detection, and
dependency validation.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import textwrap
from dataclasses import dataclass, asdict, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Tuple
import shlex
STRICT_TRUE = {"1", "true", "yes", "on"}
def parse_bool(value: str) -> bool:
if value is None:
return False
return str(value).strip().lower() in STRICT_TRUE
def load_env_file(env_path: Path) -> Dict[str, str]:
if not env_path.exists():
return {}
env: Dict[str, str] = {}
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("export "):
line = line[len("export ") :].strip()
if "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
if value.startswith('"') and value.endswith('"'):
value = value[1:-1]
elif value.startswith("'") and value.endswith("'"):
value = value[1:-1]
env[key] = value
return env
def load_manifest(manifest_path: Path) -> List[Dict[str, object]]:
if not manifest_path.exists():
raise FileNotFoundError(f"Manifest file not found: {manifest_path}")
with manifest_path.open("r", encoding="utf-8") as fh:
manifest = json.load(fh)
modules = manifest.get("modules")
if not isinstance(modules, list):
raise ValueError("Manifest must define a top-level 'modules' array")
validated: List[Dict[str, object]] = []
seen_keys: set[str] = set()
for entry in modules:
if not isinstance(entry, dict):
raise ValueError("Each manifest entry must be an object")
key = entry.get("key")
name = entry.get("name")
repo = entry.get("repo")
if not key or not isinstance(key, str):
raise ValueError("Manifest entry missing 'key'")
if key in seen_keys:
raise ValueError(f"Duplicate manifest key detected: {key}")
seen_keys.add(key)
if not name or not isinstance(name, str):
raise ValueError(f"Manifest entry {key} missing 'name'")
if not repo or not isinstance(repo, str):
raise ValueError(f"Manifest entry {key} missing 'repo'")
validated.append(entry)
return validated
@dataclass
class ModuleState:
key: str
name: str
repo: str
needs_build: bool
module_type: str
requires: List[str] = field(default_factory=list)
ref: Optional[str] = None
status: str = "active"
block_reason: Optional[str] = None
post_install_hooks: List[str] = field(default_factory=list)
config_cleanup: List[str] = field(default_factory=list)
sql: Optional[object] = None
notes: Optional[str] = None
enabled_raw: bool = False
enabled_effective: bool = False
value: str = "0"
dependency_issues: List[str] = field(default_factory=list)
warnings: List[str] = field(default_factory=list)
errors: List[str] = field(default_factory=list)
@property
def blocked(self) -> bool:
return self.status.lower() == "blocked"
@dataclass
class ModuleCollectionState:
manifest_path: Path
env_path: Path
modules: List[ModuleState]
generated_at: datetime
warnings: List[str]
errors: List[str]
def enabled_modules(self) -> List[ModuleState]:
return [module for module in self.modules if module.enabled_effective]
def compile_modules(self) -> List[ModuleState]:
return [
module
for module in self.modules
if module.enabled_effective and module.needs_build
]
def requires_playerbot_source(self) -> bool:
module_map = {m.key: m for m in self.modules}
playerbots_enabled = module_map.get("MODULE_PLAYERBOTS")
playerbots = bool(playerbots_enabled and playerbots_enabled.enabled_effective)
needs_cpp = any(module.needs_build and module.enabled_effective for module in self.modules)
return playerbots or needs_cpp
def build_state(env_path: Path, manifest_path: Path) -> ModuleCollectionState:
env_map = load_env_file(env_path)
manifest_entries = load_manifest(manifest_path)
modules: List[ModuleState] = []
errors: List[str] = []
warnings: List[str] = []
# Track which manifest keys appear in .env for coverage validation
env_keys_in_manifest: set[str] = set()
for entry in manifest_entries:
key = entry["key"]
name = entry["name"]
repo = entry["repo"]
needs_build = bool(entry.get("needs_build", False))
module_type = str(entry.get("type", "cpp"))
requires = entry.get("requires") or []
if not isinstance(requires, list):
raise ValueError(f"Manifest entry {key} has non-list 'requires'")
requires = [str(dep) for dep in requires]
depends_on = entry.get("depends_on") or []
if not isinstance(depends_on, list):
raise ValueError(f"Manifest entry {key} has non-list 'depends_on'")
depends_on = [str(dep) for dep in depends_on]
if depends_on:
requires = list(dict.fromkeys(requires + depends_on))
status = entry.get("status", "active")
block_reason = entry.get("block_reason")
post_install_hooks = entry.get("post_install_hooks") or []
if not isinstance(post_install_hooks, list):
raise ValueError(f"Manifest entry {key} has non-list 'post_install_hooks'")
post_install_hooks = [str(hook) for hook in post_install_hooks]
config_cleanup = entry.get("config_cleanup") or []
if not isinstance(config_cleanup, list):
raise ValueError(f"Manifest entry {key} has non-list 'config_cleanup'")
config_cleanup = [str(pattern) for pattern in config_cleanup]
sql = entry.get("sql")
ref = entry.get("ref")
notes = entry.get("notes")
raw_value = env_map.get(key, os.environ.get(key, "0"))
env_keys_in_manifest.add(key)
enabled_raw = parse_bool(raw_value)
module = ModuleState(
key=key,
name=name,
repo=repo,
needs_build=needs_build,
module_type=module_type,
requires=requires,
ref=ref,
status=status,
block_reason=block_reason,
post_install_hooks=post_install_hooks,
config_cleanup=config_cleanup,
sql=sql,
notes=notes,
enabled_raw=enabled_raw,
)
if module.blocked and enabled_raw:
module.errors.append(
f"{module.key} is blocked: {module.block_reason or 'blocked in manifest'}"
)
# Effective enablement respects block status
module.enabled_effective = enabled_raw and not module.blocked
module.value = "1" if module.enabled_effective else "0"
modules.append(module)
module_map: Dict[str, ModuleState] = {module.key: module for module in modules}
# Dependency validation
for module in modules:
if not module.enabled_effective:
continue
missing: List[str] = []
for dependency in module.requires:
dep_state = module_map.get(dependency)
if not dep_state or not dep_state.enabled_effective:
missing.append(dependency)
if missing:
plural = "modules" if len(missing) > 1 else "module"
list_str = ", ".join(missing)
message = f"{module.key} requires {plural}: {list_str}"
module.errors.append(message)
# Collect warnings/errors
for module in modules:
if module.errors:
errors.extend(module.errors)
if module.warnings:
warnings.extend(module.warnings)
# Warn if .env defines modules not in manifest
extra_env_modules = [
key for key in env_map.keys() if key.startswith("MODULE_") and key not in module_map
]
for unknown_key in extra_env_modules:
warnings.append(f".env defines {unknown_key} but it is missing from the manifest")
# Warn if manifest entry lacks .env toggle
for module in modules:
if module.key not in env_map and module.key not in os.environ:
warnings.append(
f"Manifest includes {module.key} but .env does not define it (defaulting to 0)"
)
return ModuleCollectionState(
manifest_path=manifest_path,
env_path=env_path,
modules=modules,
generated_at=datetime.now(timezone.utc),
warnings=warnings,
errors=errors,
)
def write_outputs(state: ModuleCollectionState, output_dir: Path) -> None:
output_dir.mkdir(parents=True, exist_ok=True)
env_lines: List[str] = [
"# Autogenerated by scripts/modules.py",
f"# Generated at {state.generated_at.isoformat()}",
f'export MODULES_MANIFEST="{state.manifest_path}"',
f'export MODULES_ENV_PATH="{state.env_path}"',
]
enabled_names: List[str] = []
compile_names: List[str] = []
for module in state.modules:
env_lines.append(f"export {module.key}={module.value}")
if module.enabled_effective:
enabled_names.append(module.name)
if module.enabled_effective and module.needs_build:
compile_names.append(module.name)
env_lines.append(f'export MODULES_ENABLED="{ " ".join(enabled_names) }"'.rstrip())
env_lines.append(f'export MODULES_COMPILE="{ " ".join(compile_names) }"'.rstrip())
env_lines.append(
f"export MODULES_REQUIRES_PLAYERBOT_SOURCE="
f'{"1" if state.requires_playerbot_source() else "0"}'
)
env_lines.append(f"export MODULES_WARNING_COUNT={len(state.warnings)}")
env_lines.append(f"export MODULES_ERROR_COUNT={len(state.errors)}")
modules_env_path = output_dir / "modules.env"
modules_env_path.write_text("\n".join(env_lines) + "\n", encoding="utf-8")
state_payload = {
"generated_at": state.generated_at.isoformat(),
"manifest_path": str(state.manifest_path),
"env_path": str(state.env_path),
"warnings": state.warnings,
"errors": state.errors,
"modules": [
{
**asdict(module),
"enabled_raw": module.enabled_raw,
"enabled_effective": module.enabled_effective,
"blocked": module.blocked,
}
for module in state.modules
],
"enabled_modules": [module.name for module in state.enabled_modules()],
"compile_modules": [module.name for module in state.compile_modules()],
"requires_playerbot_source": state.requires_playerbot_source(),
}
modules_state_path = output_dir / "modules-state.json"
modules_state_path.write_text(
json.dumps(state_payload, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
meta_dir = output_dir / ".modules-meta"
meta_dir.mkdir(parents=True, exist_ok=True)
compile_list_path = meta_dir / "modules-compile.txt"
compile_list_path.write_text(
"\n".join(state_payload["compile_modules"]) + ("\n" if compile_names else ""),
encoding="utf-8",
)
enabled_list_path = meta_dir / "modules-enabled.txt"
enabled_list_path.write_text(
"\n".join(state_payload["enabled_modules"]) + ("\n" if enabled_names else ""),
encoding="utf-8",
)
def print_list(state: ModuleCollectionState, selector: str) -> None:
if selector == "compile":
items = [module.name for module in state.compile_modules()]
elif selector == "enabled":
items = [module.name for module in state.enabled_modules()]
elif selector == "keys":
items = [module.key for module in state.enabled_modules()]
else:
raise ValueError(f"Unknown list selector: {selector}")
for item in items:
print(item)
def print_requires_playerbot(state: ModuleCollectionState) -> None:
print("1" if state.requires_playerbot_source() else "0")
def print_state(state: ModuleCollectionState, fmt: str) -> None:
payload = {
"generated_at": state.generated_at.isoformat(),
"warnings": state.warnings,
"errors": state.errors,
"modules": [
{
"key": module.key,
"name": module.name,
"enabled": module.enabled_effective,
"needs_build": module.needs_build,
"requires": module.requires,
"blocked": module.blocked,
"dependency_issues": module.dependency_issues,
"post_install_hooks": module.post_install_hooks,
"config_cleanup": module.config_cleanup,
}
for module in state.modules
],
"enabled_modules": [module.name for module in state.enabled_modules()],
"compile_modules": [module.name for module in state.compile_modules()],
"requires_playerbot_source": state.requires_playerbot_source(),
}
if fmt == "json":
json.dump(payload, sys.stdout, indent=2, sort_keys=True)
sys.stdout.write("\n")
elif fmt == "shell":
keys = [module.key for module in state.modules]
quoted_keys = " ".join(shlex.quote(key) for key in keys)
print(f"MODULE_KEYS=({quoted_keys})")
print(
"declare -A MODULE_NAME MODULE_REPO MODULE_REF MODULE_TYPE MODULE_ENABLED "
"MODULE_NEEDS_BUILD MODULE_BLOCKED MODULE_POST_INSTALL MODULE_REQUIRES "
"MODULE_CONFIG_CLEANUP "
"MODULE_NOTES MODULE_STATUS MODULE_BLOCK_REASON"
)
for module in state.modules:
key = module.key
post_install = ",".join(module.post_install_hooks)
dependencies = ",".join(module.requires)
block_reason = module.block_reason or ""
ref = module.ref or ""
notes = module.notes or ""
config_cleanup = ",".join(module.config_cleanup)
print(f"MODULE_NAME[{key}]={shlex.quote(module.name)}")
print(f"MODULE_REPO[{key}]={shlex.quote(module.repo)}")
print(f"MODULE_REF[{key}]={shlex.quote(ref)}")
print(f"MODULE_TYPE[{key}]={shlex.quote(module.module_type)}")
print(f"MODULE_ENABLED[{key}]={1 if module.enabled_effective else 0}")
print(f"MODULE_NEEDS_BUILD[{key}]={1 if module.needs_build else 0}")
print(f"MODULE_BLOCKED[{key}]={1 if module.blocked else 0}")
print(f"MODULE_POST_INSTALL[{key}]={shlex.quote(post_install)}")
print(f"MODULE_REQUIRES[{key}]={shlex.quote(dependencies)}")
print(f"MODULE_CONFIG_CLEANUP[{key}]={shlex.quote(config_cleanup)}")
print(f"MODULE_NOTES[{key}]={shlex.quote(notes)}")
print(f"MODULE_STATUS[{key}]={shlex.quote(module.status)}")
print(f"MODULE_BLOCK_REASON[{key}]={shlex.quote(block_reason)}")
else:
raise ValueError(f"Unsupported format: {fmt}")
def handle_generate(args: argparse.Namespace) -> int:
env_path = Path(args.env_path).resolve()
manifest_path = Path(args.manifest).resolve()
output_dir = Path(args.output_dir).resolve()
state = build_state(env_path, manifest_path)
write_outputs(state, output_dir)
if state.warnings:
warning_block = "\n".join(f"- {warning}" for warning in state.warnings)
print(
textwrap.dedent(
f"""\
⚠️ Module manifest warnings detected:
{warning_block}
"""
),
file=sys.stderr,
)
if state.errors:
error_block = "\n".join(f"- {error}" for error in state.errors)
print(
textwrap.dedent(
f"""\
❌ Module manifest errors detected:
{error_block}
"""
),
file=sys.stderr,
)
return 1
return 0
def configure_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Module manifest helper")
parser.add_argument(
"--env-path",
default=".env",
help="Path to .env file (default: .env)",
)
parser.add_argument(
"--manifest",
default="config/modules.json",
help="Path to module manifest (default: config/modules.json)",
)
subparsers = parser.add_subparsers(dest="command", required=True)
generate_parser = subparsers.add_parser("generate", help="Generate module state files")
generate_parser.add_argument(
"--output-dir",
default="local-storage/modules",
help="Directory for generated module artifacts (default: local-storage/modules)",
)
generate_parser.set_defaults(func=handle_generate)
list_parser = subparsers.add_parser("list", help="Print module lists")
list_parser.add_argument(
"--type",
choices=["compile", "enabled", "keys"],
default="compile",
help="List selector (default: compile)",
)
def handle_list(args: argparse.Namespace) -> int:
state = build_state(Path(args.env_path).resolve(), Path(args.manifest).resolve())
print_list(state, args.type)
return 1 if state.errors else 0
list_parser.set_defaults(func=handle_list)
rps_parser = subparsers.add_parser(
"requires-playerbot", help="Print 1 if playerbot source is required else 0"
)
def handle_requires_playerbot(args: argparse.Namespace) -> int:
state = build_state(Path(args.env_path).resolve(), Path(args.manifest).resolve())
print_requires_playerbot(state)
return 1 if state.errors else 0
rps_parser.set_defaults(func=handle_requires_playerbot)
dump_parser = subparsers.add_parser("dump", help="Dump module state (JSON format)")
dump_parser.add_argument(
"--format",
choices=["json", "shell"],
default="json",
help="Output format (default: json)",
)
def handle_dump(args: argparse.Namespace) -> int:
state = build_state(Path(args.env_path).resolve(), Path(args.manifest).resolve())
print_state(state, args.format)
return 1 if state.errors else 0
dump_parser.set_defaults(func=handle_dump)
return parser
def main(argv: Optional[Iterable[str]] = None) -> int:
parser = configure_parser()
args = parser.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
sys.exit(main())

View File

@@ -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
fi
local key
for key in "${COMPILE_MODULE_KEYS[@]}"; do
if [ "$(read_env "$key" "0")" = "1" ]; then
echo 1
return
fi
done
else
echo 0
fi
}
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

View File

@@ -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 (

View File

@@ -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)

View File

@@ -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