using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Security.Cryptography; using System.Text; using UnityEngine; public static class DlcPackageArchiveService { [Serializable] private sealed class InstallState { public string sourcePath; public long sourceFileLength; public long sourceWriteTicksUtc; } private const string RuntimeFolderName = "DLC"; private const string PackageFolderName = "packages"; private const string InstalledFolderName = "installed_packages"; private const string PackageExtension = ".bsnkdlc"; private const string StateFileName = ".bsnkdlc.installstate.json"; private static readonly HashSet PreparedPackagesThisSession = new HashSet(StringComparer.OrdinalIgnoreCase); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetRuntimeState() { PreparedPackagesThisSession.Clear(); } public static void PrepareInstalledPackages() { string[] searchRoots = GetPackageSearchRoots(); HashSet visited = new HashSet(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < searchRoots.Length; i++) { string root = searchRoots[i]; if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root)) { continue; } string[] packageFiles; try { packageFiles = Directory.GetFiles(root, "*" + PackageExtension, SearchOption.AllDirectories); } catch (Exception ex) { Debug.LogWarning("[DLC] Failed to enumerate package files in '" + root + "': " + ex.Message); continue; } for (int packageIndex = 0; packageIndex < packageFiles.Length; packageIndex++) { string packagePath = packageFiles[packageIndex]; if (string.IsNullOrWhiteSpace(packagePath)) { continue; } string fullPackagePath; try { fullPackagePath = Path.GetFullPath(packagePath); } catch { continue; } if (!visited.Add(fullPackagePath)) { continue; } TryInstallPackage(fullPackagePath); } } } public static string[] GetPackageSearchRoots() { List result = new List(); AppendIfValid(result, Path.Combine(Application.streamingAssetsPath, RuntimeFolderName, PackageFolderName)); AppendIfValid(result, Path.Combine(Application.streamingAssetsPath, RuntimeFolderName)); AppendIfValid(result, Path.Combine(Application.persistentDataPath, RuntimeFolderName, PackageFolderName)); AppendIfValid(result, Path.Combine(Application.persistentDataPath, RuntimeFolderName)); #if !UNITY_ANDROID // 项目外(可执行文件同级目录)的 DLC 扫描仅在 PC 端有意义; // 安卓无此目录概念,Application.dataPath 指向 APK,跳过以避免无效/异常路径。 string playerRoot = GetPlayerRootDirectory(); AppendIfValid(result, Path.Combine(playerRoot, RuntimeFolderName, PackageFolderName)); AppendIfValid(result, Path.Combine(playerRoot, RuntimeFolderName)); #endif return result.ToArray(); } private static void TryInstallPackage(string packagePath) { if (string.IsNullOrWhiteSpace(packagePath) || !File.Exists(packagePath)) { return; } string installDirectory = GetInstallDirectory(packagePath); if (string.IsNullOrWhiteSpace(installDirectory)) { return; } bool needsInstall = NeedsInstall(packagePath, installDirectory); if (!needsInstall && PreparedPackagesThisSession.Contains(packagePath)) { return; } try { if (!needsInstall) { PreparedPackagesThisSession.Add(packagePath); return; } string installRoot = Path.GetDirectoryName(installDirectory) ?? string.Empty; string stagingDirectory = installDirectory + ".staging"; if (!string.IsNullOrWhiteSpace(installRoot)) { Directory.CreateDirectory(installRoot); } if (Directory.Exists(stagingDirectory)) { Directory.Delete(stagingDirectory, true); } Directory.CreateDirectory(stagingDirectory); ZipFile.ExtractToDirectory(packagePath, stagingDirectory); WriteInstallState(packagePath, stagingDirectory); if (Directory.Exists(installDirectory)) { Directory.Delete(installDirectory, true); } Directory.Move(stagingDirectory, installDirectory); PreparedPackagesThisSession.Add(packagePath); } catch (Exception ex) { Debug.LogWarning("[DLC] Failed to install package '" + packagePath + "': " + ex.Message); } } private static bool NeedsInstall(string packagePath, string installDirectory) { if (!Directory.Exists(installDirectory)) { return true; } InstallState state = ReadInstallState(installDirectory); if (state == null) { return true; } FileInfo info; try { info = new FileInfo(packagePath); } catch { return true; } return !string.Equals(state.sourcePath ?? string.Empty, packagePath, StringComparison.OrdinalIgnoreCase) || state.sourceFileLength != info.Length || state.sourceWriteTicksUtc != info.LastWriteTimeUtc.Ticks; } private static string GetInstallDirectory(string packagePath) { if (string.IsNullOrWhiteSpace(packagePath)) { return string.Empty; } string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(packagePath) ?? "package"; string safeName = SanitizeFileName(fileNameWithoutExtension); string hash = ComputeShortHash(packagePath); string installRoot = Path.Combine(Application.persistentDataPath, RuntimeFolderName, InstalledFolderName); return Path.Combine(installRoot, safeName + "_" + hash); } private static void WriteInstallState(string packagePath, string directory) { FileInfo info = new FileInfo(packagePath); InstallState state = new InstallState { sourcePath = packagePath, sourceFileLength = info.Exists ? info.Length : 0L, sourceWriteTicksUtc = info.Exists ? info.LastWriteTimeUtc.Ticks : 0L }; string json = JsonUtility.ToJson(state, true); File.WriteAllText(Path.Combine(directory, StateFileName), json, Encoding.UTF8); } private static InstallState ReadInstallState(string directory) { if (string.IsNullOrWhiteSpace(directory)) { return null; } string statePath = Path.Combine(directory, StateFileName); if (!File.Exists(statePath)) { return null; } try { string json = File.ReadAllText(statePath, Encoding.UTF8); return JsonUtility.FromJson(json); } catch { return null; } } private static string GetPlayerRootDirectory() { try { string dataPath = Application.dataPath; if (string.IsNullOrWhiteSpace(dataPath)) { return string.Empty; } DirectoryInfo parent = Directory.GetParent(dataPath); return parent != null ? parent.FullName : string.Empty; } catch { return string.Empty; } } private static void AppendIfValid(List result, string path) { if (result == null || string.IsNullOrWhiteSpace(path)) { return; } try { string fullPath = Path.GetFullPath(path); if (!result.Contains(fullPath)) { result.Add(fullPath); } } catch { } } private static string ComputeShortHash(string value) { byte[] bytes = Encoding.UTF8.GetBytes(value ?? string.Empty); using (SHA1 sha1 = SHA1.Create()) { byte[] hash = sha1.ComputeHash(bytes); StringBuilder builder = new StringBuilder(8); for (int i = 0; i < 4 && i < hash.Length; i++) { builder.Append(hash[i].ToString("x2")); } return builder.ToString(); } } private static string SanitizeFileName(string value) { if (string.IsNullOrWhiteSpace(value)) { return "package"; } char[] invalidChars = Path.GetInvalidFileNameChars(); StringBuilder builder = new StringBuilder(value.Length); for (int i = 0; i < value.Length; i++) { char current = value[i]; builder.Append(Array.IndexOf(invalidChars, current) >= 0 ? '_' : current); } return builder.ToString().Trim(); } }