拉取资源

This commit is contained in:
xianyi
2025-08-28 16:09:01 +08:00
parent d2c5f509c3
commit 254a40d87d
486 changed files with 3540 additions and 28962 deletions

View File

@@ -0,0 +1,685 @@
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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: da41884ccee8c42b594e97ccc928ddf2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: