#nullable enable using Barotrauma.Extensions; using System; using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Xml.Linq; using Barotrauma.IO; using Barotrauma.Steam; using Microsoft.Xna.Framework; namespace Barotrauma { public static partial class ContentPackageManager { public const string CopyIndicatorFileName = ".copying"; public const string VanillaFileList = "Content/ContentPackages/Vanilla.xml"; public const string CorePackageElementName = "corepackage"; public const string RegularPackagesElementName = "regularpackages"; public const string RegularPackagesSubElementName = "package"; public static bool ModsEnabled => GameMain.VanillaContent == null || EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent); public static class EnabledPackages { public static CorePackage? Core { get; private set; } = null; private static readonly List regular = new List(); public static IReadOnlyList Regular => regular; /// /// Combined hash of the currently enabled packages. Can be used to check if the enabled mods or their load order has changed. /// public static Md5Hash MergedHash { get; private set; } = Md5Hash.Blank; public static IEnumerable All => Core != null ? (Core as ContentPackage).ToEnumerable().CollectionConcat(Regular) : Enumerable.Empty(); private static class BackupPackages { public static CorePackage? Core; public static ImmutableArray? Regular; } public static void SetCore(CorePackage newCore) => SetCoreEnumerable(newCore).Consume(); public static IEnumerable SetCoreEnumerable(CorePackage newCore) { var oldCore = Core; if (newCore == oldCore) { yield break; } if (newCore.FatalLoadErrors.Any()) { yield break; } Core?.UnloadContent(); Core = newCore; foreach (var p in newCore.LoadContentEnumerable()) { yield return p; } SortContent(); yield return LoadProgress.Progress(1.0f); } public static void ReloadCore() { if (Core == null) { return; } ReloadPackage(Core); } public static void ReloadPackage(ContentPackage p) { p.UnloadContent(); p.LoadContent(); SortContent(); } public static void EnableRegular(RegularPackage p) { if (regular.Contains(p)) { return; } var newRegular = regular.ToList(); newRegular.Add(p); SetRegular(newRegular); } public static void SetRegular(IReadOnlyList newRegular) => SetRegularEnumerable(newRegular).Consume(); public static IEnumerable SetRegularEnumerable(IReadOnlyList inNewRegular) { if (ReferenceEquals(inNewRegular, regular)) { yield break; } if (inNewRegular.SequenceEqual(regular)) { yield break; } ThrowIfDuplicates(inNewRegular); var newRegular = inNewRegular // Refuse to enable packages with load errors // so people are forced away from broken mods .Where(r => !r.FatalLoadErrors.Any()) .ToList(); IEnumerable toUnload = regular.Where(r => !newRegular.Contains(r)); RegularPackage[] toLoad = newRegular.Where(r => !regular.Contains(r)).ToArray(); toUnload.ForEach(r => r.UnloadContent()); Range loadingRange = new Range(0.0f, 1.0f); for (int i = 0; i < toLoad.Length; i++) { var package = toLoad[i]; loadingRange = new Range(i / (float)toLoad.Length, (i + 1) / (float)toLoad.Length); foreach (var progress in package.LoadContentEnumerable()) { if (progress.Result.IsFailure) { //If an exception was thrown while loading this package, refuse to add it to the list of enabled packages newRegular.Remove(package); break; } yield return progress.Transform(loadingRange); } } regular.Clear(); regular.AddRange(newRegular); SortContent(); yield return LoadProgress.Progress(1.0f); } public static void ThrowIfDuplicates(IEnumerable pkgs) { var contentPackages = pkgs as IList ?? pkgs.ToArray(); foreach (ContentPackage cp in contentPackages) { if (contentPackages.AtLeast(2, cp2 => cp == cp2)) { throw new InvalidOperationException($"There are duplicates in the list of selected content packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})"); } } } private class TypeComparer : IEqualityComparer { public bool Equals([AllowNull] T x, [AllowNull] T y) { if (x is null || y is null) { return x is null == y is null; } return x.GetType() == y.GetType(); } public int GetHashCode([DisallowNull] T obj) { return obj.GetType().GetHashCode(); } } private static void SortContent() { ThrowIfDuplicates(All); All .SelectMany(r => r.Files) .Distinct(new TypeComparer()) .ForEach(f => f.Sort()); MergedHash = Md5Hash.MergeHashes(All.Select(cp => cp.Hash)); TextManager.IncrementLanguageVersion(); } public static int IndexOf(ContentPackage contentPackage) { if (contentPackage is CorePackage core) { if (core == Core) { return 0; } return -1; } else if (contentPackage is RegularPackage reg) { return Regular.IndexOf(reg) + 1; } return -1; } public static void DisableMods(IReadOnlyCollection mods) { if (Core != null && mods.Contains(Core)) { var newCore = ContentPackageManager.CorePackages.FirstOrDefault(p => !mods.Contains(p)); if (newCore != null) { SetCore(newCore); } } SetRegular(Regular.Where(p => !mods.Contains(p)).ToArray()); } public static void DisableRemovedMods() { if (Core != null && !ContentPackageManager.CorePackages.Contains(Core)) { SetCore(ContentPackageManager.CorePackages.First()); } SetRegular(Regular.Where(p => ContentPackageManager.RegularPackages.Contains(p)).ToArray()); } public static void RefreshUpdatedMods() { if (Core != null && !ContentPackageManager.CorePackages.Contains(Core)) { SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.UgcId == Core.UgcId) ?? ContentPackageManager.CorePackages.First()); } List newRegular = new List(); foreach (var p in Regular) { if (ContentPackageManager.RegularPackages.Contains(p)) { newRegular.Add(p); } else if (ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2 => p2.UgcId == p.UgcId) is { } newP) { newRegular.Add(newP); } } SetRegular(newRegular); } public static void BackUp() { if (BackupPackages.Core != null || BackupPackages.Regular != null) { throw new InvalidOperationException("Tried to back up enabled packages multiple times"); } BackupPackages.Core = Core; BackupPackages.Regular = Regular.ToImmutableArray(); } public static void Restore() { if (BackupPackages.Core == null || BackupPackages.Regular == null) { DebugConsole.AddWarning("Tried to restore enabled packages multiple times/without performing a backup"); return; } SetCore(BackupPackages.Core); SetRegular(BackupPackages.Regular); BackupPackages.Core = null; BackupPackages.Regular = null; } } public sealed partial class PackageSource : ICollection { private readonly Predicate? skipPredicate; private readonly Action? onLoadFail; public PackageSource(string dir, Predicate? skipPredicate, Action? onLoadFail) { this.skipPredicate = skipPredicate; this.onLoadFail = onLoadFail; directory = dir; Directory.CreateDirectory(directory); } public void SwapPackage(ContentPackage oldPackage, ContentPackage newPackage) { bool contains = false; if (oldPackage is CorePackage oldCore && corePackages.Contains(oldCore)) { corePackages.Remove(oldCore); contains = true; } else if (oldPackage is RegularPackage oldRegular && regularPackages.Contains(oldRegular)) { regularPackages.Remove(oldRegular); contains = true; } if (contains) { if (newPackage is CorePackage newCore) { corePackages.Add(newCore); } else if (newPackage is RegularPackage newRegular) { regularPackages.Add(newRegular); } } } public void Refresh() { //remove packages that have been deleted from the directory corePackages.RemoveWhere(p => !File.Exists(p.Path)); regularPackages.RemoveWhere(p => !File.Exists(p.Path)); //load packages that have been added to the directory var subDirs = Directory.GetDirectories(directory); foreach (string subDir in subDirs) { var fileListPath = Path.Combine(subDir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform(); if (this.Any(p => p.Path.Equals(fileListPath, StringComparison.OrdinalIgnoreCase))) { continue; } if (!File.Exists(fileListPath)) { continue; } if (skipPredicate?.Invoke(fileListPath) is true) { continue; } var result = ContentPackage.TryLoad(fileListPath); if (!result.TryUnwrapSuccess(out var newPackage)) { onLoadFail?.Invoke( fileListPath, result.TryUnwrapFailure(out var exception) ? exception : throw new UnreachableCodeException()); continue; } switch (newPackage) { case CorePackage corePackage: corePackages.Add(corePackage); break; case RegularPackage regularPackage: regularPackages.Add(regularPackage); break; } Debug.WriteLine($"Loaded \"{newPackage.Name}\""); } } private readonly string directory; private readonly HashSet regularPackages = new HashSet(); public IEnumerable Regular => regularPackages; private readonly HashSet corePackages = new HashSet(); public IEnumerable Core => corePackages; public IEnumerator GetEnumerator() { foreach (var core in Core) { yield return core; } foreach (var regular in Regular) { yield return regular; } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); void ICollection.Add(ContentPackage item) => throw new InvalidOperationException(); void ICollection.Clear() => throw new InvalidOperationException(); public bool Contains(ContentPackage item) => item switch { CorePackage core => corePackages.Contains(core), RegularPackage regular => this.regularPackages.Contains(regular), _ => throw new ArgumentException($"Expected regular or core package, got {item.GetType().Name}") }; void ICollection.CopyTo(ContentPackage[] array, int arrayIndex) { foreach (var package in corePackages) { array[arrayIndex] = package; arrayIndex++; } foreach (var package in regularPackages) { array[arrayIndex] = package; arrayIndex++; } } bool ICollection.Remove(ContentPackage item) => throw new InvalidOperationException(); public int Count => corePackages.Count + regularPackages.Count; public bool IsReadOnly => true; } public static readonly PackageSource LocalPackages = new PackageSource( ContentPackage.LocalModsDir, skipPredicate: null, onLoadFail: null); public static readonly PackageSource WorkshopPackages = new PackageSource( ContentPackage.WorkshopModsDir, skipPredicate: SteamManager.Workshop.IsInstallingToPath, onLoadFail: (fileListPath, exception) => { // Delete Workshop mods that fail to load to // force a reinstall on next launch if necessary Directory.TryDelete(Path.GetDirectoryName(fileListPath)!); }); public static CorePackage? VanillaCorePackage { get; private set; } = null; public static IEnumerable CorePackages => (VanillaCorePackage is null ? Enumerable.Empty() : VanillaCorePackage.ToEnumerable()) .CollectionConcat(LocalPackages.Core.CollectionConcat(WorkshopPackages.Core)); public static IEnumerable RegularPackages => LocalPackages.Regular.CollectionConcat(WorkshopPackages.Regular); public static IEnumerable AllPackages => VanillaCorePackage.ToEnumerable().CollectionConcat(LocalPackages).CollectionConcat(WorkshopPackages) .OfType(); public static void UpdateContentPackageList() { LocalPackages.Refresh(); WorkshopPackages.Refresh(); EnabledPackages.DisableRemovedMods(); } public static Result ReloadContentPackage(ContentPackage p) { var result = ContentPackage.TryLoad(p.Path); if (result.TryUnwrapSuccess(out var newPackage)) { switch (newPackage) { case CorePackage core: { if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); } break; } case RegularPackage regular: { int index = EnabledPackages.Regular.IndexOf(p); if (index >= 0) { var newRegular = EnabledPackages.Regular.ToArray(); newRegular[index] = regular; EnabledPackages.SetRegular(newRegular); } break; } } LocalPackages.SwapPackage(p, newPackage); WorkshopPackages.SwapPackage(p, newPackage); } EnabledPackages.DisableRemovedMods(); return result; } public readonly record struct LoadProgress(Result Result) { public readonly record struct Error( Either, Exception> ErrorsOrException) { public Error(IEnumerable errorMessages) : this(ErrorsOrException: errorMessages.ToImmutableArray()) { } public Error(Exception exception) : this(ErrorsOrException: exception) { } } public static LoadProgress Failure(Exception exception) => new LoadProgress( Result.Failure(new Error(exception))); public static LoadProgress Failure(IEnumerable errorMessages) => new LoadProgress( Result.Failure(new Error(errorMessages))); public static LoadProgress Progress(float value) => new LoadProgress( Result.Success(value)); public LoadProgress Transform(Range range) => Result.TryUnwrapSuccess(out var value) ? new LoadProgress( Result.Success( MathHelper.Lerp(range.Start, range.End, value))) : this; } public static void LoadVanillaFileList() { VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList); foreach (ContentPackage.LoadError error in VanillaCorePackage.FatalLoadErrors) { DebugConsole.ThrowError(error.ToString()); } } public static IEnumerable Init() { Range loadingRange = new Range(0.0f, 1.0f); SteamManager.Workshop.DeleteFailedCopies(); UpdateContentPackageList(); if (VanillaCorePackage is null) { LoadVanillaFileList(); } SteamManager.Workshop.DeleteUnsubscribedMods(); CorePackage enabledCorePackage = VanillaCorePackage!; List enabledRegularPackages = new List(); #if CLIENT TaskPool.AddWithResult("EnqueueWorkshopUpdates", EnqueueWorkshopUpdates(), t => { }); #else #warning TODO: implement Workshop updates for servers at some point #endif var contentPackagesElement = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath)?.Root ?.GetChildElement("ContentPackages"); if (contentPackagesElement != null) { T? findPackage(IEnumerable packages, XElement? elem) where T : ContentPackage { if (elem is null) { return null; } string name = elem.GetAttributeString("name", ""); string path = elem.GetAttributeStringUnrestricted("path", "").CleanUpPathCrossPlatform(correctFilenameCase: false); return packages.FirstOrDefault(p => p.Path.Equals(path, StringComparison.OrdinalIgnoreCase)) ?? packages.FirstOrDefault(p => p.NameMatches(name)); } var corePackageElement = contentPackagesElement.GetChildElement(CorePackageElementName); if (corePackageElement == null) { DebugConsole.AddWarning($"No core package selected. Switching to the \"{enabledCorePackage.Name}\" package."); } else { var configEnabledCorePackage = findPackage(CorePackages, corePackageElement); if (configEnabledCorePackage == null) { string packageStr = corePackageElement.GetAttributeString("name", null) ?? corePackageElement.GetAttributeStringUnrestricted("path", "UNKNOWN"); DebugConsole.ThrowError($"Could not find the selected core package \"{packageStr}\". Switching to the \"{enabledCorePackage.Name}\" package."); } else { enabledCorePackage = configEnabledCorePackage; } } var regularPackagesElement = contentPackagesElement.GetChildElement(RegularPackagesElementName); if (regularPackagesElement != null) { XElement[] regularPackageElements = regularPackagesElement.GetChildElements(RegularPackagesSubElementName).ToArray(); for (int i = 0; i < regularPackageElements.Length; i++) { var regularPackage = findPackage(RegularPackages, regularPackageElements[i]); if (regularPackage != null) { enabledRegularPackages.Add(regularPackage); } } } } int pkgCount = 1 + enabledRegularPackages.Count; //core + regular loadingRange = new Range(0.01f, 0.01f + (0.99f / pkgCount)); foreach (var p in EnabledPackages.SetCoreEnumerable(enabledCorePackage)) { yield return p.Transform(loadingRange); } loadingRange = new Range(0.01f + (0.99f / pkgCount), 1.0f); foreach (var p in EnabledPackages.SetRegularEnumerable(enabledRegularPackages)) { yield return p.Transform(loadingRange); } yield return LoadProgress.Progress(1.0f); } public static void LogEnabledRegularPackageErrors() { foreach (var p in EnabledPackages.Regular) { p.LogErrors(); } } } }