686 lines
25 KiB
C#
686 lines
25 KiB
C#
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;
|
||
}
|
||
}
|