5 using System.Collections.Generic;
6 using System.Collections.Immutable;
8 using System.Security.Cryptography;
10 using System.Threading.Tasks;
11 using System.Xml.Linq;
17 public readonly record
struct LoadError(string Message, Exception? Exception)
19 public override string ToString()
21 + (Exception is { StackTrace: var stackTrace }
22 ?
'\n' + stackTrace.CleanupStackTrace()
30 SaveUtil.DefaultSaveFolder,
37 public readonly
string Name;
38 public readonly ImmutableArray<string>
AltNames;
39 public readonly
string Path;
41 public readonly Option<ContentPackageId>
UgcId;
48 public ImmutableArray<ContentFile>
Files {
get;
private set; }
64 public Option<ContentPackageManager.LoadProgress.Error>
EnableError {
get;
private set; }
71 if (!
UgcId.TryUnwrap(out var ugcId)) {
return true; }
73 if (!
InstallTime.TryUnwrap(out var installTime)) {
return true; }
75 Option<Steamworks.Ugc.Item> itemOption = await SteamManager.Workshop.GetItem(steamWorkshopId.Value);
76 if (!itemOption.TryUnwrap(out var item)) {
return true; }
77 return item.LatestUpdateTime <= installTime.ToUtcValue();
80 public int Index => ContentPackageManager.EnabledPackages.IndexOf(
this);
89 using var errorCatcher = DebugConsole.ErrorCatcher.Create();
91 Path = path.CleanUpPathCrossPlatform();
92 XElement rootElement = doc.Root ??
throw new NullReferenceException(
"XML document is invalid: root element is null.");
94 Name = rootElement.GetAttributeString(
"name",
"").Trim();
95 AltNames = rootElement.GetAttributeStringArray(
"altnames", Array.Empty<
string>())
96 .Select(n => n.Trim()).ToImmutableArray();
97 UInt64 steamWorkshopId = rootElement.GetAttributeUInt64(
"steamworkshopid", 0);
104 UgcId = steamWorkshopId != 0
106 : Option<ContentPackageId>.None();
110 InstallTime = rootElement.GetAttributeDateTime(
"installtime");
112 var fileResults = rootElement.Elements()
130 var expectedHash = rootElement.GetAttributeString(
"expectedhash",
"");
135 Message: $
"Hash calculation returned {Hash.StringRepresentation}, expected {expectedHash}",
141 .Concat(errorCatcher.Errors.Select(err =>
new LoadError(err.Text,
null)))
147 !expectedHash.IsNullOrWhiteSpace() &&
154 ? throw new ArgumentException($"Type must be subclass of
ContentFile, got {type.Name}
")
155 : Files.Where(f => f.GetType() == type || f.GetType().IsSubclassOf(type));
157 public bool NameMatches(Identifier name)
158 => Name == name || AltNames.Any(n => n == name);
160 public bool NameMatches(string name)
161 => NameMatches(name.ToIdentifier());
163 public static Result<ContentPackage, Exception> TryLoad(string path)
165 var (success, failure) = Result<ContentPackage, Exception>.GetFactoryMethods();
167 XDocument doc = XMLExtensions.TryLoadXml(path);
171 return success(doc.Root.GetAttributeBool("corepackage
", false)
172 ? new CorePackage(doc, path)
173 : new RegularPackage(doc, path));
177 return failure(e.GetInnermost());
181 public Md5Hash CalculateHash(bool logging = false, string? name = null, string? modVersion = null)
183 using IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.MD5);
187 DebugConsole.NewMessage("****************************** Calculating content
package hash " + Name);
194 var hash = file.
Hash;
197 DebugConsole.NewMessage(
" " + file.
Path +
": " + hash.StringRepresentation);
199 incrementalHash.AppendData(hash.ByteRepresentation);
204 DebugConsole.ThrowError($
"Error while calculating the MD5 hash of the content package \"{Name}\" (file path: {Path}). The content package may be corrupted. You may want to delete or reinstall the package.", e);
209 string selectedName = name ??
Name;
210 if (!selectedName.IsNullOrEmpty())
212 incrementalHash.AppendData(Encoding.UTF8.GetBytes(selectedName));
214 incrementalHash.AppendData(Encoding.UTF8.GetBytes(modVersion ??
ModVersion));
216 var md5Hash = Md5Hash.BytesAsHash(incrementalHash.GetHashAndReset());
219 DebugConsole.NewMessage(
"****************************** Package hash: " + md5Hash.StringRepresentation);
229 FatalLoadErrors = FatalLoadErrors.Add(
new LoadError(errorMsg,
null));
235 Files.Where(f => f is T).ForEach(f => f.LoadFile());
240 Files.Where(f => f is T).ForEach(f => f.UnloadFile());
251 foreach (var p
in LoadContentEnumerable())
253 if (p.Result.IsFailure) {
return LoadResult.Failure; }
260 using var errorCatcher = DebugConsole.ErrorCatcher.Create();
262 ContentFile[] getFilesToLoad(Predicate<ContentFile> predicate)
263 => Files.Where(predicate.Invoke).ToArray()
272 IEnumerable<ContentPackageManager.LoadProgress> loadFiles(
ContentFile[] filesToLoad,
int indexOffset)
274 for (
int i = 0; i < filesToLoad.Length; i++)
276 Exception? exception =
null;
285 var innermost = e.GetInnermost();
286 DebugConsole.LogError($
"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}", contentPackage:
this);
289 if (exception !=
null)
291 yield
return ContentPackageManager.LoadProgress.Failure(exception);
295 if (errorCatcher.Errors.Any())
297 yield
return ContentPackageManager.LoadProgress.Failure(errorCatcher.Errors.Select(e => e.Text));
300 yield
return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (
float)Files.Length);
308 var remainder = getFilesToLoad(f => !priorityFiles.Contains(f));
311 loadFiles(priorityFiles, 0)
312 .Concat(loadFiles(remainder, priorityFiles.Length));
314 foreach (var p
in loadEnumerable)
316 if (p.Result.TryUnwrapFailure(out var failure))
318 errorCatcher.Dispose();
320 EnableError = Option.Some(failure);
326 errorCatcher.Dispose();
331 Files.ForEach(f => f.UnloadFile());
336 XDocument doc = XMLExtensions.TryLoadXml(Path);
337 List<ContentFile> newFileList =
new List<ContentFile>();
338 XElement rootElement = doc.Root ??
throw new NullReferenceException(
"XML document is invalid: root element is null.");
340 var fileResults = rootElement.Elements()
345 foreach (var file
in fileResults.Successes())
349 newFileList.Add(file);
353 var existingFile = Files.FirstOrDefault(f => f.Path == file.Path);
354 newFileList.Add(existingFile ?? file);
358 UnloadFilesOfType<BaseSubFile>();
359 UnloadFilesOfType<ItemAssemblyFile>();
360 Files = newFileList.ToImmutableArray();
361 Hash = CalculateHash();
362 LoadFilesOfType<BaseSubFile>();
363 LoadFilesOfType<ItemAssemblyFile>();
379 if (
string.IsNullOrEmpty(temp)) {
break; }
382 return path == LocalModsDir;
387 if (!FatalLoadErrors.Any())
392 DebugConsole.AddWarning(
393 $
"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" +
394 string.Join(
'\n', FatalLoadErrors.Select(errorToStr)),
397 static string errorToStr(LoadError error)
Base class for content file types, which are loaded from filelist.xml via reflection....
static Result< ContentFile, ContentPackage.LoadError > CreateFromXElement(ContentPackage contentPackage, XElement element)
static bool IsLegacyContentType(XElement contentFileElement, ContentPackage package, bool logWarning)
readonly ContentPath Path
readonly record struct LoadError(string Message, Exception? Exception)
ImmutableArray< LoadError > FatalLoadErrors
Errors that occurred when loading this content package. Currently, all errors are considered fatal an...
ImmutableArray< ContentFile > Files
IEnumerable< ContentPackageManager.LoadProgress > LoadContentEnumerable()
static bool PathAllowedAsLocalModFile(string path)
static readonly Version MinimumHashCompatibleVersion
const string FileListFileName
Md5Hash CalculateHash(bool logging=false, string? name=null, string? modVersion=null)
readonly Version GameVersion
const string DefaultModVersion
Option< ContentPackageManager.LoadProgress.Error > EnableError
An error that occurred when trying to enable this mod. This field doesn't directly affect whether or ...
ContentPackage(XDocument doc, string path)
readonly ImmutableArray< string > AltNames
void AssertCondition(bool condition, string errorMsg)
readonly Option< ContentPackageId > UgcId
void ReloadSubsAndItemAssemblies()
bool HashMismatches(string expectedHash)
readonly Option< SerializableDateTime > InstallTime
static readonly string WorkshopModsDir
readonly string ModVersion
const string LocalModsDir
async Task< bool > IsUpToDate()
bool HasMultiplayerSyncedContent
Does the content package include some content that needs to match between all players in multiplayer.
IEnumerable< ContentFile > GetFiles(Type type)
IEnumerable< T > GetFiles< T >()
static ContentPackage VanillaContent
static readonly Version Version
readonly string StringRepresentation