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