Files

315 lines
9.5 KiB
C#

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<string> PreparedPackagesThisSession =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetRuntimeState()
{
PreparedPackagesThisSession.Clear();
}
public static void PrepareInstalledPackages()
{
string[] searchRoots = GetPackageSearchRoots();
HashSet<string> visited = new HashSet<string>(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<string> result = new List<string>();
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<InstallState>(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<string> 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();
}
}