pigflower 3 недель назад
Родитель
Сommit
645a2570de

+ 16 - 2
README.md

@@ -362,11 +362,11 @@ systemctl start gameserver-name
 
 前端/客户端只需关心以下几个对外暴露的地址,其余 RPC 端口是服务端内部通信,不需要配置。
 
-### 本机调试(服务端跑在本地
+### 本机调试(服务端跑在测试服
 
 | 用途 | 地址 | 备注 |
 |---|---|---|
-| 服务器列表 | `http://43.226.57.217/apollo_60000/serverlist.json` | Nginx 静态文件,客户端启动时拉取 |
+| 服务器列表 | `http://43.226.57.217:8007/apollo_60000/serverlist.json` | Nginx 静态文件,客户端启动时拉取 |
 | 登录 / 账号接口 | `http://43.226.57.217:18081` | AccountNode HTTP,登录、注册等接口 |
 | 游戏逻辑连接 | `43.226.57.217:19821` | TCP 长连接,游戏主逻辑入口 |
 | 战斗结算 | `http://43.226.57.217:8088` | BattleServer HTTP,战斗结果上报 |
@@ -392,3 +392,17 @@ systemctl start gameserver-name
 - 战斗服 `Config.json` 中的 `luaRoot` 不同系统路径格式不同:Windows 用 `\\`,Mac/Linux 用 `/`
 - `serverlist.json` 中的 `address` 字段需与 `ConnectorService` 实际监听的 `IP:Port`(默认 19821)一致
 - `update_server_config.json` 中的 `loginServerUrl` 需指向 AccountNode 的 HTTP 地址(默认端口 18081)
+
+
+
+### 只编译(Release,默认)
+  ./build.sh
+
+### Debug 编译
+./build.sh -c Debug
+
+### Release 编译 + 打包服务端(输出到 dist/)
+./build.sh -c Release -p
+
+### Release 编译 + 打包所有目标(server + battle report)
+./build.sh -c Release -t all -p

+ 1887 - 0
docs/dotnet-install.sh

@@ -0,0 +1,1887 @@
+#!/usr/bin/env bash
+# Copyright (c) .NET Foundation and contributors. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#
+
+# Stop script on NZEC
+set -e
+# Stop script if unbound variable found (use ${var:-} if intentional)
+set -u
+# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success
+# This is causing it to fail
+set -o pipefail
+
+# Use in the the functions: eval $invocation
+invocation='say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"'
+
+# standard output may be used as a return value in the functions
+# we need a way to write text on the screen in the functions so that
+# it won't interfere with the return value.
+# Exposing stream 3 as a pipe to standard output of the script itself
+exec 3>&1
+
+# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors.
+# See if stdout is a terminal
+if [ -t 1 ] && command -v tput > /dev/null; then
+    # see if it supports colors
+    ncolors=$(tput colors || echo 0)
+    if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then
+        bold="$(tput bold       || echo)"
+        normal="$(tput sgr0     || echo)"
+        black="$(tput setaf 0   || echo)"
+        red="$(tput setaf 1     || echo)"
+        green="$(tput setaf 2   || echo)"
+        yellow="$(tput setaf 3  || echo)"
+        blue="$(tput setaf 4    || echo)"
+        magenta="$(tput setaf 5 || echo)"
+        cyan="$(tput setaf 6    || echo)"
+        white="$(tput setaf 7   || echo)"
+    fi
+fi
+
+say_warning() {
+    printf "%b\n" "${yellow:-}dotnet_install: Warning: $1${normal:-}" >&3
+}
+
+say_err() {
+    printf "%b\n" "${red:-}dotnet_install: Error: $1${normal:-}" >&2
+}
+
+say() {
+    # using stream 3 (defined in the beginning) to not interfere with stdout of functions
+    # which may be used as return value
+    printf "%b\n" "${cyan:-}dotnet-install:${normal:-} $1" >&3
+}
+
+say_verbose() {
+    if [ "$verbose" = true ]; then
+        say "$1"
+    fi
+}
+
+# This platform list is finite - if the SDK/Runtime has supported Linux distribution-specific assets,
+#   then and only then should the Linux distribution appear in this list.
+# Adding a Linux distribution to this list does not imply distribution-specific support.
+get_legacy_os_name_from_platform() {
+    eval $invocation
+
+    platform="$1"
+    case "$platform" in
+        "centos.7")
+            echo "centos"
+            return 0
+            ;;
+        "debian.8")
+            echo "debian"
+            return 0
+            ;;
+        "debian.9")
+            echo "debian.9"
+            return 0
+            ;;
+        "fedora.23")
+            echo "fedora.23"
+            return 0
+            ;;
+        "fedora.24")
+            echo "fedora.24"
+            return 0
+            ;;
+        "fedora.27")
+            echo "fedora.27"
+            return 0
+            ;;
+        "fedora.28")
+            echo "fedora.28"
+            return 0
+            ;;
+        "opensuse.13.2")
+            echo "opensuse.13.2"
+            return 0
+            ;;
+        "opensuse.42.1")
+            echo "opensuse.42.1"
+            return 0
+            ;;
+        "opensuse.42.3")
+            echo "opensuse.42.3"
+            return 0
+            ;;
+        "rhel.7"*)
+            echo "rhel"
+            return 0
+            ;;
+        "ubuntu.14.04")
+            echo "ubuntu"
+            return 0
+            ;;
+        "ubuntu.16.04")
+            echo "ubuntu.16.04"
+            return 0
+            ;;
+        "ubuntu.16.10")
+            echo "ubuntu.16.10"
+            return 0
+            ;;
+        "ubuntu.18.04")
+            echo "ubuntu.18.04"
+            return 0
+            ;;
+        "alpine.3.4.3")
+            echo "alpine"
+            return 0
+            ;;
+    esac
+    return 1
+}
+
+get_legacy_os_name() {
+    eval $invocation
+
+    local uname=$(uname)
+    if [ "$uname" = "Darwin" ]; then
+        echo "osx"
+        return 0
+    elif [ -n "$runtime_id" ]; then
+        echo $(get_legacy_os_name_from_platform "${runtime_id%-*}" || echo "${runtime_id%-*}")
+        return 0
+    else
+        if [ -e /etc/os-release ]; then
+            . /etc/os-release
+            os=$(get_legacy_os_name_from_platform "$ID${VERSION_ID:+.${VERSION_ID}}" || echo "")
+            if [ -n "$os" ]; then
+                echo "$os"
+                return 0
+            fi
+        fi
+    fi
+
+    say_verbose "Distribution specific OS name and version could not be detected: UName = $uname"
+    return 1
+}
+
+get_linux_platform_name() {
+    eval $invocation
+
+    if [ -n "$runtime_id" ]; then
+        echo "${runtime_id%-*}"
+        return 0
+    else
+        if [ -e /etc/os-release ]; then
+            . /etc/os-release
+            echo "$ID${VERSION_ID:+.${VERSION_ID}}"
+            return 0
+        elif [ -e /etc/redhat-release ]; then
+            local redhatRelease=$(</etc/redhat-release)
+            if [[ $redhatRelease == "CentOS release 6."* || $redhatRelease == "Red Hat Enterprise Linux "*" release 6."* ]]; then
+                echo "rhel.6"
+                return 0
+            fi
+        fi
+    fi
+
+    say_verbose "Linux specific platform name and version could not be detected: UName = $uname"
+    return 1
+}
+
+is_musl_based_distro() {
+    (ldd --version 2>&1 || true) | grep -q musl
+}
+
+get_current_os_name() {
+    eval $invocation
+
+    local uname=$(uname)
+    if [ "$uname" = "Darwin" ]; then
+        echo "osx"
+        return 0
+    elif [ "$uname" = "FreeBSD" ]; then
+        echo "freebsd"
+        return 0
+    elif [ "$uname" = "Linux" ]; then
+        local linux_platform_name=""
+        linux_platform_name="$(get_linux_platform_name)" || true
+
+        if [ "$linux_platform_name" = "rhel.6" ]; then
+            echo $linux_platform_name
+            return 0
+        elif is_musl_based_distro; then
+            echo "linux-musl"
+            return 0
+        elif [ "$linux_platform_name" = "linux-musl" ]; then
+            echo "linux-musl"
+            return 0
+        else
+            echo "linux"
+            return 0
+        fi
+    fi
+
+    say_err "OS name could not be detected: UName = $uname"
+    return 1
+}
+
+machine_has() {
+    eval $invocation
+
+    command -v "$1" > /dev/null 2>&1
+    return $?
+}
+
+check_min_reqs() {
+    local hasMinimum=false
+    if machine_has "curl"; then
+        hasMinimum=true
+    elif machine_has "wget"; then
+        hasMinimum=true
+    fi
+
+    if [ "$hasMinimum" = "false" ]; then
+        say_err "curl (recommended) or wget are required to download dotnet. Install missing prerequisite to proceed."
+        return 1
+    fi
+    return 0
+}
+
+# args:
+# input - $1
+to_lowercase() {
+    #eval $invocation
+
+    echo "$1" | tr '[:upper:]' '[:lower:]'
+    return 0
+}
+
+# args:
+# input - $1
+remove_trailing_slash() {
+    #eval $invocation
+
+    local input="${1:-}"
+    echo "${input%/}"
+    return 0
+}
+
+# args:
+# input - $1
+remove_beginning_slash() {
+    #eval $invocation
+
+    local input="${1:-}"
+    echo "${input#/}"
+    return 0
+}
+
+# args:
+# root_path - $1
+# child_path - $2 - this parameter can be empty
+combine_paths() {
+    eval $invocation
+
+    # TODO: Consider making it work with any number of paths. For now:
+    if [ ! -z "${3:-}" ]; then
+        say_err "combine_paths: Function takes two parameters."
+        return 1
+    fi
+
+    local root_path="$(remove_trailing_slash "$1")"
+    local child_path="$(remove_beginning_slash "${2:-}")"
+    say_verbose "combine_paths: root_path=$root_path"
+    say_verbose "combine_paths: child_path=$child_path"
+    echo "$root_path/$child_path"
+    return 0
+}
+
+get_machine_architecture() {
+    eval $invocation
+
+    if command -v uname > /dev/null; then
+        CPUName=$(uname -m)
+        case $CPUName in
+        armv1*|armv2*|armv3*|armv4*|armv5*|armv6*)
+            echo "armv6-or-below"
+            return 0
+            ;;
+        armv*l)
+            echo "arm"
+            return 0
+            ;;
+        aarch64|arm64)
+            if [ "$(getconf LONG_BIT)" -lt 64 ]; then
+                # This is 32-bit OS running on 64-bit CPU (for example Raspberry Pi OS)
+                echo "arm"
+                return 0
+            fi
+            echo "arm64"
+            return 0
+            ;;
+        s390x)
+            echo "s390x"
+            return 0
+            ;;
+        ppc64le)
+            echo "ppc64le"
+            return 0
+            ;;
+        loongarch64)
+            echo "loongarch64"
+            return 0
+            ;;
+        riscv64)
+            echo "riscv64"
+            return 0
+            ;;
+        powerpc|ppc)
+            echo "ppc"
+            return 0
+            ;;
+        esac
+    fi
+
+    # Always default to 'x64'
+    echo "x64"
+    return 0
+}
+
+# args:
+# architecture - $1
+get_normalized_architecture_from_architecture() {
+    eval $invocation
+
+    local architecture="$(to_lowercase "$1")"
+
+    if [[ $architecture == \<auto\> ]]; then
+        machine_architecture="$(get_machine_architecture)"
+        if [[ "$machine_architecture" == "armv6-or-below" ]]; then
+            say_err "Architecture \`$machine_architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues"
+            return 1
+        fi
+
+        echo $machine_architecture
+        return 0
+    fi
+
+    case "$architecture" in
+        amd64|x64)
+            echo "x64"
+            return 0
+            ;;
+        arm)
+            echo "arm"
+            return 0
+            ;;
+        arm64)
+            echo "arm64"
+            return 0
+            ;;
+        s390x)
+            echo "s390x"
+            return 0
+            ;;
+        ppc64le)
+            echo "ppc64le"
+            return 0
+            ;;
+        loongarch64)
+            echo "loongarch64"
+            return 0
+            ;;
+    esac
+
+    say_err "Architecture \`$architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues"
+    return 1
+}
+
+# args:
+# version - $1
+# channel - $2
+# architecture - $3
+get_normalized_architecture_for_specific_sdk_version() {
+    eval $invocation
+
+    local is_version_support_arm64="$(is_arm64_supported "$1")"
+    local is_channel_support_arm64="$(is_arm64_supported "$2")"
+    local architecture="$3";
+    local osname="$(get_current_os_name)"
+
+    if [ "$osname" == "osx" ] && [ "$architecture" == "arm64" ] && { [ "$is_version_support_arm64" = false ] || [ "$is_channel_support_arm64" = false ]; }; then
+        #check if rosetta is installed
+        if [ "$(/usr/bin/pgrep oahd >/dev/null 2>&1;echo $?)" -eq 0 ]; then 
+            say_verbose "Changing user architecture from '$architecture' to 'x64' because .NET SDKs prior to version 6.0 do not support arm64." 
+            echo "x64"
+            return 0;
+        else
+            say_err "Architecture \`$architecture\` is not supported for .NET SDK version \`$version\`. Please install Rosetta to allow emulation of the \`$architecture\` .NET SDK on this platform"
+            return 1
+        fi
+    fi
+
+    echo "$architecture"
+    return 0
+}
+
+# args:
+# version or channel - $1
+is_arm64_supported() {
+    # Extract the major version by splitting on the dot
+    major_version="${1%%.*}"
+
+    # Check if the major version is a valid number and less than 6
+    case "$major_version" in
+        [0-9]*)  
+            if [ "$major_version" -lt 6 ]; then
+                echo false
+                return 0
+            fi
+            ;;
+    esac
+
+    echo true
+    return 0
+}
+
+# args:
+# user_defined_os - $1
+get_normalized_os() {
+    eval $invocation
+
+    local osname="$(to_lowercase "$1")"
+    if [ ! -z "$osname" ]; then
+        case "$osname" in
+            osx | freebsd | rhel.6 | linux-musl | linux)
+                echo "$osname"
+                return 0
+                ;;
+            macos)
+                osname='osx'
+                echo "$osname"
+                return 0
+                ;;
+            *)
+                say_err "'$user_defined_os' is not a supported value for --os option, supported values are: osx, macos, linux, linux-musl, freebsd, rhel.6. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues."
+                return 1
+                ;;
+        esac
+    else
+        osname="$(get_current_os_name)" || return 1
+    fi
+    echo "$osname"
+    return 0
+}
+
+# args:
+# quality - $1
+get_normalized_quality() {
+    eval $invocation
+
+    local quality="$(to_lowercase "$1")"
+    if [ ! -z "$quality" ]; then
+        case "$quality" in
+            daily | preview)
+                echo "$quality"
+                return 0
+                ;;
+            ga)
+                #ga quality is available without specifying quality, so normalizing it to empty
+                return 0
+                ;;
+            *)
+                say_err "'$quality' is not a supported value for --quality option. Supported values are: daily, preview, ga. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues."
+                return 1
+                ;;
+        esac
+    fi
+    return 0
+}
+
+# args:
+# channel - $1
+get_normalized_channel() {
+    eval $invocation
+
+    local channel="$(to_lowercase "$1")"
+
+    if [[ $channel == current ]]; then
+        say_warning 'Value "Current" is deprecated for -Channel option. Use "STS" instead.'
+    fi
+
+    if [[ $channel == release/* ]]; then
+        say_warning 'Using branch name with -Channel option is no longer supported with newer releases. Use -Quality option with a channel in X.Y format instead.';
+    fi
+
+    if [ ! -z "$channel" ]; then
+        case "$channel" in
+            lts)
+                echo "LTS"
+                return 0
+                ;;
+            sts)
+                echo "STS"
+                return 0
+                ;;
+            current)
+                echo "STS"
+                return 0
+                ;;
+            *)
+                echo "$channel"
+                return 0
+                ;;
+        esac
+    fi
+
+    return 0
+}
+
+# args:
+# runtime - $1
+get_normalized_product() {
+    eval $invocation
+
+    local product=""
+    local runtime="$(to_lowercase "$1")"
+    if [[ "$runtime" == "dotnet" ]]; then
+        product="dotnet-runtime"
+    elif [[ "$runtime" == "aspnetcore" ]]; then
+        product="aspnetcore-runtime"
+    elif [ -z "$runtime" ]; then
+        product="dotnet-sdk"
+    fi
+    echo "$product"
+    return 0
+}
+
+# The version text returned from the feeds is a 1-line or 2-line string:
+# For the SDK and the dotnet runtime (2 lines):
+# Line 1: # commit_hash
+# Line 2: # 4-part version
+# For the aspnetcore runtime (1 line):
+# Line 1: # 4-part version
+
+# args:
+# version_text - stdin
+get_version_from_latestversion_file_content() {
+    eval $invocation
+
+    cat | tail -n 1 | sed 's/\r$//'
+    return 0
+}
+
+# args:
+# install_root - $1
+# relative_path_to_package - $2
+# specific_version - $3
+is_dotnet_package_installed() {
+    eval $invocation
+
+    local install_root="$1"
+    local relative_path_to_package="$2"
+    local specific_version="${3//[$'\t\r\n']}"
+
+    local dotnet_package_path="$(combine_paths "$(combine_paths "$install_root" "$relative_path_to_package")" "$specific_version")"
+    say_verbose "is_dotnet_package_installed: dotnet_package_path=$dotnet_package_path"
+
+    if [ -d "$dotnet_package_path" ]; then
+        return 0
+    else
+        return 1
+    fi
+}
+
+# args:
+# downloaded file - $1
+# remote_file_size - $2
+validate_remote_local_file_sizes() 
+{
+    eval $invocation
+
+    local downloaded_file="$1"
+    local remote_file_size="$2"
+    local file_size=''
+
+    if [[ "$OSTYPE" == "linux-gnu"* ]]; then
+        file_size="$(stat -c '%s' "$downloaded_file")"
+    elif [[ "$OSTYPE" == "darwin"* ]]; then
+        # hardcode in order to avoid conflicts with GNU stat
+        file_size="$(/usr/bin/stat -f '%z' "$downloaded_file")"
+    fi  
+    
+    if [ -n "$file_size" ]; then
+        say "Downloaded file size is $file_size bytes."
+
+        if [ -n "$remote_file_size" ] && [ -n "$file_size" ]; then
+            if [ "$remote_file_size" -ne "$file_size" ]; then
+                say "The remote and local file sizes are not equal. The remote file size is $remote_file_size bytes and the local size is $file_size bytes. The local package may be corrupted."
+            else
+                say "The remote and local file sizes are equal."
+            fi
+        fi
+        
+    else
+        say "Either downloaded or local package size can not be measured. One of them may be corrupted."      
+    fi 
+}
+
+# args:
+# azure_feed - $1
+# channel - $2
+# normalized_architecture - $3
+get_version_from_latestversion_file() {
+    eval $invocation
+
+    local azure_feed="$1"
+    local channel="$2"
+    local normalized_architecture="$3"
+
+    local version_file_url=null
+    if [[ "$runtime" == "dotnet" ]]; then
+        version_file_url="$azure_feed/Runtime/$channel/latest.version"
+    elif [[ "$runtime" == "aspnetcore" ]]; then
+        version_file_url="$azure_feed/aspnetcore/Runtime/$channel/latest.version"
+    elif [ -z "$runtime" ]; then
+         version_file_url="$azure_feed/Sdk/$channel/latest.version"
+    else
+        say_err "Invalid value for \$runtime"
+        return 1
+    fi
+    say_verbose "get_version_from_latestversion_file: latest url: $version_file_url"
+
+    download "$version_file_url" || return $?
+    return 0
+}
+
+# args:
+# json_file - $1
+parse_globaljson_file_for_version() {
+    eval $invocation
+
+    local json_file="$1"
+    if [ ! -f "$json_file" ]; then
+        say_err "Unable to find \`$json_file\`"
+        return 1
+    fi
+
+    sdk_section=$(cat "$json_file" | tr -d "\r" | awk '/"sdk"/,/}/')
+    if [ -z "$sdk_section" ]; then
+        say_err "Unable to parse the SDK node in \`$json_file\`"
+        return 1
+    fi
+
+    sdk_list=$(echo $sdk_section | awk -F"[{}]" '{print $2}')
+    sdk_list=${sdk_list//[\" ]/}
+    sdk_list=${sdk_list//,/$'\n'}
+
+    local version_info=""
+    while read -r line; do
+      IFS=:
+      while read -r key value; do
+        if [[ "$key" == "version" ]]; then
+          version_info=$value
+        fi
+      done <<< "$line"
+    done <<< "$sdk_list"
+    if [ -z "$version_info" ]; then
+        say_err "Unable to find the SDK:version node in \`$json_file\`"
+        return 1
+    fi
+
+    unset IFS;
+    echo "$version_info"
+    return 0
+}
+
+# args:
+# azure_feed - $1
+# channel - $2
+# normalized_architecture - $3
+# version - $4
+# json_file - $5
+get_specific_version_from_version() {
+    eval $invocation
+
+    local azure_feed="$1"
+    local channel="$2"
+    local normalized_architecture="$3"
+    local version="$(to_lowercase "$4")"
+    local json_file="$5"
+
+    if [ -z "$json_file" ]; then
+        if [[ "$version" == "latest" ]]; then
+            local version_info
+            version_info="$(get_version_from_latestversion_file "$azure_feed" "$channel" "$normalized_architecture" false)" || return 1
+            say_verbose "get_specific_version_from_version: version_info=$version_info"
+            echo "$version_info" | get_version_from_latestversion_file_content
+            return 0
+        else
+            echo "$version"
+            return 0
+        fi
+    else
+        local version_info
+        version_info="$(parse_globaljson_file_for_version "$json_file")" || return 1
+        echo "$version_info"
+        return 0
+    fi
+}
+
+# args:
+# azure_feed - $1
+# channel - $2
+# normalized_architecture - $3
+# specific_version - $4
+# normalized_os - $5
+construct_download_link() {
+    eval $invocation
+
+    local azure_feed="$1"
+    local channel="$2"
+    local normalized_architecture="$3"
+    local specific_version="${4//[$'\t\r\n']}"
+    local specific_product_version="$(get_specific_product_version "$1" "$4")"
+    local osname="$5"
+
+    local download_link=null
+    if [[ "$runtime" == "dotnet" ]]; then
+        download_link="$azure_feed/Runtime/$specific_version/dotnet-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz"
+    elif [[ "$runtime" == "aspnetcore" ]]; then
+        download_link="$azure_feed/aspnetcore/Runtime/$specific_version/aspnetcore-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz"
+    elif [ -z "$runtime" ]; then
+        download_link="$azure_feed/Sdk/$specific_version/dotnet-sdk-$specific_product_version-$osname-$normalized_architecture.tar.gz"
+    else
+        return 1
+    fi
+
+    echo "$download_link"
+    return 0
+}
+
+# args:
+# azure_feed - $1
+# specific_version - $2
+# download link - $3 (optional)
+get_specific_product_version() {
+    # If we find a 'productVersion.txt' at the root of any folder, we'll use its contents
+    # to resolve the version of what's in the folder, superseding the specified version.
+    # if 'productVersion.txt' is missing but download link is already available, product version will be taken from download link
+    eval $invocation
+
+    local azure_feed="$1"
+    local specific_version="${2//[$'\t\r\n']}"
+    local package_download_link=""
+    if [ $# -gt 2  ]; then
+        local package_download_link="$3"
+    fi
+    local specific_product_version=null
+
+    # Try to get the version number, using the productVersion.txt file located next to the installer file.
+    local download_links=($(get_specific_product_version_url "$azure_feed" "$specific_version" true "$package_download_link")
+        $(get_specific_product_version_url "$azure_feed" "$specific_version" false "$package_download_link"))
+
+    for download_link in "${download_links[@]}"
+    do
+        say_verbose "Checking for the existence of $download_link"
+
+        if machine_has "curl"
+        then
+            if ! specific_product_version=$(curl -sL --fail "${download_link}${feed_credential}" 2>&1); then
+                continue
+            else
+                echo "${specific_product_version//[$'\t\r\n']}"
+                return 0
+            fi
+
+        elif machine_has "wget"
+        then
+            specific_product_version=$(wget -qO- "${download_link}${feed_credential}" 2>&1)
+            if [ $? = 0 ]; then
+                echo "${specific_product_version//[$'\t\r\n']}"
+                return 0
+            fi
+        fi
+    done
+    
+    # Getting the version number with productVersion.txt has failed. Try parsing the download link for a version number.
+    say_verbose "Failed to get the version using productVersion.txt file. Download link will be parsed instead."
+    specific_product_version="$(get_product_specific_version_from_download_link "$package_download_link" "$specific_version")"
+    echo "${specific_product_version//[$'\t\r\n']}"
+    return 0
+}
+
+# args:
+# azure_feed - $1
+# specific_version - $2
+# is_flattened - $3
+# download link - $4 (optional)
+get_specific_product_version_url() {
+    eval $invocation
+
+    local azure_feed="$1"
+    local specific_version="$2"
+    local is_flattened="$3"
+    local package_download_link=""
+    if [ $# -gt 3  ]; then
+        local package_download_link="$4"
+    fi
+
+    local pvFileName="productVersion.txt"
+    if [ "$is_flattened" = true ]; then
+        if [ -z "$runtime" ]; then
+            pvFileName="sdk-productVersion.txt"
+        elif [[ "$runtime" == "dotnet" ]]; then
+            pvFileName="runtime-productVersion.txt"
+        else
+            pvFileName="$runtime-productVersion.txt"
+        fi
+    fi
+
+    local download_link=null
+
+    if [ -z "$package_download_link" ]; then
+        if [[ "$runtime" == "dotnet" ]]; then
+            download_link="$azure_feed/Runtime/$specific_version/${pvFileName}"
+        elif [[ "$runtime" == "aspnetcore" ]]; then
+            download_link="$azure_feed/aspnetcore/Runtime/$specific_version/${pvFileName}"
+        elif [ -z "$runtime" ]; then
+            download_link="$azure_feed/Sdk/$specific_version/${pvFileName}"
+        else
+            return 1
+        fi
+    else
+        download_link="${package_download_link%/*}/${pvFileName}"
+    fi
+
+    say_verbose "Constructed productVersion link: $download_link"
+    echo "$download_link"
+    return 0
+}
+
+# args:
+# download link - $1
+# specific version - $2
+get_product_specific_version_from_download_link()
+{
+    eval $invocation
+
+    local download_link="$1"
+    local specific_version="$2"
+    local specific_product_version="" 
+
+    if [ -z "$download_link" ]; then
+        echo "$specific_version"
+        return 0
+    fi
+
+    #get filename
+    filename="${download_link##*/}"
+
+    #product specific version follows the product name
+    #for filename 'dotnet-sdk-3.1.404-linux-x64.tar.gz': the product version is 3.1.404
+    IFS='-'
+    read -ra filename_elems <<< "$filename"
+    count=${#filename_elems[@]}
+    if [[ "$count" -gt 2 ]]; then
+        specific_product_version="${filename_elems[2]}"
+    else
+        specific_product_version=$specific_version
+    fi
+    unset IFS;
+    echo "$specific_product_version"
+    return 0
+}
+
+# args:
+# azure_feed - $1
+# channel - $2
+# normalized_architecture - $3
+# specific_version - $4
+construct_legacy_download_link() {
+    eval $invocation
+
+    local azure_feed="$1"
+    local channel="$2"
+    local normalized_architecture="$3"
+    local specific_version="${4//[$'\t\r\n']}"
+
+    local distro_specific_osname
+    distro_specific_osname="$(get_legacy_os_name)" || return 1
+
+    local legacy_download_link=null
+    if [[ "$runtime" == "dotnet" ]]; then
+        legacy_download_link="$azure_feed/Runtime/$specific_version/dotnet-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz"
+    elif [ -z "$runtime" ]; then
+        legacy_download_link="$azure_feed/Sdk/$specific_version/dotnet-dev-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz"
+    else
+        return 1
+    fi
+
+    echo "$legacy_download_link"
+    return 0
+}
+
+get_user_install_path() {
+    eval $invocation
+
+    if [ ! -z "${DOTNET_INSTALL_DIR:-}" ]; then
+        echo "$DOTNET_INSTALL_DIR"
+    else
+        echo "$HOME/.dotnet"
+    fi
+    return 0
+}
+
+# args:
+# install_dir - $1
+resolve_installation_path() {
+    eval $invocation
+
+    local install_dir=$1
+    if [ "$install_dir" = "<auto>" ]; then
+        local user_install_path="$(get_user_install_path)"
+        say_verbose "resolve_installation_path: user_install_path=$user_install_path"
+        echo "$user_install_path"
+        return 0
+    fi
+
+    echo "$install_dir"
+    return 0
+}
+
+# args:
+# relative_or_absolute_path - $1
+get_absolute_path() {
+    eval $invocation
+
+    local relative_or_absolute_path=$1
+    echo "$(cd "$(dirname "$1")" && pwd -P)/$(basename "$1")"
+    return 0
+}
+
+# args:
+# override - $1 (boolean, true or false)
+get_cp_options() {
+    eval $invocation
+
+    local override="$1"
+    local override_switch=""
+
+    if [ "$override" = false ]; then
+        override_switch="-n"
+
+        # create temporary files to check if 'cp -u' is supported
+        tmp_dir="$(mktemp -d)"
+        tmp_file="$tmp_dir/testfile"
+        tmp_file2="$tmp_dir/testfile2"
+
+        touch "$tmp_file"
+
+        # use -u instead of -n if it's available
+        if cp -u "$tmp_file" "$tmp_file2" 2>/dev/null; then
+            override_switch="-u"
+        fi
+
+        # clean up
+        rm -f "$tmp_file" "$tmp_file2"
+        rm -rf "$tmp_dir"
+    fi
+
+    echo "$override_switch"
+}
+
+# args:
+# input_files - stdin
+# root_path - $1
+# out_path - $2
+# override - $3
+copy_files_or_dirs_from_list() {
+    eval $invocation
+
+    local root_path="$(remove_trailing_slash "$1")"
+    local out_path="$(remove_trailing_slash "$2")"
+    local override="$3"
+    local override_switch="$(get_cp_options "$override")"
+
+    cat | uniq | while read -r file_path; do
+        local path="$(remove_beginning_slash "${file_path#$root_path}")"
+        local target="$out_path/$path"
+        if [ "$override" = true ] || (! ([ -d "$target" ] || [ -e "$target" ] || [ -L "$target" ])); then
+            mkdir -p "$out_path/$(dirname "$path")"
+            if [ -d "$target" ] || [ -L "$target" ]; then
+                rm -rf "$target"
+            fi
+            cp -RP $override_switch "$root_path/$path" "$target"
+        fi
+    done
+}
+
+# args:
+# zip_uri - $1
+get_remote_file_size() {
+    local zip_uri="$1"
+
+    if machine_has "curl"; then
+        file_size=$(curl -sI  "$zip_uri" | grep -i content-length | awk '{ num = $2 + 0; print num }')
+    elif machine_has "wget"; then
+        file_size=$(wget --spider --server-response -O /dev/null "$zip_uri" 2>&1 | grep -i 'Content-Length:' | awk '{ num = $2 + 0; print num }')
+    else
+        say "Neither curl nor wget is available on this system."
+        return
+    fi
+
+    if [ -n "$file_size" ]; then
+        say "Remote file $zip_uri size is $file_size bytes."
+        echo "$file_size"
+    else
+        say_verbose "Content-Length header was not extracted for $zip_uri."
+        echo ""
+    fi
+}
+
+# args:
+# zip_path - $1
+# out_path - $2
+# remote_file_size - $3
+extract_dotnet_package() {
+    eval $invocation
+
+    local zip_path="$1"
+    local out_path="$2"
+    local remote_file_size="$3"
+
+    local temp_out_path="$(mktemp -d "$temporary_file_template")"
+
+    local failed=false
+    tar -xzf "$zip_path" -C "$temp_out_path" > /dev/null || failed=true
+
+    local folders_with_version_regex='^.*/[0-9]+\.[0-9]+[^/]+/'
+    find "$temp_out_path" \( -type f -o -type l \) | grep -Eo "$folders_with_version_regex" | sort | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" false
+    find "$temp_out_path" \( -type f -o -type l \) | grep -Ev "$folders_with_version_regex" | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" "$override_non_versioned_files"
+    
+    validate_remote_local_file_sizes "$zip_path" "$remote_file_size"
+    
+    rm -rf "$temp_out_path"
+    if [ -z ${keep_zip+x} ]; then
+        rm -f "$zip_path" && say_verbose "Temporary archive file $zip_path was removed"
+    fi
+
+    if [ "$failed" = true ]; then
+        say_err "Extraction failed"
+        return 1
+    fi
+    return 0
+}
+
+# args:
+# remote_path - $1
+# disable_feed_credential - $2
+get_http_header()
+{
+    eval $invocation
+    local remote_path="$1"
+    local disable_feed_credential="$2"
+
+    local failed=false
+    local response
+    if machine_has "curl"; then
+        get_http_header_curl $remote_path $disable_feed_credential || failed=true
+    elif machine_has "wget"; then
+        get_http_header_wget $remote_path $disable_feed_credential || failed=true
+    else
+        failed=true
+    fi
+    if [ "$failed" = true ]; then
+        say_verbose "Failed to get HTTP header: '$remote_path'."
+        return 1
+    fi
+    return 0
+}
+
+# args:
+# remote_path - $1
+# disable_feed_credential - $2
+get_http_header_curl() {
+    eval $invocation
+    local remote_path="$1"
+    local disable_feed_credential="$2"
+
+    remote_path_with_credential="$remote_path"
+    if [ "$disable_feed_credential" = false ]; then
+        remote_path_with_credential+="$feed_credential"
+    fi
+
+    curl_options="-I -sSL --retry 5 --retry-delay 2 --connect-timeout 15 "
+    curl $curl_options "$remote_path_with_credential" 2>&1 || return 1
+    return 0
+}
+
+# args:
+# remote_path - $1
+# disable_feed_credential - $2
+get_http_header_wget() {
+    eval $invocation
+    local remote_path="$1"
+    local disable_feed_credential="$2"
+    local wget_options="-q -S --spider --tries 5 "
+
+    local wget_options_extra=''
+
+    # Test for options that aren't supported on all wget implementations.
+    if [[ $(wget -h 2>&1 | grep -E 'waitretry|connect-timeout') ]]; then
+        wget_options_extra="--waitretry 2 --connect-timeout 15 "
+    else
+        say "wget extra options are unavailable for this environment"
+    fi
+
+    remote_path_with_credential="$remote_path"
+    if [ "$disable_feed_credential" = false ]; then
+        remote_path_with_credential+="$feed_credential"
+    fi
+
+    wget $wget_options $wget_options_extra "$remote_path_with_credential" 2>&1
+
+    return $?
+}
+
+# args:
+# remote_path - $1
+# [out_path] - $2 - stdout if not provided
+download() {
+    eval $invocation
+
+    local remote_path="$1"
+    local out_path="${2:-}"
+
+    if [[ "$remote_path" != "http"* ]]; then
+        cp "$remote_path" "$out_path"
+        return $?
+    fi
+
+    local failed=false
+    local attempts=0
+    while [ $attempts -lt 3 ]; do
+        attempts=$((attempts+1))
+        failed=false
+        if machine_has "curl"; then
+            downloadcurl "$remote_path" "$out_path" || failed=true
+        elif machine_has "wget"; then
+            downloadwget "$remote_path" "$out_path" || failed=true
+        else
+            say_err "Missing dependency: neither curl nor wget was found."
+            exit 1
+        fi
+
+        if [ "$failed" = false ] || [ $attempts -ge 3 ] || { [ -n "${http_code-}" ] && [ "${http_code}" = "404" ]; }; then
+            break
+        fi
+
+        say "Download attempt #$attempts has failed: ${http_code-} ${download_error_msg-}"
+        say "Attempt #$((attempts+1)) will start in $((attempts*10)) seconds."
+        sleep $((attempts*10))
+    done
+
+    if [ "$failed" = true ]; then
+        say_verbose "Download failed: $remote_path"
+        return 1
+    fi
+    return 0
+}
+
+# Updates global variables $http_code and $download_error_msg
+downloadcurl() {
+    eval $invocation
+    unset http_code
+    unset download_error_msg
+    local remote_path="$1"
+    local out_path="${2:-}"
+    # Append feed_credential as late as possible before calling curl to avoid logging feed_credential
+    # Avoid passing URI with credentials to functions: note, most of them echoing parameters of invocation in verbose output.
+    local remote_path_with_credential="${remote_path}${feed_credential}"
+    local curl_options="--retry 20 --retry-delay 2 --connect-timeout 15 -sSL -f --create-dirs "
+    local curl_exit_code=0;
+    if [ -z "$out_path" ]; then
+        curl_output=$(curl $curl_options "$remote_path_with_credential" 2>&1)
+        curl_exit_code=$?
+        echo "$curl_output"
+    else
+        curl_output=$(curl $curl_options -o "$out_path" "$remote_path_with_credential" 2>&1)
+        curl_exit_code=$?
+    fi
+
+    # Regression in curl causes curl with --retry to return a 0 exit code even when it fails to download a file - https://github.com/curl/curl/issues/17554
+    if [ $curl_exit_code -eq 0 ] && echo "$curl_output" | grep -q "^curl: ([0-9]*) "; then
+        curl_exit_code=$(echo "$curl_output" | sed 's/curl: (\([0-9]*\)).*/\1/')
+    fi
+
+    if [ $curl_exit_code -gt 0 ]; then
+        download_error_msg="Unable to download $remote_path."
+        # Check for curl timeout codes
+        if [[ $curl_exit_code == 7 || $curl_exit_code == 28 ]]; then
+            download_error_msg+=" Failed to reach the server: connection timeout."
+        else
+            local disable_feed_credential=false
+            local response=$(get_http_header_curl $remote_path $disable_feed_credential)
+            http_code=$( echo "$response" | awk '/^HTTP/{print $2}' | tail -1 )
+            if  [[ ! -z $http_code && $http_code != 2* ]]; then
+                download_error_msg+=" Returned HTTP status code: $http_code."
+            fi
+        fi
+        say_verbose "$download_error_msg"
+        return 1
+    fi
+    return 0
+}
+
+
+# Updates global variables $http_code and $download_error_msg
+downloadwget() {
+    eval $invocation
+    unset http_code
+    unset download_error_msg
+    local remote_path="$1"
+    local out_path="${2:-}"
+    # Append feed_credential as late as possible before calling wget to avoid logging feed_credential
+    local remote_path_with_credential="${remote_path}${feed_credential}"
+    local wget_options="--tries 20 "
+
+    local wget_options_extra=''
+    local wget_result=''
+
+    # Test for options that aren't supported on all wget implementations.
+    if [[ $(wget -h 2>&1 | grep -E 'waitretry|connect-timeout') ]]; then
+        wget_options_extra="--waitretry 2 --connect-timeout 15 "
+    else
+        say "wget extra options are unavailable for this environment"
+    fi
+
+    if [ -z "$out_path" ]; then
+        wget -q $wget_options $wget_options_extra -O - "$remote_path_with_credential" 2>&1
+        wget_result=$?
+    else
+        wget $wget_options $wget_options_extra -O "$out_path" "$remote_path_with_credential" 2>&1
+        wget_result=$?
+    fi
+
+    if [[ $wget_result != 0 ]]; then
+        local disable_feed_credential=false
+        local response=$(get_http_header_wget $remote_path $disable_feed_credential)
+        http_code=$( echo "$response" | awk '/^  HTTP/{print $2}' | tail -1 )
+        download_error_msg="Unable to download $remote_path."
+        if  [[ ! -z $http_code && $http_code != 2* ]]; then
+            download_error_msg+=" Returned HTTP status code: $http_code."
+        # wget exit code 4 stands for network-issue
+        elif [[ $wget_result == 4 ]]; then
+            download_error_msg+=" Failed to reach the server: connection timeout."
+        fi
+        say_verbose "$download_error_msg"
+        return 1
+    fi
+
+    return 0
+}
+
+get_download_link_from_aka_ms() {
+    eval $invocation
+
+    #quality is not supported for LTS or STS channel
+    #STS maps to current
+    if [[ ! -z "$normalized_quality"  && ("$normalized_channel" == "LTS" || "$normalized_channel" == "STS") ]]; then
+        normalized_quality=""
+        say_warning "Specifying quality for STS or LTS channel is not supported, the quality will be ignored."
+    fi
+
+    say_verbose "Retrieving primary payload URL from aka.ms for channel: '$normalized_channel', quality: '$normalized_quality', product: '$normalized_product', os: '$normalized_os', architecture: '$normalized_architecture'." 
+
+    #construct aka.ms link
+    aka_ms_link="https://aka.ms/dotnet"
+    if  [ "$internal" = true ]; then
+        aka_ms_link="$aka_ms_link/internal"
+    fi
+    aka_ms_link="$aka_ms_link/$normalized_channel"
+    if [[ ! -z "$normalized_quality" ]]; then
+        aka_ms_link="$aka_ms_link/$normalized_quality"
+    fi
+    aka_ms_link="$aka_ms_link/$normalized_product-$normalized_os-$normalized_architecture.tar.gz"
+    say_verbose "Constructed aka.ms link: '$aka_ms_link'."
+
+    #get HTTP response
+    #do not pass credentials as a part of the $aka_ms_link and do not apply credentials in the get_http_header function
+    #otherwise the redirect link would have credentials as well
+    #it would result in applying credentials twice to the resulting link and thus breaking it, and in echoing credentials to the output as a part of redirect link
+    disable_feed_credential=true
+    response="$(get_http_header $aka_ms_link $disable_feed_credential)"
+
+    say_verbose "Received response: $response"
+    # Get results of all the redirects.
+    http_codes=$( echo "$response" | awk '$1 ~ /^HTTP/ {print $2}' )
+    # Allow intermediate 301 redirects and tolerate proxy-injected 200s
+    broken_redirects=$( echo "$http_codes" | sed '$d' | grep -vE '^(301|200)$' )
+    # The response may end without final code 2xx/4xx/5xx somehow, e.g. network restrictions on www.bing.com causes redirecting to bing.com fails with connection refused.
+    # In this case it should not exclude the last.
+    last_http_code=$(  echo "$http_codes" | tail -n 1 )
+    if ! [[ $last_http_code =~ ^(2|4|5)[0-9][0-9]$ ]]; then
+        broken_redirects=$( echo "$http_codes" | grep -vE '^(301|200)$' )
+    fi
+
+    # All HTTP codes are 301 (Moved Permanently), the redirect link exists.
+    if [[ -z "$broken_redirects" ]]; then
+        aka_ms_download_link=$( echo "$response" | awk '$1 ~ /^Location/{print $2}' | tail -1 | tr -d '\r')
+
+        if [[ -z "$aka_ms_download_link" ]]; then
+            say_verbose "The aka.ms link '$aka_ms_link' is not valid: failed to get redirect location."
+            return 1
+        fi
+
+        say_verbose "The redirect location retrieved: '$aka_ms_download_link'."
+        return 0
+    else
+        say_verbose "The aka.ms link '$aka_ms_link' is not valid: received HTTP code: $(echo "$broken_redirects" | paste -sd "," -)."
+        return 1
+    fi
+}
+
+get_feeds_to_use()
+{
+    feeds=(
+    "https://builds.dotnet.microsoft.com/dotnet"
+    "https://ci.dot.net/public"
+    )
+
+    if [[ -n "$azure_feed" ]]; then
+        feeds=("$azure_feed")
+    fi
+
+    if [[ -n "$uncached_feed" ]]; then
+        feeds=("$uncached_feed")
+    fi
+}
+
+# THIS FUNCTION MAY EXIT (if the determined version is already installed).
+generate_download_links() {
+
+    download_links=()
+    specific_versions=()
+    effective_versions=()
+    link_types=()
+
+    # If generate_akams_links returns false, no fallback to old links. Just terminate.
+    # This function may also 'exit' (if the determined version is already installed).
+    generate_akams_links || return
+
+    # Check other feeds only if we haven't been able to find an aka.ms link.
+    if [[ "${#download_links[@]}" -lt 1 ]]; then
+        for feed in ${feeds[@]}
+        do
+            # generate_regular_links may also 'exit' (if the determined version is already installed).
+            generate_regular_links $feed || return
+        done
+    fi
+
+    if [[ "${#download_links[@]}" -eq 0 ]]; then
+        say_err "Failed to resolve the exact version number."
+        return 1
+    fi
+
+    say_verbose "Generated ${#download_links[@]} links."
+    for link_index in ${!download_links[@]}
+    do
+        say_verbose "Link $link_index: ${link_types[$link_index]}, ${effective_versions[$link_index]}, ${download_links[$link_index]}"
+    done
+}
+
+# THIS FUNCTION MAY EXIT (if the determined version is already installed).
+generate_akams_links() {
+    local valid_aka_ms_link=true;
+
+    normalized_version="$(to_lowercase "$version")"
+    if [[ "$normalized_version" != "latest" ]] && [ -n "$normalized_quality" ]; then
+        say_err "Quality and Version options are not allowed to be specified simultaneously. See https://learn.microsoft.com/dotnet/core/tools/dotnet-install-script#options for details."
+        return 1
+    fi
+
+    if [[ -n "$json_file" || "$normalized_version" != "latest" ]]; then
+        # aka.ms links are not needed when exact version is specified via command or json file
+        return
+    fi
+
+    get_download_link_from_aka_ms || valid_aka_ms_link=false
+
+    if [[ "$valid_aka_ms_link" == true ]]; then
+        say_verbose "Retrieved primary payload URL from aka.ms link: '$aka_ms_download_link'."
+        say_verbose "Downloading using legacy url will not be attempted."
+
+        download_link=$aka_ms_download_link
+
+        #get version from the path
+        IFS='/'
+        read -ra pathElems <<< "$download_link"
+        count=${#pathElems[@]}
+        specific_version="${pathElems[count-2]}"
+        unset IFS;
+        say_verbose "Version: '$specific_version'."
+
+        #Retrieve effective version
+        effective_version="$(get_specific_product_version "$azure_feed" "$specific_version" "$download_link")"
+
+        # Add link info to arrays
+        download_links+=($download_link)
+        specific_versions+=($specific_version)
+        effective_versions+=($effective_version)
+        link_types+=("aka.ms")
+
+        #  Check if the SDK version is already installed.
+        if [[ "$dry_run" != true ]] && is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then
+            say "$asset_name with version '$effective_version' is already installed."
+            exit 0
+        fi
+
+        return 0
+    fi
+
+    # if quality is specified - exit with error - there is no fallback approach
+    if [ ! -z "$normalized_quality" ]; then
+        say_err "Failed to locate the latest version in the channel '$normalized_channel' with '$normalized_quality' quality for '$normalized_product', os: '$normalized_os', architecture: '$normalized_architecture'."
+        say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support."
+        return 1
+    fi
+    say_verbose "Falling back to latest.version file approach."
+}
+
+# THIS FUNCTION MAY EXIT (if the determined version is already installed)
+# args:
+# feed - $1
+generate_regular_links() {
+    local feed="$1"
+    local valid_legacy_download_link=true
+
+    specific_version=$(get_specific_version_from_version "$feed" "$channel" "$normalized_architecture" "$version" "$json_file") || specific_version='0'
+
+    if [[ "$specific_version" == '0' ]]; then
+        say_verbose "Failed to resolve the specific version number using feed '$feed'"
+        return
+    fi
+
+    effective_version="$(get_specific_product_version "$feed" "$specific_version")"
+    say_verbose "specific_version=$specific_version"
+
+    download_link="$(construct_download_link "$feed" "$channel" "$normalized_architecture" "$specific_version" "$normalized_os")"
+    say_verbose "Constructed primary named payload URL: $download_link"
+
+    # Add link info to arrays
+    download_links+=($download_link)
+    specific_versions+=($specific_version)
+    effective_versions+=($effective_version)
+    link_types+=("primary")
+
+    legacy_download_link="$(construct_legacy_download_link "$feed" "$channel" "$normalized_architecture" "$specific_version")" || valid_legacy_download_link=false
+
+    if [ "$valid_legacy_download_link" = true ]; then
+        say_verbose "Constructed legacy named payload URL: $legacy_download_link"
+    
+        download_links+=($legacy_download_link)
+        specific_versions+=($specific_version)
+        effective_versions+=($effective_version)
+        link_types+=("legacy")
+    else
+        legacy_download_link=""
+        say_verbose "Could not construct a legacy_download_link; omitting..."
+    fi
+
+    #  Check if the SDK version is already installed.
+    if [[ "$dry_run" != true ]] && is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then
+        say "$asset_name with version '$effective_version' is already installed."
+        exit 0
+    fi
+}
+
+print_dry_run() {
+
+    say "Payload URLs:"
+
+    for link_index in "${!download_links[@]}"
+        do
+            say "URL #$link_index - ${link_types[$link_index]}: ${download_links[$link_index]}"
+    done
+
+    resolved_version=${specific_versions[0]}
+    repeatable_command="./$script_name --version "\""$resolved_version"\"" --install-dir "\""$install_root"\"" --architecture "\""$normalized_architecture"\"" --os "\""$normalized_os"\"""
+    
+    if [ ! -z "$normalized_quality" ]; then
+        repeatable_command+=" --quality "\""$normalized_quality"\"""
+    fi
+
+    if [[ "$runtime" == "dotnet" ]]; then
+        repeatable_command+=" --runtime "\""dotnet"\"""
+    elif [[ "$runtime" == "aspnetcore" ]]; then
+        repeatable_command+=" --runtime "\""aspnetcore"\"""
+    fi
+
+    repeatable_command+="$non_dynamic_parameters"
+
+    if [ -n "$feed_credential" ]; then
+        repeatable_command+=" --feed-credential "\""<feed_credential>"\"""
+    fi
+
+    say "Repeatable invocation: $repeatable_command"
+}
+
+calculate_vars() {
+    eval $invocation
+
+    script_name=$(basename "$0")
+    normalized_architecture="$(get_normalized_architecture_from_architecture "$architecture")"
+    say_verbose "Normalized architecture: '$normalized_architecture'."
+    normalized_os="$(get_normalized_os "$user_defined_os")"
+    say_verbose "Normalized OS: '$normalized_os'."
+    normalized_quality="$(get_normalized_quality "$quality")"
+    say_verbose "Normalized quality: '$normalized_quality'."
+    normalized_channel="$(get_normalized_channel "$channel")"
+    say_verbose "Normalized channel: '$normalized_channel'."
+    normalized_product="$(get_normalized_product "$runtime")"
+    say_verbose "Normalized product: '$normalized_product'."
+    install_root="$(resolve_installation_path "$install_dir")"
+    say_verbose "InstallRoot: '$install_root'."
+
+    normalized_architecture="$(get_normalized_architecture_for_specific_sdk_version "$version" "$normalized_channel" "$normalized_architecture")"
+
+    if [[ "$runtime" == "dotnet" ]]; then
+        asset_relative_path="shared/Microsoft.NETCore.App"
+        asset_name=".NET Core Runtime"
+    elif [[ "$runtime" == "aspnetcore" ]]; then
+        asset_relative_path="shared/Microsoft.AspNetCore.App"
+        asset_name="ASP.NET Core Runtime"
+    elif [ -z "$runtime" ]; then
+        asset_relative_path="sdk"
+        asset_name=".NET Core SDK"
+    fi
+
+    get_feeds_to_use
+}
+
+install_dotnet() {
+    eval $invocation
+    local download_failed=false
+    local download_completed=false
+    local remote_file_size=0
+
+    mkdir -p "$install_root"
+    zip_path="${zip_path:-$(mktemp "$temporary_file_template")}"
+    say_verbose "Archive path: $zip_path"
+
+    for link_index in "${!download_links[@]}"
+    do
+        download_link="${download_links[$link_index]}"
+        specific_version="${specific_versions[$link_index]}"
+        effective_version="${effective_versions[$link_index]}"
+        link_type="${link_types[$link_index]}"
+
+        say "Attempting to download using $link_type link $download_link"
+
+        # The download function will set variables $http_code and $download_error_msg in case of failure.
+        download_failed=false
+        download "$download_link" "$zip_path" 2>&1 || download_failed=true
+
+        if [ "$download_failed" = true ]; then
+            case ${http_code-} in
+            404)
+                say "The resource at $link_type link '$download_link' is not available."
+                ;;
+            *)
+                say "Failed to download $link_type link '$download_link': ${http_code-} ${download_error_msg-}"
+                ;;
+            esac
+            rm -f "$zip_path" 2>&1 && say_verbose "Temporary archive file $zip_path was removed"
+        else
+            download_completed=true
+            break
+        fi
+    done
+
+    if [[ "$download_completed" == false ]]; then
+        say_err "Could not find \`$asset_name\` with version = $specific_version"
+        say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support"
+        return 1
+    fi
+
+    remote_file_size="$(get_remote_file_size "$download_link")"
+
+    say "Extracting archive from $download_link"
+    extract_dotnet_package "$zip_path" "$install_root" "$remote_file_size" || return 1
+
+    #  Check if the SDK version is installed; if not, fail the installation.
+    # if the version contains "RTM" or "servicing"; check if a 'release-type' SDK version is installed.
+    if [[ $specific_version == *"rtm"* || $specific_version == *"servicing"* ]]; then
+        IFS='-'
+        read -ra verArr <<< "$specific_version"
+        release_version="${verArr[0]}"
+        unset IFS;
+        say_verbose "Checking installation: version = $release_version"
+        if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$release_version"; then
+            say "Installed version is $effective_version"
+            return 0
+        fi
+    fi
+
+    #  Check if the standard SDK version is installed.
+    say_verbose "Checking installation: version = $effective_version"
+    if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then
+        say "Installed version is $effective_version"
+        return 0
+    fi
+
+    # Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm.
+    say_err "Failed to verify the version of installed \`$asset_name\`.\nInstallation source: $download_link.\nInstallation location: $install_root.\nReport the bug at https://github.com/dotnet/install-scripts/issues."
+    say_err "\`$asset_name\` with version = $effective_version failed to install with an error."
+    return 1
+}
+
+args=("$@")
+
+local_version_file_relative_path="/.version"
+bin_folder_relative_path=""
+temporary_file_template="${TMPDIR:-/tmp}/dotnet.XXXXXXXXX"
+
+channel="LTS"
+version="Latest"
+json_file=""
+install_dir="<auto>"
+architecture="<auto>"
+dry_run=false
+no_path=false
+azure_feed=""
+uncached_feed=""
+feed_credential=""
+verbose=false
+runtime=""
+runtime_id=""
+quality=""
+internal=false
+override_non_versioned_files=true
+non_dynamic_parameters=""
+user_defined_os=""
+
+while [ $# -ne 0 ]
+do
+    name="$1"
+    case "$name" in
+        -c|--channel|-[Cc]hannel)
+            shift
+            channel="$1"
+            ;;
+        -v|--version|-[Vv]ersion)
+            shift
+            version="$1"
+            ;;
+        -q|--quality|-[Qq]uality)
+            shift
+            quality="$1"
+            ;;
+        --internal|-[Ii]nternal)
+            internal=true
+            non_dynamic_parameters+=" $name"
+            ;;
+        -i|--install-dir|-[Ii]nstall[Dd]ir)
+            shift
+            install_dir="$1"
+            ;;
+        --arch|--architecture|-[Aa]rch|-[Aa]rchitecture)
+            shift
+            architecture="$1"
+            ;;
+        --os|-[Oo][SS])
+            shift
+            user_defined_os="$1"
+            ;;
+        --shared-runtime|-[Ss]hared[Rr]untime)
+            say_warning "The --shared-runtime flag is obsolete and may be removed in a future version of this script. The recommended usage is to specify '--runtime dotnet'."
+            if [ -z "$runtime" ]; then
+                runtime="dotnet"
+            fi
+            ;;
+        --runtime|-[Rr]untime)
+            shift
+            runtime="$1"
+            if [[ "$runtime" != "dotnet" ]] && [[ "$runtime" != "aspnetcore" ]]; then
+                say_err "Unsupported value for --runtime: '$1'. Valid values are 'dotnet' and 'aspnetcore'."
+                if [[ "$runtime" == "windowsdesktop" ]]; then
+                    say_err "WindowsDesktop archives are manufactured for Windows platforms only."
+                fi
+                exit 1
+            fi
+            ;;
+        --dry-run|-[Dd]ry[Rr]un)
+            dry_run=true
+            ;;
+        --no-path|-[Nn]o[Pp]ath)
+            no_path=true
+            non_dynamic_parameters+=" $name"
+            ;;
+        --verbose|-[Vv]erbose)
+            verbose=true
+            non_dynamic_parameters+=" $name"
+            ;;
+        --azure-feed|-[Aa]zure[Ff]eed)
+            shift
+            azure_feed="$1"
+            non_dynamic_parameters+=" $name "\""$1"\"""
+            ;;
+        --uncached-feed|-[Uu]ncached[Ff]eed)
+            shift
+            uncached_feed="$1"
+            non_dynamic_parameters+=" $name "\""$1"\"""
+            ;;
+        --feed-credential|-[Ff]eed[Cc]redential)
+            shift
+            feed_credential="$1"
+            #feed_credential should start with "?", for it to be added to the end of the link.
+            #adding "?" at the beginning of the feed_credential if needed.
+            [[ -z "$(echo $feed_credential)" ]] || [[ $feed_credential == \?* ]] || feed_credential="?$feed_credential"
+            ;;
+        --runtime-id|-[Rr]untime[Ii]d)
+            shift
+            runtime_id="$1"
+            non_dynamic_parameters+=" $name "\""$1"\"""
+            say_warning "Use of --runtime-id is obsolete and should be limited to the versions below 2.1. To override architecture, use --architecture option instead. To override OS, use --os option instead."
+            ;;
+        --jsonfile|-[Jj][Ss]on[Ff]ile)
+            shift
+            json_file="$1"
+            ;;
+        --skip-non-versioned-files|-[Ss]kip[Nn]on[Vv]ersioned[Ff]iles)
+            override_non_versioned_files=false
+            non_dynamic_parameters+=" $name"
+            ;;
+        --keep-zip|-[Kk]eep[Zz]ip)
+            keep_zip=true
+            non_dynamic_parameters+=" $name"
+            ;;
+        --zip-path|-[Zz]ip[Pp]ath)
+            shift
+            zip_path="$1"
+            ;;
+        -?|--?|-h|--help|-[Hh]elp)
+            script_name="dotnet-install.sh"
+            echo ".NET Tools Installer"
+            echo "Usage:"
+            echo "       # Install a .NET SDK of a given Quality from a given Channel"
+            echo "       $script_name [-c|--channel <CHANNEL>] [-q|--quality <QUALITY>]"
+            echo "       # Install a .NET SDK of a specific public version"
+            echo "       $script_name [-v|--version <VERSION>]"
+            echo "       $script_name -h|-?|--help"
+            echo ""
+            echo "$script_name is a simple command line interface for obtaining dotnet cli."
+            echo "    Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:"
+            echo "    - The SDK needs to be installed without user interaction and without admin rights."
+            echo "    - The SDK installation doesn't need to persist across multiple CI runs."
+            echo "    To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer."
+            echo ""
+            echo "Options:"
+            echo "  -c,--channel <CHANNEL>         Download from the channel specified, Defaults to \`$channel\`."
+            echo "      -Channel"
+            echo "          Possible values:"
+            echo "          - STS - the most recent Standard Term Support release"
+            echo "          - LTS - the most recent Long Term Support release"
+            echo "          - 2-part version in a format A.B - represents a specific release"
+            echo "              examples: 2.0; 1.0"
+            echo "          - 3-part version in a format A.B.Cxx - represents a specific SDK release"
+            echo "              examples: 5.0.1xx, 5.0.2xx."
+            echo "              Supported since 5.0 release"
+            echo "          Warning: Value 'Current' is deprecated for the Channel parameter. Use 'STS' instead."
+            echo "          Note: The version parameter overrides the channel parameter when any version other than 'latest' is used."
+            echo "  -v,--version <VERSION>         Use specific VERSION, Defaults to \`$version\`."
+            echo "      -Version"
+            echo "          Possible values:"
+            echo "          - latest - the latest build on specific channel"
+            echo "          - 3-part version in a format A.B.C - represents specific version of build"
+            echo "              examples: 2.0.0-preview2-006120; 1.1.0"
+            echo "  -q,--quality <quality>         Download the latest build of specified quality in the channel."
+            echo "      -Quality"
+            echo "          The possible values are: daily, preview, GA."
+            echo "          Works only in combination with channel. Not applicable for STS and LTS channels and will be ignored if those channels are used." 
+            echo "          Supported since 5.0 release." 
+            echo "          Note: The version parameter overrides the channel parameter when any version other than 'latest' is used, and therefore overrides the quality."
+            echo "  --internal,-Internal               Download internal builds. Requires providing credentials via --feed-credential parameter."
+            echo "  --feed-credential <FEEDCREDENTIAL> Token to access Azure feed. Used as a query string to append to the Azure feed."
+            echo "      -FeedCredential                This parameter typically is not specified."
+            echo "  -i,--install-dir <DIR>             Install under specified location (see Install Location below)"
+            echo "      -InstallDir"
+            echo "  --architecture <ARCHITECTURE>      Architecture of dotnet binaries to be installed, Defaults to \`$architecture\`."
+            echo "      --arch,-Architecture,-Arch"
+            echo "          Possible values: x64, arm, arm64, s390x, ppc64le and loongarch64"
+            echo "  --os <system>                    Specifies operating system to be used when selecting the installer."
+            echo "          Overrides the OS determination approach used by the script. Supported values: osx, linux, linux-musl, freebsd, rhel.6."
+            echo "          In case any other value is provided, the platform will be determined by the script based on machine configuration."
+            echo "          Not supported for legacy links. Use --runtime-id to specify platform for legacy links."
+            echo "          Refer to: https://aka.ms/dotnet-os-lifecycle for more information."
+            echo "  --runtime <RUNTIME>                Installs a shared runtime only, without the SDK."
+            echo "      -Runtime"
+            echo "          Possible values:"
+            echo "          - dotnet     - the Microsoft.NETCore.App shared runtime"
+            echo "          - aspnetcore - the Microsoft.AspNetCore.App shared runtime"
+            echo "  --dry-run,-DryRun                  Do not perform installation. Display download link."
+            echo "  --no-path, -NoPath                 Do not set PATH for the current process."
+            echo "  --verbose,-Verbose                 Display diagnostics information."
+            echo "  --azure-feed,-AzureFeed            For internal use only."
+            echo "                                     Allows using a different storage to download SDK archives from."
+            echo "  --uncached-feed,-UncachedFeed      For internal use only."
+            echo "                                     Allows using a different storage to download SDK archives from."
+            echo "  --skip-non-versioned-files         Skips non-versioned files if they already exist, such as the dotnet executable."
+            echo "      -SkipNonVersionedFiles"
+            echo "  --jsonfile <JSONFILE>              Determines the SDK version from a user specified global.json file."
+            echo "                                     Note: global.json must have a value for 'SDK:Version'"
+            echo "  --keep-zip,-KeepZip                If set, downloaded file is kept."
+            echo "  --zip-path, -ZipPath               If set, downloaded file is stored at the specified path."
+            echo "  -?,--?,-h,--help,-Help             Shows this help message"
+            echo ""
+            echo "Install Location:"
+            echo "  Location is chosen in following order:"
+            echo "    - --install-dir option"
+            echo "    - Environmental variable DOTNET_INSTALL_DIR"
+            echo "    - $HOME/.dotnet"
+            exit 0
+            ;;
+        *)
+            say_err "Unknown argument \`$name\`"
+            exit 1
+            ;;
+    esac
+
+    shift
+done
+
+say_verbose "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:"
+say_verbose "- The SDK needs to be installed without user interaction and without admin rights."
+say_verbose "- The SDK installation doesn't need to persist across multiple CI runs."
+say_verbose "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.\n"
+
+if [ "$internal" = true ] && [ -z "$(echo $feed_credential)" ]; then
+    message="Provide credentials via --feed-credential parameter."
+    if [ "$dry_run" = true ]; then
+        say_warning "$message"
+    else
+        say_err "$message"
+        exit 1
+    fi
+fi
+
+check_min_reqs
+calculate_vars
+# generate_regular_links call below will 'exit' if the determined version is already installed.
+generate_download_links
+
+if [[ "$dry_run" = true ]]; then
+    print_dry_run
+    exit 0
+fi
+
+install_dotnet
+
+bin_path="$(get_absolute_path "$(combine_paths "$install_root" "$bin_folder_relative_path")")"
+if [ "$no_path" = false ]; then
+    say "Adding to current process PATH: \`$bin_path\`. Note: This change will be visible only when sourcing script."
+    export PATH="$bin_path":"$PATH"
+else
+    say "Binaries of dotnet can be found in $bin_path"
+fi
+
+say "Note that the script does not resolve dependencies during installation."
+say "To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install, select your operating system and check the \"Dependencies\" section."
+say "Installation finished successfully."

+ 3 - 3
server/src/server/OpenCards.Server.Common/BILogger/log4net.config

@@ -27,11 +27,11 @@
       <layout type="log4net.Layout.PatternLayout">
         <conversionPattern value="%date %-5level %message%newline" />
       </layout>
-<!--       <filter type="log4net.Filter.LevelRangeFilter">
+      <filter type="log4net.Filter.LevelRangeFilter">
         <levelMin value="WARN" />
         <levelMax value="FATAL" />
-      </filter> -->
-
+      </filter>
+      <filter type="log4net.Filter.DenyAllFilter" />
     </appender>
 
 

+ 228 - 0
项目导表与GM指令文档.md

@@ -0,0 +1,228 @@
+# 项目文档
+
+---
+
+## 一、项目导表功能
+
+### 1.1 目录结构 jyyz_game是svn仓库
+
+```
+jyyz_game/ServerData/
+├── templates_xls/          # 策划编辑的源 Excel 配置表(主表)
+├── templates_xls_area/     # 区服差异化配置表
+├── templates_xls_battle_strength/ # 战斗强度配置表
+├── templates_lua/          # 导出后的 Lua 文件(自动生成,不要手动修改)
+├── templates_lua_area/     # 区服 Lua 输出
+├── lang/                   # 多语言文本文件
+├── build_xls2lua.bat               # 主导表脚本(全量导出)
+├── build_xls2lua_battle.bat        # 只导出 battle 相关配置
+├── build_xls2lua_battle_strength.bat
+├── build_xls2lang.bat              # 多语言导出
+├── lang导出流程.txt                 # 多语言导表说明
+└── tool.py                         # 辅助 Python 工具脚本
+```
+
+### 1.2 客户端导表流程
+
+#### 步骤
+
+1. 策划在 `templates_xls/` 下编辑 `.xlsx` 文件
+2. 双击执行 `build_xls2lua.bat`
+3. 脚本自动完成:
+   - 调用 `xlslang lua` 将 xlsx 导出为 Lua 到 `templates_lua/`
+   - 调用 `xlslang md5` 生成 `_luaversion_.lua`(版本校验文件)
+   - 将 `templates_lua/` 下的 `.lua` 文件复制到 `..\ClientScript\Data`(客户端代码目录)
+   - 对区服表做同样处理,复制到 `..\ClientScript\DataArea`
+
+#### `build_xls2lua.bat` 核心逻辑
+
+```bat
+xlslang lua -id:.\templates_xls -od:.\templates_lua -key:id -olang:1 \
+  -filter_text:"-~;-localization/;-clx/battle/func/"
+
+xlslang md5 -id:.\templates_lua -of:.\templates_lua\_luaversion_.lua \
+  -filter_text:"-~;-_luaversion_.lua$"
+
+xcopy /Y /Q /E .\templates_lua\*.lua ..\ClientScript\Data
+```
+
+#### 只导战斗相关表
+
+```bat
+# 双击执行
+build_xls2lua_battle.bat
+```
+该脚本仅筛选文件名含 `#battle` 的表进行导出,速度更快。
+
+#### 多语言配置导表
+
+参考 `lang导出流程.txt`,有两种方式:
+
+- **方式1**:`build_xls2lang.bat` → 将 `lang/lang.csv` 内容粘贴到 `lang.xlsx` → `build_xls2lua.bat`
+- **方式2**:`build_xls2lang1.bat` → 粘贴 csv → `build_xls2lang2.bat`
+
+---
+
+### 1.3 服务器导表流程
+
+服务器不使用 Lua 文件,而是通过 C# 的 **TableManager** 在启动时直接读取 `data/ServerData/` 目录下的 Lua 文件加载为 C# 对象。
+
+实际路径:`houduan/server/src/data/ServerData/`(与客户端共享同一套 Lua 文件)
+
+#### 热重载配置表(无需重启服务器)
+
+服务器支持通过 GM 指令热重载配置表:
+
+```
+reloadtable <表名>     # 重载指定表
+reloadalltable         # 重载全部表
+```
+
+---
+
+### 1.4 导表完整操作流程
+
+| 步骤 | 操作者 | 操作内容 |
+|------|--------|----------|
+| 1 | 策划 | 修改 `templates_xls/` 下的 Excel 文件 |
+| 2 | 策划/程序 | 双击 `build_xls2lua.bat`(或 battle 专用脚本) |
+| 3 | 程序 | 验证 `ClientScript/Data/` 下 Lua 文件已更新 |
+| 4 | 程序 | 将更新后的 Lua 文件同步到服务器 `data/ServerData/` |
+| 5 | 程序 | 服务器重启,或用 GM 指令 `reloadalltable` 热重载 |
+
+---
+
+## 二、GM 指令系统
+
+### 2.1 结论:GM 系统已存在
+
+本项目已有完整的游戏内 GM 指令系统,包含:
+- **客户端 GM 面板**(游戏内 UI,**客户端研究下**)
+- **服务器 GM 命令处理**(`RoleModule.HandleGM()`)
+- **后台管理接口**(`AdminService` HTTP API)
+
+---
+
+### 2.2 服务器支持的 GM 指令列表
+
+指令由空格分隔参数,直接在 GM 面板的输入框输入后发送。
+
+#### 道具 / 货币类
+
+| 指令 | 说明 |
+|------|------|
+| `additem <物品InstanceId> <数量> [类型]` | 给当前角色添加道具(类型默认为1=Tool) |
+| `addallitem <数量> [类型]` | 给当前角色添加所有道具各 N 个 |
+| `addgold` | 添加 1 亿金币 |
+| `adddiam` | 添加 1 亿钻石 |
+| `addexp` | 添加 1 亿英雄经验 |
+| `listitems` | 列出当前背包所有道具 |
+| `useitem <uuid> <数量>` | 使用道具 |
+| `sellitem <uuid> <数量>` | 出售道具 |
+| `removeitem <uuid> <数量>` | 移除道具 |
+| `drop <dropGroupId>` | 测试掉落组 |
+
+#### 英雄类
+
+| 指令 | 说明 |
+|------|------|
+| `addAllHero` | 添加所有英雄 |
+| `addAllHero2` | 添加所有英雄(方式2) |
+
+#### 召唤类
+
+| 指令 | 说明 |
+|------|------|
+| `summonsingle` | 单次召唤 |
+| `summonten` | 十连召唤 |
+| `addsummon <次数>` | 添加召唤次数 |
+| `adddrawpoints <点数>` | 添加抽卡积分 |
+
+#### 配置表热更
+
+| 指令 | 说明 |
+|------|------|
+| `reloadtable <表名>` | 热重载指定配置表 |
+| `reloadalltable` | 热重载所有配置表 |
+
+#### 角色数据
+
+| 指令 | 说明 |
+|------|------|
+| `roledata` | 查看当前角色完整数据(JSON) |
+| `roleextdata` | 查看当前角色扩展数据 |
+| `savefulldata` | 将完整数据保存到本地文件 |
+| `gc` | 触发 GC |
+| `totalmemory` | 查看内存占用 |
+
+#### 活动 / 时间类
+
+| 指令 | 说明 |
+|------|------|
+| `customCreateTime <YYYY-MM-DD> [HH:MM:SS]` | 设置角色创建时间(测试新手活动) |
+| `setTimeOffset <秒数>` | 设置时间偏移 |
+| `targettask` | 完成目标任务 |
+| `activitytask` | 完成活动任务 |
+
+#### 其他常用
+
+| 指令 | 说明 |
+|------|------|
+| `sendmail <mailId>` | 发送邮件给自己 |
+| `unlockAllSkin` | 解锁所有皮肤 |
+| `clearaccount` | 清除账号数据 |
+| `guide <步骤>` | 跳转到指定新手引导步骤 |
+| `addflag <flagId>` | 添加角色标记 |
+
+---
+
+### 2.3 `additem` 指令物品类型说明
+
+`additem` 的第三个参数 `[类型]` 对应 `ItemType`:
+
+| 值 | 类型 |
+|----|------|
+| `1` | Tool(道具,默认) |
+| `2` | Hero(英雄) |
+| `3` | Equip(装备) |
+
+**示例:**
+```
+additem 1001 100        # 添加 InstanceId=1001 的道具 100 个
+additem 2001 1 2        # 添加 ID=2001 的英雄 1 个
+adddiam                 # 快速添加大量钻石
+addgold                 # 快速添加大量金币
+```
+
+---
+
+### 2.4 后台管理 GM 接口(服务器端 HTTP API)
+
+`AdminService` 提供 HTTP 接口,供后台管理平台远程执行 GM 操作:
+
+- **地址**:`http://服务器IP:18088/api/`(本地:`http://127.0.0.1:18088/api/`)
+- **`type: "server_gm"`**:向在线玩家或指定玩家广播 GM 命令
+
+后台 GM 支持的主要模块:
+
+| type 值 | 功能 |
+|---------|------|
+| `server_mail` | 发送系统邮件(含附件道具) |
+| `server_gm` | 执行 GM 命令(支持全服/在线/指定玩家) |
+| `query_server_role_info` | 查询玩家数据 |
+| `modify_server_role_info` | 修改玩家数据 |
+| `server_mute_role` | 禁言玩家 |
+| `server_block_role` | 封禁玩家 |
+| `cdk_opt` | CDK 操作 |
+
+
+### 最常用 GM 指令
+```
+adddiam              # 加钻石
+addgold              # 加金币
+additem <id> <n>     # 加道具
+addAllHero           # 解锁所有英雄
+reloadalltable       # 热重载配置表(不重启服务器)
+listitems            # 查看当前背包
+roledata             # 查看角色数据
+```