private void LoadModsFromDirectories(params string[] modDirectories) { var stopWatch = Stopwatch.StartNew(); // Look for mods, load their manifests var allMods = new List <string>(); foreach (var modDirectory in modDirectories) { if (!modDirectory.IsNullOrWhiteSpace() && Directory.Exists(modDirectory)) { var prevCount = allMods.Count; allMods.AddRange(Directory.GetFiles(modDirectory, "*", SearchOption.AllDirectories) .Where(x => x.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) || x.EndsWith(".zipmod", StringComparison.OrdinalIgnoreCase))); Logger.LogInfo("Found " + (allMods.Count - prevCount) + " zipmods in directory: " + modDirectory); } } var archives = allMods.RunParallel(archivePath => { ZipFile archive = null; try { archive = new ZipFile(archivePath); if (Manifest.TryLoadFromZip(archive, out Manifest manifest)) { //Skip the mod if it is not for this game if (!manifest.Game.IsNullOrWhiteSpace() && !GameNameList.Contains(manifest.Game.ToLower().Replace("!", ""))) { Logger.LogInfo($"Skipping archive \"{GetRelativeArchiveDir(archivePath)}\" because it's meant for {manifest.Game}"); return(null); } return(new { archive, manifest }); } } catch (Exception ex) { Logger.LogError($"Failed to load archive \"{GetRelativeArchiveDir(archivePath)}\" with error: {ex}"); archive?.Close(); } return(null); }, 3).Where(x => x != null).ToList(); var enableModLoadingLogging = DebugLoggingModLoading.Value; var modLoadInfoSb = enableModLoadingLogging ? new StringBuilder(1000) : null; // Handle duplicate GUIDs and load unique mods foreach (var modGroup in archives.GroupBy(x => x.manifest.GUID).OrderBy(x => x.Key)) { // Order by version if available, else use modified dates (less reliable) // If versions match, prefer mods inside folders or with more descriptive names so modpacks are preferred var orderedModsQuery = modGroup.All(x => !string.IsNullOrEmpty(x.manifest.Version)) ? modGroup.OrderByDescending(x => x.manifest.Version, new ManifestVersionComparer()).ThenByDescending(x => x.archive.Name.Length) : modGroup.OrderByDescending(x => File.GetLastWriteTime(x.archive.Name)); var orderedMods = orderedModsQuery.ToList(); if (orderedMods.Count > 1) { var modList = string.Join(", ", orderedMods.Skip(1).Select(x => '"' + GetRelativeArchiveDir(x.archive.Name) + '"').ToArray()); Logger.LogWarning($"Multiple versions detected, only \"{GetRelativeArchiveDir(orderedMods[0].archive.Name)}\" will be loaded. Skipped versions: {modList}"); // Don't keep the duplicate archives in memory foreach (var dupeMod in orderedMods.Skip(1)) { dupeMod.archive.Close(); } } // Actually load the mods (only one per GUID, the newest one) var archive = orderedMods[0].archive; var manifest = orderedMods[0].manifest; try { Archives.Add(archive); ZipArchives[manifest.GUID] = archive.Name; Manifests[manifest.GUID] = manifest; LoadAllUnityArchives(archive, archive.Name); LoadAllLists(archive, manifest); BuildPngFolderList(archive); UniversalAutoResolver.GenerateMigrationInfo(manifest, _gatheredMigrationInfos); #if AI || HS2 UniversalAutoResolver.GenerateHeadPresetInfo(manifest, _gatheredHeadPresetInfos); UniversalAutoResolver.GenerateFaceSkinInfo(manifest, _gatheredFaceSkinInfos); #endif var trimmedName = manifest.Name?.Trim(); var displayName = !string.IsNullOrEmpty(trimmedName) ? trimmedName : Path.GetFileName(archive.Name); if (enableModLoadingLogging) { modLoadInfoSb.AppendLine($"Loaded {displayName} {manifest.Version}"); } } catch (Exception ex) { Logger.LogError($"Failed to load archive \"{GetRelativeArchiveDir(archive.Name)}\" with error: {ex}"); } } UniversalAutoResolver.SetResolveInfos(_gatheredResolutionInfos); UniversalAutoResolver.SetMigrationInfos(_gatheredMigrationInfos); #if AI || HS2 UniversalAutoResolver.SetHeadPresetInfos(_gatheredHeadPresetInfos); UniversalAutoResolver.SetFaceSkinInfos(_gatheredFaceSkinInfos); UniversalAutoResolver.ResolveFaceSkins(); #endif BuildPngOnlyFolderList(); #pragma warning disable CS0618 // Type or member is obsolete LoadedManifests = Manifests.Values.AsEnumerable().ToList(); #pragma warning restore CS0618 // Type or member is obsolete stopWatch.Stop(); if (enableModLoadingLogging) { Logger.LogInfo($"List of loaded mods:\n{modLoadInfoSb}"); } Logger.LogInfo($"Successfully loaded {Archives.Count} mods out of {allMods.Count} archives in {stopWatch.ElapsedMilliseconds}ms"); var failedPaths = allMods.Except(Archives.Select(x => x.Name)); var failedStrings = failedPaths.Select(GetRelativeArchiveDir).ToArray(); if (failedStrings.Length > 0) { Logger.LogWarning("Could not load " + failedStrings.Length + " mods, see previous warnings for more information. File names of skipped archives:\n" + string.Join(" | ", failedStrings)); } }
private void LoadModsFromDirectories(params string[] modDirectories) { Logger.LogInfo("Scanning the \"mods\" directory..."); var stopWatch = Stopwatch.StartNew(); // Look for mods, load their manifests var allMods = new List <string>(); foreach (var modDirectory in modDirectories) { if (!modDirectory.IsNullOrWhiteSpace() && Directory.Exists(modDirectory)) { allMods.AddRange(GetZipmodsFromDirectory(modDirectory)); } } var archives = new Dictionary <ZipFile, Manifest>(); foreach (var archivePath in allMods) { ZipFile archive = null; try { archive = new ZipFile(archivePath); if (Manifest.TryLoadFromZip(archive, out Manifest manifest)) { if (manifest.Game.IsNullOrWhiteSpace() || GameNameList.Contains(manifest.Game.ToLower().Replace("!", ""))) { archives.Add(archive, manifest); } else { Logger.LogInfo($"Skipping archive \"{GetRelativeArchiveDir(archivePath)}\" because it's meant for {manifest.Game}"); } } } catch (Exception ex) { Logger.LogError($"Failed to load archive \"{GetRelativeArchiveDir(archivePath)}\" with error: {ex}"); archive?.Close(); } } var modLoadInfoSb = new StringBuilder(); // Handle duplicate GUIDs and load unique mods foreach (var modGroup in archives.GroupBy(x => x.Value.GUID)) { // Order by version if available, else use modified dates (less reliable) // If versions match, prefer mods inside folders or with more descriptive names so modpacks are preferred var orderedModsQuery = modGroup.All(x => !string.IsNullOrEmpty(x.Value.Version)) ? modGroup.OrderByDescending(x => x.Value.Version, new ManifestVersionComparer()).ThenByDescending(x => x.Key.Name.Length) : modGroup.OrderByDescending(x => File.GetLastWriteTime(x.Key.Name)); var orderedMods = orderedModsQuery.ToList(); if (orderedMods.Count > 1) { var modList = string.Join(", ", orderedMods.Select(x => '"' + GetRelativeArchiveDir(x.Key.Name) + '"').ToArray()); Logger.LogWarning($"Archives with identical GUIDs detected! Archives: {modList}; Only \"{GetRelativeArchiveDir(orderedMods[0].Key.Name)}\" will be loaded because it's the newest"); // Don't keep the duplicate archives in memory foreach (var dupeMod in orderedMods.Skip(1)) { dupeMod.Key.Close(); } } // Actually load the mods (only one per GUID, the newest one) var archive = orderedMods[0].Key; var manifest = orderedMods[0].Value; try { Archives.Add(archive); Manifests[manifest.GUID] = manifest; LoadAllUnityArchives(archive, archive.Name); LoadAllLists(archive, manifest); BuildPngFolderList(archive); UniversalAutoResolver.GenerateMigrationInfo(manifest, _gatheredMigrationInfos); #if AI UniversalAutoResolver.GenerateHeadPresetInfo(manifest, _gatheredHeadPresetInfos); UniversalAutoResolver.GenerateFaceSkinInfo(manifest, _gatheredFaceSkinInfos); #endif var trimmedName = manifest.Name?.Trim(); var displayName = !string.IsNullOrEmpty(trimmedName) ? trimmedName : Path.GetFileName(archive.Name); modLoadInfoSb.AppendLine($"Loaded {displayName} {manifest.Version}"); } catch (Exception ex) { Logger.LogError($"Failed to load archive \"{GetRelativeArchiveDir(archive.Name)}\" with error: {ex}"); } } UniversalAutoResolver.SetResolveInfos(_gatheredResolutionInfos); UniversalAutoResolver.SetMigrationInfos(_gatheredMigrationInfos); #if AI UniversalAutoResolver.SetHeadPresetInfos(_gatheredHeadPresetInfos); UniversalAutoResolver.SetFaceSkinInfos(_gatheredFaceSkinInfos); UniversalAutoResolver.ResolveFaceSkins(); #endif BuildPngOnlyFolderList(); #pragma warning disable CS0618 // Type or member is obsolete LoadedManifests = Manifests.Values.AsEnumerable().ToList(); #pragma warning restore CS0618 // Type or member is obsolete stopWatch.Stop(); if (ModLoadingLogging.Value) { Logger.LogInfo($"List of loaded mods:\n{modLoadInfoSb}"); } Logger.LogInfo($"Successfully loaded {Archives.Count} mods out of {allMods.Count()} archives in {stopWatch.ElapsedMilliseconds}ms"); }