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/ images/
node_modules/ node_modules/
.mcp*/ .mcp*/
scripts/__pycache__/
.env .env
package-lock.json package-lock.json
package.json package.json
.modules_state .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 #### `scripts/manage-modules.sh` - Module Management Container
Internal script that manages module lifecycle within the ac-modules 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 #### `scripts/manage-modules-sql.sh` - Module Database Integration
Executes module-specific SQL scripts for database schema updates. 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 docker
require_cmd python3
read_env(){ read_env(){
local key="$1" default="${2:-}" local key="$1" default="${2:-}"
@@ -80,28 +81,47 @@ read_env(){
echo "$value" echo "$value"
} }
# Module detection logic (extracted from deploy.sh) MODULE_HELPER="$ROOT_DIR/scripts/modules.py"
COMPILE_MODULE_VARS=( MODULE_STATE_INITIALIZED=0
MODULE_AOE_LOOT MODULE_LEARN_SPELLS MODULE_FIREWORKS MODULE_INDIVIDUAL_PROGRESSION MODULE_AHBOT MODULE_AUTOBALANCE declare -a MODULES_COMPILE_LIST=()
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 resolve_local_storage_path(){
MODULE_ELUNA MODULE_TIME_IS_TIME MODULE_POCKET_PORTAL MODULE_RANDOM_ENCHANTS MODULE_SOLOCRAFT MODULE_PVP_TITLES MODULE_NPC_BEASTMASTER local local_root
MODULE_NPC_ENCHANTER MODULE_INSTANCE_RESET MODULE_LEVEL_GRANT MODULE_ARAC MODULE_ASSISTANT MODULE_REAGENT_BANK local_root="$(read_env STORAGE_PATH_LOCAL "./local-storage")"
MODULE_BLACK_MARKET_AUCTION_HOUSE MODULE_CHALLENGE_MODES MODULE_OLLAMA_CHAT MODULE_PLAYER_BOT_LEVEL_BRACKETS MODULE_STATBOOSTER MODULE_DUNGEON_RESPAWN if [[ "$local_root" != /* ]]; then
MODULE_SKELETON_MODULE MODULE_BG_SLAVERYVALLEY MODULE_AZEROTHSHARD MODULE_WORGOBLIN MODULE_ELUNA_TS 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(){ requires_playerbot_source(){
if [ "$(read_env MODULE_PLAYERBOTS "0")" = "1" ]; then if [ "$MODULE_STATE_INITIALIZED" -ne 1 ]; then
return 0 generate_module_state
fi fi
local var [ "${MODULES_REQUIRES_PLAYERBOT_SOURCE:-0}" = "1" ]
for var in "${COMPILE_MODULE_VARS[@]}"; do
if [ "$(read_env "$var" "0")" = "1" ]; then
return 0
fi
done
return 1
} }
ensure_source_repo(){ ensure_source_repo(){
@@ -197,14 +217,14 @@ detect_rebuild_reasons(){
fi fi
# Check if any C++ modules are enabled but modules-latest images don't exist # 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 any_cxx_modules=0
local var if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then
for var in "${COMPILE_MODULE_VARS[@]}"; do any_cxx_modules=1
if [ "$(read_env "$var" "0")" = "1" ]; then fi
any_cxx_modules=1
break
fi
done
if [ "$any_cxx_modules" = "1" ]; then if [ "$any_cxx_modules" = "1" ]; then
local authserver_modules_image local authserver_modules_image
@@ -296,11 +316,7 @@ confirm_build(){
# Module staging logic (extracted from setup.sh) # Module staging logic (extracted from setup.sh)
sync_modules(){ sync_modules(){
local storage_path local storage_path
storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")" storage_path="$(resolve_local_storage_path)"
if [[ "$storage_path" != /* ]]; then
storage_path="${storage_path#./}"
storage_path="$ROOT_DIR/$storage_path"
fi
mkdir -p "$storage_path/modules" mkdir -p "$storage_path/modules"
info "Using local module staging at $storage_path/modules" info "Using local module staging at $storage_path/modules"
@@ -323,10 +339,10 @@ resolve_project_name(){
stage_modules(){ stage_modules(){
local src_path="$1" local src_path="$1"
local storage_path local storage_path
storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")" storage_path="$(resolve_local_storage_path)"
if [[ "$storage_path" != /* ]]; then
storage_path="${storage_path#./}" if [ -z "${MODULES_ENABLED:-}" ]; then
storage_path="$ROOT_DIR/$storage_path" generate_module_state
fi fi
info "Staging modules to source directory: $src_path/modules" info "Staging modules to source directory: $src_path/modules"
@@ -340,28 +356,17 @@ stage_modules(){
local local_modules_dir="${src_path}/modules" local local_modules_dir="${src_path}/modules"
mkdir -p "$local_modules_dir" 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" local staging_modules_dir="${storage_path}/modules"
export MODULES_HOST_DIR="$staging_modules_dir" 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 # Set up local storage path for build sentinel tracking
local local_storage_path local local_storage_path
local_storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")" local_storage_path="$(read_env STORAGE_PATH_LOCAL "./local-storage")"
@@ -388,6 +393,7 @@ stage_modules(){
# Run module staging script in local modules directory # Run module staging script in local modules directory
export MODULES_LOCAL_RUN=1 export MODULES_LOCAL_RUN=1
export MODULES_SKIP_SQL=1
if [ -n "$staging_modules_dir" ]; then if [ -n "$staging_modules_dir" ]; then
mkdir -p "$staging_modules_dir" mkdir -p "$staging_modules_dir"
rm -f "$staging_modules_dir/.modules_state" "$staging_modules_dir/.requires_rebuild" 2>/dev/null || true 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 \ rsync -a --delete \
--exclude '.modules_state' \ --exclude '.modules_state' \
--exclude '.requires_rebuild' \ --exclude '.requires_rebuild' \
--exclude 'modules.env' \
--exclude 'modules-state.json' \
--exclude 'modules-compile.txt' \
--exclude 'modules-enabled.txt' \
"$local_modules_dir"/ "$staging_modules_dir"/ "$local_modules_dir"/ "$staging_modules_dir"/
else else
find "$staging_modules_dir" -mindepth 1 -maxdepth 1 \ find "$staging_modules_dir" -mindepth 1 -maxdepth 1 \
! -name '.modules_state' \ ! -name '.modules_state' \
! -name '.requires_rebuild' \ ! -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 -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 -) (cd "$local_modules_dir" && tar cf - --exclude='.modules_state' --exclude='.requires_rebuild' .) | (cd "$staging_modules_dir" && tar xf -)
fi fi
@@ -420,6 +434,7 @@ stage_modules(){
# Cleanup # Cleanup
export GIT_CONFIG_GLOBAL="$prev_git_config_global" export GIT_CONFIG_GLOBAL="$prev_git_config_global"
unset MODULES_LOCAL_RUN unset MODULES_LOCAL_RUN
unset MODULES_SKIP_SQL
unset MODULES_HOST_DIR unset MODULES_HOST_DIR
[ -n "$git_temp_config" ] && [ -f "$git_temp_config" ] && rm -f "$git_temp_config" [ -n "$git_temp_config" ] && [ -f "$git_temp_config" ] && rm -f "$git_temp_config"
} }
@@ -492,6 +507,9 @@ main(){
local src_dir local src_dir
local rebuild_reasons local rebuild_reasons
info "Preparing module manifest metadata"
generate_module_state
info "Step 1/6: Setting up source repository" info "Step 1/6: Setting up source repository"
src_dir="$(ensure_source_repo)" 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_SKIP_STORAGE=0
REMOTE_ARGS_PROVIDED=0 REMOTE_ARGS_PROVIDED=0
COMPILE_MODULE_VARS=( MODULE_HELPER="$ROOT_DIR/scripts/modules.py"
MODULE_AOE_LOOT MODULE_LEARN_SPELLS MODULE_FIREWORKS MODULE_INDIVIDUAL_PROGRESSION MODULE_AHBOT MODULE_AUTOBALANCE MODULE_STATE_INITIALIZED=0
MODULE_TRANSMOG MODULE_NPC_BUFFER MODULE_DYNAMIC_XP MODULE_SOLO_LFG MODULE_1V1_ARENA MODULE_PHASED_DUELS declare -a MODULES_COMPILE_LIST=()
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
)
BLUE='\033[0;34m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' 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}"; } info(){ printf '%b\n' "${BLUE} $*${NC}"; }
@@ -248,6 +242,7 @@ require_cmd(){
} }
require_cmd docker require_cmd docker
require_cmd python3
if [ "$REMOTE_MODE" -eq 1 ]; then if [ "$REMOTE_MODE" -eq 1 ]; then
if [ -z "$REMOTE_HOST" ]; then if [ -z "$REMOTE_HOST" ]; then
@@ -283,6 +278,43 @@ read_env(){
echo "$value" 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(){ resolve_project_name(){
local raw_name="$(read_env COMPOSE_PROJECT_NAME "acore-compose")" local raw_name="$(read_env COMPOSE_PROJECT_NAME "acore-compose")"
local sanitized local sanitized
@@ -327,14 +359,12 @@ detect_build_needed(){
fi fi
# Check if any C++ modules are enabled but modules-latest images don't exist # Check if any C++ modules are enabled but modules-latest images don't exist
ensure_module_state
local any_cxx_modules=0 local any_cxx_modules=0
local var if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then
for var in "${COMPILE_MODULE_VARS[@]}"; do any_cxx_modules=1
if [ "$(read_env "$var" "0")" = "1" ]; then fi
any_cxx_modules=1
break
fi
done
if [ "$any_cxx_modules" = "1" ]; then if [ "$any_cxx_modules" = "1" ]; then
local authserver_modules_image local authserver_modules_image
@@ -473,13 +503,11 @@ determine_profile(){
return return
fi fi
local var ensure_module_state
for var in "${COMPILE_MODULE_VARS[@]}"; do if [ "${#MODULES_COMPILE_LIST[@]}" -gt 0 ]; then
if [ "$(read_env "$var" "0")" = "1" ]; then echo "modules"
echo "modules" return
return fi
fi
done
echo "standard" echo "standard"
} }

View File

@@ -590,7 +590,9 @@ services:
- ${STORAGE_PATH}/modules:/modules - ${STORAGE_PATH}/modules:/modules
- ${STORAGE_PATH}/config:/azerothcore/env/dist/etc - ${STORAGE_PATH}/config:/azerothcore/env/dist/etc
- ./scripts:/tmp/scripts:ro - ./scripts:/tmp/scripts:ro
- ./config:/tmp/config:ro
environment: environment:
- MODULES_MANIFEST_PATH=/tmp/config/modules.json
- MODULE_PLAYERBOTS=${MODULE_PLAYERBOTS} - MODULE_PLAYERBOTS=${MODULE_PLAYERBOTS}
- MODULE_AOE_LOOT=${MODULE_AOE_LOOT} - MODULE_AOE_LOOT=${MODULE_AOE_LOOT}
- MODULE_LEARN_SPELLS=${MODULE_LEARN_SPELLS} - MODULE_LEARN_SPELLS=${MODULE_LEARN_SPELLS}
@@ -634,6 +636,8 @@ services:
- MODULE_WORGOBLIN=${MODULE_WORGOBLIN} - MODULE_WORGOBLIN=${MODULE_WORGOBLIN}
- MODULE_ELUNA_TS=${MODULE_ELUNA_TS} - MODULE_ELUNA_TS=${MODULE_ELUNA_TS}
- CONTAINER_MYSQL=${CONTAINER_MYSQL} - CONTAINER_MYSQL=${CONTAINER_MYSQL}
- MYSQL_PORT=${MYSQL_PORT}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- DB_AUTH_NAME=${DB_AUTH_NAME} - DB_AUTH_NAME=${DB_AUTH_NAME}
- DB_WORLD_NAME=${DB_WORLD_NAME} - DB_WORLD_NAME=${DB_WORLD_NAME}
@@ -646,7 +650,7 @@ services:
command: command:
- -c - -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 (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 # Fix permissions after module operations
chown -R ${CONTAINER_USER} /modules /azerothcore/env/dist/etc 2>/dev/null || true 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" CUSTOM_SQL_ROOT="/tmp/scripts/sql/custom"
ALT_CUSTOM_SQL_ROOT="/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(){ run_custom_sql_group(){
local subdir="$1" target_db="$2" label="$3" local subdir="$1" target_db="$2" label="$3"
local dir="${CUSTOM_SQL_ROOT}/${subdir}" local dir="${CUSTOM_SQL_ROOT}/${subdir}"
@@ -13,25 +82,76 @@ run_custom_sql_group(){
dir="${ALT_CUSTOM_SQL_ROOT}/${subdir}" dir="${ALT_CUSTOM_SQL_ROOT}/${subdir}"
fi fi
[ -d "$dir" ] || return 0 [ -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 local base_name
base_name="$(basename "$sql_file")" 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}" 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}" echo " ✅ Successfully executed ${base_name}"
log_sql_success "$target_db" "$sql_file"
else else
echo " ❌ Failed to execute $sql_file" echo " ❌ Failed to execute $sql_file"
sed 's/^/ /' "$sql_output"
log_sql_failure "$target_db" "$sql_file"
fi 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 # Function to execute SQL files for a module
execute_module_sql() { module_sql_run_module(){
local module_dir="$1" local module_key="$1"
local module_name="$2" 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 playerbots_db="${DB_PLAYERBOTS_NAME:-acore_playerbots}"
local character_set="${MYSQL_CHARACTER_SET:-utf8mb4}" local character_set="${MYSQL_CHARACTER_SET:-utf8mb4}"
local collation="${MYSQL_COLLATION:-utf8mb4_unicode_ci}" 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 local run_sorted_sql
run_sorted_sql() { run_sorted_sql() {
@@ -40,27 +160,22 @@ execute_module_sql() {
local label="$3" local label="$3"
local skip_regex="${4:-}" local skip_regex="${4:-}"
[ -d "$dir" ] || return [ -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 local base_name
base_name="$(basename "$sql_file")" base_name="$(basename "$sql_file")"
if [ -n "$skip_regex" ] && [[ "$base_name" =~ $skip_regex ]]; then if [ -n "$skip_regex" ] && [[ "$base_name" =~ $skip_regex ]]; then
echo " Skipping ${label}: ${base_name}" echo " Skipping ${label}: ${base_name}"
continue continue
fi fi
echo " Executing ${label}: ${base_name}" execute_sql_file_in_db "$target_db" "$sql_file" "$label"
if mariadb --ssl=false -h "${CONTAINER_MYSQL}" -P 3306 -u root -p"${MYSQL_ROOT_PASSWORD}" "${target_db}" < "$sql_file" >/dev/null 2>&1; then done < <(LC_ALL=C find "$dir" -type f -name "*.sql" | sort) || true
echo " ✅ Successfully executed ${base_name}"
else
echo " ❌ Failed to execute $sql_file"
fi
done || true
} }
echo "Processing SQL scripts for $module_name..." echo "Processing SQL scripts for $module_name..."
if [ "$module_name" = "Playerbots" ]; then if [ "$module_key" = "MODULE_PLAYERBOTS" ]; then
echo " Ensuring database ${playerbots_db} exists..." 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" echo " ✅ Playerbots database ready"
else else
echo " ❌ Failed to ensure playerbots database" echo " ❌ Failed to ensure playerbots database"
@@ -71,45 +186,30 @@ execute_module_sql() {
if [ -d "$module_dir/data/sql" ]; then if [ -d "$module_dir/data/sql" ]; then
# Execute world database scripts # Execute world database scripts
if [ -d "$module_dir/data/sql/world" ]; then if [ -d "$module_dir/data/sql/world" ]; then
find "$module_dir/data/sql/world" -name "*.sql" -type f | while read sql_file; do while IFS= read -r sql_file; do
echo " Executing world SQL: $(basename "$sql_file")" execute_sql_file_in_db "$world_db" "$sql_file" "world SQL"
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 done < <(find "$module_dir/data/sql/world" -type f -name "*.sql") || true
echo " ✅ Successfully executed $(basename "$sql_file")"
else
echo " ❌ Failed to execute $sql_file"
fi
done
fi 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 # Execute auth database scripts
if [ -d "$module_dir/data/sql/auth" ]; then if [ -d "$module_dir/data/sql/auth" ]; then
find "$module_dir/data/sql/auth" -name "*.sql" -type f | while read sql_file; do while IFS= read -r sql_file; do
echo " Executing auth SQL: $(basename "$sql_file")" execute_sql_file_in_db "$auth_db" "$sql_file" "auth SQL"
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 done < <(find "$module_dir/data/sql/auth" -type f -name "*.sql") || true
echo " ✅ Successfully executed $(basename "$sql_file")"
else
echo " ❌ Failed to execute $sql_file"
fi
done
fi 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 # Execute character database scripts
if [ -d "$module_dir/data/sql/characters" ]; then if [ -d "$module_dir/data/sql/characters" ]; then
find "$module_dir/data/sql/characters" -name "*.sql" -type f | while read sql_file; do while IFS= read -r sql_file; do
echo " Executing characters SQL: $(basename "$sql_file")" execute_sql_file_in_db "$characters_db" "$sql_file" "characters SQL"
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 done < <(find "$module_dir/data/sql/characters" -type f -name "*.sql") || true
echo " ✅ Successfully executed $(basename "$sql_file")"
else
echo " ❌ Failed to execute $sql_file"
fi
done
fi 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 # 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" local pb_root="$module_dir/data/sql/playerbots"
run_sorted_sql "$pb_root/base" "$playerbots_db" "playerbots SQL" run_sorted_sql "$pb_root/base" "$playerbots_db" "playerbots SQL"
run_sorted_sql "$pb_root/custom" "$playerbots_db" "playerbots SQL" run_sorted_sql "$pb_root/custom" "$playerbots_db" "playerbots SQL"
@@ -119,18 +219,16 @@ execute_module_sql() {
fi fi
# Execute base SQL files (common pattern) # Execute base SQL files (common pattern)
find "$module_dir/data/sql" -maxdepth 1 -name "*.sql" -type f | while read sql_file; do while IFS= read -r sql_file; do
echo " Executing base SQL: $(basename "$sql_file")" execute_sql_file_in_db "$world_db" "$sql_file" "base SQL"
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 < <(find "$module_dir/data/sql" -maxdepth 1 -type f -name "*.sql") || true
done
fi fi
# Look for SQL files in other common locations # Look for SQL files in other common locations
if [ -d "$module_dir/sql" ]; then if [ -d "$module_dir/sql" ]; then
find "$module_dir/sql" -name "*.sql" -type f | while read sql_file; do while IFS= read -r sql_file; do
echo " Executing SQL: $(basename "$sql_file")" execute_sql_file_in_db "$world_db" "$sql_file" "module SQL"
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 < <(find "$module_dir/sql" -type f -name "*.sql") || true
done
fi fi
return 0 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" 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 SQL_SUCCESS_LOG=()
for module_dir in */; do SQL_FAILURE_LOG=()
[[ -d "$module_dir" ]] || continue
[[ "$module_dir" == "." || "$module_dir" == ".." ]] && continue # Iterate modules from manifest metadata
module_dir="${module_dir%/}" local key module_dir enabled
# Only process directories that follow mod-* convention or known module names local world_db="${DB_WORLD_NAME:-acore_world}"
if [[ "$module_dir" != mod-* && "$module_dir" != StatBooster && "$module_dir" != DungeonRespawn && "$module_dir" != eluna-ts ]]; then 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 continue
fi fi
local enabled=0 if [ "$module_dir" = "mod-pocket-portal" ]; then
case "$module_dir" in echo '⚠️ Skipping mod-pocket-portal SQL: module disabled until C++20 patch is applied.'
mod-playerbots) enabled="$MODULE_PLAYERBOTS" ;; continue
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 fi
module_sql_run_module "$key" "$module_dir"
done done
run_custom_sql_group world "${DB_WORLD_NAME}" "custom world SQL" run_custom_sql_group world "${world_db}" "custom world SQL"
run_custom_sql_group auth "${DB_AUTH_NAME}" "custom auth SQL" run_custom_sql_group auth "${auth_db}" "custom auth SQL"
run_custom_sql_group characters "${DB_CHARACTERS_NAME}" "custom characters 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 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="" SOURCE_OVERRIDE=""
SKIP_STOP=0 SKIP_STOP=0
COMPILE_MODULE_KEYS=( MODULE_HELPER="$PROJECT_DIR/scripts/modules.py"
MODULE_AOE_LOOT MODULE_LEARN_SPELLS MODULE_FIREWORKS MODULE_INDIVIDUAL_PROGRESSION MODULE_AHBOT MODULE_AUTOBALANCE MODULE_STATE_DIR=""
MODULE_TRANSMOG MODULE_NPC_BUFFER MODULE_DYNAMIC_XP MODULE_SOLO_LFG MODULE_1V1_ARENA MODULE_PHASED_DUELS declare -a MODULES_COMPILE_LIST=()
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 resolve_local_storage_path(){
MODULE_NPC_ENCHANTER MODULE_INSTANCE_RESET MODULE_LEVEL_GRANT MODULE_ARAC MODULE_ASSISTANT MODULE_REAGENT_BANK local path
MODULE_BLACK_MARKET_AUCTION_HOUSE MODULE_CHALLENGE_MODES MODULE_OLLAMA_CHAT MODULE_PLAYER_BOT_LEVEL_BRACKETS MODULE_STATBOOSTER MODULE_DUNGEON_RESPAWN path="$(read_env STORAGE_PATH_LOCAL "./local-storage")"
MODULE_SKELETON_MODULE MODULE_BG_SLAVERYVALLEY MODULE_AZEROTHSHARD MODULE_WORGOBLIN MODULE_ELUNA_TS 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(){ 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 echo 1
return else
echo 0
fi 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 while [[ $# -gt 0 ]]; do
@@ -124,6 +145,11 @@ if ! command -v docker >/dev/null 2>&1; then
exit 1 exit 1
fi 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")" STORAGE_PATH="$(read_env STORAGE_PATH "./storage")"
if [[ "$STORAGE_PATH" != /* ]]; then if [[ "$STORAGE_PATH" != /* ]]; then
STORAGE_PATH="$PROJECT_DIR/${STORAGE_PATH#./}" STORAGE_PATH="$PROJECT_DIR/${STORAGE_PATH#./}"
@@ -191,65 +217,16 @@ if [ ! -f "$SOURCE_COMPOSE" ]; then
fi fi
fi fi
declare -A MODULE_REPO_MAP=( ensure_module_state
[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
)
compile_modules=() if [ ${#MODULES_COMPILE_LIST[@]} -eq 0 ]; then
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
echo "✅ No C++ modules enabled that require a source rebuild." echo "✅ No C++ modules enabled that require a source rebuild."
rm -f "$SENTINEL_FILE" 2>/dev/null || true rm -f "$SENTINEL_FILE" 2>/dev/null || true
exit 0 exit 0
fi fi
echo "🔧 Modules requiring compilation:" echo "🔧 Modules requiring compilation:"
for mod in "${compile_modules[@]}"; do for mod in "${MODULES_COMPILE_LIST[@]}"; do
echo "$mod" echo "$mod"
done done

View File

@@ -8,7 +8,7 @@ WHERE guid IN (
AND c.deleteInfos_Account IS NULL AND c.deleteInfos_Account IS NULL
AND c.name IN ( AND c.name IN (
SELECT p.name SELECT p.name
FROM playerbots p FROM `{{PLAYERBOTS_DB}}`.playerbots p
WHERE p.bot = 1 WHERE p.bot = 1
) )
AND EXISTS ( AND EXISTS (

View File

@@ -97,6 +97,37 @@ read_env(){
echo "$value" 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(){ confirm(){
local prompt="$1" default="$2" reply local prompt="$1" default="$2" reply
if [ "$ASSUME_YES" = "1" ]; then if [ "$ASSUME_YES" = "1" ]; then
@@ -141,15 +172,15 @@ STORAGE_PATH="$(read_env STORAGE_PATH "./storage")"
if [[ "$STORAGE_PATH" != /* ]]; then if [[ "$STORAGE_PATH" != /* ]]; then
STORAGE_PATH="$PROJECT_DIR/$STORAGE_PATH" STORAGE_PATH="$PROJECT_DIR/$STORAGE_PATH"
fi fi
STORAGE_PATH="$(canonical_path "$STORAGE_PATH")"
MODULES_DIR="$STORAGE_PATH/modules" MODULES_DIR="$STORAGE_PATH/modules"
# Build sentinel is in local storage, deployment modules are in shared storage # Build sentinel is in local storage, deployment modules are in shared storage
LOCAL_STORAGE_PATH="$(read_env STORAGE_PATH_LOCAL "./local-storage")" LOCAL_STORAGE_PATH="$(read_env STORAGE_PATH_LOCAL "./local-storage")"
if [[ "$LOCAL_STORAGE_PATH" != /* ]]; then if [[ "$LOCAL_STORAGE_PATH" != /* ]]; then
# Remove leading ./ if present
LOCAL_STORAGE_PATH="${LOCAL_STORAGE_PATH#./}"
LOCAL_STORAGE_PATH="$PROJECT_DIR/$LOCAL_STORAGE_PATH" LOCAL_STORAGE_PATH="$PROJECT_DIR/$LOCAL_STORAGE_PATH"
fi fi
LOCAL_STORAGE_PATH="$(canonical_path "$LOCAL_STORAGE_PATH")"
SENTINEL_FILE="$LOCAL_STORAGE_PATH/modules/.requires_rebuild" SENTINEL_FILE="$LOCAL_STORAGE_PATH/modules/.requires_rebuild"
# Define module mappings (from rebuild-with-modules.sh) # Define module mappings (from rebuild-with-modules.sh)

View File

@@ -1,10 +1,39 @@
#!/bin/bash #!/bin/bash
# Thin wrapper to bring the AzerothCore stack online without triggering rebuilds. # 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 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