4 using System.Collections;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Diagnostics;
8 using System.Diagnostics.CodeAnalysis;
10 using System.Xml.Linq;
13 using Microsoft.Xna.Framework;
17 public static partial class ContentPackageManager
19 public const string CopyIndicatorFileName =
".copying";
20 public const string VanillaFileList =
"Content/ContentPackages/Vanilla.xml";
22 public const string CorePackageElementName =
"corepackage";
23 public const string RegularPackagesElementName =
"regularpackages";
24 public const string RegularPackagesSubElementName =
"package";
26 public static bool ModsEnabled => GameMain.VanillaContent ==
null || EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent);
28 public static class EnabledPackages
30 public static CorePackage? Core {
get;
private set; } =
null;
32 private static readonly List<RegularPackage> regular =
new List<RegularPackage>();
33 public static IReadOnlyList<RegularPackage> Regular => regular;
38 public static Md5Hash MergedHash {
get;
private set; } = Md5Hash.Blank;
40 public static IEnumerable<ContentPackage>
All =>
42 ? (Core as ContentPackage).ToEnumerable().CollectionConcat(Regular)
43 : Enumerable.Empty<ContentPackage>();
45 public static class BackupPackages
47 public static CorePackage? Core;
48 public static ImmutableArray<RegularPackage>? Regular;
51 public static void SetCore(CorePackage newCore) => SetCoreEnumerable(newCore).Consume();
53 public static IEnumerable<LoadProgress> SetCoreEnumerable(CorePackage newCore)
56 if (newCore == oldCore) { yield
break; }
57 if (newCore.FatalLoadErrors.Any()) { yield
break; }
58 Core?.UnloadContent();
60 foreach (var p
in newCore.LoadContentEnumerable()) { yield
return p; }
62 yield
return LoadProgress.Progress(1.0f);
65 public static void ReloadCore()
67 if (Core ==
null) {
return; }
71 public static void ReloadPackage(ContentPackage p)
78 public static void EnableRegular(RegularPackage p)
80 if (regular.Contains(p)) {
return; }
82 var newRegular = regular.ToList();
84 SetRegular(newRegular);
87 public static void SetRegular(IReadOnlyList<RegularPackage> newRegular)
88 => SetRegularEnumerable(newRegular).Consume();
90 public static IEnumerable<LoadProgress> SetRegularEnumerable(IReadOnlyList<RegularPackage> inNewRegular)
92 if (ReferenceEquals(inNewRegular, regular)) { yield
break; }
93 if (inNewRegular.SequenceEqual(regular)) { yield
break; }
94 ThrowIfDuplicates(inNewRegular);
95 var newRegular = inNewRegular
98 .Where(r => !r.FatalLoadErrors.Any())
100 IEnumerable<RegularPackage> toUnload = regular.Where(r => !newRegular.Contains(r));
101 RegularPackage[] toLoad = newRegular.Where(r => !regular.Contains(r)).ToArray();
102 toUnload.ForEach(r => r.UnloadContent());
104 Range<float> loadingRange =
new Range<float>(0.0f, 1.0f);
106 for (
int i = 0; i < toLoad.Length; i++)
108 var
package = toLoad[i];
109 loadingRange =
new Range<float>(i / (
float)toLoad.Length, (i + 1) / (
float)toLoad.Length);
110 foreach (var progress
in package.LoadContentEnumerable())
112 if (progress.Result.IsFailure)
115 newRegular.Remove(package);
118 yield
return progress.Transform(loadingRange);
121 regular.Clear(); regular.AddRange(newRegular);
123 yield
return LoadProgress.Progress(1.0f);
126 public static void ThrowIfDuplicates(IEnumerable<ContentPackage> pkgs)
128 var contentPackages = pkgs as IList<ContentPackage> ?? pkgs.ToArray();
129 foreach (ContentPackage cp
in contentPackages)
131 if (contentPackages.AtLeast(2, cp2 => cp == cp2))
133 throw new InvalidOperationException($
"There are duplicates in the list of selected content packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none
"})");
138 private class TypeComparer<T> : IEqualityComparer<T>
140 public bool Equals([AllowNull] T x, [AllowNull] T y)
142 if (x is
null || y is
null)
144 return x is
null == y is
null;
146 return x.GetType() == y.GetType();
149 public int GetHashCode([DisallowNull] T obj)
151 return obj.GetType().GetHashCode();
155 private static void SortContent()
157 ThrowIfDuplicates(All);
159 .SelectMany(r => r.Files)
160 .Distinct(
new TypeComparer<ContentFile>())
161 .ForEach(f => f.Sort());
162 MergedHash = Md5Hash.MergeHashes(
All.Select(cp => cp.Hash));
163 TextManager.IncrementLanguageVersion();
166 public static int IndexOf(ContentPackage contentPackage)
168 if (contentPackage is CorePackage core)
170 if (core == Core) {
return 0; }
173 else if (contentPackage is RegularPackage reg)
175 return Regular.IndexOf(reg) + 1;
180 public static void DisableMods(IReadOnlyCollection<ContentPackage> mods)
182 if (Core !=
null && mods.Contains(Core))
184 var newCore = ContentPackageManager.CorePackages.FirstOrDefault(p => !mods.Contains(p));
190 SetRegular(Regular.Where(p => !mods.Contains(p)).ToArray());
193 public static void DisableRemovedMods()
195 if (Core !=
null && !ContentPackageManager.CorePackages.Contains(Core))
197 SetCore(ContentPackageManager.CorePackages.First());
199 SetRegular(Regular.Where(p => ContentPackageManager.RegularPackages.Contains(p)).ToArray());
202 public static void RefreshUpdatedMods()
204 if (Core !=
null && !ContentPackageManager.CorePackages.Contains(Core))
206 SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.UgcId == Core.UgcId) ??
207 ContentPackageManager.CorePackages.First());
210 List<RegularPackage> newRegular =
new List<RegularPackage>();
211 foreach (var p
in Regular)
213 if (ContentPackageManager.RegularPackages.Contains(p))
217 else if (ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2
218 => p2.UgcId == p.UgcId) is { } newP)
220 newRegular.Add(newP);
223 SetRegular(newRegular);
226 public static void BackUp()
228 if (BackupPackages.Core !=
null || BackupPackages.Regular !=
null)
230 throw new InvalidOperationException(
"Tried to back up enabled packages multiple times");
233 BackupPackages.Core = Core;
234 BackupPackages.Regular = Regular.ToImmutableArray();
237 public static void Restore()
239 if (BackupPackages.Core ==
null || BackupPackages.Regular ==
null)
241 DebugConsole.AddWarning(
"Tried to restore enabled packages multiple times/without performing a backup");
245 SetCore(BackupPackages.Core);
246 SetRegular(BackupPackages.Regular);
248 BackupPackages.Core =
null;
249 BackupPackages.Regular =
null;
255 private readonly Predicate<string>? skipPredicate;
256 private readonly Action<string, Exception>? onLoadFail;
258 public PackageSource(
string dir, Predicate<string>? skipPredicate, Action<string, Exception>? onLoadFail)
260 this.skipPredicate = skipPredicate;
261 this.onLoadFail = onLoadFail;
263 Directory.CreateDirectory(directory);
268 bool contains =
false;
269 if (oldPackage is
CorePackage oldCore && corePackages.Contains(oldCore))
271 corePackages.Remove(oldCore);
274 else if (oldPackage is
RegularPackage oldRegular && regularPackages.Contains(oldRegular))
276 regularPackages.Remove(oldRegular);
284 corePackages.Add(newCore);
288 regularPackages.Add(newRegular);
296 corePackages.RemoveWhere(p => !File.Exists(p.Path));
297 regularPackages.RemoveWhere(p => !File.Exists(p.Path));
300 var subDirs = Directory.GetDirectories(directory);
301 foreach (
string subDir
in subDirs)
304 if (this.
Any(p => p.Path.Equals(fileListPath, StringComparison.OrdinalIgnoreCase))) {
continue; }
306 if (!File.Exists(fileListPath)) {
continue; }
307 if (skipPredicate?.Invoke(fileListPath) is
true) {
continue; }
310 if (!result.TryUnwrapSuccess(out var newPackage))
314 result.TryUnwrapFailure(out var exception) ? exception :
throw new UnreachableCodeException());
321 corePackages.Add(corePackage);
324 regularPackages.Add(regularPackage);
328 Debug.WriteLine($
"Loaded \"{newPackage.Name}\"");
332 private readonly
string directory;
333 private readonly HashSet<RegularPackage> regularPackages =
new HashSet<RegularPackage>();
334 public IEnumerable<RegularPackage> Regular => regularPackages;
336 private readonly HashSet<CorePackage> corePackages =
new HashSet<CorePackage>();
337 public IEnumerable<CorePackage> Core => corePackages;
341 foreach (var core
in Core) { yield
return core; }
342 foreach (var regular
in Regular) { yield
return regular; }
345 IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
347 void ICollection<ContentPackage>.Add(
ContentPackage item) =>
throw new InvalidOperationException();
349 void ICollection<ContentPackage>.Clear() =>
throw new InvalidOperationException();
356 _ =>
throw new ArgumentException($
"Expected regular or core package, got {item.GetType().Name}")
359 void ICollection<ContentPackage>.CopyTo(
ContentPackage[] array,
int arrayIndex)
361 foreach (var package
in corePackages)
363 array[arrayIndex] = package;
367 foreach (var package
in regularPackages)
369 array[arrayIndex] = package;
374 bool ICollection<ContentPackage>.Remove(ContentPackage item) =>
throw new InvalidOperationException();
376 public int Count => corePackages.Count + regularPackages.Count;
377 public bool IsReadOnly =>
true;
387 skipPredicate: SteamManager.Workshop.IsInstallingToPath,
388 onLoadFail: (fileListPath, exception) =>
392 Directory.TryDelete(Path.GetDirectoryName(fileListPath)!);
395 public static CorePackage? VanillaCorePackage {
get;
private set; } =
null;
397 public static IEnumerable<CorePackage> CorePackages
398 => (VanillaCorePackage is
null
400 : VanillaCorePackage.ToEnumerable())
401 .CollectionConcat(LocalPackages.Core.CollectionConcat(WorkshopPackages.Core));
403 public static IEnumerable<RegularPackage> RegularPackages
404 => LocalPackages.Regular.CollectionConcat(WorkshopPackages.Regular);
406 public static IEnumerable<ContentPackage> AllPackages
407 => VanillaCorePackage.ToEnumerable().CollectionConcat(LocalPackages).CollectionConcat(WorkshopPackages)
410 public static void UpdateContentPackageList()
412 LocalPackages.Refresh();
413 WorkshopPackages.Refresh();
414 EnabledPackages.DisableRemovedMods();
417 public static Result<ContentPackage, Exception> ReloadContentPackage(ContentPackage p)
419 var result = ContentPackage.
TryLoad(p.Path);
421 if (result.TryUnwrapSuccess(out var newPackage))
425 case CorePackage core:
427 if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); }
431 case RegularPackage regular:
433 int index = EnabledPackages.Regular.IndexOf(p);
436 var newRegular = EnabledPackages.Regular.ToArray();
437 newRegular[index] = regular;
438 EnabledPackages.SetRegular(newRegular);
445 LocalPackages.SwapPackage(p, newPackage);
446 WorkshopPackages.SwapPackage(p, newPackage);
448 EnabledPackages.DisableRemovedMods();
452 public readonly record
struct LoadProgress(Result<float, LoadProgress.
Error> Result)
454 public readonly record
struct Error(
455 Either<ImmutableArray<string>, Exception> ErrorsOrException)
457 public Error(IEnumerable<string> errorMessages) : this(ErrorsOrException: errorMessages.ToImmutableArray()) { }
458 public Error(Exception exception) : this(ErrorsOrException: exception) { }
461 public static LoadProgress Failure(Exception exception)
463 Result<float, Error>.Failure(
new Error(exception)));
465 public static LoadProgress Failure(IEnumerable<string> errorMessages)
467 Result<float, Error>.Failure(
new Error(errorMessages)));
469 public static LoadProgress Progress(
float value)
471 Result<float, Error>.Success(value));
473 public LoadProgress Transform(Range<float> range)
474 => Result.TryUnwrapSuccess(out var value)
476 Result<float, Error>.Success(
477 MathHelper.Lerp(range.Start, range.End, value)))
481 public static void LoadVanillaFileList()
483 VanillaCorePackage =
new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList);
484 foreach (ContentPackage.LoadError error in VanillaCorePackage.FatalLoadErrors)
486 DebugConsole.ThrowError(error.ToString());
490 public static IEnumerable<LoadProgress> Init()
492 Range<float> loadingRange =
new Range<float>(0.0f, 1.0f);
494 SteamManager.Workshop.DeleteFailedCopies();
495 UpdateContentPackageList();
497 if (VanillaCorePackage is
null) { LoadVanillaFileList(); }
499 SteamManager.Workshop.DeleteUnsubscribedMods();
501 CorePackage enabledCorePackage = VanillaCorePackage!;
502 List<RegularPackage> enabledRegularPackages =
new List<RegularPackage>();
505 TaskPool.AddWithResult(
"EnqueueWorkshopUpdates", EnqueueWorkshopUpdates(), t => { });
507 #warning TODO: implement Workshop updates for servers at some point
510 var contentPackagesElement = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath)?.Root
511 ?.GetChildElement(
"ContentPackages");
512 if (contentPackagesElement !=
null)
514 T? findPackage<T>(IEnumerable<T> packages, XElement? elem) where T : ContentPackage
516 if (elem is
null) {
return null; }
517 string name = elem.GetAttributeString(
"name",
"");
518 string path = elem.GetAttributeStringUnrestricted(
"path",
"").CleanUpPathCrossPlatform(correctFilenameCase:
false);
520 packages.FirstOrDefault(p => p.Path.Equals(path, StringComparison.OrdinalIgnoreCase))
521 ?? packages.FirstOrDefault(p => p.NameMatches(name));
524 var corePackageElement = contentPackagesElement.GetChildElement(CorePackageElementName);
525 if (corePackageElement ==
null)
527 DebugConsole.AddWarning($
"No core package selected. Switching to the \"{enabledCorePackage.Name}\" package.");
531 var configEnabledCorePackage = findPackage(CorePackages, corePackageElement);
532 if (configEnabledCorePackage ==
null)
534 string packageStr = corePackageElement.GetAttributeString(
"name",
null) ?? corePackageElement.GetAttributeStringUnrestricted(
"path",
"UNKNOWN");
535 DebugConsole.ThrowError($
"Could not find the selected core package \"{packageStr}\". Switching to the \"{enabledCorePackage.Name}\" package.");
539 enabledCorePackage = configEnabledCorePackage;
543 var regularPackagesElement = contentPackagesElement.GetChildElement(RegularPackagesElementName);
544 if (regularPackagesElement !=
null)
546 XElement[] regularPackageElements = regularPackagesElement.GetChildElements(RegularPackagesSubElementName).ToArray();
547 for (
int i = 0; i < regularPackageElements.Length; i++)
549 var regularPackage = findPackage(RegularPackages, regularPackageElements[i]);
550 if (regularPackage !=
null) { enabledRegularPackages.Add(regularPackage); }
555 int pkgCount = 1 + enabledRegularPackages.Count;
557 loadingRange =
new Range<float>(0.01f, 0.01f + (0.99f / pkgCount));
558 foreach (var p
in EnabledPackages.SetCoreEnumerable(enabledCorePackage))
560 yield
return p.Transform(loadingRange);
563 loadingRange =
new Range<float>(0.01f + (0.99f / pkgCount), 1.0f);
564 foreach (var p
in EnabledPackages.SetRegularEnumerable(enabledRegularPackages))
566 yield
return p.Transform(loadingRange);
569 yield
return LoadProgress.Progress(1.0f);
572 public static void LogEnabledRegularPackageErrors()
574 foreach (var p
in EnabledPackages.Regular)
const string FileListFileName
static readonly string WorkshopModsDir
const string LocalModsDir
static Result< ContentPackage, Exception > TryLoad(string path)
IEnumerator< ContentPackage > GetEnumerator()
PackageSource(string dir, Predicate< string >? skipPredicate, Action< string, Exception >? onLoadFail)
bool Contains(ContentPackage item)
void SwapPackage(ContentPackage oldPackage, ContentPackage newPackage)