Files
tcg-client/Assets/TcgEngine/Scripts/Network/ResourceDownloader.cs
2025-08-28 16:09:01 +08:00

686 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
namespace TcgEngine
{
/// <summary>
/// 资源下载管理器
/// </summary>
public class ResourceDownloader : MonoBehaviour
{
[Header("Download Settings")]
public string serverUrl = "https://cardcdn.ambigrat.com";
public string resourcesEndpoint = "/test/{version}.zip";
public string versionEndpoint = "/api/version";
[Header("URL Pattern Options")]
[Tooltip("支持的占位符: {version} - 版本号")]
public bool useVersionInFilename = true;
[Tooltip("如果启用,会先检查版本化文件是否存在")]
public bool validateVersionedUrl = false;
[Tooltip("调试模式:当版本接口失败时,使用固定版本号")]
public bool debugMode = true;
[Tooltip("调试模式下使用的固定版本号")]
public string debugVersion = "0.0.1";
[Tooltip("调试模式下使用的固定MD5值")]
public string debugMd5 = "ceb24758054d6dcf1e23ddb41811a525";
private string currentVersion = "0.0.0";
private string currentMd5 = "";
private string targetVersion = "0.0.1";
private string targetMd5 = "";
private string persistentDataPath;
private string spritesPath;
public static ResourceDownloader instance;
// 事件
public event Action<float> OnDownloadProgress;
public event Action<string> OnDownloadComplete;
public event Action<string> OnDownloadError;
public event Action<float> OnExtractProgress;
public event Action OnExtractComplete;
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
InitializePaths();
}
else
{
// 如果已经存在实例,销毁这个重复的组件
Debug.LogWarning("ResourceDownloader instance already exists, destroying duplicate");
Destroy(gameObject);
}
}
private void InitializePaths()
{
persistentDataPath = Application.persistentDataPath;
spritesPath = Path.Combine(persistentDataPath, "Sprites");
// 确保目录存在
if (!Directory.Exists(spritesPath))
{
Directory.CreateDirectory(spritesPath);
}
LoadCurrentVersion();
}
/// <summary>
/// 检查是否需要更新资源
/// </summary>
public async Task<bool> CheckForUpdates()
{
try
{
Debug.Log($"Starting update check. Current version: {currentVersion}, Current MD5: {currentMd5}");
var serverVersionData = await GetServerVersionData();
if (serverVersionData == null || string.IsNullOrEmpty(serverVersionData.version))
{
Debug.LogWarning("Failed to get server version");
// 调试模式:版本接口失败时使用固定版本号
if (debugMode)
{
Debug.Log($"Debug mode enabled: using fixed version {debugVersion} and MD5 {debugMd5}");
targetVersion = debugVersion;
targetMd5 = debugMd5;
return await CheckVersionAndMd5();
}
return false;
}
targetVersion = serverVersionData.version;
targetMd5 = serverVersionData.md5 ?? "";
Debug.Log($"Server version: {targetVersion}, MD5: {targetMd5}, Local version: {currentVersion}, Local MD5: {currentMd5}");
return await CheckVersionAndMd5();
}
catch (Exception e)
{
Debug.LogError($"Error checking for updates: {e.Message}");
// 调试模式:出现异常时也尝试使用固定版本
if (debugMode)
{
Debug.Log($"Debug mode: attempting to use version {debugVersion} despite error");
targetVersion = debugVersion;
targetMd5 = debugMd5;
return await CheckVersionAndMd5();
}
return false;
}
}
/// <summary>
/// 检查版本和MD5决定是否需要下载
/// </summary>
private async Task<bool> CheckVersionAndMd5()
{
// 检查版本是否不同
if (currentVersion != targetVersion)
{
Debug.Log($"Version mismatch. Current: {currentVersion}, Target: {targetVersion}");
return true;
}
// 检查本地文件是否存在
if (!Directory.Exists(spritesPath) || Directory.GetFiles(spritesPath, "*", SearchOption.AllDirectories).Length == 0)
{
Debug.Log("Local sprites not found, need to download");
return true;
}
// 版本相同检查MD5
if (string.IsNullOrEmpty(targetMd5))
{
Debug.Log("No target MD5 provided, assuming files are correct");
return false;
}
Debug.Log($"版本匹配开始检查MD5校验...");
Debug.Log($"当前版本: {currentVersion}");
Debug.Log($"目标版本: {targetVersion}");
Debug.Log($"保存的MD5: {currentMd5}");
Debug.Log($"目标MD5: {targetMd5}");
string localMd5 = await CalculateDirectoryMd5(spritesPath);
if (localMd5 != targetMd5)
{
Debug.LogWarning($"🚨 MD5校验失败");
Debug.LogWarning($"本地计算MD5: {localMd5}");
Debug.LogWarning($"期望的MD5: {targetMd5}");
Debug.Log("正在删除损坏的文件并重新下载...");
// 删除损坏的文件
if (Directory.Exists(spritesPath))
{
Directory.Delete(spritesPath, true);
}
return true; // 需要重新下载
}
Debug.Log($"✅ MD5校验通过: {localMd5}");
Debug.Log("文件完整性验证成功,无需下载");
return false; // 无需下载
}
/// <summary>
/// 计算目录下所有文件的MD5和
/// </summary>
private async Task<string> CalculateDirectoryMd5(string directoryPath)
{
try
{
if (!Directory.Exists(directoryPath))
return "";
var files = Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories);
if (files.Length == 0)
return "";
Debug.Log($"=== 开始计算目录MD5 ===");
Debug.Log($"目录路径: {directoryPath}");
Debug.Log($"文件数量: {files.Length}");
// 按路径排序确保一致性
Array.Sort(files);
// 打印前几个文件作为示例
Debug.Log("文件列表示例:");
for (int i = 0; i < Math.Min(5, files.Length); i++)
{
string relativePath = files[i].Substring(directoryPath.Length + 1).Replace('\\', '/');
Debug.Log($" [{i+1}] {relativePath}");
}
if (files.Length > 5)
{
Debug.Log($" ... 还有 {files.Length - 5} 个文件");
}
using (var md5 = System.Security.Cryptography.MD5.Create())
{
foreach (string file in files)
{
// 只处理相对路径部分
string relativePath = file.Substring(directoryPath.Length + 1).Replace('\\', '/');
byte[] pathBytes = System.Text.Encoding.UTF8.GetBytes(relativePath);
md5.TransformBlock(pathBytes, 0, pathBytes.Length, null, 0);
// 读取文件内容
byte[] fileBytes = File.ReadAllBytes(file);
md5.TransformBlock(fileBytes, 0, fileBytes.Length, null, 0);
// 每10个文件yield一次避免阻塞
if (Array.IndexOf(files, file) % 10 == 0)
{
await Task.Yield();
}
}
// 完成哈希计算
md5.TransformFinalBlock(new byte[0], 0, 0);
string result = BitConverter.ToString(md5.Hash).Replace("-", "").ToLowerInvariant();
Debug.Log($"=== MD5计算完成 ===");
Debug.Log($"本地计算的MD5: {result}");
Debug.Log($"目标MD5: {targetMd5}");
Debug.Log($"MD5匹配: {(result == targetMd5 ? " " : " ")}");
Debug.Log($"===================");
return result;
}
}
catch (Exception e)
{
Debug.LogError($"Error calculating directory MD5: {e.Message}");
return "";
}
}
/// <summary>
/// 构建版本化的下载URL
/// </summary>
private string BuildVersionedDownloadUrl(string version)
{
string targetFileName;
if (!useVersionInFilename)
{
// 不使用版本化文件名时,统一使用"sprites"
targetFileName = "sprites";
}
else if (string.IsNullOrEmpty(version))
{
// 如果版本为空,使用调试版本
targetFileName = debugVersion;
}
else
{
// 使用实际版本号
targetFileName = version;
}
string endpoint = resourcesEndpoint.Replace("{version}", targetFileName);
string url = serverUrl + endpoint;
Debug.Log($"Built download URL: {url} (version: {version}, filename: {targetFileName})");
return url;
}
/// <summary>
/// 验证版本化URL是否存在可选
/// </summary>
private async Task<bool> ValidateVersionedUrl(string url)
{
if (!validateVersionedUrl)
return true;
try
{
using (UnityWebRequest request = UnityWebRequest.Head(url))
{
var operation = request.SendWebRequest();
while (!operation.isDone)
{
await Task.Yield();
}
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log($"Validated versioned URL: {url}");
return true;
}
else
{
Debug.LogWarning($"Versioned URL not found: {url} - {request.error}");
return false;
}
}
}
catch (Exception e)
{
Debug.LogError($"Error validating URL {url}: {e.Message}");
return false;
}
}
/// <summary>
/// 获取服务器版本数据
/// </summary>
private async Task<ResourceVersionResponse> GetServerVersionData()
{
// 从NetworkData获取游戏服务器地址
string gameServerUrl = NetworkData.Get().api_url;
if (string.IsNullOrEmpty(gameServerUrl))
{
Debug.LogError("Failed to get game server URL from NetworkData");
return null;
}
// 构建完整的版本检查URL
string protocol = NetworkData.Get().api_https ? "https://" : "http://";
string url = protocol + gameServerUrl + versionEndpoint;
Debug.Log($"Version check URL: {url}");
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
var operation = request.SendWebRequest();
while (!operation.isDone)
{
await Task.Yield();
}
if (request.result == UnityWebRequest.Result.Success)
{
try
{
var versionData = JsonUtility.FromJson<ResourceVersionResponse>(request.downloadHandler.text);
return versionData;
}
catch (Exception e)
{
Debug.LogError($"Failed to parse version response: {e.Message}");
return null;
}
}
else
{
Debug.LogError($"Failed to get version: {request.error}");
return null;
}
}
}
/// <summary>
/// 下载并更新资源
/// </summary>
public async Task<bool> DownloadAndUpdateResources()
{
try
{
string zipFilePath = Path.Combine(persistentDataPath, $"{targetVersion}.zip");
// 构建版本化的下载URL
string downloadUrl = BuildVersionedDownloadUrl(targetVersion);
// 验证URL是否存在可选
if (validateVersionedUrl)
{
bool urlExists = await ValidateVersionedUrl(downloadUrl);
if (!urlExists)
{
OnDownloadError?.Invoke($"Versioned resource not found for version {targetVersion}");
return false;
}
}
// 下载压缩包
bool downloadSuccess = await DownloadFile(downloadUrl, zipFilePath);
if (!downloadSuccess)
{
OnDownloadError?.Invoke("Failed to download resources");
return false;
}
OnDownloadComplete?.Invoke(zipFilePath);
// 解压文件
bool extractSuccess = await ExtractZipFile(zipFilePath, spritesPath);
if (!extractSuccess)
{
OnDownloadError?.Invoke("Failed to extract resources");
return false;
}
OnExtractComplete?.Invoke();
// 清理下载的压缩包
if (File.Exists(zipFilePath))
{
File.Delete(zipFilePath);
}
// 更新版本信息
SaveCurrentVersion(targetVersion, targetMd5);
currentVersion = targetVersion;
currentMd5 = targetMd5;
Debug.Log($"Resources updated successfully to version {currentVersion}");
return true;
}
catch (Exception e)
{
Debug.LogError($"Error downloading/updating resources: {e.Message}");
OnDownloadError?.Invoke(e.Message);
return false;
}
}
/// <summary>
/// 下载文件
/// </summary>
private async Task<bool> DownloadFile(string url, string filePath)
{
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
var operation = request.SendWebRequest();
while (!operation.isDone)
{
OnDownloadProgress?.Invoke(request.downloadProgress);
await Task.Yield();
}
if (request.result == UnityWebRequest.Result.Success)
{
File.WriteAllBytes(filePath, request.downloadHandler.data);
return true;
}
else
{
Debug.LogError($"Download failed: {request.error}");
return false;
}
}
}
/// <summary>
/// 解压ZIP文件
/// </summary>
private async Task<bool> ExtractZipFile(string zipFilePath, string extractPath)
{
try
{
// 清空目标目录
if (Directory.Exists(extractPath))
{
Directory.Delete(extractPath, true);
}
Directory.CreateDirectory(extractPath);
using (var archive = ZipFile.OpenRead(zipFilePath))
{
int totalEntries = archive.Entries.Count;
int processedEntries = 0;
foreach (var entry in archive.Entries)
{
// 跳过目录条目
if (string.IsNullOrEmpty(entry.Name))
continue;
string entryPath = Path.Combine(extractPath, entry.FullName);
string directoryName = Path.GetDirectoryName(entryPath);
if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
{
Directory.CreateDirectory(directoryName);
}
using (var entryStream = entry.Open())
using (var outputStream = File.Create(entryPath))
{
await entryStream.CopyToAsync(outputStream);
}
processedEntries++;
OnExtractProgress?.Invoke((float)processedEntries / totalEntries);
if (processedEntries % 10 == 0) // 每10个文件yield一次
{
await Task.Yield();
}
}
}
return true;
}
catch (Exception e)
{
Debug.LogError($"Extract failed: {e.Message}");
return false;
}
}
/// <summary>
/// 加载当前版本信息
/// </summary>
private void LoadCurrentVersion()
{
string versionFile = Path.Combine(persistentDataPath, "version.txt");
if (File.Exists(versionFile))
{
string[] lines = File.ReadAllLines(versionFile);
if (lines.Length > 0)
{
currentVersion = lines[0].Trim();
}
if (lines.Length > 1)
{
currentMd5 = lines[1].Trim();
}
Debug.Log($"Loaded current version: {currentVersion}, MD5: {currentMd5}");
}
else
{
currentVersion = "0.0.0";
currentMd5 = "";
Debug.Log($"No version file found, using defaults: {currentVersion}");
}
}
/// <summary>
/// 保存当前版本信息
/// </summary>
private void SaveCurrentVersion(string version, string md5 = "")
{
string versionFile = Path.Combine(persistentDataPath, "version.txt");
string content = version;
if (!string.IsNullOrEmpty(md5))
{
content += "\n" + md5;
}
File.WriteAllText(versionFile, content);
Debug.Log($"Saved version: {version}, MD5: {md5}");
}
/// <summary>
/// 获取本地Sprite资源路径
/// </summary>
public string GetLocalSpritePath(string relativePath)
{
return Path.Combine(spritesPath, relativePath);
}
/// <summary>
/// 检查本地资源是否存在
/// </summary>
public bool HasLocalResources()
{
return Directory.Exists(spritesPath) &&
Directory.GetFiles(spritesPath, "*", SearchOption.AllDirectories).Length > 0;
}
public string GetCurrentVersion()
{
return currentVersion;
}
/// <summary>
/// 获取指定版本的下载URL用于调试
/// </summary>
public string GetDownloadUrlForVersion(string version)
{
return BuildVersionedDownloadUrl(version);
}
/// <summary>
/// 获取当前目标版本的下载URL
/// </summary>
public string GetCurrentDownloadUrl()
{
return BuildVersionedDownloadUrl(targetVersion);
}
/// <summary>
/// 获取本地Sprites目录的完整路径用于调试
/// </summary>
public string GetSpritesDirectoryPath()
{
return spritesPath;
}
/// <summary>
/// 手动计算并打印当前Sprites目录的MD5
/// </summary>
public async void CalculateAndLogCurrentMd5()
{
Debug.Log("=== 手动计算当前Sprites目录MD5 ===");
if (!Directory.Exists(spritesPath))
{
Debug.LogWarning("Sprites目录不存在");
return;
}
string calculatedMd5 = await CalculateDirectoryMd5(spritesPath);
Debug.Log($"手动计算结果: {calculatedMd5}");
Debug.Log($"当前保存的MD5: {currentMd5}");
Debug.Log($"调试MD5: {debugMd5}");
Debug.Log("================================");
}
/// <summary>
/// 打印本地资源路径信息
/// </summary>
public void LogResourcePaths()
{
Debug.Log($"=== Resource Paths ===");
Debug.Log($"persistentDataPath: {persistentDataPath}");
Debug.Log($"Sprites Directory: {spritesPath}");
Debug.Log($"Directory Exists: {Directory.Exists(spritesPath)}");
if (Directory.Exists(spritesPath))
{
var files = Directory.GetFiles(spritesPath, "*", SearchOption.AllDirectories);
Debug.Log($"Total files in Sprites: {files.Length}");
// 显示前10个文件作为示例
for (int i = 0; i < Math.Min(10, files.Length); i++)
{
string relativePath = files[i].Substring(spritesPath.Length + 1);
Debug.Log($" {relativePath}");
}
if (files.Length > 10)
{
Debug.Log($" ... and {files.Length - 10} more files");
}
}
Debug.Log($"======================");
}
public static ResourceDownloader Get()
{
if (instance == null)
{
// 如果没有实例,创建一个新的
GameObject go = new GameObject("ResourceDownloader");
instance = go.AddComponent<ResourceDownloader>();
DontDestroyOnLoad(go);
Debug.Log("Created new ResourceDownloader instance");
}
return instance;
}
}
/// <summary>
/// 资源版本响应数据结构
/// </summary>
[Serializable]
public class ResourceVersionResponse
{
public string version;
public string md5;
public string description;
public long timestamp;
}
}