diff --git a/src/IronyModManager.IO/Mods/ModWriter.cs b/src/IronyModManager.IO/Mods/ModWriter.cs index 15cb4972..38497638 100644 --- a/src/IronyModManager.IO/Mods/ModWriter.cs +++ b/src/IronyModManager.IO/Mods/ModWriter.cs @@ -1,11 +1,10 @@ - -// *********************************************************************** +// *********************************************************************** // Assembly : IronyModManager.IO // Author : Mario // Created : 03-31-2020 // // Last Modified By : Mario -// Last Modified On : 01-23-2024 +// Last Modified On : 12-23-2024 // *********************************************************************** // // Mario @@ -31,7 +30,6 @@ namespace IronyModManager.IO.Mods { - /// /// Class ModWriter. /// Implements the @@ -113,16 +111,17 @@ public async Task ApplyModsAsync(ModWriterParameters parameters) { throw new ArgumentException("Invalid descriptor type."); } + Task[] tasks; - using (var mutex = await writeLock.LockAsync()) + using (await writeLock.LockAsync()) { - tasks = new Task[] - { - Task.Run(async() => await sqliteExporter.ExportAsync(parameters)), - Task.Run(async() => await sqliteExporterBeta.ExportAsync(parameters)), - Task.Run(async() => await jsonExporter.ExportModsAsync(parameters)) - }; + tasks = + [ + Task.Run(async () => await sqliteExporter.ExportAsync(parameters)), + Task.Run(async () => await sqliteExporterBeta.ExportAsync(parameters)), + Task.Run(async () => await jsonExporter.ExportModsAsync(parameters)) + ]; await Task.WhenAll(tasks); } @@ -158,6 +157,7 @@ public Task CanWriteToModDirectoryAsync(ModWriterParameters parameters) // Real genious you picked C, D or whatever drive return Task.FromResult(false); } + var root = Path.Combine(el[0], el[1]); if (!Directory.Exists(root) && !IsAdmin()) { @@ -177,12 +177,15 @@ public Task CanWriteToModDirectoryAsync(ModWriterParameters parameters) { return Task.FromResult(false); } + return Task.FromResult(true); } } } + return Task.FromResult(true); } + return Task.FromResult(false); } @@ -199,6 +202,7 @@ public Task CreateModDirectoryAsync(ModWriterParameters parameters) Directory.CreateDirectory(fullPath); return Task.FromResult(true); } + return Task.FromResult(false); } @@ -217,8 +221,10 @@ Task delete() DiskOperations.DeleteFile(fullPath); return Task.FromResult(true); } + return Task.FromResult(false); } + var retry = new RetryStrategy(); return retry.RetryActionAsync(() => delete()); } @@ -235,6 +241,7 @@ public Task DescriptorExistsAsync(ModWriterParameters parameters) { return Task.FromResult(true); } + return Task.FromResult(false); } @@ -250,6 +257,7 @@ public string FormatPrefixModName(string prefix, string name) { return $"{prefix}{name}"; } + return name; } @@ -265,6 +273,7 @@ public virtual bool ModDirectoryExists(ModWriterParameters parameters) { return false; } + return Directory.EnumerateFiles(fullPath, "*", SearchOption.AllDirectories).Any(); } @@ -303,21 +312,25 @@ Task purge() { DiskOperations.DeleteDirectory(fullPath, true); } + return Task.FromResult(true); } else if (File.Exists(fullPath)) { DiskOperations.DeleteFile(fullPath); var directory = Path.GetDirectoryName(fullPath); - var files = Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories); + var files = Directory.EnumerateFiles(directory!, "*", SearchOption.AllDirectories); if (!files.Any()) { DiskOperations.DeleteDirectory(directory, true); } + return Task.FromResult(true); } + return Task.FromResult(false); } + var retry = new RetryStrategy(); return retry.RetryActionAsync(() => purge()); } @@ -333,12 +346,10 @@ public Task SetDescriptorLockAsync(ModWriterParameters parameters, bool is var fullPath = Path.Combine(parameters.RootDirectory ?? string.Empty, parameters.Mod.DescriptorFile ?? string.Empty); if (File.Exists(fullPath)) { - _ = new System.IO.FileInfo(fullPath) - { - IsReadOnly = isLocked - }; + _ = new System.IO.FileInfo(fullPath) { IsReadOnly = isLocked }; return Task.FromResult(true); } + return Task.FromResult(false); } @@ -356,6 +367,7 @@ public async Task WriteDescriptorAsync(ModWriterParameters parameters, boo { throw new ArgumentException("Invalid descriptor type."); } + async Task writeDescriptors() { // If needed I've got a much more complex serializer, it is written for Kerbal Space Program but the structure seems to be the same though this is much more simpler @@ -367,12 +379,10 @@ async Task writeDescriptors() { if (File.Exists(fullPath)) { - _ = new System.IO.FileInfo(fullPath) - { - IsReadOnly = true - }; + _ = new System.IO.FileInfo(fullPath) { IsReadOnly = true }; } } + if (writeDescriptorInModDirectory) { var modPath = Path.Combine(parameters.Mod.FileName ?? string.Empty, parameters.DescriptorType == DescriptorType.DescriptorMod ? Shared.Constants.DescriptorFile : Shared.Constants.DescriptorJsonMetadata); @@ -381,13 +391,17 @@ async Task writeDescriptors() var dir = Path.GetDirectoryName(modPath); if (!Directory.Exists(dir)) { - Directory.CreateDirectory(dir); + Directory.CreateDirectory(dir!); } } + await writeDescriptor(modPath, true); } + return true; } + + // ReSharper disable once UnusedLocalFunctionReturnValue async Task writeDescriptor(string fullPath, bool truncatePath) { bool? state = null; @@ -397,15 +411,14 @@ async Task writeDescriptor(string fullPath, bool truncatePath) state = fileInfo.IsReadOnly; fileInfo.IsReadOnly = false; } - using var fs = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.Read); + + await using var fs = new FileStream(fullPath!, FileMode.Create, FileAccess.Write, FileShare.Read); var result = await WriteDescriptorToStreamAsync(parameters, fs, truncatePath); if (state.HasValue) { - var fileInfo = new System.IO.FileInfo(fullPath) - { - IsReadOnly = state.GetValueOrDefault() - }; + _ = new System.IO.FileInfo(fullPath) { IsReadOnly = state.GetValueOrDefault() }; } + return result; } @@ -428,6 +441,7 @@ public Task WriteDescriptorToStreamAsync(ModWriterParameters parameters, S { throw new ArgumentException("Invalid descriptor type."); } + return WriteDescriptorToStreamInternalAsync(parameters.Mod, stream, parameters.DescriptorType, truncatePath); } @@ -452,7 +466,7 @@ static async Task serializeDescriptorMod(IMod content, StreamWriter sw) { if (col.Any()) { - if (attr.KeyedArray) + if (attr!.KeyedArray) { foreach (var item in col) { @@ -466,6 +480,7 @@ static async Task serializeDescriptorMod(IMod content, StreamWriter sw) { await sw.WriteLineAsync($"\t\"{item.Replace("\"", "\\\"")}\""); } + await sw.WriteLineAsync("}"); } } @@ -474,18 +489,19 @@ static async Task serializeDescriptorMod(IMod content, StreamWriter sw) { if (!string.IsNullOrWhiteSpace(val != null ? val.ToString() : string.Empty)) { - if (attr.AlternateNameEndsWithCondition?.Count() > 0 && attr.AlternateNameEndsWithCondition.Any(p => val.ToString().EndsWith(p, StringComparison.OrdinalIgnoreCase))) + if (attr!.AlternateNameEndsWithCondition?.Count() > 0 && attr.AlternateNameEndsWithCondition.Any(p => val!.ToString()!.EndsWith(p, StringComparison.OrdinalIgnoreCase))) { - await sw.WriteLineAsync($"{attr.AlternatePropertyName}=\"{val.ToString().Replace("\"", "\\\"")}\""); + await sw.WriteLineAsync($"{attr.AlternatePropertyName}=\"{val?.ToString()?.Replace("\"", "\\\"")}\""); } else { - await sw.WriteLineAsync($"{attr.PropertyName}=\"{val.ToString().Replace("\"", "\\\"")}\""); + await sw.WriteLineAsync($"{attr.PropertyName}=\"{val?.ToString()?.Replace("\"", "\\\"")}\""); } } } } } + static async Task serializeJsonDescriptorMod(IMod content, StreamWriter sw) { var customData = content.AdditionalData != null ? new Dictionary(content.AdditionalData) : new Dictionary(); @@ -493,17 +509,24 @@ static async Task serializeJsonDescriptorMod(IMod content, StreamWriter sw) { customData[Shared.Constants.JsonMetadataReplacePaths] = content.ReplacePath; } + if (content.UserDir != null && content.UserDir.Any()) { customData[Shared.Constants.DescriptorUserDir] = content.UserDir; } - var metaData = new JsonMetadata() + var relationshipData = new List>(); + if (content.RelationshipData != null) + { + relationshipData.AddRange(content.RelationshipData.Select(relData => new Dictionary(relData))); + } + + var metaData = new JsonMetadata { - Id = content.RemoteId.HasValue ? content.RemoteId.ToString() : string.Empty, + Id = !string.IsNullOrWhiteSpace(content.JsonId) ? content.JsonId : content.RemoteId.HasValue ? content.RemoteId.ToString() : string.Empty, Name = content.Name, Path = content.FileName, - Relationships = content.Dependencies != null ? content.Dependencies.ToList() : new List(), + Relationships = relationshipData, SupportedGameVersion = content.Version, Tags = content.Tags != null ? content.Tags.ToList() : new List(), ShortDescription = string.Empty, @@ -519,7 +542,8 @@ static async Task serializeJsonDescriptorMod(IMod content, StreamWriter sw) mod = mapper.Map(mod); mod.FileName = descriptorType == DescriptorType.JsonMetadata ? null : string.Empty; } - using var sw = new StreamWriter(stream, leaveOpen: true); + + await using var sw = new StreamWriter(stream, leaveOpen: true); if (descriptorType == DescriptorType.DescriptorMod) { await serializeDescriptorMod(mod, sw); @@ -528,6 +552,7 @@ static async Task serializeJsonDescriptorMod(IMod content, StreamWriter sw) { await serializeJsonDescriptorMod(mod, sw); } + await sw.FlushAsync(); return true; } @@ -545,6 +570,7 @@ private bool IsAdmin() using var identity = WindowsIdentity.GetCurrent(); return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); } + return false; } catch @@ -597,7 +623,7 @@ private class JsonMetadata /// /// The relationships. [JsonProperty("relationships", NullValueHandling = NullValueHandling.Include)] - public List Relationships { get; set; } + public List> Relationships { get; set; } /// /// Gets or sets the short description. diff --git a/src/IronyModManager.Models/Mod.cs b/src/IronyModManager.Models/Mod.cs index 2bc8b701..853b9188 100644 --- a/src/IronyModManager.Models/Mod.cs +++ b/src/IronyModManager.Models/Mod.cs @@ -4,16 +4,18 @@ // Created : 02-29-2020 // // Last Modified By : Mario -// Last Modified On : 11-30-2022 +// Last Modified On : 12-23-2024 // *********************************************************************** // // Mario // // // *********************************************************************** + using System; using System.Collections.Generic; using System.IO; +using System.Linq; using IronyModManager.Models.Common; using IronyModManager.Shared; @@ -112,6 +114,12 @@ public class Mod : BaseModel, IMod /// true if this instance is valid; otherwise, false. public virtual bool IsValid { get; set; } + /// + /// Gets or sets the identifier. + /// + /// The identifier. + public virtual string JsonId { get; set; } + /// /// Gets or sets the name. /// @@ -137,10 +145,12 @@ public virtual string ParentDirectory { return FullPath; } + if (!string.IsNullOrWhiteSpace(Path.GetExtension(FullPath))) { return Path.GetDirectoryName(FullPath); } + return FullPath; } } @@ -152,6 +162,12 @@ public virtual string ParentDirectory [DescriptorProperty("picture")] public virtual string Picture { get; set; } + /// + /// Gets or sets the relationship data. + /// + /// The relationship data. + public virtual IEnumerable> RelationshipData { get; set; } + /// /// Gets or sets the remote identifier. /// @@ -160,9 +176,9 @@ public virtual string ParentDirectory public virtual long? RemoteId { get; set; } /// - /// Gets or sets the replace path. + /// Gets or sets the replacement path. /// - /// The replace path. + /// The replacement path. [DescriptorProperty("replace_path", true)] public virtual IEnumerable ReplacePath { get; set; } @@ -228,6 +244,7 @@ public virtual Shared.Version VersionData { versionData = new Shared.Version(); } + return versionData; } } @@ -247,6 +264,7 @@ public virtual bool IsMatch(string term) { return false; } + term ??= string.Empty; return Name.StartsWith(term, StringComparison.OrdinalIgnoreCase); } diff --git a/src/IronyModManager.Parser/Mod/ModObject.cs b/src/IronyModManager.Parser/Mod/ModObject.cs index e61a4f49..e1af25e5 100644 --- a/src/IronyModManager.Parser/Mod/ModObject.cs +++ b/src/IronyModManager.Parser/Mod/ModObject.cs @@ -4,15 +4,17 @@ // Created : 02-22-2020 // // Last Modified By : Mario -// Last Modified On : 11-03-2022 +// Last Modified On : 12-23-2024 // *********************************************************************** // // Mario // // // *********************************************************************** + using System; using System.Collections.Generic; +using System.Linq; using IronyModManager.Shared; using IronyModManager.Shared.Models; @@ -47,6 +49,12 @@ public class ModObject : IModObject [DescriptorProperty("path", "archive", ".zip", ".bin")] public string FileName { get; set; } + /// + /// Gets or sets the json identifier. + /// + /// The json identifier. + public string JsonId { get; set; } + /// /// Gets or sets the name. /// @@ -61,6 +69,12 @@ public class ModObject : IModObject [DescriptorProperty("picture")] public string Picture { get; set; } + /// + /// Gets or sets the relationship data. + /// + /// The relationship data. + public IEnumerable> RelationshipData { get; set; } + /// /// Gets or sets the remote identifier. /// @@ -69,9 +83,9 @@ public class ModObject : IModObject public long? RemoteId { get; set; } /// - /// Gets or sets the replace path. + /// Gets or sets the replacement path. /// - /// The replace path. + /// The replacement path. [DescriptorProperty("replace_path", true)] public IEnumerable ReplacePath { get; set; } diff --git a/src/IronyModManager.Parser/Mod/ModParser.cs b/src/IronyModManager.Parser/Mod/ModParser.cs index 9090e104..28d6dccc 100644 --- a/src/IronyModManager.Parser/Mod/ModParser.cs +++ b/src/IronyModManager.Parser/Mod/ModParser.cs @@ -4,13 +4,14 @@ // Created : 02-22-2020 // // Last Modified By : Mario -// Last Modified On : 01-11-2023 +// Last Modified On : 12-23-2024 // *********************************************************************** // // Mario // // // *********************************************************************** + using System; using System.Collections.Generic; using System.Linq; @@ -32,35 +33,37 @@ namespace IronyModManager.Parser.Mod /// /// /// - public class ModParser : BaseGenericObjectParser, IModParser + /// Initializes a new instance of the class. + public class ModParser(ILogger logger, ICodeParser codeParser) : BaseGenericObjectParser(codeParser), IModParser { #region Fields /// - /// The json serializer settings + /// The display name key /// - private static JsonSerializerSettings jsonSerializerSettings = null; + private const string DisplayNameKey = "display_name"; /// - /// The logger + /// The resource type key /// - private readonly ILogger logger; + private const string ResourceTypeKey = "resource_type"; - #endregion Fields + /// + /// The resource type key value + /// + private const string ResourceTypeKeyValue = "mod"; - #region Constructors + /// + /// The json serializer settings + /// + private static JsonSerializerSettings jsonSerializerSettings; /// - /// Initializes a new instance of the class. + /// The logger /// - /// The logger. - /// The code parser. - public ModParser(ILogger logger, ICodeParser codeParser) : base(codeParser) - { - this.logger = logger; - } + private readonly ILogger logger = logger; - #endregion Constructors + #endregion Fields #region Methods @@ -75,7 +78,7 @@ public IModObject Parse(IEnumerable lines, DescriptorModType descriptorM return descriptorModType switch { DescriptorModType.JsonMetadata => ParseJsonMetadata(lines), - _ => ParseDescriptorMod(lines), + _ => ParseDescriptorMod(lines) }; } @@ -85,11 +88,7 @@ public IModObject Parse(IEnumerable lines, DescriptorModType descriptorM /// JsonSerializerSettings. private JsonSerializerSettings GetJsonSerializerSettings() { - jsonSerializerSettings ??= new JsonSerializerSettings() - { - Error = (sender, error) => error.ErrorContext.Handled = true, - NullValueHandling = NullValueHandling.Ignore - }; + jsonSerializerSettings ??= new JsonSerializerSettings { Error = (_, error) => error.ErrorContext.Handled = true, NullValueHandling = NullValueHandling.Ignore }; return jsonSerializerSettings; } @@ -114,6 +113,7 @@ private IModObject ParseDescriptorMod(IEnumerable lines) obj.RemoteId = GetValue(data.Values, "remote_file_id"); obj.Dependencies = GetValues(data.Values, "dependencies"); } + return obj; } @@ -122,31 +122,59 @@ private IModObject ParseDescriptorMod(IEnumerable lines) /// /// The lines. /// IModObject. + /// private IModObject ParseJsonMetadata(IEnumerable lines) { var obj = DIResolver.Get(); try { var json = string.Join(Environment.NewLine, lines); - var result = JsonConvert.DeserializeObject(json, GetJsonSerializerSettings()); - if (result.GameCustomData != null) + JsonMetadataBase result = null; + var ex = new List(); + try + { + result = JsonConvert.DeserializeObject(json, GetJsonSerializerSettings()); + } + catch (Exception e) + { + ex.Add(e); + } + + try { - if (result.GameCustomData.ContainsKey(Shared.Constants.JsonMetadataReplacePaths)) + result = JsonConvert.DeserializeObject(json, GetJsonSerializerSettings()); + } + catch (Exception e) + { + ex.Add(e); + } + + if (ex.Count >= 2) + { + throw new AggregateException(ex); + } + + if (result!.GameCustomData != null) + { + if (result.GameCustomData.TryGetValue(Shared.Constants.JsonMetadataReplacePaths, out var replacePath)) { - if (result.GameCustomData[Shared.Constants.JsonMetadataReplacePaths] is JArray jArray) + if (replacePath is JArray jArray) { obj.ReplacePath = jArray.ToObject>(); } } - if (result.GameCustomData.ContainsKey(Shared.Constants.DescriptorUserDir)) + + if (result.GameCustomData.TryGetValue(Shared.Constants.DescriptorUserDir, out var userDir)) { - if (result.GameCustomData[Shared.Constants.DescriptorUserDir] is JArray jArray) + if (userDir is JArray jArray) { obj.UserDir = jArray.ToObject>(); } } + obj.AdditionalData = result.GameCustomData.Where(p => p.Key != Shared.Constants.JsonMetadataReplacePaths && p.Key != Shared.Constants.DescriptorUserDir).ToDictionary(p => p.Key, p => p.Value); } + obj.FileName = result.Path; obj.Name = result.Name; obj.Version = result.SupportedGameVersion; @@ -155,12 +183,57 @@ private IModObject ParseJsonMetadata(IEnumerable lines) { obj.RemoteId = id; } - obj.Dependencies = result.Relationships; + + obj.JsonId = result.Id; + + switch (result) + { + case JsonMetadata metadata: + obj.Dependencies = metadata.Relationships; + break; + case JsonMetadataV2 metadataV2: + { + var dependencies = new List(); + if (metadataV2.Relationships != null) + { + foreach (var relationship in metadataV2.Relationships) + { + if (relationship.TryGetValue(ResourceTypeKey, out var resTypeVal)) + { + if (resTypeVal != null) + { + var resTypeStr = resTypeVal.ToString(); + if (resTypeStr!.Equals(ResourceTypeKeyValue, StringComparison.OrdinalIgnoreCase)) + { + if (relationship.TryGetValue(DisplayNameKey, out var displayName)) + { + if (displayName != null) + { + dependencies.Add(displayName.ToString()); + } + } + } + } + } + } + + obj.RelationshipData = metadataV2.Relationships; + } + + if (dependencies.Count > 0) + { + obj.Dependencies = dependencies; + } + + break; + } + } } catch (Exception ex) { logger.Error(ex); } + return obj; } @@ -171,7 +244,24 @@ private IModObject ParseJsonMetadata(IEnumerable lines) /// /// Class JsonMetadata. /// - private class JsonMetadata + private class JsonMetadata : JsonMetadataBase + { + #region Properties + + /// + /// Gets or sets the relationships. + /// + /// The relationships. + [JsonProperty("relationships")] + public List Relationships { get; set; } + + #endregion Properties + } + + /// + /// Class JsonMetadataBase. + /// + private abstract class JsonMetadataBase { #region Properties @@ -203,13 +293,6 @@ private class JsonMetadata [JsonProperty("path")] public string Path { get; set; } - /// - /// Gets or sets the relationships. - /// - /// The relationships. - [JsonProperty("relationships")] - public List Relationships { get; set; } - /// /// Gets or sets the short description. /// @@ -241,6 +324,23 @@ private class JsonMetadata #endregion Properties } + /// + /// Class JsonMetadataV2. + /// + private class JsonMetadataV2 : JsonMetadataBase + { + #region Properties + + /// + /// Gets or sets the relationships. + /// + /// The relationships. + [JsonProperty("relationships")] + public List> Relationships { get; set; } + + #endregion Properties + } + #endregion Classes } } diff --git a/src/IronyModManager.Services/ModMergeService.cs b/src/IronyModManager.Services/ModMergeService.cs index feee8555..24f79161 100644 --- a/src/IronyModManager.Services/ModMergeService.cs +++ b/src/IronyModManager.Services/ModMergeService.cs @@ -263,6 +263,12 @@ public virtual async Task MergeCollectionByFilesAsync(string collectionNam mod.AdditionalData = collectionMods.Where(p => p.AdditionalData != null).SelectMany(p => p.AdditionalData).ToLookup(p => p.Key, p => p.Value).ToDictionary(p => p.Key, p => p.First()); } + // Copy relationships although it should be redundant? + if (collectionMods.Any(p => p.RelationshipData != null)) + { + mod.RelationshipData = collectionMods.Where(p => p.RelationshipData != null).GroupBy(p => p.RelationshipData).SelectMany(p => p.FirstOrDefault()?.RelationshipData).ToList(); + } + await ModWriter.WriteDescriptorAsync(new ModWriterParameters { Mod = mod, diff --git a/src/IronyModManager.Services/ModService.cs b/src/IronyModManager.Services/ModService.cs index 116d865f..1e153803 100644 --- a/src/IronyModManager.Services/ModService.cs +++ b/src/IronyModManager.Services/ModService.cs @@ -4,7 +4,7 @@ // Created : 02-24-2020 // // Last Modified By : Mario -// Last Modified On : 04-27-2023 +// Last Modified On : 12-23-2024 // *********************************************************************** // // Mario @@ -106,6 +106,7 @@ public virtual string BuildModUrl(IMod mod) { return string.Empty; } + if (mod.Source == ModSource.Paradox) { return string.Format(Constants.Paradox_Url, mod.RemoteId); @@ -127,6 +128,7 @@ public virtual string BuildSteamUrl(IMod mod) { return string.Format(Constants.Steam_protocol_uri, BuildModUrl(mod)); } + return string.Empty; } @@ -142,15 +144,14 @@ public virtual async Task CustomModDirectoryEmptyAsync(string gameType) { return true; } + if (string.IsNullOrWhiteSpace(game.CustomModDirectory)) { return true; } + var path = GetModDirectoryRootPath(game); - var result = await ModWriter.ModDirectoryExistsAsync(new ModWriterParameters() - { - RootDirectory = path - }); + var result = await ModWriter.ModDirectoryExistsAsync(new ModWriterParameters { RootDirectory = path }); return !result; } @@ -168,7 +169,7 @@ public virtual Task DeleteDescriptorsAsync(IEnumerable mods) /// Evals the achievement compatibility. /// /// The mods. - /// true if XXXX, false otherwise. + /// true if achievement compatible, false otherwise. public virtual bool EvalAchievementCompatibility(IEnumerable mods) { if (mods?.Count() > 0) @@ -181,6 +182,7 @@ public virtual bool EvalAchievementCompatibility(IEnumerable mods) { return false; } + foreach (var item in filtered) { if (item.Files.Any()) @@ -193,9 +195,11 @@ public virtual bool EvalAchievementCompatibility(IEnumerable mods) item.AchievementStatus = AchievementStatus.AttemptedEvaluation; } } + return true; } } + return false; } @@ -213,33 +217,31 @@ public virtual async Task ExportModsAsync(IReadOnlyCollection enable { return false; } + var allMods = GetInstalledModsInternal(game, false); var mod = GeneratePatchModDescriptor(allMods, game, GenerateCollectionPatchName(modCollection.Name)); - var applyModParams = new ModWriterParameters() + var applyModParams = new ModWriterParameters { OtherMods = regularMods.Where(p => !enabledMods.Any(m => m.DescriptorFile.Equals(p.DescriptorFile))).ToList(), EnabledMods = enabledMods, RootDirectory = game.UserDirectory, DescriptorType = MapDescriptorType(game.ModDescriptorType) }; - if (await ModWriter.ModDirectoryExistsAsync(new ModWriterParameters() - { - RootDirectory = mod.FullPath - })) + if (await ModWriter.ModDirectoryExistsAsync(new ModWriterParameters { RootDirectory = mod.FullPath })) { if (modCollection.PatchModEnabled && enabledMods.Any()) { - if (await ModWriter.WriteDescriptorAsync(new ModWriterParameters() - { - Mod = mod, - RootDirectory = game.UserDirectory, - Path = mod.DescriptorFile, - LockDescriptor = CheckIfModShouldBeLocked(game, mod), - DescriptorType = MapDescriptorType(game.ModDescriptorType) - }, IsPatchModInternal(mod))) + if (await ModWriter.WriteDescriptorAsync(new ModWriterParameters + { + Mod = mod, + RootDirectory = game.UserDirectory, + Path = mod.DescriptorFile, + LockDescriptor = CheckIfModShouldBeLocked(game, mod), + DescriptorType = MapDescriptorType(game.ModDescriptorType) + }, IsPatchModInternal(mod))) { - applyModParams.TopPriorityMods = new List() { mod }; - Cache.Invalidate(new CacheInvalidateParameters() { Region = ModsCacheRegion, Prefix = game.Type, Keys = new List { GetModsCacheKey(true), GetModsCacheKey(false) } }); + applyModParams.TopPriorityMods = new List { mod }; + Cache.Invalidate(new CacheInvalidateParameters { Region = ModsCacheRegion, Prefix = game.Type, Keys = new List { GetModsCacheKey(true), GetModsCacheKey(false) } }); } } } @@ -248,9 +250,10 @@ public virtual async Task ExportModsAsync(IReadOnlyCollection enable // Remove left over descriptor if (allMods.Any(p => p.Name.Equals(mod.Name))) { - await DeleteDescriptorsInternalAsync(new List() { mod }); + await DeleteDescriptorsInternalAsync(new List { mod }); } } + return await ModWriter.ApplyModsAsync(applyModParams); } @@ -264,30 +267,35 @@ public virtual IEnumerable FilterMods(IEnumerable collection, string { if (collection == null) { - return collection; + return null; } + var parameters = CleanSearchResult(searchParser.Parse(languageService.GetSelected().Abrv, text)); var nameNegateCol = parameters.Name.Any() ? parameters.Name.Where(p => p.Negate).ToList() : new List(); var sourceNegateCol = parameters.Source.Any() ? parameters.Source.Where(p => p.Negate).ToList() : new List(); var versionNegateCol = parameters.Version.Any() ? parameters.Version.Where(p => p.Negate).ToList() : new List(); - var result = collection.ConditionalFilter(parameters.Name.Any(), q => q.Where(p => parameters.Name.Any(x => !x.Negate ? p.Name.Contains(x.Text, StringComparison.OrdinalIgnoreCase) : !nameNegateCol.Any(a => p.Name.Contains(a.Text, StringComparison.OrdinalIgnoreCase))))) - .ConditionalFilter(parameters.RemoteIds.Any(), q => q.Where(p => p.RemoteId.HasValue && parameters.RemoteIds.Any(x => !x.Negate ? p.RemoteId.GetValueOrDefault().ToString().Contains(x.Text, StringComparison.OrdinalIgnoreCase) + var result = collection.ConditionalFilter(parameters.Name.Any(), + q => q.Where(p => parameters.Name.Any(x => !x.Negate ? p.Name.Contains(x.Text, StringComparison.OrdinalIgnoreCase) : !nameNegateCol.Any(a => p.Name.Contains(a.Text, StringComparison.OrdinalIgnoreCase))))) + .ConditionalFilter(parameters.RemoteIds.Any(), q => q.Where(p => p.RemoteId.HasValue && parameters.RemoteIds.Any(x => !x.Negate + ? p.RemoteId.GetValueOrDefault().ToString().Contains(x.Text, StringComparison.OrdinalIgnoreCase) : !p.RemoteId.GetValueOrDefault().ToString().Contains(x.Text, StringComparison.OrdinalIgnoreCase)))) - .ConditionalFilter(parameters.AchievementCompatible.Result.HasValue, q => q.Where(p => - { - var result = p.AchievementStatus == (parameters.AchievementCompatible.Result.GetValueOrDefault() ? AchievementStatus.Compatible : AchievementStatus.NotCompatible); - return !parameters.AchievementCompatible.Negate ? result : !result; - })) - .ConditionalFilter(parameters.IsSelected.Result.HasValue, q => + .ConditionalFilter(parameters.AchievementCompatible.Result.HasValue, q => q.Where(p => + { + var result = p.AchievementStatus == (parameters.AchievementCompatible.Result.GetValueOrDefault() ? AchievementStatus.Compatible : AchievementStatus.NotCompatible); + return !parameters.AchievementCompatible.Negate ? result : !result; + })) + .ConditionalFilter(parameters.IsSelected.Result.HasValue, q => + { + if (parameters.IsSelected.Negate) { - if (parameters.IsSelected.Negate) - { - return q.Where(p => p.IsSelected != parameters.IsSelected.Result.GetValueOrDefault()); - } - return q.Where(p => p.IsSelected == parameters.IsSelected.Result.GetValueOrDefault()); - }) - .ConditionalFilter(parameters.Source.Any(), q => q.Where(p => parameters.Source.Any(s => !s.Negate ? p.Source == SourceTypeToModSource(s.Result) : !sourceNegateCol.Any(a => p.Source == SourceTypeToModSource(a.Result))))) - .ConditionalFilter(parameters.Version.Any(), q => q.Where(p => parameters.Version.Any(s => !s.Negate ? IsValidVersion(p.VersionData, s.Version) : !versionNegateCol.Any(a => IsValidVersion(p.VersionData, a.Version))))); + return q.Where(p => p.IsSelected != parameters.IsSelected.Result.GetValueOrDefault()); + } + + return q.Where(p => p.IsSelected == parameters.IsSelected.Result.GetValueOrDefault()); + }) + // ReSharper disable once SimplifyLinqExpressionUseAll + .ConditionalFilter(parameters.Source.Any(), q => q.Where(p => parameters.Source.Any(s => !s.Negate ? p.Source == SourceTypeToModSource(s.Result) : !sourceNegateCol.Any(a => p.Source == SourceTypeToModSource(a.Result))))) + .ConditionalFilter(parameters.Version.Any(), q => q.Where(p => parameters.Version.Any(s => !s.Negate ? IsValidVersion(p.VersionData, s.Version) : !versionNegateCol.Any(a => IsValidVersion(p.VersionData, a.Version))))); return result.ToList(); } @@ -305,29 +313,34 @@ public virtual IMod FindMod(IEnumerable collection, string text, bool reve { return null; } + var parameters = CleanSearchResult(searchParser.Parse(languageService.GetSelected().Abrv, text)); var nameNegateCol = parameters.Name.Any() ? parameters.Name.Where(p => p.Negate).ToList() : new List(); var sourceNegateCol = parameters.Source.Any() ? parameters.Source.Where(p => p.Negate).ToList() : new List(); var versionNegateCol = parameters.Version.Any() ? parameters.Version.Where(p => p.Negate).ToList() : new List(); var result = !reverse ? collection.Skip(skipIndex.GetValueOrDefault()) : collection.Reverse().Skip(skipIndex.GetValueOrDefault()); - result = result.ConditionalFilter(parameters.Name.Any(), q => q.Where(p => parameters.Name.Any(x => !x.Negate ? p.Name.Contains(x.Text, StringComparison.OrdinalIgnoreCase) : !nameNegateCol.Any(a => p.Name.Contains(a.Text, StringComparison.OrdinalIgnoreCase))))) - .ConditionalFilter(parameters.RemoteIds.Any(), q => q.Where(p => p.RemoteId.HasValue && parameters.RemoteIds.Any(x => !x.Negate ? p.RemoteId.GetValueOrDefault().ToString().Contains(x.Text, StringComparison.OrdinalIgnoreCase) + result = result.ConditionalFilter(parameters.Name.Any(), + q => q.Where(p => parameters.Name.Any(x => !x.Negate ? p.Name.Contains(x.Text, StringComparison.OrdinalIgnoreCase) : !nameNegateCol.Any(a => p.Name.Contains(a.Text, StringComparison.OrdinalIgnoreCase))))) + .ConditionalFilter(parameters.RemoteIds.Any(), q => q.Where(p => p.RemoteId.HasValue && parameters.RemoteIds.Any(x => !x.Negate + ? p.RemoteId.GetValueOrDefault().ToString().Contains(x.Text, StringComparison.OrdinalIgnoreCase) : !p.RemoteId.GetValueOrDefault().ToString().Contains(x.Text, StringComparison.OrdinalIgnoreCase)))) - .ConditionalFilter(parameters.AchievementCompatible.Result.HasValue, q => q.Where(p => - { - var result = p.AchievementStatus == (parameters.AchievementCompatible.Result.GetValueOrDefault() ? AchievementStatus.Compatible : AchievementStatus.NotCompatible); - return !parameters.AchievementCompatible.Negate ? result : !result; - })) - .ConditionalFilter(parameters.IsSelected.Result.HasValue, q => + .ConditionalFilter(parameters.AchievementCompatible.Result.HasValue, q => q.Where(p => + { + var result = p.AchievementStatus == (parameters.AchievementCompatible.Result.GetValueOrDefault() ? AchievementStatus.Compatible : AchievementStatus.NotCompatible); + return !parameters.AchievementCompatible.Negate ? result : !result; + })) + .ConditionalFilter(parameters.IsSelected.Result.HasValue, q => + { + if (parameters.IsSelected.Negate) { - if (parameters.IsSelected.Negate) - { - return q.Where(p => p.IsSelected != parameters.IsSelected.Result.GetValueOrDefault()); - } - return q.Where(p => p.IsSelected == parameters.IsSelected.Result.GetValueOrDefault()); - }) - .ConditionalFilter(parameters.Source.Any(), q => q.Where(p => parameters.Source.Any(s => !s.Negate ? p.Source == SourceTypeToModSource(s.Result) : !sourceNegateCol.Any(a => p.Source == SourceTypeToModSource(a.Result))))) - .ConditionalFilter(parameters.Version.Any(), q => q.Where(p => parameters.Version.Any(s => !s.Negate ? IsValidVersion(p.VersionData, s.Version) : !versionNegateCol.Any(a => IsValidVersion(p.VersionData, a.Version))))); + return q.Where(p => p.IsSelected != parameters.IsSelected.Result.GetValueOrDefault()); + } + + return q.Where(p => p.IsSelected == parameters.IsSelected.Result.GetValueOrDefault()); + }) + // ReSharper disable once SimplifyLinqExpressionUseAll + .ConditionalFilter(parameters.Source.Any(), q => q.Where(p => parameters.Source.Any(s => !s.Negate ? p.Source == SourceTypeToModSource(s.Result) : !sourceNegateCol.Any(a => p.Source == SourceTypeToModSource(a.Result))))) + .ConditionalFilter(parameters.Version.Any(), q => q.Where(p => parameters.Version.Any(s => !s.Negate ? IsValidVersion(p.VersionData, s.Version) : !versionNegateCol.Any(a => IsValidVersion(p.VersionData, a.Version))))); return result.FirstOrDefault(); } @@ -340,6 +353,8 @@ public virtual async Task> GetAvailableModsAsync(IGame game) { using var mutex = await modReadLock.LockAsync(); var result = GetInstalledModsInternal(game, true); + + // ReSharper disable once DisposeOnUsingVariable mutex.Dispose(); return result; } @@ -358,6 +373,7 @@ public virtual Task GetImageStreamAsync(string modName, string pat { return Task.FromResult((MemoryStream)null); } + var mods = GetInstalledModsInternal(game, false); return GetImageStreamAsync(mods.FirstOrDefault(p => p.Name.Equals(modName)), path, isFromGame); } @@ -382,6 +398,7 @@ public virtual Task GetImageStreamAsync(IMod mod, string path, boo { return Reader.GetImageStreamAsync(Path.GetDirectoryName(GameService.GetSelected().ExecutableLocation), path); } + return Task.FromResult((MemoryStream)null); } @@ -395,9 +412,12 @@ public virtual async Task> GetInstalledModsAsync(IGame game) using var mutex = await modReadLock.LockAsync(); if (game != null) { - Cache.Invalidate(new CacheInvalidateParameters() { Region = ModsCacheRegion, Prefix = game.Type, Keys = new List { GetModsCacheKey(true), GetModsCacheKey(false) } }); + Cache.Invalidate(new CacheInvalidateParameters { Region = ModsCacheRegion, Prefix = game.Type, Keys = new List { GetModsCacheKey(true), GetModsCacheKey(false) } }); } + var result = GetInstalledModsInternal(game, true); + + // ReSharper disable once DisposeOnUsingVariable mutex.Dispose(); return result; } @@ -411,24 +431,20 @@ public virtual async Task> InstallMo { using var mutex = await modReadLock.LockAsync(); var game = GameService.GetSelected(); - if (game == null || !await ModWriter.CanWriteToModDirectoryAsync(new ModWriterParameters() - { - RootDirectory = game.UserDirectory, - Path = Shared.Constants.ModDirectory - })) + if (game == null || !await ModWriter.CanWriteToModDirectoryAsync(new ModWriterParameters { RootDirectory = game.UserDirectory, Path = Shared.Constants.ModDirectory })) { + // ReSharper disable once DisposeOnUsingVariable mutex.Dispose(); return null; } - if (game.ModDescriptorType == ModDescriptorType.JsonMetadata && !await ModWriter.CanWriteToModDirectoryAsync(new ModWriterParameters() - { - RootDirectory = game.UserDirectory, - Path = Shared.Constants.JsonModDirectory - })) + + if (game.ModDescriptorType == ModDescriptorType.JsonMetadata && !await ModWriter.CanWriteToModDirectoryAsync(new ModWriterParameters { RootDirectory = game.UserDirectory, Path = Shared.Constants.JsonModDirectory })) { + // ReSharper disable once DisposeOnUsingVariable mutex.Dispose(); return null; } + var mods = GetInstalledModsInternal(game, false); var descriptors = new List(); var userDirectoryMods = GetAllModDescriptors(Path.Combine(game.UserDirectory, Shared.Constants.ModDirectory), ModSource.Local, game.ModDescriptorType); @@ -436,6 +452,7 @@ public virtual async Task> InstallMo { descriptors.AddRange(userDirectoryMods); } + if (!string.IsNullOrWhiteSpace(game.CustomModDirectory)) { var customMods = GetAllModDescriptors(GetModDirectoryRootPath(game), ModSource.Local, game.ModDescriptorType); @@ -444,60 +461,52 @@ public virtual async Task> InstallMo descriptors.AddRange(customMods); } } + var workshopDirectoryMods = game.WorkshopDirectory.SelectMany(p => GetAllModDescriptors(p, ModSource.Steam, game.ModDescriptorType)); if (workshopDirectoryMods.Any()) { descriptors.AddRange(workshopDirectoryMods); } + var filteredDescriptors = new List(); var grouped = descriptors.GroupBy(p => p.ParentDirectory); foreach (var item in grouped) { if (item.Any()) { - if (item.All(p => p.IsFile)) - { - filteredDescriptors.AddRange(item); - } - else - { - filteredDescriptors.AddRange(item.Where(p => !p.IsFile)); - } + filteredDescriptors.AddRange(item.All(p => p.IsFile) ? item : item.Where(p => !p.IsFile)); } } + var diffs = filteredDescriptors.Where(p => p.Mod != null && !mods.Any(m => AreModsSame(m, p.Mod))).ToList(); if (diffs.Count > 0) { var result = new List(); - await ModWriter.CreateModDirectoryAsync(new ModWriterParameters() - { - RootDirectory = game.UserDirectory, - Path = Shared.Constants.ModDirectory - }); + await ModWriter.CreateModDirectoryAsync(new ModWriterParameters { RootDirectory = game.UserDirectory, Path = Shared.Constants.ModDirectory }); if (game.ModDescriptorType == ModDescriptorType.JsonMetadata) { - await ModWriter.CreateModDirectoryAsync(new ModWriterParameters() - { - RootDirectory = game.UserDirectory, - Path = Shared.Constants.JsonModDirectory - }); + await ModWriter.CreateModDirectoryAsync(new ModWriterParameters { RootDirectory = game.UserDirectory, Path = Shared.Constants.JsonModDirectory }); } + var tasks = new List(); foreach (var diff in diffs.GroupBy(p => p.Mod.DescriptorFile)) { - IModInstallationResult installResult = diff.FirstOrDefault(); - if (game.WorkshopDirectory.Any() && diff.Any(p => p.Path.StartsWith(game.WorkshopDirectory.FirstOrDefault()))) + var installResult = diff.FirstOrDefault(); + if (game.WorkshopDirectory.Any() && diff.Any(p => p.Path.StartsWith(game.WorkshopDirectory.FirstOrDefault() ?? string.Empty))) { - installResult = diff.FirstOrDefault(p => p.Path.StartsWith(game.WorkshopDirectory.FirstOrDefault())); + installResult = diff.FirstOrDefault(p => p.Path.StartsWith(game.WorkshopDirectory.FirstOrDefault() ?? string.Empty)); } + + // ReSharper disable once PossibleNullReferenceException var localDiff = installResult.Mod; if (IsPatchModInternal(localDiff)) { continue; } + tasks.Add(Task.Run(async () => { - bool shouldLock = CheckIfModShouldBeLocked(game, localDiff); + var shouldLock = CheckIfModShouldBeLocked(game, localDiff); if (statusToRetain != null && !shouldLock) { var mod = statusToRetain.FirstOrDefault(p => p.DescriptorFile.Equals(localDiff.DescriptorFile, StringComparison.OrdinalIgnoreCase)); @@ -506,7 +515,8 @@ await ModWriter.CreateModDirectoryAsync(new ModWriterParameters() shouldLock = mod.IsLocked; } } - await ModWriter.WriteDescriptorAsync(new ModWriterParameters() + + await ModWriter.WriteDescriptorAsync(new ModWriterParameters { Mod = localDiff, RootDirectory = game.UserDirectory, @@ -518,23 +528,31 @@ await ModWriter.WriteDescriptorAsync(new ModWriterParameters() installResult.Installed = true; result.Add(installResult); } + if (tasks.Count > 0) { await Task.WhenAll(tasks); - Cache.Invalidate(new CacheInvalidateParameters() { Region = ModsCacheRegion, Prefix = game.Type, Keys = new List { GetModsCacheKey(true), GetModsCacheKey(false) } }); + Cache.Invalidate(new CacheInvalidateParameters { Region = ModsCacheRegion, Prefix = game.Type, Keys = new List { GetModsCacheKey(true), GetModsCacheKey(false) } }); } + if (filteredDescriptors.Any(p => p.Invalid)) { result.AddRange(filteredDescriptors.Where(p => p.Invalid)); } + + // ReSharper disable once DisposeOnUsingVariable mutex.Dispose(); return result; } + if (filteredDescriptors.Any(p => p.Invalid)) { + // ReSharper disable once DisposeOnUsingVariable mutex.Dispose(); return filteredDescriptors.Where(p => p.Invalid).ToList(); } + + // ReSharper disable once DisposeOnUsingVariable mutex.Dispose(); return null; } @@ -556,18 +574,16 @@ public virtual async Task LockDescriptorsAsync(IEnumerable mods, boo // Cannot lock\unlock mandatory local zipped mods if (!CheckIfModShouldBeLocked(game, item)) { - var task = ModWriter.SetDescriptorLockAsync(new ModWriterParameters() - { - Mod = item, - RootDirectory = game.UserDirectory - }, isLocked); + var task = ModWriter.SetDescriptorLockAsync(new ModWriterParameters { Mod = item, RootDirectory = game.UserDirectory }, isLocked); item.IsLocked = isLocked; tasks.Add(task); } } + await Task.WhenAll(tasks); return true; } + return false; } @@ -583,19 +599,13 @@ public virtual async Task ModDirectoryExistsAsync(string folder) { return false; } - var result = await ModWriter.ModDirectoryExistsAsync(new ModWriterParameters() - { - RootDirectory = game.UserDirectory, - Path = Path.Combine(Shared.Constants.ModDirectory, folder) - }); + + var result = await ModWriter.ModDirectoryExistsAsync(new ModWriterParameters { RootDirectory = game.UserDirectory, Path = Path.Combine(Shared.Constants.ModDirectory, folder) }); if (!result && !string.IsNullOrEmpty(game.CustomModDirectory)) { - result = await ModWriter.ModDirectoryExistsAsync(new ModWriterParameters() - { - RootDirectory = GetModDirectoryRootPath(game), - Path = folder - }); + result = await ModWriter.ModDirectoryExistsAsync(new ModWriterParameters { RootDirectory = GetModDirectoryRootPath(game), Path = folder }); } + return result; } @@ -631,19 +641,15 @@ public virtual async Task PurgeModDirectoryAsync(string folder) { return false; } + var fullPath = Path.Combine(game.UserDirectory, Shared.Constants.ModDirectory, folder); - var exists = await ModWriter.ModDirectoryExistsAsync(new ModWriterParameters() - { - RootDirectory = fullPath - }); + var exists = await ModWriter.ModDirectoryExistsAsync(new ModWriterParameters { RootDirectory = fullPath }); if (!exists) { fullPath = Path.Combine(GetModDirectoryRootPath(game), folder); } - var result = await ModWriter.PurgeModDirectoryAsync(new ModWriterParameters() - { - RootDirectory = fullPath - }, true); + + var result = await ModWriter.PurgeModDirectoryAsync(new ModWriterParameters { RootDirectory = fullPath }, true); var mods = GetInstalledModsInternal(game, false); if (mods.Any(p => !string.IsNullOrWhiteSpace(p.FullPath) && p.FullPath.Contains(fullPath))) { @@ -653,6 +659,7 @@ public virtual async Task PurgeModDirectoryAsync(string folder) await DeleteDescriptorsInternalAsync(mod); } } + return result; } @@ -670,11 +677,11 @@ public virtual Task PurgeModPatchAsync(string collectionName) /// Queries the contains achievements. /// /// The query. - /// true if XXXX, false otherwise. + /// true if query contains achievements false otherwise. public virtual bool QueryContainsAchievements(string query) { var result = searchParser.Parse(languageService.GetSelected().Abrv, query); - return result != null && result.AchievementCompatible != null && result.AchievementCompatible.Result.HasValue; + return result is { AchievementCompatible.Result: not null }; } /// @@ -682,16 +689,17 @@ public virtual bool QueryContainsAchievements(string query) /// /// The mod. /// The other mod. - /// true if XXXX, false otherwise. + /// true if mods are the same, false otherwise. protected virtual bool AreModsSame(IMod mod, IMod otherMod) { if (mod == null || otherMod == null) { return false; } + return mod.DescriptorFile.Equals(otherMod.DescriptorFile, StringComparison.OrdinalIgnoreCase) && mod.Version.Equals(otherMod.Version) && - mod.Name.Equals(otherMod.Name) && mod.Dependencies.ListsSame(otherMod.Dependencies) && mod.RemoteId.GetValueOrDefault().Equals(otherMod.RemoteId.GetValueOrDefault()) && - mod.ReplacePath.ListsSame(otherMod.ReplacePath) && mod.UserDir.ListsSame(otherMod.UserDir); + mod.Name.Equals(otherMod.Name) && mod.Dependencies.ListsSame(otherMod.Dependencies) && mod.RemoteId.GetValueOrDefault().Equals(otherMod.RemoteId.GetValueOrDefault()) && + mod.ReplacePath.ListsSame(otherMod.ReplacePath) && mod.UserDir.ListsSame(otherMod.UserDir) && (mod.JsonId ?? string.Empty).Equals(otherMod.JsonId ?? string.Empty, StringComparison.OrdinalIgnoreCase); } /// @@ -702,7 +710,7 @@ protected virtual bool AreModsSame(IMod mod, IMod otherMod) protected virtual ParsedSearchResult CleanSearchResult(ISearchParserResult parserResult) { var names = parserResult.Name.Where(p => !string.IsNullOrWhiteSpace(p.Text)).ToList(); - var remoteIds = names.Where(p => long.TryParse(p.Text, out var _)).ToList(); + var remoteIds = names.Where(p => long.TryParse(p.Text, out _)).ToList(); remoteIds.ForEach(p => names.Remove(p)); var result = new ParsedSearchResult { @@ -726,7 +734,9 @@ protected virtual ParsedSearchResult CleanSearchResult(ISearchParserResult parse protected virtual IEnumerable GetAllModDescriptors(string path, ModSource modSource, ModDescriptorType modDescriptorType) { // Json metadata doesn't support zips to ignore them - var files = Directory.Exists(path) && modDescriptorType == ModDescriptorType.DescriptorMod ? Directory.EnumerateFiles(path, $"*{Shared.Constants.ZipExtension}").Union(Directory.EnumerateFiles(path, $"*{Shared.Constants.BinExtension}")) : Array.Empty(); + var files = Directory.Exists(path) && modDescriptorType == ModDescriptorType.DescriptorMod + ? Directory.EnumerateFiles(path, $"*{Shared.Constants.ZipExtension}").Union(Directory.EnumerateFiles(path, $"*{Shared.Constants.BinExtension}")) + : Array.Empty(); var directories = Directory.Exists(path) ? Directory.EnumerateDirectories(path) : Array.Empty(); var mods = new ConcurrentBag(); @@ -755,12 +765,9 @@ string readModPrefix(string path) if (modDescriptorType == ModDescriptorType.DescriptorMod) { var fileInfo = Reader.GetFileInfo(path, Shared.Constants.ModNamePrefixOverride); - if (fileInfo == null) - { - return string.Empty; - } - return fileInfo.Content.FirstOrDefault(); + return fileInfo == null ? string.Empty : fileInfo.Content.FirstOrDefault(); } + return string.Empty; } @@ -790,6 +797,7 @@ void parseModFiles(string path, ModSource source, bool isDirectory, string modNa } } } + var mod = Mapper.Map(ModParser.Parse(fileInfo.Content, MapDescriptorModType(modDescriptorType))); mod.Name = ModWriter.FormatPrefixModName(modNamePrefix, mod.Name); if (!string.IsNullOrWhiteSpace(modNamePrefix) && mod.Dependencies != null && mod.Dependencies.Any()) @@ -799,6 +807,7 @@ void parseModFiles(string path, ModSource source, bool isDirectory, string modNa dependencies.ToList().ForEach(p => newDependencies.Add(ModWriter.FormatPrefixModName(modNamePrefix, p))); mod.Dependencies = newDependencies; } + mod.FileName = path.Replace("\\", "/"); mod.FullPath = path.StandardizeDirectorySeparator(); mod.IsLocked = fileInfo.IsReadOnly; @@ -806,18 +815,13 @@ void parseModFiles(string path, ModSource source, bool isDirectory, string modNa var cleanedPath = path; if (!isDirectory) { - cleanedPath = Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(path)); + cleanedPath = Path.Combine(Path.GetDirectoryName(path) ?? string.Empty, Path.GetFileNameWithoutExtension(path)); } - string localPath; - if (modDescriptorType == ModDescriptorType.DescriptorMod) - { - localPath = $"{Shared.Constants.ModDirectory}/{cleanedPath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()}{Shared.Constants.ModExtension}"; - } - else - { - localPath = $"{Shared.Constants.JsonModDirectory}/{cleanedPath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()}{Shared.Constants.JsonExtension}"; - } + var localPath = modDescriptorType == ModDescriptorType.DescriptorMod + ? $"{Shared.Constants.ModDirectory}/{cleanedPath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()}{Shared.Constants.ModExtension}" + : $"{Shared.Constants.JsonModDirectory}/{cleanedPath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()}{Shared.Constants.JsonExtension}"; + switch (mod.Source) { case ModSource.Local: @@ -834,15 +838,11 @@ void parseModFiles(string path, ModSource source, bool isDirectory, string modNa { mod.RemoteId = GetSteamModId(path); } - string steamPath; - if (modDescriptorType == ModDescriptorType.DescriptorMod) - { - steamPath = $"{Shared.Constants.ModDirectory}/{Constants.Steam_mod_id}{mod.RemoteId}{Shared.Constants.ModExtension}"; - } - else - { - steamPath = $"{Shared.Constants.JsonModDirectory}/{Constants.Steam_mod_id}{mod.RemoteId}{Shared.Constants.JsonExtension}"; - } + + var steamPath = modDescriptorType == ModDescriptorType.DescriptorMod + ? $"{Shared.Constants.ModDirectory}/{Constants.Steam_mod_id}{mod.RemoteId}{Shared.Constants.ModExtension}" + : $"{Shared.Constants.JsonModDirectory}/{Constants.Steam_mod_id}{mod.RemoteId}{Shared.Constants.JsonExtension}"; + setDescriptorPath(mod, steamPath, localPath); break; @@ -856,21 +856,15 @@ void parseModFiles(string path, ModSource source, bool isDirectory, string modNa { mod.RemoteId = GetPdxModId(path); } - string pdxPath; - if (modDescriptorType == ModDescriptorType.DescriptorMod) - { - pdxPath = $"{Shared.Constants.ModDirectory}/{Constants.Paradox_mod_id}{mod.RemoteId}{Shared.Constants.ModExtension}"; - } - else - { - pdxPath = $"{Shared.Constants.JsonModDirectory}/{Constants.Paradox_mod_id}{mod.RemoteId}{Shared.Constants.JsonExtension}"; - } - setDescriptorPath(mod, pdxPath, localPath); - break; - default: + var pdxPath = modDescriptorType == ModDescriptorType.DescriptorMod + ? $"{Shared.Constants.ModDirectory}/{Constants.Paradox_mod_id}{mod.RemoteId}{Shared.Constants.ModExtension}" + : $"{Shared.Constants.JsonModDirectory}/{Constants.Paradox_mod_id}{mod.RemoteId}{Shared.Constants.JsonExtension}"; + + setDescriptorPath(mod, pdxPath, localPath); break; } + result.Mod = mod; } catch (Exception ex) @@ -878,16 +872,11 @@ void parseModFiles(string path, ModSource source, bool isDirectory, string modNa logger.Error(ex); result.Invalid = true; } + result.Path = path; result.IsFile = File.Exists(path); - if (result.IsFile) - { - result.ParentDirectory = Path.GetDirectoryName(path); - } - else - { - result.ParentDirectory = path; - } + result.ParentDirectory = result.IsFile ? Path.GetDirectoryName(path) : path; + mods.Add(result); } @@ -898,12 +887,15 @@ void parseModFiles(string path, ModSource source, bool isDirectory, string modNa parseModFiles(file, modSource, false, string.Empty); }); } + if (directories.Any()) { directories.AsParallel().WithDegreeOfParallelism(MaxModsToProcess).ForAll(directory => { - var modSourceOverride = directory.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries). - LastOrDefault().Contains(Constants.Paradox_mod_id, StringComparison.OrdinalIgnoreCase) ? ModSource.Paradox : modSource; + // ReSharper disable once PossibleNullReferenceException + var modSourceOverride = directory.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).LastOrDefault().Contains(Constants.Paradox_mod_id, StringComparison.OrdinalIgnoreCase) + ? ModSource.Paradox + : modSource; var modNamePrefix = readModPrefix(directory); parseModFiles(directory, modSourceOverride, true, modNamePrefix); @@ -922,13 +914,16 @@ void parseModFiles(string path, ModSource source, bool isDirectory, string modNa { foreach (var subdirectory in subdirectories) { - var subDirectoryModSourceOverride = subdirectory.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries). - LastOrDefault().Contains(Constants.Paradox_mod_id, StringComparison.OrdinalIgnoreCase) ? ModSource.Paradox : modSource; + // ReSharper disable once PossibleNullReferenceException + var subDirectoryModSourceOverride = subdirectory.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).LastOrDefault().Contains(Constants.Paradox_mod_id, StringComparison.OrdinalIgnoreCase) + ? ModSource.Paradox + : modSource; parseModFiles(subdirectory, subDirectoryModSourceOverride, true, modNamePrefix); } } }); } + return mods.ToList(); } @@ -967,6 +962,7 @@ static bool validateValues(int x, int y) return false; } } + if (validateValues(currentVersion.Minor, requestedVersion.Minor)) { var result = currentVersion.Minor.CompareTo(requestedVersion.Minor); @@ -975,6 +971,7 @@ static bool validateValues(int x, int y) return false; } } + if (validateValues(currentVersion.Build, requestedVersion.Build)) { var result = currentVersion.Build.CompareTo(requestedVersion.Build); @@ -983,6 +980,7 @@ static bool validateValues(int x, int y) return false; } } + if (validateValues(currentVersion.Revision, requestedVersion.Revision)) { var result = currentVersion.Revision.CompareTo(requestedVersion.Revision); @@ -991,6 +989,7 @@ static bool validateValues(int x, int y) return false; } } + return true; } @@ -1005,7 +1004,7 @@ protected virtual ModSource SourceTypeToModSource(SourceType type) { SourceType.Paradox => ModSource.Paradox, SourceType.Steam => ModSource.Steam, - _ => ModSource.Local, + _ => ModSource.Local }; } diff --git a/src/IronyModManager.Shared/Models/IModObject.cs b/src/IronyModManager.Shared/Models/IModObject.cs index 91d13e84..acf9a2aa 100644 --- a/src/IronyModManager.Shared/Models/IModObject.cs +++ b/src/IronyModManager.Shared/Models/IModObject.cs @@ -4,15 +4,17 @@ // Created : 02-22-2020 // // Last Modified By : Mario -// Last Modified On : 11-03-2022 +// Last Modified On : 12-23-2024 // *********************************************************************** // // Mario // // // *********************************************************************** + using System; using System.Collections.Generic; +using System.Linq; namespace IronyModManager.Shared.Models { @@ -41,6 +43,12 @@ public interface IModObject /// The name of the file. string FileName { get; set; } + /// + /// Gets or sets the json identifier. + /// + /// The json identifier. + public string JsonId { get; set; } + /// /// Gets or sets the name. /// @@ -53,6 +61,12 @@ public interface IModObject /// The picture. string Picture { get; set; } + /// + /// Gets or sets the relationship data. + /// + /// The relationship data. + IEnumerable> RelationshipData { get; set; } + /// /// Gets or sets the remote identifier. /// @@ -60,9 +74,9 @@ public interface IModObject long? RemoteId { get; set; } /// - /// Gets or sets the replace path. + /// Gets or sets the replacement path. /// - /// The replace path. + /// The replacement path. IEnumerable ReplacePath { get; set; } ///