mirror of
https://github.com/mod-playerbots/azerothcore-wotlk.git
synced 2026-01-22 05:06:24 +00:00
feat(apps/config): Config Merger in python (#24081)
Co-authored-by: FlyingArowana <TheSCREWEDSoftware@users.noreply.github.com> Co-authored-by: Brian Aldridge <baldridge@resourcedata.com>
This commit is contained in:
148
apps/config-merger/python/README.md
Normal file
148
apps/config-merger/python/README.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# AzerothCore Config Updater/Merger - Python Version
|
||||
|
||||
A command-line tool to update your AzerothCore configuration files with new options from distribution files.
|
||||
|
||||
> [!NOTE]
|
||||
> Based on and modified from [@BoiseComputer](https://github.com/BoiseComputer) (Brian Aldridge)'s [update_module_confs](https://github.com/Brian-Aldridge/update_module_confs) project to meet AzerothCore's needs
|
||||
|
||||
## Overview
|
||||
|
||||
This tool compares your existing configuration files (`.conf`) with the latest distribution files (`.conf.dist`) and helps you add new configuration options that may have been introduced in updates. It ensures your configs stay up-to-date while preserving your custom settings.
|
||||
|
||||
## Features
|
||||
|
||||
- **Interactive Menu System** - Easy-to-use numbered menu options
|
||||
- **Server Config Support** - Update authserver.conf and worldserver.conf
|
||||
- **Module Config Support** - Update all or selected module configurations
|
||||
- **Automatic Backups** - If you choose a valid option and there are changes, a timestamped backup is created before any changes are made (e.g. `filename(d11_m12_y2025_14h_30m_45s).bak`)
|
||||
- **Selective Updates** - Choose which new config options to add (y/n prompts)
|
||||
- **Safe Operation** - Only creates backups and makes changes when new options are found
|
||||
|
||||
## How to Use
|
||||
|
||||
### Interactive Mode (Default)
|
||||
|
||||
1. **Run the script** in your configs directory:
|
||||
```bash
|
||||
python config_merger.py
|
||||
```
|
||||
Or simply **double-click** the `config_merger.py` file to run it directly.
|
||||
|
||||
2. **Specify configs path** (or press Enter for current directory):
|
||||
```
|
||||
Enter the path to your configs folder (default: .) which means current folder:
|
||||
```
|
||||
|
||||
3. **Choose from the menu**:
|
||||
```
|
||||
AzerothCore Config Updater/Merger (v. 1)
|
||||
--------------------------
|
||||
1 - Update Auth Config
|
||||
2 - Update World Config
|
||||
3 - Update Auth and World Configs
|
||||
4 - Update All Modules Configs
|
||||
5 - Update Modules (Selection) Configs
|
||||
0 - Quit
|
||||
```
|
||||
|
||||
### Command Line Interface (CLI)
|
||||
|
||||
For automation and scripting, you can use CLI mode:
|
||||
|
||||
```bash
|
||||
python config_merger.py [config_dir] [target] [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `config_dir` (optional): Path to configs directory (default: current directory)
|
||||
- `target` (optional): What to update:
|
||||
- `auth` - Update authserver.conf only
|
||||
- `world` - Update worldserver.conf only
|
||||
- `both` - Update both server configs
|
||||
- `modules` - Update all module configs
|
||||
- `modules-select` - Interactive module selection
|
||||
|
||||
**Options:**
|
||||
- `-y, --yes`: Skip prompts and auto-add all new config options (default: prompt for each option)
|
||||
- `--version`: Show version information
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Interactive mode (default)
|
||||
python config_merger.py
|
||||
|
||||
# Update auth config with prompts
|
||||
python config_merger.py . auth
|
||||
|
||||
# Update both configs automatically (no prompts)
|
||||
python config_merger.py /path/to/configs both -y
|
||||
|
||||
# Update all modules with confirmation
|
||||
python config_merger.py . modules
|
||||
```
|
||||
|
||||
## Menu Options Explained
|
||||
|
||||
- **Option 1**: Updates only `authserver.conf` from `authserver.conf.dist`
|
||||
- **Option 2**: Updates only `worldserver.conf` from `worldserver.conf.dist`
|
||||
- **Option 3**: Updates both server config files
|
||||
- **Option 4**: Automatically processes all module config files in the `modules/` folder
|
||||
- **Option 5**: Shows you a list of available modules and lets you select specific ones to update
|
||||
- **Option 0**: Exit the program
|
||||
|
||||
## Interactive Process
|
||||
|
||||
For each missing configuration option found, the tool will:
|
||||
|
||||
1. **Show you the option** with its comments and default value
|
||||
2. **Ask for confirmation**: `Add [option_name] to config? (y/n):`
|
||||
3. **Add or skip** based on your choice
|
||||
4. **Create backup** (before any changes are made) only if you choose a valid option and there are changes (format: `filename(d11_m12_y2025_14h_30m_45s).bak`)
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
Processing worldserver.conf ...
|
||||
Backup created: worldserver.conf(d11_m12_y2025_14h_30m_45s).bak
|
||||
|
||||
# New feature for XP rates
|
||||
XP.Rate = 1
|
||||
Add XP.Rate to config? (y/n): y
|
||||
Added XP.Rate.
|
||||
|
||||
# Database connection pool size
|
||||
Database.PoolSize = 5
|
||||
Add Database.PoolSize to config? (y/n): n
|
||||
Skipped Database.PoolSize.
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.6 or higher
|
||||
- No additional libraries needed (uses built-in modules only)
|
||||
|
||||
## File Structure Expected
|
||||
|
||||
```
|
||||
configs/
|
||||
├── config_merger.py (this script)
|
||||
├── authserver.conf.dist
|
||||
├── authserver.conf
|
||||
├── worldserver.conf.dist
|
||||
├── worldserver.conf
|
||||
└── modules/
|
||||
├── mod_example.conf.dist
|
||||
├── mod_example.conf
|
||||
└── ...
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This file is part of the AzerothCore Project. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
**Note:** Original code portions were licensed under the MIT License by Brian Aldridge (https://github.com/BoiseComputer)
|
||||
Original project: https://github.com/Brian-Aldridge/update_module_confs
|
||||
276
apps/config-merger/python/config_merger.py
Normal file
276
apps/config-merger/python/config_merger.py
Normal file
@@ -0,0 +1,276 @@
|
||||
# Version 1
|
||||
# Based and modified from: https://github.com/Brian-Aldridge/update_module_confs
|
||||
#
|
||||
# This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Original code portions licensed under MIT License by Brian Aldridge (https://github.com/BoiseComputer)
|
||||
# Original project: https://github.com/Brian-Aldridge/update_module_confs
|
||||
|
||||
VERSION = "1"
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import argparse
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
def find_modules(folder):
|
||||
dist_files = []
|
||||
try:
|
||||
files = os.listdir(folder)
|
||||
except (OSError, IOError) as e:
|
||||
print(f"[ERROR] Could not list directory '{folder}': {e}")
|
||||
return []
|
||||
for file in files:
|
||||
if file.endswith('.conf.dist'):
|
||||
dist_files.append(file)
|
||||
return sorted(dist_files)
|
||||
|
||||
def prompt_module_selection(dist_files):
|
||||
print("Found the following modules:")
|
||||
for idx, fname in enumerate(dist_files, 1):
|
||||
print(f" {idx}. {fname}")
|
||||
nums = input("Enter numbers of modules to update (comma-separated): ").strip()
|
||||
raw_inputs = [x.strip() for x in nums.split(",") if x.strip()]
|
||||
indices = []
|
||||
invalid = []
|
||||
for x in raw_inputs:
|
||||
if not x.isdigit():
|
||||
invalid.append(f"'{x}' (not a number)")
|
||||
continue
|
||||
idx = int(x)
|
||||
if 0 < idx <= len(dist_files):
|
||||
indices.append(idx-1)
|
||||
else:
|
||||
invalid.append(f"'{x}' (out of range, must be 1-{len(dist_files)})")
|
||||
if invalid:
|
||||
print("Invalid input:")
|
||||
for msg in invalid:
|
||||
print(f" {msg}")
|
||||
if not indices:
|
||||
print("No valid module numbers were entered.")
|
||||
return []
|
||||
selected = [dist_files[i] for i in indices]
|
||||
return selected
|
||||
|
||||
def backup_file(filepath):
|
||||
timestamp = datetime.now().strftime("d%d_m%m_y%Y_%Hh_%Mm_%Ss")
|
||||
bakpath = f"{filepath}({timestamp}).bak"
|
||||
try:
|
||||
shutil.copy2(filepath, bakpath)
|
||||
print(f" Backup created: {bakpath}")
|
||||
except (OSError, IOError) as e:
|
||||
print(f"[ERROR] Failed to create backup '{bakpath}': {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def parse_conf(filepath):
|
||||
# Returns a dict of key: (line, [preceding_comments])
|
||||
try:
|
||||
with open(filepath, encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
except (OSError, IOError) as e:
|
||||
print(f"[ERROR] Failed to read config file '{filepath}': {e}")
|
||||
return None
|
||||
conf = {}
|
||||
comments = []
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
comments.append(line)
|
||||
continue
|
||||
if stripped.startswith("[") and stripped.endswith("]"):
|
||||
# Ignore [headers of configs]
|
||||
comments.clear()
|
||||
continue
|
||||
if stripped.count("=") == 1:
|
||||
key, value = [s.strip() for s in stripped.split("=", 1)]
|
||||
if '#' in value:
|
||||
value = value.split('#', 1)[0].rstrip()
|
||||
if key:
|
||||
conf[key] = (f"{key} = {value}\n", comments.copy())
|
||||
comments.clear()
|
||||
continue
|
||||
return conf
|
||||
|
||||
def find_missing_keys(dist_conf, user_conf):
|
||||
missing = {}
|
||||
for key, (line, comments) in dist_conf.items():
|
||||
if key not in user_conf:
|
||||
missing[key] = (line, comments)
|
||||
return missing
|
||||
|
||||
def update_conf(dist_path, conf_path, skip_prompts=False):
|
||||
if not os.path.exists(conf_path):
|
||||
print(f" User config {conf_path} does not exist, skipping.")
|
||||
return False
|
||||
dist_conf = parse_conf(dist_path)
|
||||
user_conf = parse_conf(conf_path)
|
||||
missing = find_missing_keys(dist_conf, user_conf)
|
||||
if not missing:
|
||||
print(" No new config options to add.")
|
||||
return False
|
||||
updated = False
|
||||
lines_to_add = []
|
||||
for key, (line, comments) in missing.items():
|
||||
if skip_prompts:
|
||||
lines_to_add.append((comments, line, key))
|
||||
else:
|
||||
print("\n" + "".join(comments if comments else []) + line, end="")
|
||||
add = input(f" Add {key} to config? (y/n): ").strip().lower()
|
||||
if add in ("", "y", "yes"):
|
||||
lines_to_add.append((comments, line, key))
|
||||
else:
|
||||
print(f" Skipped {key}.")
|
||||
if lines_to_add:
|
||||
backup_file(conf_path)
|
||||
# Write using system's default line ending to avoid mixing CRLF and LF in the config file
|
||||
newline = os.linesep.encode('utf-8')
|
||||
with open(conf_path, "ab") as f:
|
||||
for comments, line, key in lines_to_add:
|
||||
if comments:
|
||||
for c in comments:
|
||||
f.write(c.rstrip('\r\n').encode('utf-8') + newline)
|
||||
f.write(line.rstrip('\r\n').encode('utf-8') + newline)
|
||||
print(f" Added {key}.")
|
||||
updated = True
|
||||
return updated
|
||||
|
||||
def update_server_config(config_name, config_dir, skip_prompts=False):
|
||||
dist_path = os.path.join(config_dir, f"{config_name}.conf.dist")
|
||||
conf_path = os.path.join(config_dir, f"{config_name}.conf")
|
||||
|
||||
if not os.path.exists(dist_path):
|
||||
print(f" Distribution config {dist_path} does not exist, skipping.")
|
||||
return False
|
||||
|
||||
print(f"\nProcessing {config_name}.conf ...")
|
||||
return update_conf(dist_path, conf_path, skip_prompts)
|
||||
|
||||
def update_modules(config_dir, selected_only=False, skip_prompts=False):
|
||||
modules_dir = os.path.join(config_dir, "modules")
|
||||
if not os.path.exists(modules_dir):
|
||||
print(f" Modules directory {modules_dir} does not exist, skipping.")
|
||||
return
|
||||
|
||||
dist_files = find_modules(modules_dir)
|
||||
if not dist_files:
|
||||
print(" No .conf.dist files found in modules folder.")
|
||||
return
|
||||
|
||||
if selected_only:
|
||||
selected = prompt_module_selection(dist_files)
|
||||
if not selected:
|
||||
print(" No modules selected.")
|
||||
return
|
||||
else:
|
||||
selected = dist_files
|
||||
|
||||
for dist_fname in selected:
|
||||
module = dist_fname[:-5] # Removes ".dist"
|
||||
conf_fname = module # e.g., mod_x.conf
|
||||
dist_path = os.path.join(modules_dir, dist_fname)
|
||||
conf_path = os.path.join(modules_dir, conf_fname)
|
||||
print(f"\nProcessing {conf_fname} ...")
|
||||
update_conf(dist_path, conf_path, skip_prompts)
|
||||
|
||||
def show_main_menu():
|
||||
print(f"\nAzerothCore Config Updater/Merger (v. {VERSION})")
|
||||
print("--------------------------")
|
||||
print("1 - Update Auth Config")
|
||||
print("2 - Update World Config")
|
||||
print("3 - Update Auth and World Configs")
|
||||
print("4 - Update All Modules Configs")
|
||||
print("5 - Update Modules (Selection) Configs")
|
||||
print("0 - Quit")
|
||||
return input("Select an option: ").strip()
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='AzerothCore Config Updater/Merger')
|
||||
parser.add_argument('config_dir', nargs='?', default='.',
|
||||
help='Path to configs directory (default: current directory)')
|
||||
parser.add_argument('target', nargs='?',
|
||||
choices=['auth', 'world', 'both', 'modules', 'modules-select'],
|
||||
help='What to update: auth, world, both, modules, modules-select')
|
||||
parser.add_argument('-y', '--yes', action='store_true',
|
||||
help='Automatically answer yes to all prompts')
|
||||
parser.add_argument('--version', action='version', version=f'%(prog)s {VERSION}')
|
||||
return parser.parse_args()
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
# If no target specified, run interactive mode
|
||||
if args.target is None:
|
||||
print(f"AzerothCore Config Updater/Merger (v. {VERSION})")
|
||||
print("==========================")
|
||||
config_dir = input("Enter the path to your configs folder (Default / Empty will use the folder where this script is located): ").strip()
|
||||
if not config_dir:
|
||||
config_dir = "."
|
||||
|
||||
if not os.path.isdir(config_dir):
|
||||
print("Provided path is not a valid directory.")
|
||||
return
|
||||
|
||||
while True:
|
||||
choice = show_main_menu()
|
||||
|
||||
if choice == "1":
|
||||
update_server_config("authserver", config_dir)
|
||||
elif choice == "2":
|
||||
update_server_config("worldserver", config_dir)
|
||||
elif choice == "3":
|
||||
update_server_config("authserver", config_dir)
|
||||
update_server_config("worldserver", config_dir)
|
||||
elif choice == "4":
|
||||
update_modules(config_dir, selected_only=False)
|
||||
elif choice == "5":
|
||||
update_modules(config_dir, selected_only=True)
|
||||
elif choice == "0":
|
||||
print("Goodbye!")
|
||||
break
|
||||
else:
|
||||
print("Invalid selection. Please try again.")
|
||||
else:
|
||||
# CLI mode
|
||||
config_dir = args.config_dir
|
||||
|
||||
if not os.path.isdir(config_dir):
|
||||
print(f"Error: Directory '{config_dir}' does not exist.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"AzerothCore Config Updater/Merger (v. {VERSION}) - CLI Mode")
|
||||
print(f"Config directory: {os.path.abspath(config_dir)}")
|
||||
print(f"Target: {args.target}")
|
||||
if args.yes:
|
||||
print("Skip prompts: Yes")
|
||||
|
||||
if args.target == 'auth':
|
||||
update_server_config("authserver", config_dir, args.yes)
|
||||
elif args.target == 'world':
|
||||
update_server_config("worldserver", config_dir, args.yes)
|
||||
elif args.target == 'both':
|
||||
update_server_config("authserver", config_dir, args.yes)
|
||||
update_server_config("worldserver", config_dir, args.yes)
|
||||
elif args.target == 'modules':
|
||||
update_modules(config_dir, selected_only=False, skip_prompts=args.yes)
|
||||
elif args.target == 'modules-select':
|
||||
if args.yes:
|
||||
print("Warning: --yes flag ignored for modules-select (requires interactive selection)")
|
||||
update_modules(config_dir, selected_only=True, skip_prompts=False)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user