2026-04-16 22:04:55 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using Cysharp.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
|
|
#if WECHAT_MINIGAME
|
|
|
|
|
|
using WeChatWASM;
|
2026-04-16 22:26:36 +08:00
|
|
|
|
|
2026-04-16 22:04:55 +08:00
|
|
|
|
#else
|
|
|
|
|
|
using System.IO; // 仅在非微信平台使用
|
|
|
|
|
|
using ICSharpCode.SharpZipLib.Zip;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
namespace Stary.Evo.Unzip
|
|
|
|
|
|
{
|
|
|
|
|
|
public static class CrossPlatformUnzip
|
|
|
|
|
|
{
|
2026-04-16 22:26:36 +08:00
|
|
|
|
public static async UniTask UnzipAsync(string zipPath, string extractPath,
|
2026-04-16 22:04:55 +08:00
|
|
|
|
Action<float> 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
|
2026-04-16 22:26:36 +08:00
|
|
|
|
private static async UniTask UnzipWeChatNativeAsync(string zipPath, string extractPath,
|
2026-04-16 22:04:55 +08:00
|
|
|
|
Action<float> progressCallback, bool deleteAfterExtract)
|
|
|
|
|
|
{
|
|
|
|
|
|
var tcs = new UniTaskCompletionSource();
|
|
|
|
|
|
var fs = WX.GetFileSystemManager();
|
|
|
|
|
|
|
|
|
|
|
|
// 检查 ZIP 存在(使用微信 API)
|
|
|
|
|
|
bool fileExists = false;
|
2026-04-16 22:26:36 +08:00
|
|
|
|
fs.Access(new AccessParam()
|
2026-04-16 22:04:55 +08:00
|
|
|
|
{
|
|
|
|
|
|
path = zipPath,
|
|
|
|
|
|
success = (res) => fileExists = true,
|
|
|
|
|
|
fail = (err) => { }
|
|
|
|
|
|
});
|
2026-04-16 22:26:36 +08:00
|
|
|
|
|
2026-04-16 22:04:55 +08:00
|
|
|
|
await UniTask.Delay(50); // 等待异步结果
|
|
|
|
|
|
if (!fileExists)
|
|
|
|
|
|
throw new Exception($"ZIP 不存在: {zipPath}");
|
|
|
|
|
|
|
|
|
|
|
|
// 清理目标目录(递归删除)
|
2026-04-16 22:26:36 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
fs.RmdirSync(extractPath, true);
|
2026-04-16 22:04:55 +08:00
|
|
|
|
}
|
2026-04-16 22:26:36 +08:00
|
|
|
|
catch
|
|
|
|
|
|
{
|
2026-04-16 22:04:55 +08:00
|
|
|
|
// 目录不存在或已删除
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建目标目录
|
|
|
|
|
|
fs.MkdirSync(extractPath, true);
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟进度(WX.Unzip 无细粒度进度)
|
|
|
|
|
|
float reportedProgress = 0f;
|
|
|
|
|
|
var progressTask = SimulateProgressAsync(progressCallback, () => reportedProgress >= 1f);
|
|
|
|
|
|
|
|
|
|
|
|
// 微信原生解压(性能最佳,C++ 实现)
|
2026-04-16 22:26:36 +08:00
|
|
|
|
WX.GetFileSystemManager().Unzip(new UnzipOption()
|
2026-04-16 22:04:55 +08:00
|
|
|
|
{
|
|
|
|
|
|
zipFilePath = zipPath,
|
|
|
|
|
|
targetPath = extractPath,
|
|
|
|
|
|
success = (res) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
reportedProgress = 1f;
|
|
|
|
|
|
progressCallback?.Invoke(1f);
|
|
|
|
|
|
tcs.TrySetResult();
|
|
|
|
|
|
},
|
2026-04-16 22:26:36 +08:00
|
|
|
|
fail = (err) => { tcs.TrySetException(new Exception($"微信解压失败: {err.errMsg}")); }
|
2026-04-16 22:04:55 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await tcs.Task;
|
|
|
|
|
|
await progressTask;
|
|
|
|
|
|
|
|
|
|
|
|
// 删除 ZIP 释放空间(微信 200MB 限制严格)
|
|
|
|
|
|
if (deleteAfterExtract)
|
|
|
|
|
|
{
|
|
|
|
|
|
DeleteWeChatFileSafe(fs, zipPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static async UniTask SimulateProgressAsync(Action<float> callback, Func<bool> isCompleted)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (callback == null) return;
|
2026-04-16 22:26:36 +08:00
|
|
|
|
|
2026-04-16 22:04:55 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 22:26:36 +08:00
|
|
|
|
private static void DeleteWeChatFileSafe(WXFileSystemManager fs, string path)
|
2026-04-16 22:04:55 +08:00
|
|
|
|
{
|
2026-04-16 22:26:36 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
fs.UnlinkSync(path);
|
2026-04-16 22:04:55 +08:00
|
|
|
|
Debug.Log($"[微信] 已删除 ZIP");
|
|
|
|
|
|
}
|
2026-04-16 22:26:36 +08:00
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning($"[微信] 删除失败: {ex.Message}");
|
2026-04-16 22:04:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取微信缓存路径(字符串拼接,不使用 Path.Combine)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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<float> 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<ZipEntry>();
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-04-16 22:26:36 +08:00
|
|
|
|
#if !UNITY_WEBGL && !WECHAT_MINIGAME
|
|
|
|
|
|
private static async UniTask UnzipStandardAsync(string zipPath, string extractPath,
|
2026-04-16 22:04:55 +08:00
|
|
|
|
Action<float> progressCallback, bool deleteAfterExtract)
|
|
|
|
|
|
{
|
|
|
|
|
|
await UniTask.SwitchToThreadPool();
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!File.Exists(zipPath))
|
|
|
|
|
|
throw new FileNotFoundException($"ZIP 不存在: {zipPath}");
|
|
|
|
|
|
|
|
|
|
|
|
string fullExtractPath = Path.GetFullPath(extractPath);
|
2026-04-16 22:26:36 +08:00
|
|
|
|
|
2026-04-16 22:04:55 +08:00
|
|
|
|
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)
|
2026-04-16 22:26:36 +08:00
|
|
|
|
if (entry.IsFile)
|
|
|
|
|
|
total++;
|
2026-04-16 22:04:55 +08:00
|
|
|
|
|
|
|
|
|
|
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++;
|
2026-04-16 22:26:36 +08:00
|
|
|
|
|
2026-04-16 22:04:55 +08:00
|
|
|
|
await UniTask.SwitchToMainThread();
|
|
|
|
|
|
progressCallback?.Invoke((float)processed / total);
|
|
|
|
|
|
await UniTask.SwitchToThreadPool();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
await UniTask.SwitchToMainThread();
|
|
|
|
|
|
if (deleteAfterExtract && File.Exists(zipPath))
|
|
|
|
|
|
File.Delete(zipPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-16 22:26:36 +08:00
|
|
|
|
#endif
|
2026-04-16 22:04:55 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 4. 公共工具(平台特定)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取缓存路径(平台特定实现)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static string GetCachePath(string relativePath)
|
|
|
|
|
|
{
|
|
|
|
|
|
#if WECHAT_MINIGAME
|
|
|
|
|
|
return GetWeChatPath(relativePath);
|
|
|
|
|
|
#else
|
|
|
|
|
|
return Path.Combine(Application.persistentDataPath, relativePath);
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 检查文件存在(平台特定)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static bool FileExists(string path)
|
|
|
|
|
|
{
|
|
|
|
|
|
#if WECHAT_MINIGAME
|
|
|
|
|
|
bool exists = false;
|
2026-04-16 22:26:36 +08:00
|
|
|
|
WX.GetFileSystemManager().Access(new AccessParam()
|
2026-04-16 22:04:55 +08:00
|
|
|
|
{
|
|
|
|
|
|
path = path,
|
|
|
|
|
|
success = (res) => exists = true,
|
|
|
|
|
|
fail = (err) => { }
|
|
|
|
|
|
});
|
|
|
|
|
|
return exists;
|
|
|
|
|
|
#else
|
|
|
|
|
|
return File.Exists(path);
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|