using System; using System.Collections.Generic; using UnityEngine; using Cysharp.Threading.Tasks; #if WECHAT_MINIGAME using WeChatWASM; #else using System.IO; // 仅在非微信平台使用 using ICSharpCode.SharpZipLib.Zip; #endif namespace Stary.Evo.Unzip { public static class CrossPlatformUnzip { public static async UniTask UnzipAsync(string zipPath, string extractPath, Action progressCallback = null, bool deleteAfterExtract = true) { #if WECHAT_MINIGAME && UNITY_WEBGL // 微信小程序:使用原生 WX API,零 System.IO await UnzipWeChatNativeAsync(zipPath, extractPath, progressCallback, deleteAfterExtract); #elif UNITY_WEBGL && !WECHAT_MINIGAME // WebGL:使用 SharpZipLib(虚拟文件系统) await UnzipWebGLAsync(zipPath, extractPath, progressCallback, deleteAfterExtract); #else // PC/移动端:标准 System.IO await UnzipStandardAsync(zipPath, extractPath, progressCallback, deleteAfterExtract); #endif } #region 1. 微信小程序实现(零 System.IO) #if WECHAT_MINIGAME private static async UniTask UnzipWeChatNativeAsync(string zipPath, string extractPath, Action progressCallback, bool deleteAfterExtract) { var tcs = new UniTaskCompletionSource(); var fs = WX.GetFileSystemManager(); // 检查 ZIP 存在(使用微信 API) bool fileExists = false; fs.Access(new AccessParam() { path = zipPath, success = (res) => fileExists = true, fail = (err) => { } }); await UniTask.Delay(50); // 等待异步结果 if (!fileExists) throw new Exception($"ZIP 不存在: {zipPath}"); // 清理目标目录(递归删除) try { fs.RmdirSync(extractPath, true); } catch { // 目录不存在或已删除 } // 创建目标目录 fs.MkdirSync(extractPath, true); // 模拟进度(WX.Unzip 无细粒度进度) float reportedProgress = 0f; var progressTask = SimulateProgressAsync(progressCallback, () => reportedProgress >= 1f); // 微信原生解压(性能最佳,C++ 实现) WX.GetFileSystemManager().Unzip(new UnzipOption() { zipFilePath = zipPath, targetPath = extractPath, success = (res) => { reportedProgress = 1f; progressCallback?.Invoke(1f); tcs.TrySetResult(); }, fail = (err) => { tcs.TrySetException(new Exception($"微信解压失败: {err.errMsg}")); } }); await tcs.Task; await progressTask; // 删除 ZIP 释放空间(微信 200MB 限制严格) if (deleteAfterExtract) { DeleteWeChatFileSafe(fs, zipPath); } } private static async UniTask SimulateProgressAsync(Action callback, Func isCompleted) { if (callback == null) return; float progress = 0f; while (!isCompleted()) { progress = Mathf.Min(progress + UnityEngine.Random.Range(0.02f, 0.08f), 0.95f); callback.Invoke(progress); await UniTask.Delay(200); } } private static void DeleteWeChatFileSafe(WXFileSystemManager fs, string path) { try { fs.UnlinkSync(path); Debug.Log($"[微信] 已删除 ZIP"); } catch (Exception ex) { Debug.LogWarning($"[微信] 删除失败: {ex.Message}"); } } /// /// 获取微信缓存路径(字符串拼接,不使用 Path.Combine) /// public static string GetWeChatPath(string relativePath) { // 手动拼接路径,避免使用 System.IO.Path string basePath = WX.env.USER_DATA_PATH; if (relativePath.StartsWith("/")) return basePath + relativePath; return basePath + "/" + relativePath; } #endif #endregion #region 2. WebGL 实现(SharpZipLib + 虚拟文件系统) #if UNITY_WEBGL && !WECHAT_MINIGAME private static async UniTask UnzipWebGLAsync(string zipPath, string extractPath, Action progressCallback, bool deleteAfterExtract) { // WebGL 使用 Unity 的虚拟文件系统(基于 IndexedDB) // 注意:虽然是 FileStream,但在 WebGL 中实际是虚拟 IO if (!File.Exists(zipPath)) throw new FileNotFoundException($"ZIP 不存在: {zipPath}"); string fullExtractPath = Path.GetFullPath(extractPath); // 清理目录 if (Directory.Exists(extractPath)) Directory.Delete(extractPath, true); Directory.CreateDirectory(extractPath); using (FileStream fs = File.OpenRead(zipPath)) using (ZipFile zipFile = new ZipFile(fs)) { int total = 0; var validEntries = new List(); foreach (ZipEntry entry in zipFile) { if (entry.IsFile && !string.IsNullOrEmpty(entry.Name)) { validEntries.Add(entry); total++; } } // WebGL 分帧解压,避免卡死浏览器 int processed = 0; const int FILES_PER_FRAME = 3; foreach (var entry in validEntries) { // 安全检查 string fullPath = Path.GetFullPath(Path.Combine(extractPath, entry.Name)); if (!fullPath.StartsWith(fullExtractPath)) { Debug.LogWarning($"跳过非法路径: {entry.Name}"); continue; } // 创建目录 string dir = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); // 解压单文件 using (Stream input = zipFile.GetInputStream(entry)) using (FileStream output = File.Create(fullPath)) { byte[] buffer = new byte[4096]; // WebGL 使用更小缓冲区 int read; while ((read = input.Read(buffer, 0, buffer.Length)) > 0) { output.Write(buffer, 0, read); } } processed++; // 每 N 个文件让出一帧,保持浏览器响应 if (processed % FILES_PER_FRAME == 0) { progressCallback?.Invoke((float)processed / total); await UniTask.Yield(); } } } if (deleteAfterExtract) { File.Delete(zipPath); } } #endif #endregion #region 3. PC/移动端实现(标准 System.IO) #if !UNITY_WEBGL && !WECHAT_MINIGAME private static async UniTask UnzipStandardAsync(string zipPath, string extractPath, Action progressCallback, bool deleteAfterExtract) { await UniTask.SwitchToThreadPool(); try { if (!File.Exists(zipPath)) throw new FileNotFoundException($"ZIP 不存在: {zipPath}"); string fullExtractPath = Path.GetFullPath(extractPath); if (Directory.Exists(extractPath)) Directory.Delete(extractPath, true); Directory.CreateDirectory(extractPath); using (FileStream fs = File.OpenRead(zipPath)) using (ZipFile zipFile = new ZipFile(fs)) { int total = 0; foreach (ZipEntry entry in zipFile) if (entry.IsFile) total++; int processed = 0; foreach (ZipEntry entry in zipFile) { if (!entry.IsFile) continue; string fullPath = Path.GetFullPath(Path.Combine(extractPath, entry.Name)); if (!fullPath.StartsWith(fullExtractPath)) continue; Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); using (Stream input = zipFile.GetInputStream(entry)) using (FileStream output = File.Create(fullPath)) { await input.CopyToAsync(output); } processed++; await UniTask.SwitchToMainThread(); progressCallback?.Invoke((float)processed / total); await UniTask.SwitchToThreadPool(); } } } finally { await UniTask.SwitchToMainThread(); if (deleteAfterExtract && File.Exists(zipPath)) File.Delete(zipPath); } } #endif #endregion #region 4. 公共工具(平台特定) /// /// 获取缓存路径(平台特定实现) /// public static string GetCachePath(string relativePath) { #if WECHAT_MINIGAME return GetWeChatPath(relativePath); #else return Path.Combine(Application.persistentDataPath, relativePath); #endif } /// /// 检查文件存在(平台特定) /// public static bool FileExists(string path) { #if WECHAT_MINIGAME bool exists = false; WX.GetFileSystemManager().Access(new AccessParam() { path = path, success = (res) => exists = true, fail = (err) => { } }); return exists; #else return File.Exists(path); #endif } #endregion } }