315 lines
9.5 KiB
C#
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();
|
|
}
|
|
}
|