# ──────────────────────────────────────────
# NEW_CDN_prepatch_sync —— H2-EROLABS client patch 發布到「騰訊雲 COS bucket + CDN」
#
# 這是 NEW_CDN_prepatch_sync（AWS CloudFront 共用函式）的 COS 版分身：
#   - 不動原 AWS 函式，也不動既有前置函式（rename_*/norename_*）。
#   - 全程在 TEST 機本地跑：本機鏡像目錄裡建版本目錄 + difflist 比對 + 產清單，
#     再 coscli 推 COS、cdn.php 刷新/預熱。
#   - CTRL（prepatch source）退場；改成「把原 CTRL 的 LIVE 目錄搬到 TEST 本機」。
#
# 【定案：只換目錄版號、檔名固定】（對齊 G2H bucket 觀察）
#   - 版號只在目錄：NEWSYSTEM.<版號>/；目錄底下的檔名「固定、不帶版號尾綴」。
#   - cache-busting 靠「換目錄」（每版全新 URL 前綴，繞過邊緣/中間/客戶端快取）。
#   - 前提：setup.txt 的版號 key 由 NFSExtension 改名為 NFSExtensionDir，
#     client 找不到舊 NFSExtension → 不再拼檔名尾綴，改用「固定檔名 + default 指向
#     版號目錄」定位（與 G2H 一致）。版號仍遞增，但只拿來當「目錄版號」用。
#     ← key 改名會影響整個 client 端，請 review 確認。
#
# 本機鏡像目錄 LIVE_DIR（= 原 CTRL 的 ~/www/CDN/${GAME_TYPE}-LIVE/ 搬到 TEST）：
#   ~/www/${GAME_TYPE}_PREVPUB/
#   ├── NEWSYSTEM.<舊版號>/      ← 線上當前版（difflist 舊基準；穩態只留這「最後一份」）
#   ├── NEWSYSTEM.<新版號>/      ← 本次發布新建（difflist 新邊 + coscli 上傳來源）
#   └── patch/h2_erolabs.txt     ← 入口指標（default= 指向最新版號目錄）
#
# 六個分支分兩條路徑（用 ARG2 分流）：
#   ┌ 換版號組 CLIENT / TRANSLATE / FORCESYNC（換目錄，NEW≠OLD）
#   │   前置 rename_*（已改為不加尾綴，檔名天然固定）→ 建新版號目錄 →
#   │   difflist 比「NEWSYSTEM.新 ↔ NEWSYSTEM.舊」(原生、檔名固定天然配對) →
#   │   coscli sync 新版號目錄 → 改入口 txt 換目錄版號 + cp → purge 入口 txt → push 增量
#   └ norename 組 SETUP / NOTICE / MAINTENANCE（不換目錄，NEW==OLD）
#       前置 norename_*（本來就固定檔名）→ difflist 比「SYNCTOLIVE ↔ 現有版號目錄」(覆蓋前) →
#       rsync 覆蓋現有版號目錄 → coscli sync(CRC64 只傳變動檔) →
#       purge 那幾個固定 URL（URL 不變、邊緣有舊快取）→ push
#
# difflist：本版「不換檔名」→ 兩路徑都用原生比對，不需 --strip-*。
# ──────────────────────────────────────────

# build_always_list <host> —— 輸出該域名「常用」預熱 URL（preheat_always.conf；每次必熱基線）
#   prefix 根 glob、<VER> 代換版號；host 由參數帶入 → 同份清單套到不同域名。
build_always_list () {
    local HOST="$1"
    [ -f "${PREHEAT_ALWAYS_CONF}" ] || return 0
    (
        cd "${LIVE_DIR}" 2>/dev/null || exit 0
        shopt -s nullglob
        while IFS= read -r PAT; do
            case "$PAT" in ''|\#*) continue ;; esac
            PAT="${PAT//<VER>/${DIRECTORY_NEWNUM}}"
            REL="${PAT#/}"
            for F in $REL; do
                [ -f "$F" ] && echo "http://${HOST}/${COS_PREFIX}/${F}"
            done
        done < "${PREHEAT_ALWAYS_CONF}"
    )
}

# build_full_urls —— 產出「新版號目錄全檔」URL 基底清單（以主域名為基底，存進 FULL_URLS）。
#   作法：difflist.php 比「新版號目錄 ↔ 空目錄」→ 全檔計新增 → push 清單即全檔；
#   rawurlencode 逐段編碼與 diff 路徑完全一致（同一支 difflist），避免自刻編碼對不齊。
#   懶生成：FULL_URLS 已存在就跳過 → 多個 full 域名整支只算一次。DRYRUN 下照常真跑（純本地）。
build_full_urls () {
    [ -s "${FULL_URLS}" ] && return 0
    [ -d "${NEW_VERDIR}" ] || return 0
    local EMPTY_DIR FULL_OUT
    EMPTY_DIR=$(mktemp -d)
    FULL_OUT=$(mktemp -d)
    php "${DIFFLIST_PHP}" "${NEW_VERDIR}" "${EMPTY_DIR}" "${BASE_URL}" --out-dir "${FULL_OUT}" >/dev/null 2>&1
    [ -s "${FULL_OUT}/push_urls.txt" ] && grep -E '^https?://' "${FULL_OUT}/push_urls.txt" > "${FULL_URLS}"
    rm -rf "${EMPTY_DIR}" "${FULL_OUT}"
}

# _cdn_preheat_tencent —— 讀 cdn_domains.conf，逐「可驅動(provider=tencent)」域名組預熱清單並 push。
#   首發（IS_RENAME 且無舊版號）用 first_preheat 欄；其餘用 recurring_preheat 欄。
#     full = 新版號目錄全檔 + 常用（即使非首發也全推；可在 conf 把 recurring_preheat 設 full）；
#     always = 只常用；diff = difflist 差異 + 常用；none = 跳過。
#   diff 來源 = difflist 產的 PUSH_URLS；full 來源 = build_full_urls（difflist 比空目錄、編碼一致）；
#   兩者皆以主域名 CLIENT_PATCH_DOMAIN_NAME 為基底 → sed 換 host 套到各域名。
#   每域名各帶自己的 area（mainland/overseas）給 cdn.php。
_cdn_preheat_tencent () {
    local FIRST_PUBLISH=0
    [ "${IS_RENAME}" -eq 1 ] && [ -z "${DIRECTORY_OLDNUM}" ] && FIRST_PUBLISH=1
    [ -f "${CDN_DOMAINS_CONF}" ] || { echo "[ preheat ] no cdn_domains.conf => skip"; return 0; }
    # 預熱前抓一次 UrlPush 配額（read-only）；每行「<area> <available>」。共用 g2h key、他專案可能同時在耗 →
    #   此為 best-effort 預檢：僅示警不阻斷（抓不到/解析失敗則靜默跳過檢查）。每域名 push 前比對自己 area 的剩餘。
    local PUSH_QUOTA
    PUSH_QUOTA=$(php "${CDN_PHP}" tencent quota 2>/dev/null | awk '$1=="UrlPush"{print $2" "$3}')
    if [ -n "${PUSH_QUOTA}" ]; then
        echo "[ preheat ] UrlPush 配額（預熱前）：$(echo "${PUSH_QUOTA}" | awk '{printf "%s=%s ", $1, $2}')"
    else
        echo "[ preheat ] UrlPush 配額（預熱前）：讀取失敗（best-effort，仍續推）"
    fi
    while read -r D_HOST D_PROVIDER D_AREA D_FIRST D_RECUR D_PURGE <&3; do
        case "$D_HOST" in ''|\#*) continue ;; esac
        [ "$D_PROVIDER" = "tencent" ] || { echo "[ preheat ] ${D_HOST}: provider=${D_PROVIDER} => skip (not drivable)"; continue; }
        local MODE
        if [ "${FIRST_PUBLISH}" -eq 1 ]; then MODE="$D_FIRST"; else MODE="$D_RECUR"; fi
        local DOM_PUSH="${OUT_DIR}/push_${D_HOST}.txt"
        local SEDHOST="s|http://${CLIENT_PATCH_DOMAIN_NAME}/|http://${D_HOST}/|g"
        local INTENDED="${MODE}" EFF_MODE="${MODE}"
        # 先擋掉不可驅動模式（none / 未知值）
        case "$MODE" in
            full|diff|always) : ;;
            none) echo "[ preheat ] ${D_HOST}: mode=none => skip"; continue ;;
            *)    echo "[ preheat ] ${D_HOST}: mode='${MODE}' unknown => skip (default: no preheat)"; continue ;;
        esac
        # always-list（最關鍵、最小，永遠保留到最後一階；host 已由 build_always_list 代換）
        local F_AL="${OUT_DIR}/push_${D_HOST}.al.txt"
        build_always_list "${D_HOST}" > "${F_AL}"; sort -u "${F_AL}" -o "${F_AL}"
        local AVAIL QKNOWN=0
        AVAIL=$(echo "${PUSH_QUOTA}" | awk -v a="${D_AREA}" '$1==a{print $2}')
        [ -n "${AVAIL}" ] && [[ "${AVAIL}" =~ ^[0-9]+$ ]] && QKNOWN=1
        if [ "${MODE}" = "full" ]; then
            # 配額階梯：full(全檔+常用) → always+diff(變動+常用) → always(僅常用)，挑塞得下的最大集
            local F_FULL="${OUT_DIR}/push_${D_HOST}.full.txt" F_AD="${OUT_DIR}/push_${D_HOST}.ad.txt"
            build_full_urls
            : > "${F_FULL}"
            [ -s "${FULL_URLS}" ] && sed "${SEDHOST}" "${FULL_URLS}" | grep -E '^https?://' >> "${F_FULL}"
            cat "${F_AL}" >> "${F_FULL}"; sort -u "${F_FULL}" -o "${F_FULL}"
            : > "${F_AD}"
            [ -s "${PUSH_URLS}" ] && sed "${SEDHOST}" "${PUSH_URLS}" | grep -E '^https?://' >> "${F_AD}"
            cat "${F_AL}" >> "${F_AD}"; sort -u "${F_AD}" -o "${F_AD}"
            local NF NAD
            NF=$(wc -l < "${F_FULL}"); NAD=$(wc -l < "${F_AD}")
            if [ "${QKNOWN}" -eq 0 ]; then
                cp "${F_FULL}" "${DOM_PUSH}"; EFF_MODE="full"
                echo "[ preheat ] ${D_HOST}: UrlPush ${D_AREA} 配額未知 => 照推 full ${NF}（best-effort）"
            elif [ "${NF}" -le "${AVAIL}" ]; then
                cp "${F_FULL}" "${DOM_PUSH}"; EFF_MODE="full"
            elif [ "${NAD}" -le "${AVAIL}" ]; then
                cp "${F_AD}" "${DOM_PUSH}"; EFF_MODE="always+diff"
            else
                cp "${F_AL}" "${DOM_PUSH}"; EFF_MODE="always"
            fi
        else
            # diff / always：直接組設定模式清單（diff=變動檔+常用、always=只常用）
            : > "${DOM_PUSH}"
            [ "${MODE}" = "diff" ] && [ -s "${PUSH_URLS}" ] && sed "${SEDHOST}" "${PUSH_URLS}" | grep -E '^https?://' >> "${DOM_PUSH}"
            cat "${F_AL}" >> "${DOM_PUSH}"; sort -u "${DOM_PUSH}" -o "${DOM_PUSH}"
        fi
        # 底線：選定清單仍 > 剩餘（連 always 都塞不下、或 diff/always 模式本身超量）→ 截斷到剩餘配額
        local NURLS TRUNC=0
        NURLS=$([ -s "${DOM_PUSH}" ] && wc -l < "${DOM_PUSH}" || echo 0)
        if [ "${QKNOWN}" -eq 1 ] && [ "${NURLS}" -gt "${AVAIL}" ]; then
            head -n "${AVAIL}" "${DOM_PUSH}" > "${DOM_PUSH}.cut" && mv "${DOM_PUSH}.cut" "${DOM_PUSH}"
            TRUNC=1; NURLS="${AVAIL}"
        fi
        local TAG=""; [ "${TRUNC}" -eq 1 ] && TAG="+截斷"
        echo "[ preheat ] ${D_HOST}: 設定=${INTENDED} 實推=${EFF_MODE}${TAG} area=${D_AREA} => ${NURLS} urls (UrlPush ${D_AREA} avail=${AVAIL:-?})"
        # 偏離設定就通知（降階或截斷＝預設作法沒成功）；逐域名各發一則，DRYRUN 只印不發
        if [ "${EFF_MODE}" != "${INTENDED}" ] || [ "${TRUNC}" -eq 1 ]; then
            local DMSG="[WARN] Client Patch 預熱降階 ${C_PART}：${D_HOST} ${D_AREA} 設定 ${INTENDED} 但剩餘 ${AVAIL:-?} => 實推 ${EFF_MODE}${TAG} ${NURLS} 筆（共用配額不足）"
            echo "[ preheat ] ${DMSG}"
            if [ "${DRYRUN:-0}" = "1" ]; then
                echo "[ DRYRUN ] would notify: ${DMSG}"
            else
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "${DMSG}" minute 10
            fi
        fi
        [ -s "${DOM_PUSH}" ] && run_or_echo cdn_run "${D_PROVIDER}" push-urls --area "${D_AREA}" --file "${DOM_PUSH}"
        # bootstrap 檔境外暖邊緣：非 overseas 的 global 域名(如 xjep)額外把 always-list 推 overseas，
        #   讓境外 client 第一抓即命中熱邊緣，不吃冷邊緣回源超時(version/version_extra 曾 eMsg_Network_Error)。
        #   always-list 僅數~十條，遠低於 overseas UrlPush 1000/日；run_or_echo 自帶 DRYRUN。
        if [ "${D_AREA}" != "overseas" ] && [ -s "${F_AL}" ]; then
            echo "[ preheat ] ${D_HOST}: bootstrap 境外暖邊緣 => $(wc -l < "${F_AL}") urls (area=overseas)"
            run_or_echo cdn_run "${D_PROVIDER}" push-urls --area overseas --file "${F_AL}"
        fi
    done 3< "${CDN_DOMAINS_CONF}"
    # 預熱後再抓一次（read-only），對照可看本輪實耗多少；DRYRUN 沒實推、配額不變
    local PUSH_QUOTA_AFTER
    PUSH_QUOTA_AFTER=$(php "${CDN_PHP}" tencent quota 2>/dev/null | awk '$1=="UrlPush"{print $2" "$3}')
    if [ -n "${PUSH_QUOTA_AFTER}" ]; then
        echo "[ preheat ] UrlPush 配額（預熱後）：$(echo "${PUSH_QUOTA_AFTER}" | awk '{printf "%s=%s ", $1, $2}')"
    else
        echo "[ preheat ] UrlPush 配額（預熱後）：讀取失敗"
    fi
}

# _cdn_preheat_byteplus —— byteplus 無 area 概念（quota rows area=''），不走騰訊的 area 配額階梯。
#   讀 cdn_domains.conf 取 provider=byteplus 域名，依 first/recurring 模式(full/diff/always)組清單並 push-urls。
#   配額 best-effort：cdn.php byteplus quota 取 UrlPush 單一剩餘值（無 area）；清單超量則截斷。push-urls 不帶 --area。
#   ⚠️ 盲串草稿（未對真實 BytePlus API 驗證）：簽約後須核對 quota 欄位名/單位、以及 push-urls 是否如預期忽略 area。
_cdn_preheat_byteplus () {
    local FIRST_PUBLISH=0
    [ "${IS_RENAME}" -eq 1 ] && [ -z "${DIRECTORY_OLDNUM}" ] && FIRST_PUBLISH=1
    [ -f "${CDN_DOMAINS_CONF}" ] || { echo "[ preheat ] no cdn_domains.conf => skip"; return 0; }
    local PUSH_AVAIL
    PUSH_AVAIL=$(php "${CDN_PHP}" byteplus quota 2>/dev/null | awk '$1=="UrlPush"{print $2}')
    [[ "${PUSH_AVAIL}" =~ ^[0-9]+$ ]] || PUSH_AVAIL=""
    echo "[ preheat ] byteplus UrlPush 配額（預熱前）：${PUSH_AVAIL:-讀取失敗(best-effort，仍續推)}"
    while read -r D_HOST D_PROVIDER D_AREA D_FIRST D_RECUR D_PURGE <&3; do
        case "$D_HOST" in ''|\#*) continue ;; esac
        [ "$D_PROVIDER" = "byteplus" ] || { echo "[ preheat ] ${D_HOST}: provider=${D_PROVIDER} => skip (not byteplus)"; continue; }
        local MODE; if [ "${FIRST_PUBLISH}" -eq 1 ]; then MODE="$D_FIRST"; else MODE="$D_RECUR"; fi
        case "$MODE" in
            full|diff|always) : ;;
            none) echo "[ preheat ] ${D_HOST}: mode=none => skip"; continue ;;
            *)    echo "[ preheat ] ${D_HOST}: mode='${MODE}' unknown => skip (default: no preheat)"; continue ;;
        esac
        local DOM_PUSH="${OUT_DIR}/push_${D_HOST}.txt"
        local F_AL="${OUT_DIR}/push_${D_HOST}.al.txt"
        local SEDHOST="s|http://${CLIENT_PATCH_DOMAIN_NAME}/|http://${D_HOST}/|g"
        build_always_list "${D_HOST}" > "${F_AL}"; sort -u "${F_AL}" -o "${F_AL}"
        : > "${DOM_PUSH}"
        if [ "${MODE}" = "full" ]; then
            build_full_urls
            [ -s "${FULL_URLS}" ] && sed "${SEDHOST}" "${FULL_URLS}" | grep -E '^https?://' >> "${DOM_PUSH}"
        elif [ "${MODE}" = "diff" ]; then
            [ -s "${PUSH_URLS}" ] && sed "${SEDHOST}" "${PUSH_URLS}" | grep -E '^https?://' >> "${DOM_PUSH}"
        fi
        cat "${F_AL}" >> "${DOM_PUSH}"; sort -u "${DOM_PUSH}" -o "${DOM_PUSH}"
        local NURLS TRUNC=0
        NURLS=$([ -s "${DOM_PUSH}" ] && wc -l < "${DOM_PUSH}" || echo 0)
        if [ -n "${PUSH_AVAIL}" ] && [ "${NURLS}" -gt "${PUSH_AVAIL}" ]; then
            head -n "${PUSH_AVAIL}" "${DOM_PUSH}" > "${DOM_PUSH}.cut" && mv "${DOM_PUSH}.cut" "${DOM_PUSH}"
            TRUNC=1; NURLS="${PUSH_AVAIL}"
        fi
        local TAG=""; [ "${TRUNC}" -eq 1 ] && TAG="+截斷"
        echo "[ preheat ] ${D_HOST}: 模式=${MODE}${TAG} => ${NURLS} urls (UrlPush avail=${PUSH_AVAIL:-?})"
        if [ "${TRUNC}" -eq 1 ]; then
            local DMSG="[WARN] Client Patch 預熱截斷 ${C_PART}：${D_HOST} byteplus 模式 ${MODE} 但剩餘 ${PUSH_AVAIL:-?} => 截斷 ${NURLS} 筆"
            echo "[ preheat ] ${DMSG}"
            if [ "${DRYRUN:-0}" = "1" ]; then
                echo "[ DRYRUN ] would notify: ${DMSG}"
            else
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "${DMSG}" minute 10
            fi
        fi
        [ -s "${DOM_PUSH}" ] && run_or_echo cdn_run "${D_PROVIDER}" push-urls --file "${DOM_PUSH}"
    done 3< "${CDN_DOMAINS_CONF}"
}

# purge_urls_all_domains <base_urls_file> —— 把「以主域名為基底」的 URL 清單，逐域名換 host 後 purge。
#   provider=${CDN_PROVIDER} 且 (purge_entry=yes 或 該域名=.gamerc 當前主域名) 才做；tencent 各帶自己 area。
#   主域名一律刷（不看 purge_entry），避免切 domain 後忘了改 conf；刷不到主域名時尾端會警告。
purge_urls_all_domains () {
    local SRC="$1"
    [ -s "${SRC}" ] || return 0
    [ -f "${CDN_DOMAINS_CONF}" ] || return 0
    local MAIN_PURGED=0
    while read -r D_HOST D_PROVIDER D_AREA D_FIRST D_RECUR D_PURGE <&3; do
        case "$D_HOST" in ''|\#*) continue ;; esac
        [ "$D_PROVIDER" = "$CDN_PROVIDER" ] || continue
        # 主域名(.gamerc)的入口一律刷，免得切 domain 後忘了改 conf 的 purge_entry
        [ "$D_PURGE" = "yes" ] || [ "$D_HOST" = "$CLIENT_PATCH_DOMAIN_NAME" ] || continue
        [ "$D_HOST" = "$CLIENT_PATCH_DOMAIN_NAME" ] && MAIN_PURGED=1
        local DOM_PURGE="${OUT_DIR}/purge_${D_HOST}.txt"
        sed "s|http://${CLIENT_PATCH_DOMAIN_NAME}/|http://${D_HOST}/|g" "${SRC}" > "${DOM_PURGE}"
        echo "[ purge ] ${D_HOST}: provider=${D_PROVIDER} area=${D_AREA} => $(wc -l < "${DOM_PURGE}") urls"
        # --area 僅騰訊有效（cdn.php：area 限 tencent push/purge）；byteplus 等不帶 area
        if [ "${CDN_PROVIDER}" = "tencent" ]; then
            run_or_echo cdn_run "${D_PROVIDER}" purge-urls --area "${D_AREA}" --file "${DOM_PURGE}"
        else
            run_or_echo cdn_run "${D_PROVIDER}" purge-urls --file "${DOM_PURGE}"
        fi
    done 3< "${CDN_DOMAINS_CONF}"
    [ "${MAIN_PURGED}" = "1" ] || echo "[ purge ] ⚠️ 主域名 ${CLIENT_PATCH_DOMAIN_NAME} 非 conf 內 provider=${CDN_PROVIDER} 可驅動域名，入口 txt 未刷新，請確認其快取會自行過期"
}

# cdn_run：包裝 cdn.php 呼叫，加時間戳 banner + 把 stdout/stderr tee 進持久化 log。
#   依賴 NEW_CDN 設好的：CDN_PHP / CDN_LOG。cdn.php 本身不寫檔，全靠這裡留存。
cdn_run () {
    echo "[ $(date '+%F %T %Z') ] cdn.php $*" | tee -a "${CDN_LOG}"
    php "${CDN_PHP}" "$@" 2>&1 | tee -a "${CDN_LOG}"
}

# run_or_echo：DRYRUN=1 時只印不做，包外部不可逆操作（coscli 上傳 / cdn_run 刷新預熱）。
#   rsync 建本機鏡像 + difflist + 預熱清單組裝 照常真跑 → 各域名 push 清單仍真實可檢視。
run_or_echo () {
    if [ "${DRYRUN:-0}" = 1 ]; then echo "[ DRYRUN ] would run: $*"; else "$@"; fi
}

# ── 物件儲存上傳 hook（COS 家：tencent/byteplus）─────────────────────────────────
#   共用 body 設好 NEW_VERDIR / COS_PREFIX / COS_ALIAS / DIRECTORY_* / C_PART / PROCEDURE_NAME 後呼叫；
#   靠 bash dynamic scope 取用，介面只傳會變動的 oldnum/newnum/srcdir/localfile/destkey。
#   三個 hook：servercopy（服務端增量複製+核對，rc!=0 中止發布）/ sync（目錄→版號前綴，回 rc）/ putfile（單檔，如入口 txt）。

# tencent：coscli + cos://
_cdn_storage_servercopy_tencent () {
    # <oldnum> <newnum>；rc0=可繼續（含 no-op）/rc1=中止發布
    local OLDNUM="$1" NEWNUM="$2"
    # 增量關閉 或 首發（無舊版號）→ 不做服務端複製，回 0 走後續全量 sync
    [ "${CLIENT_PATCH_COS_INCREMENTAL:-no}" = "yes" ] && [ -n "${OLDNUM}" ] || return 0
    echo "[ cos ] incremental on: server-side copy NEWSYSTEM.${OLDNUM} => NEWSYSTEM.${NEWNUM}, then sync diff"
    run_or_echo "${COSCLI}" cp -r "cos://${COS_ALIAS}/${COS_PREFIX}/NEWSYSTEM.${OLDNUM}/" "cos://${COS_ALIAS}/${COS_PREFIX}/NEWSYSTEM.${NEWNUM}/" 2>> "${RSYNC_ERROR_LOG}"
    local RC=$?   # cp 失敗就中止：新前綴不完整時絕不繼續到翻入口 txt；以 exit code 判（coscli [ERRO] 走 stdout）
    if [ "${RC}" -ne 0 ]; then
        automatic_send_messages_by_thread_tag ERROR cos_prepatch_sync "Client Patch COS 桶內複製 ${C_PART} 異常 (rc=${RC})，已中止發布" "${PROCEDURE_NAME}" minute 10
        return 1
    fi
    # 複製完整性核對：新前綴物件數須 == 舊前綴（尚未 sync，兩者應一致）；不符即中止。DRYRUN 不真複製→跳過。
    #   ⚠️ coscli ls TOTAL OBJECTS 顯示上限 10000：單版 ~3900 檔遠低於上限、可信；某版超過 10000 須改別法。
    if [ "${DRYRUN:-0}" != 1 ]; then
        local OLD_CNT NEW_CNT
        OLD_CNT=$("${COSCLI}" ls "cos://${COS_ALIAS}/${COS_PREFIX}/NEWSYSTEM.${OLDNUM}/" -r 2>/dev/null | awk -F'|' '/TOTAL OBJECTS/{gsub(/ /,"",$2);print $2}')
        NEW_CNT=$("${COSCLI}" ls "cos://${COS_ALIAS}/${COS_PREFIX}/NEWSYSTEM.${NEWNUM}/" -r 2>/dev/null | awk -F'|' '/TOTAL OBJECTS/{gsub(/ /,"",$2);print $2}')
        echo "[ cos ] server-side copy verify: old=${OLD_CNT} new=${NEW_CNT} objects"
        if [ -z "${NEW_CNT}" ] || [ "${OLD_CNT}" != "${NEW_CNT}" ]; then
            automatic_send_messages_by_thread_tag ERROR cos_prepatch_sync "Client Patch COS 桶內複製檔數不符 ${C_PART} (old=${OLD_CNT} new=${NEW_CNT})，已中止發布" "${PROCEDURE_NAME}" minute 10
            return 1
        fi
    fi
    return 0
}
_cdn_storage_sync_tencent () {
    # <srcdir> <newnum>；回 coscli sync 的 rc（body 依 rc + RSYNC_ERROR_LOG 判）
    local SRC="$1" NEWNUM="$2"
    run_or_echo "${COSCLI}" sync -r "${SRC}/" "cos://${COS_ALIAS}/${COS_PREFIX}/NEWSYSTEM.${NEWNUM}/" 2>> "${RSYNC_ERROR_LOG}"
}
_cdn_storage_putfile_tencent () {
    # <localfile> <destkey>（destkey 相對 COS_PREFIX，如 patch/xxx.txt）
    local LOCAL="$1" KEY="$2"
    run_or_echo "${COSCLI}" cp "${LOCAL}" "cos://${COS_ALIAS}/${COS_PREFIX}/${KEY}"
}

# byteplus：tosutil + tos://（⚠️ 盲串草稿，未對真實 BytePlus API 驗證）
#   NEEDS-CONFIRM：① 上傳工具(假設 tosutil，路徑 ${TOSUTIL:-$HOME/bin/tosutil}) ② bucket(假設環境變數 BYTEPLUS_TOS_BUCKET)
#     ③ tosutil 是否支援服務端複製(下方一律全量 sync 最穩，簽約後可加) ④ tos:// scheme 與 key 結構是否同 COS（沿用 COS_PREFIX）
#     ⑤ tosutil sync 子指令/遞迴旗標是否如此（coscli 用 sync -r；tosutil 可能是 cp -r -u，須核對）
_cdn_storage_servercopy_byteplus () {
    # 盲串：不做服務端複製，回 0 → 走全量 sync（最安全；簽約後再評估增量）
    return 0
}
_cdn_storage_sync_byteplus () {
    local SRC="$1" NEWNUM="$2"
    run_or_echo "${TOSUTIL:-$HOME/bin/tosutil}" sync "${SRC}/" "tos://${BYTEPLUS_TOS_BUCKET}/${COS_PREFIX}/NEWSYSTEM.${NEWNUM}/" 2>> "${RSYNC_ERROR_LOG}"
}
_cdn_storage_putfile_byteplus () {
    local LOCAL="$1" KEY="$2"
    run_or_echo "${TOSUTIL:-$HOME/bin/tosutil}" cp "${LOCAL}" "tos://${BYTEPLUS_TOS_BUCKET}/${COS_PREFIX}/${KEY}"
}

# ── dispatcher：NEW_CDN_prepatch_sync 為穩定入口，依 CDN_PROVIDER 指向各家 provider body ──
#   CDN_PROVIDER 預設 tencent（本機在跑騰訊）；發佈端永遠只呼叫 NEW_CDN_prepatch_sync。
#   tencent/byteplus 同屬「推物件儲存 + 刷新+預熱」家 → 共用 _cdn_prepatch_sync_cos（差異只在 storage/preheat hook）。
#   byteplus 為盲串草稿（hook 標 NEEDS-CONFIRM）；aws 為「兩機回源+失效」家 → 另一支獨立 body（待 N1 範本整合，暫擋板）。
NEW_CDN_prepatch_sync () {
    case "${CDN_PROVIDER:-tencent}" in
        tencent|byteplus)  _cdn_prepatch_sync_cos "$@" ;;
        aws)               _cdn_prepatch_sync_aws "$@" ;;
        *)                 exec echo "ERROR: unknown CDN_PROVIDER='${CDN_PROVIDER}'" ;;
    esac
}

# _cdn_prepatch_sync_aws —— AWS CloudFront 家獨立 body（兩機 TEST↔CTRL 回源失效型，範本 = N1-EN）。
#   與 COS 家不同：無物件儲存，rsync 推到 CTRL 檔案系統由 CloudFront 回源；NEWSYSTEM.<版號> 目錄 + symlink 切版；
#   flush.php 做失效（無預熱）。開頭照樣先 diff 比較 + Google Chat「同步開始」通知（與騰訊雲一致）。
#   [OPT] 兩處上傳段插入 hardlink seed（cp -al 上一版 + 改尾綴），rsync 只補差異；最壞退回全量、不影響正確性。
_cdn_prepatch_sync_aws () { # Maintenance -> sync client or translate data to prepatch directory and flush CDN ( New Patch System: CLIENT (nobackup) / TRANSLATE (nobackup) / SETUP (nobackup) / NOTICE (nobackup) / MAINTENANCE (nobackup) / FORCESYNC (nobackup) )
    [ "$ARG2" == "" ] && exec echo "Usage: $0 NEW_CDN_prepatch_sync <CLIENT or TRANSLATE or SETUP or FORCESYNC>"

    DATETIME="$(/bin/date +%Y%m%dT%H%M)"
    CDN_DIFF_FILE="$HOME/patch/cdn_diff_new.txt.$DATETIME"
    CDN_DIFF_FILE_TOTAL_COUNT_FILE="$HOME/patch/cdn_diff_count_new.$DATETIME"
    RSYNC_ERROR_LOG="/tmp/rsync_cdn_prepatch_sync_monitor.log"
    MACHINE_HOSTNAME="$(echo $HOST_NAME)"

    [ -f "${RSYNC_ERROR_LOG}" ] && rm -rf ${RSYNC_ERROR_LOG}

    if [ "$MACHINE_HOSTNAME" == "TEST" ]; then
        rm -rf /tmp/setup.txt.old
        cp -arf ~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/setup.txt /tmp/setup.txt.old
    elif [ "$MACHINE_HOSTNAME" == "CTRL" ]; then
        rm -rf /tmp/setup.txt.old
        cp -arf ~/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM/setup.txt /tmp/setup.txt.old
    fi

    PROCEDURE_NAME=$(ssh TEST "psql -U postgres Tools -c \"COPY (SELECT procedure_name FROM web_maintenance_procedure_mapping WHERE sub_command_name = '$ARG2') TO STDOUT;\"")

    if [ "$MACHINE_HOSTNAME" == "TEST" ]; then
        if [ "$ARG2" == "CLIENT" ]; then
            $HOME/bin/compare_platforms_for_clientpatch --action live_sync || true

            C_PART="全部"
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

            rename_client_extension
        elif [ "$ARG2" == "TRANSLATE" ]; then
            if [ "$ARG3" == "nobackup" ]; then
                C_PART="翻譯 全語系"
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

                rename_translate_extension
            else
                C_PART="翻譯 $ARG3"
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

                rename_translate_extension $ARG3
            fi
        elif [ "$ARG2" == "SETUP" ]; then
            C_PART="setup.txt"
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

            norename_setup_update
        elif [ "$ARG2" == "NOTICE" ]; then
            if [ "$ARG3" == "nobackup" ]; then
                C_PART="遊戲內公告 全語系"
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

                norename_notice_update
            else
                C_PART="遊戲內公告 $ARG3"
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

                norename_notice_update $ARG3
            fi
        elif [ "$ARG2" == "MAINTENANCE" ]; then
            if [ "$ARG3" == "nobackup" ]; then
                C_PART="維護公告 全語系"
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

                norename_maintenance_update
            else
                C_PART="維護公告 $ARG3"
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

                norename_maintenance_update $ARG3
            fi
        elif [ "$ARG2" == "FORCESYNC" ]; then
            C_PART="全部 強制重新刷新"
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

            OLD_VERSION=$(grep -E -a NFSExtension /tmp/setup.txt.old | sed 's/NFSExtension=//')
            rm -rf ~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/Android/version${OLD_VERSION}
            touch ~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/Android/version${OLD_VERSION}
            rename_client_extension
        else
            exec echo "ERROR: Wrong parameter 2 (Only accept for CLIENT or TRANSLATE or SETUP or FORCESYNC)"
        fi

        if [ ! -f "/tmp/setup.txt.old" ]; then
            ssh CTRL "if [ ! -d "~/www/CDN/${GAME_TYPE}-LIVE_previous_version/" ]; then mkdir -p ~/www/CDN/${GAME_TYPE}-LIVE_previous_version/; fi"
            if test -z $CLIENT_PATCH_NFS_START; then
                DIRECTORY_NEWNUM=$(grep -E -a 'NFSExtension' ~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/setup.txt | sed "s/NFSExtension=//;s/.//")
            else
                DIRECTORY_NEWNUM="${CLIENT_PATCH_NFS_START}"
            fi
        else
            rsync -i -avn --exclude="nfs" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/ CTRL:~/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\/NEWSYSTEM\//g' > $CDN_DIFF_FILE
            [ "$(wc -l < $CDN_DIFF_FILE)" -eq "0" ] && exec echo "ERROR: No file to flush or cannot compare with prepatch directory, please try again."

            if [ "$ARG3" == "nobackup" -o "$ARG4" == "nobackup" ]; then
                echo "INFO: No Backup prepatch" | colorize red black
            else
                DIRNAME=$(ssh CTRL "readlink -f ~/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM" | awk -F'/' '{print $NF}')
                ssh CTRL "cd ~/www/CDN/${GAME_TYPE}-LIVE_previous_version/; unlink NEWSYSTEM; ln -s ../${GAME_TYPE}-LIVE/${DIRNAME} NEWSYSTEM"
            fi

            DIRECTORY_OLDNUM=$(grep -E -a 'NFSExtension' /tmp/setup.txt.old | sed "s/NFSExtension=//;s/.//")
            DIRECTORY_NEWNUM=$(grep -E -a 'NFSExtension' ~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/setup.txt | sed "s/NFSExtension=//;s/.//")
        fi

        # [OPT] hardlink seed 上一版 + 本機批次改尾綴（perl rename），rsync 只補差異（最終內容由 --delete 保證 == SYNCTOLIVE）
        if [ -n "${DIRECTORY_OLDNUM}" ]; then
            ssh CTRL "if [ -d \"${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_OLDNUM}\" ] && [ ! -d \"${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_NEWNUM}\" ]; then cp -al ${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_OLDNUM} ${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_NEWNUM} && cd ${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_NEWNUM} && find . -type f -name '*.${DIRECTORY_OLDNUM}' -exec rename 's/\.${DIRECTORY_OLDNUM}\$/.${DIRECTORY_NEWNUM}/' {} +; fi"
        fi
        ssh CTRL "if [ ! -d \"${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_NEWNUM}\" ]; then mkdir -p ${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_NEWNUM}; fi"
        rsync -av --timeout=60 --delete --progress ~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/ CTRL:~/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_NEWNUM}/ 2> ${RSYNC_ERROR_LOG}
        if [[ -s "${RSYNC_ERROR_LOG}" ]]; then
            automatic_send_messages_by_thread_tag ERROR rsync_cdn_prepatch_sync "Client Patch Rsync ${C_PART} 同步異常" "${PROCEDURE_NAME}" minute 10
        else
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client Patch Rsync ${C_PART} 同步正常" minute 10
        fi
        if [ "$ARG2" == "CLIENT" ] || [ "$ARG2" == "TRANSLATE" ] || [ "$ARG2" == "FORCESYNC" ]; then
            ssh CTRL "if [ ! -d \"${HOME}/www/CDN/${GAME_TYPE}-LIVE/patch\" ]; then mkdir -p ${HOME}/www/CDN/${GAME_TYPE}-LIVE/patch; fi"
            for FILE in ${CLIENT_PATCH_FILENAME_LISTS}; do
                FILE_CHECK=$(ssh CTRL "cd ~/www/CDN/${GAME_TYPE}-LIVE/patch/; if [ -f \"${FILE}\" ]; then echo 1; else echo 0; fi")
                if [ "${FILE_CHECK}" -eq 0 ]; then
                    ssh CTRL "cd ~/www/CDN/${GAME_TYPE}-LIVE/patch/; echo \"default=https://${CLIENT_PATCH_DOMAIN_NAME}/NEWSYSTEM.${DIRECTORY_NEWNUM}/\" > ${FILE}"
                fi
            done

            if test -z ${DIRECTORY_OLDNUM}; then
                ssh CTRL "cd ~/www/CDN/${GAME_TYPE}-LIVE/; ln -s NEWSYSTEM.${DIRECTORY_NEWNUM} NEWSYSTEM"
            else
                ssh CTRL "cd ~/www/CDN/${GAME_TYPE}-LIVE/patch/; sed -i -r \"s|NEWSYSTEM.${DIRECTORY_OLDNUM}|NEWSYSTEM.${DIRECTORY_NEWNUM}|\" ${GAME_TYPE,,}_${COUNTRY_CODE,,}*.txt"
                ssh CTRL "cd ~/www/CDN/${GAME_TYPE}-LIVE/patch/; sed -i -r \"s|NEWSYSTEM.${DIRECTORY_OLDNUM}|NEWSYSTEM.${DIRECTORY_NEWNUM}|\" ${GAME_TYPE,,}${COUNTRY_CODE,,}*.txt"
                ssh CTRL "cd ~/www/CDN/${GAME_TYPE}-LIVE/; unlink NEWSYSTEM; ln -s NEWSYSTEM.${DIRECTORY_NEWNUM} NEWSYSTEM"
            fi
        fi

        cat $HOME/patch/flush_all.txt > $HOME/patch/tmp1_new
        php $HOME/patch/flush.php $HOME/patch/tmp1_new
        automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步完成" minute 10

    elif [ "$MACHINE_HOSTNAME" == "CTRL" ]; then
        if [ "$ARG2" == "CLIENT" ]; then
            ssh -o StrictHostKeyChecking=no TEST 'bash -l -c "$HOME/bin/compare_platforms_for_clientpatch --action live_sync"' || true

            C_PART="全部"
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

            ssh TEST "rename_client_extension"
        elif [ "$ARG2" == "TRANSLATE" ]; then
            if [ "$ARG3" == "nobackup" ]; then
                C_PART="翻譯 全語系"
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

                ssh TEST "rename_translate_extension"
            else
                C_PART="翻譯 $ARG3"
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

                ssh TEST "rename_translate_extension $ARG3"
            fi
        elif [ "$ARG2" == "SETUP" ]; then
            C_PART="setup.txt"
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

            ssh TEST "norename_setup_update"
        elif [ "$ARG2" == "NOTICE" ]; then
            if [ "$ARG3" == "nobackup" ]; then
                C_PART="遊戲內公告 全語系"
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

                ssh TEST "norename_notice_update"
            else
                C_PART="遊戲內公告 $ARG3"
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

                ssh TEST "norename_notice_update $ARG3"
            fi
        elif [ "$ARG2" == "MAINTENANCE" ]; then
            if [ "$ARG3" == "nobackup" ]; then
                C_PART="維護公告 全語系"
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

                ssh TEST "norename_maintenance_update"
            else
                C_PART="維護公告 $ARG3"
                automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

                ssh TEST "norename_maintenance_update $ARG3"
            fi
        elif [ "$ARG2" == "FORCESYNC" ]; then
            C_PART="全部 強制重新刷新"
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10

            OLD_VERSION=$(ssh TEST "grep -E -a 'NFSExtension' /tmp/setup.txt.old | sed 's/NFSExtension=//'")
            ssh TEST "rm -rf ~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/Android/version${OLD_VERSION}"
            ssh TEST "touch ~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/Android/version${OLD_VERSION}"
            ssh TEST "rename_client_extension"
        else
            exec echo "ERROR: Wrong parameter 2 (Only accept for CLIENT or TRANSLATE or SETUP or FORCESYNC)"
        fi

        if [ ! -f "/tmp/setup.txt.old" ]; then
            if [ ! -d "~/www/CDN/${GAME_TYPE}-LIVE_previous_version/" ]; then mkdir -p ~/www/CDN/${GAME_TYPE}-LIVE_previous_version/; fi
            if test -z $CLIENT_PATCH_NFS_START; then
                DIRECTORY_NEWNUM=$(ssh TEST "grep -E -a 'NFSExtension' ~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/setup.txt | sed \"s/NFSExtension=//;s/.//\"")
            else
                DIRECTORY_NEWNUM="${CLIENT_PATCH_NFS_START}"
            fi
        else
            ssh TEST "rsync -i -avn --exclude=\"nfs\" --out-format=\"FILE:%n\" ~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/ CTRL:~/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM/ | grep -a \"^FILE:\" | grep -a -v \"/$\" | sed 's/^FILE:/\/NEWSYSTEM\//g' > $CDN_DIFF_FILE"
            ssh TEST "wc -l < $CDN_DIFF_FILE > $CDN_DIFF_FILE_TOTAL_COUNT_FILE"
            CDN_DIFF_FILE_TOTAL_COUNT=$(ssh TEST "cat $CDN_DIFF_FILE_TOTAL_COUNT_FILE")
            [ "$CDN_DIFF_FILE_TOTAL_COUNT" -eq "0" ] && exec echo "ERROR: No file to flush or cannot compare with prepatch directory, please try again."

            if [ "$ARG3" == "nobackup" -o "$ARG4" == "nobackup" ]; then
                echo "INFO: No Backup prepatch" | colorize red black
            else
                DIRNAME=$(readlink -f ~/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM | awk -F'/' '{print $NF}')
                cd ~/www/CDN/${GAME_TYPE}-LIVE_previous_version/
                unlink NEWSYSTEM
                ln -s ../${GAME_TYPE}-LIVE/${DIRNAME} NEWSYSTEM
            fi

            DIRECTORY_OLDNUM=$(grep -E -a 'NFSExtension' /tmp/setup.txt.old | sed "s/NFSExtension=//;s/.//")
            DIRECTORY_NEWNUM=$(ssh TEST "grep -E -a 'NFSExtension' ~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/setup.txt | sed \"s/NFSExtension=//;s/.//\"")
        fi

        # [OPT] hardlink seed 上一版 + 本機批次改尾綴（perl rename），rsync 只補差異（最終內容由 --delete 保證 == SYNCTOLIVE）
        if [ -n "${DIRECTORY_OLDNUM}" ] && [ -d "${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_OLDNUM}" ] && [ ! -d "${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_NEWNUM}" ]; then
            cp -al ${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_OLDNUM} ${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_NEWNUM}
            ( cd ${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_NEWNUM} && find . -type f -name "*.${DIRECTORY_OLDNUM}" -exec rename "s/\.${DIRECTORY_OLDNUM}\$/.${DIRECTORY_NEWNUM}/" {} + )
        fi
        if [ ! -d "${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_NEWNUM}" ]; then mkdir -p ${HOME}/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_NEWNUM}; fi
        rsync -av --timeout=60 --delete --progress TEST:~/www/${GAME_TYPE}-TEST_SYNCTOLIVE/ ~/www/CDN/${GAME_TYPE}-LIVE/NEWSYSTEM.${DIRECTORY_NEWNUM}/ 2> ${RSYNC_ERROR_LOG}
        if [[ -s "${RSYNC_ERROR_LOG}" ]]; then
            automatic_send_messages_by_thread_tag ERROR rsync_cdn_prepatch_sync "Client Patch Rsync ${C_PART} 同步異常" "${PROCEDURE_NAME}" minute 10
        else
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client Patch Rsync ${C_PART} 同步正常" minute 10
        fi
        if [ "$ARG2" == "CLIENT" ] || [ "$ARG2" == "TRANSLATE" ] || [ "$ARG2" == "FORCESYNC" ]; then
            if [ ! -d "${HOME}/www/CDN/${GAME_TYPE}-LIVE/patch" ]; then mkdir -p ${HOME}/www/CDN/${GAME_TYPE}-LIVE/patch; fi
            for FILE in ${CLIENT_PATCH_FILENAME_LISTS}; do
                FILE_CHECK=$(
                    cd ~/www/CDN/${GAME_TYPE}-LIVE/patch/
                    if [ -f "${FILE}" ]; then echo 1; else echo 0; fi
                )
                if [ "${FILE_CHECK}" -eq 0 ]; then
                    cd ~/www/CDN/${GAME_TYPE}-LIVE/patch/
                    echo "default=https://${CLIENT_PATCH_DOMAIN_NAME}/NEWSYSTEM.${DIRECTORY_NEWNUM}/" > ${FILE}
                fi
            done

            if test -z ${DIRECTORY_OLDNUM}; then
                cd ~/www/CDN/${GAME_TYPE}-LIVE/
                ln -s NEWSYSTEM.${DIRECTORY_NEWNUM} NEWSYSTEM
            else
                cd ~/www/CDN/${GAME_TYPE}-LIVE/patch/
                sed -i -r "s|NEWSYSTEM.${DIRECTORY_OLDNUM}|NEWSYSTEM.${DIRECTORY_NEWNUM}|" ${GAME_TYPE,,}_${COUNTRY_CODE,,}*.txt
                cd ~/www/CDN/${GAME_TYPE}-LIVE/patch/
                sed -i -r "s|NEWSYSTEM.${DIRECTORY_OLDNUM}|NEWSYSTEM.${DIRECTORY_NEWNUM}|" ${GAME_TYPE,,}${COUNTRY_CODE,,}*.txt
                cd ~/www/CDN/${GAME_TYPE}-LIVE/
                unlink NEWSYSTEM
                ln -s NEWSYSTEM.${DIRECTORY_NEWNUM} NEWSYSTEM
            fi
        fi

        ssh TEST "cat $HOME/patch/flush_all.txt > ~/patch/tmp1_new"
        ssh TEST "cd $HOME/patch; php ~/patch/flush.php ~/patch/tmp1_new"
        automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步完成" minute 10
    fi
}

# _cdn_prepatch_sync_cos —— COS 家共用骨架（tencent / byteplus）：前置 rename/norename → 建鏡像 → difflist →
#   入口 txt → 物件儲存上傳(hook) → purge(共用) → 預熱(hook) → prune。storage/preheat 依 CDN_PROVIDER 分派。
_cdn_prepatch_sync_cos () { # Maintenance -> sync client/translate to prepatch & push object-storage CDN ( New Patch System: CLIENT (nobackup) / TRANSLATE (nobackup) / SETUP (nobackup) / NOTICE (nobackup) / MAINTENANCE (nobackup) / FORCESYNC (nobackup) )
    : "${CDN_PROVIDER:=tencent}"   # storage/preheat hook 後綴用；發佈端未設時預設騰訊
    [ "$ARG2" == "" ] && exec echo "Usage: $0 NEW_CDN_prepatch_sync <CLIENT|TRANSLATE|FORCESYNC|SETUP|NOTICE|MAINTENANCE>"

    # ── 路徑 / 常數（全走 .gamerc 的 GAME_TYPE / COUNTRY_CODE，不認專案）──────────
    COSCLI="$HOME/bin/coscli"
    CDN_PHP="$HOME/cdn_toolkit/cdn.php"
    DIFFLIST_PHP="$HOME/cdn_difflist/difflist.php"
    COS_ALIAS="${GAME_TYPE,,}${COUNTRY_CODE,,}"        # coscli config 別名（小寫無連字號；須對齊 ~/.cos.yaml 的 alias）
    COS_PREFIX="${GAME_TYPE}-${COUNTRY_CODE}"          # bucket key 前綴 = 專案隔離目錄（= URL 路徑前綴）
    SYNCTOLIVE_DIR="$HOME/www/${GAME_TYPE}-TEST_SYNCTOLIVE"
    LIVE_DIR="$HOME/www/${GAME_TYPE}_PREVPUB"          # 本機鏡像目錄（CTRL 退場，發佈正本留 TEST 本機）
    PATCH_DIR="${LIVE_DIR}/patch"                      # 入口 txt 正本（隨鏡像留在本機）
    OUT_DIR="$HOME/patch"                              # difflist 清單輸出
    PURGE_URLS="${OUT_DIR}/purge_urls.txt"
    PUSH_URLS="${OUT_DIR}/push_urls.txt"
    FULL_URLS="${OUT_DIR}/push_full_urls.txt"          # 全檔預熱基底（full 模式用；build_full_urls 比空目錄產生，編碼與 diff 一致）
    PREHEAT_ALWAYS_CONF="$HOME/cdn_difflist/preheat_always.conf"   # 每次必熱固定檔（glob，prefix 根、<VER> 代換版號）
    CDN_DOMAINS_CONF="$HOME/cdn_difflist/cdn_domains.conf"         # 各對外域名 CDN 驅動策略（host/provider/area/首發/後續/purge）
    ENTRY_URLS="${OUT_DIR}/entry_urls.txt"                         # 入口 txt URL 清單（換目錄組 purge 來源；以主域名為基底）
    RSYNC_ERROR_LOG="/tmp/rsync_cos_prepatch_sync_monitor.log"
    CDN_LOG="${OUT_DIR}/cdn_$(date +%Y%m%d_%H%M%S).log"            # CDN 刷新/預熱持久化 log（每次發布一檔；cdn.php 自身不寫檔）
    [ "${DRYRUN:-0}" = 1 ] && echo "[ DRYRUN ] mode on => coscli upload / CDN purge+push skipped; rsync+difflist+push_urls still real"

    [ -f "${RSYNC_ERROR_LOG}" ] && rm -rf "${RSYNC_ERROR_LOG}"
    mkdir -p "${LIVE_DIR}" "${PATCH_DIR}" "${OUT_DIR}"
    rm -f "${PURGE_URLS}" "${PUSH_URLS}" "${ENTRY_URLS}" "${OUT_DIR}"/push_*.txt "${OUT_DIR}"/purge_*.txt

    # 換版號組 / norename 組 旗標（依 ARG2）
    case "$ARG2" in
        CLIENT|TRANSLATE|FORCESYNC)        IS_RENAME=1 ;;
        SETUP|NOTICE|MAINTENANCE)          IS_RENAME=0 ;;
        *) exec echo "ERROR: Wrong parameter 2 (CLIENT|TRANSLATE|FORCESYNC|SETUP|NOTICE|MAINTENANCE)" ;;
    esac

    # ── 1. 備份 setup.txt（記舊版號）──────────────────────────────────────────
    rm -rf /tmp/setup.txt.old
    cp -arf "${SYNCTOLIVE_DIR}/setup.txt" /tmp/setup.txt.old

    PROCEDURE_NAME=$(ssh TEST "psql -U postgres Tools -c \"COPY (SELECT procedure_name FROM web_maintenance_procedure_mapping WHERE sub_command_name = '$ARG2') TO STDOUT;\"")

    # ── 2. 前置處理：沿用既有函式（不動）把 TEST 內容處理進 SYNCTOLIVE ─────────
    if [ "$ARG2" == "CLIENT" ]; then
        $HOME/bin/compare_platforms_for_clientpatch --action live_sync || true
        C_PART="全部"
        automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10
        rename_client_extension
    elif [ "$ARG2" == "TRANSLATE" ]; then
        if [ "$ARG3" == "nobackup" ]; then
            C_PART="翻譯 全語系"
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10
            rename_translate_extension
        else
            C_PART="翻譯 $ARG3"
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10
            rename_translate_extension $ARG3
        fi
    elif [ "$ARG2" == "FORCESYNC" ]; then
        C_PART="全部 強制重新刷新"
        automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10
        rm -rf "${SYNCTOLIVE_DIR}/Android/version"
        touch "${SYNCTOLIVE_DIR}/Android/version"
        rename_client_extension
    elif [ "$ARG2" == "SETUP" ]; then
        C_PART="setup.txt"
        automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10
        norename_setup_update
    elif [ "$ARG2" == "NOTICE" ]; then
        if [ "$ARG3" == "nobackup" ]; then
            C_PART="遊戲內公告 全語系"
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10
            norename_notice_update
        else
            C_PART="遊戲內公告 $ARG3"
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10
            norename_notice_update $ARG3
        fi
    elif [ "$ARG2" == "MAINTENANCE" ]; then
        if [ "$ARG3" == "nobackup" ]; then
            C_PART="維護公告 全語系"
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10
            norename_maintenance_update
        else
            C_PART="維護公告 $ARG3"
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步開始" minute 10
            norename_maintenance_update $ARG3
        fi
    fi

    # ── 3. 算目錄版號（NFSExtensionDir 去點）────────────────────────
    #   rename_* 已不加檔名尾綴 → 檔名天然固定，無需脫尾綴步驟。
    DIRECTORY_OLDNUM=$(grep -E -a 'NFSExtensionDir' /tmp/setup.txt.old              | sed "s/NFSExtensionDir=//;s/.//")
    if [ "${IS_RENAME}" -eq 1 ] && [ -n "$CLIENT_PATCH_NFS_START" ]; then
        DIRECTORY_NEWNUM="${CLIENT_PATCH_NFS_START}"
    else
        DIRECTORY_NEWNUM=$(grep -E -a 'NFSExtensionDir' "${SYNCTOLIVE_DIR}/setup.txt" | sed "s/NFSExtensionDir=//;s/.//")
    fi
    NEW_VERDIR="${LIVE_DIR}/NEWSYSTEM.${DIRECTORY_NEWNUM}"
    OLD_VERDIR="${LIVE_DIR}/NEWSYSTEM.${DIRECTORY_OLDNUM}"
    BASE_URL="http://${CLIENT_PATCH_DOMAIN_NAME}/${COS_PREFIX}/NEWSYSTEM.${DIRECTORY_NEWNUM}"   # http（定案）；CLIENT_PATCH_DOMAIN_NAME 為裸網域，前綴由 COS_PREFIX 補

    # ──────────────────────────────────────
    if [ "${IS_RENAME}" -eq 1 ]; then
        # ════ 路徑 A：換版號組（換目錄，NEW≠OLD）════════════════════════════════

        # 4. 在本機鏡像建新版號目錄，把本次整包放進去
        mkdir -p "${NEW_VERDIR}"
        rsync -av --timeout=60 --delete --progress "${SYNCTOLIVE_DIR}/" "${NEW_VERDIR}/" 2> "${RSYNC_ERROR_LOG}"
        if [[ -s "${RSYNC_ERROR_LOG}" ]]; then
            automatic_send_messages_by_thread_tag ERROR cos_prepatch_sync "Client Patch 本機版本目錄 ${C_PART} 建立異常" "${PROCEDURE_NAME}" minute 10
        else
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client Patch 本機版本目錄 ${C_PART} 建立正常" minute 10
        fi

        # 5. difflist 原生比 新版號目錄(新) ↔ 舊版號目錄(舊) → push_urls.txt
        #    首發時舊版號目錄不存在，difflist 自動建空 → 全部計新增（全量預熱，須分日）。
        php "${DIFFLIST_PHP}" "${NEW_VERDIR}" "${OLD_VERDIR}" "${BASE_URL}" --out-dir "${OUT_DIR}"
        # 首發殘留清理：OLDNUM 空時 difflist 會建出無版號空目錄 NEWSYSTEM.，比完即無用 → rmdir（只刪空、防呆）
        [ -z "${DIRECTORY_OLDNUM}" ] && [ -d "${OLD_VERDIR}" ] && rmdir "${OLD_VERDIR}" 2>/dev/null

        # 6. 入口 redirect txt：每個檔指向「自己配對的域名」(容災用)
        #    檔名順序(.gamerc CLIENT_PATCH_FILENAME_LISTS) 對齊域名順序(cdn_domains.conf)：
        #    檔1→xjep、檔2→dl、檔3→patch。客戶端 fail over 到哪個域名，default= 就指那家 → 從那家下載，
        #    與「客戶端照域名或照 default 下載」無關，兩種都對。每次覆寫(內容只有 default=)，host+版號一次寫對。
        ENTRY_DOMAINS=()
        while read -r D_HOST _ <&3; do
            case "$D_HOST" in ''|\#*) continue ;; esac
            ENTRY_DOMAINS+=("$D_HOST")
        done 3< "${CDN_DOMAINS_CONF}"
        ENTRY_FILES=(${CLIENT_PATCH_FILENAME_LISTS})
        EIDX=0
        for FILE in "${ENTRY_FILES[@]}"; do
            ENTRY_D="${ENTRY_DOMAINS[$EIDX]:-${CLIENT_PATCH_DOMAIN_NAME}}"   # 域名數不足 → fallback 主域名
            echo "default=http://${ENTRY_D}/${COS_PREFIX}/NEWSYSTEM.${DIRECTORY_NEWNUM}/" > "${PATCH_DIR}/${FILE}"
            echo "[ entry ] ${FILE} => http://${ENTRY_D}/${COS_PREFIX}/NEWSYSTEM.${DIRECTORY_NEWNUM}/"
            EIDX=$((EIDX + 1))
        done
        [ "${#ENTRY_FILES[@]}" -ne "${#ENTRY_DOMAINS[@]}" ] && echo "[ entry ] ⚠️ 入口檔數(${#ENTRY_FILES[@]}) 與 conf 域名數(${#ENTRY_DOMAINS[@]}) 不一致，請確認檔↔域名對應"

        # 7. 上傳到物件儲存（全新前綴）：各家 storage hook（tencent=coscli/cos、byteplus=tosutil/tos）。
        #    servercopy：增量開啟且有舊版號才在桶內服務端複製舊→新+核對檔數（rc!=0 中止，不往下翻入口 txt）；
        #      增量關閉/首發回 0 → 後續一律全量 sync（最穩）。安全前提：舊版號前綴全程只讀不動。
        #      （tencent 增量細節見 _cdn_storage_servercopy_tencent；byteplus 盲串不做服務端複製、直接全量。）
        _cdn_storage_servercopy_${CDN_PROVIDER} "${DIRECTORY_OLDNUM}" "${DIRECTORY_NEWNUM}" || return 1
        _cdn_storage_sync_${CDN_PROVIDER} "${NEW_VERDIR}" "${DIRECTORY_NEWNUM}"
        COS_SYNC_RC=$?   # coscli/tosutil 可能把錯誤寫 stdout，光看 stderr 會漏判 → 以 exit code 為主、stderr 非空為輔
        if [ "${COS_SYNC_RC}" -ne 0 ] || [[ -s "${RSYNC_ERROR_LOG}" ]]; then
            automatic_send_messages_by_thread_tag ERROR cos_prepatch_sync "Client Patch 物件儲存上傳 ${C_PART} 異常 (rc=${COS_SYNC_RC})" "${PROCEDURE_NAME}" minute 10
        else
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client Patch 物件儲存上傳 ${C_PART} 正常" minute 10
        fi

        # 8. 本機入口 txt → 物件儲存（固定路徑）
        for FILE in ${CLIENT_PATCH_FILENAME_LISTS}; do
            _cdn_storage_putfile_${CDN_PROVIDER} "${PATCH_DIR}/${FILE}" "patch/${FILE}"
        done

        # 9. 刷新：逐域名 purge 入口 txt（禁 /*；各帶 area，見 cdn_domains.conf）
        : > "${ENTRY_URLS}"
        for FILE in ${CLIENT_PATCH_FILENAME_LISTS}; do
            echo "http://${CLIENT_PATCH_DOMAIN_NAME}/${COS_PREFIX}/patch/${FILE}" >> "${ENTRY_URLS}"
        done
        purge_urls_all_domains "${ENTRY_URLS}"

        # 10. 預熱：逐域名依設定組清單並 push（首發 xjep 全量 / dl 只常用，見 cdn_domains.conf）
        _cdn_preheat_${CDN_PROVIDER}

        # 11. 只留最新 + 前一版本機版號目錄（省硬碟）；更舊的 NEWSYSTEM.<版號> 刪除。
        #     只挑 NEWSYSTEM.<數字> 目錄、sort -V 由舊到新、head -n -2 保留最新兩版、其餘 rm -rf。
        #     入口 txt(patch/) 與空目錄(NEWSYSTEM.) 不在 glob 內 → 不會被碰；DRYRUN 時只列不刪。
        ls -1d "${LIVE_DIR}"/NEWSYSTEM.[0-9]* 2>/dev/null | sort -V | head -n -2 | while read -r PRUNE_DIR; do
            [ -d "${PRUNE_DIR}" ] || continue
            if [ "${DRYRUN:-0}" = 1 ]; then
                echo "[ DRYRUN ] would prune old version dir: ${PRUNE_DIR}"
            else
                echo "[ prune ] remove old version dir: ${PRUNE_DIR}"
                rm -rf "${PRUNE_DIR}"
            fi
        done

    else
        # ════ 路徑 B：norename 組（不換目錄，NEW==OLD）═══════════════════════════
        #   改的是現有版號目錄內的固定檔（setup.txt / *notice.txt / *maintenance.txt），
        #   URL 不變 → 邊緣有舊快取 → 必須 purge 變動檔本身 + 重傳 + 重新預熱。

        # 5. difflist 比 SYNCTOLIVE(新，剛被 norename 改過) ↔ 現有版號目錄(舊，覆蓋前)
        #    （必須在 rsync 覆蓋前先比；現有版號目錄不存在代表狀態異常，difflist 會報錯）
        php "${DIFFLIST_PHP}" "${SYNCTOLIVE_DIR}" "${NEW_VERDIR}" "${BASE_URL}" --out-dir "${OUT_DIR}"

        # 6. rsync 覆蓋更新本機鏡像（同版號目錄，更新那幾個固定檔）
        rsync -av --timeout=60 --delete --progress "${SYNCTOLIVE_DIR}/" "${NEW_VERDIR}/" 2> "${RSYNC_ERROR_LOG}"
        if [[ -s "${RSYNC_ERROR_LOG}" ]]; then
            automatic_send_messages_by_thread_tag ERROR cos_prepatch_sync "Client Patch 本機鏡像 ${C_PART} 更新異常" "${PROCEDURE_NAME}" minute 10
        else
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client Patch 本機鏡像 ${C_PART} 更新正常" minute 10
        fi

        # 7. 物件儲存 sync：CRC64/雜湊比對只傳變動檔 → 同版號前綴、同檔名（storage hook，norename 組無服務端複製）
        _cdn_storage_sync_${CDN_PROVIDER} "${NEW_VERDIR}" "${DIRECTORY_NEWNUM}"
        COS_SYNC_RC=$?   # coscli/tosutil 可能把錯誤寫 stdout，光看 stderr 會漏判 → 以 exit code 為主、stderr 非空為輔
        if [ "${COS_SYNC_RC}" -ne 0 ] || [[ -s "${RSYNC_ERROR_LOG}" ]]; then
            automatic_send_messages_by_thread_tag ERROR cos_prepatch_sync "Client Patch 物件儲存上傳 ${C_PART} 異常 (rc=${COS_SYNC_RC})" "${PROCEDURE_NAME}" minute 10
        else
            automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client Patch 物件儲存上傳 ${C_PART} 正常" minute 10
        fi

        # 8. 刷新：逐域名 purge 變動檔固定 URL（URL 不變、有舊快取；禁 /*）
        purge_urls_all_domains "${PURGE_URLS}"

        # 9. 預熱：等 purge 後逐域名 push（順序不可反；後續 = 常用+差異）
        _cdn_preheat_${CDN_PROVIDER}

        # 注意：norename 組不動入口 txt（default 維持指向當前版號目錄）
    fi

    automatic_send_chatbot_text_only_by_thread_tag "${PROCEDURE_NAME}" "Client ${C_PART} 同步完成" minute 10
}

### Game Server Status ###
android_status () { # Submit -> Check Status of GOOGLE-SUBMIT all servers
    ssh GOOGLE-SUBMIT "pl"
}

ios_status () { # Submit -> Check Status of APPLE-SUBMIT all servers
    ssh APPLE-SUBMIT "pl"
}

submit_status () { # Submit -> Check Status SUBMIT all servers
    ssh SUBMIT "pl"
}

data_status () { # Data -> Check Status DATA all servers
    ssh DATA "pl"
}

time1_status () { # Time1 -> Check Status TIME1 all servers
    ssh TIME1 "pl"
}

old_test_status () { # OldTest -> Check Status OLD_TEST all servers
    ssh OLD_TEST "pl"
}

media_status () { # Media -> Check Status MEDIA all servers
    ssh MEDIA "pl"
}

mapping_status () { # Mapping -> Check Status MAPPING all servers
    ssh MAPPING "pl"
}

### Game Server Start ###
android_start () { # Submit -> Start GOOGLE-SUBMIT all servers
    ssh GOOGLE-SUBMIT "SKServer start all"
}

ios_start () { # Submit -> Start APPLE-SUBMIT all servers
    ssh APPLE-SUBMIT "SKServer start all"
}

submit_start () { # Submit -> Start SUBMIT all servers
    ssh SUBMIT "SKServer start all"
}

data_start () { # Data -> Start DATA all servers
    ssh DATA "SKServer start all"
}

time1_start () { # Time1 -> Start TIME1 all servers
    ssh TIME1 "SKServer start all"
}

old_test_start () { # OldTest -> Start OLD_TEST all servers
    ssh OLD_TEST "SKServer start all"
}

media_start () { # Media -> Start MEDIA all servers
    ssh MEDIA "SKServer start all"
}

mapping_start () { # Mapping -> Start MAPPING all servers
    ssh MAPPING "SKServer start all"
}

### Game Server Stop ###
android_stop () { # Submit -> Stop GOOGLE-SUBMIT all servers
    ssh GOOGLE-SUBMIT "SKServer stop all"
}

ios_stop () { # Submit -> Stop APPLE-SUBMIT all servers
    ssh APPLE-SUBMIT "SKServer stop all"
}

submit_stop () { # Submit -> Stop SUBMIT all servers
    ssh SUBMIT "SKServer stop all"
}

data_stop () { # Data -> Stop DATA all servers
    ssh DATA "SKServer stop all"
}

time1_stop () { # Time1 -> Stop TIME1 all servers
    ssh TIME1 "SKServer stop all"
}

old_test_stop () { # OldTest -> Stop OLD_TEST all servers
    ssh OLD_TEST "SKServer stop all"
}

media_stop () { # Media -> Stop MEDIA all servers
    ssh MEDIA "SKServer stop all"
}

mapping_stop () { # Mapping -> Stop MAPPING all servers
    ssh MAPPING "SKServer stop all"
}

### Game Server ReStart ###
android_restart () { # Submit -> ReStart GOOGLE-SUBMIT all servers
    ssh GOOGLE-SUBMIT "SKServer stop all; SKServer start all"
}

ios_restart () { # Submit -> ReStart APPLE-SUBMIT all servers
    ssh APPLE-SUBMIT "SKServer stop all; SKServer start all"
}

submit_restart () { # Submit -> ReStart SUBMIT all servers
    ssh SUBMIT "SKServer stop all; SKServer start all"
}

data_restart () { # Data -> ReStart DATA all servers
    ssh DATA "SKServer stop all; SKServer start all"
}

time1_restart () { # Time1 -> ReStart TIME1 all servers
    ssh TIME1 "SKServer stop all; SKServer start all"
}

old_test_restart () { # OldTest -> ReStart OLD_TEST all servers
    ssh OLD_TEST "SKServer stop all; SKServer start all"
}

media_restart () { # Media -> ReStart MEDIA all servers
    ssh MEDIA "SKServer stop all; SKServer start all"
}

mapping_restart () { # Mapping -> ReStart MAPPING all servers
    ssh MAPPING "SKServer stop all; SKServer start all"
}

### Game Server DBbackup ###
android_dbbackup () { # Submit -> Backup GOOGLE-SUBMIT Database
    ssh GOOGLE-SUBMIT "TestCtrl dbbackup"
}

ios_dbbackup () { # Submit -> Backup APPLE-SUBMIT Database
    ssh APPLE-SUBMIT "TestCtrl dbbackup"
}

submit_dbbackup () { # Submit -> Backup SUBMIT Database
    ssh SUBMIT "TestCtrl dbbackup"
}

data_dbbackup () { # Data -> Backup DATA Database
    ssh DATA "TestCtrl dbbackup"
    #ssh DATA "DBbackup ${GAME_DB_NAME}${CROSS_ROUTER_SID_TEST}"
    #if [ "$ENABLE_SOCIETY" == "1" ]; then
    #    ssh DATA "sed -i -e 's/system (\"pg_dump/#system (\"pg_dump/g;s/^\(.*vacuum.*\)$/#\1/g' common-TEST/db/db_society_alter"
    #    ssh DATA "cd common-TEST/db;perl db_society_alter :${GAME_TYPE}Society"
    #fi
}

time1_dbbackup () { # Time1 -> Backup TIME1 Database
    ssh TIME1 "TestCtrl dbbackup"
}

old_test_dbbackup () { # OldTest -> Backup OLD_TEST Database
    ssh OLD_TEST "TestCtrl dbbackup"
}

media_dbbackup () { # Media -> Backup MEDIA Database
    ssh MEDIA "TestCtrl dbbackup"
}

mapping_dbbackup () { # Mapping -> Backup MAPPING Database
    ssh MAPPING "TestCtrl dbbackup"
    #ssh MAPPING "DBbackup ${GAME_DB_NAME}${CROSS_ROUTER_SID_TEST}"
    if [ "$ENABLE_SOCIETY" == "1" ]; then
        ssh MAPPING "sed -i -e 's/system (\"pg_dump/#system (\"pg_dump/g;s/^\(.*vacuum.*\)$/#\1/g' common-TEST/db/db_society_alter"
        ssh MAPPING "cd common-TEST/db;perl db_society_alter :${GAME_TYPE}Society"
    fi
}

### Game Server Sync ###
android_sync () { # Submit -> Sync test to GOOGLE-SUBMIT (argument: "server", "client")
    DATETIME="$(/bin/date +%Y%m%dT%H%M)"
    # Server only
    if grep -a -qi "server" <<< "$ALLARGS"; then
        [ ! -d "$HOME/submit_sync/android" ] && mkdir -p $HOME/submit_sync/android
        cd $HOME/submit_sync/android/
        SERVER_DIFF_FILE="$HOME/submit_sync/android/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude-submit --out-format="FILE:%n" ~/common-TEST/ GOOGLE-SUBMIT:~/common-GOOGLE-SUBMIT/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ] && exec echo "ERROR: No server file need to sync to GOOGLE-SUBMIT"
        ssh GOOGLE-SUBMIT "rsync -av --progress ~/common-GOOGLE-SUBMIT/ ~/common-GOOGLE-SUBMIT_previous/"
        rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. GOOGLE-SUBMIT:~/common-GOOGLE-SUBMIT/
    # Client only
    elif grep -a -qi "client" <<< "$ALLARGS"; then
        [ ! -d "$HOME/submit_sync/android" ] && mkdir -p $HOME/submit_sync/android
        cd $HOME/submit_sync/android/
        CLIENT_DIFF_FILE="$HOME/submit_sync/android/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ GOOGLE-SUBMIT:~/www/${GAME_TYPE}-GOOGLE-SUBMIT/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ] && exec echo "ERROR: No client file need to sync to GOOGLE-SUBMIT"
        ssh GOOGLE-SUBMIT "rsync -av --progress ~/www/${GAME_TYPE}-GOOGLE-SUBMIT/ ~/www/${GAME_TYPE}-GOOGLE-SUBMIT_previous/"
        rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ GOOGLE-SUBMIT:~/www/${GAME_TYPE}-GOOGLE-SUBMIT/
    # Both Server and Client
    else
        [ ! -d "$HOME/submit_sync/android" ] && mkdir -p $HOME/submit_sync/android
        cd $HOME/submit_sync/android/
        SERVER_DIFF_FILE="$HOME/submit_sync/android/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude-submit --out-format="FILE:%n" ~/common-TEST/ GOOGLE-SUBMIT:~/common-GOOGLE-SUBMIT/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        if [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No server file need to sync to GOOGLE-SUBMIT"
        else
            ssh GOOGLE-SUBMIT "rsync -av --progress ~/common-GOOGLE-SUBMIT/ ~/common-GOOGLE-SUBMIT_previous/"
            rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. GOOGLE-SUBMIT:~/common-GOOGLE-SUBMIT/
        fi

        cd $HOME/submit_sync/android/
        CLIENT_DIFF_FILE="$HOME/submit_sync/android/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ GOOGLE-SUBMIT:~/www/${GAME_TYPE}-GOOGLE-SUBMIT/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        if [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No client file need to sync to GOOGLE-SUBMIT"
        else
            ssh GOOGLE-SUBMIT "rsync -av --progress ~/www/${GAME_TYPE}-GOOGLE-SUBMIT/ ~/www/${GAME_TYPE}-GOOGLE-SUBMIT_previous/"
            rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ GOOGLE-SUBMIT:~/www/${GAME_TYPE}-GOOGLE-SUBMIT/
        fi
    fi
}

ios_sync () { # Submit -> Sync test to APPLE-SUBMIT (argument: "server", "client")
    DATETIME="$(/bin/date +%Y%m%dT%H%M)"
    # Server only
    if grep -a -qi "server" <<< "$ALLARGS"; then
        [ ! -d "$HOME/submit_sync/ios" ] && mkdir -p $HOME/submit_sync/ios
        cd $HOME/submit_sync/ios/
        SERVER_DIFF_FILE="$HOME/submit_sync/ios/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude-submit --out-format="FILE:%n" ~/common-TEST/ APPLE-SUBMIT:~/common-APPLE-SUBMIT/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ] && exec echo "ERROR: No server file need to sync to APPLE-SUBMIT"
        ssh APPLE-SUBMIT "rsync -av --progress ~/common-APPLE-SUBMIT/ ~/common-APPLE-SUBMIT_previous/"
        rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. APPLE-SUBMIT:~/common-APPLE-SUBMIT/
    # Client only
    elif grep -a -qi "server" <<< "$ALLARGS"; then
        [ ! -d "$HOME/submit_sync/ios" ] && mkdir -p $HOME/submit_sync/ios
        cd $HOME/submit_sync/ios/
        CLIENT_DIFF_FILE="$HOME/submit_sync/ios/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ APPLE-SUBMIT:~/www/${GAME_TYPE}-APPLE-SUBMIT/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ] && exec echo "ERROR: No client file need to sync to APPLE-SUBMIT"
        ssh APPLE-SUBMIT "rsync -av --progress ~/www/${GAME_TYPE}-APPLE-SUBMIT/ ~/www/${GAME_TYPE}-APPLE-SUBMIT_previous/"
        rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ APPLE-SUBMIT:~/www/${GAME_TYPE}-APPLE-SUBMIT/
    else
        # Both Server and Client
        [ ! -d "$HOME/submit_sync/ios" ] && mkdir -p $HOME/submit_sync/ios
        cd $HOME/submit_sync/ios/
        SERVER_DIFF_FILE="$HOME/submit_sync/ios/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude-submit --out-format="FILE:%n" ~/common-TEST/ APPLE-SUBMIT:~/common-APPLE-SUBMIT/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        if [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No server file need to sync to APPLE-SUBMIT"
        else
            ssh APPLE-SUBMIT "rsync -av --progress ~/common-APPLE-SUBMIT/ ~/common-APPLE-SUBMIT_previous/"
            rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. APPLE-SUBMIT:~/common-APPLE-SUBMIT/
        fi

        cd $HOME/submit_sync/ios/
        CLIENT_DIFF_FILE="$HOME/submit_sync/ios/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ APPLE-SUBMIT:~/www/${GAME_TYPE}-APPLE-SUBMIT/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        if [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No client file need to sync to APPLE-SUBMIT"
        else
            ssh APPLE-SUBMIT "rsync -av --progress ~/www/${GAME_TYPE}-APPLE-SUBMIT/ ~/www/${GAME_TYPE}-APPLE-SUBMIT_previous/"
            rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ APPLE-SUBMIT:~/www/${GAME_TYPE}-APPLE-SUBMIT/
        fi
    fi
}

submit_sync () { # Submit -> Sync test to SUBMIT (argument: "server", "client")
    DATETIME="$(/bin/date +%Y%m%dT%H%M)"
    # Server only
    if grep -a -qi "server" <<< "$ALLARGS"; then
        [ ! -d "$HOME/submit_sync/submit" ] && mkdir -p $HOME/submit_sync/submit
        cd $HOME/submit_sync/submit/
        SERVER_DIFF_FILE="$HOME/submit_sync/submit/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude-submit --out-format="FILE:%n" ~/common-TEST/ SUBMIT:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ] && exec echo "ERROR: No server file need to sync to SUBMIT"
        ssh SUBMIT "rsync -av --progress ~/common-TEST/ ~/common-SUBMIT_previous/"
        rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. SUBMIT:~/common-TEST/
        ssh SUBMIT "sed -i 's/AllGM=1/AllGM=0/' ~/common-TEST/bin/config.ini"
    # Client only
    elif grep -a -qi "client" <<< "$ALLARGS"; then
        [ ! -d "$HOME/submit_sync/submit" ] && mkdir -p $HOME/submit_sync/submit
        cd $HOME/submit_sync/submit/
        CLIENT_DIFF_FILE="$HOME/submit_sync/submit/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ SUBMIT:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ] && exec echo "ERROR: No client file need to sync to SUBMIT"
        ssh SUBMIT "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-SUBMIT_previous/"
        rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ SUBMIT:~/www/${GAME_TYPE}-TEST/
    # H5 only
    elif grep -a -qi "h5" <<< "$ALLARGS"; then
        [ ! -d "$HOME/submit_sync/submit" ] && mkdir -p $HOME/submit_sync/submit
        cd $HOME/submit_sync/submit/
        H5_DIFF_FILE="$HOME/submit_sync/submit/h5_diff.txt.$DATETIME"
        rsync -i -avn --out-format="FILE:%n" ~/www/H5-TEST/ SUBMIT:~/www/H5-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $H5_DIFF_FILE
        [ "$(wc -l < $H5_DIFF_FILE)" == "0" ] && exec echo "ERROR: No h5 file need to sync to SUBMIT"
        ssh SUBMIT "rsync -av --progress ~/www/H5-TEST/ ~/www/H5-SUBMIT_previous/"
        rsync -av --progress ~/www/H5-TEST/ SUBMIT:~/www/H5-TEST/
    else
        # Both Server and Client
        [ ! -d "$HOME/submit_sync/submit" ] && mkdir -p $HOME/submit_sync/submit
        cd $HOME/submit_sync/submit/
        SERVER_DIFF_FILE="$HOME/submit_sync/submit/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude-submit --out-format="FILE:%n" ~/common-TEST/ SUBMIT:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        if [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No server file need to sync to SUBMIT"
        else
            ssh SUBMIT "rsync -av --progress ~/common-TEST/ ~/common-SUBMIT_previous/"
            rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. SUBMIT:~/common-TEST/
            ssh SUBMIT "sed -i 's/AllGM=1/AllGM=0/' ~/common-TEST/bin/config.ini"
        fi

        cd $HOME/submit_sync/submit/
        CLIENT_DIFF_FILE="$HOME/submit_sync/submit/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ SUBMIT:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        if [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No client file need to sync to SUBMIT"
        else
            ssh SUBMIT "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-SUBMIT_previous/"
            rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ SUBMIT:~/www/${GAME_TYPE}-TEST/
        fi

        cd $HOME/submit_sync/submit/
        H5_DIFF_FILE="$HOME/submit_sync/submit/h5_diff.txt.$DATETIME"
        rsync -i -avn --out-format="FILE:%n" ~/www/H5-TEST/ SUBMIT:~/www/H5-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $H5_DIFF_FILE
        if [ "$(wc -l < $H5_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No h5 file need to sync to SUBMIT"
        else
            ssh SUBMIT "rsync -av --progress ~/www/H5-TEST/ ~/www/H5-SUBMIT_previous/"
            rsync -av --progress ~/www/H5-TEST/ SUBMIT:~/www/H5-TEST/
        fi
    fi
}

merge_sync () { # Merge -> Sync test to MERGE (argument: "server", "client")
    DATETIME="$(/bin/date +%Y%m%dT%H%M)"
    # Server only
    if grep -a -qi "server" <<< "$ALLARGS"; then
        [ ! -d "$HOME/merge_sync/merge" ] && mkdir -p $HOME/merge_sync/merge
        cd $HOME/merge_sync/merge/
        SERVER_DIFF_FILE="$HOME/merge_sync/merge/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude-submit --out-format="FILE:%n" ~/common-TEST/ ASIA-MERGE:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ] && exec echo "ERROR: No server file need to sync to MERGE"
        ssh ASIA-MERGE "rsync -av --progress ~/common-TEST/ ~/common-TEST_previous/"
        rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. ASIA-MERGE:~/common-TEST/
        ssh US-MERGE "rsync -av --progress ~/common-TEST/ ~/common-TEST_previous/"
        rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. US-MERGE:~/common-TEST/
        ssh EUROPE-MERGE "rsync -av --progress ~/common-TEST/ ~/common-TEST_previous/"
        rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. EUROPE-MERGE:~/common-TEST/
    # Client only
    elif grep -a -qi "client" <<< "$ALLARGS"; then
        [ ! -d "$HOME/merge_sync/merge" ] && mkdir -p $HOME/merge_sync/merge
        cd $HOME/merge_sync/merge/
        CLIENT_DIFF_FILE="$HOME/merge_sync/merge/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ ASIA-MERGE:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ] && exec echo "ERROR: No client file need to sync to MERGE"
        ssh ASIA-MERGE "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-TEST_previous/"
        rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ ASIA-MERGE:~/www/${GAME_TYPE}-TEST/
    else
        # Both Server and Client
        [ ! -d "$HOME/merge_sync/merge" ] && mkdir -p $HOME/merge_sync/merge
        cd $HOME/merge_sync/merge/
        SERVER_DIFF_FILE="$HOME/merge_sync/merge/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude-submit --out-format="FILE:%n" ~/common-TEST/ ASIA-MERGE:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        if [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No server file need to sync to MERGE"
        else
            ssh ASIA-MERGE "rsync -av --progress ~/common-TEST/ ~/common-TEST_previous/"
            rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. ASIA-MERGE:~/common-TEST/
            ssh US-MERGE "rsync -av --progress ~/common-TEST/ ~/common-TEST_previous/"
            rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. US-MERGE:~/common-TEST/
            ssh EUROPE-MERGE "rsync -av --progress ~/common-TEST/ ~/common-TEST_previous/"
            rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. EUROPE-MERGE:~/common-TEST/
        fi

        cd $HOME/merge_sync/merge/
        CLIENT_DIFF_FILE="$HOME/merge_sync/merge/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ ASIA-MERGE:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        if [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No client file need to sync to MERGE"
        else
            ssh ASIA-MERGE "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-TEST_previous/"
            rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ ASIA-MERGE:~/www/${GAME_TYPE}-TEST/
        fi
    fi
}

data_sync () { # Data -> Sync test to DATA (argument: "server", "client")
    DATETIME="$(/bin/date +%Y%m%dT%H%M)"
    # Server only
    if grep -a -qi "server" <<< "$ALLARGS"; then
        [ ! -d "$HOME/data_sync/data" ] && mkdir -p $HOME/data_sync/data
        cd $HOME/data_sync/data/
        SERVER_DIFF_FILE="$HOME/data_sync/data/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude-submit --out-format="FILE:%n" ~/common-TEST/ DATA:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ] && exec echo "ERROR: No server file need to sync to DATA"
        ssh DATA "rsync -av --progress ~/common-TEST/ ~/common-DATA_previous/"
        rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. DATA:~/common-TEST/
        #ssh DATA "sed -i 's/AllGM=1/AllGM=0/' ~/common-DATA/bin/config.ini"
    # Client only
    elif grep -a -qi "client" <<< "$ALLARGS"; then
        [ ! -d "$HOME/data_sync/data" ] && mkdir -p $HOME/data_sync/data
        cd $HOME/data_sync/data/
        CLIENT_DIFF_FILE="$HOME/data_sync/data/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ DATA:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ] && exec echo "ERROR: No client file need to sync to DATA"
        ssh DATA "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-DATA_previous/"
        rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ DATA:~/www/${GAME_TYPE}-TEST/
    # H5 only
    elif grep -a -qi "h5" <<< "$ALLARGS"; then
        [ ! -d "$HOME/data_sync/data" ] && mkdir -p $HOME/data_sync/data
        cd $HOME/data_sync/data/
        H5_DIFF_FILE="$HOME/data_sync/data/h5_diff.txt.$DATETIME"
        rsync -i -avn --out-format="FILE:%n" ~/www/H5-TEST/ DATA:~/www/H5-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $H5_DIFF_FILE
        [ "$(wc -l < $H5_DIFF_FILE)" == "0" ] && exec echo "ERROR: No h5 file need to sync to DATA"
        ssh DATA "rsync -av --progress ~/www/H5-TEST/ ~/www/H5-DATA_previous/"
        rsync -av --progress ~/www/H5-TEST/ DATA:~/www/H5-TEST/
    else
        # Both Server and Client
        [ ! -d "$HOME/data_sync/data" ] && mkdir -p $HOME/data_sync/data
        cd $HOME/data_sync/data/
        SERVER_DIFF_FILE="$HOME/data_sync/data/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude-submit --out-format="FILE:%n" ~/common-TEST/ DATA:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        if [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No server file need to sync to DATA"
        else
            ssh DATA "rsync -av --progress ~/common-TEST/ ~/common-DATA_previous/"
            rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. DATA:~/common-TEST/
            #ssh DATA "sed -i 's/AllGM=1/AllGM=0/' ~/common-TEST/bin/config.ini"
        fi

        cd $HOME/data_sync/data/
        CLIENT_DIFF_FILE="$HOME/data_sync/data/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ DATA:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        if [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No client file need to sync to DATA"
        else
            ssh DATA "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-DATA_previous/"
            rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ DATA:~/www/${GAME_TYPE}-TEST/
        fi

        cd $HOME/data_sync/data/
        H5_DIFF_FILE="$HOME/data_sync/data/h5_diff.txt.$DATETIME"
        rsync -i -avn --out-format="FILE:%n" ~/www/H5-TEST/ DATA:~/www/H5-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $H5_DIFF_FILE
        if [ "$(wc -l < $H5_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No h5 file need to sync to DATA"
        else
            ssh DATA "rsync -av --progress ~/www/H5-TEST/ ~/www/H5-DATA_previous/"
            rsync -av --progress ~/www/H5-TEST/ DATA:~/www/H5-TEST/
        fi
    fi
}

time1_sync () { # Time1 -> Sync test to TIME1 (argument: "server", "client")
    DATETIME="$(/bin/date +%Y%m%dT%H%M)"
    # Server only
    if grep -a -qi "server" <<< "$ALLARGS"; then
        [ ! -d "$HOME/time1_sync/time1" ] && mkdir -p $HOME/time1_sync/time1
        cd $HOME/time1_sync/time1/
        SERVER_DIFF_FILE="$HOME/time1_sync/time1/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude --out-format="FILE:%n" ~/common-TEST/ TIME1:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ] && exec echo "ERROR: No server file need to sync to TIME1"
        ssh TIME1 "rsync -av --progress ~/common-TEST/ ~/common-TIME1_previous/"
        rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude ~/common-TEST/. TIME1:~/common-TEST/
    # Client only
    elif grep -a -qi "client" <<< "$ALLARGS"; then
        [ ! -d "$HOME/time1_sync/time1" ] && mkdir -p $HOME/time1_sync/time1
        cd $HOME/time1_sync/time1/
        CLIENT_DIFF_FILE="$HOME/time1_sync/time1/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ TIME1:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ] && exec echo "ERROR: No client file need to sync to TIME1"
        ssh TIME1 "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-TIME1_previous/"
        rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ TIME1:~/www/${GAME_TYPE}-TEST/
    # H5 only
    elif grep -a -qi "h5" <<< "$ALLARGS"; then
        [ ! -d "$HOME/time1_sync/time1" ] && mkdir -p $HOME/time1_sync/time1
        cd $HOME/time1_sync/time1/
        H5_DIFF_FILE="$HOME/time1_sync/time1/h5_diff.txt.$DATETIME"
        rsync -i -avn --out-format="FILE:%n" ~/www/H5-TEST/ TIME1:~/www/H5-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $H5_DIFF_FILE
        [ "$(wc -l < $H5_DIFF_FILE)" == "0" ] && exec echo "ERROR: No h5 file need to sync to TIME1"
        ssh TIME1 "rsync -av --progress ~/www/H5-TEST/ ~/www/H5-TIME1_previous/"
        rsync -av --progress ~/www/H5-TEST/ TIME1:~/www/H5-TEST/
    else
        # Both Server and Client
        [ ! -d "$HOME/time1_sync/time1" ] && mkdir -p $HOME/time1_sync/time1
        cd $HOME/time1_sync/time1/
        SERVER_DIFF_FILE="$HOME/time1_sync/time1/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude --out-format="FILE:%n" ~/common-TEST/ TIME1:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        if [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No server file need to sync to TIME1"
        else
            ssh TIME1 "rsync -av --progress ~/common-TEST/ ~/common-TIME1_previous/"
            rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude ~/common-TEST/. TIME1:~/common-TEST/
        fi

        cd $HOME/time1_sync/time1/
        CLIENT_DIFF_FILE="$HOME/time1_sync/time1/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ TIME1:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        if [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No client file need to sync to TIME1"
        else
            ssh TIME1 "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-TIME1_previous/"
            rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ TIME1:~/www/${GAME_TYPE}-TEST/
        fi

        cd $HOME/time1_sync/time1/
        H5_DIFF_FILE="$HOME/time1_sync/time1/h5_diff.txt.$DATETIME"
        rsync -i -avn --out-format="FILE:%n" ~/www/H5-TEST/ TIME1:~/www/H5-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $H5_DIFF_FILE
        if [ "$(wc -l < $H5_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No h5 file need to sync to TIME1"
        else
            ssh TIME1 "rsync -av --progress ~/www/H5-TEST/ ~/www/H5-TIME1_previous/"
            rsync -av --progress ~/www/H5-TEST/ TIME1:~/www/H5-TEST/
        fi
    fi
}

old_test_sync () { # OldTest -> Sync test to OLD_TEST (argument: "server", "client")
    DATETIME="$(/bin/date +%Y%m%dT%H%M)"
    # Server only
    if grep -a -qi "server" <<< "$ALLARGS"; then
        [ ! -d "$HOME/old_test_sync/old_test" ] && mkdir -p $HOME/old_test_sync/old_test
        cd $HOME/old_test_sync/old_test/
        SERVER_DIFF_FILE="$HOME/old_test_sync/old_test/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude --out-format="FILE:%n" ~/common-TEST/ OLD_TEST:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ] && exec echo "ERROR: No server file need to sync to OLD_TEST"
        ssh OLD_TEST "rsync -av --progress ~/common-TEST/ ~/common-OLD_TEST_previous/"
        rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude ~/common-TEST/. OLD_TEST:~/common-TEST/
    # Client only
    elif grep -a -qi "client" <<< "$ALLARGS"; then
        [ ! -d "$HOME/old_test_sync/old_test" ] && mkdir -p $HOME/old_test_sync/old_test
        cd $HOME/old_test_sync/old_test/
        CLIENT_DIFF_FILE="$HOME/old_test_sync/old_test/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ OLD_TEST:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ] && exec echo "ERROR: No client file need to sync to OLD_TEST"
        ssh OLD_TEST "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-OLD_TEST_previous/"
        rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ OLD_TEST:~/www/${GAME_TYPE}-TEST/
    else
        # Both Server and Client
        [ ! -d "$HOME/old_test_sync/old_test" ] && mkdir -p $HOME/old_test_sync/old_test
        cd $HOME/old_test_sync/old_test/
        SERVER_DIFF_FILE="$HOME/old_test_sync/old_test/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude --out-format="FILE:%n" ~/common-TEST/ OLD_TEST:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        if [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No server file need to sync to OLD_TEST"
        else
            ssh OLD_TEST "rsync -av --progress ~/common-TEST/ ~/common-OLD_TEST_previous/"
            rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude ~/common-TEST/. OLD_TEST:~/common-TEST/
        fi

        cd $HOME/old_test_sync/old_test/
        CLIENT_DIFF_FILE="$HOME/old_test_sync/old_test/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ OLD_TEST:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        if [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No client file need to sync to OLD_TEST"
        else
            ssh OLD_TEST "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-OLD_TEST_previous/"
            rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ OLD_TEST:~/www/${GAME_TYPE}-TEST/
        fi
    fi
}

media_sync () { # Media -> Sync test to MEDIA (argument: "server", "client")
    DATETIME="$(/bin/date +%Y%m%dT%H%M)"
    # Server only
    if grep -a -qi "server" <<< "$ALLARGS"; then
        [ ! -d "$HOME/media_sync/media" ] && mkdir -p $HOME/media_sync/media
        cd $HOME/media_sync/media/
        SERVER_DIFF_FILE="$HOME/media_sync/media/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude --out-format="FILE:%n" ~/common-TEST/ MEDIA:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ] && exec echo "ERROR: No server file need to sync to MEDIA"
        ssh MEDIA "rsync -av --progress ~/common-TEST/ ~/common-MEDIA_previous/"
        rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude ~/common-TEST/. MEDIA:~/common-TEST/
    # Client only
    elif grep -a -qi "client" <<< "$ALLARGS"; then
        [ ! -d "$HOME/media_sync/media" ] && mkdir -p $HOME/media_sync/media
        cd $HOME/media_sync/media/
        CLIENT_DIFF_FILE="$HOME/media_sync/media/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ MEDIA:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ] && exec echo "ERROR: No client file need to sync to MEDIA"
        ssh MEDIA "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-MEDIA_previous/"
        rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ MEDIA:~/www/${GAME_TYPE}-TEST/
    else
        # Both Server and Client
        [ ! -d "$HOME/media_sync/media" ] && mkdir -p $HOME/media_sync/media
        cd $HOME/media_sync/media/
        SERVER_DIFF_FILE="$HOME/media_sync/media/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude --out-format="FILE:%n" ~/common-TEST/ MEDIA:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        if [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No server file need to sync to MEDIA"
        else
            ssh MEDIA "rsync -av --progress ~/common-TEST/ ~/common-MEDIA_previous/"
            rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude ~/common-TEST/. MEDIA:~/common-TEST/
        fi

        cd $HOME/media_sync/media/
        CLIENT_DIFF_FILE="$HOME/media_sync/media/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ MEDIA:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        if [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No client file need to sync to MEDIA"
        else
            ssh MEDIA "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-MEDIA_previous/"
            rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ MEDIA:~/www/${GAME_TYPE}-TEST/
        fi
    fi
}

mapping_sync () { # Mapping -> Sync test to MAPPING (argument: "server", "client")
    DATETIME="$(/bin/date +%Y%m%dT%H%M)"
    # Server only
    if grep -a -qi "server" <<< "$ALLARGS"; then
        [ ! -d "$HOME/mapping_sync/mapping" ] && mkdir -p $HOME/mapping_sync/mapping
        cd $HOME/mapping_sync/mapping/
        SERVER_DIFF_FILE="$HOME/mapping_sync/mapping/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude-submit --out-format="FILE:%n" ~/common-TEST/ MAPPING:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ] && exec echo "ERROR: No server file need to sync to MAPPING"
        ssh MAPPING "rsync -av --progress ~/common-TEST/ ~/common-MAPPING_previous/"
        rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. MAPPING:~/common-TEST/
        #ssh MAPPING "sed -i 's/AllGM=1/AllGM=0/' ~/common-TEST/bin/config.ini"
    # Client only
    elif grep -a -qi "client" <<< "$ALLARGS"; then
        [ ! -d "$HOME/mapping_sync/mapping" ] && mkdir -p $HOME/mapping_sync/mapping
        cd $HOME/mapping_sync/mapping/
        CLIENT_DIFF_FILE="$HOME/mapping_sync/mapping/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ MAPPING:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ] && exec echo "ERROR: No client file need to sync to MAPPING"
        ssh MAPPING "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-MAPPING_previous/"
        rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ MAPPING:~/www/${GAME_TYPE}-TEST/
    else
        # Both Server and Client
        [ ! -d "$HOME/mapping_sync/mapping" ] && mkdir -p $HOME/mapping_sync/mapping
        cd $HOME/mapping_sync/mapping/
        SERVER_DIFF_FILE="$HOME/mapping_sync/mapping/server_diff.txt.$DATETIME"
        rsync -i -avn --exclude-from=$HOME/bin/rsync.exclude-submit --out-format="FILE:%n" ~/common-TEST/ MAPPING:~/common-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $SERVER_DIFF_FILE
        if [ "$(wc -l < $SERVER_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No server file need to sync to MAPPING"
        else
            ssh MAPPING "rsync -av --progress ~/common-TEST/ ~/common-MAPPING_previous/"
            rsync -av --progress --exclude-from=$HOME/bin/rsync.exclude-submit ~/common-TEST/. MAPPING:~/common-TEST/
            #ssh MAPPING "sed -i 's/AllGM=1/AllGM=0/' ~/common-TEST/bin/config.ini"
        fi

        cd $HOME/mapping_sync/mapping/
        CLIENT_DIFF_FILE="$HOME/mapping_sync/mapping/client_diff.txt.$DATETIME"
        rsync -i -avn --exclude="setup.txt" --out-format="FILE:%n" ~/www/${GAME_TYPE}-TEST/ MAPPING:~/www/${GAME_TYPE}-TEST/ | grep -a "^FILE:" | grep -a -v "/$" | sed 's/^FILE:/\//g' > $CLIENT_DIFF_FILE
        if [ "$(wc -l < $CLIENT_DIFF_FILE)" == "0" ]; then
            echo "ERROR: No client file need to sync to MAPPING"
        else
            ssh MAPPING "rsync -av --progress ~/www/${GAME_TYPE}-TEST/ ~/www/${GAME_TYPE}-MAPPING_previous/"
            rsync -av --progress --exclude="setup.txt" ~/www/${GAME_TYPE}-TEST/ MAPPING:~/www/${GAME_TYPE}-TEST/
        fi
    fi
}

### Sync Translate of Client ###
time1_sync_translate () { # Time1 -> Sync Translate to TIME1
    ALLARGS=$(echo $ALLARGS | sed 's/time1_sync_translate//;s/^ //')
    echo "ALLARGS => $ALLARGS" | colorize yellow black
    if [ -z "$ALLARGS" ]; then
        for multi_lang_directory in $(grep -E -a TRANSLATED_INI_UPLOAD_DIR_ ${HOME}/.gamerc | grep -a -v '#' | awk -F"=" '{print $2}'); do
            LCOUNTRY=$(echo ${multi_lang_directory} | sed 's/"//g' | awk -F"/" '{print $(NF-1)}' | awk -F"_" '{print $NF}')
            COUNTRY=$(echo ${LCOUNTRY} | tr '[:lower:]' '[:upper:]')

            TEST_TRANSLATE_DIRECTORY=$(echo ${multi_lang_directory} | sed 's/"//g')
            echo "LOOP MULTI => ${COUNTRY}" | colorize red green
            if [ -f "${TEST_TRANSLATE_DIRECTORY}/GameDataTranslateFileList_${LCOUNTRY}.txt" ]; then
                MD5_TEST_TRANSLATE_FILELIST=$(md5sum ${TEST_TRANSLATE_DIRECTORY}/GameDataTranslateFileList_${LCOUNTRY}.txt | awk -F" " '{print $1}')
                MD5_SYNCTOTIME1_TRANSLATE_FILELIST=$(ssh TIME1 "md5sum ~/www/${GAME_TYPE}-TEST/GameDataTranslateFileList_${LCOUNTRY}.txt" | awk -F" " '{print $1}')

                if [ "${MD5_TEST_TRANSLATE_FILELIST}" == "${MD5_SYNCTOTIME1_TRANSLATE_FILELIST}" ]; then
                    echo "ERROR: No ${COUNTRY} translate file to flush or cannot compare with synctolive directory, please try again." | colorize red black
                else
                    cd ${TEST_TRANSLATE_DIRECTORY}
                    rsync -avz GameDataTranslateFileList_${LCOUNTRY}.txt nfs TIME1:~/www/${GAME_TYPE}-TEST/.
                fi
            else
                echo "ERROR: No ${COUNTRY} translate file exists!" | colorize red black
            fi
        done
    else
        COUNTRY=$(echo $ALLARGS | sed 's/time1_sync_translate//;s/^ //')
        LCOUNTRY=$(echo ${COUNTRY} | tr '[:upper:]' '[:lower:]')

        TEST_TRANSLATE_DIRECTORY=$(grep -E -a TRANSLATED_INI_UPLOAD_DIR_${COUNTRY} $HOME/.gamerc | grep -a -v '#' | awk -F"=" '{print $2}' | sed 's/"//g')
        echo "SINGLE => ${COUNTRY}" | colorize red green
        MD5_TEST_TRANSLATE_FILELIST=$(md5sum ${TEST_TRANSLATE_DIRECTORY}/GameDataTranslateFileList_${LCOUNTRY}.txt | awk -F" " '{print $1}')
        MD5_SYNCTOTIME1_TRANSLATE_FILELIST=$(ssh TIME1 "md5sum ~/www/${GAME_TYPE}-TEST/GameDataTranslateFileList_${LCOUNTRY}.txt" | awk -F" " '{print $1}')

        if [ "${MD5_TEST_TRANSLATE_FILELIST}" == "${MD5_SYNCTOTIME1_TRANSLATE_FILELIST}" ]; then
            exec echo "ERROR: No ${COUNTRY} translate file to flush or cannot compare with synctolive directory, please try again." | colorize red black
        else
            cd ${TEST_TRANSLATE_DIRECTORY}
            rsync -avz GameDataTranslateFileList_${LCOUNTRY}.txt nfs TIME1:~/www/${GAME_TYPE}-TEST/.
        fi
    fi
}

media_sync_translate () { # Media -> Sync Translate to MEDIA
    ALLARGS=$(echo $ALLARGS | sed 's/media_sync_translate//;s/^ //')
    echo "ALLARGS => $ALLARGS" | colorize yellow black
    if [ -z "$ALLARGS" ]; then
        for multi_lang_directory in $(grep -E -a TRANSLATED_INI_UPLOAD_DIR_ ${HOME}/.gamerc | grep -a -v '#' | awk -F"=" '{print $2}'); do
            LCOUNTRY=$(echo ${multi_lang_directory} | sed 's/"//g' | awk -F"/" '{print $(NF-1)}' | awk -F"_" '{print $NF}')
            COUNTRY=$(echo ${LCOUNTRY} | tr '[:lower:]' '[:upper:]')

            TEST_TRANSLATE_DIRECTORY=$(echo ${multi_lang_directory} | sed 's/"//g')
            echo "LOOP MULTI => ${COUNTRY}" | colorize red green
            if [ -f "${TEST_TRANSLATE_DIRECTORY}/GameDataTranslateFileList_${LCOUNTRY}.txt" ]; then
                MD5_TEST_TRANSLATE_FILELIST=$(md5sum ${TEST_TRANSLATE_DIRECTORY}/GameDataTranslateFileList_${LCOUNTRY}.txt | awk -F" " '{print $1}')
                MD5_SYNCTOMEDIA_TRANSLATE_FILELIST=$(ssh MEDIA "md5sum ~/www/${GAME_TYPE}-TEST/GameDataTranslateFileList_${LCOUNTRY}.txt" | awk -F" " '{print $1}')

                if [ "${MD5_TEST_TRANSLATE_FILELIST}" == "${MD5_SYNCTOMEDIA_TRANSLATE_FILELIST}" ]; then
                    echo "ERROR: No ${COUNTRY} translate file to flush or cannot compare with synctolive directory, please try again." | colorize red black
                else
                    cd ${TEST_TRANSLATE_DIRECTORY}
                    rsync -avz GameDataTranslateFileList_${LCOUNTRY}.txt nfs MEDIA:~/www/${GAME_TYPE}-TEST/.
                fi
            else
                echo "ERROR: No ${COUNTRY} translate file exists!" | colorize red black
            fi
        done
    else
        COUNTRY=$(echo $ALLARGS | sed 's/media_sync_translate//;s/^ //')
        LCOUNTRY=$(echo ${COUNTRY} | tr '[:upper:]' '[:lower:]')

        TEST_TRANSLATE_DIRECTORY=$(grep -E -a TRANSLATED_INI_UPLOAD_DIR_${COUNTRY} $HOME/.gamerc | grep -a -v '#' | awk -F"=" '{print $2}' | sed 's/"//g')
        echo "SINGLE => ${COUNTRY}" | colorize red green
        MD5_TEST_TRANSLATE_FILELIST=$(md5sum ${TEST_TRANSLATE_DIRECTORY}/GameDataTranslateFileList_${LCOUNTRY}.txt | awk -F" " '{print $1}')
        MD5_SYNCTOMEDIA_TRANSLATE_FILELIST=$(ssh MEDIA "md5sum ~/www/${GAME_TYPE}-TEST/GameDataTranslateFileList_${LCOUNTRY}.txt" | awk -F" " '{print $1}')

        if [ "${MD5_TEST_TRANSLATE_FILELIST}" == "${MD5_SYNCTOMEDIA_TRANSLATE_FILELIST}" ]; then
            exec echo "ERROR: No ${COUNTRY} translate file to flush or cannot compare with synctolive directory, please try again." | colorize red black
        else
            cd ${TEST_TRANSLATE_DIRECTORY}
            rsync -avz GameDataTranslateFileList_${LCOUNTRY}.txt nfs MEDIA:~/www/${GAME_TYPE}-TEST/.
        fi
    fi
}

### Game Server DBPatch ###
android_dbpatch () { # Submit -> Patch the databases to GOOGLE-SUBMIT
    if ! [ "$DO_NOT_RUN_DBBACKUP_BEFORE_DBPATCH" == "1" ]; then
        ssh GOOGLE-SUBMIT "DBbackup ${GAME_TYPE}Member"
        ssh GOOGLE-SUBMIT "DBbackup ${GAME_TYPE}Account"
        ssh GOOGLE-SUBMIT "DBbackup ${GAME_TYPE}DB1"
    fi

    ssh GOOGLE-SUBMIT "sed -i -e 's/system (\"pg_dump/#system (\"pg_dump/g;s/^\(.*vacuum.*\)$/#\1/g' common-GOOGLE-SUBMIT/db/dbalter"
    ssh GOOGLE-SUBMIT "cd common-GOOGLE-SUBMIT/db;perl dbalter :${GAME_TYPE}DB1"

    ssh GOOGLE-SUBMIT "sed -i -e 's/system (\"pg_dump/#system (\"pg_dump/g;s/^\(.*vacuum.*\)$/#\1/g' common-GOOGLE-SUBMIT/db/db_account_alter"
    ssh GOOGLE-SUBMIT "cd common-GOOGLE-SUBMIT/db;perl db_account_alter :${GAME_TYPE}Account"

    if ! [ "$DO_NOT_RUN_DBVACUUM_AFTER_DBPATCH" == "1" ]; then
        ssh GOOGLE-SUBMIT "psql -U postgres <<< 'vacuum analyze;vacuum full;'"
    fi
}

ios_dbpatch () { # Submit -> Patch the databases to APPLE-SUBMIT
    if ! [ "$DO_NOT_RUN_DBBACKUP_BEFORE_DBPATCH" == "1" ]; then
        ssh APPLE-SUBMIT "DBbackup ${GAME_TYPE}Member"
        ssh APPLE-SUBMIT "DBbackup ${GAME_TYPE}Account"
        ssh APPLE-SUBMIT "DBbackup ${GAME_TYPE}DB1"
    fi

    ssh APPLE-SUBMIT "sed -i -e 's/system (\"pg_dump/#system (\"pg_dump/g;s/^\(.*vacuum.*\)$/#\1/g' common-APPLE-SUBMIT/db/dbalter"
    ssh APPLE-SUBMIT "cd common-APPLE-SUBMIT/db;perl dbalter :${GAME_TYPE}DB1"

    ssh APPLE-SUBMIT "sed -i -e 's/system (\"pg_dump/#system (\"pg_dump/g;s/^\(.*vacuum.*\)$/#\1/g' common-APPLE-SUBMIT/db/db_account_alter"
    ssh APPLE-SUBMIT "cd common-APPLE-SUBMIT/db;perl db_account_alter :${GAME_TYPE}Account"

    if ! [ "$DO_NOT_RUN_DBVACUUM_AFTER_DBPATCH" == "1" ]; then
        ssh APPLE-SUBMIT "psql -U postgres <<< 'vacuum analyze;vacuum full;'"
    fi
}

submit_dbpatch () { # Submit -> Patch the databases to SUBMIT
    ssh SUBMIT "cd common-TEST/db;perl db_account_alter :${ACCOUNT_DB_NAME}"

    ssh SUBMIT "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}10"
    ssh SUBMIT "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}20"
    ssh SUBMIT "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}30"

    ssh SUBMIT "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}1001"
    ssh SUBMIT "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}2001"
    ssh SUBMIT "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}3001"

    if [ "$ENABLE_SOCIETY" == "1" ]; then
        ssh SUBMIT "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}10"
        ssh SUBMIT "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}20"
        ssh SUBMIT "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}30"
    fi
}

data_dbpatch () { # Data -> Patch the databases to DATA
    ssh DATA "cd common-TEST/db;perl db_account_alter :${ACCOUNT_DB_NAME}"

    ssh DATA "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}10"
    ssh DATA "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}20"
    ssh DATA "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}30"

    ssh DATA "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}1001"
    ssh DATA "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}1002"
    ssh DATA "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}1003"
    ssh DATA "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}2001"
    ssh DATA "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}2002"
    ssh DATA "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}2003"
    ssh DATA "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}3001"
    ssh DATA "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}3002"
    ssh DATA "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}3003"

    if [ "$ENABLE_SOCIETY" == "1" ]; then
        ssh DATA "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}10"
        ssh DATA "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}20"
        ssh DATA "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}30"
    fi
}

time1_dbpatch () { # Time1 -> Patch the databases to TIME1
    ssh TIME1 "cd common-TEST/db;perl db_account_alter :${ACCOUNT_DB_NAME}"

    ssh TIME1 "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}10"
    ssh TIME1 "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}20"
    ssh TIME1 "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}30"

    ssh TIME1 "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}1001"
    ssh TIME1 "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}2001"
    ssh TIME1 "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}3001"

    if [ "$ENABLE_SOCIETY" == "1" ]; then
        ssh TIME1 "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}10"
        ssh TIME1 "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}20"
        ssh TIME1 "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}30"
    fi
}

old_test_dbpatch () { # OldTest -> Patch the databases to OLD_TEST
    ssh OLD_TEST "cd common-TEST/db;perl db_account_alter :${ACCOUNT_DB_NAME}"

    ssh OLD_TEST "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}10"
    ssh OLD_TEST "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}20"
    ssh OLD_TEST "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}30"

    ssh OLD_TEST "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}1001"
    ssh OLD_TEST "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}2001"
    ssh OLD_TEST "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}3001"

    if [ "$ENABLE_SOCIETY" == "1" ]; then
        ssh OLD_TEST "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}10"
        ssh OLD_TEST "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}20"
        ssh OLD_TEST "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}30"
    fi
}

media_dbpatch () { # Media -> Patch the databases to MEDIA
    ssh MEDIA "cd common-TEST/db;perl db_account_alter :${ACCOUNT_DB_NAME}"

    ssh MEDIA "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}10"
    ssh MEDIA "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}20"
    ssh MEDIA "cd common-TEST/db;perl db_game_alter :${GAME_DB_NAME}30"

    ssh MEDIA "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}1001"
    ssh MEDIA "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}2001"
    ssh MEDIA "cd common-TEST/db;perl db_world_alter :${WORLD_DB_NAME}3001"

    if [ "$ENABLE_SOCIETY" == "1" ]; then
        ssh MEDIA "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}10"
        ssh MEDIA "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}20"
        ssh MEDIA "cd common-TEST/db;perl db_society_alter :${SOCIETY_DB}30"
    fi
}

mapping_dbpatch () { # Mapping -> Patch the databases to MAPPING
    if ! [ "$DO_NOT_RUN_DBBACKUP_BEFORE_DBPATCH" == "1" ]; then
        ssh MAPPING "DBbackup ${MEMBER_DB_NAME}"
        ssh MAPPING "DBbackup ${ACCOUNT_DB_NAME}"
        if [ "$ENABLE_SOCIETY" == "1" ]; then
            ssh MAPPING "DBbackup ${GAME_TYPE}Society"
        fi
        ssh MAPPING "DBbackup ${GAME_DB_NAME}1"
        ssh MAPPING "DBbackup ${GAME_DB_NAME}2"
        ssh MAPPING "DBbackup ${GAME_DB_NAME}${CROSS_ROUTER_SID_TEST}"
    fi

    ssh MAPPING "sed -i -e 's/system (\"pg_dump/#system (\"pg_dump/g;s/^\(.*vacuum.*\)$/#\1/g' common-TEST/db/dbalter"
    ssh MAPPING "sed -i -e 's/system (\"pg_dump/#system (\"pg_dump/g;s/^\(.*vacuum.*\)$/#\1/g' common-TEST/db/db_account_alter"
    ssh MAPPING "cd common-TEST/db;perl db_account_alter :${GAME_TYPE}Account"
    ssh MAPPING "cd common-TEST/db;perl db_account_alter :${ACCOUNT_DB_NAME}"

    if [ "$ENABLE_SOCIETY" == "1" ]; then
        ssh MAPPING "sed -i -e 's/system (\"pg_dump/#system (\"pg_dump/g;s/^\(.*vacuum.*\)$/#\1/g' common-TEST/db/db_society_alter"
        ssh MAPPING "cd common-TEST/db;perl db_society_alter :${GAME_TYPE}Society"
    fi

    if ! [ "$DO_NOT_RUN_DBVACUUM_AFTER_DBPATCH" == "1" ]; then
        ssh MAPPING "psql -U postgres <<< 'vacuum analyze;vacuum full;'"
    fi
}

### Game Server Itemmall ###
android_itemmall () { # Submit -> Update Promotion Files/Tables from TEST Server to GOOGLE-SUBMIT (argument: table names)

    TABLES_TO_APPLY="$ARG2 $ARG3 $ARG4 $ARG5 $ARG6 $ARG7"
    grep -a -q "[A-Za-z]" <<< "$TABLES_TO_APPLY" && PROMOTION_TABLES_APPLY_TO_LIVE="$TABLES_TO_APPLY"

    if [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "2" ]; then # 2 = File(on TEST)  -->  Table(on GOOGLE-SUBMIT)
        echo "Mode 2: File(on TEST)  -->  Table(on GOOGLE-SUBMIT)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on TEST)  -->  Table(on GOOGLE-SUBMIT):" | colorize yellow
            ssh $TEST_SERVER_USER@GOOGLE-SUBMIT "cat ~/common-$SOURCE_OF_SYNC_TO_LIVE/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh GOOGLE-SUBMIT "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    elif [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "3" ]; then # 3 = File(on GOOGLE-SUBMIT)  -->  Table(on GOOGLE-SUBMIT)
        echo "Mode 3: File(on GOOGLE-SUBMIT)  -->  Table(on GOOGLE-SUBMIT)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on GOOGLE-SUBMIT)  -->  Table(on GOOGLE-SUBMIT):" | colorize yellow
            ssh GOOGLE-SUBMIT "cat ~/servers1/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh GOOGLE-SUBMIT "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    else
        echo "Mode 1: Table(on TEST) -->  Table(on GOOGLE-SUBMIT)" | colorize green
        for TABLE in $(psql -U postgres Tools -tAc "SELECT table_name_db FROM web_table_mapping WHERE table_name_db NOT LIKE 'x_%' ORDER BY table_name_db;"); do # 1 = Table(on TEST) -->  Table(on GOOGLE-SUBMIT)
            echo "Syncing table $TABLE(on TEST) -->  Table(on GOOGLE-SUBMIT):" | colorize yellow black
            ssh $TEST_SERVER_USER@TEST pg_dump --clean --attribute-inserts -t $TABLE $ACCOUNT_DB_NAME | grep -a -v "SET lock_timeout" | sed '/^\\unrestrict /d' | sed '/^\\restrict /d' | ssh GOOGLE-SUBMIT "psql $ACCOUNT_DB_NAME" | uniq -c &&
                echo "Table $TABLE done." | colorize green ||
                echo "Table $TABLE failed." | colorize red
        done
    fi

    echo "
Remember, if you are updating the Promotion Files/Tables on running servers,
You will have to run these GM commands to apply in game:
1. /reload_itemmall_db
2. /rnba
3. /reload_daily_login_reward
4. /reload_playground_db
5. /reload_iap_db" | colorize green
}

ios_itemmall () { # Submit -> Update Promotion Files/Tables from TEST Server to APPLE-SUBMIT (argument: table names)

    TABLES_TO_APPLY="$ARG2 $ARG3 $ARG4 $ARG5 $ARG6 $ARG7"
    grep -a -q "[A-Za-z]" <<< "$TABLES_TO_APPLY" && PROMOTION_TABLES_APPLY_TO_LIVE="$TABLES_TO_APPLY"

    if [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "2" ]; then # 2 = File(on TEST)  -->  Table(on APPLE-SUBMIT)
        echo "Mode 2: File(on TEST)  -->  Table(on APPLE-SUBMIT)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on TEST)  -->  Table(on APPLE-SUBMIT):" | colorize yellow
            ssh $TEST_SERVER_USER@APPLE-SUBMIT "cat ~/common-$SOURCE_OF_SYNC_TO_LIVE/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh APPLE-SUBMIT "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    elif [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "3" ]; then # 3 = File(on APPLE-SUBMIT)  -->  Table(on APPLE-SUBMIT)
        echo "Mode 3: File(on APPLE-SUBMIT)  -->  Table(on APPLE-SUBMIT)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on APPLE-SUBMIT)  -->  Table(on APPLE-SUBMIT):" | colorize yellow
            ssh APPLE-SUBMIT "cat ~/servers1/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh APPLE-SUBMIT "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    else
        echo "Mode 1: Table(on TEST) -->  Table(on APPLE-SUBMIT)" | colorize green
        for TABLE in $(psql -U postgres Tools -tAc "SELECT table_name_db FROM web_table_mapping WHERE table_name_db NOT LIKE 'x_%' ORDER BY table_name_db;"); do # 1 = Table(on TEST) -->  Table(on APPLE-SUBMIT)
            echo "Syncing table $TABLE(on TEST) -->  Table(on APPLE-SUBMIT):" | colorize yellow black
            ssh $TEST_SERVER_USER@TEST pg_dump --clean --attribute-inserts -t $TABLE $ACCOUNT_DB_NAME | grep -a -v "SET lock_timeout" | sed '/^\\unrestrict /d' | sed '/^\\restrict /d' | ssh APPLE-SUBMIT "psql $ACCOUNT_DB_NAME" | uniq -c &&
                echo "Table $TABLE done." | colorize green ||
                echo "Table $TABLE failed." | colorize red
        done
    fi

    echo "
Remember, if you are updating the Promotion Files/Tables on running servers,
You will have to run these GM commands to apply in game:
1. /reload_itemmall_db
2. /rnba
3. /reload_daily_login_reward
4. /reload_playground_db
5. /reload_iap_db" | colorize green
}

submit_itemmall () { # Submit -> Update Promotion Files/Tables from TEST Server to SUBMIT (argument: table names)

    TABLES_TO_APPLY="$ARG2 $ARG3 $ARG4 $ARG5 $ARG6 $ARG7"
    grep -a -q "[A-Za-z]" <<< "$TABLES_TO_APPLY" && PROMOTION_TABLES_APPLY_TO_LIVE="$TABLES_TO_APPLY"

    if [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "2" ]; then # 2 = File(on TEST)  -->  Table(on SUBMIT)
        echo "Mode 2: File(on TEST)  -->  Table(on SUBMIT)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on TEST)  -->  Table(on SUBMIT):" | colorize yellow
            ssh $TEST_SERVER_USER@SUBMIT "cat ~/common-$SOURCE_OF_SYNC_TO_LIVE/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh SUBMIT "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    elif [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "3" ]; then # 3 = File(on SUBMIT)  -->  Table(on SUBMIT)
        echo "Mode 3: File(on SUBMIT)  -->  Table(on SUBMIT)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on SUBMIT)  -->  Table(on SUBMIT):" | colorize yellow
            ssh SUBMIT "cat ~/servers1/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh SUBMIT "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    else
        echo "Mode 1: Table(on TEST) -->  Table(on SUBMIT)" | colorize green
        for TABLE in $(psql -U postgres Tools -tAc "SELECT table_name_db FROM web_table_mapping WHERE table_name_db NOT LIKE 'x_%' ORDER BY table_name_db;"); do # 1 = Table(on TEST) -->  Table(on SUBMIT)
            echo "Syncing table $TABLE(on TEST) -->  Table(on SUBMIT):" | colorize yellow black
            ssh $TEST_SERVER_USER@TEST pg_dump --clean --attribute-inserts -t $TABLE $ACCOUNT_DB_NAME | grep -a -v "SET lock_timeout" | sed '/^\\unrestrict /d' | sed '/^\\restrict /d' | ssh SUBMIT "psql $ACCOUNT_DB_NAME" | uniq -c &&
                echo "Table $TABLE done." | colorize green ||
                echo "Table $TABLE failed." | colorize red
        done
    fi

    echo "
Remember, if you are updating the Promotion Files/Tables on running servers,
You will have to run these GM commands to apply in game:
1. /reload_itemmall_db
2. /rnba
3. /reload_daily_login_reward
4. /reload_playground_db
5. /reload_iap_db" | colorize green
}

data_itemmall () { # Data -> Update Promotion Files/Tables from TEST Server to DATA (argument: table names)

    TABLES_TO_APPLY="$ARG2 $ARG3 $ARG4 $ARG5 $ARG6 $ARG7"
    grep -a -q "[A-Za-z]" <<< "$TABLES_TO_APPLY" && PROMOTION_TABLES_APPLY_TO_LIVE="$TABLES_TO_APPLY"

    if [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "2" ]; then # 2 = File(on TEST)  -->  Table(on DATA)
        echo "Mode 2: File(on TEST)  -->  Table(on DATA)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on TEST)  -->  Table(on DATA):" | colorize yellow
            ssh $TEST_SERVER_USER@DATA "cat ~/common-$SOURCE_OF_SYNC_TO_LIVE/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh DATA "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    elif [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "3" ]; then # 3 = File(on DATA)  -->  Table(on DATA)
        echo "Mode 3: File(on DATA)  -->  Table(on DATA)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on DATA)  -->  Table(on DATA):" | colorize yellow
            ssh DATA "cat ~/servers1/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh DATA "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    else
        echo "Mode 1: Table(on TEST) -->  Table(on DATA)" | colorize green
        for TABLE in $(psql -U postgres Tools -tAc "SELECT table_name_db FROM web_table_mapping WHERE table_name_db NOT LIKE 'x_%' ORDER BY table_name_db;"); do # 1 = Table(on TEST) -->  Table(on DATA)
            echo "Syncing table $TABLE(on TEST) -->  Table(on DATA):" | colorize yellow black
            ssh $TEST_SERVER_USER@TEST pg_dump --clean --attribute-inserts -t $TABLE $ACCOUNT_DB_NAME | grep -a -v "SET lock_timeout" | sed '/^\\unrestrict /d' | sed '/^\\restrict /d' | ssh DATA "psql $ACCOUNT_DB_NAME" | uniq -c &&
                echo "Table $TABLE done." | colorize green ||
                echo "Table $TABLE failed." | colorize red
        done
    fi

    echo "
Remember, if you are updating the Promotion Files/Tables on running servers,
You will have to run these GM commands to apply in game:
1. /reload_itemmall_db
2. /rnba
3. /reload_daily_login_reward
4. /reload_playground_db
5. /reload_iap_db" | colorize green
}

time1_itemmall () { # Time1 -> Update Promotion Files/Tables from TEST Server to TIME1 (argument: table names)

    TABLES_TO_APPLY="$ARG2 $ARG3 $ARG4 $ARG5 $ARG6 $ARG7"
    grep -a -q "[A-Za-z]" <<< "$TABLES_TO_APPLY" && PROMOTION_TABLES_APPLY_TO_LIVE="$TABLES_TO_APPLY"

    if [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "2" ]; then # 2 = File(on TEST)  -->  Table(on TIME1)
        echo "Mode 2: File(on TEST)  -->  Table(on TIME1)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on TEST)  -->  Table(on TIME1):" | colorize yellow
            cat ~/common-$SOURCE_OF_SYNC_TO_LIVE/$SQL_FILE | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh TIME1 "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    elif [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "3" ]; then # 3 = File(on TIME1)  -->  Table(on TIME1)
        echo "Mode 3: File(on TIME1)  -->  Table(on TIME1)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on TIME1)  -->  Table(on TIME1):" | colorize yellow
            ssh TIME1 "cat ~/servers1/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh TIME1 "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    else
        echo "Mode 1: Table(on TEST) -->  Table(on TIME1)" | colorize green
        for TABLE in $(psql -U postgres Tools -tAc "SELECT table_name_db FROM web_table_mapping WHERE table_name_db NOT LIKE 'x_%' ORDER BY table_name_db;"); do # 1 = Table(on TEST) -->  Table(on TIME1)
            echo "Syncing table $TABLE(on TEST) -->  Table(on TIME1):" | colorize yellow black
            pg_dump --clean --attribute-inserts -t $TABLE $ACCOUNT_DB_NAME | grep -a -v "SET lock_timeout" | sed '/^\\unrestrict /d' | sed '/^\\restrict /d' | ssh TIME1 "psql $ACCOUNT_DB_NAME" | uniq -c &&
                echo "Table $TABLE done." | colorize green ||
                echo "Table $TABLE failed." | colorize red
        done
    fi

    $HOME/bin/send_fastcgi_time1_n1 reload_activity_db
    $HOME/bin/send_fastcgi_time1_n1 reload_itemmall_db
    $HOME/bin/send_fastcgi_time1_n1 reload_cheatdetection_db
    echo "
Remember, if you are updating the Promotion Files/Tables on running servers,
You will have to run these GM commands to apply in game:
1. /reload_itemmall_db
2. /rnba
3. /reload_daily_login_reward
4. /reload_playground_db
5. /reload_iap_db" | colorize green
}

old_test_itemmall () { # OldTest -> Update Promotion Files/Tables from TEST Server to OLD_TEST (argument: table names)

    TABLES_TO_APPLY="$ARG2 $ARG3 $ARG4 $ARG5 $ARG6 $ARG7"
    grep -a -q "[A-Za-z]" <<< "$TABLES_TO_APPLY" && PROMOTION_TABLES_APPLY_TO_LIVE="$TABLES_TO_APPLY"

    if [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "2" ]; then # 2 = File(on TEST)  -->  Table(on OLD_TEST)
        echo "Mode 2: File(on TEST)  -->  Table(on OLD_TEST)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on TEST)  -->  Table(on OLD_TEST):" | colorize yellow
            cat ~/common-$SOURCE_OF_SYNC_TO_LIVE/$SQL_FILE | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh OLD_TEST "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    elif [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "3" ]; then # 3 = File(on OLD_TEST)  -->  Table(on OLD_TEST)
        echo "Mode 3: File(on OLD_TEST)  -->  Table(on OLD_TEST)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on OLD_TEST)  -->  Table(on OLD_TEST):" | colorize yellow
            ssh OLD_TEST "cat ~/servers1/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh OLD_TEST "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    else
        echo "Mode 1: Table(on TEST) -->  Table(on OLD_TEST)" | colorize green
        for TABLE in $(psql -U postgres Tools -tAc "SELECT table_name_db FROM web_table_mapping WHERE table_name_db NOT LIKE 'x_%' ORDER BY table_name_db;"); do # 1 = Table(on TEST) -->  Table(on OLD_TEST)
            echo "Syncing table $TABLE(on TEST) -->  Table(on OLD_TEST):" | colorize yellow black
            pg_dump --clean --attribute-inserts -t $TABLE $ACCOUNT_DB_NAME | grep -a -v "SET lock_timeout" | sed '/^\\unrestrict /d' | sed '/^\\restrict /d' | ssh OLD_TEST "psql $ACCOUNT_DB_NAME" | uniq -c &&
                echo "Table $TABLE done." | colorize green ||
                echo "Table $TABLE failed." | colorize red
        done
    fi

    echo "
Remember, if you are updating the Promotion Files/Tables on running servers,
You will have to run these GM commands to apply in game:
1. /reload_itemmall_db
2. /rnba
3. /reload_daily_login_reward
4. /reload_playground_db
5. /reload_iap_db" | colorize green
}

media_itemmall () { # Media -> Update Promotion Files/Tables from TEST Server to MEDIA (argument: table names)

    TABLES_TO_APPLY="$ARG2 $ARG3 $ARG4 $ARG5 $ARG6 $ARG7"
    grep -a -q "[A-Za-z]" <<< "$TABLES_TO_APPLY" && PROMOTION_TABLES_APPLY_TO_LIVE="$TABLES_TO_APPLY"

    if [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "2" ]; then # 2 = File(on TEST)  -->  Table(on MEDIA)
        echo "Mode 2: File(on TEST)  -->  Table(on MEDIA)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on TEST)  -->  Table(on MEDIA):" | colorize yellow
            cat ~/common-$SOURCE_OF_SYNC_TO_LIVE/$SQL_FILE | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh MEDIA "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    elif [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "3" ]; then # 3 = File(on MEDIA)  -->  Table(on MEDIA)
        echo "Mode 3: File(on MEDIA)  -->  Table(on MEDIA)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on MEDIA)  -->  Table(on MEDIA):" | colorize yellow
            ssh MEDIA "cat ~/servers1/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh MEDIA "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    else
        echo "Mode 1: Table(on TEST) -->  Table(on MEDIA)" | colorize green
        for TABLE in $(psql -U postgres Tools -tAc "SELECT table_name_db FROM web_table_mapping WHERE table_name_db NOT LIKE 'x_%' ORDER BY table_name_db;"); do # 1 = Table(on TEST) -->  Table(on MEDIA)
            echo "Syncing table $TABLE(on TEST) -->  Table(on MEDIA):" | colorize yellow black
            pg_dump --clean --attribute-inserts -t $TABLE $ACCOUNT_DB_NAME | grep -a -v "SET lock_timeout" | sed '/^\\unrestrict /d' | sed '/^\\restrict /d' | ssh MEDIA "psql $ACCOUNT_DB_NAME" | uniq -c &&
                echo "Table $TABLE done." | colorize green ||
                echo "Table $TABLE failed." | colorize red
        done
    fi

    echo "
Remember, if you are updating the Promotion Files/Tables on running servers,
You will have to run these GM commands to apply in game:
1. /reload_itemmall_db
2. /rnba
3. /reload_daily_login_reward
4. /reload_playground_db
5. /reload_iap_db" | colorize green
}

mapping_itemmall () { # Mapping -> Update Promotion Files/Tables from TEST Server to MAPPING (argument: table names)

    TABLES_TO_APPLY="$ARG2 $ARG3 $ARG4 $ARG5 $ARG6 $ARG7"
    grep -a -q "[A-Za-z]" <<< "$TABLES_TO_APPLY" && PROMOTION_TABLES_APPLY_TO_LIVE="$TABLES_TO_APPLY"

    if [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "2" ]; then # 2 = File(on TEST)  -->  Table(on MAPPING)
        echo "Mode 2: File(on TEST)  -->  Table(on MAPPING)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on TEST)  -->  Table(on MAPPING):" | colorize yellow
            ssh $TEST_SERVER_USER@MAPPING "cat ~/common-$SOURCE_OF_SYNC_TO_LIVE/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh MAPPING "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    elif [ "$PROMOTION_DATA_APPLY_TO_LIVE_MODE" == "3" ]; then # 3 = File(on MAPPING)  -->  Table(on MAPPING)
        echo "Mode 3: File(on MAPPING)  -->  Table(on MAPPING)" | colorize green
        for SQL_FILE in $PROMOTION_FILES_APPLY_TO_LIVE; do
            echo "File $SQL_FILE(on MAPPING)  -->  Table(on MAPPING):" | colorize yellow
            ssh MAPPING "cat ~/servers1/$SQL_FILE" | grep -a -v "GRANT ALL" | sed 's/PUBLIC/PUBLIC\;/g' | ssh MAPPING "psql $ACCOUNT_DB_NAME" 2>&1 | uniq -c
        done

    else
        echo "Mode 1: Table(on TEST) -->  Table(on MAPPING)" | colorize green
        for TABLE in $(psql -U postgres Tools -tAc "SELECT table_name_db FROM web_table_mapping WHERE table_name_db NOT LIKE 'x_%' ORDER BY table_name_db;"); do # 1 = Table(on TEST) -->  Table(on MAPPING)
            echo "Syncing table $TABLE(on TEST) -->  Table(on MAPPING):" | colorize yellow black
            ssh $TEST_SERVER_USER@TEST pg_dump --clean --attribute-inserts -t $TABLE $ACCOUNT_DB_NAME | grep -a -v "SET lock_timeout" | sed '/^\\unrestrict /d' | sed '/^\\restrict /d' | ssh MAPPING "psql $ACCOUNT_DB_NAME" | uniq -c &&
                echo "Table $TABLE done." | colorize green ||
                echo "Table $TABLE failed." | colorize red
        done
    fi

    echo "
Remember, if you are updating the Promotion Files/Tables on running servers,
You will have to run these GM commands to apply in game:
1. /reload_itemmall_db
2. /rnba
3. /reload_daily_login_reward
4. /reload_playground_db
5. /reload_iap_db" | colorize green
}

online_reload_cgi () {
    local TABLE_NAME=$1
    if [[ -z "$TABLE_NAME" ]]; then
        echo "用法: online_reload_cgi <table_name>" >&2
        return 1
    fi

    # Tools DB 連線（env 可覆蓋；預設依該機器主要環境調整）
    local DB="${TOOLS_DB_NAME:-Tools}"
    local DBUSER="${TOOLS_DB_USER:-postgres}"
    local DBHOST="${TOOLS_DB_HOST:-127.0.0.1}"

    # Debug：顯示這次連哪個 DB
    echo "[online_reload_cgi] query ${DB}@${DBHOST} for table: ${TABLE_NAME}"

    # 查 web_table_mapping → cgi_cmd
    local RESULT
    RESULT=$(psql -U "${DBUSER}" -h "${DBHOST}" -d "${DB}" -tAc \
        "SELECT COALESCE(NULLIF(cgi_cmd, ''), '__NOCMD__') FROM web_table_mapping WHERE table_name_db = '${TABLE_NAME}'" \
        2>/dev/null | xargs)

    if [[ -z "${RESULT}" ]]; then
        local MSG="❗ 資料表 ${TABLE_NAME} 未登錄於 web_table_mapping，請至 Tools 後台「資料表管理」新增"
        echo "$MSG"
        $HOME/bin/automatic_send_chatbot_text_only_by_thread_tag "itemmall" "$MSG" minute 10
        return 1
    fi

    if [[ "${RESULT}" == "__NOCMD__" ]]; then
        $HOME/bin/automatic_send_chatbot_text_only_by_thread_tag "itemmall" "資料表 ${TABLE_NAME} 同步完成" minute 10
        return 0
    fi

    local CGI_CMD="${RESULT}"

    # ── server online 才走 CGI 發送 + log 驗證流程 ──────────────────────
    server_online_check
    local SOC=$?
    echo "[online_reload_cgi] server_online_check returned: ${SOC}"

    if [ ${SOC} -ne 1 ]; then
        $HOME/bin/automatic_send_chatbot_text_only_by_thread_tag "itemmall" "資料表 ${TABLE_NAME} 同步完成" minute 10
        return 0
    fi

    # ── 第一則通知：同步完成 ────────────────────────────────────────────
    $HOME/bin/automatic_send_chatbot_text_only_by_thread_tag "itemmall" "資料表 ${TABLE_NAME} 同步完成" minute 10

    # ── 發送 CGI（全部 MS）──────────────────────────────────────────────
    echo "[online_reload_cgi] sending CGI: ${CGI_CMD}"
    $HOME/bin/send_fastcgi_all_n1 ${CGI_CMD}
    local CGI_RC=$?

    if [[ ${CGI_RC} -ne 0 ]]; then
        echo "[online_reload_cgi] CGI 發送失敗 (rc=${CGI_RC})"
        $HOME/bin/automatic_send_chatbot_text_only_by_thread_tag "itemmall" "❌ CGI ${CGI_CMD} 發送失敗 (資料表 ${TABLE_NAME})" minute 10
        return 1
    fi

    # ── Log 驗證 + 單台補送 ─────────────────────────────────────────────
    local HOSTS_FILE="$HOME/bin/hosts"
    local MAX_RETRY=5
    local SLEEP_SEC=3
    local CURL_RETRY=3
    local CURL_SLEEP=5
    local CGI_KEY="0KjaM85BjfqjA"
    local GREP_PATTERN="CGI use '${CGI_CMD} ' !"

    local MS_LIST
    MS_LIST=$(grep -oP 'MS\d+' "${HOSTS_FILE}" | sort -u)

    if [[ -z "${MS_LIST}" ]]; then
        echo "[online_reload_cgi] hosts 中找不到任何 MS server"
        $HOME/bin/automatic_send_chatbot_text_only_by_thread_tag "itemmall" "CGI ${CGI_CMD} 已執行，但 hosts 中無 MS server 可驗證 (資料表 ${TABLE_NAME})" minute 10
        return 0
    fi

    echo "[online_reload_cgi] 驗證 log: pattern='${GREP_PATTERN}'"
    echo "[online_reload_cgi] MS servers: ${MS_LIST}"

    # ── helper: 搜尋某台 MS 的 GMCommand log ───────────────────────────
    _check_ms_log() {
        local ms=$1 set_id=$2
        local log_dir="\$HOME/servers${set_id}/MissionServer${set_id}"
        ssh -o ConnectTimeout=5 -o BatchMode=yes "${ms}" \
            "cd ${log_dir} 2>/dev/null && ls -t GMCommand*log* 2>/dev/null | head -1 | xargs -I{} grep -q \"${GREP_PATTERN}\" {} 2>/dev/null" \
            < /dev/null 2>/dev/null
    }

    # ── helper: 對單台 MS 補送 CGI ─────────────────────────────────────
    _resend_cgi_to_ms() {
        local ms=$1 set_id=$2
        local ip port
        ip=$(getent hosts "${ms}" | awk '{print $1; exit}')
        if [[ -z "$ip" ]]; then
            echo "[online_reload_cgi]   ${ms}: 無法解析 IP"
            return 1
        fi
        port=$(ssh -o ConnectTimeout=5 -o BatchMode=yes "${ms}" \
            "grep '^CGIPort=' \"\$HOME/servers${set_id}/MissionServer${set_id}/setup.ini\" 2>/dev/null | sed 's/CGIPort=//'" \
            < /dev/null 2>/dev/null)
        if [[ -z "$port" ]]; then
            echo "[online_reload_cgi]   ${ms}: 無法取得 CGIPort"
            return 1
        fi

        local cgi_url="http://127.0.0.1/cgi-bin/CGIAgent.fcgi?${ip},${port},${CGI_KEY},${CGI_CMD}"
        local try
        for try in $(seq 1 ${CURL_RETRY}); do
            echo "[online_reload_cgi]   ${ms}: 補送 CGI (attempt ${try}/${CURL_RETRY}): ${cgi_url}"
            if curl --connect-timeout 5 -m 30 -sf "${cgi_url}" < /dev/null; then
                echo ""
                echo "[online_reload_cgi]   ${ms}: 補送 curl OK (attempt ${try})"
                return 0
            else
                echo "[online_reload_cgi]   ${ms}: 補送 curl FAILED (attempt ${try})"
                [ "$try" -lt "${CURL_RETRY}" ] && sleep "${CURL_SLEEP}"
            fi
        done
        return 1
    }

    # ── 主迴圈：驗證每台 MS，失敗則補送後再驗證 ────────────────────────
    local all_ok=1
    local failed_ms=""

    for MS in ${MS_LIST}; do
        local SET_ID="${MS#MS}"
        echo "[online_reload_cgi] 檢查 ${MS} (S${SET_ID})..."

        # 第一輪驗證
        local found=0
        local attempt=1
        while [ "$attempt" -le "$MAX_RETRY" ]; do
            echo "[online_reload_cgi]   ${MS}: verify attempt ${attempt}/${MAX_RETRY}"
            if _check_ms_log "${MS}" "${SET_ID}"; then
                echo "[online_reload_cgi]   ${MS}: ✅ found (attempt ${attempt})"
                found=1
                break
            else
                echo "[online_reload_cgi]   ${MS}: not found (attempt ${attempt})"
                [ "$attempt" -lt "$MAX_RETRY" ] && sleep "$SLEEP_SEC"
            fi
            attempt=$((attempt + 1))
        done

        # 第一輪失敗 → 補送 CGI + 第二輪驗證
        if [ "$found" -eq 0 ]; then
            echo "[online_reload_cgi]   ${MS}: ⚠ 第一輪驗證失敗，嘗試補送 CGI..."

            if _resend_cgi_to_ms "${MS}" "${SET_ID}"; then
                # 補送成功，再驗證一輪
                local attempt2=1
                while [ "$attempt2" -le "$MAX_RETRY" ]; do
                    echo "[online_reload_cgi]   ${MS}: re-verify attempt ${attempt2}/${MAX_RETRY}"
                    if _check_ms_log "${MS}" "${SET_ID}"; then
                        echo "[online_reload_cgi]   ${MS}: ✅ found after resend (attempt ${attempt2})"
                        found=1
                        break
                    else
                        echo "[online_reload_cgi]   ${MS}: not found (attempt ${attempt2})"
                        [ "$attempt2" -lt "$MAX_RETRY" ] && sleep "$SLEEP_SEC"
                    fi
                    attempt2=$((attempt2 + 1))
                done
            else
                echo "[online_reload_cgi]   ${MS}: ❌ 補送 CGI 也失敗"
            fi
        fi

        if [ "$found" -eq 0 ]; then
            echo "[online_reload_cgi]   ${MS}: ❌ 最終失敗"
            all_ok=0
            failed_ms="${failed_ms} ${MS}"
        fi
    done

    # ── 第二則通知：CGI 驗證結果 ────────────────────────────────────────
    if [ "$all_ok" -eq 1 ]; then
        echo "[online_reload_cgi] ✅ 全部 MS log 驗證通過"
        $HOME/bin/automatic_send_chatbot_text_only_by_thread_tag "itemmall" "✅ CGI ${CGI_CMD} 驗證 OK (資料表 ${TABLE_NAME})" minute 10
    else
        local trimmed_failed=$(echo "${failed_ms}" | xargs)
        echo "[online_reload_cgi] ❌ Log 驗證失敗: ${trimmed_failed}"
        $HOME/bin/automatic_send_chatbot_text_only_by_thread_tag "itemmall" "❌ CGI ${CGI_CMD} Log 驗證失敗 (資料表 ${TABLE_NAME})\n未找到: ${trimmed_failed}" minute 10
    fi
}
