pigflower 2 недель назад
Родитель
Сommit
9c19fed1d2
4 измененных файлов с 2459 добавлено и 0 удалено
  1. 1887 0
      docs/dotnet-install.sh
  2. 404 0
      docs/代币系统技术设计文档.md
  3. 17 0
      docs/协议.md
  4. 151 0
      docs/需求整理.txt

+ 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."

+ 404 - 0
docs/代币系统技术设计文档.md

@@ -0,0 +1,404 @@
+# 代币系统技术设计文档
+
+## 1. 需求概述
+
+新增两种道具:**代币** 和 **绑定代币**,支持以下核心功能:
+
+1. 代币/绑定代币可消耗用于购买部分付费商品(配置项控制哪些商品支持)
+2. 代币购买商品时触发累充VIP、首充、每日累充等功能
+3. 玩家可通过代币交易功能与其他玩家交换获得泪滴石
+4. 通过交易获得的代币为绑定代币,绑定代币不可再次交易
+5. 出售代币也触发累充VIP、首充、每日累充等功能
+6. 消耗比例:100个代币/绑定代币 = 1元付费货币等值商品
+
+---
+
+## 2. 道具定义
+
+### 2.1 道具类型
+
+两个道具均属于**代币类(Token,ItemToolType=3)**,进入背包,通过 `RoleItemData.Items` 存储,不加入 `ItemsNotInPackage`。
+
+| 道具名称 | 建议ItemId | ItemInstanceId | 是否可交易 | 备注 |
+|---------|-----------|----------------|-----------|------|
+| 代币    | 36        | 3600           | ✅ 可交易  | 正常获得 |
+| 绑定代币 | 37        | 3700           | ❌ 不可交易 | 仅通过交易获得 |
+
+> **注意**:实际 ItemId 需对照现有 `item.xlsx` 最大值后续分配,不得与已有道具冲突。
+
+### 2.2 配置表改动(item.xlsx)
+
+新增两行:
+
+| 字段        | 代币       | 绑定代币   |
+|-----------|-----------|-----------|
+| Id        | 36        | 37        |
+| ItemType  | 3(Token) | 3(Token) |
+| ItemName  | 代币       | 绑定代币   |
+| Stack     | 99999999  | 99999999  |
+| AutoUse   | 0         | 0         |
+| HideInBag | 0         | 0         |
+| DataItemName | Token  | BoundToken |
+| ItemTypeDataName | Token | BoundToken |
+
+### 2.3 配置表改动(iteminstance.xlsx)
+
+新增两行:
+
+| 字段       | 代币  | 绑定代币 |
+|----------|------|---------|
+| InstanceId | 3600 | 3700   |
+| ItemId   | 36   | 37      |
+| ItemType | 1    | 1       |
+
+### 2.4 代码常量(ItemDefines.cs)
+
+文件:`server/src/server/OpenCards.Server.Logic/Module/Item/ItemDefines.cs`
+
+```csharp
+public static int TokenItemId = 36;         // 代币
+public static int BoundTokenItemId = 37;    // 绑定代币
+public static int TokenItemInstanceId = 3600;      // 代币捆绑实例ID
+public static int BoundTokenItemInstanceId = 3700; // 绑定代币捆绑实例ID
+```
+
+---
+
+## 3. 商品代币购买功能
+
+### 3.1 配置表改动(Market.xlsx - market_gift 表)
+
+在 `Table_MarketGift` 对应的 `Market.xlsx` 的 `market_gift` sheet 新增字段:
+
+| 字段名       | 类型 | 说明 |
+|------------|-----|-----|
+| TokenEnable | int | 是否支持代币购买:0=不支持,1=支持 |
+
+对应代码改动,文件 `server/src/server/OpenCards.Server.Core/Table/Table_Market.cs`:
+
+```csharp
+/// <summary>
+/// 是否支持代币购买 0=不支持 1=支持
+/// </summary>
+public int TokenEnable;
+```
+
+### 3.2 消耗比例
+
+- 1元付费货币(CNY)= 100 代币/绑定代币
+- 下单时根据商品的 `Table_IAP.CNY` 字段计算所需代币数:`tokenCost = (int)(iap.CNY * 100)`
+
+### 3.3 代币购买流程(服务端)
+
+新增 `ChargeType.Token` 枚举值,文件 `MarketModule.cs`:
+
+```csharp
+private enum ChargeType
+{
+    Charge,
+    Ticket,
+    GMSend,
+    Token,      // 代币购买
+}
+```
+
+在下单处理 `ClientGetRechargeOrderRequest` 的处理函数中,在现有 `Ticket` 判断之后追加代币购买逻辑:
+
+```
+if (giftConfig.TokenEnable == 1)
+{
+    // 1. 计算所需代币数量
+    tokenCost = (int)(iap.CNY * 100)
+
+    // 2. 优先尝试扣代币,失败则扣绑定代币,两者都不足则返回代币不足错误码
+    // 3. 扣款成功后调用 OnPaySuccess(productId, ext1, ..., ChargeType.Token)
+    // 4. 同时调用 RecordChargeToken(iap.CNY) 记录代币消费金额
+    // 5. 返回 CODE_CHARGE_BY_TOKEN 告知客户端
+}
+```
+
+**扣款优先级**:优先扣代币,代币不足时扣绑定代币,两者数量之和不足时返回失败。
+
+在 `ClientGetRechargeOrderResponse` 中新增响应码:
+
+```csharp
+public const int CODE_CHARGE_BY_TOKEN = 4;   // 代币购买成功
+public const int CODE_TOKEN_NOT_ENOUGH = 505; // 代币不足
+public const int CODE_TOKEN_NOT_SUPPORT = 506; // 该商品不支持代币购买
+```
+
+### 3.4 代币购买对累充系统的触发
+
+在 `OnPaySuccess(ChargeType.Token)` 分支中,与 `ChargeType.Ticket` 处理方式相同:
+
+- `mRoleFlag.AddFlag(FlagDefines.ChargeFlag, convertPrice)` → 触发累计充值
+- `mRoleFlag.AddFlag(FlagDefines.UnrealRMBChargeFlag, rmbConvertPrice)` → 非真实RMB累计(不影响真实充值统计)
+- `mRoleFlag.AddFlag(FlagDefines.BuyCountFlag, 1)` → 购买次数累计
+- `CheckFosterRecharge(convertPrice)` → 触发每日累充(养成豪礼)
+- `CheckRechargeInfo()` → 触发动态难度检查
+- `GetModule<HeroModule>().AddRechargBUFData(convertPrice)` → 充值BUFF
+- `DispatchEvent(EventDefines.EventAfterPayDelivery, productId)` → 发货后事件(触发首充、VIP等检测)
+- `DispatchEvent(EventDefines.EventAfterPayDeliveryWithPrice, productId, price)` → 带价格的发货后事件
+
+> **关键**:代币购买同样走完整的 `OnPaySuccess` 流程,确保所有充值触发链均正常响应。VipExp 通过 `GiftDropID` 掉落表配置自动发放,无需单独处理。
+
+### 3.5 客户端二次确认弹窗
+
+当商品 `TokenEnable == 1` 时,客户端在点击购买按钮后弹出二次确认弹窗。
+
+**弹窗内容**(服务端下发商品信息中含 `TokenEnable` 字段,客户端据此决定是否展示):
+
+```
+标题:购买确认
+正文:该商品支持代币购买
+  · 直接充值:使用真实货币支付,立即购买
+  · 代币购买:消耗 {tokenCost} 个代币(当前拥有: {ownToken})
+[直接充值]  [代币购买]
+```
+
+**服务端协议新增**:在商品列表同步协议中,将 `TokenEnable` 和 `TokenCost` 下发给客户端。
+
+---
+
+## 4. 代币交易系统
+
+### 4.1 功能概述
+
+玩家A(卖方)挂出代币,玩家B(买方)支付泪滴石,完成交易后:
+- 玩家B消耗泪滴石,获得**绑定代币**(非代币)
+- 玩家A获得泪滴石,此次出售触发累充系统
+
+绑定代币不可再次挂出交易。
+
+### 4.2 新增数据表(TokenTradeData)
+
+新增 `TokenTradeData.cs`(`OpenCards.Core/ORM/` 目录):
+
+```csharp
+[PersistType]
+public class TokenTradeOrder : ISerializable, IObjectMapping
+{
+    [PersistField(PersistStrategy.Primary)]
+    public string OrderId;          // 订单唯一ID(UUID)
+
+    [PersistField]
+    public string SellerRoleId;     // 卖方角色ID
+
+    [PersistField]
+    public int TokenAmount;         // 挂出的代币数量
+
+    [PersistField]
+    public int TearStonePrice;      // 要求的泪滴石数量
+
+    [PersistField]
+    public long CreateTime;         // 创建时间(ms时间戳)
+
+    [PersistField]
+    public long ExpireTime;         // 过期时间(ms时间戳)
+
+    [PersistField]
+    public int Status;              // 0=挂单中,1=已成交,2=已取消,3=已过期
+}
+```
+
+持久化类型常量在 `PersistenceConstants` 中新增:
+
+```csharp
+public const string TYPE_TOKEN_TRADE_ORDER = "token_trade_order";
+```
+
+### 4.3 新增协议(TokenTrade 协议文件)
+
+新建 `0x23A00.TokenTrade.cs`(`OpenCards.Core/Protocol/Client/` 目录):
+
+```
+消息码段:0x23A00 起
+```
+
+| 协议名 | 方向 | 说明 |
+|-------|-----|-----|
+| `ClientTokenTradeListRequest` | C→S | 获取交易列表 |
+| `ClientTokenTradeListResponse` | S→C | 返回交易列表 |
+| `ClientTokenTradePutRequest` | C→S | 挂出代币 |
+| `ClientTokenTradePutResponse` | S→C | 挂出结果 |
+| `ClientTokenTradeCancelRequest` | C→S | 取消挂单 |
+| `ClientTokenTradeCancelResponse` | S→C | 取消结果 |
+| `ClientTokenTradeBuyRequest` | C→S | 购买(用泪滴石换绑定代币) |
+| `ClientTokenTradeBuyResponse` | S→C | 购买结果 |
+| `ClientTokenTradeMyOrderRequest` | C→S | 查询自己的挂单 |
+| `ClientTokenTradeMyOrderResponse` | S→C | 返回自己的挂单 |
+
+**关键字段说明**:
+
+`ClientTokenTradePutRequest`:
+- `c2s_tokenAmount`:挂出代币数量
+- `c2s_tearStonePrice`:期望收取的泪滴石数量
+
+`ClientTokenTradeBuyRequest`:
+- `c2s_orderId`:目标订单ID
+
+### 4.4 新增模块(TokenTradeModule)
+
+新建 `server/src/server/OpenCards.Server.Logic/Module/TokenTradeModule.cs`。
+
+**挂单逻辑(DoTokenTradePut)**:
+
+```
+1. 校验:挂出的必须是代币(ItemId=36),不得挂绑定代币(ItemId=37)
+2. 校验代币余量是否足够
+3. 校验玩家当前挂单数量是否超上限(配置表控制,默认5单)
+4. 扣除玩家代币(EventRemoveItem,RemoveItemReason.TokenTrade)
+5. 生成 TokenTradeOrder,写入全服 Redis/DB(中心服维护订单列表)
+6. 返回成功
+```
+
+**购买逻辑(DoTokenTradeBuy)**:
+
+```
+1. 从中心服获取订单,校验订单状态=挂单中
+2. 校验买方泪滴石余量 >= 订单价格
+3. 原子操作:
+   a. 扣除买方泪滴石(EventRemoveItem,RemoveItemReason.TokenTradeBuy)
+   b. 发放买方 绑定代币(EventAddItem,ItemInstanceId=3700,AddItemReason.TokenTrade)
+   c. 向卖方发邮件,附带泪滴石奖励(EventSendMailWithItems)
+   d. 订单状态改为已成交
+4. 触发卖方出售代币的累充事件(见 4.5)
+```
+
+**取消挂单逻辑(DoTokenTradeCancel)**:
+
+```
+1. 校验订单属于本玩家且状态=挂单中
+2. 退还代币给玩家(EventAddItem,ItemInstanceId=3600,AddItemReason.TokenTradeCancel)
+3. 订单状态改为已取消
+```
+
+**定时过期**:订单过期时间由配置表控制(建议7天),服务端定时扫描过期订单,自动取消并退还代币。
+
+### 4.5 出售代币触发累充
+
+出售成交后,对**卖方**触发累充系统,等效金额按 `泪滴石数量 / 100` CNY 计算:
+
+```csharp
+float equivalentCNY = tearStoneAmount / 100f;
+long convertPrice = Convert.ToInt64(equivalentCNY * 100);
+
+// 触发累充系列
+GetModule<MarketModule>().CheckFosterRecharge(convertPrice);
+GetModule<MarketModule>().CheckRechargeInfo();
+mRoleFlag.AddFlag(FlagDefines.ChargeFlag, convertPrice, Flag.MapType.EPersist);
+mRoleFlag.AddFlag(FlagDefines.UnrealRMBChargeFlag, convertPrice, Flag.MapType.EPersist);
+mRoleFlag.AddFlag(FlagDefines.BuyCountFlag, 1, Flag.MapType.EPersist);
+DispatchEvent(EventDefines.EventAfterPayDeliveryWithPrice, 0, equivalentCNY);
+```
+
+> 出售代币不触发首充(`EventAfterPayDelivery` 不传 productId,或传 0 使相关模块跳过首充发货)。首充只在商品购买时触发,按现有逻辑执行。
+
+### 4.6 新增 AddItemReason / RemoveItemReason
+
+在 `ItemDefines.cs` 中追加:
+
+```csharp
+// AddItemReason
+public static int TokenTrade = 129;         // 代币交易获得绑定代币
+public static int TokenTradeCancel = 130;   // 代币交易取消退还代币
+
+// RemoveItemReason
+public static int TokenTrade = 60;          // 挂出代币
+public static int TokenTradeBuy = 61;       // 购买时消耗泪滴石
+```
+
+---
+
+## 5. 泪滴石道具确认
+
+泪滴石为现有道具,已在英雄重置、斗技场等功能中使用(见客户端语言文件)。实现前需确认服务端 ItemId,通过查询 `item.xlsx` 获取,代币交易模块中引用该 ItemId 作为常量。
+
+```csharp
+// 待确认实际ID,填入后在 ItemDefines 中注册
+public static int TearStoneItemId = ???;
+```
+
+---
+
+## 6. 涉及文件清单
+
+### 6.1 配置表
+
+| 文件 | Sheet | 操作 |
+|-----|------|-----|
+| `item.xlsx` | item | 新增代币(36)、绑定代币(37)两行 |
+| `iteminstance.xlsx` | item_instance | 新增 3600、3700 两行 |
+| `Market.xlsx` | market_gift | 新增 `TokenEnable` 字段列 |
+
+### 6.2 服务端代码
+
+| 文件 | 操作 |
+|-----|-----|
+| `OpenCards.Core/ORM/TokenTradeData.cs` | **新建**:订单数据结构 |
+| `OpenCards.Core/Protocol/Client/0x23A00.TokenTrade.cs` | **新建**:交易协议定义 |
+| `OpenCards.Core/Data/Constants.cs` | 新增消息码段常量 `TOKEN_TRADE_START` |
+| `OpenCards.Server.Logic/Module/Item/ItemDefines.cs` | 新增道具ID常量、新增 AddItemReason/RemoveItemReason |
+| `OpenCards.Server.Logic/Module/TokenTradeModule.cs` | **新建**:代币交易业务模块 |
+| `OpenCards.Server.Logic/Module/MarketModule.cs` | 新增 `ChargeType.Token`;在下单处理中追加代币购买分支;新增 `RecordChargeToken()` |
+| `OpenCards.Server.Core/Table/Table_Market.cs` | `Table_MarketGift` 新增 `TokenEnable` 字段 |
+| `OpenCards.Server.Core/Table/Table_IAP.cs` | 无需改动(复用 `CNY` 字段计算代币数) |
+| `OpenCards.GenCodec/` | 重新生成序列化代码(自动生成,运行代码生成工具) |
+| `OpenCards.Server.GenORM/` | 重新生成 ORM 代码(自动生成) |
+
+### 6.3 客户端(服务端接口侧)
+
+| 文件 | 操作 |
+|-----|-----|
+| 商品列表协议 | 在现有商品信息中新增 `TokenEnable`、`TokenCost` 字段下发 |
+| 下单协议响应 | 新增 `CODE_CHARGE_BY_TOKEN`、`CODE_TOKEN_NOT_ENOUGH` 等响应码 |
+
+---
+
+## 7. 关键设计决策
+
+### 7.1 代币/绑定代币均进背包,不走 CurrencyModule
+
+代币属于代币类(`ItemToolType.Token=3`),存储在 `RoleItemData.Items`,无需改动 `CurrencyModule.cs` 和 `RoleData.cs`。查询数量、增删均通过事件 `EventGetItemCount` / `EventAddItemImp` / `EventRemoveItem` 走背包通用逻辑。
+
+### 7.2 绑定代币以独立 ItemId 区分
+
+绑定代币(ItemId=37)与代币(ItemId=36)是两个独立道具,通过 ItemId 区分是否可交易,无需额外绑定标记字段,逻辑简单清晰。
+
+### 7.3 代币购买触发累充等同于 Ticket 机制
+
+现有 `ChargeType.Ticket` 已实现"非真实充值走完整充值流程"的模式,`ChargeType.Token` 与其保持一致,复用 `OnPaySuccess` 完整逻辑,确保首充、VIP、每日累充等所有依赖充值事件的功能均正常触发。
+
+### 7.4 代币交易为全服功能
+
+代币交易订单是全服共享的(非单角色),需中心服(或独立的交易服务)维护订单列表。订单数据存储在全服 Redis/DB,而非角色本地数据。
+
+### 7.5 购买代币时扣款优先级
+
+优先消耗代币,代币不足时消耗绑定代币。两者合计不足才返回错误。逻辑在服务端实现,客户端展示合计数量。
+
+---
+
+## 8. 错误码汇总
+
+| 错误码常量 | 值 | 说明 |
+|----------|---|-----|
+| `CODE_CHARGE_BY_TOKEN` | 4 | 代币购买成功(下单响应) |
+| `CODE_TOKEN_NOT_ENOUGH` | 505 | 代币不足 |
+| `CODE_TOKEN_NOT_SUPPORT` | 506 | 该商品不支持代币购买 |
+| `CODE_TRADE_ORDER_NOT_FOUND` | 507 | 交易订单不存在 |
+| `CODE_TRADE_ORDER_SOLD` | 508 | 订单已被购买 |
+| `CODE_TRADE_TOKEN_NOT_TRADEABLE` | 509 | 绑定代币不可交易 |
+| `CODE_TRADE_EXCEED_MAX_ORDER` | 510 | 超出最大挂单数量 |
+| `CODE_TRADE_TEARSTONE_NOT_ENOUGH` | 511 | 泪滴石不足 |
+
+---
+
+## 9. 开发顺序建议
+
+1. **配置表**:`item.xlsx`、`iteminstance.xlsx`、`Market.xlsx` 新增字段,更新到 ServerData
+2. **基础道具**:`ItemDefines.cs` 新增常量,导表,验证背包可正常收发
+3. **商品购买**:`Table_Market.cs` 加字段 → `MarketModule.cs` 加代币购买分支 → 联调充值触发链
+4. **交易系统**:新建协议 → 新建 `TokenTradeModule` → 接入中心服订单存储 → 联调全流程
+5. **代码生成**:重新生成 GenCodec / GenORM
+6. **GM指令**:新增 GM 指令发放/扣除代币以方便测试

+ 17 - 0
docs/协议.md

@@ -0,0 +1,17 @@
+协议文件分布在以下位置:
+协议定义(手写源文件)
+客户端服务端协议(按消息ID分模块):server/src/core/0penCards.Core/Protocol/Client/-主协议定义-0x35000.Logic.Common.cs、0x35200.Logic.Fight.cs、0x35300.Logic.Role.cs0x39000.Logic.Arena.cs、0x37300.Logic.Mail.cs等,按功能模块拆分server/src/core/0penCards.Core/Protocol/Constants.cs -消息 ID 段常量定义server/src/core/0penCards.Core/Protocol/MsgAttribute.cs - [RequestMsg]/[ResponseMsg] 标记
+服务端内部 RPC协议:-server/src/server/0penCards.Server.Core/RPC/ -服务器间 RPC 消息(Account、Arena、Guild、Role等)
+共享数据结构:-server/src/core/0penCards.Core/Data/ -协议里用到的数据对象(0x22000.Role.cs、0x23800.Arena.cs 等)
+自动生成文件(不要手改)
+目录
+server/src/core/0penCards.GenCodec/generated_msg/
+server/src/server/0pencards.Server.GenORM/generated_msg/
+data/clientscript/Protocol/generated/
+内容
+C#序列化/反序列化 codec(gitignored)
+服务端 RPC codec (gitignored)
+Lua 版 codec,给客户端用
+整体架构
+-协议在 C#的 OpenCards.Core.Protocol.Client 命名空间里手写定义,用[RequestMsg]标记配对OpenCards.GenCodec项目的 PostBuild 把这些定义生成对应的 C#codec和 Lua codec-客户端(Lua)用 data/ClientScript/Protocol/generated/下的 Lua 文件通信-服务端用C#codeC,消息ID按模块分段(Arena在0x39000段,等等)
+改协议的正确流程:移改 DpenCards.Core/ProtocoU/CLient/ 里的源文件  重新编译 DpenCards.TooLs (触发 PostBuild 生成新 codec)  前后端 codec 同步更新

+ 151 - 0
docs/需求整理.txt

@@ -0,0 +1,151 @@
+  货币类 vs 代币类 — 核心区别
+
+  ┌───────────────────────┬──────────────────────────────┬───────────────────────────────────────┐
+  │                       │       货币(Currency)       │             代币(Token)             │
+  ├───────────────────────┼──────────────────────────────┼───────────────────────────────────────┤
+  │ 举例                  │ 钻石(1)、金币(5)、友情点(15) │ 迷宫币(9)、血战筹码(12)、悬赏点(35)   │
+  ├───────────────────────┼──────────────────────────────┼───────────────────────────────────────┤
+  │ 存储位置              │ RoleData 字段(专用字段)    │ RoleItemData.Items 背包(通用道具格) │
+  ├───────────────────────┼──────────────────────────────┼───────────────────────────────────────┤
+  │ 不进背包              │ ✅ 加入 ItemsNotInPackage    │ ❌ 不用加                             │
+  ├───────────────────────┼──────────────────────────────┼───────────────────────────────────────┤
+  │ 需要改 CurrencyModule │ ✅ 必须加                    │ ❌ 不需要                             │
+  ├───────────────────────┼──────────────────────────────┼───────────────────────────────────────┤
+  │ 需要改 RoleData       │ ✅ 需要加字段                │ ❌ 不需要                             │
+  ├───────────────────────┼──────────────────────────────┼───────────────────────────────────────┤
+  │ 客户端同步            │ 通过 SyncClientRoleDataType  │ 通过背包增量同步                      │
+  └───────────────────────┴──────────────────────────────┴───────────────────────────────────────┘
+
+  ---
+  一、新增货币类道具(如新货币)
+
+  步骤 1:item.xlsx 新增一行
+
+  Id=36, ItemType=1(Currency), ItemName=XX货币, Stack=0, AutoUse=0
+
+  步骤 2:iteminstance.xlsx 新增捆绑实例
+
+  InstanceId=3600, ItemId=36, ItemType=1
+
+  步骤 3:ItemDefines.cs 新增 ID 常量 + 加入 ItemsNotInPackage
+
+  文件:server/src/server/OpenCards.Server.Logic/Module/Item/ItemDefines.cs
+
+  public static int NewCurrencyItemId = 36; // 新货币
+
+  public static List<int> ItemsNotInPackage = new List<int>(){
+      DiamondItemId, FreeDiamondItemId, CoinItemId, VipExpItemId,
+      FreeVipExpItemId, HeroExpItemId, FriendPointId, RoleExpId,
+      NewCurrencyItemId  // ← 加这里
+  };
+
+  步骤 4:RoleData.cs 新增存储字段
+
+  文件:server/src/core/OpenCards.Core/ORM/RoleData.cs
+
+  [PersistField]
+  [SyncClientData(SyncClientRoleDataType.NewCurrency)]  // 如需客户端实时同步
+  public int NewCurrency;
+
+  步骤 5:0x35300.Logic.Role.cs 新增同步枚举值
+
+  文件:server/src/core/OpenCards.Core/Protocol/Client/0x35300.Logic.Role.cs
+
+  public enum SyncClientRoleDataType
+  {
+      // ... 现有值 ...
+      PresetHeroSelect2 = 45,
+      NewCurrency = 46,  // ← 新增
+  }
+
+  步骤 6:CurrencyModule.cs 新增增删查逻辑
+
+  文件:server/src/server/OpenCards.Server.Logic/Module/CurrencyModule.cs
+
+  在 OnEventAddItemImp 中加:
+  else if (itemInstance.ItemId == ItemDefines.NewCurrencyItemId)
+  {
+      if (ItemDefines.ItemsNotInPackage.Contains(itemInstance.ItemId))
+      {
+          beforeItemJObject.Add(EventTrackPropertyDefines.ResourceID, itemInstance.ItemId);
+          beforeItemJObject.Add(EventTrackPropertyDefines.ResourceName, itemName);
+          beforeItemJObject.Add(EventTrackPropertyDefines.ResourceNum, roleModule.roleDataMapping.NewCurrency);
+          needDig = true;
+      }
+      roleModule.roleDataMapping.NewCurrency += itemInstance.Count;
+  }
+
+  在 OnEventRemoveItem 中加:
+  else if (itemid == ItemDefines.NewCurrencyItemId)
+  {
+      if (ItemDefines.ItemsNotInPackage.Contains(itemid))
+          beforeItemJObject.Add(EventTrackPropertyDefines.ResourceNum, roleDataMapping.NewCurrency);
+      if (roleModule.roleDataMapping.NewCurrency < count)
+          success.value = false;
+      else
+      {
+          roleModule.roleDataMapping.NewCurrency -= count;
+          success.value = true;
+      }
+  }
+
+  在 OnEventGetItemCount 中加:
+  else if (itemid == ItemDefines.NewCurrencyItemId)
+  {
+      count.value = roleDataMapping.NewCurrency;
+  }
+
+  ---
+  二、新增代币类道具(不进背包,但走通用背包存储)
+
+  代币比货币简单得多,只需要配置表 + 常量,不需要改 CurrencyModule 和 RoleData。
+
+  步骤 1:item.xlsx 新增一行
+
+  Id=37, ItemType=3(Token), ItemName=XX代币, Stack=999999, AutoUse=0
+
+  步骤 2:iteminstance.xlsx 新增捆绑实例
+
+  InstanceId=3700, ItemId=37, ItemType=1
+
+  步骤 3:ItemDefines.cs 新增 ID 常量(不加 ItemsNotInPackage)
+
+  public static int NewTokenItemId = 37; // 新代币(走背包存储)
+
+  步骤 4:业务代码中使用
+
+  发放:
+  DispatchEvent(EventDefines.EventAddItem,
+      新的ItemInstanceId,  // iteminstance.xlsx 中的 InstanceId
+      count,
+      AddItemReason.YourReason, 0, LogicUtils.FileLine);
+
+  查询数量:
+  Ref<long> count = new Ref<long>();
+  DispatchEvent(EventDefines.EventGetItemCount, ItemDefines.NewTokenItemId, count);
+
+  扣除:
+  Ref<bool> success = new Ref<bool>();
+  DispatchEvent(EventDefines.EventRemoveItem, ItemDefines.NewTokenItemId,
+      amount, RemoveItemReason.YourReason, 0, LogicUtils.FileLine, success);
+
+  ---
+  总结对比
+
+  ┌────────────────────────┬───────────────┬───────────┐
+  │         修改点         │    货币类     │  代币类   │
+  ├────────────────────────┼───────────────┼───────────┤
+  │ item.xlsx              │ ✅            │ ✅        │
+  ├────────────────────────┼───────────────┼───────────┤
+  │ iteminstance.xlsx      │ ✅            │ ✅        │
+  ├────────────────────────┼───────────────┼───────────┤
+  │ ItemDefines.cs 常量    │ ✅            │ ✅        │
+  ├────────────────────────┼───────────────┼───────────┤
+  │ ItemsNotInPackage      │ ✅ 必须加     │ ❌ 不加   │
+  ├────────────────────────┼───────────────┼───────────┤
+  │ RoleData.cs 新字段     │ ✅ 必须加     │ ❌ 不需要 │
+  ├────────────────────────┼───────────────┼───────────┤
+  │ SyncClientRoleDataType │ ✅ 新增枚举   │ ❌ 不需要 │
+  ├────────────────────────┼───────────────┼───────────┤
+  │ CurrencyModule.cs      │ ✅ 三处加逻辑 │ ❌ 不需要 │
+  └────────────────────────┴───────────────┴───────────┘