using System; using System.Collections.Generic; using System.IO; using UnityEngine; public static class DlcManifestService { private static readonly string[] ManifestFilePatterns = { "*.json" }; private const string RuntimeFolderName = "DLC"; private const string ManifestFolderName = "manifests"; public static List LoadInstalledEntries() { DlcPackageArchiveService.PrepareInstalledPackages(); List result = new List(); HashSet visitedFiles = new HashSet(StringComparer.OrdinalIgnoreCase); string[] roots = GetManifestSearchRoots(); for (int i = 0; i < roots.Length; i++) { string root = roots[i]; if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root)) { continue; } CollectManifestEntries(root, result, visitedFiles); } return result; } public static string[] GetManifestSearchRoots() { List result = new List(); AppendIfValid(result, Path.Combine(Application.streamingAssetsPath, RuntimeFolderName, ManifestFolderName)); AppendIfValid(result, Path.Combine(Application.streamingAssetsPath, RuntimeFolderName)); AppendIfValid(result, Path.Combine(Application.persistentDataPath, RuntimeFolderName, ManifestFolderName)); AppendIfValid(result, Path.Combine(Application.persistentDataPath, RuntimeFolderName)); #if !UNITY_ANDROID // 项目外(可执行文件同级目录)的 DLC 扫描仅在 PC 端有意义; // 安卓无此目录概念,Application.dataPath 指向 APK,跳过以避免无效/异常路径。 string playerRoot = GetPlayerRootDirectory(); AppendIfValid(result, Path.Combine(playerRoot, RuntimeFolderName, ManifestFolderName)); AppendIfValid(result, Path.Combine(playerRoot, RuntimeFolderName)); #endif return result.ToArray(); } public static DlcManifestFile ParseManifestJson(string json) { if (string.IsNullOrWhiteSpace(json)) { return null; } try { return JsonUtility.FromJson(json); } catch (Exception ex) { Debug.LogWarning("[DLC] Failed to parse manifest json: " + ex.Message); return null; } } private static void CollectManifestEntries( string root, List result, HashSet visitedFiles) { for (int patternIndex = 0; patternIndex < ManifestFilePatterns.Length; patternIndex++) { string pattern = ManifestFilePatterns[patternIndex]; string[] files; try { files = Directory.GetFiles(root, pattern, SearchOption.AllDirectories); } catch (Exception ex) { Debug.LogWarning("[DLC] Failed to enumerate manifest files in '" + root + "': " + ex.Message); continue; } for (int i = 0; i < files.Length; i++) { string manifestPath = files[i]; if (string.IsNullOrWhiteSpace(manifestPath)) { continue; } string fullPath = Path.GetFullPath(manifestPath); if (!visitedFiles.Add(fullPath)) { continue; } TryLoadManifestFile(fullPath, result); } } } private static void TryLoadManifestFile(string manifestPath, List result) { try { string json = File.ReadAllText(manifestPath); DlcManifestFile file = ParseManifestJson(json); if (file == null || file.entries == null || file.entries.Count == 0) { return; } string manifestDirectory = Path.GetDirectoryName(manifestPath) ?? string.Empty; string manifestId = string.IsNullOrWhiteSpace(file.manifestId) ? Path.GetFileNameWithoutExtension(manifestPath) : file.manifestId.Trim(); for (int i = 0; i < file.entries.Count; i++) { DlcManifestEntry entry = file.entries[i]; if (entry == null || !entry.enabled) { continue; } string resolvedCatalogPath = ResolveCatalogPath(entry.catalogPath, manifestDirectory); if (string.IsNullOrWhiteSpace(resolvedCatalogPath)) { Debug.LogWarning("[DLC] Manifest entry '" + manifestId + "' has no valid catalogPath."); continue; } result.Add(new DlcManifestRuntimeEntry { manifestId = manifestId, manifestSourcePath = manifestPath, manifestDirectory = manifestDirectory, entry = entry, resolvedCatalogPath = resolvedCatalogPath }); } } catch (Exception ex) { Debug.LogWarning("[DLC] Failed to load manifest '" + manifestPath + "': " + ex.Message); } } private static string ResolveCatalogPath(string catalogPath, string manifestDirectory) { if (string.IsNullOrWhiteSpace(catalogPath)) { return string.Empty; } string trimmed = catalogPath.Trim(); if (trimmed.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || trimmed.StartsWith("https://", StringComparison.OrdinalIgnoreCase) || trimmed.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) { return trimmed; } string combined = trimmed; if (!Path.IsPathRooted(combined)) { combined = Path.Combine(manifestDirectory ?? string.Empty, combined); } try { return new Uri(Path.GetFullPath(combined)).AbsoluteUri; } catch { return string.Empty; } } 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; } string fullPath; try { fullPath = Path.GetFullPath(path); } catch { return; } if (!result.Contains(fullPath)) { result.Add(fullPath); } } }