Files
AzerothCore-RealmMaster/scripts/backup-merge.sh
2025-11-03 20:54:20 -05:00

1042 lines
35 KiB
Bash
Executable File

#!/bin/bash
# Merge accounts and characters from a backup into an existing ACore database
# This script handles ID remapping to avoid conflicts
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
COLOR_RED='\033[0;31m'
COLOR_GREEN='\033[0;32m'
COLOR_YELLOW='\033[1;33m'
COLOR_BLUE='\033[0;34m'
COLOR_CYAN='\033[0;36m'
COLOR_RESET='\033[0m'
log(){ printf '%b\n' "${COLOR_GREEN}$*${COLOR_RESET}"; }
info(){ printf '%b\n' "${COLOR_CYAN}$*${COLOR_RESET}"; }
warn(){ printf '%b\n' "${COLOR_YELLOW}$*${COLOR_RESET}"; }
err(){ printf '%b\n' "${COLOR_RED}$*${COLOR_RESET}"; }
fatal(){ err "$*"; exit 1; }
MYSQL_PW=""
BACKUP_DIR=""
AUTH_DB="acore_auth"
CHARACTERS_DB="acore_characters"
DRY_RUN=false
AUTO_CONFIRM=false
IMPORT_ACCOUNTS=()
IMPORT_CHARACTERS=()
IMPORT_ALL_ACCOUNTS=false
IMPORT_ALL_CHARACTERS=false
SKIP_CONFLICTS=false
EXCLUDE_BOTS=false
TEMP_DIR=""
MERGE_LOG=""
usage(){
cat <<'EOF'
Usage: ./backup-merge.sh [options]
Merges accounts and characters from a backup into the current database.
Automatically handles ID remapping to avoid conflicts.
Options:
-b, --backup-dir DIR Backup directory containing SQL dumps (required)
-p, --password PASS MySQL root password (required)
--auth-db NAME Auth database name (default: acore_auth)
--characters-db NAME Characters database name (default: acore_characters)
--account USERNAME Import specific account by username (repeatable)
--character NAME Import specific character by name (repeatable)
--all-accounts Import all accounts from backup
--all-characters Import all characters from backup
--skip-conflicts Skip accounts/characters that already exist
--exclude-bots Exclude bot accounts/characters (RNDBOT*, playerbots)
--dry-run Show what would be imported without making changes
--yes Skip confirmation prompt
-h, --help Show this help and exit
Examples:
# Dry-run to see what would be imported
./backup-merge.sh --backup-dir ../ac-backup --password azerothcore123 --all-accounts --all-characters --dry-run
# Import all accounts and characters, excluding bots
./backup-merge.sh --backup-dir ../ac-backup --password azerothcore123 --all-accounts --all-characters --exclude-bots
# Import specific accounts
./backup-merge.sh --backup-dir ../ac-backup --password azerothcore123 --account ARTIMAGE --account HAMSAMMY
# Import specific character (imports its account too)
./backup-merge.sh --backup-dir ../ac-backup --password azerothcore123 --character Artimage
EOF
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-b|--backup-dir)
[[ $# -ge 2 ]] || fatal "--backup-dir requires a value"
BACKUP_DIR="$2"
shift 2
;;
-p|--password)
[[ $# -ge 2 ]] || fatal "--password requires a value"
MYSQL_PW="$2"
shift 2
;;
--auth-db)
[[ $# -ge 2 ]] || fatal "--auth-db requires a value"
AUTH_DB="$2"
shift 2
;;
--characters-db)
[[ $# -ge 2 ]] || fatal "--characters-db requires a value"
CHARACTERS_DB="$2"
shift 2
;;
--account)
[[ $# -ge 2 ]] || fatal "--account requires a value"
IMPORT_ACCOUNTS+=("$2")
shift 2
;;
--character)
[[ $# -ge 2 ]] || fatal "--character requires a value"
IMPORT_CHARACTERS+=("$2")
shift 2
;;
--all-accounts)
IMPORT_ALL_ACCOUNTS=true
shift
;;
--all-characters)
IMPORT_ALL_CHARACTERS=true
shift
;;
--skip-conflicts)
SKIP_CONFLICTS=true
shift
;;
--exclude-bots)
EXCLUDE_BOTS=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--yes)
AUTO_CONFIRM=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
fatal "Unknown option: $1"
;;
esac
done
# Validation
[[ -n "$BACKUP_DIR" ]] || fatal "Backup directory is required (use --backup-dir)"
[[ -d "$BACKUP_DIR" ]] || fatal "Backup directory not found: $BACKUP_DIR"
[[ -n "$MYSQL_PW" ]] || fatal "MySQL password is required (use --password)"
if [[ ${#IMPORT_ACCOUNTS[@]} -eq 0 ]] && [[ ${#IMPORT_CHARACTERS[@]} -eq 0 ]] && ! $IMPORT_ALL_ACCOUNTS && ! $IMPORT_ALL_CHARACTERS; then
fatal "Must specify what to import: --account, --character, --all-accounts, or --all-characters"
fi
# Setup temp directory
TEMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TEMP_DIR"' EXIT
MERGE_LOG="$TEMP_DIR/merge.log"
touch "$MERGE_LOG"
# MySQL connection helper
mysql_exec(){
local db="$1"
docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" "$db" 2>/dev/null
}
mysql_query(){
local db="$1"
local query="$2"
docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -N -B "$db" -e "$query" 2>/dev/null
}
# Extract SQL dumps
log "Extracting backup files..."
AUTH_DUMP=""
CHARACTERS_DUMP=""
# Find auth dump
for pattern in "acore_auth.sql.gz" "auth.sql.gz" "acore_auth.sql" "auth.sql"; do
if [[ -f "$BACKUP_DIR/$pattern" ]]; then
AUTH_DUMP="$BACKUP_DIR/$pattern"
break
fi
done
# Find characters dump
for pattern in "acore_characters.sql.gz" "characters.sql.gz" "acore_characters.sql" "characters.sql"; do
if [[ -f "$BACKUP_DIR/$pattern" ]]; then
CHARACTERS_DUMP="$BACKUP_DIR/$pattern"
break
fi
done
[[ -n "$AUTH_DUMP" ]] || fatal "Auth database dump not found in $BACKUP_DIR"
[[ -n "$CHARACTERS_DUMP" ]] || fatal "Characters database dump not found in $BACKUP_DIR"
log "Found auth dump: ${AUTH_DUMP##*/}"
log "Found characters dump: ${CHARACTERS_DUMP##*/}"
# Extract dumps to temp files
info "Decompressing backup files..."
if [[ "$AUTH_DUMP" == *.gz ]]; then
zcat "$AUTH_DUMP" > "$TEMP_DIR/auth.sql"
else
cp "$AUTH_DUMP" "$TEMP_DIR/auth.sql"
fi
if [[ "$CHARACTERS_DUMP" == *.gz ]]; then
zcat "$CHARACTERS_DUMP" > "$TEMP_DIR/characters.sql"
else
cp "$CHARACTERS_DUMP" "$TEMP_DIR/characters.sql"
fi
# Load backup data into temp database
log "Creating temporary staging database..."
STAGE_AUTH_DB="merge_stage_auth_$$"
STAGE_CHARS_DB="merge_stage_chars_$$"
# Drop any existing staging databases
docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "DROP DATABASE IF EXISTS $STAGE_AUTH_DB;" 2>/dev/null || true
docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "DROP DATABASE IF EXISTS $STAGE_CHARS_DB;" 2>/dev/null || true
# Create staging databases
docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "CREATE DATABASE $STAGE_AUTH_DB;" 2>/dev/null
docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "CREATE DATABASE $STAGE_CHARS_DB;" 2>/dev/null
# Cleanup staging databases on exit
cleanup_staging(){
if [[ -n "${STAGE_AUTH_DB:-}" ]]; then
docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "DROP DATABASE IF EXISTS $STAGE_AUTH_DB;" 2>/dev/null || true
fi
if [[ -n "${STAGE_CHARS_DB:-}" ]]; then
docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "DROP DATABASE IF EXISTS $STAGE_CHARS_DB;" 2>/dev/null || true
fi
}
trap 'cleanup_staging; rm -rf "$TEMP_DIR"' EXIT
info "Loading backup into staging database..."
# Modify SQL to use staging database names
sed "s/\`acore_auth\`/\`$STAGE_AUTH_DB\`/g; s/USE \`acore_auth\`;/USE \`$STAGE_AUTH_DB\`;/g" "$TEMP_DIR/auth.sql" | \
docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" 2>/dev/null
sed "s/\`acore_characters\`/\`$STAGE_CHARS_DB\`/g; s/USE \`acore_characters\`;/USE \`$STAGE_CHARS_DB\`;/g" "$TEMP_DIR/characters.sql" | \
docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" 2>/dev/null
log "Backup loaded into staging databases"
# Analysis phase
info ""
info "═══════════════════════════════════════════════════════════"
info " ANALYSIS PHASE"
info "═══════════════════════════════════════════════════════════"
# Get current database state
CURRENT_MAX_ACCOUNT_ID=$(mysql_query "$AUTH_DB" "SELECT COALESCE(MAX(id), 0) FROM account;")
CURRENT_MAX_CHAR_GUID=$(mysql_query "$CHARACTERS_DB" "SELECT COALESCE(MAX(guid), 0) FROM characters;")
CURRENT_MAX_ITEM_GUID=$(mysql_query "$CHARACTERS_DB" "SELECT COALESCE(MAX(guid), 0) FROM item_instance;")
info "Current database state:"
info " Max account ID: $CURRENT_MAX_ACCOUNT_ID"
info " Max character GUID: $CURRENT_MAX_CHAR_GUID"
info " Max item GUID: $CURRENT_MAX_ITEM_GUID"
# Get backup database state
BACKUP_ACCOUNT_COUNT=$(mysql_query "$STAGE_AUTH_DB" "SELECT COUNT(*) FROM account;")
BACKUP_CHAR_COUNT=$(mysql_query "$STAGE_CHARS_DB" "SELECT COUNT(*) FROM characters;")
info ""
info "Backup contains:"
info " Accounts: $BACKUP_ACCOUNT_COUNT"
info " Characters: $BACKUP_CHAR_COUNT"
# Build list of accounts to import
info ""
info "Building import list..."
if $IMPORT_ALL_ACCOUNTS; then
if $EXCLUDE_BOTS; then
# Exclude bot accounts (RNDBOT%, bot%, etc.)
mysql_query "$STAGE_AUTH_DB" "
SELECT username FROM account
WHERE username NOT LIKE 'RNDBOT%'
AND username NOT LIKE 'bot%'
AND username NOT LIKE 'BOT%'
" > "$TEMP_DIR/accounts_to_import.txt"
else
mysql_query "$STAGE_AUTH_DB" "SELECT username FROM account;" > "$TEMP_DIR/accounts_to_import.txt"
fi
else
> "$TEMP_DIR/accounts_to_import.txt"
for username in "${IMPORT_ACCOUNTS[@]}"; do
echo "$username" >> "$TEMP_DIR/accounts_to_import.txt"
done
fi
# Build list of characters to import
if $IMPORT_ALL_CHARACTERS; then
if $EXCLUDE_BOTS; then
# Check if playerbots DB exists in backup
PLAYERBOTS_DB_EXISTS=$(docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "SHOW DATABASES LIKE 'merge_stage_playerbots_%';" 2>/dev/null | wc -l)
if [[ $PLAYERBOTS_DB_EXISTS -gt 1 ]]; then
# Exclude characters linked to playerbots_random_bots table
STAGE_PLAYERBOTS_DB=$(docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "SHOW DATABASES LIKE 'merge_stage_playerbots_%';" 2>/dev/null | tail -1)
mysql_query "$STAGE_CHARS_DB" "
SELECT c.name
FROM characters c
INNER JOIN account a ON c.account = a.id
LEFT JOIN $STAGE_PLAYERBOTS_DB.playerbots_random_bots pb ON c.guid = pb.bot
WHERE pb.bot IS NULL
AND a.username NOT LIKE 'RNDBOT%'
AND a.username NOT LIKE 'bot%'
AND a.username NOT LIKE 'BOT%'
" > "$TEMP_DIR/characters_to_import.txt" 2>/dev/null || {
# Fallback if playerbots DB structure is different
mysql_query "$STAGE_CHARS_DB" "
SELECT c.name
FROM characters c
INNER JOIN $STAGE_AUTH_DB.account a ON c.account = a.id
WHERE a.username NOT LIKE 'RNDBOT%'
AND a.username NOT LIKE 'bot%'
AND a.username NOT LIKE 'BOT%'
" > "$TEMP_DIR/characters_to_import.txt"
}
else
# No playerbots DB, just exclude characters from bot accounts
mysql_query "$STAGE_CHARS_DB" "
SELECT c.name
FROM characters c
INNER JOIN $STAGE_AUTH_DB.account a ON c.account = a.id
WHERE a.username NOT LIKE 'RNDBOT%'
AND a.username NOT LIKE 'bot%'
AND a.username NOT LIKE 'BOT%'
" > "$TEMP_DIR/characters_to_import.txt"
fi
else
mysql_query "$STAGE_CHARS_DB" "SELECT name FROM characters;" > "$TEMP_DIR/characters_to_import.txt"
fi
else
> "$TEMP_DIR/characters_to_import.txt"
for charname in "${IMPORT_CHARACTERS[@]}"; do
echo "$charname" >> "$TEMP_DIR/characters_to_import.txt"
done
fi
# If importing specific characters, also import their accounts
if [[ -s "$TEMP_DIR/characters_to_import.txt" ]] && ! $IMPORT_ALL_ACCOUNTS; then
while IFS= read -r charname; do
account_id=$(mysql_query "$STAGE_CHARS_DB" "SELECT account FROM characters WHERE name='$charname';" || echo "")
if [[ -n "$account_id" ]]; then
username=$(mysql_query "$STAGE_AUTH_DB" "SELECT username FROM account WHERE id=$account_id;" || echo "")
if [[ -n "$username" ]]; then
echo "$username" >> "$TEMP_DIR/accounts_to_import.txt"
fi
fi
done < "$TEMP_DIR/characters_to_import.txt"
# Remove duplicates
sort -u "$TEMP_DIR/accounts_to_import.txt" -o "$TEMP_DIR/accounts_to_import.txt"
fi
ACCOUNTS_TO_IMPORT=$(wc -l < "$TEMP_DIR/accounts_to_import.txt" | tr -d ' ')
CHARACTERS_TO_IMPORT=$(wc -l < "$TEMP_DIR/characters_to_import.txt" | tr -d ' ')
if $EXCLUDE_BOTS; then
BOT_ACCOUNTS_EXCLUDED=$((BACKUP_ACCOUNT_COUNT - ACCOUNTS_TO_IMPORT))
BOT_CHARS_EXCLUDED=$((BACKUP_CHAR_COUNT - CHARACTERS_TO_IMPORT))
info ""
info "Bot filtering enabled:"
info " Bot accounts excluded: $BOT_ACCOUNTS_EXCLUDED"
info " Bot characters excluded: $BOT_CHARS_EXCLUDED"
fi
info ""
info "Accounts to import: $ACCOUNTS_TO_IMPORT"
info "Characters to import: $CHARACTERS_TO_IMPORT"
if [[ $ACCOUNTS_TO_IMPORT -eq 0 ]] && [[ $CHARACTERS_TO_IMPORT -eq 0 ]]; then
fatal "No accounts or characters selected for import"
fi
# Conflict detection
info ""
info "Checking for conflicts..."
ACCOUNT_CONFLICTS=0
CHARACTER_CONFLICTS=0
# Check account username conflicts
> "$TEMP_DIR/account_conflicts.txt"
while IFS= read -r username; do
existing=$(mysql_query "$AUTH_DB" "SELECT COUNT(*) FROM account WHERE username='$username';" || echo "0")
if [[ "$existing" != "0" ]]; then
echo "$username" >> "$TEMP_DIR/account_conflicts.txt"
((ACCOUNT_CONFLICTS++)) || true
fi
done < "$TEMP_DIR/accounts_to_import.txt"
# Check character name conflicts
> "$TEMP_DIR/character_conflicts.txt"
while IFS= read -r charname; do
existing=$(mysql_query "$CHARACTERS_DB" "SELECT COUNT(*) FROM characters WHERE name='$charname';" || echo "0")
if [[ "$existing" != "0" ]]; then
echo "$charname" >> "$TEMP_DIR/character_conflicts.txt"
((CHARACTER_CONFLICTS++)) || true
fi
done < "$TEMP_DIR/characters_to_import.txt"
if [[ $ACCOUNT_CONFLICTS -gt 0 ]] || [[ $CHARACTER_CONFLICTS -gt 0 ]]; then
warn "Found conflicts:"
if [[ $ACCOUNT_CONFLICTS -gt 0 ]]; then
warn " $ACCOUNT_CONFLICTS account username(s) already exist:"
while IFS= read -r username; do
warn " - $username"
done < "$TEMP_DIR/account_conflicts.txt"
fi
if [[ $CHARACTER_CONFLICTS -gt 0 ]]; then
warn " $CHARACTER_CONFLICTS character name(s) already exist:"
while IFS= read -r charname; do
warn " - $charname"
done < "$TEMP_DIR/character_conflicts.txt"
fi
if $SKIP_CONFLICTS; then
warn "Skipping conflicting entries (--skip-conflicts enabled)"
# Remove conflicts from import lists
if [[ -s "$TEMP_DIR/account_conflicts.txt" ]]; then
grep -vxF -f "$TEMP_DIR/account_conflicts.txt" "$TEMP_DIR/accounts_to_import.txt" > "$TEMP_DIR/accounts_to_import_filtered.txt" || true
mv "$TEMP_DIR/accounts_to_import_filtered.txt" "$TEMP_DIR/accounts_to_import.txt"
fi
if [[ -s "$TEMP_DIR/character_conflicts.txt" ]]; then
grep -vxF -f "$TEMP_DIR/character_conflicts.txt" "$TEMP_DIR/characters_to_import.txt" > "$TEMP_DIR/characters_to_import_filtered.txt" || true
mv "$TEMP_DIR/characters_to_import_filtered.txt" "$TEMP_DIR/characters_to_import.txt"
fi
ACCOUNTS_TO_IMPORT=$(wc -l < "$TEMP_DIR/accounts_to_import.txt" | tr -d ' ')
CHARACTERS_TO_IMPORT=$(wc -l < "$TEMP_DIR/characters_to_import.txt" | tr -d ' ')
if [[ $ACCOUNTS_TO_IMPORT -eq 0 ]] && [[ $CHARACTERS_TO_IMPORT -eq 0 ]]; then
warn "All entries had conflicts. Nothing to import."
exit 0
fi
else
err ""
err "Conflicts detected. Options:"
err " 1. Use --skip-conflicts to skip existing entries"
err " 2. Manually rename conflicting accounts/characters in the backup"
err " 3. Delete conflicting entries from current database"
exit 1
fi
else
log "No conflicts detected"
fi
# Calculate ID offsets
ACCOUNT_OFFSET=$CURRENT_MAX_ACCOUNT_ID
CHAR_OFFSET=$CURRENT_MAX_CHAR_GUID
ITEM_OFFSET=$CURRENT_MAX_ITEM_GUID
info ""
info "ID remapping offsets:"
info " Account ID offset: +$ACCOUNT_OFFSET"
info " Character GUID offset: +$CHAR_OFFSET"
info " Item GUID offset: +$ITEM_OFFSET"
# Generate mapping tables
info ""
info "Generating ID mapping tables..."
# Account ID mapping
mysql_exec "$STAGE_AUTH_DB" <<EOF
CREATE TABLE IF NOT EXISTS account_id_map (
old_id INT UNSIGNED PRIMARY KEY,
new_id INT UNSIGNED,
username VARCHAR(32)
);
INSERT INTO account_id_map (old_id, new_id, username)
SELECT
id,
id + $ACCOUNT_OFFSET,
username
FROM account
WHERE username IN ($(printf "'%s'," $(cat "$TEMP_DIR/accounts_to_import.txt") | sed 's/,$//'));
EOF
# Character GUID mapping
mysql_exec "$STAGE_CHARS_DB" <<EOF
CREATE TABLE IF NOT EXISTS character_guid_map (
old_guid INT UNSIGNED PRIMARY KEY,
new_guid INT UNSIGNED,
name VARCHAR(12),
account INT UNSIGNED
);
INSERT INTO character_guid_map (old_guid, new_guid, name, account)
SELECT
guid,
guid + $CHAR_OFFSET,
name,
account
FROM characters
WHERE name IN ($(printf "'%s'," $(cat "$TEMP_DIR/characters_to_import.txt") | sed 's/,$//'));
EOF
# Item GUID mapping
mysql_exec "$STAGE_CHARS_DB" <<EOF
CREATE TABLE IF NOT EXISTS item_guid_map (
old_guid INT UNSIGNED PRIMARY KEY,
new_guid INT UNSIGNED,
owner_guid INT UNSIGNED
);
INSERT INTO item_guid_map (old_guid, new_guid, owner_guid)
SELECT
i.guid,
i.guid + $ITEM_OFFSET,
i.owner_guid
FROM item_instance i
INNER JOIN character_guid_map cm ON i.owner_guid = cm.old_guid;
EOF
log "ID mapping tables created"
# Debug: Show mapping table counts
CHAR_MAP_COUNT=$(mysql_query "$STAGE_CHARS_DB" "SELECT COUNT(*) FROM character_guid_map;")
ITEM_MAP_COUNT=$(mysql_query "$STAGE_CHARS_DB" "SELECT COUNT(*) FROM item_guid_map;")
info " Character mappings created: $CHAR_MAP_COUNT"
info " Item mappings created: $ITEM_MAP_COUNT"
# Summary
info ""
info "═══════════════════════════════════════════════════════════"
info " IMPORT SUMMARY"
info "═══════════════════════════════════════════════════════════"
mysql_query "$STAGE_AUTH_DB" "SELECT CONCAT(' ', username, ' (account id: ', old_id, ' → ', new_id, ')') FROM account_id_map;" | while read -r line; do
info "$line"
done
if [[ $CHARACTERS_TO_IMPORT -gt 0 ]]; then
info ""
info "Characters to import:"
mysql_query "$STAGE_CHARS_DB" "SELECT CONCAT(' ', name, ' (guid: ', old_guid, ' → ', new_guid, ', account: ', account, ')') FROM character_guid_map;" | while read -r line; do
info "$line"
done
fi
if $DRY_RUN; then
warn ""
warn "═══════════════════════════════════════════════════════════"
warn " DRY RUN MODE - No changes will be made"
warn "═══════════════════════════════════════════════════════════"
log ""
log "Review complete. Remove --dry-run to perform the actual import."
exit 0
fi
# Confirmation prompt
if ! $AUTO_CONFIRM; then
info ""
read -p "Proceed with import? [y/N]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
warn "Import cancelled"
exit 0
fi
fi
# Import phase
log ""
log "═══════════════════════════════════════════════════════════"
log " IMPORT PHASE"
log "═══════════════════════════════════════════════════════════"
log "Stopping world/auth services..."
docker stop ac-worldserver ac-authserver >/dev/null 2>&1 || warn "Services already stopped"
log "Starting import..."
# Import accounts
if [[ $ACCOUNTS_TO_IMPORT -gt 0 ]]; then
log "Importing $ACCOUNTS_TO_IMPORT account(s)..."
# Import main account table
info " Importing main account table..."
ACCOUNT_SQL=$(cat <<EOSQL
INSERT INTO account (id, username, salt, verifier, session_key, totp_secret, email, reg_mail,
joindate, last_ip, last_attempt_ip, failed_logins, locked, lock_country,
last_login, online, expansion, Flags, mutetime, mutereason, muteby, locale,
os, recruiter, totaltime)
SELECT
m.new_id,
a.username,
a.salt,
a.verifier,
a.session_key,
a.totp_secret,
a.email,
a.reg_mail,
a.joindate,
a.last_ip,
a.last_attempt_ip,
a.failed_logins,
a.locked,
a.lock_country,
a.last_login,
0 as online,
a.expansion,
a.Flags,
a.mutetime,
a.mutereason,
a.muteby,
a.locale,
a.os,
a.recruiter,
a.totaltime
FROM $STAGE_AUTH_DB.account a
INNER JOIN $STAGE_AUTH_DB.account_id_map m ON a.id = m.old_id;
EOSQL
)
ACCOUNT_SQL_EXPANDED=$(echo "$ACCOUNT_SQL" | sed "s/STAGE_AUTH_DB/$STAGE_AUTH_DB/g")
ACCOUNT_RESULT=$(echo "$ACCOUNT_SQL_EXPANDED" | docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" "$AUTH_DB" 2>&1 || true)
if echo "$ACCOUNT_RESULT" | grep -q "ERROR"; then
err " ✗ Account import failed:"
echo "$ACCOUNT_RESULT" | grep "ERROR" >&2
fatal "Account import failed. Check errors above."
fi
info " ✓ Main account table imported"
# Import account_access
info " Importing account_access..."
ACCESS_SQL=$(cat <<EOSQL
INSERT INTO account_access (id, gmlevel, RealmID, comment)
SELECT
m.new_id,
aa.gmlevel,
aa.RealmID,
aa.comment
FROM STAGE_AUTH_DB.account_access aa
INNER JOIN STAGE_AUTH_DB.account_id_map m ON aa.id = m.old_id;
EOSQL
)
ACCESS_SQL_EXPANDED=$(echo "$ACCESS_SQL" | sed "s/STAGE_AUTH_DB/$STAGE_AUTH_DB/g")
ACCESS_RESULT=$(echo "$ACCESS_SQL_EXPANDED" | docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" "$AUTH_DB" 2>&1 || true)
if echo "$ACCESS_RESULT" | grep -q "ERROR"; then
err " ✗ account_access import failed:"
echo "$ACCESS_RESULT" | grep "ERROR" >&2
fi
info " ✓ account_access imported"
# Import account_banned
info " Importing account_banned..."
BANNED_SQL=$(cat <<EOSQL
INSERT INTO account_banned (id, bandate, unbandate, bannedby, banreason, active)
SELECT
m.new_id,
ab.bandate,
ab.unbandate,
ab.bannedby,
ab.banreason,
ab.active
FROM STAGE_AUTH_DB.account_banned ab
INNER JOIN STAGE_AUTH_DB.account_id_map m ON ab.id = m.old_id;
EOSQL
)
BANNED_SQL_EXPANDED=$(echo "$BANNED_SQL" | sed "s/STAGE_AUTH_DB/$STAGE_AUTH_DB/g")
BANNED_RESULT=$(echo "$BANNED_SQL_EXPANDED" | docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" "$AUTH_DB" 2>&1 || true)
if echo "$BANNED_RESULT" | grep -q "ERROR"; then
err " ✗ account_banned import failed:"
echo "$BANNED_RESULT" | grep "ERROR" >&2
fi
info " ✓ account_banned imported"
# Import account_muted
info " Importing account_muted..."
MUTED_SQL=$(cat <<EOSQL
INSERT INTO account_muted (guid, mutedate, mutetime, mutedby, mutereason)
SELECT
m.new_id,
am.mutedate,
am.mutetime,
am.mutedby,
am.mutereason
FROM STAGE_AUTH_DB.account_muted am
INNER JOIN STAGE_AUTH_DB.account_id_map m ON am.guid = m.old_id;
EOSQL
)
MUTED_SQL_EXPANDED=$(echo "$MUTED_SQL" | sed "s/STAGE_AUTH_DB/$STAGE_AUTH_DB/g")
MUTED_RESULT=$(echo "$MUTED_SQL_EXPANDED" | docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" "$AUTH_DB" 2>&1 || true)
if echo "$MUTED_RESULT" | grep -q "ERROR"; then
err " ✗ account_muted import failed:"
echo "$MUTED_RESULT" | grep "ERROR" >&2
fi
info " ✓ account_muted imported"
log "✓ Accounts imported successfully"
fi
# Import characters
if [[ $CHARACTERS_TO_IMPORT -gt 0 ]]; then
log "Importing $CHARACTERS_TO_IMPORT character(s) and their data..."
# Import account_data (from characters DB, linked to account)
cat <<EOSQL | sed "s/STAGE_AUTH_DB/$STAGE_AUTH_DB/g; s/STAGE_CHARS_DB/$STAGE_CHARS_DB/g" | mysql_exec "$CHARACTERS_DB"
INSERT INTO account_data (accountId, type, time, data)
SELECT
m.new_id,
ad.type,
ad.time,
ad.data
FROM $STAGE_CHARS_DB.account_data ad
INNER JOIN $STAGE_CHARS_DB.character_guid_map cm ON ad.accountId = cm.account
INNER JOIN $STAGE_AUTH_DB.account_id_map m ON ad.accountId = m.old_id
ON DUPLICATE KEY UPDATE time=VALUES(time), data=VALUES(data);
EOSQL
# Import account_tutorial
cat <<EOSQL | sed "s/STAGE_AUTH_DB/$STAGE_AUTH_DB/g; s/STAGE_CHARS_DB/$STAGE_CHARS_DB/g" | mysql_exec "$CHARACTERS_DB"
INSERT INTO account_tutorial (accountId, tut0, tut1, tut2, tut3, tut4, tut5, tut6, tut7)
SELECT
m.new_id,
at.tut0,
at.tut1,
at.tut2,
at.tut3,
at.tut4,
at.tut5,
at.tut6,
at.tut7
FROM $STAGE_CHARS_DB.account_tutorial at
INNER JOIN $STAGE_CHARS_DB.character_guid_map cm ON at.accountId = cm.account
INNER JOIN $STAGE_AUTH_DB.account_id_map m ON at.accountId = m.old_id
ON DUPLICATE KEY UPDATE tut0=VALUES(tut0), tut1=VALUES(tut1), tut2=VALUES(tut2),
tut3=VALUES(tut3), tut4=VALUES(tut4), tut5=VALUES(tut5),
tut6=VALUES(tut6), tut7=VALUES(tut7);
EOSQL
# Import main characters table
CHAR_SQL=$(cat <<EOSQL
INSERT INTO characters (guid, account, name, race, class, gender, level, xp, money, skin, face,
hairStyle, hairColor, facialStyle, bankSlots, restState, playerFlags,
position_x, position_y, position_z, map, instance_id, instance_mode_mask,
orientation, taximask, online, cinematic, totaltime, leveltime, logout_time,
is_logout_resting, rest_bonus, resettalents_cost, resettalents_time,
trans_x, trans_y, trans_z, trans_o, transguid, extra_flags, stable_slots,
at_login, zone, death_expire_time, taxi_path, arenaPoints, totalHonorPoints,
todayHonorPoints, yesterdayHonorPoints, totalKills, todayKills, yesterdayKills,
chosenTitle, knownCurrencies, watchedFaction, drunk, health, power1, power2,
power3, power4, power5, power6, power7, latency, talentGroupsCount,
activeTalentGroup, exploredZones, equipmentCache, ammoId, knownTitles,
actionBars, grantableLevels, \`order\`, creation_date, deleteInfos_Account,
deleteInfos_Name, deleteDate, innTriggerId, extraBonusTalentCount)
SELECT
cm.new_guid,
am.new_id,
c.name,
c.race,
c.class,
c.gender,
c.level,
c.xp,
c.money,
c.skin,
c.face,
c.hairStyle,
c.hairColor,
c.facialStyle,
c.bankSlots,
c.restState,
c.playerFlags,
c.position_x,
c.position_y,
c.position_z,
c.map,
c.instance_id,
c.instance_mode_mask,
c.orientation,
c.taximask,
0 as online,
c.cinematic,
c.totaltime,
c.leveltime,
c.logout_time,
c.is_logout_resting,
c.rest_bonus,
c.resettalents_cost,
c.resettalents_time,
c.trans_x,
c.trans_y,
c.trans_z,
c.trans_o,
c.transguid,
c.extra_flags,
c.stable_slots,
c.at_login,
c.zone,
c.death_expire_time,
c.taxi_path,
c.arenaPoints,
c.totalHonorPoints,
c.todayHonorPoints,
c.yesterdayHonorPoints,
c.totalKills,
c.todayKills,
c.yesterdayKills,
c.chosenTitle,
c.knownCurrencies,
c.watchedFaction,
c.drunk,
c.health,
c.power1,
c.power2,
c.power3,
c.power4,
c.power5,
c.power6,
c.power7,
c.latency,
c.talentGroupsCount,
c.activeTalentGroup,
c.exploredZones,
c.equipmentCache,
c.ammoId,
c.knownTitles,
c.actionBars,
c.grantableLevels,
c.\`order\`,
c.creation_date,
c.deleteInfos_Account,
c.deleteInfos_Name,
c.deleteDate,
c.innTriggerId,
c.extraBonusTalentCount
FROM $STAGE_CHARS_DB.characters c
INNER JOIN $STAGE_CHARS_DB.character_guid_map cm ON c.guid = cm.old_guid
INNER JOIN $STAGE_AUTH_DB.account_id_map am ON c.account = am.old_id;
EOSQL
)
CHAR_SQL_EXPANDED=$(echo "$CHAR_SQL" | sed "s/STAGE_AUTH_DB/$STAGE_AUTH_DB/g; s/STAGE_CHARS_DB/$STAGE_CHARS_DB/g")
CHAR_RESULT=$(echo "$CHAR_SQL_EXPANDED" | docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" "$CHARACTERS_DB" 2>&1 | tee /tmp/char-import-result.log)
if echo "$CHAR_RESULT" | grep -q "ERROR"; then
err "✗ Character import failed with errors:"
echo "$CHAR_RESULT" >&2
fatal "Character import SQL failed. See /tmp/char-import-result.log for details."
fi
CHARS_IMPORTED=$(mysql_query "$CHARACTERS_DB" "SELECT COUNT(*) FROM characters WHERE account IN (101, 102);")
info " Characters imported: $CHARS_IMPORTED"
log "✓ Main character data imported"
# Import items
log "Importing character items..."
ITEM_SQL=$(cat <<EOSQL
INSERT INTO item_instance (guid, itemEntry, owner_guid, creatorGuid, giftCreatorGuid, count,
duration, charges, flags, enchantments, randomPropertyId, durability,
playedTime, text)
SELECT
im.new_guid,
ii.itemEntry,
cm.new_guid,
ii.creatorGuid,
ii.giftCreatorGuid,
ii.count,
ii.duration,
ii.charges,
ii.flags,
ii.enchantments,
ii.randomPropertyId,
ii.durability,
ii.playedTime,
ii.text
FROM $STAGE_CHARS_DB.item_instance ii
INNER JOIN $STAGE_CHARS_DB.item_guid_map im ON ii.guid = im.old_guid
INNER JOIN $STAGE_CHARS_DB.character_guid_map cm ON ii.owner_guid = cm.old_guid;
EOSQL
)
ITEM_SQL_EXPANDED=$(echo "$ITEM_SQL" | sed "s/STAGE_CHARS_DB/$STAGE_CHARS_DB/g")
ITEM_COUNT=$(echo "$ITEM_SQL_EXPANDED" | docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" "$CHARACTERS_DB" 2>&1 | tee /dev/stderr | grep -c "ERROR" || echo "0")
if [[ "$ITEM_COUNT" != "0" ]]; then
warn " Warning: Errors occurred during item_instance import"
fi
INV_SQL=$(cat <<EOSQL
INSERT INTO character_inventory (guid, bag, slot, item)
SELECT
cm.new_guid,
ci.bag,
ci.slot,
im.new_guid
FROM $STAGE_CHARS_DB.character_inventory ci
INNER JOIN $STAGE_CHARS_DB.character_guid_map cm ON ci.guid = cm.old_guid
INNER JOIN $STAGE_CHARS_DB.item_guid_map im ON ci.item = im.old_guid;
EOSQL
)
INV_SQL_EXPANDED=$(echo "$INV_SQL" | sed "s/STAGE_CHARS_DB/$STAGE_CHARS_DB/g")
INV_COUNT=$(echo "$INV_SQL_EXPANDED" | docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" "$CHARACTERS_DB" 2>&1 | tee /dev/stderr | grep -c "ERROR" || echo "0")
if [[ "$INV_COUNT" != "0" ]]; then
warn " Warning: Errors occurred during character_inventory import"
fi
# Report counts
ITEMS_IMPORTED=$(mysql_query "$CHARACTERS_DB" "SELECT COUNT(*) FROM item_instance WHERE owner_guid IN (SELECT new_guid FROM $STAGE_CHARS_DB.character_guid_map);")
INV_IMPORTED=$(mysql_query "$CHARACTERS_DB" "SELECT COUNT(*) FROM character_inventory WHERE guid IN (SELECT new_guid FROM $STAGE_CHARS_DB.character_guid_map);")
info " Items imported: $ITEMS_IMPORTED"
info " Inventory slots imported: $INV_IMPORTED"
log "✓ Items imported"
# Import character sub-tables
log "Importing character progression data..."
# List of character tables to import (guid-based)
CHAR_TABLES=(
"character_account_data"
"character_achievement"
"character_achievement_progress"
"character_action"
"character_arena_stats"
"character_aura"
"character_banned"
"character_battleground_random"
"character_brew_of_the_month"
"character_declinedname"
"character_entry_point"
"character_equipmentsets"
"character_gifts"
"character_glyphs"
"character_homebind"
"character_instance"
"character_pet"
"character_pet_declinedname"
"character_queststatus"
"character_queststatus_daily"
"character_queststatus_monthly"
"character_queststatus_rewarded"
"character_queststatus_seasonal"
"character_queststatus_weekly"
"character_reputation"
"character_settings"
"character_skills"
"character_social"
"character_spell"
"character_spell_cooldown"
"character_stats"
"character_talent"
"character_void_storage"
)
for table in "${CHAR_TABLES[@]}"; do
# Check if table exists in staging DB
table_exists=$(mysql_query "$STAGE_CHARS_DB" "SHOW TABLES LIKE '$table';" || echo "")
if [[ -z "$table_exists" ]]; then
continue
fi
# Count rows for this character in the table
row_count=$(mysql_query "$STAGE_CHARS_DB" "
SELECT COUNT(*)
FROM $table t
INNER JOIN character_guid_map cm ON t.guid = cm.old_guid
" 2>/dev/null || echo "0")
if [[ "$row_count" == "0" ]]; then
continue
fi
# Get all columns except guid
columns=$(mysql_query "$STAGE_CHARS_DB" "
SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY ORDINAL_POSITION SEPARATOR ', ')
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = '$STAGE_CHARS_DB' AND TABLE_NAME = '$table'
")
# Build select list with guid remapping
select_list=$(echo "$columns" | sed 's/\bguid\b/cm.new_guid/g')
# Import with guid remapping
PROG_SQL="INSERT IGNORE INTO $table ($columns) SELECT $select_list FROM $STAGE_CHARS_DB.$table t INNER JOIN $STAGE_CHARS_DB.character_guid_map cm ON t.guid = cm.old_guid;"
PROG_SQL_EXPANDED=$(echo "$PROG_SQL" | sed "s/STAGE_CHARS_DB/$STAGE_CHARS_DB/g")
PROG_RESULT=$(echo "$PROG_SQL_EXPANDED" | docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" "$CHARACTERS_DB" 2>&1)
if echo "$PROG_RESULT" | grep -q "ERROR"; then
warn " Warning: Errors importing $table:"
echo "$PROG_RESULT" | grep "ERROR" >&2
else
ROWS_IMPORTED=$(mysql_query "$CHARACTERS_DB" "SELECT COUNT(*) FROM $table WHERE guid IN (SELECT new_guid FROM $STAGE_CHARS_DB.character_guid_map);")
info " $table: $ROWS_IMPORTED rows"
fi
done
log "✓ Character progression data imported"
fi
# Restart services
log ""
log "Restarting services..."
docker restart ac-authserver ac-worldserver >/dev/null 2>&1
log "Waiting for services to initialize..."
sleep 5
for i in {1..30}; do
if docker exec ac-worldserver pgrep worldserver >/dev/null 2>&1 && docker exec ac-authserver pgrep authserver >/dev/null 2>&1; then
log "✓ Services running"
break
fi
if [ $i -eq 30 ]; then
warn "Services took longer than expected to start"
fi
sleep 2
done
# Final report
log ""
log "═══════════════════════════════════════════════════════════"
log " IMPORT COMPLETE"
log "═══════════════════════════════════════════════════════════"
if [[ $ACCOUNTS_TO_IMPORT -gt 0 ]]; then
log ""
log "Imported accounts:"
while IFS= read -r username; do
new_id=$(mysql_query "$AUTH_DB" "SELECT id FROM account WHERE username='$username';" || echo "?")
log "$username (account id: $new_id)"
done < "$TEMP_DIR/accounts_to_import.txt"
fi
if [[ $CHARACTERS_TO_IMPORT -gt 0 ]]; then
log ""
log "Imported characters:"
while IFS= read -r charname; do
new_guid=$(mysql_query "$CHARACTERS_DB" "SELECT guid FROM characters WHERE name='$charname';" || echo "?")
log "$charname (guid: $new_guid)"
done < "$TEMP_DIR/characters_to_import.txt"
fi
log ""
log "Merge complete! All accounts and characters have been imported."
log "Players can now log in with their restored accounts."