Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs
1 #nullable enable
3 using System;
4 using System.Collections;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Diagnostics;
8 using System.Diagnostics.CodeAnalysis;
9 using System.Linq;
10 using System.Xml.Linq;
11 using Barotrauma.IO;
12 using Barotrauma.Steam;
13 using Microsoft.Xna.Framework;
14 
15 namespace Barotrauma
16 {
17  public static partial class ContentPackageManager
18  {
19  public const string CopyIndicatorFileName = ".copying";
20  public const string VanillaFileList = "Content/ContentPackages/Vanilla.xml";
21 
22  public const string CorePackageElementName = "corepackage";
23  public const string RegularPackagesElementName = "regularpackages";
24  public const string RegularPackagesSubElementName = "package";
25 
26  public static bool ModsEnabled => GameMain.VanillaContent == null || EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent);
27 
28  public static class EnabledPackages
29  {
30  public static CorePackage? Core { get; private set; } = null;
31 
32  private static readonly List<RegularPackage> regular = new List<RegularPackage>();
33  public static IReadOnlyList<RegularPackage> Regular => regular;
34 
38  public static Md5Hash MergedHash { get; private set; } = Md5Hash.Blank;
39 
40  public static IEnumerable<ContentPackage> All =>
41  Core != null
42  ? (Core as ContentPackage).ToEnumerable().CollectionConcat(Regular)
43  : Enumerable.Empty<ContentPackage>();
44 
45  public static class BackupPackages
46  {
47  public static CorePackage? Core;
48  public static ImmutableArray<RegularPackage>? Regular;
49  }
50 
51  public static void SetCore(CorePackage newCore) => SetCoreEnumerable(newCore).Consume();
52 
53  public static IEnumerable<LoadProgress> SetCoreEnumerable(CorePackage newCore)
54  {
55  var oldCore = Core;
56  if (newCore == oldCore) { yield break; }
57  if (newCore.FatalLoadErrors.Any()) { yield break; }
58  Core?.UnloadContent();
59  Core = newCore;
60  foreach (var p in newCore.LoadContentEnumerable()) { yield return p; }
61  SortContent();
62  yield return LoadProgress.Progress(1.0f);
63  }
64 
65  public static void ReloadCore()
66  {
67  if (Core == null) { return; }
68  ReloadPackage(Core);
69  }
70 
71  public static void ReloadPackage(ContentPackage p)
72  {
73  p.UnloadContent();
74  p.LoadContent();
75  SortContent();
76  }
77 
78  public static void EnableRegular(RegularPackage p)
79  {
80  if (regular.Contains(p)) { return; }
81 
82  var newRegular = regular.ToList();
83  newRegular.Add(p);
84  SetRegular(newRegular);
85  }
86 
87  public static void SetRegular(IReadOnlyList<RegularPackage> newRegular)
88  => SetRegularEnumerable(newRegular).Consume();
89 
90  public static IEnumerable<LoadProgress> SetRegularEnumerable(IReadOnlyList<RegularPackage> inNewRegular)
91  {
92  if (ReferenceEquals(inNewRegular, regular)) { yield break; }
93  if (inNewRegular.SequenceEqual(regular)) { yield break; }
94  ThrowIfDuplicates(inNewRegular);
95  var newRegular = inNewRegular
96  // Refuse to enable packages with load errors
97  // so people are forced away from broken mods
98  .Where(r => !r.FatalLoadErrors.Any())
99  .ToList();
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());
103 
104  Range<float> loadingRange = new Range<float>(0.0f, 1.0f);
105 
106  for (int i = 0; i < toLoad.Length; i++)
107  {
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())
111  {
112  if (progress.Result.IsFailure)
113  {
114  //If an exception was thrown while loading this package, refuse to add it to the list of enabled packages
115  newRegular.Remove(package);
116  break;
117  }
118  yield return progress.Transform(loadingRange);
119  }
120  }
121  regular.Clear(); regular.AddRange(newRegular);
122  SortContent();
123  yield return LoadProgress.Progress(1.0f);
124  }
125 
126  public static void ThrowIfDuplicates(IEnumerable<ContentPackage> pkgs)
127  {
128  var contentPackages = pkgs as IList<ContentPackage> ?? pkgs.ToArray();
129  foreach (ContentPackage cp in contentPackages)
130  {
131  if (contentPackages.AtLeast(2, cp2 => cp == cp2))
132  {
133  throw new InvalidOperationException($"There are duplicates in the list of selected content packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})");
134  }
135  }
136  }
137 
138  private class TypeComparer<T> : IEqualityComparer<T>
139  {
140  public bool Equals([AllowNull] T x, [AllowNull] T y)
141  {
142  if (x is null || y is null)
143  {
144  return x is null == y is null;
145  }
146  return x.GetType() == y.GetType();
147  }
148 
149  public int GetHashCode([DisallowNull] T obj)
150  {
151  return obj.GetType().GetHashCode();
152  }
153  }
154 
155  private static void SortContent()
156  {
157  ThrowIfDuplicates(All);
158  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();
164  }
165 
166  public static int IndexOf(ContentPackage contentPackage)
167  {
168  if (contentPackage is CorePackage core)
169  {
170  if (core == Core) { return 0; }
171  return -1;
172  }
173  else if (contentPackage is RegularPackage reg)
174  {
175  return Regular.IndexOf(reg) + 1;
176  }
177  return -1;
178  }
179 
180  public static void DisableMods(IReadOnlyCollection<ContentPackage> mods)
181  {
182  if (Core != null && mods.Contains(Core))
183  {
184  var newCore = ContentPackageManager.CorePackages.FirstOrDefault(p => !mods.Contains(p));
185  if (newCore != null)
186  {
187  SetCore(newCore);
188  }
189  }
190  SetRegular(Regular.Where(p => !mods.Contains(p)).ToArray());
191  }
192 
193  public static void DisableRemovedMods()
194  {
195  if (Core != null && !ContentPackageManager.CorePackages.Contains(Core))
196  {
197  SetCore(ContentPackageManager.CorePackages.First());
198  }
199  SetRegular(Regular.Where(p => ContentPackageManager.RegularPackages.Contains(p)).ToArray());
200  }
201 
202  public static void RefreshUpdatedMods()
203  {
204  if (Core != null && !ContentPackageManager.CorePackages.Contains(Core))
205  {
206  SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.UgcId == Core.UgcId) ??
207  ContentPackageManager.CorePackages.First());
208  }
209 
210  List<RegularPackage> newRegular = new List<RegularPackage>();
211  foreach (var p in Regular)
212  {
213  if (ContentPackageManager.RegularPackages.Contains(p))
214  {
215  newRegular.Add(p);
216  }
217  else if (ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2
218  => p2.UgcId == p.UgcId) is { } newP)
219  {
220  newRegular.Add(newP);
221  }
222  }
223  SetRegular(newRegular);
224  }
225 
226  public static void BackUp()
227  {
228  if (BackupPackages.Core != null || BackupPackages.Regular != null)
229  {
230  throw new InvalidOperationException("Tried to back up enabled packages multiple times");
231  }
232 
233  BackupPackages.Core = Core;
234  BackupPackages.Regular = Regular.ToImmutableArray();
235  }
236 
237  public static void Restore()
238  {
239  if (BackupPackages.Core == null || BackupPackages.Regular == null)
240  {
241  DebugConsole.AddWarning("Tried to restore enabled packages multiple times/without performing a backup");
242  return;
243  }
244 
245  SetCore(BackupPackages.Core);
246  SetRegular(BackupPackages.Regular);
247 
248  BackupPackages.Core = null;
249  BackupPackages.Regular = null;
250  }
251  }
252 
253  public sealed partial class PackageSource : ICollection<ContentPackage>
254  {
255  private readonly Predicate<string>? skipPredicate;
256  private readonly Action<string, Exception>? onLoadFail;
257 
258  public PackageSource(string dir, Predicate<string>? skipPredicate, Action<string, Exception>? onLoadFail)
259  {
260  this.skipPredicate = skipPredicate;
261  this.onLoadFail = onLoadFail;
262  directory = dir;
263  Directory.CreateDirectory(directory);
264  }
265 
266  public void SwapPackage(ContentPackage oldPackage, ContentPackage newPackage)
267  {
268  bool contains = false;
269  if (oldPackage is CorePackage oldCore && corePackages.Contains(oldCore))
270  {
271  corePackages.Remove(oldCore);
272  contains = true;
273  }
274  else if (oldPackage is RegularPackage oldRegular && regularPackages.Contains(oldRegular))
275  {
276  regularPackages.Remove(oldRegular);
277  contains = true;
278  }
279 
280  if (contains)
281  {
282  if (newPackage is CorePackage newCore)
283  {
284  corePackages.Add(newCore);
285  }
286  else if (newPackage is RegularPackage newRegular)
287  {
288  regularPackages.Add(newRegular);
289  }
290  }
291  }
292 
293  public void Refresh()
294  {
295  //remove packages that have been deleted from the directory
296  corePackages.RemoveWhere(p => !File.Exists(p.Path));
297  regularPackages.RemoveWhere(p => !File.Exists(p.Path));
298 
299  //load packages that have been added to the directory
300  var subDirs = Directory.GetDirectories(directory);
301  foreach (string subDir in subDirs)
302  {
303  var fileListPath = Path.Combine(subDir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform();
304  if (this.Any(p => p.Path.Equals(fileListPath, StringComparison.OrdinalIgnoreCase))) { continue; }
305 
306  if (!File.Exists(fileListPath)) { continue; }
307  if (skipPredicate?.Invoke(fileListPath) is true) { continue; }
308 
309  var result = ContentPackage.TryLoad(fileListPath);
310  if (!result.TryUnwrapSuccess(out var newPackage))
311  {
312  onLoadFail?.Invoke(
313  fileListPath,
314  result.TryUnwrapFailure(out var exception) ? exception : throw new UnreachableCodeException());
315  continue;
316  }
317 
318  switch (newPackage)
319  {
320  case CorePackage corePackage:
321  corePackages.Add(corePackage);
322  break;
323  case RegularPackage regularPackage:
324  regularPackages.Add(regularPackage);
325  break;
326  }
327 
328  Debug.WriteLine($"Loaded \"{newPackage.Name}\"");
329  }
330  }
331 
332  private readonly string directory;
333  private readonly HashSet<RegularPackage> regularPackages = new HashSet<RegularPackage>();
334  public IEnumerable<RegularPackage> Regular => regularPackages;
335 
336  private readonly HashSet<CorePackage> corePackages = new HashSet<CorePackage>();
337  public IEnumerable<CorePackage> Core => corePackages;
338 
339  public IEnumerator<ContentPackage> GetEnumerator()
340  {
341  foreach (var core in Core) { yield return core; }
342  foreach (var regular in Regular) { yield return regular; }
343  }
344 
345  IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
346 
347  void ICollection<ContentPackage>.Add(ContentPackage item) => throw new InvalidOperationException();
348 
349  void ICollection<ContentPackage>.Clear() => throw new InvalidOperationException();
350 
351  public bool Contains(ContentPackage item)
352  => item switch
353  {
354  CorePackage core => corePackages.Contains(core),
355  RegularPackage regular => this.regularPackages.Contains(regular),
356  _ => throw new ArgumentException($"Expected regular or core package, got {item.GetType().Name}")
357  };
358 
359  void ICollection<ContentPackage>.CopyTo(ContentPackage[] array, int arrayIndex)
360  {
361  foreach (var package in corePackages)
362  {
363  array[arrayIndex] = package;
364  arrayIndex++;
365  }
366 
367  foreach (var package in regularPackages)
368  {
369  array[arrayIndex] = package;
370  arrayIndex++;
371  }
372  }
373 
374  bool ICollection<ContentPackage>.Remove(ContentPackage item) => throw new InvalidOperationException();
375 
376  public int Count => corePackages.Count + regularPackages.Count;
377  public bool IsReadOnly => true;
378  }
379 
380  public static readonly PackageSource LocalPackages
381  = new PackageSource(
383  skipPredicate: null,
384  onLoadFail: null);
385  public static readonly PackageSource WorkshopPackages = new PackageSource(
387  skipPredicate: SteamManager.Workshop.IsInstallingToPath,
388  onLoadFail: (fileListPath, exception) =>
389  {
390  // Delete Workshop mods that fail to load to
391  // force a reinstall on next launch if necessary
392  Directory.TryDelete(Path.GetDirectoryName(fileListPath)!);
393  });
394 
395  public static CorePackage? VanillaCorePackage { get; private set; } = null;
396 
397  public static IEnumerable<CorePackage> CorePackages
398  => (VanillaCorePackage is null
399  ? Enumerable.Empty<CorePackage>()
400  : VanillaCorePackage.ToEnumerable())
401  .CollectionConcat(LocalPackages.Core.CollectionConcat(WorkshopPackages.Core));
402 
403  public static IEnumerable<RegularPackage> RegularPackages
404  => LocalPackages.Regular.CollectionConcat(WorkshopPackages.Regular);
405 
406  public static IEnumerable<ContentPackage> AllPackages
407  => VanillaCorePackage.ToEnumerable().CollectionConcat(LocalPackages).CollectionConcat(WorkshopPackages)
408  .OfType<ContentPackage>();
409 
410  public static void UpdateContentPackageList()
411  {
412  LocalPackages.Refresh();
413  WorkshopPackages.Refresh();
414  EnabledPackages.DisableRemovedMods();
415  }
416 
417  public static Result<ContentPackage, Exception> ReloadContentPackage(ContentPackage p)
418  {
419  var result = ContentPackage.TryLoad(p.Path);
420 
421  if (result.TryUnwrapSuccess(out var newPackage))
422  {
423  switch (newPackage)
424  {
425  case CorePackage core:
426  {
427  if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); }
428 
429  break;
430  }
431  case RegularPackage regular:
432  {
433  int index = EnabledPackages.Regular.IndexOf(p);
434  if (index >= 0)
435  {
436  var newRegular = EnabledPackages.Regular.ToArray();
437  newRegular[index] = regular;
438  EnabledPackages.SetRegular(newRegular);
439  }
440 
441  break;
442  }
443  }
444 
445  LocalPackages.SwapPackage(p, newPackage);
446  WorkshopPackages.SwapPackage(p, newPackage);
447  }
448  EnabledPackages.DisableRemovedMods();
449  return result;
450  }
451 
452  public readonly record struct LoadProgress(Result<float, LoadProgress.Error> Result)
453  {
454  public readonly record struct Error(
455  Either<ImmutableArray<string>, Exception> ErrorsOrException)
456  {
457  public Error(IEnumerable<string> errorMessages) : this(ErrorsOrException: errorMessages.ToImmutableArray()) { }
458  public Error(Exception exception) : this(ErrorsOrException: exception) { }
459  }
460 
461  public static LoadProgress Failure(Exception exception)
462  => new LoadProgress(
463  Result<float, Error>.Failure(new Error(exception)));
464 
465  public static LoadProgress Failure(IEnumerable<string> errorMessages)
466  => new LoadProgress(
467  Result<float, Error>.Failure(new Error(errorMessages)));
468 
469  public static LoadProgress Progress(float value)
470  => new LoadProgress(
471  Result<float, Error>.Success(value));
472 
473  public LoadProgress Transform(Range<float> range)
474  => Result.TryUnwrapSuccess(out var value)
475  ? new LoadProgress(
476  Result<float, Error>.Success(
477  MathHelper.Lerp(range.Start, range.End, value)))
478  : this;
479  }
480 
481  public static void LoadVanillaFileList()
482  {
483  VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList);
484  foreach (ContentPackage.LoadError error in VanillaCorePackage.FatalLoadErrors)
485  {
486  DebugConsole.ThrowError(error.ToString());
487  }
488  }
489 
490  public static IEnumerable<LoadProgress> Init()
491  {
492  Range<float> loadingRange = new Range<float>(0.0f, 1.0f);
493 
494  SteamManager.Workshop.DeleteFailedCopies();
495  UpdateContentPackageList();
496 
497  if (VanillaCorePackage is null) { LoadVanillaFileList(); }
498 
499  SteamManager.Workshop.DeleteUnsubscribedMods();
500 
501  CorePackage enabledCorePackage = VanillaCorePackage!;
502  List<RegularPackage> enabledRegularPackages = new List<RegularPackage>();
503 
504 #if CLIENT
505  TaskPool.AddWithResult("EnqueueWorkshopUpdates", EnqueueWorkshopUpdates(), t => { });
506 #else
507  #warning TODO: implement Workshop updates for servers at some point
508 #endif
509 
510  var contentPackagesElement = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath)?.Root
511  ?.GetChildElement("ContentPackages");
512  if (contentPackagesElement != null)
513  {
514  T? findPackage<T>(IEnumerable<T> packages, XElement? elem) where T : ContentPackage
515  {
516  if (elem is null) { return null; }
517  string name = elem.GetAttributeString("name", "");
518  string path = elem.GetAttributeStringUnrestricted("path", "").CleanUpPathCrossPlatform(correctFilenameCase: false);
519  return
520  packages.FirstOrDefault(p => p.Path.Equals(path, StringComparison.OrdinalIgnoreCase))
521  ?? packages.FirstOrDefault(p => p.NameMatches(name));
522  }
523 
524  var corePackageElement = contentPackagesElement.GetChildElement(CorePackageElementName);
525  if (corePackageElement == null)
526  {
527  DebugConsole.AddWarning($"No core package selected. Switching to the \"{enabledCorePackage.Name}\" package.");
528  }
529  else
530  {
531  var configEnabledCorePackage = findPackage(CorePackages, corePackageElement);
532  if (configEnabledCorePackage == null)
533  {
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.");
536  }
537  else
538  {
539  enabledCorePackage = configEnabledCorePackage;
540  }
541  }
542 
543  var regularPackagesElement = contentPackagesElement.GetChildElement(RegularPackagesElementName);
544  if (regularPackagesElement != null)
545  {
546  XElement[] regularPackageElements = regularPackagesElement.GetChildElements(RegularPackagesSubElementName).ToArray();
547  for (int i = 0; i < regularPackageElements.Length; i++)
548  {
549  var regularPackage = findPackage(RegularPackages, regularPackageElements[i]);
550  if (regularPackage != null) { enabledRegularPackages.Add(regularPackage); }
551  }
552  }
553  }
554 
555  int pkgCount = 1 + enabledRegularPackages.Count; //core + regular
556 
557  loadingRange = new Range<float>(0.01f, 0.01f + (0.99f / pkgCount));
558  foreach (var p in EnabledPackages.SetCoreEnumerable(enabledCorePackage))
559  {
560  yield return p.Transform(loadingRange);
561  }
562 
563  loadingRange = new Range<float>(0.01f + (0.99f / pkgCount), 1.0f);
564  foreach (var p in EnabledPackages.SetRegularEnumerable(enabledRegularPackages))
565  {
566  yield return p.Transform(loadingRange);
567  }
568 
569  yield return LoadProgress.Progress(1.0f);
570  }
571 
572  public static void LogEnabledRegularPackageErrors()
573  {
574  foreach (var p in EnabledPackages.Regular)
575  {
576  p.LogErrors();
577  }
578  }
579  }
580 }
static readonly string WorkshopModsDir
static Result< ContentPackage, Exception > TryLoad(string path)
PackageSource(string dir, Predicate< string >? skipPredicate, Action< string, Exception >? onLoadFail)