Files
tcg-client/Assets/TcgEngine/Scripts/Network/ResourceDownloader.cs
2025-08-28 17:44:28 +08:00

565 lines
20 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("Configuration")]
[Tooltip("资源下载器配置文件")]
public ResourceDownloaderConfig config;
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))
{
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}");
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校验... 当前版本: {currentVersion} 目标版本: {targetVersion} 当前MD5: {currentMd5} 目标MD5: {targetMd5}");
string localMd5 = await CalculateDirectoryMd5(spritesPath);
if (localMd5 != targetMd5)
{
Debug.Log("MD5校验失败正在删除损坏的文件并重新下载...");
// 删除损坏的文件
if (Directory.Exists(spritesPath))
{
Directory.Delete(spritesPath, true);
}
return true; // 需要重新下载
}
Debug.Log($"✅ MD5校验通过");
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 "";
// 按路径排序确保一致性
Array.Sort(files);
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();
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 (string.IsNullOrEmpty(version))
{
Debug.LogError("Version is null");
return "0.0.0";
}
else
{
targetFileName = version;
}
if (config == null)
{
Debug.LogError("ResourceDownloaderConfig is not assigned!");
return "0.0.0";
}
string url = config.GetResourceUrl(targetFileName);
Debug.Log($"Built download URL: {url} (version: {version}, filename: {targetFileName})");
return url;
}
/// <summary>
/// 验证版本化URL是否存在可选
/// </summary>
private async Task<bool> ValidateVersionedUrl(string url)
{
if (config == null || !config.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()
{
if (config == null)
{
Debug.LogError("ResourceDownloaderConfig is not assigned!");
return null;
}
string url = config.GetVersionUrl();
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 (config != null && config.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;
}
public static ResourceDownloader Get()
{
if (instance == null)
{
// 如果没有实例,创建一个新的
GameObject go = new GameObject("ResourceDownloader");
instance = go.AddComponent<ResourceDownloader>();
DontDestroyOnLoad(go);
// 加载配置文件
instance.LoadConfig();
Debug.Log("Created new ResourceDownloader instance");
}
return instance;
}
/// <summary>
/// 加载配置文件
/// </summary>
private void LoadConfig()
{
// 从Resources文件夹加载配置文件
config = Resources.Load<ResourceDownloaderConfig>("ResourceDownloaderConfig");
if (config == null)
{
Debug.LogError("ResourceDownloaderConfig not found in Resources folder! Please create a ResourceDownloaderConfig asset in the Resources folder.");
}
else
{
Debug.Log("ResourceDownloaderConfig loaded successfully");
}
}
}
/// <summary>
/// 资源版本响应数据结构
/// </summary>
[Serializable]
public class ResourceVersionResponse
{
public string version;
public string md5;
public string description;
public long timestamp;
}
}