From 28428b3badd830600157ce270843f9571afa4c9b Mon Sep 17 00:00:00 2001 From: fgsfds <4870330+fgsfds@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:45:57 +0500 Subject: [PATCH 1/7] Raze config overflow fix --- src/Ports/Ports/Raze.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ports/Ports/Raze.cs b/src/Ports/Ports/Raze.cs index c2b97495..bb2c2f6d 100644 --- a/src/Ports/Ports/Raze.cs +++ b/src/Ports/Ports/Raze.cs @@ -447,7 +447,7 @@ private static void AddGamePathsToConfig(BaseGame game, BaseAddon campaign, stri { i++; } - while (!string.IsNullOrWhiteSpace(contents[i])); + while (i < contents.Length && !string.IsNullOrWhiteSpace(contents[i])); _ = sb.AppendLine(); continue; From 1879c8791131b58db9e26ecffbc0b663ea99269f Mon Sep 17 00:00:00 2001 From: fgsfds <4870330+fgsfds@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:27:29 +0500 Subject: [PATCH 2/7] renamed AddonJsonModel to AddonManifestJsonModel --- .../Providers/InstalledAddonsProvider.cs | 14 +- .../ViewModels/DevViewModel.cs | 18 +- ...JsonModel.cs => AddonManifestJsonModel.cs} | 12 +- .../Serializable/Addon/DependencyJsonModel.cs | 15 +- .../Serializable/Addon/ManifestsJsonModel.cs | 38 +- .../Serializable/Addon/MapFileJsonModel.cs | 4 +- .../Serializable/Addon/MapSlotJsonModel.cs | 4 +- .../Serializable/Addon/OptionJsonModel.cs | 2 +- .../Addon/SupportedGameJsonModel.cs | 4 +- .../DownloadableAddonJsonModel.cs | 18 +- src/Core.All/Serializable/JsonConverters.cs | 21 +- src/Core.Client/Api/GitHubApiInterface.cs | 12 +- src/Core.Client/Api/OfflineApiInterface.cs | 6 +- src/Core.Client/Helpers/ManifestHelper.cs | 4 +- src/Core.Client/Helpers/UriHelper.cs | 2 +- src/Core.Client/Interfaces/IApiInterface.cs | 4 +- src/Core.Client/Providers/MetadataProvider.cs | 16 +- .../Tools/AddonsDatabaseManager.cs | 6 +- src/Tests.Database/AddonsDatabaseTests.cs | 2 +- src/Tests.Unit/SerializeTests.cs | 258 -------- src/Tests.Unit/SerializerTests.cs | 571 ++++++++++++++++++ 21 files changed, 665 insertions(+), 366 deletions(-) rename src/Core.All/Serializable/Addon/{AddonJsonModel.cs => AddonManifestJsonModel.cs} (88%) delete mode 100644 src/Tests.Unit/SerializeTests.cs create mode 100644 src/Tests.Unit/SerializerTests.cs diff --git a/src/Addons/Providers/InstalledAddonsProvider.cs b/src/Addons/Providers/InstalledAddonsProvider.cs index d5855104..8c6149ec 100644 --- a/src/Addons/Providers/InstalledAddonsProvider.cs +++ b/src/Addons/Providers/InstalledAddonsProvider.cs @@ -532,7 +532,7 @@ newAddon.AddonId.Version is not null && var manifest = await JsonSerializer.DeserializeAsync( jsonStream, - AddonManifestContext.Default.AddonJsonModel + AddonManifestJsonContext.Default.AddonManifestJsonModel ).ConfigureAwait(false); if (manifest is null) @@ -902,7 +902,7 @@ or GameEnum.NAM /// Path to archive /// Addon manifests /// Path to unpacked folder or if not unpacked. - private string? UnpackIfNeededAndGetAddonManifests(string pathToFile, out List? manifests) + private string? UnpackIfNeededAndGetAddonManifests(string pathToFile, out List? manifests) { try { @@ -935,7 +935,7 @@ or GameEnum.NAM var addonDto = JsonSerializer.Deserialize( addonJsonStream, - AddonManifestContext.Default.AddonJsonModel + AddonManifestJsonContext.Default.AddonManifestJsonModel )!; if (addonDto.MainRff is not null || addonDto.SoundRff is not null) @@ -949,7 +949,7 @@ or GameEnum.NAM unpackedTo = Unpack(pathToFile, archive); } - List result = []; + List result = []; if (unpackedTo is not null) { @@ -964,7 +964,7 @@ or GameEnum.NAM var addonDto2 = JsonSerializer.Deserialize( text, - AddonManifestContext.Default.AddonJsonModel + AddonManifestJsonContext.Default.AddonManifestJsonModel )!; result.Add(addonDto2); @@ -978,7 +978,7 @@ or GameEnum.NAM var addonDto2 = JsonSerializer.Deserialize( addonJsonStream2, - AddonManifestContext.Default.AddonJsonModel + AddonManifestJsonContext.Default.AddonManifestJsonModel )!; result.Add(addonDto2); @@ -1024,7 +1024,7 @@ private string Unpack(string pathToFile, IArchive archive) } private AddonCarcass GetCarcass( - AddonJsonModel manifest, + AddonManifestJsonModel manifest, string pathToFile, bool isUnpacked, long? gridImageHash, diff --git a/src/Avalonia.Desktop/ViewModels/DevViewModel.cs b/src/Avalonia.Desktop/ViewModels/DevViewModel.cs index 9ad489da..102a02a9 100644 --- a/src/Avalonia.Desktop/ViewModels/DevViewModel.cs +++ b/src/Avalonia.Desktop/ViewModels/DevViewModel.cs @@ -663,7 +663,7 @@ private async Task UpdateManifestsAsync() return; } - List result = new(files.Count); + List result = new(files.Count); foreach (var file in files) { @@ -676,7 +676,7 @@ private async Task UpdateManifestsAsync() var jsonStr = await JsonSerializer.DeserializeAsync( jsonStream, - AddonManifestContext.Default.AddonJsonModel + AddonManifestJsonContext.Default.AddonManifestJsonModel ).ConfigureAwait(false); if (jsonStr is null) @@ -688,14 +688,14 @@ private async Task UpdateManifestsAsync() } } - var list = JsonSerializer.Serialize(result, ManifestsJsonModelContext.Default.ListAddonJsonModel); + var list = JsonSerializer.Serialize(result, AddonManifestJsonContext.Default.ListAddonManifestJsonModel); await File.WriteAllTextAsync(ClientProperties.PathToLocalManifestsJson, list).ConfigureAwait(false); } #endregion - private AddonJsonModel GetAddonJson(out string jsonString) + private AddonManifestJsonModel GetAddonJson(out string jsonString) { if (PathToAddonFolder is null) { @@ -928,7 +928,7 @@ SelectedGame is not GameEnum.Duke3D ? null executables[OSEnum.Linux].Add(PortEnum.PCExhumed, LinuxPCExhumedExe); } - AddonJsonModel addon = new() + AddonManifestJsonModel addon = new() { AddonType = addonType, Id = AddonIdPrefix + AddonId, @@ -958,7 +958,7 @@ SelectedGame is not GameEnum.Duke3D ? null Executables = executables.Count == 0 ? null : executables }; - jsonString = JsonSerializer.Serialize(addon, AddonManifestContext.Default.AddonJsonModel); + jsonString = JsonSerializer.Serialize(addon, AddonManifestJsonContext.Default.AddonManifestJsonModel); JsonText = jsonString; return addon; @@ -970,7 +970,7 @@ private void LoadJson(string pathToFile) var addon = JsonSerializer.Deserialize( jsonStream, - AddonManifestContext.Default.AddonJsonModel + AddonManifestJsonContext.Default.AddonManifestJsonModel ); if (addon is null) @@ -1071,7 +1071,7 @@ private void LoadJson(string pathToFile) /// Rename addon folder to {addon_id}_v{addon_version} /// /// Addon - private void RenameAddonFolder(AddonJsonModel addon) + private void RenameAddonFolder(AddonManifestJsonModel addon) { ArgumentNullException.ThrowIfNull(PathToAddonFolder); @@ -1085,7 +1085,7 @@ private void RenameAddonFolder(AddonJsonModel addon) } } - private static string GetAddonFullName(AddonJsonModel addon) + private static string GetAddonFullName(AddonManifestJsonModel addon) { StringBuilder version = new(); diff --git a/src/Core.All/Serializable/Addon/AddonJsonModel.cs b/src/Core.All/Serializable/Addon/AddonManifestJsonModel.cs similarity index 88% rename from src/Core.All/Serializable/Addon/AddonJsonModel.cs rename to src/Core.All/Serializable/Addon/AddonManifestJsonModel.cs index a04b6e06..8003ee28 100644 --- a/src/Core.All/Serializable/Addon/AddonJsonModel.cs +++ b/src/Core.All/Serializable/Addon/AddonManifestJsonModel.cs @@ -4,7 +4,7 @@ namespace Core.All.Serializable.Addon; -public sealed class AddonJsonModel +public sealed record AddonManifestJsonModel { [JsonRequired] [JsonPropertyName("id")] @@ -77,7 +77,10 @@ public sealed class AddonJsonModel public List? Options { get; set; } [Obsolete] - public Dictionary? ExecutablesOld { get; } = null; + public Dictionary? ExecutablesOld { get; } + + [JsonIgnore] + public AddonId AddonId => new(Id, Version); } [JsonSourceGenerationOptions( @@ -94,5 +97,6 @@ public sealed class AddonJsonModel DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, RespectNullableAnnotations = true )] -[JsonSerializable(typeof(AddonJsonModel))] -public sealed partial class AddonManifestContext : JsonSerializerContext; +[JsonSerializable(typeof(AddonManifestJsonModel))] +[JsonSerializable(typeof(List))] +public sealed partial class AddonManifestJsonContext : JsonSerializerContext; diff --git a/src/Core.All/Serializable/Addon/DependencyJsonModel.cs b/src/Core.All/Serializable/Addon/DependencyJsonModel.cs index 51af9ff9..526eaaa2 100644 --- a/src/Core.All/Serializable/Addon/DependencyJsonModel.cs +++ b/src/Core.All/Serializable/Addon/DependencyJsonModel.cs @@ -3,7 +3,7 @@ namespace Core.All.Serializable.Addon; -public sealed class DependencyJsonModel +public sealed record DependencyJsonModel { [JsonPropertyName("addons")] public List? Addons { get; set; } @@ -12,15 +12,8 @@ public sealed class DependencyJsonModel public List? RequiredFeatures { get; set; } } -[JsonSourceGenerationOptions( - Converters = [typeof(JsonStringEnumConverter)], - RespectNullableAnnotations = true - )] -[JsonSerializable(typeof(DependencyJsonModel))] -public sealed partial class DependencyDtoContext : JsonSerializerContext; - -public sealed class DependantAddonJsonModel +public sealed record DependantAddonJsonModel { [JsonPropertyName("id")] public required string Id { get; set; } @@ -28,7 +21,3 @@ public sealed class DependantAddonJsonModel [JsonPropertyName("version")] public string? Version { get; set; } } - -[JsonSourceGenerationOptions(RespectNullableAnnotations = true)] -[JsonSerializable(typeof(DependantAddonJsonModel))] -public sealed partial class DependantAddonJsonModelContext : JsonSerializerContext; diff --git a/src/Core.All/Serializable/Addon/ManifestsJsonModel.cs b/src/Core.All/Serializable/Addon/ManifestsJsonModel.cs index 1f854deb..081ddeca 100644 --- a/src/Core.All/Serializable/Addon/ManifestsJsonModel.cs +++ b/src/Core.All/Serializable/Addon/ManifestsJsonModel.cs @@ -1,21 +1,21 @@ -using System.Text.Json.Serialization; -using Core.All.Enums; +//using System.Text.Json.Serialization; +//using Core.All.Enums; -namespace Core.All.Serializable.Addon; +//namespace Core.All.Serializable.Addon; -[JsonSourceGenerationOptions( - Converters = [ - typeof(JsonStringEnumConverter), - typeof(JsonStringEnumConverter), - typeof(JsonStringEnumConverter), - typeof(JsonStringEnumConverter), - typeof(JsonStringEnumConverter) - ], - UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, - AllowTrailingCommas = true, - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - RespectNullableAnnotations = true - )] -[JsonSerializable(typeof(List))] -public sealed partial class ManifestsJsonModelContext : JsonSerializerContext; +//[JsonSourceGenerationOptions( +// Converters = [ +// typeof(JsonStringEnumConverter), +// typeof(JsonStringEnumConverter), +// typeof(JsonStringEnumConverter), +// typeof(JsonStringEnumConverter), +// typeof(JsonStringEnumConverter) +// ], +// UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, +// AllowTrailingCommas = true, +// WriteIndented = true, +// DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, +// RespectNullableAnnotations = true +// )] +//[JsonSerializable(typeof(List))] +//public sealed partial class ManifestsJsonContext : JsonSerializerContext; diff --git a/src/Core.All/Serializable/Addon/MapFileJsonModel.cs b/src/Core.All/Serializable/Addon/MapFileJsonModel.cs index 28f700e4..bad7654e 100644 --- a/src/Core.All/Serializable/Addon/MapFileJsonModel.cs +++ b/src/Core.All/Serializable/Addon/MapFileJsonModel.cs @@ -3,7 +3,7 @@ namespace Core.All.Serializable.Addon; -public sealed class MapFileJsonModel : IStartMap +public sealed record MapFileJsonModel : IStartMap { [JsonPropertyName("file")] public required string File { get; set; } @@ -12,4 +12,4 @@ public sealed class MapFileJsonModel : IStartMap [JsonSourceGenerationOptions(RespectNullableAnnotations = true)] [JsonSerializable(typeof(MapFileJsonModel))] -public sealed partial class MapFileJsonModelContext : JsonSerializerContext; +public sealed partial class MapFileJsonContext : JsonSerializerContext; diff --git a/src/Core.All/Serializable/Addon/MapSlotJsonModel.cs b/src/Core.All/Serializable/Addon/MapSlotJsonModel.cs index e15cfe2c..bc91fc62 100644 --- a/src/Core.All/Serializable/Addon/MapSlotJsonModel.cs +++ b/src/Core.All/Serializable/Addon/MapSlotJsonModel.cs @@ -3,7 +3,7 @@ namespace Core.All.Serializable.Addon; -public sealed class MapSlotJsonModel : IStartMap +public sealed record MapSlotJsonModel : IStartMap { [JsonPropertyName("volume")] public required int Episode { get; set; } @@ -15,4 +15,4 @@ public sealed class MapSlotJsonModel : IStartMap [JsonSourceGenerationOptions(RespectNullableAnnotations = true)] [JsonSerializable(typeof(MapSlotJsonModel))] -public sealed partial class MapSlotJsonModelContext : JsonSerializerContext; +public sealed partial class MapSlotJsonContext : JsonSerializerContext; diff --git a/src/Core.All/Serializable/Addon/OptionJsonModel.cs b/src/Core.All/Serializable/Addon/OptionJsonModel.cs index 9f26f906..9ee88ba3 100644 --- a/src/Core.All/Serializable/Addon/OptionJsonModel.cs +++ b/src/Core.All/Serializable/Addon/OptionJsonModel.cs @@ -3,7 +3,7 @@ namespace Core.All.Serializable.Addon; -public sealed class OptionJsonModel +public sealed record OptionJsonModel { [JsonPropertyName("name")] public string OptionName { get; set; } = string.Empty; diff --git a/src/Core.All/Serializable/Addon/SupportedGameJsonModel.cs b/src/Core.All/Serializable/Addon/SupportedGameJsonModel.cs index cae2890e..47858b39 100644 --- a/src/Core.All/Serializable/Addon/SupportedGameJsonModel.cs +++ b/src/Core.All/Serializable/Addon/SupportedGameJsonModel.cs @@ -3,7 +3,7 @@ namespace Core.All.Serializable.Addon; -public sealed class SupportedGameJsonModel +public sealed record SupportedGameJsonModel { [JsonPropertyName("name")] [JsonConverter(typeof(GameEnumJsonConverter))] @@ -24,4 +24,4 @@ public sealed class SupportedGameJsonModel ] )] [JsonSerializable(typeof(SupportedGameJsonModel))] -public sealed partial class SupportedGameJsonModelContext : JsonSerializerContext; +public sealed partial class SupportedGameJsonContext : JsonSerializerContext; diff --git a/src/Core.All/Serializable/Downloadable/DownloadableAddonJsonModel.cs b/src/Core.All/Serializable/Downloadable/DownloadableAddonJsonModel.cs index 6139f16b..48a2391c 100644 --- a/src/Core.All/Serializable/Downloadable/DownloadableAddonJsonModel.cs +++ b/src/Core.All/Serializable/Downloadable/DownloadableAddonJsonModel.cs @@ -1,7 +1,7 @@ using System.Text; using System.Text.Json.Serialization; -using Core.All.Helpers; using Core.All.Enums; +using Core.All.Helpers; namespace Core.All.Serializable.Downloadable; @@ -113,18 +113,12 @@ public string UpdateDateString var now = DateTime.UtcNow; var span = now - UpdateDate; - if (span.TotalDays < 1) - { - return "Today"; - } - else if (span.TotalDays < 2) + return span.TotalDays switch { - return "Yesterday"; - } - else - { - return $"{(int)span.TotalDays} days ago"; - } + < 1 => "Today", + < 2 => "Yesterday", + _ => $"{(int)span.TotalDays} days ago" + }; } } diff --git a/src/Core.All/Serializable/JsonConverters.cs b/src/Core.All/Serializable/JsonConverters.cs index dcbe934d..dd13c8e3 100644 --- a/src/Core.All/Serializable/JsonConverters.cs +++ b/src/Core.All/Serializable/JsonConverters.cs @@ -12,9 +12,10 @@ public sealed class SupportedGameDtoConverter : JsonConverter { try { - return JsonSerializer.Deserialize(ref reader, MapFileJsonModelContext.Default.MapFileJsonModel); + return JsonSerializer.Deserialize(ref reader, MapFileJsonContext.Default.MapFileJsonModel); } catch { } try { - return JsonSerializer.Deserialize(ref reader, MapSlotJsonModelContext.Default.MapSlotJsonModel); + return JsonSerializer.Deserialize(ref reader, MapSlotJsonContext.Default.MapSlotJsonModel); } catch { } } @@ -98,13 +99,13 @@ public sealed class ExecutablesConverter : JsonConverter logger return null; } - public async Task AddAddonToDatabaseAsync(AddonJsonModel addonJson, DownloadableAddonJsonModel downloadableAddonJson) + public async Task AddAddonToDatabaseAsync(AddonManifestJsonModel addonJson, DownloadableAddonJsonModel downloadableAddonJson) { if (ClientProperties.PathToLocalAddonsJson is null) { @@ -186,13 +186,13 @@ public async Task AddAddonToDatabaseAsync(AddonJsonModel addonJson, Downlo var newAddonsJson = JsonSerializer.Serialize(addons, DownloadableAddonJsonModelDictionaryContext.Default.DictionaryGameEnumListDownloadableAddonJsonModel); await File.WriteAllTextAsync(ClientProperties.PathToLocalAddonsJson, newAddonsJson).ConfigureAwait(false); - List? manifests; + List? manifests; using (var manifestsJson = File.OpenRead(ClientProperties.PathToLocalManifestsJson)) { manifests = await JsonSerializer.DeserializeAsync( manifestsJson, - ManifestsJsonModelContext.Default.ListAddonJsonModel + AddonManifestJsonContext.Default.ListAddonManifestJsonModel ).ConfigureAwait(false); } @@ -205,7 +205,7 @@ public async Task AddAddonToDatabaseAsync(AddonJsonModel addonJson, Downlo manifests.RemoveAll(x => x.Id.Equals(addonJson.Id)); manifests.Add(addonJson); - var newManifestsJson = JsonSerializer.Serialize(manifests, ManifestsJsonModelContext.Default.ListAddonJsonModel); + var newManifestsJson = JsonSerializer.Serialize(manifests, AddonManifestJsonContext.Default.ListAddonManifestJsonModel); await File.WriteAllTextAsync(ClientProperties.PathToLocalManifestsJson, newManifestsJson).ConfigureAwait(false); return true; @@ -235,7 +235,7 @@ public async Task AddAddonToDatabaseAsync(AddonJsonModel addonJson, Downlo } } - public async Task?> GetMetadataAsync() + public async Task?> GetMetadataAsync() { try { @@ -245,7 +245,7 @@ public async Task AddAddonToDatabaseAsync(AddonJsonModel addonJson, Downlo var meta = await JsonSerializer.DeserializeAsync( jsonStream, - ManifestsJsonModelContext.Default.ListAddonJsonModel + AddonManifestJsonContext.Default.ListAddonManifestJsonModel ).ConfigureAwait(false); return meta; diff --git a/src/Core.Client/Api/OfflineApiInterface.cs b/src/Core.Client/Api/OfflineApiInterface.cs index a5966856..39d78310 100644 --- a/src/Core.Client/Api/OfflineApiInterface.cs +++ b/src/Core.Client/Api/OfflineApiInterface.cs @@ -83,7 +83,7 @@ public OfflineApiInterface(ILogger logger) public Task GetLatestToolReleaseAsync(ToolEnum toolEnum) => Task.FromResult(null); - public Task AddAddonToDatabaseAsync(AddonJsonModel addonJson, DownloadableAddonJsonModel downloadableAddonJson) => Task.FromResult(false); + public Task AddAddonToDatabaseAsync(AddonManifestJsonModel addonJson, DownloadableAddonJsonModel downloadableAddonJson) => Task.FromResult(false); public async Task GetUploadFolderAsync() { @@ -98,12 +98,12 @@ public OfflineApiInterface(ILogger logger) return uploadFolder; } - public async Task?> GetMetadataAsync() + public async Task?> GetMetadataAsync() { using var dataJson = File.OpenRead(ClientProperties.PathToLocalManifestsJson); var data = await JsonSerializer.DeserializeAsync( dataJson, - ManifestsJsonModelContext.Default.ListAddonJsonModel + AddonManifestJsonContext.Default.ListAddonManifestJsonModel ).ConfigureAwait(false); return data; diff --git a/src/Core.Client/Helpers/ManifestHelper.cs b/src/Core.Client/Helpers/ManifestHelper.cs index 7463d39e..69308eea 100644 --- a/src/Core.Client/Helpers/ManifestHelper.cs +++ b/src/Core.Client/Helpers/ManifestHelper.cs @@ -7,7 +7,7 @@ namespace Core.Client.Helpers; public static class ManifestHelper { - public static async Task> GetMainManifestAsync(string pathToFile) + public static async Task> GetMainManifestAsync(string pathToFile) { using var archive = ZipArchive.OpenArchive(pathToFile); var addonJson = archive.Entries.FirstOrDefault(static x => x.Key!.Equals("addon.json", StringComparison.OrdinalIgnoreCase)); @@ -18,7 +18,7 @@ public static class ManifestHelper } using var stream = await addonJson.OpenEntryStreamAsync().ConfigureAwait(false); - var manifest = await JsonSerializer.DeserializeAsync(stream, AddonManifestContext.Default.AddonJsonModel).ConfigureAwait(false); + var manifest = await JsonSerializer.DeserializeAsync(stream, AddonManifestJsonContext.Default.AddonManifestJsonModel).ConfigureAwait(false); return manifest is null ? new(ResultEnum.Error, null, "Error while deserializing addon.json.") diff --git a/src/Core.Client/Helpers/UriHelper.cs b/src/Core.Client/Helpers/UriHelper.cs index 92fb0386..269dd5b9 100644 --- a/src/Core.Client/Helpers/UriHelper.cs +++ b/src/Core.Client/Helpers/UriHelper.cs @@ -5,7 +5,7 @@ namespace Core.Client.Helpers; public static class UriHelper { - public static string GetRelativeFilePath(AddonJsonModel manifest, string pathToFile) + public static string GetRelativeFilePath(AddonManifestJsonModel manifest, string pathToFile) { var folderName = manifest.AddonType switch { diff --git a/src/Core.Client/Interfaces/IApiInterface.cs b/src/Core.Client/Interfaces/IApiInterface.cs index 26e14f29..ceb77308 100644 --- a/src/Core.Client/Interfaces/IApiInterface.cs +++ b/src/Core.Client/Interfaces/IApiInterface.cs @@ -7,7 +7,7 @@ namespace Core.Client.Interfaces; public interface IApiInterface { - Task AddAddonToDatabaseAsync(AddonJsonModel addonJson, DownloadableAddonJsonModel downloadableAddonJson); + Task AddAddonToDatabaseAsync(AddonManifestJsonModel addonJson, DownloadableAddonJsonModel downloadableAddonJson); Task ChangeScoreAsync(string addonId, sbyte score, bool isNew); Task?> GetAddonsAsync(GameEnum gameEnum); Task GetLatestAppReleaseAsync(); @@ -16,6 +16,6 @@ public interface IApiInterface Task?> GetRatingsAsync(); Task> GetSignedUrlAsync(string path); Task GetUploadFolderAsync(); - Task?> GetMetadataAsync(); + Task?> GetMetadataAsync(); Task IncreaseNumberOfInstallsAsync(string addonId); } diff --git a/src/Core.Client/Providers/MetadataProvider.cs b/src/Core.Client/Providers/MetadataProvider.cs index 7cd2e9ba..13b39f13 100644 --- a/src/Core.Client/Providers/MetadataProvider.cs +++ b/src/Core.Client/Providers/MetadataProvider.cs @@ -18,7 +18,7 @@ public sealed class MetadataProvider private readonly IApiInterface _apiInterface; private readonly ILogger _logger; - private readonly Dictionary> _updatesCache = []; + private readonly Dictionary> _updatesCache = []; public MetadataProvider( IApiInterface apiInterface, @@ -56,7 +56,7 @@ public async Task InitializeAsync() using var stream = await manifest.OpenEntryStreamAsync().ConfigureAwait(false); var originalManifest = await JsonSerializer.DeserializeAsync( stream, - AddonManifestContext.Default.AddonJsonModel + AddonManifestJsonContext.Default.AddonManifestJsonModel ).ConfigureAwait(false); if (originalManifest is null) @@ -72,7 +72,7 @@ public async Task InitializeAsync() using var originalManifestStr = File.OpenRead(file); var originalManifest = await JsonSerializer.DeserializeAsync( originalManifestStr, - AddonManifestContext.Default.AddonJsonModel + AddonManifestJsonContext.Default.AddonManifestJsonModel ).ConfigureAwait(false); if (originalManifest is null) @@ -124,7 +124,7 @@ public async Task> UpdateMetadataAsync(string path) var ms = new MemoryStream(); streams.Add(ms); - await JsonSerializer.SerializeAsync(ms, update.Value, AddonManifestContext.Default.AddonJsonModel).ConfigureAwait(false); + await JsonSerializer.SerializeAsync(ms, update.Value, AddonManifestJsonContext.Default.AddonManifestJsonModel).ConfigureAwait(false); archive.AddEntry(update.Key, ms); } @@ -147,7 +147,7 @@ public async Task> UpdateMetadataAsync(string path) else if (path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) { File.Delete(path); - var addonJson = JsonSerializer.Serialize(updates.First().Value, AddonManifestContext.Default.AddonJsonModel); + var addonJson = JsonSerializer.Serialize(updates.First().Value, AddonManifestJsonContext.Default.AddonManifestJsonModel); await File.WriteAllTextAsync(path, addonJson).ConfigureAwait(false); MetadataUpdatedEvent?.Invoke(this, new( @@ -165,15 +165,15 @@ public async Task> UpdateMetadataAsync(string path) return new(ResultEnum.Success, false, string.Empty); } - private void AddToCacheIfNewer(Dictionary metaDict, string file, AddonJsonModel originalManifest, string jsonName) + private void AddToCacheIfNewer(Dictionary metaDict, string file, AddonManifestJsonModel originalManifest, string jsonName) { if (!metaDict.TryGetValue(new(originalManifest.Id, originalManifest.Version), out var actualVersion)) { return; } - var newManifestStr = JsonSerializer.Serialize(actualVersion, AddonManifestContext.Default.AddonJsonModel); - var originalManifestStr = JsonSerializer.Serialize(originalManifest, AddonManifestContext.Default.AddonJsonModel); + var newManifestStr = JsonSerializer.Serialize(actualVersion, AddonManifestJsonContext.Default.AddonManifestJsonModel); + var originalManifestStr = JsonSerializer.Serialize(originalManifest, AddonManifestJsonContext.Default.AddonManifestJsonModel); if (!originalManifestStr.Equals(newManifestStr)) { diff --git a/src/Core.Client/Tools/AddonsDatabaseManager.cs b/src/Core.Client/Tools/AddonsDatabaseManager.cs index ec746443..a8ea22b8 100644 --- a/src/Core.Client/Tools/AddonsDatabaseManager.cs +++ b/src/Core.Client/Tools/AddonsDatabaseManager.cs @@ -18,7 +18,7 @@ public AddonsDatabaseManager(IApiInterface apiInterface, ILogger AddToDatabaseAsync(string pathToFile, Uri downloadUrl, AddonJsonModel manifest) + public async Task AddToDatabaseAsync(string pathToFile, Uri downloadUrl, AddonManifestJsonModel manifest) { var downloadAddonEntity = await GetDownloadableAddonDtoAsync(pathToFile, downloadUrl, manifest).ConfigureAwait(false); var dbResult = await _apiInterface.AddAddonToDatabaseAsync(manifest!, downloadAddonEntity).ConfigureAwait(false); @@ -26,7 +26,7 @@ public async Task AddToDatabaseAsync(string pathToFile, Uri downloadUrl, return new(dbResult ? ResultEnum.Success : ResultEnum.Error, dbResult ? string.Empty : "Error while adding addon to the database."); } - private static async Task GetDownloadableAddonDtoAsync(string pathToFile, Uri downloadUrl, AddonJsonModel manifest) + private static async Task GetDownloadableAddonDtoAsync(string pathToFile, Uri downloadUrl, AddonManifestJsonModel manifest) { FileInfo fileInfo = new(pathToFile); using var fileStream = File.OpenRead(pathToFile); @@ -52,4 +52,4 @@ private static async Task GetDownloadableAddonDtoAsy Sha256 = Convert.ToHexString(sha) }; } -} \ No newline at end of file +} diff --git a/src/Tests.Database/AddonsDatabaseTests.cs b/src/Tests.Database/AddonsDatabaseTests.cs index 77d2ce98..166d9648 100644 --- a/src/Tests.Database/AddonsDatabaseTests.cs +++ b/src/Tests.Database/AddonsDatabaseTests.cs @@ -153,7 +153,7 @@ public async Task UploadAddonTest() public async Task ManifestsJsonTest() { var manifestsJsonString = await File.ReadAllTextAsync(ClientProperties.PathToLocalManifestsJson); - var manifests = JsonSerializer.Deserialize(manifestsJsonString, ManifestsJsonModelContext.Default.ListAddonJsonModel); + var manifests = JsonSerializer.Deserialize(manifestsJsonString, AddonManifestJsonContext.Default.ListAddonManifestJsonModel); Assert.NotNull(manifests); } diff --git a/src/Tests.Unit/SerializeTests.cs b/src/Tests.Unit/SerializeTests.cs deleted file mode 100644 index 46d1b84c..00000000 --- a/src/Tests.Unit/SerializeTests.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System.Text.Json; -using Core.All.Enums; -using Core.All.Serializable.Addon; - -namespace Tests.Unit; - -public sealed class SerializerTests -{ - private const string AddonJson = -""" - { - "id": "addon-id", - "type": "mod", - "game": - { - "name": "shadowwarrior", - "version": "1.3d", - "crc": "0x982AFE4A" - }, - "title": "Addon Title", - "author": "Author", - "release_date": "1991-06-10", - "version": "1.0", - "con_main": "MAIN.CON", - "con_modules": [ "MODULE.CON", "MODULE2.CON" ], - "def_main": "MAIN.DEF", - "def_modules": [ "MODULE.DEF" ], - "rts": "MAIN.RTS", - "ini": "MAIN.INI", - "rff_main": "MAIN.RFF", - "rff_sound": "SOUND.RFF", - "dependencies": - { - "addons": - [ - { "id": "Addon1" }, - { "id": "Addon2", "version": "1.0" } - ], - "features": - [ - "eduke32_con", - "tror" - ], - }, - "incompatibles": - { - "addons": - [ - { "id": "IncompatibleAddon1" }, - { "id": "IncompatibleAddon2", "version": "1.1" } - ] - }, - "description": "Addon description", - "startmap": { "file": "TEST.MAP" }, - "options": - [ - { - "name": "option 1", - "parameters": { - "opt1.def": "DEF" - } - }, - { - "name": "option 2", - "parameters": { - "opt2.def": "DEF", - "opt2_2.def": "DEF" - } - }, - ] - } -"""; - - private const string BrokenAddonJson = -""" - { - "id": "addon-id", - "type": "mod", - "game": - { - "name": "shadowwarrior", - "version": "1.3d", - "crc": "0x982AFE4A", - "unknown_token": "123" - }, - "title": "Addon Title", - "author": "Author", - "version": "1.0" - } -"""; - - private const string SlotMapJson = -""" - { - "id": "addon-id", - "type": "Map", - "game": { - "name": "Duke3D" - }, - "title": "Addon Title", - "version": "1.0", - "author": "Author", - "startmap": { - "volume": 1, - "level": 2 - } - } -"""; - - private const string StandaloneJsonOld = -""" - { - "id": "amc-squad", - "type": "TC", - "game": { - "name": "Standalone" - }, - "title": "AMC Squad", - "version": "4.5.2", - "author": "AMCSquad", - "description": "---", - "executables": { - "Windows": "amcsquad.exe", - "Linux": "amcsquad" - } - } -"""; - - private const string StandaloneJson = -""" - { - "id": "game-id", - "type": "TC", - "game": { - "name": "Standalone" - }, - "title": "Standalone Game", - "version": "1.0", - "author": "Author", - "executables": { - "Windows": { - "EDuke32": "eduke32.exe" - }, - "Linux": { - "EDuke32": "eduke32" - }, - } - } -"""; - - [Fact] - public void DeserializeAddonJson() - { - var result = JsonSerializer.Deserialize(AddonJson, AddonManifestContext.Default.AddonJsonModel); - - Assert.NotNull(result); - - Assert.Equal(AddonTypeEnum.Mod, result.AddonType); - Assert.Equal("addon-id", result.Id); - - Assert.Equal(GameEnum.Wang, result.SupportedGame.Game); - Assert.Equal("1.3d", result.SupportedGame.Version); - Assert.Equal("0x982AFE4A", result.SupportedGame.Crc); - - Assert.Equal("Addon Title", result.Title); - Assert.Equal("Author", result.Author); - Assert.Equal("1.0", result.Version); - - Assert.Equal("MAIN.CON", result.MainCon); - Assert.Contains("MODULE.CON", result.AdditionalCons!); - Assert.Contains("MODULE2.CON", result.AdditionalCons!); - - Assert.Equal("MAIN.DEF", result.MainDef); - Assert.Contains("MODULE.DEF", result.AdditionalDefs!); - - var depsIds = result.Dependencies!.Addons!.Select(x => x.Id); - Assert.Contains("Addon1", depsIds); - Assert.Contains("Addon2", depsIds); - Assert.Equal("1.0", result.Dependencies!.Addons![^1].Version); - - var incompIds = result.Incompatibles!.Addons!.Select(x => x.Id); - Assert.Contains("IncompatibleAddon1", incompIds); - Assert.Contains("IncompatibleAddon2", incompIds); - Assert.Equal("1.1", result.Incompatibles!.Addons![^1].Version); - - var depsFeatures = result.Dependencies!.RequiredFeatures!; - Assert.Contains(FeatureEnum.EDuke32_CON, depsFeatures); - Assert.Contains(FeatureEnum.TROR, depsFeatures); - - Assert.Equal("MAIN.RTS", result.Rts); - Assert.Equal("MAIN.INI", result.Ini); - Assert.Equal("MAIN.RFF", result.MainRff); - Assert.Equal("SOUND.RFF", result.SoundRff); - - Assert.Equal("TEST.MAP", ((MapFileJsonModel)result.StartMap!).File); - - Assert.Equal("Addon description", result.Description); - - Assert.Equal(DateOnly.Parse("1991-06-10"), result.ReleaseDate); - } - - [Fact] - public void DeserializeBrokenAddonJson() - { - AddonJsonModel? result = null; - - try - { - result = JsonSerializer.Deserialize(BrokenAddonJson, AddonManifestContext.Default.AddonJsonModel); - } - catch (JsonException ex) - { - Assert.Contains("unknown_token", ex.Message); - } - - Assert.Null(result); - } - - [Fact] - public void DeserializeSlotMapJson() - { - var result = JsonSerializer.Deserialize(SlotMapJson, AddonManifestContext.Default.AddonJsonModel); - - Assert.NotNull(result); - _ = Assert.IsType(result.StartMap); - - Assert.Equal(1, ((MapSlotJsonModel)result.StartMap).Episode); - Assert.Equal(2, ((MapSlotJsonModel)result.StartMap).Level); - } - - [Fact] - public void DeserializeStandaloneJsonOld() - { - var result = JsonSerializer.Deserialize(StandaloneJsonOld, AddonManifestContext.Default.AddonJsonModel); - - Assert.NotNull(result); - - Assert.Equal(AddonTypeEnum.TC, result.AddonType); - Assert.Equal(GameEnum.Standalone, result.SupportedGame.Game); - Assert.Equal("AMC Squad", result.Title); - Assert.Equal("amcsquad.exe", result.Executables?[OSEnum.Windows]?[PortEnum.Stub]); - Assert.Equal("amcsquad", result.Executables?[OSEnum.Linux]?[PortEnum.Stub]); - } - - [Fact] - public void DeserializeStandaloneJson() - { - var result = JsonSerializer.Deserialize(StandaloneJson, AddonManifestContext.Default.AddonJsonModel); - - Assert.NotNull(result); - - Assert.Equal(AddonTypeEnum.TC, result.AddonType); - Assert.Equal(GameEnum.Standalone, result.SupportedGame.Game); - Assert.Equal("Standalone Game", result.Title); - Assert.Equal("eduke32.exe", result.Executables?[OSEnum.Windows]?[PortEnum.EDuke32]); - Assert.Equal("eduke32", result.Executables?[OSEnum.Linux]?[PortEnum.EDuke32]); - } -} diff --git a/src/Tests.Unit/SerializerTests.cs b/src/Tests.Unit/SerializerTests.cs new file mode 100644 index 00000000..83e4107c --- /dev/null +++ b/src/Tests.Unit/SerializerTests.cs @@ -0,0 +1,571 @@ +using System.Text.Json; +using Core.All.Enums; +using Core.All.Serializable.Addon; + +namespace Tests.Unit; + +public sealed class SerializerTests +{ + private const string AddonJson = +""" + { + "id": "addon-id", + "type": "mod", + "game": + { + "name": "shadowwarrior", + "version": "1.3d", + "crc": "0x982AFE4A" + }, + "title": "Addon Title", + "author": "Author", + "release_date": "1991-06-10", + "version": "1.0", + "con_main": "MAIN.CON", + "con_modules": [ "MODULE.CON", "MODULE2.CON" ], + "def_main": "MAIN.DEF", + "def_modules": [ "MODULE.DEF" ], + "rts": "MAIN.RTS", + "ini": "MAIN.INI", + "rff_main": "MAIN.RFF", + "rff_sound": "SOUND.RFF", + "dependencies": + { + "addons": + [ + { "id": "Addon1" }, + { "id": "Addon2", "version": "1.0" } + ], + "features": + [ + "eduke32_con", + "tror" + ], + }, + "incompatibles": + { + "addons": + [ + { "id": "IncompatibleAddon1" }, + { "id": "IncompatibleAddon2", "version": "1.1" } + ] + }, + "description": "Addon description", + "startmap": { "file": "TEST.MAP" }, + "options": + [ + { + "name": "option 1", + "parameters": { + "opt1.def": "DEF" + } + }, + { + "name": "option 2", + "parameters": { + "opt2.def": "DEF", + "opt2_2.def": "DEF" + } + }, + ] + } +"""; + + private const string MinimalAddonJson = +""" + { + "id": "minimal-id", + "type": "mod", + "game": { "name": "duke3d" }, + "title": "Minimal Addon", + "version": "0.1" + } +"""; + + private const string OfficialAddonJson = +""" + { + "id": "duke1", + "type": "official", + "game": { "name": "duke3d" }, + "title": "Duke Nukem Ep1", + "version": "1.0", + "author": "3D Realms", + "description": "Shareware episode" + } +"""; + + private const string BrokenAddonJson = +""" + { + "id": "addon-id", + "type": "mod", + "game": + { + "name": "shadowwarrior", + "version": "1.3d", + "crc": "0x982AFE4A", + "unknown_token": "123" + }, + "title": "Addon Title", + "author": "Author", + "version": "1.0" + } +"""; + + private const string SlotMapJson = +""" + { + "id": "addon-id", + "type": "Map", + "game": { + "name": "Duke3D" + }, + "title": "Addon Title", + "version": "1.0", + "author": "Author", + "startmap": { + "volume": 1, + "level": 2 + } + } +"""; + + private const string NoStartmapJson = +""" + { + "id": "no-startmap", + "type": "TC", + "game": { "name": "Blood" }, + "title": "No Startmap", + "version": "2.0", + "author": "Author" + } +"""; + + private const string ExhumedGameJson = +""" + { + "id": "exhumed-addon", + "type": "mod", + "game": { "name": "Exhumed" }, + "title": "Exhumed Mod", + "version": "1.0" + } +"""; + + private const string IniOptionsJson = +""" + { + "id": "ini-options", + "type": "mod", + "game": { "name": "duke3d" }, + "title": "Ini Options Test", + "version": "1.0", + "options": [ + { + "name": "widescreen", + "parameters": { + "widescreen.ini": "INI" + } + } + ] + } +"""; + + private const string AllFeaturesJson = +""" + { + "id": "all-features", + "type": "mod", + "game": { "name": "duke3d" }, + "title": "All Features", + "version": "1.0", + "dependencies": { + "features": [ + "eduke32_con", "Hightile", "Models", "Sloped_Sprites", + "tror", "Wall_Rotate_Cstat", "Dynamic_Lighting", + "Modern_Types", "SndInfo", "TileFromTexture" + ] + } + } +"""; + + private const string StandaloneJsonOld = +""" + { + "id": "amc-squad", + "type": "TC", + "game": { + "name": "Standalone" + }, + "title": "AMC Squad", + "version": "4.5.2", + "author": "AMCSquad", + "description": "---", + "executables": { + "Windows": "amcsquad.exe", + "Linux": "amcsquad" + } + } +"""; + + private const string StandaloneJson = +""" + { + "id": "game-id", + "type": "TC", + "game": { + "name": "Standalone" + }, + "title": "Standalone Game", + "version": "1.0", + "author": "Author", + "executables": { + "Windows": { + "EDuke32": "eduke32.exe" + }, + "Linux": { + "EDuke32": "eduke32" + }, + } + } +"""; + + private const string EmptyListsJson = +""" + { + "id": "empty-lists", + "type": "mod", + "game": { "name": "duke3d" }, + "title": "Empty Lists", + "version": "1.0", + "con_modules": [], + "def_modules": [] + } +"""; + + [Fact] + public void DeserializeAddonJson() + { + var result = JsonSerializer.Deserialize(AddonJson, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + Assert.NotNull(result); + + Assert.Equal(AddonTypeEnum.Mod, result.AddonType); + Assert.Equal("addon-id", result.Id); + + Assert.Equal(GameEnum.Wang, result.SupportedGame.Game); + Assert.Equal("1.3d", result.SupportedGame.Version); + Assert.Equal("0x982AFE4A", result.SupportedGame.Crc); + + Assert.Equal("Addon Title", result.Title); + Assert.Equal("Author", result.Author); + Assert.Equal("1.0", result.Version); + + Assert.Equal("MAIN.CON", result.MainCon); + Assert.Contains("MODULE.CON", result.AdditionalCons!); + Assert.Contains("MODULE2.CON", result.AdditionalCons!); + + Assert.Equal("MAIN.DEF", result.MainDef); + Assert.Contains("MODULE.DEF", result.AdditionalDefs!); + + var depsIds = result.Dependencies!.Addons!.Select(x => x.Id); + Assert.Contains("Addon1", depsIds); + Assert.Contains("Addon2", depsIds); + Assert.Equal("1.0", result.Dependencies!.Addons![^1].Version); + + var incompIds = result.Incompatibles!.Addons!.Select(x => x.Id); + Assert.Contains("IncompatibleAddon1", incompIds); + Assert.Contains("IncompatibleAddon2", incompIds); + Assert.Equal("1.1", result.Incompatibles!.Addons![^1].Version); + + var depsFeatures = result.Dependencies!.RequiredFeatures!; + Assert.Contains(FeatureEnum.EDuke32_CON, depsFeatures); + Assert.Contains(FeatureEnum.TROR, depsFeatures); + + Assert.Equal("MAIN.RTS", result.Rts); + Assert.Equal("MAIN.INI", result.Ini); + Assert.Equal("MAIN.RFF", result.MainRff); + Assert.Equal("SOUND.RFF", result.SoundRff); + + Assert.Equal("TEST.MAP", ((MapFileJsonModel)result.StartMap!).File); + + Assert.Equal("Addon description", result.Description); + + Assert.Equal(DateOnly.Parse("1991-06-10"), result.ReleaseDate); + } + + [Fact] + public void DeserializeMinimalAddon() + { + var result = JsonSerializer.Deserialize(MinimalAddonJson, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + Assert.NotNull(result); + Assert.Equal("minimal-id", result.Id); + Assert.Equal(AddonTypeEnum.Mod, result.AddonType); + Assert.Equal(GameEnum.Duke3D, result.SupportedGame.Game); + Assert.Equal("Minimal Addon", result.Title); + Assert.Equal("0.1", result.Version); + + Assert.Null(result.Author); + Assert.Null(result.ReleaseDate); + Assert.Null(result.MainCon); + Assert.Null(result.AdditionalCons); + Assert.Null(result.MainDef); + Assert.Null(result.AdditionalDefs); + Assert.Null(result.Rts); + Assert.Null(result.Ini); + Assert.Null(result.MainRff); + Assert.Null(result.SoundRff); + Assert.Null(result.Dependencies); + Assert.Null(result.Incompatibles); + Assert.Null(result.StartMap); + Assert.Null(result.Description); + Assert.Null(result.Executables); + Assert.Null(result.Options); + } + + [Fact] + public void DeserializeOfficialAddon() + { + var result = JsonSerializer.Deserialize(OfficialAddonJson, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + Assert.NotNull(result); + Assert.Equal("duke1", result.Id); + Assert.Equal(AddonTypeEnum.Official, result.AddonType); + Assert.Equal(GameEnum.Duke3D, result.SupportedGame.Game); + Assert.Equal("Duke Nukem Ep1", result.Title); + Assert.Equal("1.0", result.Version); + Assert.Equal("3D Realms", result.Author); + Assert.Equal("Shareware episode", result.Description); + + Assert.Null(result.StartMap); + Assert.Null(result.AdditionalCons); + } + + [Fact] + public void DeserializeExhumedGame() + { + var result = JsonSerializer.Deserialize(ExhumedGameJson, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + Assert.NotNull(result); + Assert.Equal(GameEnum.Slave, result.SupportedGame.Game); + } + + [Fact] + public void DeserializeBrokenAddonJson_Throws() + { + var ex = Assert.Throws(() => + JsonSerializer.Deserialize(BrokenAddonJson, AddonManifestJsonContext.Default.AddonManifestJsonModel)); + + Assert.Contains("unknown_token", ex.Message); + } + + [Fact] + public void DeserializeSlotMapJson() + { + var result = JsonSerializer.Deserialize(SlotMapJson, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + Assert.NotNull(result); + Assert.Equal(AddonTypeEnum.Map, result.AddonType); + _ = Assert.IsType(result.StartMap); + + Assert.Equal(1, ((MapSlotJsonModel)result.StartMap).Episode); + Assert.Equal(2, ((MapSlotJsonModel)result.StartMap).Level); + } + + [Fact] + public void DeserializeAddonWithoutStartmap() + { + var result = JsonSerializer.Deserialize(NoStartmapJson, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + Assert.NotNull(result); + Assert.Equal("no-startmap", result.Id); + Assert.Equal(GameEnum.Blood, result.SupportedGame.Game); + Assert.Equal(AddonTypeEnum.TC, result.AddonType); + Assert.Null(result.StartMap); + } + + [Fact] + public void DeserializeAddonWithIniOptions() + { + var result = JsonSerializer.Deserialize(IniOptionsJson, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + Assert.NotNull(result); + Assert.NotNull(result.Options); + var option = Assert.Single(result.Options); + Assert.Equal("widescreen", option.OptionName); + Assert.NotNull(option.Parameters); + Assert.Equal(OptionalParameterTypeEnum.INI, Assert.Single(option.Parameters.Values)); + } + + [Fact] + public void DeserializeAddonWithAllFeatures() + { + var result = JsonSerializer.Deserialize(AllFeaturesJson, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + Assert.NotNull(result); + Assert.NotNull(result.Dependencies); + Assert.NotNull(result.Dependencies.RequiredFeatures); + + Assert.Equal(10, result.Dependencies.RequiredFeatures.Count); + Assert.Contains(FeatureEnum.EDuke32_CON, result.Dependencies.RequiredFeatures); + Assert.Contains(FeatureEnum.Hightile, result.Dependencies.RequiredFeatures); + Assert.Contains(FeatureEnum.Models, result.Dependencies.RequiredFeatures); + Assert.Contains(FeatureEnum.Sloped_Sprites, result.Dependencies.RequiredFeatures); + Assert.Contains(FeatureEnum.TROR, result.Dependencies.RequiredFeatures); + Assert.Contains(FeatureEnum.Wall_Rotate_Cstat, result.Dependencies.RequiredFeatures); + Assert.Contains(FeatureEnum.Dynamic_Lighting, result.Dependencies.RequiredFeatures); + Assert.Contains(FeatureEnum.Modern_Types, result.Dependencies.RequiredFeatures); + Assert.Contains(FeatureEnum.SndInfo, result.Dependencies.RequiredFeatures); + Assert.Contains(FeatureEnum.TileFromTexture, result.Dependencies.RequiredFeatures); + } + + [Fact] + public void DeserializeEmptyLists() + { + var result = JsonSerializer.Deserialize(EmptyListsJson, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + Assert.NotNull(result); + Assert.Empty(result.AdditionalCons!); + Assert.Empty(result.AdditionalDefs!); + } + + [Fact] + public void DeserializeStandaloneJsonOld() + { + var result = JsonSerializer.Deserialize(StandaloneJsonOld, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + Assert.NotNull(result); + + Assert.Equal(AddonTypeEnum.TC, result.AddonType); + Assert.Equal(GameEnum.Standalone, result.SupportedGame.Game); + Assert.Equal("AMC Squad", result.Title); + Assert.Equal("amcsquad.exe", result.Executables?[OSEnum.Windows]?[PortEnum.Stub]); + Assert.Equal("amcsquad", result.Executables?[OSEnum.Linux]?[PortEnum.Stub]); + } + + [Fact] + public void DeserializeStandaloneJson() + { + var result = JsonSerializer.Deserialize(StandaloneJson, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + Assert.NotNull(result); + + Assert.Equal(AddonTypeEnum.TC, result.AddonType); + Assert.Equal(GameEnum.Standalone, result.SupportedGame.Game); + Assert.Equal("Standalone Game", result.Title); + Assert.Equal("eduke32.exe", result.Executables?[OSEnum.Windows]?[PortEnum.EDuke32]); + Assert.Equal("eduke32", result.Executables?[OSEnum.Linux]?[PortEnum.EDuke32]); + } + + [Fact] + public void SerializeThenDeserialize_RoundTrips() + { + var original = JsonSerializer.Deserialize(AddonJson, AddonManifestJsonContext.Default.AddonManifestJsonModel); + Assert.NotNull(original); + + var serialized = JsonSerializer.Serialize(original, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + var deserialized = JsonSerializer.Deserialize(serialized, AddonManifestJsonContext.Default.AddonManifestJsonModel); + Assert.NotNull(deserialized); + + Assert.Equal(original.Id, deserialized.Id); + Assert.Equal(original.AddonType, deserialized.AddonType); + Assert.Equal(original.SupportedGame.Game, deserialized.SupportedGame.Game); + Assert.Equal(original.Title, deserialized.Title); + Assert.Equal(original.Version, deserialized.Version); + Assert.Equal(original.MainCon, deserialized.MainCon); + Assert.Equal(original.MainDef, deserialized.MainDef); + Assert.Equal(original.Rts, deserialized.Rts); + Assert.Equal(original.Ini, deserialized.Ini); + Assert.Equal(original.MainRff, deserialized.MainRff); + Assert.Equal(original.SoundRff, deserialized.SoundRff); + Assert.Equal(original.Description, deserialized.Description); + Assert.Equal(original.Author, deserialized.Author); + Assert.Equal(original.ReleaseDate, deserialized.ReleaseDate); + } + + [Fact] + public void SerializeMinimalAddon_RoundTrips() + { + var original = JsonSerializer.Deserialize(MinimalAddonJson, AddonManifestJsonContext.Default.AddonManifestJsonModel); + Assert.NotNull(original); + + var serialized = JsonSerializer.Serialize(original, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + var deserialized = JsonSerializer.Deserialize(serialized, AddonManifestJsonContext.Default.AddonManifestJsonModel); + Assert.NotNull(deserialized); + + Assert.Equal(original.Id, deserialized.Id); + Assert.Equal(original.Title, deserialized.Title); + Assert.Equal(original.Version, deserialized.Version); + Assert.Equal(original.AddonType, deserialized.AddonType); + Assert.Equal(original.SupportedGame.Game, deserialized.SupportedGame.Game); + } + + [Fact] + public void DeserializeAllAddonTypes() + { + Assert.Equal(AddonTypeEnum.Official, DeserializeType("official")); + Assert.Equal(AddonTypeEnum.TC, DeserializeType("TC")); + Assert.Equal(AddonTypeEnum.Map, DeserializeType("Map")); + Assert.Equal(AddonTypeEnum.Mod, DeserializeType("mod")); + + static AddonTypeEnum DeserializeType(string typeName) + { + var json = $$""" + { + "id": "test-{{typeName}}", + "type": "{{typeName}}", + "game": { "name": "duke3d" }, + "title": "Test", + "version": "1.0" + } + """; + var result = JsonSerializer.Deserialize(json, AddonManifestJsonContext.Default.AddonManifestJsonModel); + Assert.NotNull(result); + return result.AddonType; + } + } + + [Fact] + public void DeserializeAllGameTypes() + { + var games = new Dictionary + { + ["duke3d"] = GameEnum.Duke3D, + ["Duke64"] = GameEnum.Duke64, + ["blood"] = GameEnum.Blood, + ["ShadowWarrior"] = GameEnum.Wang, + ["fury"] = GameEnum.Fury, + ["Exhumed"] = GameEnum.Slave, + ["nam"] = GameEnum.NAM, + ["ww2gi"] = GameEnum.WW2GI, + ["redneck"] = GameEnum.Redneck, + ["ridesagain"] = GameEnum.RidesAgain, + ["tekwar"] = GameEnum.TekWar, + ["witchaven"] = GameEnum.Witchaven, + ["witchaven2"] = GameEnum.Witchaven2, + ["standalone"] = GameEnum.Standalone, + ["DukeZeroHour"] = GameEnum.DukeZeroHour, + }; + + foreach (var (name, expected) in games) + { + var json = $$""" + { + "id": "test-{{name}}", + "type": "mod", + "game": { "name": "{{name}}" }, + "title": "Game {{name}}", + "version": "1.0" + } + """; + var result = JsonSerializer.Deserialize(json, AddonManifestJsonContext.Default.AddonManifestJsonModel); + Assert.NotNull(result); + Assert.Equal(expected, result.SupportedGame.Game); + } + } +} From d918d76b77e555d594d276545ce5126b9243c470 Mon Sep 17 00:00:00 2001 From: fgsfds <4870330+fgsfds@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:47:24 +0500 Subject: [PATCH 3/7] renamed GameStruct to GameInfo --- src/Addons/Addons/BaseAddon.cs | 2 +- src/Core.All/{GameStruct.cs => GameInfo.cs} | 16 ++++++++-------- .../CmdArguments/AutoloadModsProvider.cs | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) rename src/Core.All/{GameStruct.cs => GameInfo.cs} (51%) diff --git a/src/Addons/Addons/BaseAddon.cs b/src/Addons/Addons/BaseAddon.cs index 91431822..aca960e7 100644 --- a/src/Addons/Addons/BaseAddon.cs +++ b/src/Addons/Addons/BaseAddon.cs @@ -24,7 +24,7 @@ public abstract class BaseAddon /// /// List of supported games /// - public required GameStruct SupportedGame { get; init; } + public required GameInfo SupportedGame { get; init; } /// /// Name of the addon diff --git a/src/Core.All/GameStruct.cs b/src/Core.All/GameInfo.cs similarity index 51% rename from src/Core.All/GameStruct.cs rename to src/Core.All/GameInfo.cs index 54ddddb0..1d2a2d31 100644 --- a/src/Core.All/GameStruct.cs +++ b/src/Core.All/GameInfo.cs @@ -3,14 +3,14 @@ namespace Core.All; -public readonly struct GameStruct +public readonly struct GameInfo { - public GameEnum GameEnum { get; } - public string? GameVersion { get; } - public string? GameCrc { get; } + public required GameEnum GameEnum { get; init; } + public required string? GameVersion { get; init; } + public required string? GameCrc { get; init; } [SetsRequiredMembers] - public GameStruct(GameEnum gameEnum) + public GameInfo(GameEnum gameEnum) { GameEnum = gameEnum; GameVersion = null; @@ -18,15 +18,15 @@ public GameStruct(GameEnum gameEnum) } [SetsRequiredMembers] - public GameStruct(GameEnum gameEnum, Enum? gameVersion) + public GameInfo(GameEnum gameEnum, Enum gameVersion) { GameEnum = gameEnum; - GameVersion = gameVersion?.ToString(); + GameVersion = gameVersion.ToString(); GameCrc = null; } [SetsRequiredMembers] - public GameStruct(GameEnum gameEnum, string? gameVersion, string? gameCrc) + public GameInfo(GameEnum gameEnum, string? gameVersion, string? gameCrc) { GameEnum = gameEnum; GameVersion = gameVersion; diff --git a/src/Tests.Unit/CmdArguments/AutoloadModsProvider.cs b/src/Tests.Unit/CmdArguments/AutoloadModsProvider.cs index 8f45c606..ae107c94 100644 --- a/src/Tests.Unit/CmdArguments/AutoloadModsProvider.cs +++ b/src/Tests.Unit/CmdArguments/AutoloadModsProvider.cs @@ -8,7 +8,7 @@ namespace Tests.Unit.CmdArguments; internal sealed class AutoloadModsProvider { - private readonly GameStruct _game; + private readonly GameInfo _game; private readonly string _addon; private readonly FeatureEnum _feature; From dcb441aede37cc261bed42efe23adef548cb84ff Mon Sep 17 00:00:00 2001 From: fgsfds <4870330+fgsfds@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:50:56 +0500 Subject: [PATCH 4/7] VersionComparer fix --- src/Core.All/Helpers/VersionComparer.cs | 2 +- src/Tests.Unit/VersionCompareTests.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Core.All/Helpers/VersionComparer.cs b/src/Core.All/Helpers/VersionComparer.cs index 939aadae..4fc49157 100644 --- a/src/Core.All/Helpers/VersionComparer.cs +++ b/src/Core.All/Helpers/VersionComparer.cs @@ -48,7 +48,7 @@ public static bool Compare(string? v1, string? v2) } else { - throw new InvalidOperationException(); + comparisonOperator = ComparisonOperatorEnum.Equals; } return InternalCompare(v1.AsSpan(), s2, comparisonOperator); diff --git a/src/Tests.Unit/VersionCompareTests.cs b/src/Tests.Unit/VersionCompareTests.cs index 1382e514..9dc83884 100644 --- a/src/Tests.Unit/VersionCompareTests.cs +++ b/src/Tests.Unit/VersionCompareTests.cs @@ -24,6 +24,7 @@ public sealed class VersionCompareTests [InlineData(null, "<1")] [InlineData(null, "<=1")] [InlineData("1", null)] + [InlineData("1", "1")] public void Compare_ShouldReturnTrue(string? v1, string? v2) { var result = VersionComparer.Compare(v1, v2); @@ -42,6 +43,7 @@ public void Compare_ShouldReturnTrue(string? v1, string? v2) [InlineData("1.10", "<=1.9")] [InlineData("1.9", ">=1.10")] [InlineData("p2", " Date: Tue, 23 Jun 2026 21:52:31 +0500 Subject: [PATCH 5/7] moved external tests to separate project --- .github/workflows/build-and-test.yml | 4 ++ BuildLauncher.slnx | 1 + .../AppReleasesTests.cs | 2 +- .../PortsInstallerTests.cs | 4 +- src/Tests.External/Tests.External.csproj | 40 +++++++++++++++++++ 5 files changed, 48 insertions(+), 3 deletions(-) rename src/{Tests.Unit => Tests.External}/AppReleasesTests.cs (96%) rename src/{Tests.Unit => Tests.External}/PortsInstallerTests.cs (97%) create mode 100644 src/Tests.External/Tests.External.csproj diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 58b27386..b3b0f255 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -17,6 +17,8 @@ jobs: run: dotnet build --no-restore - name: Test Unit run: dotnet test ./src/Tests.Unit/Tests.Unit.csproj --no-build --verbosity normal + - name: Test External + run: dotnet test ./src/Tests.External/Tests.External.csproj --no-build --verbosity normal build-and-test-linux: runs-on: ubuntu-latest @@ -32,3 +34,5 @@ jobs: run: dotnet build --no-restore - name: Test Unit run: dotnet test ./src/Tests.Unit/Tests.Unit.csproj --no-build --verbosity normal + - name: Test External + run: dotnet test ./src/Tests.External/Tests.External.csproj --no-build --verbosity normal diff --git a/BuildLauncher.slnx b/BuildLauncher.slnx index ca26aeb7..56fc67ad 100644 --- a/BuildLauncher.slnx +++ b/BuildLauncher.slnx @@ -17,6 +17,7 @@ + diff --git a/src/Tests.Unit/AppReleasesTests.cs b/src/Tests.External/AppReleasesTests.cs similarity index 96% rename from src/Tests.Unit/AppReleasesTests.cs rename to src/Tests.External/AppReleasesTests.cs index 07aa0d62..31374ab9 100644 --- a/src/Tests.Unit/AppReleasesTests.cs +++ b/src/Tests.External/AppReleasesTests.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; -namespace Tests.Unit; +namespace Tests.External; public sealed class AppReleasesTests { diff --git a/src/Tests.Unit/PortsInstallerTests.cs b/src/Tests.External/PortsInstallerTests.cs similarity index 97% rename from src/Tests.Unit/PortsInstallerTests.cs rename to src/Tests.External/PortsInstallerTests.cs index eeefaa9b..69bb1371 100644 --- a/src/Tests.Unit/PortsInstallerTests.cs +++ b/src/Tests.External/PortsInstallerTests.cs @@ -11,7 +11,7 @@ using Ports.Providers; using Tools.Providers; -namespace Tests.Unit; +namespace Tests.External; public sealed class PortsInstallerTests { @@ -74,7 +74,7 @@ public async Task InstallPortTest(BasePort port) } - HttpClient GetHttpClient() + private static HttpClient GetHttpClient() { HttpClient httpClient = new(); httpClient.DefaultRequestHeaders.Add("User-Agent", "BuildLauncher"); diff --git a/src/Tests.External/Tests.External.csproj b/src/Tests.External/Tests.External.csproj new file mode 100644 index 00000000..3692e0d3 --- /dev/null +++ b/src/Tests.External/Tests.External.csproj @@ -0,0 +1,40 @@ + + + + false + true + $(MSBuildProjectName) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From bb1bcee9eb347f1b8d8cfa22fdb187b2988cde39 Mon Sep 17 00:00:00 2001 From: fgsfds <4870330+fgsfds@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:48:15 +0500 Subject: [PATCH 6/7] added KeyedServicesEnum --- src/Addons/Providers/InstalledAddonsProvider.cs | 3 ++- src/Addons/Providers/InstalledAddonsProviderFactory.cs | 3 ++- src/Avalonia.Desktop/Helpers/DiHelper.cs | 5 +++-- src/Core.Client/Enums/KeyedServicesEnum.cs | 6 ++++++ 4 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 src/Core.Client/Enums/KeyedServicesEnum.cs diff --git a/src/Addons/Providers/InstalledAddonsProvider.cs b/src/Addons/Providers/InstalledAddonsProvider.cs index 8c6149ec..a4ed02d5 100644 --- a/src/Addons/Providers/InstalledAddonsProvider.cs +++ b/src/Addons/Providers/InstalledAddonsProvider.cs @@ -8,6 +8,7 @@ using Core.All.Interfaces; using Core.All.Serializable.Addon; using Core.Client.Cache; +using Core.Client.Enums; using Core.Client.Helpers; using Core.Client.Interfaces; using Core.Client.Providers; @@ -42,7 +43,7 @@ public sealed class InstalledAddonsProvider : IDisposable public InstalledAddonsProvider( BaseGame game, IConfigProvider config, - [FromKeyedServices("Bitmaps")] ICacheAdder bitmapsCache, + [FromKeyedServices(KeyedServicesEnum.Bitmaps)] ICacheAdder bitmapsCache, OriginalCampaignsProvider originalCampaignsProvider, MetadataProvider metadataProvider, ILogger logger diff --git a/src/Addons/Providers/InstalledAddonsProviderFactory.cs b/src/Addons/Providers/InstalledAddonsProviderFactory.cs index 700e022b..8736046b 100644 --- a/src/Addons/Providers/InstalledAddonsProviderFactory.cs +++ b/src/Addons/Providers/InstalledAddonsProviderFactory.cs @@ -1,5 +1,6 @@ using Core.All.Enums; using Core.Client.Cache; +using Core.Client.Enums; using Core.Client.Interfaces; using Core.Client.Providers; using Games.Games; @@ -19,7 +20,7 @@ public sealed class InstalledAddonsProviderFactory public InstalledAddonsProviderFactory( IConfigProvider config, - [FromKeyedServices("Bitmaps")] ICacheAdder bitmapsCache, + [FromKeyedServices(KeyedServicesEnum.Bitmaps)] ICacheAdder bitmapsCache, OriginalCampaignsProvider originalCampaignsProvider, MetadataProvider metadataProvider, ILoggerFactory loggerFactory diff --git a/src/Avalonia.Desktop/Helpers/DiHelper.cs b/src/Avalonia.Desktop/Helpers/DiHelper.cs index b109b2a5..6ea843a0 100644 --- a/src/Avalonia.Desktop/Helpers/DiHelper.cs +++ b/src/Avalonia.Desktop/Helpers/DiHelper.cs @@ -2,6 +2,7 @@ using Avalonia.Desktop.ViewModels; using Avalonia.Media.Imaging; using Core.Client.Cache; +using Core.Client.Enums; using Microsoft.Extensions.DependencyInjection; namespace Avalonia.Desktop.Helpers; @@ -24,7 +25,7 @@ public static IServiceCollection WithMVVM(this IServiceCollection container) public static IServiceCollection WithBitmapsCache(this IServiceCollection container) { _ = container.AddSingleton(); - _ = container.AddKeyedSingleton>("Bitmaps", (x, _) => x.GetRequiredService()); - return container.AddKeyedSingleton>("Bitmaps", (x, _) => x.GetRequiredService()); + _ = container.AddKeyedSingleton>(KeyedServicesEnum.Bitmaps, (x, _) => x.GetRequiredService()); + return container.AddKeyedSingleton>(KeyedServicesEnum.Bitmaps, (x, _) => x.GetRequiredService()); } } diff --git a/src/Core.Client/Enums/KeyedServicesEnum.cs b/src/Core.Client/Enums/KeyedServicesEnum.cs new file mode 100644 index 00000000..7fa83353 --- /dev/null +++ b/src/Core.Client/Enums/KeyedServicesEnum.cs @@ -0,0 +1,6 @@ +namespace Core.Client.Enums; + +public enum KeyedServicesEnum +{ + Bitmaps +} From 6b51e76f946cff95bda4b1d6978bcd70db44708a Mon Sep 17 00:00:00 2001 From: fgsfds <4870330+fgsfds@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:01:50 +0500 Subject: [PATCH 7/7] fixed app version --- src/Addons/Addons/BaseAddon.cs | 23 +- src/Addons/Helpers/AddonDropHelper.cs | 38 +- src/Addons/Helpers/AutoloadModsValidator.cs | 103 +- src/Addons/Helpers/DiHelper.cs | 2 +- .../Providers/DownloadableAddonsProvider.cs | 22 +- .../DownloadableAddonsProviderFactory.cs | 4 + src/Addons/Providers/GrpInfoProvider.cs | 36 +- .../Providers/InstalledAddonsProvider.cs | 1254 ++++++++--------- .../InstalledAddonsProviderFactory.cs | 21 +- src/Addons/Providers/LocalFilesProvider.cs | 468 ++++++ src/Addons/Providers/MetadataProvider.cs | 177 +++ .../Providers/OriginalCampaignsProvider.cs | 83 +- src/Avalonia.Desktop/App.axaml.cs | 2 + .../ViewModels/CampaignsViewModel.cs | 24 +- .../ViewModels/DownloadsViewModel.cs | 2 +- .../ViewModels/GamePageViewModel.cs | 16 +- .../ViewModels/MapsViewModel.cs | 14 +- .../ViewModels/ModsViewModel.cs | 16 +- .../ViewModels/RightPanelViewModel.cs | 7 +- .../ViewModels/ViewModelsFactory.cs | 3 +- src/Core.All/ChannelBroadcaster.cs | 64 + src/Core.All/Helpers/CommonConstants.cs | 2 + src/Core.All/Helpers/ConfigureAwaitHelper.cs | 34 - src/Core.All/Result.cs | 2 +- src/Core.Client/Api/OfflineApiInterface.cs | 7 +- src/Core.Client/Enums/KeyedServicesEnum.cs | 3 +- .../Helpers/AddonFilePathWrapper.cs | 74 + src/Core.Client/Helpers/ClientProperties.cs | 4 +- src/Core.Client/Helpers/DiHelper.cs | 32 +- src/Core.Client/Helpers/ParsedAddonFile.cs | 36 + src/Core.Client/Providers/MetadataProvider.cs | 186 --- src/Games/DiHelper.cs | 4 +- src/Games/Providers/InstalledGamesProvider.cs | 26 +- src/Ports/Ports/BasePort.cs | 93 +- src/Ports/Ports/BuildGDX.cs | 9 +- src/Ports/Ports/DosBox.cs | 38 +- src/Ports/Ports/EDuke32/EDuke32.cs | 61 +- src/Ports/Ports/EDuke32/Fury.cs | 7 +- src/Ports/Ports/EDuke32/NBlood.cs | 3 +- src/Ports/Ports/EDuke32/NotBlood.cs | 3 +- src/Ports/Ports/EDuke32/PCExhumed.cs | 3 +- src/Ports/Ports/EDuke32/RedNukem.cs | 12 +- src/Ports/Ports/EDuke32/VoidSW.cs | 10 +- src/Ports/Ports/PortStarter.cs | 31 +- src/Ports/Ports/Raze.cs | 25 +- src/Ports/Ports/StubPort.cs | 3 +- src/Ports/Providers/PortsProvider.cs | 8 +- src/Tests.Unit/AddonFilePathWrapperTests.cs | 226 +++ src/Tests.Unit/AddonFilesTests.cs | 161 --- src/Tests.Unit/AutoloadModsValidatorTests.cs | 474 +++++++ src/Tests.Unit/BaseAddonTests.cs | 170 +++ .../CmdArguments/BloodCmdArgumentsTests.cs | 701 --------- .../BloodLooseMapCmdArgumentsTests.cs | 160 --- .../CmdArguments/BuildGDXCmdArgumentsTests.cs | 138 ++ .../CmdArguments/DosBoxCmdArgumentsTests.cs | 346 +++++ .../CmdArguments/DukeCmdArgumentsTests.cs | 680 --------- .../DukeLooseMapCmdArgumentsTests.cs | 220 --- .../CmdArguments/EDuke32CmdArgumentsTests.cs | 352 +++++ .../CmdArguments/FuryCmdArgumentsTests.cs | 66 +- .../CmdArguments/NBloodCmdArgumentsTests.cs | 121 ++ .../CmdArguments/NamCmdArgumentsTests.cs | 170 --- .../CmdArguments/NotBloodCmdArgumentsTests.cs | 139 ++ .../PCExhumedCmdArgumentsTests.cs | 43 + .../CmdArguments/RazeCmdArgumentsTests.cs | 764 ++++++++++ .../CmdArguments/RedNukemCmdArgumentsTests.cs | 260 ++++ .../CmdArguments/RedneckCmdArgumentsTests.cs | 232 --- .../CmdArguments/SlaveCmdArgumentsTests.cs | 124 -- .../CmdArguments/VoidSWCmdArgumentsTests.cs | 114 ++ .../CmdArguments/WW2GICmdArgumentsTests.cs | 230 --- .../CmdArguments/WangCmdArgumentsTests.cs | 256 ---- .../WangLooseMapsCmdArgumentsTests.cs | 153 -- .../CmdArguments/ZeroHourCmdArgumentsTests.cs | 30 + src/Tests.Unit/Files/WhatLiesBeneathAddon.zip | Bin 710 -> 0 bytes src/Tests.Unit/GrpInfoParsingTests.cs | 64 - src/Tests.Unit/GrpInfoProviderTests.cs | 188 +++ .../AutoloadModsTestSetups.cs} | 165 ++- src/Tests.Unit/Helpers/FileCreationHelper.cs | 39 + src/Tests.Unit/Helpers/GamesTestHelper.cs | 25 + src/Tests.Unit/Helpers/NormalizerHelper.cs | 33 + .../Helpers/ObjectCreationHelper.cs | 50 + .../Helpers/ParsedAddonFileHelper.cs | 77 + src/Tests.Unit/Helpers/PathHelper.cs | 12 + src/Tests.Unit/Helpers/PortTestSetups.cs | 926 ++++++++++++ src/Tests.Unit/ResultTests.cs | 13 - src/Tests.Unit/SaveFilesTestHelper.cs | 60 + src/Tests.Unit/Sync/AddonDropHelperTests.cs | 222 +++ src/Tests.Unit/Sync/AddonFilesTests.cs | 199 +++ .../Sync/InstalledAddonsProviderTests.cs | 832 +++++++++++ .../Sync/LocalFilesProviderTests.cs | 366 +++++ src/Tests.Unit/Sync/MetadataProviderTests.cs | 331 +++++ src/Tests.Unit/Sync/SaveFilesTests.cs | 298 ++++ src/Tests.Unit/Tests.Unit.csproj | 4 - 92 files changed, 8785 insertions(+), 4544 deletions(-) create mode 100644 src/Addons/Providers/LocalFilesProvider.cs create mode 100644 src/Addons/Providers/MetadataProvider.cs create mode 100644 src/Core.All/ChannelBroadcaster.cs delete mode 100644 src/Core.All/Helpers/ConfigureAwaitHelper.cs create mode 100644 src/Core.Client/Helpers/AddonFilePathWrapper.cs create mode 100644 src/Core.Client/Helpers/ParsedAddonFile.cs delete mode 100644 src/Core.Client/Providers/MetadataProvider.cs create mode 100644 src/Tests.Unit/AddonFilePathWrapperTests.cs delete mode 100644 src/Tests.Unit/AddonFilesTests.cs create mode 100644 src/Tests.Unit/AutoloadModsValidatorTests.cs create mode 100644 src/Tests.Unit/BaseAddonTests.cs delete mode 100644 src/Tests.Unit/CmdArguments/BloodCmdArgumentsTests.cs delete mode 100644 src/Tests.Unit/CmdArguments/BloodLooseMapCmdArgumentsTests.cs create mode 100644 src/Tests.Unit/CmdArguments/BuildGDXCmdArgumentsTests.cs create mode 100644 src/Tests.Unit/CmdArguments/DosBoxCmdArgumentsTests.cs delete mode 100644 src/Tests.Unit/CmdArguments/DukeCmdArgumentsTests.cs delete mode 100644 src/Tests.Unit/CmdArguments/DukeLooseMapCmdArgumentsTests.cs create mode 100644 src/Tests.Unit/CmdArguments/EDuke32CmdArgumentsTests.cs create mode 100644 src/Tests.Unit/CmdArguments/NBloodCmdArgumentsTests.cs delete mode 100644 src/Tests.Unit/CmdArguments/NamCmdArgumentsTests.cs create mode 100644 src/Tests.Unit/CmdArguments/NotBloodCmdArgumentsTests.cs create mode 100644 src/Tests.Unit/CmdArguments/PCExhumedCmdArgumentsTests.cs create mode 100644 src/Tests.Unit/CmdArguments/RazeCmdArgumentsTests.cs create mode 100644 src/Tests.Unit/CmdArguments/RedNukemCmdArgumentsTests.cs delete mode 100644 src/Tests.Unit/CmdArguments/RedneckCmdArgumentsTests.cs delete mode 100644 src/Tests.Unit/CmdArguments/SlaveCmdArgumentsTests.cs create mode 100644 src/Tests.Unit/CmdArguments/VoidSWCmdArgumentsTests.cs delete mode 100644 src/Tests.Unit/CmdArguments/WW2GICmdArgumentsTests.cs delete mode 100644 src/Tests.Unit/CmdArguments/WangCmdArgumentsTests.cs delete mode 100644 src/Tests.Unit/CmdArguments/WangLooseMapsCmdArgumentsTests.cs create mode 100644 src/Tests.Unit/CmdArguments/ZeroHourCmdArgumentsTests.cs delete mode 100644 src/Tests.Unit/Files/WhatLiesBeneathAddon.zip delete mode 100644 src/Tests.Unit/GrpInfoParsingTests.cs create mode 100644 src/Tests.Unit/GrpInfoProviderTests.cs rename src/Tests.Unit/{CmdArguments/AutoloadModsProvider.cs => Helpers/AutoloadModsTestSetups.cs} (61%) create mode 100644 src/Tests.Unit/Helpers/FileCreationHelper.cs create mode 100644 src/Tests.Unit/Helpers/GamesTestHelper.cs create mode 100644 src/Tests.Unit/Helpers/NormalizerHelper.cs create mode 100644 src/Tests.Unit/Helpers/ObjectCreationHelper.cs create mode 100644 src/Tests.Unit/Helpers/ParsedAddonFileHelper.cs create mode 100644 src/Tests.Unit/Helpers/PathHelper.cs create mode 100644 src/Tests.Unit/Helpers/PortTestSetups.cs create mode 100644 src/Tests.Unit/SaveFilesTestHelper.cs create mode 100644 src/Tests.Unit/Sync/AddonDropHelperTests.cs create mode 100644 src/Tests.Unit/Sync/AddonFilesTests.cs create mode 100644 src/Tests.Unit/Sync/InstalledAddonsProviderTests.cs create mode 100644 src/Tests.Unit/Sync/LocalFilesProviderTests.cs create mode 100644 src/Tests.Unit/Sync/MetadataProviderTests.cs create mode 100644 src/Tests.Unit/Sync/SaveFilesTests.cs diff --git a/src/Addons/Addons/BaseAddon.cs b/src/Addons/Addons/BaseAddon.cs index aca960e7..e95aa72c 100644 --- a/src/Addons/Addons/BaseAddon.cs +++ b/src/Addons/Addons/BaseAddon.cs @@ -3,6 +3,7 @@ using Core.All; using Core.All.Enums; using Core.All.Interfaces; +using Core.Client.Helpers; namespace Addons.Addons; @@ -16,13 +17,18 @@ public abstract class BaseAddon /// public required AddonId AddonId { get; init; } + /// + /// Addon file information. + /// + public required AddonFilePathWrapper? FileInfo { get; init; } + /// /// Type of the addon /// public required AddonTypeEnum Type { get; init; } /// - /// List of supported games + /// Supported game /// public required GameInfo SupportedGame { get; init; } @@ -61,11 +67,6 @@ public abstract class BaseAddon /// public required IReadOnlyDictionary? IncompatibleAddons { get; init; } - /// - /// Path to addon file - /// - public required string? PathToFile { get; init; } - /// /// Cover image hash /// @@ -91,11 +92,6 @@ public abstract class BaseAddon /// public required IStartMap? StartMap { get; init; } - /// - /// Is addon unpacked to a folder - /// - public required bool IsUnpacked { get; init; } - /// /// Is the item marked as a favorite. /// @@ -116,11 +112,6 @@ public abstract class BaseAddon /// public required Dictionary>? Options { get; init; } - /// - /// Name of the addon file - /// - public string? FileName => PathToFile is null ? null : Path.GetFileName(PathToFile); - /// public override string ToString() => Title; diff --git a/src/Addons/Helpers/AddonDropHelper.cs b/src/Addons/Helpers/AddonDropHelper.cs index e5745349..d170c310 100644 --- a/src/Addons/Helpers/AddonDropHelper.cs +++ b/src/Addons/Helpers/AddonDropHelper.cs @@ -5,6 +5,8 @@ using Microsoft.Extensions.Logging; using SharpCompress.Archives; +namespace Addons.Helpers; + public interface IAddonDropHelper { /// @@ -18,12 +20,15 @@ public interface IAddonDropHelper public sealed class AddonDropHelper : IAddonDropHelper { - private readonly InstalledAddonsProviderFactory _installedAddonsProvider; + private readonly LocalFilesProvider _addonScanner; private readonly ILogger _logger; - public AddonDropHelper(InstalledAddonsProviderFactory installedAddonsProvider, ILogger logger) + public AddonDropHelper( + LocalFilesProvider addonScanner, + ILogger logger + ) { - _installedAddonsProvider = installedAddonsProvider; + _addonScanner = addonScanner; _logger = logger; } @@ -35,21 +40,19 @@ public AddonDropHelper(InstalledAddonsProviderFactory installedAddonsProvider, I return null; } - List failedInstalls = []; + List? failedInstalls = null; foreach (var file in filePaths) { var isAdded = await AddAddonAsync(file, game).ConfigureAwait(false); - if (!isAdded) + if (isAdded) { - failedInstalls.Add(Path.GetFileName(file)); + continue; } - } - if (failedInstalls.Count == 0) - { - return null; + failedInstalls ??= []; + failedInstalls.Add(Path.GetFileName(file)); } return failedInstalls; @@ -71,7 +74,7 @@ private async Task AddAddonAsync(string pathToFile, BaseGame game) return false; } - var addon = await GetGameAndTypeFromFileAsync(pathToFile, game).ConfigureAwait(false); + var addon = await GetGameAndTypeFromFileAsync(pathToFile, game.GameEnum).ConfigureAwait(false); if (addon is null) { @@ -109,8 +112,12 @@ private async Task AddAddonAsync(string pathToFile, BaseGame game) File.Copy(pathToFile, newPathToFile, true); - using var installer = _installedAddonsProvider.Get(game); - await installer.AddAddonAsync(newPathToFile).ConfigureAwait(false); + var parsedFiles = await _addonScanner.TryAddFileToCacheAsync(newPathToFile, game.GameEnum).ConfigureAwait(false); + + if (parsedFiles is null) + { + return false; + } return true; } @@ -119,7 +126,8 @@ private async Task AddAddonAsync(string pathToFile, BaseGame game) /// Get game enum and addon type enum from a file. /// /// Path to file. - private async Task?> GetGameAndTypeFromFileAsync(string pathToFile, BaseGame game) + /// The game to associate with standalone .map files. + private async Task?> GetGameAndTypeFromFileAsync(string pathToFile, GameEnum gameEnum) { if (ArchiveFactory.IsArchive(pathToFile, out _)) { @@ -138,7 +146,7 @@ private async Task AddAddonAsync(string pathToFile, BaseGame game) if (pathToFile.EndsWith(".map", StringComparison.OrdinalIgnoreCase)) { - return new(game.GameEnum, AddonTypeEnum.Map); + return new(gameEnum, AddonTypeEnum.Map); } return null; diff --git a/src/Addons/Helpers/AutoloadModsValidator.cs b/src/Addons/Helpers/AutoloadModsValidator.cs index 79c2fd5d..df3b9122 100644 --- a/src/Addons/Helpers/AutoloadModsValidator.cs +++ b/src/Addons/Helpers/AutoloadModsValidator.cs @@ -1,5 +1,4 @@ using Addons.Addons; -using Core.All; using Core.All.Enums; using Core.All.Helpers; @@ -14,7 +13,7 @@ public static class AutoloadModsValidator /// Campaign /// Autoload mods /// Features supported by the port - public static bool ValidateAutoloadMod(AutoloadMod autoloadMod, BaseAddon campaign, IReadOnlyDictionary mods, List features) + public static bool ValidateAutoloadMod(AutoloadMod autoloadMod, BaseAddon campaign, IReadOnlyList mods, List features) { if (!autoloadMod.IsEnabled) { @@ -41,7 +40,7 @@ public static bool ValidateAutoloadMod(AutoloadMod autoloadMod, BaseAddon campai return false; } - //check if campaign is incomatible with this or all addons + //check if campaign is incompatible with this or all addons if (campaign.IncompatibleAddons is not null) { foreach (var incompatibleAddon in campaign.IncompatibleAddons) @@ -84,52 +83,49 @@ public static bool ValidateAutoloadMod(AutoloadMod autoloadMod, BaseAddon campai private static bool CheckDependencies( AutoloadMod autoloadMod, BaseAddon campaign, - IReadOnlyDictionary mods) + IReadOnlyList mods + ) { - if (autoloadMod.DependentAddons is not null) + if (autoloadMod.DependentAddons is null) { - byte passedDependenciesCount = 0; + return true; + } - foreach (var dependentAddon in autoloadMod.DependentAddons) + byte passedDependenciesCount = 0; + + foreach (var dependentAddon in autoloadMod.DependentAddons) + { + if (campaign.AddonId.Id.Equals(dependentAddon.Key, StringComparison.OrdinalIgnoreCase) && + (dependentAddon.Value is null || VersionComparer.Compare(campaign.AddonId.Version, dependentAddon.Value))) { - if (campaign.AddonId.Id.Equals(dependentAddon.Key, StringComparison.OrdinalIgnoreCase) && - (dependentAddon.Value is null || VersionComparer.Compare(campaign.AddonId.Version, dependentAddon.Value))) - { - passedDependenciesCount++; - continue; - } + passedDependenciesCount++; + continue; + } + + if (campaign.DependentAddons is not null && + campaign.DependentAddons.TryGetValue(dependentAddon.Key, out var dependentAddonVersion) && + (dependentAddon.Value is null || VersionComparer.Compare(dependentAddonVersion, dependentAddon.Value))) + { + passedDependenciesCount++; + continue; + } - if (campaign.DependentAddons?.TryGetValue(dependentAddon.Key, out var dependentAddonVersion) ?? false && - (dependentAddon.Value is null || VersionComparer.Compare(dependentAddonVersion, dependentAddon.Value))) + foreach (var addon in mods) + { + if (!dependentAddon.Key.Equals(addon.AddonId.Id, StringComparison.InvariantCultureIgnoreCase)) { - passedDependenciesCount++; continue; } - foreach (var addon in mods) + if (dependentAddon.Value is null || VersionComparer.Compare(addon.AddonId.Version, dependentAddon.Value)) { - if (!dependentAddon.Key.Equals(addon.Key.Id, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (dependentAddon.Value is null) - { - passedDependenciesCount++; - } - else if (VersionComparer.Compare(addon.Key.Version, dependentAddon.Value)) - { - passedDependenciesCount++; - } + passedDependenciesCount++; } } - - return autoloadMod.DependentAddons.Count == passedDependenciesCount; - } - else - { - return true; } + + return autoloadMod.DependentAddons.Count == passedDependenciesCount; + } /// @@ -138,30 +134,35 @@ private static bool CheckDependencies( private static bool CheckIncompatibles( AutoloadMod autoloadMod, BaseAddon campaign, - IReadOnlyDictionary mods + IReadOnlyList mods ) { + if (autoloadMod.IncompatibleAddons is null) + { + return true; + } + var campaignIncompatibles = campaign.IncompatibleAddons?.ToDictionary() ?? []; - campaignIncompatibles.Add(campaign.AddonId.Id, campaign.AddonId.Version); - campaignIncompatibles.AddRange(mods.Where(x => x.Value is AutoloadMod { IsEnabled: true }).ToDictionary(x => x.Key.Id, x => x.Key.Version)); + campaignIncompatibles.TryAdd(campaign.AddonId.Id, campaign.AddonId.Version); + foreach (var addon in mods.Where(x => x is AutoloadMod { IsEnabled: true })) + { + campaignIncompatibles.TryAdd(addon.AddonId.Id, addon.AddonId.Version); + } - if (autoloadMod.IncompatibleAddons is not null) + foreach (var a in campaignIncompatibles) { - foreach (var a in campaignIncompatibles) + foreach (var b in autoloadMod.IncompatibleAddons) { - foreach (var b in autoloadMod.IncompatibleAddons) + if (!a.Key.Equals(b.Key, StringComparison.OrdinalIgnoreCase)) { - if (!a.Key.Equals(b.Key, StringComparison.OrdinalIgnoreCase)) - { - continue; - } + continue; + } - var areEqual = VersionComparer.Compare(a.Value, b.Value); + var areEqual = VersionComparer.Compare(a.Value, b.Value); - if (areEqual) - { - return false; - } + if (areEqual) + { + return false; } } } diff --git a/src/Addons/Helpers/DiHelper.cs b/src/Addons/Helpers/DiHelper.cs index 8f17f2aa..c91c0381 100644 --- a/src/Addons/Helpers/DiHelper.cs +++ b/src/Addons/Helpers/DiHelper.cs @@ -1,5 +1,4 @@ using Addons.Providers; -using Core.Client.Providers; using Microsoft.Extensions.DependencyInjection; namespace Addons.Helpers; @@ -15,6 +14,7 @@ public static IServiceCollection WithAddons(this IServiceCollection container) _ = container.AddSingleton(); _ = container.AddSingleton(); _ = container.AddSingleton(); + _ = container.AddSingleton(); return container.AddTransient(); } diff --git a/src/Addons/Providers/DownloadableAddonsProvider.cs b/src/Addons/Providers/DownloadableAddonsProvider.cs index a503e4cf..59cccbcd 100644 --- a/src/Addons/Providers/DownloadableAddonsProvider.cs +++ b/src/Addons/Providers/DownloadableAddonsProvider.cs @@ -20,6 +20,7 @@ public sealed class DownloadableAddonsProvider private readonly BaseGame _game; private readonly ArchiveTools _archiveTools; private readonly FilesDownloader _filesDownloader; + private readonly LocalFilesProvider _filesProvider; private readonly IApiInterface _apiInterface; private readonly InstalledAddonsProvider _installedAddonsProvider; private readonly ILogger _logger; @@ -28,7 +29,7 @@ public sealed class DownloadableAddonsProvider private static readonly SemaphoreSlim _semaphore = new(1); - public event AddonChanged? AddonsChangedEvent; + //public event AddonChanged? AddonsChangedEvent; /// /// Download progress @@ -41,6 +42,7 @@ public DownloadableAddonsProvider( BaseGame game, ArchiveTools archiveTools, FilesDownloader filesDownloader, + LocalFilesProvider filesProvider, IApiInterface apiInterface, InstalledAddonsProviderFactory installedAddonsProviderFactory, ILogger logger @@ -49,6 +51,7 @@ ILogger logger _game = game; _archiveTools = archiveTools; _filesDownloader = filesDownloader; + _filesProvider = filesProvider; _apiInterface = apiInterface; _logger = logger; @@ -105,7 +108,6 @@ public async Task CreateCacheAsync(bool createNew) finally { _ = _semaphore.Release(); - AddonsChangedEvent?.Invoke(_game.GameEnum, null); } } @@ -130,8 +132,8 @@ public ImmutableList GetDownloadableAddons(AddonType foreach (var downloadableAddon in addonTypeCache) { var existingAddons = installedAddons - .Where(x => x.Key.Id.Equals(downloadableAddon.Key.Id, StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Key) + .Where(x => x.AddonId.Id.Equals(downloadableAddon.Key.Id, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.AddonId) .ToList(); downloadableAddon.Value.IsInstalled = true; @@ -145,7 +147,7 @@ public ImmutableList GetDownloadableAddons(AddonType //Death Wish hack if (addonType is AddonTypeEnum.TC && downloadableAddon.Key.Id.Contains("death-wish", StringComparison.OrdinalIgnoreCase) && - downloadableAddon.Key.Version!.StartsWith('1')) + downloadableAddon.Key.Version?.StartsWith('1') is true) { downloadableAddon.Value.IsInstalled = existingAddons.Contains(downloadableAddon.Key); } @@ -153,11 +155,11 @@ public ImmutableList GetDownloadableAddons(AddonType { foreach (var existingVersion in existingAddons.Select(static x => x.Version).Where(static x => x is not null)) { - downloadableAddon.Value.IsUpdateAvailable = true; + downloadableAddon.Value.IsUpdateAvailable = false; - if (VersionComparer.Compare(downloadableAddon.Value.Version, existingVersion, ComparisonOperatorEnum.LessThan)) + if (VersionComparer.Compare(downloadableAddon.Value.Version, existingVersion, ComparisonOperatorEnum.GreaterThan)) { - downloadableAddon.Value.IsUpdateAvailable = false; + downloadableAddon.Value.IsUpdateAvailable = true; break; } } @@ -226,7 +228,7 @@ CancellationToken cancellationToken return false; } - await _installedAddonsProvider.AddAddonAsync(pathToFile).ConfigureAwait(false); + _ = await _filesProvider.TryAddFileToCacheAsync(pathToFile, _game.GameEnum).ConfigureAwait(false); if (!ClientProperties.IsDeveloperMode) { @@ -238,7 +240,7 @@ CancellationToken cancellationToken } } - AddonsChangedEvent?.Invoke(_game.GameEnum, addon.AddonType); + //AddonsChangedEvent?.Invoke(_game.GameEnum, addon.AddonType); return true; } diff --git a/src/Addons/Providers/DownloadableAddonsProviderFactory.cs b/src/Addons/Providers/DownloadableAddonsProviderFactory.cs index a1ea4a9b..f91038d4 100644 --- a/src/Addons/Providers/DownloadableAddonsProviderFactory.cs +++ b/src/Addons/Providers/DownloadableAddonsProviderFactory.cs @@ -12,6 +12,7 @@ public sealed class DownloadableAddonsProviderFactory private readonly ArchiveTools _archiveTools; private readonly FilesDownloader _filesDownloader; + private readonly LocalFilesProvider _filesProvider; private readonly IApiInterface _apiInterface; private readonly InstalledAddonsProviderFactory _installedAddonsProviderFactory; private readonly ILoggerFactory _loggerFactory; @@ -19,6 +20,7 @@ public sealed class DownloadableAddonsProviderFactory public DownloadableAddonsProviderFactory( ArchiveTools archiveTools, FilesDownloader filesDownloader, + LocalFilesProvider filesProvider, IApiInterface apiInterface, InstalledAddonsProviderFactory installedAddonsProviderFactory, ILoggerFactory loggerFactory @@ -26,6 +28,7 @@ ILoggerFactory loggerFactory { _archiveTools = archiveTools; _filesDownloader = filesDownloader; + _filesProvider = filesProvider; _apiInterface = apiInterface; _installedAddonsProviderFactory = installedAddonsProviderFactory; _loggerFactory = loggerFactory; @@ -47,6 +50,7 @@ public DownloadableAddonsProvider Get(BaseGame game) game, _archiveTools, _filesDownloader, + _filesProvider, _apiInterface, _installedAddonsProviderFactory, _loggerFactory.CreateLogger() diff --git a/src/Addons/Providers/GrpInfoProvider.cs b/src/Addons/Providers/GrpInfoProvider.cs index ccb7f585..b098d80a 100644 --- a/src/Addons/Providers/GrpInfoProvider.cs +++ b/src/Addons/Providers/GrpInfoProvider.cs @@ -2,7 +2,6 @@ using Addons.Addons; using Core.All; using Core.All.Enums; -using Core.All.Enums.Versions; namespace Addons.Providers; @@ -11,26 +10,10 @@ public static class GrpInfoProvider /// /// Get list of addons from grpinfo files located in the folder and its subfolders. /// - public static List? GetAddonsFromGrpInfo(string pathToFolder) + public static IReadOnlyList? GetAddonsFromGrpInfo(string pathToGrpInfoFile) { - List newAddons = []; + return TryGetAddonsFromGrpInfo(pathToGrpInfoFile, out var foundAddons) ? foundAddons : null; - var grpInfos = Directory.GetFiles(pathToFolder, "*.grpinfo", SearchOption.AllDirectories); - - if (grpInfos.Length == 0) - { - return null; - } - - foreach (var grpInfo in grpInfos) - { - if (TryGetAddonsFromGrpInfo(grpInfo, out var foundAddons)) - { - newAddons.AddRange(foundAddons); - } - } - - return newAddons; } private static bool TryGetAddonsFromGrpInfo(string pathToGrpInfo, [NotNullWhen(true)] out List? newAddons) @@ -59,11 +42,11 @@ private static bool TryGetAddonsFromGrpInfo(string pathToGrpInfo, [NotNullWhen(t continue; } - AddonId version = new(grpInfo.Name.ToLower().Replace(" ", "_"), null); + AddonId addonId = new(grpInfo.Name.ToLower().Replace(" ", "_"), null); DukeCampaign camp = new() { - AddonId = version, + AddonId = addonId, Type = AddonTypeEnum.TC, SupportedGame = new(GameEnum.Duke3D), Title = grpInfo.Name, @@ -72,7 +55,7 @@ private static bool TryGetAddonsFromGrpInfo(string pathToGrpInfo, [NotNullWhen(t Description = null, Author = null, ReleaseDate = null, - PathToFile = grp, + FileInfo = new(grpInfoFolder, Path.GetFileName(grp)), DependentAddons = null, IncompatibleAddons = null, StartMap = null, @@ -82,7 +65,6 @@ private static bool TryGetAddonsFromGrpInfo(string pathToGrpInfo, [NotNullWhen(t AdditionalDefs = grpInfo.AddDef is null ? null : [grpInfo.AddDef], RTS = null, RequiredFeatures = [FeatureEnum.EDuke32_CON], - IsUnpacked = false, Executables = null, IsFavorite = false, Options = null @@ -110,7 +92,7 @@ internal static List Parse(string pathToFile, int expectedGrpsCoun string? def = null; var size = 0; - var isInsideGrpinfoBlock = false; + var isInsideGrpInfoBlock = false; foreach (var line in lines) { @@ -128,11 +110,11 @@ internal static List Parse(string pathToFile, int expectedGrpsCoun def = null; size = 0; - isInsideGrpinfoBlock = true; + isInsideGrpInfoBlock = true; continue; } - if (!isInsideGrpinfoBlock) + if (!isInsideGrpInfoBlock) { continue; } @@ -169,7 +151,7 @@ internal static List Parse(string pathToFile, int expectedGrpsCoun addons.Add(addon); } - isInsideGrpinfoBlock = false; + isInsideGrpInfoBlock = false; } } diff --git a/src/Addons/Providers/InstalledAddonsProvider.cs b/src/Addons/Providers/InstalledAddonsProvider.cs index a4ed02d5..a3bf3631 100644 --- a/src/Addons/Providers/InstalledAddonsProvider.cs +++ b/src/Addons/Providers/InstalledAddonsProvider.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Text.Json; +using System.Threading.Channels; using Addons.Addons; using Core.All; using Core.All.Enums; @@ -7,35 +8,35 @@ using Core.All.Helpers; using Core.All.Interfaces; using Core.All.Serializable.Addon; -using Core.Client.Cache; -using Core.Client.Enums; using Core.Client.Helpers; using Core.Client.Interfaces; -using Core.Client.Providers; using Games.Games; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SharpCompress.Archives; +using StandaloneGame = Addons.Addons.StandaloneGame; namespace Addons.Providers; /// -/// Class that provides lists of installed mods +/// Provides cached lists of installed campaigns, maps, and mods for a specific game. /// public sealed class InstalledAddonsProvider : IDisposable { private readonly BaseGame _game; private readonly IConfigProvider _config; private readonly ILogger _logger; - private readonly ICacheAdder _bitmapsCache; private readonly OriginalCampaignsProvider _originalCampaignsProvider; private readonly MetadataProvider _metadataProvider; + private readonly LocalFilesProvider _localFilesProvider; - private readonly Dictionary _campaignsCache = []; - private readonly Dictionary _mapsCache = []; - private readonly Dictionary _modsCache = []; + private readonly List _campaignsCache = []; + private readonly List _mapsCache = []; + private readonly List _modsCache = []; - private readonly SemaphoreSlim _semaphore = new(1); + private readonly IChannelSubscriber _channelPublisher; + private readonly ChannelReader _channelReader; + private readonly CancellationTokenSource _channelCancellation = new(); + private readonly SemaphoreSlim _cacheUpdateSemaphore = new(1); public event AddonChanged? AddonsChangedEvent; @@ -43,250 +44,396 @@ public sealed class InstalledAddonsProvider : IDisposable public InstalledAddonsProvider( BaseGame game, IConfigProvider config, - [FromKeyedServices(KeyedServicesEnum.Bitmaps)] ICacheAdder bitmapsCache, OriginalCampaignsProvider originalCampaignsProvider, MetadataProvider metadataProvider, + LocalFilesProvider localFilesProvider, + IChannelSubscriber channelPublisher, ILogger logger ) { _game = game; _config = config; _logger = logger; - _bitmapsCache = bitmapsCache; _originalCampaignsProvider = originalCampaignsProvider; _metadataProvider = metadataProvider; + _channelPublisher = channelPublisher; + _localFilesProvider = localFilesProvider; - _metadataProvider.MetadataUpdatedEvent += OnMetadataUpdatedAsync; + _metadataProvider.MetadataUpdatedEvent += OnMetadataUpdated; + _metadataProvider.MetadataInitializedEvent += OnMetadataInitialized; + + _channelReader = channelPublisher.Subscribe(); + + Task.Run(async () => + { + try + { + await foreach (var e in _channelReader.ReadAllAsync(_channelCancellation.Token)) + { + foreach (var parsedFile in e.Files) + { + await _cacheUpdateSemaphore.WaitAsync(_channelCancellation.Token); + + try + { + if (parsedFile.SupportedGame != _game.GameEnum) + { + continue; + } + + if (e.IsAdded) + { + var isUnpacked = await UnpackAndUpdateIfNeededAsync(parsedFile); + + if (isUnpacked) + { + break; + } + + AddAddon(parsedFile); + } + else + { + DeleteAddon(parsedFile); + } + } + finally + { + _cacheUpdateSemaphore.Release(); + } + } + } + } + catch (OperationCanceledException e) + { + //nothing to do + } + }); } /// - /// Create cache of installed addons. + /// Build or refresh the internal caches for all addon types. /// - /// Clear current cache and create new. - /// Addon type. - public async Task CreateCache(bool createNew, AddonTypeEnum addonType) + /// If true, clear the cache for before rebuilding. + /// Addon type whose cache to optionally clear. + public async Task CreateCacheAsync(bool createNew, AddonTypeEnum addonType) { - await _semaphore.WaitAsync().ConfigureAwait(false); - - var cache = addonType switch - { - AddonTypeEnum.TC => _campaignsCache, - AddonTypeEnum.Map => _mapsCache, - AddonTypeEnum.Mod => _modsCache, - _ => throw new NotSupportedException(), - }; - - if (createNew) - { - cache.Clear(); - } + await _cacheUpdateSemaphore.WaitAsync().ConfigureAwait(false); try { - if (_campaignsCache.Count == 0) + var cache = GetCacheByAddonType(addonType); + + if (createNew) { - //campaigns - List filesTcs = [.. Directory.GetFiles(_game.CampaignsFolderPath, "*.zip")]; + cache.Clear(); + } + + var files = await _localFilesProvider.GetCachedAddonFilesAsync().ConfigureAwait(false); - var dirs = Directory.GetDirectories(_game.CampaignsFolderPath, "*", SearchOption.TopDirectoryOnly); + switch (addonType) + { + case AddonTypeEnum.TC when cache.Count == 0: + _campaignsCache.Clear(); + await FillCacheAsync(files, AddonTypeEnum.TC); + break; + case AddonTypeEnum.Map when cache.Count == 0: + _mapsCache.Clear(); + await FillCacheAsync(files, AddonTypeEnum.Map); + break; + case AddonTypeEnum.Mod when cache.Count == 0: + _modsCache.Clear(); + await FillCacheAsync(files, AddonTypeEnum.Mod); + break; + case AddonTypeEnum.Official: + default: + throw new ArgumentOutOfRangeException(nameof(addonType), addonType, null); + } - foreach (var dir in dirs) + if (addonType is AddonTypeEnum.Mod) + { + foreach (var mod in _modsCache) { - var addonJsons = Directory.GetFiles(dir, "addon*.json"); + if (mod is not AutoloadMod autoloadMod) + { + _logger.LogError($"=== Error while enabling/disabling addon {mod.AddonId.Id}"); + continue; + } - if (addonJsons.Length > 0) + if (autoloadMod.IsEnabled) + { + EnableAddon(mod.AddonId); + } + else if (!autoloadMod.IsEnabled) { - filesTcs.AddRange(addonJsons); + DisableAddon(mod.AddonId); } } + } + } + catch (Exception ex) + { + _logger.LogCritical(ex, "=== Error while creating installed addons cache ==="); + } + finally + { + _ = _cacheUpdateSemaphore.Release(); + ArgumentNullException.ThrowIfNull(_campaignsCache); + ArgumentNullException.ThrowIfNull(_mapsCache); + ArgumentNullException.ThrowIfNull(_modsCache); - var tcs = await GetAddonsFromFilesAsync(filesTcs).ConfigureAwait(false); + AddonsChangedEvent?.Invoke(_game.GameEnum, addonType); + } + } - foreach (var wrongFile in tcs.Where(x => x.Value.Type is not AddonTypeEnum.TC)) - { - _logger.LogError($"File {wrongFile.Value.FileName} of type {wrongFile.Value.Type} is in the Campaigns folder"); - } + private List GetCacheByAddonType(AddonTypeEnum addonType) + { + var cache = addonType switch + { + AddonTypeEnum.TC => _campaignsCache, + AddonTypeEnum.Map => _mapsCache, + AddonTypeEnum.Mod => _modsCache, + _ => throw new NotSupportedException(), + }; - _campaignsCache.AddRange(tcs); + return cache; + } - //grpinfo - var grpInfoAddons = GrpInfoProvider.GetAddonsFromGrpInfo(_game.CampaignsFolderPath); + /// + /// Scan parsed addon files and populate the cache for . + /// + private async Task FillCacheAsync(IReadOnlyList parsedAddonFiles, AddonTypeEnum addonType) + { + foreach (var parsedAddonFile in parsedAddonFiles) + { + if (parsedAddonFile.Manifest is not null && parsedAddonFile.Manifest.SupportedGame.Game != _game.GameEnum) + { + continue; + } - if (grpInfoAddons?.Count > 0) - { - foreach (var addon in grpInfoAddons) - { - var result = _campaignsCache.TryAdd(new(addon.AddonId.Id, null), addon); + var isUnpacked = await UnpackAndUpdateIfNeededAsync(parsedAddonFile); - if (!result) - { - _logger.LogError($"Failed to add {addon.FileName} to the campaigns list."); - } - } - } + if (isUnpacked) + { + return; } + var cache = GetCacheByAddonType(addonType); - if (_mapsCache.Count == 0) + if (parsedAddonFile.FileInfo.IsGrpInfo) { - //maps - var filesMaps = Directory.GetFiles(_game.MapsFolderPath).Where(static x => x.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) || x.EndsWith(".map", StringComparison.OrdinalIgnoreCase)); - var maps = await GetAddonsFromFilesAsync(filesMaps).ConfigureAwait(false); - - foreach (var wrongFile in maps.Where(x => x.Value.Type is not AddonTypeEnum.Map)) + if (addonType != AddonTypeEnum.TC) { - _logger.LogError($"File {wrongFile.Value.FileName} of type {wrongFile.Value.Type} is in the Maps folder"); + continue; } - _mapsCache.AddRange(maps); - } + var addons = GrpInfoProvider.GetAddonsFromGrpInfo(parsedAddonFile.FileInfo.PathToFile); + if (addons is null or []) + { + continue; + } - if (_modsCache.Count == 0) + cache.AddRange(addons); + } + else if (parsedAddonFile.FileInfo.IsMap) { - //mods - var filesMods = Directory.GetFiles(_game.ModsFolderPath, "*.zip"); - var mods = await GetAddonsFromFilesAsync(filesMods).ConfigureAwait(false); + var addon = GetLooseMapFromFile(parsedAddonFile); - foreach (var wrongFile in mods.Where(x => x.Value.Type is not AddonTypeEnum.Mod)) + if (addon is null) { - _logger.LogError($"File {wrongFile.Value.FileName} of type {wrongFile.Value.Type} is in the Mods folder"); + continue; } - _modsCache.AddRange(mods); + cache.Add(addon); } - - - //enabling/disabling addons - foreach (var mod in _modsCache) + else { - if (mod.Value is not AutoloadMod autoloadMod) + var addon = GetAddonFromFile(parsedAddonFile); + + if (addon is null || addon.Type != addonType) { - _logger.LogError($"=== Error while enabling/disabling addon {mod.Key.Id}"); continue; } - if (autoloadMod.IsEnabled) - { - EnableAddon(mod.Key); - } - else if (!autoloadMod.IsEnabled) - { - DisableAddon(mod.Key); - } + cache.Add(addon); } } - catch (Exception ex) + } + + private BaseAddon? GetLooseMapFromFile(ParsedAddonFile parsedAddonFile) + { + if (!parsedAddonFile.FileInfo.IsMap) { - _logger.LogCritical(ex, "=== Error while creating installed addons cache ==="); + return null; } - finally - { - _ = _semaphore.Release(); - ArgumentNullException.ThrowIfNull(_campaignsCache); - ArgumentNullException.ThrowIfNull(_mapsCache); - ArgumentNullException.ThrowIfNull(_modsCache); - AddonsChangedEvent?.Invoke(_game.GameEnum, addonType); - } + var bloodIniName = parsedAddonFile.FileInfo.FileName.Replace(".map", ".ini", StringComparison.InvariantCultureIgnoreCase); + var actualIni = Path.GetFileName(Directory.EnumerateFiles(parsedAddonFile.FileInfo.PathToFolder).FirstOrDefault(f => Path.GetFileName(f).Equals(bloodIniName, StringComparison.OrdinalIgnoreCase))); + + AddonId id = new(parsedAddonFile.FileInfo.FileName); + + return new LooseMap + { + AddonId = id, + Type = AddonTypeEnum.Map, + FileInfo = parsedAddonFile.FileInfo, + Title = parsedAddonFile.FileInfo.FileName, + SupportedGame = new(_game.GameEnum, null, null), + StartMap = new MapFileJsonModel { File = parsedAddonFile.FileInfo.FileName }, + BloodIni = actualIni, + GridImageHash = null, + Description = null, + Author = null, + ReleaseDate = null, + MainDef = null, + AdditionalDefs = null, + DependentAddons = null, + IncompatibleAddons = null, + RequiredFeatures = null, + PreviewImageHash = null, + Executables = null, + Options = null, + IsFavorite = _config.FavoriteAddons.Contains(id), + IsMetadataUpdateAvailable = _metadataProvider.IsMetadataUpdateAvailable(id, parsedAddonFile.FileInfo), + }; } /// - /// Add addon to cache + /// Add a single addon from its parsed file into the appropriate cache. /// - /// Path to addon file - public async Task AddAddonAsync(string pathToFile) + /// Parsed addon file to add. + public void AddAddon(ParsedAddonFile parsedAddonFile) { ArgumentNullException.ThrowIfNull(_campaignsCache); ArgumentNullException.ThrowIfNull(_mapsCache); ArgumentNullException.ThrowIfNull(_modsCache); - var addons = await GetAddonFromFileAsync(pathToFile).ConfigureAwait(false); + BaseAddon? addon; - if (addons is null or []) + if (parsedAddonFile.FileInfo.IsGrpInfo) { + var addons = GrpInfoProvider.GetAddonsFromGrpInfo(parsedAddonFile.FileInfo.PathToFile); + if (addons is not null) + { + _campaignsCache.AddRange(addons); + } + return; } - foreach (var addon in addons) + if (parsedAddonFile.FileInfo.IsMap) { - var cache = addon.Type switch - { - AddonTypeEnum.TC => _campaignsCache, - AddonTypeEnum.Map => _mapsCache, - AddonTypeEnum.Mod => _modsCache, - _ => throw new NotSupportedException(), - }; + addon = GetLooseMapFromFile(parsedAddonFile); + } + else + { + addon = GetAddonFromFile(parsedAddonFile); + } + if (addon is null) + { + return; + } - if (cache.TryGetValue(addon.AddonId, out _)) - { - cache[addon.AddonId] = addon; - } - else - { - cache.Add(addon.AddonId, addon); - } + var cache = addon.Type switch + { + AddonTypeEnum.TC => _campaignsCache, + AddonTypeEnum.Map => _mapsCache, + AddonTypeEnum.Mod => _modsCache, + AddonTypeEnum.Official => throw new NotSupportedException(), + _ => throw new NotSupportedException(), + }; + + var existing = cache.FindIndex(x => x.AddonId == addon.AddonId); + if (existing >= 0) + { + cache[existing] = addon; + } + else + { + cache.Add(addon); + } + + AddonsChangedEvent?.Invoke(_game.GameEnum, addon.Type); + } + + /// + /// Delete addon from disk and remove it from the cache. + /// + /// Addon to delete. + public void DeleteAddon(ParsedAddonFile parsedAddonFile) + { + if (parsedAddonFile.Manifest is null) + { + throw new InvalidOperationException(); + } + + var cache = GetCacheByAddonType(parsedAddonFile.Manifest.AddonType); + var addonToDelete = cache.FirstOrDefault(x => x.FileInfo is not null && x.FileInfo.Equals(parsedAddonFile.FileInfo)); - AddonsChangedEvent?.Invoke(_game.GameEnum, addon.Type); + if (addonToDelete is not null) + { + DeleteAddon(addonToDelete); } } /// - /// Delete addon from cache and disk + /// Delete addon from disk and remove it from the cache. /// - /// Addon + /// Addon to delete. public void DeleteAddon(BaseAddon addon) { ArgumentNullException.ThrowIfNull(_campaignsCache); ArgumentNullException.ThrowIfNull(_mapsCache); ArgumentNullException.ThrowIfNull(_modsCache); - ArgumentNullException.ThrowIfNull(addon.PathToFile); + ArgumentNullException.ThrowIfNull(addon.FileInfo); - if (addon.IsUnpacked) + if (addon is LooseMap map) { - Directory.Delete(Path.GetDirectoryName(addon.PathToFile)!, true); + File.Delete(map.FileInfo.PathToFile); + + var bloodIni = Path.Combine(addon.FileInfo.PathToFolder, map.BloodIni ?? string.Empty); + if (map.BloodIni is not null && + File.Exists(bloodIni)) + { + File.Delete(bloodIni); + } } - else + else if (addon.FileInfo.IsFolder) { - File.Delete(addon.PathToFile); + Directory.Delete(addon.FileInfo.PathToFolder, true); } - - if (addon is LooseMap lMap) + else { - var files = Directory.GetFiles(Path.GetDirectoryName(addon.PathToFile)!, $"{Path.GetFileNameWithoutExtension(lMap.FileName)!}.*"); - - foreach (var file in files) - { - File.Delete(file); - } + File.Delete(addon.FileInfo.PathToFile); } if (addon.Type is AddonTypeEnum.TC) { - _ = _campaignsCache.Remove(addon.AddonId); + _ = _campaignsCache.Remove(addon); } else if (addon.Type is AddonTypeEnum.Map) { - _ = _mapsCache.Remove(addon.AddonId); + _ = _mapsCache.Remove(addon); } else if (addon.Type is AddonTypeEnum.Mod) { - _ = _modsCache.Remove(addon.AddonId); + _ = _modsCache.Remove(addon); } AddonsChangedEvent?.Invoke(_game.GameEnum, addon.Type); } /// - /// Enable addon + /// Enable an autoload mod by id, cascading to dependencies and disabling incompatible mods. /// - /// Addon id + /// Addon id to enable. public void EnableAddon(AddonId addon) { - var existing = _modsCache.FirstOrDefault(x => x.Key.Equals(addon)); + var existing = _modsCache.FirstOrDefault(x => x.AddonId.Equals(addon)); - if (existing.Value is not AutoloadMod autoloadMod) + if (existing is not AutoloadMod autoloadMod) { return; } @@ -316,28 +463,28 @@ public void EnableAddon(AddonId addon) var otherVersions = _modsCache .Where(x => - x.Key.Id.Equals(addon.Id, StringComparison.OrdinalIgnoreCase) && - !VersionComparer.Compare(x.Key.Version, addon.Version, ComparisonOperatorEnum.Equals) && - !x.Value.FileName!.Equals(autoloadMod.FileName) + x.AddonId.Id.Equals(addon.Id, StringComparison.OrdinalIgnoreCase) && + !VersionComparer.Compare(x.AddonId.Version, addon.Version, ComparisonOperatorEnum.Equals) && + (x.FileInfo is null || !x.FileInfo.Equals(autoloadMod.FileInfo)) ); foreach (var version in otherVersions) { - DisableAddon(version.Key); + DisableAddon(version.AddonId); } _config.ChangeModState(addon, true); } /// - /// Disable addon + /// Disable an autoload mod by id, cascading to dependant mods. /// - /// Addon id + /// Addon id to disable. public void DisableAddon(AddonId addon) { - var existing = _modsCache.FirstOrDefault(x => x.Key.Equals(addon)); + var existing = _modsCache.FirstOrDefault(x => x.AddonId.Equals(addon)); - if (existing.Value is not AutoloadMod autoloadMod) + if (existing is not AutoloadMod autoloadMod) { return; } @@ -349,21 +496,21 @@ public void DisableAddon(AddonId addon) autoloadMod.IsEnabled = false; - var deps = _modsCache.Where(x => x.Value.DependentAddons?.ContainsKey(autoloadMod.AddonId.Id) ?? false); + var deps = _modsCache.Where(x => x.DependentAddons?.ContainsKey(autoloadMod.AddonId.Id) ?? false); foreach (var dep in deps) { - DisableAddon(dep.Key); + DisableAddon(dep.AddonId); } _config.ChangeModState(addon, false); } /// - /// Get list od installed addons of a type + /// Get list of installed addons of a given type. /// /// Addon type - public IReadOnlyDictionary GetInstalledAddonsByType(AddonTypeEnum addonType) + public IReadOnlyList GetInstalledAddonsByType(AddonTypeEnum addonType) { return addonType switch { @@ -375,13 +522,13 @@ public IReadOnlyDictionary GetInstalledAddonsByType(AddonTyp } - private IReadOnlyDictionary GetInstalledCampaigns() + private IReadOnlyList GetInstalledCampaigns() { var campaigns = _originalCampaignsProvider.GetOriginalCampaigns(_game); - if (!_semaphore.Wait(1)) + if (!_cacheUpdateSemaphore.Wait(1)) { - return campaigns; + return [.. campaigns.Values]; } try @@ -390,534 +537,111 @@ private IReadOnlyDictionary GetInstalledCampaigns() if (_campaignsCache.Count == 0) { - return campaigns; + return [.. campaigns.Values]; } if (_game.GameEnum is GameEnum.Wang) { //hack to make SW addons appear at the top of the list foreach (var customCamp in _campaignsCache - .OrderByDescending(static x => x.Key.Id.Equals(nameof(WangAddonEnum.TwinDragon), StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(static x => x.Key.Id.Equals(nameof(WangAddonEnum.Wanton), StringComparison.OrdinalIgnoreCase)) - .ThenBy(static x => x.Value.Title)) + .OrderByDescending(static x => x.AddonId.Id.Equals(nameof(WangAddonEnum.TwinDragon), StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(static x => x.AddonId.Id.Equals(nameof(WangAddonEnum.Wanton), StringComparison.OrdinalIgnoreCase)) + .ThenBy(static x => x.Title)) { - campaigns.Add(customCamp.Key, customCamp.Value); + campaigns[customCamp.AddonId] = customCamp; } } else { - foreach (var customCamp in _campaignsCache.OrderBy(static x => x.Value.Title)) + foreach (var customCamp in _campaignsCache.OrderBy(static x => x.Title)) { - campaigns.Add(customCamp.Key, customCamp.Value); + campaigns[customCamp.AddonId] = customCamp; } } - return campaigns; + return [.. campaigns.Values]; } finally { - _semaphore.Release(); + _cacheUpdateSemaphore.Release(); } } - private IReadOnlyDictionary GetInstalledMaps() + private IReadOnlyList GetInstalledMaps() { - if (!_semaphore.Wait(1)) + if (!_cacheUpdateSemaphore.Wait(1)) { - return new Dictionary(); + return []; } try { ArgumentNullException.ThrowIfNull(_mapsCache); - return _mapsCache; + return [.. _mapsCache]; } finally { - _semaphore.Release(); + _cacheUpdateSemaphore.Release(); } } - private IReadOnlyDictionary GetInstalledMods() + private IReadOnlyList GetInstalledMods() { - if (!_semaphore.Wait(1)) + if (!_cacheUpdateSemaphore.Wait(1)) { - return new Dictionary(); + return []; } try { ArgumentNullException.ThrowIfNull(_modsCache); - return _modsCache; + return [.. _modsCache]; } finally { - _semaphore.Release(); + _cacheUpdateSemaphore.Release(); } } - /// - /// Get addons from list of files - /// - /// Paths to addon files - internal async Task> GetAddonsFromFilesAsync(IEnumerable files) + private async Task UnpackAndUpdateIfNeededAsync(ParsedAddonFile parsedAddonFile) { - Dictionary addedAddons = []; - - foreach (var file in files) + if (!parsedAddonFile.FileInfo.IsZip) { - try - { - var newAddons = await GetAddonFromFileAsync(file).ConfigureAwait(false); - - if (newAddons is null or []) - { - _logger.LogInformation($"Can't get addon from file {file}"); - continue; - } - - foreach (var newAddon in newAddons) - { - try - { - if (newAddon is AutoloadMod && - addedAddons.TryGetValue(newAddon.AddonId, out var existingMod)) - { - if (existingMod.AddonId.Version is null && - newAddon.AddonId.Version is not null) - { - //replacing with addon that have version - addedAddons[newAddon.AddonId] = newAddon; - } - else if (existingMod.AddonId.Version is not null && - newAddon.AddonId.Version is not null && - VersionComparer.Compare(newAddon.AddonId.Version, existingMod.AddonId.Version, ComparisonOperatorEnum.GreaterThan)) - { - //replacing with addon that has higher version - addedAddons[newAddon.AddonId] = newAddon; - } - } - else - { - _ = addedAddons.TryAdd(newAddon.AddonId, newAddon); - } - } - catch (Exception) - { - continue; - } - } - } - catch (Exception) - { - continue; - } + return false; } - return addedAddons; - } - - /// - /// Get addon from a file - /// - /// Path to addon file - private async Task?> GetAddonFromFileAsync(string pathToFile) - { - List carcasses = []; + var unpackedTo = UnpackIfNeeded(parsedAddonFile.FileInfo); - if (pathToFile.EndsWith(".json")) + if (unpackedTo is not null) { - using var jsonStream = File.OpenRead(pathToFile); - - var manifest = await JsonSerializer.DeserializeAsync( - jsonStream, - AddonManifestJsonContext.Default.AddonManifestJsonModel - ).ConfigureAwait(false); - - if (manifest is null) - { - return null; - } - - var addonDir = Path.GetDirectoryName(pathToFile)!; - var gridFile = Directory.GetFiles(addonDir, "grid.*"); - var previewFile = Directory.GetFiles(addonDir, "preview.*"); - - long? gridImageHash = null; - - if (gridFile.Length > 0) - { - var crc = Crc32Helper.GetCrc32(gridFile[0]); - await using var stream = File.OpenRead(gridFile[0]); - _ = _bitmapsCache.TryAddGridToCache(crc, stream); - gridImageHash = crc; - } - - long? previewImageHash = null; - - if (previewFile.Length > 0) - { - var crc = Crc32Helper.GetCrc32(previewFile[0]); - await using var stream = File.OpenRead(previewFile[0]); - _ = _bitmapsCache.TryAddPreviewToCache(crc, stream); - previewImageHash = crc; - } - - var carcass = GetCarcass(manifest, pathToFile, true, gridImageHash, previewImageHash); - carcasses.Add(carcass); + await _localFilesProvider.ReplacePathAsync(parsedAddonFile.FileInfo.PathToFile, unpackedTo); + return true; } - else if (pathToFile.EndsWith(".map", StringComparison.OrdinalIgnoreCase)) - { - var bloodIni = pathToFile.Replace(".map", ".ini", StringComparison.OrdinalIgnoreCase); - var iniExists = File.Exists(bloodIni); - AddonId id = new(Path.GetFileName(pathToFile), null); - var addon = new LooseMap() - { - AddonId = id, - Type = AddonTypeEnum.Map, - Title = Path.GetFileName(pathToFile), - SupportedGame = new(_game.GameEnum, null, null), - PathToFile = pathToFile, - StartMap = new MapFileJsonModel() { File = Path.GetFileName(pathToFile) }, - BloodIni = iniExists ? bloodIni : null, - GridImageHash = null, - Description = null, - Author = null, - ReleaseDate = null, - MainDef = null, - AdditionalDefs = null, - DependentAddons = null, - IncompatibleAddons = null, - RequiredFeatures = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null, - IsFavorite = _config.FavoriteAddons.Contains(id), - IsMetadataUpdateAvailable = _metadataProvider.IsMetadataUpdateAvailable(pathToFile), - }; - - return [addon]; - } - else if (ArchiveFactory.IsArchive(pathToFile, out _)) - { - var unpackedTo = UnpackIfNeededAndGetAddonManifests(pathToFile, out var manifests); - - if (manifests is null or []) - { - return null; - } - - bool isUnpacked; - long? gridImageHash; - long? previewImageHash; - - if (unpackedTo is not null) - { - pathToFile = Path.Combine(unpackedTo, "addon.json"); - isUnpacked = true; - - var addonDir = Path.GetDirectoryName(pathToFile)!; - - var gridFile = Directory.GetFiles(addonDir, "grid.*"); - var previewFile = Directory.GetFiles(addonDir, "preview.*"); - - if (gridFile.Length > 0) - { - gridImageHash = Crc32Helper.GetCrc32(gridFile[0]); - await using var stream = File.OpenRead(gridFile[0]); - _ = _bitmapsCache.TryAddGridToCache(gridImageHash.Value, stream); - } - else - { - gridImageHash = null; - } - - if (previewFile.Length > 0) - { - previewImageHash = Crc32Helper.GetCrc32(previewFile[0]); - await using var stream = File.OpenRead(previewFile[0]); - _ = _bitmapsCache.TryAddPreviewToCache(previewImageHash.Value, stream); - } - else - { - previewImageHash = null; - } - } - else - { - isUnpacked = false; - using var archive = ArchiveFactory.OpenArchive(pathToFile); - - await using var cover = ImageHelper.GetCoverFromArchive(archive); - await using var preview = ImageHelper.GetPreviewFromArchive(archive); - - gridImageHash = cover?.Crc; - previewImageHash = preview?.Crc; - - if (cover.HasValue) - { - _ = _bitmapsCache.TryAddGridToCache(cover.Value.Crc, cover.Value.Stream); - } - - if (preview.HasValue) - { - _ = _bitmapsCache.TryAddPreviewToCache(preview.Value.Crc, preview.Value.Stream); - } - - } - - foreach (var manifest in manifests) - { - var carcass = GetCarcass(manifest, pathToFile, isUnpacked, gridImageHash, previewImageHash); - carcasses.Add(carcass); - } - } - else if (pathToFile.EndsWith(".grp", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - else - { - return null; - } - - List addons = []; - - foreach (var carcass in carcasses) - { - if (carcass.Type is AddonTypeEnum.Mod) - { - var isEnabled = !_config.DisabledAutoloadMods.Contains(carcass.Id); - - if (carcass.MainDef is not null) - { - throw new ArgumentException("Autoload mod can't have Main DEF"); - } - - AddonId id = new(carcass.Id, carcass.Version); - - var addon = new AutoloadMod() - { - AddonId = id, - Type = AddonTypeEnum.Mod, - Title = carcass.Title, - GridImageHash = carcass.GridImageHash, - PreviewImageHash = carcass.PreviewImageHash, - Description = carcass.Description, - Author = carcass.Author, - ReleaseDate = carcass.ReleaseDate, - IsEnabled = isEnabled, - PathToFile = pathToFile, - MainDef = null, - AdditionalDefs = carcass.AddDefs, - AdditionalCons = carcass.AddCons, - SupportedGame = new(carcass.SupportedGame, carcass.GameVersion, carcass.GameCrc), - DependentAddons = carcass.Dependencies, - IncompatibleAddons = carcass.Incompatibles, - StartMap = carcass.StartMap, - RequiredFeatures = carcass.RequiredFeatures, - IsUnpacked = carcass.IsUnpacked, - Executables = null, - Options = null, - IsFavorite = _config.FavoriteAddons.Contains(id), - IsMetadataUpdateAvailable = _metadataProvider.IsMetadataUpdateAvailable(pathToFile), - }; - - addons.Add(addon); - } - else - { - if (_game.GameEnum - is GameEnum.Duke3D - or GameEnum.Fury - or GameEnum.Redneck - or GameEnum.NAM - or GameEnum.WW2GI) - { - var addon = new DukeCampaign() - { - AddonId = new(carcass.Id, carcass.Version), - Type = carcass.Type, - SupportedGame = new(carcass.SupportedGame, carcass.GameVersion, carcass.GameCrc), - Title = carcass.Title, - GridImageHash = carcass.GridImageHash, - PreviewImageHash = carcass.PreviewImageHash, - Description = carcass.Description, - Author = carcass.Author, - ReleaseDate = carcass.ReleaseDate, - PathToFile = pathToFile, - DependentAddons = carcass.Dependencies, - IncompatibleAddons = carcass.Incompatibles, - StartMap = carcass.StartMap, - MainCon = carcass.MainCon, - AdditionalCons = carcass.AddCons, - MainDef = carcass.MainDef, - AdditionalDefs = carcass.AddDefs, - RTS = carcass.Rts, - RequiredFeatures = carcass.RequiredFeatures, - IsUnpacked = carcass.IsUnpacked, - Executables = carcass.Executables, - Options = carcass.Options, - IsFavorite = _config.FavoriteAddons.Contains(new(carcass.Id, carcass.Version)), - IsMetadataUpdateAvailable = _metadataProvider.IsMetadataUpdateAvailable(pathToFile), - }; - - addons.Add(addon); - } - else if (_game.GameEnum is GameEnum.Wang) - { - var addon = new GenericCampaign() - { - AddonId = new(carcass.Id, carcass.Version), - Type = carcass.Type, - SupportedGame = new(carcass.SupportedGame, carcass.GameVersion, carcass.GameCrc), - Title = carcass.Title, - GridImageHash = carcass.GridImageHash, - PreviewImageHash = carcass.PreviewImageHash, - Description = carcass.Description, - Author = carcass.Author, - ReleaseDate = carcass.ReleaseDate, - PathToFile = pathToFile, - DependentAddons = carcass.Dependencies, - IncompatibleAddons = carcass.Incompatibles, - StartMap = carcass.StartMap, - MainDef = carcass.MainDef, - AdditionalDefs = carcass.AddDefs, - RequiredFeatures = carcass.RequiredFeatures, - IsUnpacked = carcass.IsUnpacked, - Executables = carcass.Executables, - Options = carcass.Options, - IsFavorite = _config.FavoriteAddons.Contains(new(carcass.Id, carcass.Version)), - IsMetadataUpdateAvailable = _metadataProvider.IsMetadataUpdateAvailable(pathToFile), - }; - - addons.Add(addon); - } - else if (_game.GameEnum is GameEnum.Blood) - { - var addon = new BloodCampaign() - { - AddonId = new(carcass.Id, carcass.Version), - Type = carcass.Type, - SupportedGame = new(carcass.SupportedGame, carcass.GameVersion, carcass.GameCrc), - Title = carcass.Title, - GridImageHash = carcass.GridImageHash, - PreviewImageHash = carcass.PreviewImageHash, - Description = carcass.Description, - Author = carcass.Author, - ReleaseDate = carcass.ReleaseDate, - PathToFile = pathToFile, - DependentAddons = carcass.Dependencies, - IncompatibleAddons = carcass.Incompatibles, - StartMap = carcass.StartMap, - MainDef = carcass.MainDef, - AdditionalDefs = carcass.AddDefs, - INI = carcass.Ini, - RFF = carcass.Rff, - SND = carcass.Snd, - RequiredFeatures = carcass.RequiredFeatures, - IsUnpacked = carcass.IsUnpacked, - Executables = carcass.Executables, - Options = carcass.Options, - IsFavorite = _config.FavoriteAddons.Contains(new(carcass.Id, carcass.Version)), - IsMetadataUpdateAvailable = _metadataProvider.IsMetadataUpdateAvailable(pathToFile), - }; - - addons.Add(addon); - } - else if (_game.GameEnum is GameEnum.Slave) - { - var addon = new GenericCampaign() - { - AddonId = new(carcass.Id, carcass.Version), - Type = carcass.Type, - SupportedGame = new(carcass.SupportedGame, carcass.GameVersion, carcass.GameCrc), - Title = carcass.Title, - GridImageHash = carcass.GridImageHash, - PreviewImageHash = carcass.PreviewImageHash, - Description = carcass.Description, - Author = carcass.Author, - ReleaseDate = carcass.ReleaseDate, - PathToFile = pathToFile, - DependentAddons = carcass.Dependencies, - IncompatibleAddons = carcass.Incompatibles, - StartMap = carcass.StartMap, - MainDef = carcass.MainDef, - AdditionalDefs = carcass.AddDefs, - RequiredFeatures = carcass.RequiredFeatures, - IsUnpacked = carcass.IsUnpacked, - Executables = carcass.Executables, - Options = carcass.Options, - IsFavorite = _config.FavoriteAddons.Contains(new(carcass.Id, carcass.Version)), - IsMetadataUpdateAvailable = _metadataProvider.IsMetadataUpdateAvailable(pathToFile), - }; - - addons.Add(addon); - } - else if (_game.GameEnum is GameEnum.Standalone) - { - var addon = new Addons.StandaloneGame() - { - AddonId = new(carcass.Id, carcass.Version), - Type = carcass.Type, - SupportedGame = new(carcass.SupportedGame, carcass.GameVersion, carcass.GameCrc), - Title = carcass.Title, - GridImageHash = carcass.GridImageHash, - PreviewImageHash = carcass.PreviewImageHash, - Description = carcass.Description, - Author = carcass.Author, - ReleaseDate = carcass.ReleaseDate, - PathToFile = pathToFile, - DependentAddons = carcass.Dependencies, - IncompatibleAddons = carcass.Incompatibles, - StartMap = carcass.StartMap, - MainDef = carcass.MainDef, - AdditionalDefs = carcass.AddDefs, - RequiredFeatures = carcass.RequiredFeatures, - IsUnpacked = carcass.IsUnpacked, - Executables = carcass.Executables, - Options = carcass.Options, - IsFavorite = _config.FavoriteAddons.Contains(new(carcass.Id, carcass.Version)), - IsMetadataUpdateAvailable = _metadataProvider.IsMetadataUpdateAvailable(pathToFile), - }; - - addons.Add(addon); - } - else - { - throw new NotSupportedException(); - } - } - } - - return addons; + return false; } /// - /// Unpack archive if needed and return path to folder + /// Unpack an archive if it contains addon manifests or GRP info files. /// - /// Path to archive - /// Addon manifests - /// Path to unpacked folder or if not unpacked. - private string? UnpackIfNeededAndGetAddonManifests(string pathToFile, out List? manifests) + /// Path to archive. + /// Path to unpacked folder, or if the archive was not unpacked. + private string? UnpackIfNeeded(AddonFilePathWrapper pathToFile) { try { - using var archive = ArchiveFactory.OpenArchive(pathToFile); + using var archive = ArchiveFactory.OpenArchive(pathToFile.PathToFile); string? unpackedTo = null; if (archive.Entries.Any(static x => x.Key!.Equals("addons.grpinfo", StringComparison.OrdinalIgnoreCase))) { //need to unpack archive with grpinfo - unpackedTo = Unpack(pathToFile, archive); - manifests = null; - archive?.Dispose(); - File.Delete(pathToFile); + unpackedTo = Unpack(pathToFile.PathToFile, archive); + archive.Dispose(); + File.Delete(pathToFile.PathToFile); return unpackedTo; } @@ -928,7 +652,6 @@ or GameEnum.NAM if (addonJsonsInsideArchive.Count == 0) { - manifests = null; return null; } @@ -937,25 +660,30 @@ or GameEnum.NAM var addonDto = JsonSerializer.Deserialize( addonJsonStream, AddonManifestJsonContext.Default.AddonManifestJsonModel - )!; + ); + + if (addonDto is null) + { + return null; + } if (addonDto.MainRff is not null || addonDto.SoundRff is not null) { //need to unpack addons that contain custom RFF files - unpackedTo = Unpack(pathToFile, archive); + unpackedTo = Unpack(pathToFile.PathToFile, archive); } else if (addonDto.Executables is not null) { //need to unpack addons with custom executables - unpackedTo = Unpack(pathToFile, archive); + unpackedTo = Unpack(pathToFile.PathToFile, archive); } List result = []; if (unpackedTo is not null) { - archive?.Dispose(); - File.Delete(pathToFile); + archive.Dispose(); + File.Delete(pathToFile.PathToFile); var unpackedAddonJsons = Directory.GetFiles(unpackedTo, "addon*.json"); @@ -986,25 +714,22 @@ or GameEnum.NAM } } - manifests = result.Count > 0 ? result : null; return unpackedTo; } catch (Exception ex) { _logger.LogCritical(ex, "=== Error while unpacking archive ==="); - - manifests = null; return null; } } /// - /// Unpack archive and return path to folder + /// Extract archive contents to a subfolder named after the archive. /// - /// Path to archive - /// Archive - /// Path to unpacked folder. - private string Unpack(string pathToFile, IArchive archive) + /// Path to archive. + /// Archive to extract. + /// Path to the unpacked folder. + private static string Unpack(string pathToFile, IArchive archive) { var fileFolder = Path.GetDirectoryName(pathToFile)!; var unpackTo = Path.Combine(fileFolder, Path.GetFileNameWithoutExtension(pathToFile)); @@ -1024,16 +749,208 @@ private string Unpack(string pathToFile, IArchive archive) return unpackTo; } - private AddonCarcass GetCarcass( + /// + /// Convert a parsed addon file into a domain addon object + /// + /// Parsed addon file to convert. + internal BaseAddon? GetAddonFromFile(ParsedAddonFile parsedAddonFile) + { + if (parsedAddonFile.Manifest is null) + { + throw new InvalidOperationException($"{nameof(GetAddonFromFile)} requires a non-null manifest. File: {parsedAddonFile.FileInfo.PathToFile}"); + } + + AddonCarcass? carcass; + BaseAddon? addon; + + if (parsedAddonFile.FileInfo.IsJson || parsedAddonFile.FileInfo.IsZip || parsedAddonFile.FileInfo.IsFolder) + { + carcass = GetCarcass(parsedAddonFile.Manifest, parsedAddonFile.FileInfo, parsedAddonFile.GridHash, parsedAddonFile.PreviewHash); + } + else if (parsedAddonFile.FileInfo.IsGrpInfo || parsedAddonFile.FileInfo.IsMap) + { + throw new NotSupportedException(); + } + else + { + return null; + } + + var carcassValue = carcass.Value; + if (carcassValue.Type is AddonTypeEnum.Mod) + { + var isEnabled = !_config.DisabledAutoloadMods.Contains(carcassValue.Id); + + if (carcassValue.MainDef is not null) + { + throw new ArgumentException("Autoload mod can't have Main DEF"); + } + + AddonId id = new(carcassValue.Id, carcassValue.Version); + addon = new AutoloadMod + { + AddonId = id, + Type = AddonTypeEnum.Mod, + Title = carcassValue.Title, + GridImageHash = carcassValue.GridImageHash, + PreviewImageHash = carcassValue.PreviewImageHash, + Description = carcassValue.Description, + Author = carcassValue.Author, + ReleaseDate = carcassValue.ReleaseDate, + IsEnabled = isEnabled, + FileInfo = parsedAddonFile.FileInfo, + MainDef = null, + AdditionalDefs = carcassValue.AddDefs, + AdditionalCons = carcassValue.AddCons, + SupportedGame = new(carcassValue.SupportedGame, carcassValue.GameVersion, carcassValue.GameCrc), + DependentAddons = carcassValue.Dependencies, + IncompatibleAddons = carcassValue.Incompatibles, + StartMap = carcassValue.StartMap, + RequiredFeatures = carcassValue.RequiredFeatures, + Executables = null, + Options = null, + IsFavorite = _config.FavoriteAddons.Contains(id), + IsMetadataUpdateAvailable = _metadataProvider.IsMetadataUpdateAvailable(id, parsedAddonFile.FileInfo), + }; + } + else + { + addon = CreateCampaignAddon(carcassValue, parsedAddonFile.FileInfo); + } + + return addon; + } + + private BaseAddon CreateCampaignAddon(AddonCarcass carcass, AddonFilePathWrapper fileInfo) + { + AddonId id = new(carcass.Id, carcass.Version); + var game = new GameInfo(carcass.SupportedGame, carcass.GameVersion, carcass.GameCrc); + var isFavorite = _config.FavoriteAddons.Contains(id); + var isUpdate = _metadataProvider.IsMetadataUpdateAvailable(id, fileInfo); + + return _game.GameEnum switch + { + GameEnum.Duke3D + or GameEnum.Fury + or GameEnum.Redneck + or GameEnum.NAM + or GameEnum.WW2GI => + new DukeCampaign + { + AddonId = id, + Type = carcass.Type, + Title = carcass.Title, + GridImageHash = carcass.GridImageHash, + PreviewImageHash = carcass.PreviewImageHash, + Description = carcass.Description, + Author = carcass.Author, + ReleaseDate = carcass.ReleaseDate, + FileInfo = fileInfo, + DependentAddons = carcass.Dependencies, + IncompatibleAddons = carcass.Incompatibles, + StartMap = carcass.StartMap, + MainCon = carcass.MainCon, + AdditionalCons = carcass.AddCons, + MainDef = carcass.MainDef, + AdditionalDefs = carcass.AddDefs, + RTS = carcass.Rts, + RequiredFeatures = carcass.RequiredFeatures, + Executables = carcass.Executables, + Options = carcass.Options, + SupportedGame = game, + IsFavorite = isFavorite, + IsMetadataUpdateAvailable = isUpdate, + }, + GameEnum.Wang or GameEnum.Slave => + new GenericCampaign + { + AddonId = id, + Type = carcass.Type, + Title = carcass.Title, + GridImageHash = carcass.GridImageHash, + PreviewImageHash = carcass.PreviewImageHash, + Description = carcass.Description, + Author = carcass.Author, + ReleaseDate = carcass.ReleaseDate, + FileInfo = fileInfo, + DependentAddons = carcass.Dependencies, + IncompatibleAddons = carcass.Incompatibles, + StartMap = carcass.StartMap, + MainDef = carcass.MainDef, + AdditionalDefs = carcass.AddDefs, + RequiredFeatures = carcass.RequiredFeatures, + Executables = carcass.Executables, + Options = carcass.Options, + SupportedGame = game, + IsFavorite = isFavorite, + IsMetadataUpdateAvailable = isUpdate, + }, + GameEnum.Blood => + new BloodCampaign + { + AddonId = id, + Type = carcass.Type, + Title = carcass.Title, + GridImageHash = carcass.GridImageHash, + PreviewImageHash = carcass.PreviewImageHash, + Description = carcass.Description, + Author = carcass.Author, + ReleaseDate = carcass.ReleaseDate, + FileInfo = fileInfo, + DependentAddons = carcass.Dependencies, + IncompatibleAddons = carcass.Incompatibles, + StartMap = carcass.StartMap, + MainDef = carcass.MainDef, + AdditionalDefs = carcass.AddDefs, + INI = carcass.Ini, + RFF = carcass.Rff, + SND = carcass.Snd, + RequiredFeatures = carcass.RequiredFeatures, + Executables = carcass.Executables, + Options = carcass.Options, + SupportedGame = game, + IsFavorite = isFavorite, + IsMetadataUpdateAvailable = isUpdate, + }, + GameEnum.Standalone => + new StandaloneGame + { + AddonId = id, + Type = carcass.Type, + Title = carcass.Title, + GridImageHash = carcass.GridImageHash, + PreviewImageHash = carcass.PreviewImageHash, + Description = carcass.Description, + Author = carcass.Author, + ReleaseDate = carcass.ReleaseDate, + FileInfo = fileInfo, + DependentAddons = carcass.Dependencies, + IncompatibleAddons = carcass.Incompatibles, + StartMap = carcass.StartMap, + MainDef = carcass.MainDef, + AdditionalDefs = carcass.AddDefs, + RequiredFeatures = carcass.RequiredFeatures, + Executables = carcass.Executables, + Options = carcass.Options, + SupportedGame = game, + IsFavorite = isFavorite, + IsMetadataUpdateAvailable = isUpdate, + }, + _ => throw new NotSupportedException(), + }; + } + + /// + /// Build an from a manifest and file metadata. + /// + private static AddonCarcass GetCarcass( AddonManifestJsonModel manifest, - string pathToFile, - bool isUnpacked, + AddonFilePathWrapper fileInfo, long? gridImageHash, long? previewImageHash) { AddonCarcass carcass = new() { - IsUnpacked = isUnpacked, GridImageHash = gridImageHash ?? previewImageHash, PreviewImageHash = previewImageHash, Type = manifest.AddonType, @@ -1051,7 +968,7 @@ private AddonCarcass GetCarcass( Rff = manifest.MainRff, Snd = manifest.SoundRff, StartMap = manifest.StartMap, - RequiredFeatures = manifest.Dependencies?.RequiredFeatures?.Select(static x => x)?.ToImmutableArray(), + RequiredFeatures = manifest.Dependencies?.RequiredFeatures?.ToImmutableArray(), MainCon = manifest.MainCon, AddCons = manifest.AdditionalCons?.ToImmutableArray(), MainDef = manifest.MainDef, @@ -1070,7 +987,7 @@ private AddonCarcass GetCarcass( foreach (var x in osPortsPair.Value) { - carcass.Executables[osPortsPair.Key].Add(x.Key, Path.Combine(Path.GetDirectoryName(pathToFile)!, x.Value)); + carcass.Executables[osPortsPair.Key].Add(x.Key, Path.Combine(fileInfo.PathToFolder, x.Value)); } } } @@ -1105,57 +1022,69 @@ private AddonCarcass GetCarcass( } - private async void OnMetadataUpdatedAsync(object? sender, ValueTuple? e) + private void OnMetadataInitialized(object? sender, EventArgs e) { - if (e is not null - && _game.GameEnum != e.Value.Item1) - { - return; - } - - IEnumerable allAddons = [.. _campaignsCache.Values, .. _mapsCache.Values, .. _modsCache.Values]; + IEnumerable allAddons = [.. _campaignsCache, .. _mapsCache, .. _modsCache]; foreach (var camp in allAddons) { - if (camp.PathToFile is null) + if (camp.FileInfo is null) { continue; } - if (e is null) + camp.IsMetadataUpdateAvailable = _metadataProvider.IsMetadataUpdateAvailable(camp.AddonId, camp.FileInfo); + } + } + + private void OnMetadataUpdated(object? sender, ParsedAddonFile e) + { + try + { + if (_game.GameEnum != e.Manifest.SupportedGame.Game) { - camp.IsMetadataUpdateAvailable = _metadataProvider.IsMetadataUpdateAvailable(camp.PathToFile); + return; } - else if (camp.PathToFile.Equals(e.Value.Item3) - && camp.IsMetadataUpdateAvailable - && !_metadataProvider.IsMetadataUpdateAvailable(camp.PathToFile)) - { - _campaignsCache.Remove(camp.AddonId); - await AddAddonAsync(e.Value.Item3).ConfigureAwait(false); - AddonsChangedEvent?.Invoke(_game.GameEnum, camp.Type); + var cache = GetCacheByAddonType(e.Manifest.AddonType); + + var oldVersion = cache.FirstOrDefault(x => x.AddonId == e.Manifest.AddonId); + + if (oldVersion is null) + { + throw new InvalidOperationException($"Metadata update target not found in cache: {e.Manifest.AddonId}"); } - } - if (e is null) - { - AddonsChangedEvent?.Invoke(_game.GameEnum, AddonTypeEnum.TC); - AddonsChangedEvent?.Invoke(_game.GameEnum, AddonTypeEnum.Map); - AddonsChangedEvent?.Invoke(_game.GameEnum, AddonTypeEnum.Mod); + cache.Remove(oldVersion); + AddAddon(e); + + AddonsChangedEvent?.Invoke(_game.GameEnum, e.Manifest.AddonType); } - else + catch (Exception ex) { - AddonsChangedEvent?.Invoke(_game.GameEnum, e.Value.Item2); + _logger.LogCritical(ex, "Error while updating metadata."); } } + /// + /// Unsubscribe from events and release resources. + /// public void Dispose() { - _metadataProvider.MetadataUpdatedEvent -= OnMetadataUpdatedAsync; + _metadataProvider.MetadataUpdatedEvent -= OnMetadataUpdated; + _metadataProvider.MetadataInitializedEvent -= OnMetadataInitialized; + + _channelCancellation.Cancel(); + _channelCancellation.Dispose(); + _cacheUpdateSemaphore.Dispose(); + _channelPublisher.Unsubscribe(_channelReader); } } +/// +/// Intermediate representation built from an addon manifest before converting to a domain . +/// internal struct AddonCarcass { public GameEnum SupportedGame { get; init; } @@ -1163,7 +1092,6 @@ internal struct AddonCarcass public string Title { get; init; } public AddonTypeEnum Type { get; init; } public string Version { get; init; } - public bool IsUnpacked { get; init; } public string? Author { get; init; } public DateOnly? ReleaseDate { get; init; } public string? Description { get; init; } diff --git a/src/Addons/Providers/InstalledAddonsProviderFactory.cs b/src/Addons/Providers/InstalledAddonsProviderFactory.cs index 8736046b..da97ddf6 100644 --- a/src/Addons/Providers/InstalledAddonsProviderFactory.cs +++ b/src/Addons/Providers/InstalledAddonsProviderFactory.cs @@ -1,8 +1,8 @@ -using Core.All.Enums; -using Core.Client.Cache; +using Core.All; +using Core.All.Enums; using Core.Client.Enums; +using Core.Client.Helpers; using Core.Client.Interfaces; -using Core.Client.Providers; using Games.Games; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -13,23 +13,26 @@ public sealed class InstalledAddonsProviderFactory { private readonly Dictionary _list = []; private readonly IConfigProvider _config; - private readonly ICacheAdder _bitmapsCache; private readonly OriginalCampaignsProvider _originalCampaignsProvider; private readonly MetadataProvider _metadataProvider; + private readonly LocalFilesProvider _localFilesProvider; + private readonly IChannelSubscriber _channelPublisher; private readonly ILoggerFactory _loggerFactory; public InstalledAddonsProviderFactory( IConfigProvider config, - [FromKeyedServices(KeyedServicesEnum.Bitmaps)] ICacheAdder bitmapsCache, OriginalCampaignsProvider originalCampaignsProvider, MetadataProvider metadataProvider, + LocalFilesProvider localFilesProvider, + [FromKeyedServices(KeyedServicesEnum.LocalFilesChannel)] IChannelSubscriber channelPublisher, ILoggerFactory loggerFactory ) { _config = config; - _bitmapsCache = bitmapsCache; _originalCampaignsProvider = originalCampaignsProvider; _metadataProvider = metadataProvider; + _localFilesProvider = localFilesProvider; + _channelPublisher = channelPublisher; _loggerFactory = loggerFactory; } @@ -48,10 +51,10 @@ public InstalledAddonsProvider Get(BaseGame game) InstalledAddonsProvider newProvider = new( game, _config, - _bitmapsCache, _originalCampaignsProvider, - _metadataProvider -, + _metadataProvider, + _localFilesProvider, + _channelPublisher, _loggerFactory.CreateLogger()); #pragma warning restore CS0618 // Type or member is obsolete _list.Add(game.GameEnum, newProvider); diff --git a/src/Addons/Providers/LocalFilesProvider.cs b/src/Addons/Providers/LocalFilesProvider.cs new file mode 100644 index 00000000..c1dcd7e0 --- /dev/null +++ b/src/Addons/Providers/LocalFilesProvider.cs @@ -0,0 +1,468 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Core.All; +using Core.All.Enums; +using Core.All.Helpers; +using Core.All.Serializable.Addon; +using Core.Client.Cache; +using Core.Client.Enums; +using Core.Client.Helpers; +using Games.Games; +using Games.Providers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SharpCompress.Archives; + +namespace Addons.Providers; + +/// +/// Scans the addons folder for manifests, caches parsed results, and loads grid/preview images. +/// +public sealed class LocalFilesProvider +{ + private static readonly string _manifestNameBase = Path.GetFileNameWithoutExtension(CommonConstants.AddonManifestName); + private static readonly string _manifestNameExt = Path.GetExtension(CommonConstants.AddonManifestName); + + private readonly InstalledGamesProvider _gamesProvider; + private readonly ICacheAdder _bitmapsCache; + private readonly IChannelPublisher _channelPublisher; + private readonly ILogger _logger; + private readonly SemaphoreSlim _cacheUpdateSemaphore = new(1, 1); + + private List? _cachedDataDict; + + private static readonly EnumerationOptions RecursiveOptions = new() + { + MatchCasing = MatchCasing.CaseInsensitive, + RecurseSubdirectories = true + }; + + private static readonly EnumerationOptions NonRecursiveOptions = new() + { + MatchCasing = MatchCasing.CaseInsensitive, + RecurseSubdirectories = false + }; + + [MemberNotNullWhen(true, nameof(_cachedDataDict))] + public bool IsInitialized => _cachedDataDict is not null; + + public LocalFilesProvider( + InstalledGamesProvider gamesProvider, + [FromKeyedServices(KeyedServicesEnum.Bitmaps)] ICacheAdder bitmapsCache, + [FromKeyedServices(KeyedServicesEnum.LocalFilesChannel)] IChannelPublisher channelPublisher, + ILogger logger + ) + { + _gamesProvider = gamesProvider; + _bitmapsCache = bitmapsCache; + _channelPublisher = channelPublisher; + _logger = logger; + } + + /// + /// Scan all zip and manifest files in the addons folder and populate the cache. + /// + public async Task InitializeAsync() + { + if (IsInitialized) + { + return true; + } + + await _cacheUpdateSemaphore.WaitAsync().ConfigureAwait(false); + + try + { + if (IsInitialized) + { + return true; + } + + if (!Directory.Exists(ClientProperties.AddonsFolderPath)) + { + _cachedDataDict = []; + return true; + } + + var results = new List(); + + foreach (var zip in Directory.EnumerateFiles(ClientProperties.AddonsFolderPath, "*.zip", RecursiveOptions)) + { + var result = await ProcessArchiveAsync(zip); + if (result is not null) + { + results.AddRange(result); + } + + } + + var manifestPattern = $"{_manifestNameBase}*{_manifestNameExt}"; + foreach (var manifest in Directory.EnumerateFiles(ClientProperties.AddonsFolderPath, manifestPattern, RecursiveOptions)) + { + var result = await ProcessManifestFileAsync(manifest); + if (result is not null) + { + results.Add(result); + } + } + + foreach (var grpinfo in Directory.EnumerateFiles(ClientProperties.AddonsFolderPath, "*.grpinfo", RecursiveOptions)) + { + results.Add(CreateSimpleEntry(grpinfo, GameEnum.Duke3D)); + } + + foreach (var game in _gamesProvider.GetGames()) + { + if (!Directory.Exists(game.MapsFolderPath)) + { + continue; + } + + foreach (var map in Directory.EnumerateFiles(game.MapsFolderPath, "*.map", NonRecursiveOptions)) + { + results.Add(CreateSimpleEntry(map, game.GameEnum)); + } + } + + _cachedDataDict = results; + return true; + } + catch (Exception ex) + { + _logger.LogCritical(ex, "Failed to initialize addon cache"); + return false; + } + finally + { + _cacheUpdateSemaphore.Release(); + } + } + + /// + /// Try to retrieve a previously cached parsed addon file by its file descriptor. + /// + public bool TryGetCachedAddonFile(AddonFilePathWrapper fileInfo, [NotNullWhen(true)] out ParsedAddonFile? file) + { + if (_cachedDataDict is null) + { + file = null; + return false; + } + + file = _cachedDataDict.FirstOrDefault(x => x.FileInfo.Equals(fileInfo)); + return file is not null; + } + + /// + /// Updates all cached entries whose file path matches to point to . + /// + /// The old file path to replace (typically a zip path). + /// The new folder path. + /// The list of updated entries. + public async Task> ReplacePathAsync(string oldPathToFile, string newFolderPath) + { + if (!IsInitialized) + { + throw new InvalidOperationException("Cache is not initialized."); + } + + try + { + await _cacheUpdateSemaphore.WaitAsync(); + + var existingFiles = _cachedDataDict.Where(x => + x.FileInfo.PathToFile.Equals(oldPathToFile, StringComparison.InvariantCultureIgnoreCase) || + x.FileInfo.PathToFolder.Equals(oldPathToFile, StringComparison.InvariantCultureIgnoreCase)) + .ToList(); + + var updatedPaths = new List(existingFiles.Count); + + foreach (var file in existingFiles) + { + var newFileInfo = file.FileInfo.WithChangedFolder(newFolderPath); + var newFile = file with + { + FileInfo = newFileInfo + }; + + _cachedDataDict.Remove(file); + _cachedDataDict.Add(newFile); + + updatedPaths.Add(newFile); + } + + await _channelPublisher.PublishAsync(new() { Files = [.. existingFiles], IsAdded = false}); + await _channelPublisher.PublishAsync(new() { Files = [.. updatedPaths], IsAdded = true}); + + return updatedPaths; + } + finally + { + _cacheUpdateSemaphore.Release(); + } + } + + /// + /// Return cached list of parsed addon files, initializing the scanner first if needed. + /// + public async Task> GetCachedAddonFilesAsync() + { + if (!IsInitialized) + { + await InitializeAsync().ConfigureAwait(false); + } + + if (_cachedDataDict is null) + { + throw new InvalidOperationException("Initialization failed, cache is null."); + } + + return [.. _cachedDataDict]; + } + + /// + /// Add a single file to the cache by parsing it as a zip archive or manifest. + /// + public async Task?> TryAddFileToCacheAsync(string pathToFile, GameEnum? gameEnum) + { + try + { + await _cacheUpdateSemaphore.WaitAsync(); + + var results = await ProcessFileAsync(pathToFile, gameEnum); + + if (results is not null && _cachedDataDict is not null) + { + _cachedDataDict.AddRange(results); + await _channelPublisher.PublishAsync(new() + { + Files = [.. results], + IsAdded = true + }); + } + + return results; + } + finally + { + _cacheUpdateSemaphore.Release(); + } + } + + /// + /// Remove all cached entries whose physical file matches . + /// Handles zips (multiple entries), folders (manifests), .map files and .grpinfo files. + /// + public async Task TryRemoveFileFromCacheAsync(string pathToFile) + { + if (!IsInitialized) + { + return false; + } + + try + { + await _cacheUpdateSemaphore.WaitAsync(); + + var toRemove = _cachedDataDict + .Where(x => + x.FileInfo.PathToFile.Equals(pathToFile, StringComparison.InvariantCultureIgnoreCase) || + x.FileInfo.PathToFolder.Equals(pathToFile, StringComparison.InvariantCultureIgnoreCase)) + .ToList(); + + if (toRemove.Count == 0) + { + return false; + } + + foreach (var file in toRemove) + { + _cachedDataDict.Remove(file); + } + + await _channelPublisher.PublishAsync(new() { Files = [.. toRemove], IsAdded = false}); + return true; + } + finally + { + _cacheUpdateSemaphore.Release(); + } + } + + private static ParsedAddonFile CreateSimpleEntry(string path, GameEnum gameEnum) + { + return new ParsedAddonFile + { + FileInfo = new(Path.GetDirectoryName(path), Path.GetFileName(path)), + SupportedGame = gameEnum, + Manifest = null, + GridHash = null, + PreviewHash = null + }; + } + + private async Task?> ProcessFileAsync(string pathToFile, GameEnum? gameEnum) + { + if (pathToFile.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + { + var result = await ProcessArchiveAsync(pathToFile); + + if (result is not null) + { + return [..result]; + } + } + else if (pathToFile.EndsWith(_manifestNameExt, StringComparison.OrdinalIgnoreCase) && + Path.GetFileName(pathToFile).StartsWith(_manifestNameBase, StringComparison.OrdinalIgnoreCase)) + { + var result = await ProcessManifestFileAsync(pathToFile); + + if (result is not null) + { + return [result]; + } + } + else if (pathToFile.EndsWith(".grpinfo", StringComparison.OrdinalIgnoreCase)) + { + return [CreateSimpleEntry(pathToFile, GameEnum.Duke3D)]; + } + else if (pathToFile.EndsWith(".map", StringComparison.OrdinalIgnoreCase)) + { + return [CreateSimpleEntry(pathToFile, gameEnum ?? throw new InvalidOperationException())]; + } + + return null; + } + + /// + /// Parse a single JSON manifest file and load its grid/preview images. + /// + private async Task ProcessManifestFileAsync(string file) + { + var folderPath = Path.GetDirectoryName(file); + if (folderPath is null) + { + return null; + } + + await using var stream = File.OpenRead(file); + var manifest = await JsonSerializer.DeserializeAsync(stream, AddonManifestJsonContext.Default.AddonManifestJsonModel).ConfigureAwait(false); + + string? gridFile = null; + string? previewFile = null; + foreach (var f in Directory.EnumerateFiles(folderPath)) + { + var name = Path.GetFileNameWithoutExtension(f.AsSpan()); + if (name.Equals("grid", StringComparison.OrdinalIgnoreCase)) + gridFile = f; + else if (name.Equals("preview", StringComparison.OrdinalIgnoreCase)) + previewFile = f; + + if (gridFile is not null && previewFile is not null) + break; + } + + long? gridHash = null; + long? previewHash = null; + + if (gridFile is not null) + { + await using var cover = File.OpenRead(gridFile); + gridHash = Crc32Helper.GetCrc32(gridFile); + _bitmapsCache.TryAddGridToCache(gridHash.Value, cover); + } + + if (previewFile is not null) + { + await using var previewStream = File.OpenRead(previewFile); + previewHash = Crc32Helper.GetCrc32(previewFile); + _bitmapsCache.TryAddPreviewToCache(previewHash.Value, previewStream); + } + + if (manifest is not null) + { + return new ParsedAddonFile + { + FileInfo = new(Path.GetDirectoryName(file), Path.GetFileName(file)), + SupportedGame = manifest.SupportedGame.Game, + Manifest = manifest, + GridHash = gridHash, + PreviewHash = previewHash + }; + } + + return null; + } + + /// + /// Parse manifests inside a zip archive and load any embedded grid/preview images. + /// + private async Task?> ProcessArchiveAsync(string file) + { + List? results = null; + using var archive = ArchiveFactory.OpenArchive(file); + + await using var cover = ImageHelper.GetCoverFromArchive(archive); + await using var preview = ImageHelper.GetPreviewFromArchive(archive); + + var gridHash = cover?.Crc; + var previewHash = preview?.Crc; + + if (cover.HasValue) + { + _bitmapsCache.TryAddGridToCache(cover.Value.Crc, cover.Value.Stream); + } + + if (preview.HasValue) + { + _bitmapsCache.TryAddPreviewToCache(preview.Value.Crc, preview.Value.Stream); + } + + var manifests = archive.Entries.Where(x => + x.Key!.Contains(Path.GetFileNameWithoutExtension(CommonConstants.AddonManifestName), StringComparison.OrdinalIgnoreCase) + && x.Key.EndsWith(Path.GetExtension(CommonConstants.AddonManifestName)) + ); + + var grpInfos = archive.Entries.Where(x => + x.Key!.EndsWith(".grpinfo") + ); + + foreach (var manifestEntry in manifests) + { + await using var stream = await manifestEntry.OpenEntryStreamAsync().ConfigureAwait(false); + var manifest = await JsonSerializer.DeserializeAsync(stream, AddonManifestJsonContext.Default.AddonManifestJsonModel).ConfigureAwait(false); + + if (manifest is null) + { + continue; + } + + results ??= []; + + results.Add(new ParsedAddonFile + { + FileInfo = new(file, manifestEntry.Key!), + SupportedGame = manifest.SupportedGame.Game, + Manifest = manifest, + GridHash = gridHash, + PreviewHash = previewHash + }); + } + + foreach (var grpInfo in grpInfos) + { + results ??= []; + + results.Add(new ParsedAddonFile + { + FileInfo = new(file, grpInfo.Key!), + SupportedGame = GameEnum.Duke3D, + Manifest = null, + GridHash = null, + PreviewHash = null + }); + } + + return results; + } +} diff --git a/src/Addons/Providers/MetadataProvider.cs b/src/Addons/Providers/MetadataProvider.cs new file mode 100644 index 00000000..e2971bea --- /dev/null +++ b/src/Addons/Providers/MetadataProvider.cs @@ -0,0 +1,177 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Core.All; +using Core.All.Serializable.Addon; +using Core.Client.Helpers; +using Core.Client.Interfaces; +using Microsoft.Extensions.Logging; +using SharpCompress.Archives; +using SharpCompress.Archives.Zip; +using SharpCompress.Common; + +namespace Addons.Providers; + +/// +/// Checks for and applies remote metadata updates to locally installed addons. +/// +public sealed class MetadataProvider +{ + /// Raised when metadata has been loaded from the API and the lookup dictionary is ready. + public event EventHandler? MetadataInitializedEvent; + + /// Raised when a metadata update has been applied to an addon file. + public event EventHandler? MetadataUpdatedEvent; + + private readonly LocalFilesProvider _localFilesProvider; + private readonly IApiInterface _apiInterface; + private readonly ILogger _logger; + private readonly SemaphoreSlim _semaphore = new(1, 1); + + private readonly Dictionary _updatesCache = []; + private Dictionary? _metaDict; + + [MemberNotNullWhen(true, nameof(_metaDict))] + public bool IsInitialized => _metaDict is not null; + + public MetadataProvider( + LocalFilesProvider localFilesProvider, + IApiInterface apiInterface, + ILogger logger + ) + { + _localFilesProvider = localFilesProvider; + _apiInterface = apiInterface; + _logger = logger; + } + + /// + /// Load metadata from the API and build an internal lookup dictionary. + /// + public async Task InitializeAsync() + { + if (IsInitialized) + { + return true; + } + + await _semaphore.WaitAsync().ConfigureAwait(false); + + try + { + var metadata = await _apiInterface.GetMetadataAsync().ConfigureAwait(false); + + if (metadata is null) + { + return false; + } + + _metaDict = metadata.ToDictionary(x => new AddonId(x.Id, x.Version)); + + MetadataInitializedEvent?.Invoke(this, EventArgs.Empty); + + return true; + } + catch (Exception ex) + { + _logger.LogCritical(ex, "Failed to initialize metadata cache"); + return false; + } + finally + { + _semaphore.Release(); + } + } + + /// + /// Return true if a newer version of the manifest is available for . + /// + public bool IsMetadataUpdateAvailable(AddonId addonId, AddonFilePathWrapper fileInfo) + { + if (_updatesCache.TryGetValue(fileInfo, out _)) + { + return true; + } + + if (_metaDict is null || !_metaDict.TryGetValue(addonId, out var actualVersion)) + { + return false; + } + + if (!_localFilesProvider.TryGetCachedAddonFile(fileInfo, out var originalManifest)) + { + return false; + } + + var originalManifestStr = JsonSerializer.Serialize(originalManifest.Manifest, AddonManifestJsonContext.Default.AddonManifestJsonModel); + var newManifestStr = JsonSerializer.Serialize(actualVersion, AddonManifestJsonContext.Default.AddonManifestJsonModel); + + if (originalManifestStr.Equals(newManifestStr)) + { + return false; + } + + var newManifest = originalManifest with { Manifest = actualVersion }; + if (!_updatesCache.TryAdd(newManifest.FileInfo, newManifest)) + { + _updatesCache[newManifest.FileInfo] = newManifest; + } + + return true; + } + + /// + /// Apply a pending manifest update by rewriting the addon file on disk. + /// + public async Task> UpdateMetadataAsync(AddonFilePathWrapper fileInfo) + { + try + { + if (!_updatesCache.TryGetValue(fileInfo, out var update)) + { + return new(ResultEnum.Error, false, string.Empty); + } + + if (fileInfo.IsZip) + { + var tempPath = fileInfo.PathToFile + ".temp"; + + using (var archive = ZipArchive.OpenArchive(fileInfo.PathToFile)) + { + var existing = archive.Entries.FirstOrDefault(x => x.Key.Equals(Path.GetFileName(update.FileInfo.FileName))); + + if (existing is not null) + { + archive.RemoveEntry(existing); + } + + using var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, update.Manifest, AddonManifestJsonContext.Default.AddonManifestJsonModel).ConfigureAwait(false); + + archive.AddEntry(Path.GetFileName(update.FileInfo.FileName), ms); + + archive.SaveTo(tempPath, new(CompressionType.None)); + } + + File.Delete(fileInfo.PathToFile); + File.Move(tempPath, fileInfo.PathToFile); + + _updatesCache.Remove(fileInfo); + + MetadataUpdatedEvent?.Invoke(this, update); + } + else if (fileInfo.IsFolder) + { + var addonJson = JsonSerializer.Serialize(update.Manifest, AddonManifestJsonContext.Default.AddonManifestJsonModel); + await File.WriteAllTextAsync(fileInfo.PathToFile, addonJson).ConfigureAwait(false); + + MetadataUpdatedEvent?.Invoke(this, update); + } + } + catch (Exception ex) + { + return new(ResultEnum.Error, false, ex.ToString()); + } + + return new(ResultEnum.Success, false, string.Empty); + } +} diff --git a/src/Addons/Providers/OriginalCampaignsProvider.cs b/src/Addons/Providers/OriginalCampaignsProvider.cs index 8743279b..df035f81 100644 --- a/src/Addons/Providers/OriginalCampaignsProvider.cs +++ b/src/Addons/Providers/OriginalCampaignsProvider.cs @@ -83,7 +83,7 @@ private Dictionary GetDuke3DCampaigns(BaseGame game) """, SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_WT), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainCon = null, @@ -92,7 +92,6 @@ private Dictionary GetDuke3DCampaigns(BaseGame game) AdditionalDefs = null, RTS = null, StartMap = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -116,15 +115,15 @@ private Dictionary GetDuke3DCampaigns(BaseGame game) Author = "3D Realms", ReleaseDate = new(1996, 01, 29), Description = """ - Duke Nukem 3D is a first-person shooter developed and published by **3D Realms**. - Released on April 19, 1996, Duke Nukem 3D is the third game in the Duke Nukem series and a sequel to Duke Nukem II. + Duke Nukem 3D is a first-person shooter developed and published by **3D Realms**. + Released on April 19, 1996, Duke Nukem 3D is the third game in the Duke Nukem series and a sequel to Duke Nukem II. - The player assumes the role of Duke Nukem, an imperious action hero, and fights through 48 levels spread across 5 episodes. The player encounters a host of enemies and fights them with a range of weaponry. - In the end, Duke annihilates the alien overlords and celebrates by desecrating their corpses. - """, + The player assumes the role of Duke Nukem, an imperious action hero, and fights through 48 levels spread across 5 episodes. The player encounters a host of enemies and fights them with a range of weaponry. + In the end, Duke annihilates the alien overlords and celebrates by desecrating their corpses. + """, SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainCon = null, @@ -133,7 +132,6 @@ Duke Nukem 3D is a first-person shooter developed and published by **3D Realms** AdditionalDefs = null, RTS = null, StartMap = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -163,8 +161,8 @@ Duke Nukem 3D is a first-person shooter developed and published by **3D Realms** """, SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), RequiredFeatures = null, - PathToFile = null, - DependentAddons = new Dictionary() { { nameof(DukeAddonEnum.DukeVaca), null } }, + FileInfo = null, + DependentAddons = null, IncompatibleAddons = null, MainCon = null, AdditionalCons = null, @@ -172,7 +170,6 @@ Duke Nukem 3D is a first-person shooter developed and published by **3D Realms** AdditionalDefs = null, RTS = null, StartMap = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -200,8 +197,8 @@ Duke Nukem must travel to the North Pole in order to stop the brainwashed Santa """, SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), RequiredFeatures = null, - PathToFile = null, - DependentAddons = new Dictionary() { { nameof(DukeAddonEnum.DukeNW), null } }, + FileInfo = null, + DependentAddons = null, IncompatibleAddons = null, MainCon = null, AdditionalCons = null, @@ -210,7 +207,6 @@ Duke Nukem must travel to the North Pole in order to stop the brainwashed Santa RTS = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -240,8 +236,8 @@ Duke Nukem must travel to the North Pole in order to stop the brainwashed Santa """, SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), RequiredFeatures = null, - PathToFile = null, - DependentAddons = new Dictionary() { { nameof(DukeAddonEnum.DukeDC), null } }, + FileInfo = null, + DependentAddons = null, IncompatibleAddons = null, MainCon = null, AdditionalCons = null, @@ -250,7 +246,6 @@ Duke Nukem must travel to the North Pole in order to stop the brainwashed Santa RTS = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -278,7 +273,7 @@ The game's mature themes have been minimized to satisfy Nintendo's adult content """, SupportedGame = new(GameEnum.Duke64), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainCon = null, @@ -288,7 +283,6 @@ The game's mature themes have been minimized to satisfy Nintendo's adult content RTS = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -317,7 +311,7 @@ by eliminating Duke's ancestors. """, SupportedGame = new(GameEnum.DukeZeroHour), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainCon = null, @@ -327,7 +321,6 @@ by eliminating Duke's ancestors. RTS = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -374,7 +367,7 @@ private Dictionary GetBloodCampaigns(BaseGame game) """, SupportedGame = new(GameEnum.Blood), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainDef = null, @@ -384,7 +377,6 @@ private Dictionary GetBloodCampaigns(BaseGame game) SND = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -414,8 +406,8 @@ Caleb travels to the Carpathian mountains after he hears of an ancient scroll ta """, SupportedGame = new(GameEnum.Blood), RequiredFeatures = null, - PathToFile = null, - DependentAddons = new Dictionary() { { nameof(BloodAddonEnum.BloodCP), null } }, + FileInfo = null, + DependentAddons = null, IncompatibleAddons = null, MainDef = null, AdditionalDefs = null, @@ -424,7 +416,6 @@ Caleb travels to the Carpathian mountains after he hears of an ancient scroll ta SND = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -467,7 +458,7 @@ private Dictionary GetWangCampaigns(BaseGame game) However, this led to corruption, and Master Zilla - the president - planned to conquer Japan using creatures from the "dark side". In discovery of this, Lo Wang quit his job as a bodyguard. Master Zilla realized that not having a warrior as powerful as Lo Wang would be dangerous, and sent his creatures to battle Lo Wang. """, - PathToFile = null, + FileInfo = null, SupportedGame = new(GameEnum.Wang), RequiredFeatures = null, DependentAddons = null, @@ -476,7 +467,6 @@ private Dictionary GetWangCampaigns(BaseGame game) AdditionalDefs = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -519,7 +509,7 @@ private Dictionary GetFuryCampaigns(BaseGame game) """, SupportedGame = new(GameEnum.Fury), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainCon = null, @@ -529,7 +519,6 @@ private Dictionary GetFuryCampaigns(BaseGame game) RTS = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -597,7 +586,7 @@ private Dictionary GetRedneckCampaigns(BaseGame game) """, SupportedGame = new(GameEnum.Redneck), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainCon = null, @@ -607,7 +596,6 @@ private Dictionary GetRedneckCampaigns(BaseGame game) RTS = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -633,8 +621,8 @@ private Dictionary GetRedneckCampaigns(BaseGame game) """, SupportedGame = new(GameEnum.Redneck), RequiredFeatures = null, - PathToFile = null, - DependentAddons = new Dictionary() { { nameof(RedneckAddonEnum.Route66), null } }, + FileInfo = null, + DependentAddons = null, IncompatibleAddons = null, MainCon = null, AdditionalCons = null, @@ -643,7 +631,6 @@ private Dictionary GetRedneckCampaigns(BaseGame game) RTS = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -672,7 +659,7 @@ private Dictionary GetRedneckCampaigns(BaseGame game) """, SupportedGame = new(GameEnum.RidesAgain), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainCon = null, @@ -682,7 +669,6 @@ private Dictionary GetRedneckCampaigns(BaseGame game) RTS = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -729,7 +715,7 @@ NAM is the first game of its kind. NAM IS WAR! """, SupportedGame = new(GameEnum.NAM), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainCon = null, @@ -739,7 +725,6 @@ NAM is the first game of its kind. NAM IS WAR! AdditionalDefs = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -782,7 +767,7 @@ This is D-Day! """, SupportedGame = new(GameEnum.WW2GI), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainCon = null, @@ -792,7 +777,6 @@ This is D-Day! AdditionalDefs = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -819,7 +803,7 @@ This is D-Day! """, SupportedGame = new(GameEnum.WW2GI), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainCon = null, @@ -829,7 +813,6 @@ This is D-Day! AdditionalDefs = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -870,7 +853,7 @@ private Dictionary GetTekWarCampaigns(BaseGame game) """, SupportedGame = new(GameEnum.TekWar), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainCon = null, @@ -880,7 +863,6 @@ private Dictionary GetTekWarCampaigns(BaseGame game) AdditionalDefs = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -927,14 +909,13 @@ private Dictionary GetSlaveCampaigns(BaseGame game) """, SupportedGame = new(GameEnum.Slave), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainDef = null, AdditionalDefs = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -977,14 +958,13 @@ Dare to enter this 3D Hell... Dare to enter Witchaven! """, SupportedGame = new(GameEnum.Witchaven), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainDef = null, AdditionalDefs = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null @@ -1015,14 +995,13 @@ The witches have been destroyed in their lair on the Island of Char! """, SupportedGame = new(GameEnum.Witchaven2), RequiredFeatures = null, - PathToFile = null, + FileInfo = null, DependentAddons = null, IncompatibleAddons = null, MainDef = null, AdditionalDefs = null, StartMap = null, PreviewImageHash = null, - IsUnpacked = false, Executables = null, IsFavorite = _config.FavoriteAddons.Contains(version), Options = null diff --git a/src/Avalonia.Desktop/App.axaml.cs b/src/Avalonia.Desktop/App.axaml.cs index d6d27db4..2ffcd5a6 100644 --- a/src/Avalonia.Desktop/App.axaml.cs +++ b/src/Avalonia.Desktop/App.axaml.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using Addons.Helpers; +using Addons.Providers; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Desktop.Helpers; @@ -196,6 +197,7 @@ private static void LoadBindings() _ = services.WithPorts(); _ = services.WithTools(); _ = services.WithAddons(); + _ = services.WithChannels(); _services?.Dispose(); _services = services.BuildServiceProvider(new ServiceProviderOptions diff --git a/src/Avalonia.Desktop/ViewModels/CampaignsViewModel.cs b/src/Avalonia.Desktop/ViewModels/CampaignsViewModel.cs index eae45c95..68eb5041 100644 --- a/src/Avalonia.Desktop/ViewModels/CampaignsViewModel.cs +++ b/src/Avalonia.Desktop/ViewModels/CampaignsViewModel.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Diagnostics; using Addons.Addons; +using Addons.Helpers; using Addons.Providers; using Avalonia.Controls.Notifications; using Avalonia.Desktop.Helpers; @@ -50,18 +51,18 @@ public ImmutableList CampaignsList foreach (var addon in addons) { - if (!isSearchEmpty && !addon.Value.Title.Contains(SearchBoxText, StringComparison.OrdinalIgnoreCase)) + if (!isSearchEmpty && !addon.Title.Contains(SearchBoxText, StringComparison.OrdinalIgnoreCase)) { continue; } - if (addon.Value.IsFavorite) + if (addon.IsFavorite) { - favorites.Add(addon.Value); + favorites.Add(addon); continue; } - list.Add(addon.Value); + list.Add(addon); } if (favorites.Count > 0) @@ -145,7 +146,7 @@ ILogger logger _gamesProvider.GameChangedEvent += OnGameChanged; _installedAddonsProvider.AddonsChangedEvent += OnAddonChanged; - _downloadableAddonsProvider.AddonsChangedEvent += OnAddonChanged; + //_downloadableAddonsProvider.AddonsChangedEvent += OnAddonChanged; } @@ -160,7 +161,7 @@ ILogger logger private async Task UpdateAsync(bool createNew) { IsInProgress = true; - await _installedAddonsProvider.CreateCache(createNew, AddonTypeEnum.TC).ConfigureAwait(true); + await _installedAddonsProvider.CreateCacheAsync(createNew, AddonTypeEnum.TC).ConfigureAwait(true); IsInProgress = false; } @@ -273,7 +274,7 @@ private void DeleteCampaign() /// - /// Delete selected campaign + /// Add selected campaign to favorites /// [RelayCommand] private void AddToFavorite(object? value) @@ -291,7 +292,7 @@ private void AddToFavorite(object? value) /// - /// Delete selected campaign + /// Remove selected campaign from favorites /// [RelayCommand] private void RemoveFromFavorite(object? value) @@ -327,7 +328,12 @@ public override async Task UpdateMetadataAsync(object? value) IsInProgress = true; - var result = await _metadataProvider.UpdateMetadataAsync(addon.PathToFile!).ConfigureAwait(true); + if (addon.FileInfo is null) + { + throw new InvalidOperationException(); + } + + var result = await _metadataProvider.UpdateMetadataAsync(addon.FileInfo).ConfigureAwait(true); IsInProgress = false; diff --git a/src/Avalonia.Desktop/ViewModels/DownloadsViewModel.cs b/src/Avalonia.Desktop/ViewModels/DownloadsViewModel.cs index bf1e255c..1e11fb86 100644 --- a/src/Avalonia.Desktop/ViewModels/DownloadsViewModel.cs +++ b/src/Avalonia.Desktop/ViewModels/DownloadsViewModel.cs @@ -158,7 +158,7 @@ ILogger logger _logger = logger; _installedAddonsProvider.AddonsChangedEvent += OnAddonChanged; - _downloadableAddonsProvider.AddonsChangedEvent += OnAddonChanged; + //_downloadableAddonsProvider.AddonsChangedEvent += OnAddonChanged; SelectedDownloads.CollectionChanged += OnSelectedDownloadsChanged; } diff --git a/src/Avalonia.Desktop/ViewModels/GamePageViewModel.cs b/src/Avalonia.Desktop/ViewModels/GamePageViewModel.cs index 2a5b6212..e3bcc755 100644 --- a/src/Avalonia.Desktop/ViewModels/GamePageViewModel.cs +++ b/src/Avalonia.Desktop/ViewModels/GamePageViewModel.cs @@ -1,7 +1,7 @@ using Addons.Providers; -using Core.All.Enums; -using Core.Client.Providers; using CommunityToolkit.Mvvm.ComponentModel; +using Core.All.Enums; +using Core.Client.Helpers; namespace Avalonia.Desktop.ViewModels; @@ -50,7 +50,8 @@ DownloadableAddonsProvider downloadablesProvider Downloads = downloads; metadataProvider.MetadataUpdatedEvent += OnMetadataUpdated; - downloadablesProvider.AddonsChangedEvent += OnAddonsChanged; + metadataProvider.MetadataInitializedEvent += OnMetadataInitialized; + //downloadablesProvider.AddonsChangedEvent += OnAddonsChanged; } private void OnAddonsChanged(GameEnum gameEnum, AddonTypeEnum? addonType) @@ -63,7 +64,14 @@ private void OnAddonsChanged(GameEnum gameEnum, AddonTypeEnum? addonType) IsDownloadsAlarmShown = Downloads.HasUpdates; } - private void OnMetadataUpdated(object? sender, ValueTuple? e) + private void OnMetadataUpdated(object? sender, ParsedAddonFile e) + { + IsCampaignsAlarmShown = Campaigns.CampaignsList.Any(x => x.IsMetadataUpdateAvailable); + IsMapsAlarmShown = Maps?.MapsList.Any(x => x.IsMetadataUpdateAvailable) ?? false; + IsModsAlarmShown = Mods?.ModsList.Any(x => x.IsMetadataUpdateAvailable) ?? false; + } + + private void OnMetadataInitialized(object? sender, EventArgs e) { IsCampaignsAlarmShown = Campaigns.CampaignsList.Any(x => x.IsMetadataUpdateAvailable); IsMapsAlarmShown = Maps?.MapsList.Any(x => x.IsMetadataUpdateAvailable) ?? false; diff --git a/src/Avalonia.Desktop/ViewModels/MapsViewModel.cs b/src/Avalonia.Desktop/ViewModels/MapsViewModel.cs index 8b788467..1b018562 100644 --- a/src/Avalonia.Desktop/ViewModels/MapsViewModel.cs +++ b/src/Avalonia.Desktop/ViewModels/MapsViewModel.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Diagnostics; using Addons.Addons; +using Addons.Helpers; using Addons.Providers; using Avalonia.Controls.Notifications; using Avalonia.Desktop.Misc; @@ -41,7 +42,7 @@ public sealed partial class MapsViewModel : RightPanelViewModel, IPortsButtonCon private async Task UpdateAsync(bool createNew) { IsInProgress = true; - await _installedAddonsProvider.CreateCache(createNew, AddonTypeEnum.Map).ConfigureAwait(true); + await _installedAddonsProvider.CreateCacheAsync(createNew, AddonTypeEnum.Map).ConfigureAwait(true); IsInProgress = false; } @@ -55,7 +56,7 @@ public ImmutableList MapsList { get { - var result = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Map).Select(static x => x.Value).OrderBy(static x => x.Title); + var result = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Map).OrderBy(static x => x.Title); if (string.IsNullOrWhiteSpace(SearchBoxText)) { @@ -136,7 +137,7 @@ ILogger logger _gamesProvider.GameChangedEvent += OnGameChanged; _installedAddonsProvider.AddonsChangedEvent += OnAddonChanged; - _downloadableAddonsProvider.AddonsChangedEvent += OnAddonChanged; + //_downloadableAddonsProvider.AddonsChangedEvent += OnAddonChanged; } @@ -251,9 +252,14 @@ public override async Task UpdateMetadataAsync(object? value) throw new InvalidOperationException(value?.GetType().Name); } + if (addon.FileInfo is null) + { + throw new InvalidOperationException(); + } + IsInProgress = true; - var result = await _metadataProvider.UpdateMetadataAsync(addon.PathToFile).ConfigureAwait(true); + var result = await _metadataProvider.UpdateMetadataAsync(addon.FileInfo).ConfigureAwait(true); IsInProgress = false; diff --git a/src/Avalonia.Desktop/ViewModels/ModsViewModel.cs b/src/Avalonia.Desktop/ViewModels/ModsViewModel.cs index 58f3ae8d..9f313405 100644 --- a/src/Avalonia.Desktop/ViewModels/ModsViewModel.cs +++ b/src/Avalonia.Desktop/ViewModels/ModsViewModel.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Diagnostics; using Addons.Addons; +using Addons.Helpers; using Addons.Providers; using Avalonia.Controls.Notifications; using Avalonia.Desktop.Misc; @@ -52,7 +53,7 @@ ILogger logger _gamesProvider.GameChangedEvent += OnGameChanged; _installedAddonsProvider.AddonsChangedEvent += OnAddonChanged; - _downloadableAddonsProvider.AddonsChangedEvent += OnAddonChanged; + //_downloadableAddonsProvider.AddonsChangedEvent += OnAddonChanged; } @@ -67,7 +68,7 @@ ILogger logger private async Task UpdateAsync(bool createNew) { IsInProgress = true; - await _installedAddonsProvider.CreateCache(createNew, AddonTypeEnum.Mod).ConfigureAwait(true); + await _installedAddonsProvider.CreateCacheAsync(createNew, AddonTypeEnum.Mod).ConfigureAwait(true); IsInProgress = false; } @@ -77,7 +78,7 @@ private async Task UpdateAsync(bool createNew) /// /// List of installed autoload mods /// - public ImmutableList ModsList => [.. _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod).Select(x => (AutoloadMod)x.Value).OrderBy(static x => x.Title)]; + public ImmutableList ModsList => [.. _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod).Select(x => (AutoloadMod)x).OrderBy(static x => x.Title)]; private BaseAddon? _selectedAddon; /// @@ -126,7 +127,7 @@ private void OpenFolder() /// - /// Refresh campaigns list + /// Refresh mods list /// [RelayCommand] private Task RefreshListAsync() => UpdateAsync(true); @@ -196,9 +197,14 @@ public override async Task UpdateMetadataAsync(object? value) throw new InvalidOperationException(value?.GetType().Name); } + if (addon.FileInfo is null) + { + throw new InvalidOperationException(); + } + IsInProgress = true; - var result = await _metadataProvider.UpdateMetadataAsync(addon.PathToFile).ConfigureAwait(true); + var result = await _metadataProvider.UpdateMetadataAsync(addon.FileInfo).ConfigureAwait(true); IsInProgress = false; diff --git a/src/Avalonia.Desktop/ViewModels/RightPanelViewModel.cs b/src/Avalonia.Desktop/ViewModels/RightPanelViewModel.cs index f7c337d1..f7232348 100644 --- a/src/Avalonia.Desktop/ViewModels/RightPanelViewModel.cs +++ b/src/Avalonia.Desktop/ViewModels/RightPanelViewModel.cs @@ -1,13 +1,14 @@ using System.Collections.ObjectModel; using System.ComponentModel; using Addons.Addons; +using Addons.Providers; using Avalonia.Desktop.Misc; using Avalonia.Media.Imaging; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using Core.All.Helpers; using Core.Client.Interfaces; using Core.Client.Providers; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; namespace Avalonia.Desktop.ViewModels; @@ -55,7 +56,7 @@ IConfigProvider config /// public bool IsPreviewVisible => SelectedAddonPreview is not null; - public bool IsMetadataUpdateAvailable => SelectedAddon?.PathToFile is null ? false : _metadatUpdater.IsMetadataUpdateAvailable(SelectedAddon.PathToFile); + public bool IsMetadataUpdateAvailable => SelectedAddon?.FileInfo is not null && _metadatUpdater.IsMetadataUpdateAvailable(SelectedAddon.AddonId, SelectedAddon.FileInfo); public string? SelectedAddonRating { diff --git a/src/Avalonia.Desktop/ViewModels/ViewModelsFactory.cs b/src/Avalonia.Desktop/ViewModels/ViewModelsFactory.cs index bd6c4220..a6a9af55 100644 --- a/src/Avalonia.Desktop/ViewModels/ViewModelsFactory.cs +++ b/src/Avalonia.Desktop/ViewModels/ViewModelsFactory.cs @@ -1,4 +1,5 @@ -using Addons.Providers; +using Addons.Helpers; +using Addons.Providers; using Avalonia.Desktop.Misc; using Avalonia.Threading; using Core.All.Enums; diff --git a/src/Core.All/ChannelBroadcaster.cs b/src/Core.All/ChannelBroadcaster.cs new file mode 100644 index 00000000..f65f2261 --- /dev/null +++ b/src/Core.All/ChannelBroadcaster.cs @@ -0,0 +1,64 @@ +using System.Collections.Concurrent; +using System.Threading.Channels; + +namespace Core.All; + +public interface IChannelSubscriber +{ + ChannelReader Subscribe(); + void Unsubscribe(ChannelReader reader); +} + + +public interface IChannelPublisher +{ + ValueTask PublishAsync(T message); +} + + +public class ChannelBroadcaster : IChannelSubscriber, IChannelPublisher +{ + private readonly Dictionary, ChannelWriter> _readerChannelDict = []; + private readonly Lock _locker = new(); + + public ChannelReader Subscribe() + { + using (_locker.EnterScope()) + { + var channel = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleWriter = true + }); + + _readerChannelDict.Add(channel.Reader, channel.Writer); + + return channel.Reader; + } + } + /// + public void Unsubscribe(ChannelReader reader) + { + using (_locker.EnterScope()) + { + if (_readerChannelDict.Remove(reader, out var writer)) + { + writer.Complete(); + } + } + } + + public async ValueTask PublishAsync(T message) + { + List> targets; + + using (_locker.EnterScope()) + { + targets = _readerChannelDict.Values.ToList(); + } + + foreach (var writer in targets) + { + await writer.WriteAsync(message); + } + } +} diff --git a/src/Core.All/Helpers/CommonConstants.cs b/src/Core.All/Helpers/CommonConstants.cs index d906bb88..f8fd44e4 100644 --- a/src/Core.All/Helpers/CommonConstants.cs +++ b/src/Core.All/Helpers/CommonConstants.cs @@ -21,4 +21,6 @@ public static class CommonConstants /// Link to manifests.json file /// public static Uri ManifestsJsonUrl => new("https://raw.githubusercontent.com/fgsfds/BuildLauncher/refs/heads/master/db/manifests.json"); + + public const string AddonManifestName = "addon.json"; } diff --git a/src/Core.All/Helpers/ConfigureAwaitHelper.cs b/src/Core.All/Helpers/ConfigureAwaitHelper.cs deleted file mode 100644 index e65743e9..00000000 --- a/src/Core.All/Helpers/ConfigureAwaitHelper.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Core.All.Helpers; - -public static class ConfigureAwaitHelper -{ - public static ConfiguredValueTaskAwaitable Caf( - this ValueTask task - ) - { - return task.ConfigureAwait(false); - } - - public static ConfiguredValueTaskAwaitable Caf( - this ValueTask task - ) - { - return task.ConfigureAwait(false); - } - - public static ConfiguredTaskAwaitable Caf( - this Task task - ) - { - return task.ConfigureAwait(false); - } - - public static ConfiguredTaskAwaitable Caf( - this Task task - ) - { - return task.ConfigureAwait(false); - } -} diff --git a/src/Core.All/Result.cs b/src/Core.All/Result.cs index 1dded96e..8e11f053 100644 --- a/src/Core.All/Result.cs +++ b/src/Core.All/Result.cs @@ -42,7 +42,7 @@ public readonly struct Result /// /// Operation result enum /// - private ResultEnum _resultEnum { get; } + private readonly ResultEnum _resultEnum; /// /// Operation result message diff --git a/src/Core.Client/Api/OfflineApiInterface.cs b/src/Core.Client/Api/OfflineApiInterface.cs index 39d78310..0592d3cf 100644 --- a/src/Core.Client/Api/OfflineApiInterface.cs +++ b/src/Core.Client/Api/OfflineApiInterface.cs @@ -93,7 +93,12 @@ public OfflineApiInterface(ILogger logger) DataJsonModelContext.Default.DictionaryStringString ).ConfigureAwait(false); - _ = data!.TryGetValue(DataJson.UploadFolder, out var uploadFolder) ? uploadFolder : null; + if (data is null) + { + return null; + } + + _ = data.TryGetValue(DataJson.UploadFolder, out var uploadFolder) ? uploadFolder : null; return uploadFolder; } diff --git a/src/Core.Client/Enums/KeyedServicesEnum.cs b/src/Core.Client/Enums/KeyedServicesEnum.cs index 7fa83353..7e25faed 100644 --- a/src/Core.Client/Enums/KeyedServicesEnum.cs +++ b/src/Core.Client/Enums/KeyedServicesEnum.cs @@ -2,5 +2,6 @@ public enum KeyedServicesEnum { - Bitmaps + Bitmaps, + LocalFilesChannel } diff --git a/src/Core.Client/Helpers/AddonFilePathWrapper.cs b/src/Core.Client/Helpers/AddonFilePathWrapper.cs new file mode 100644 index 00000000..ee5b32b1 --- /dev/null +++ b/src/Core.Client/Helpers/AddonFilePathWrapper.cs @@ -0,0 +1,74 @@ +namespace Core.Client.Helpers; + +/// +/// Wraps a folder or archive path together with a manifest file name. +/// +public sealed record AddonFilePathWrapper +{ + private readonly string _pathToAddonFileOrFolder; + private readonly string _mainFileName; + + /// + /// Initializes a new wrapper, normalizing path separators to the platform native character. + /// + /// Path to the folder or zip file containing the addon. + /// Name of the manifest file (entry name inside zip for packed addons). + public AddonFilePathWrapper(string pathToAddon, string mainFileName) + { + _pathToAddonFileOrFolder = pathToAddon.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + _mainFileName = mainFileName; + } + + /// + /// True when the path is a folder (no file extension). + /// + public bool IsFolder => string.IsNullOrEmpty(Path.GetExtension(_pathToAddonFileOrFolder)); + + /// + /// True when the path ends with ".zip". + /// + public bool IsZip => _pathToAddonFileOrFolder.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase); + + /// + /// True when this is an unpacked folder addon with a ".json" manifest. + /// + public bool IsJson => IsFolder && _mainFileName.EndsWith(".json", StringComparison.InvariantCultureIgnoreCase); + + /// + /// True when this is a loose ".map" file. + /// + public bool IsMap => IsFolder && _mainFileName.EndsWith(".map", StringComparison.InvariantCultureIgnoreCase); + + /// + /// True when this is a ".grpinfo" file. + /// + public bool IsGrpInfo => IsFolder && _mainFileName.EndsWith(".grpinfo", StringComparison.InvariantCultureIgnoreCase); + + /// + /// Returns the folder path. For zips, this is the parent directory. + /// + public string PathToFolder => IsFolder ? _pathToAddonFileOrFolder : Path.GetDirectoryName(_pathToAddonFileOrFolder); + + /// + /// Returns the file path to pass to the port. + /// For folders: combined folder and filename. For zips: the zip path alone. + /// + public string PathToFile => IsFolder ? Path.Combine(_pathToAddonFileOrFolder, _mainFileName) : _pathToAddonFileOrFolder; + + /// + /// Returns the file name component. For zips, this is the zip filename. + /// + public string FileName => Path.GetFileName(PathToFile); + + /// + /// Returns a new wrapper with the same filename but a different folder path. + /// + /// New folder or zip path. + public AddonFilePathWrapper WithChangedFolder(string newFolderPath) + { + return new(newFolderPath, _mainFileName); + } + + [Obsolete("Don't use ToString(), use properties instead.", true)] + public override string ToString() => Path.Combine(_pathToAddonFileOrFolder, _mainFileName); +} diff --git a/src/Core.Client/Helpers/ClientProperties.cs b/src/Core.Client/Helpers/ClientProperties.cs index d1db1092..bce7769b 100644 --- a/src/Core.Client/Helpers/ClientProperties.cs +++ b/src/Core.Client/Helpers/ClientProperties.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Reflection; using System.Runtime.InteropServices; namespace Core.Client.Helpers; @@ -53,7 +54,8 @@ public static class ClientProperties /// /// Current app version /// - public static Version CurrentVersion => new(1, 0, 0, 0); + public static Version CurrentVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? throw new ArgumentNullException(); + /// /// Name of the executable file /// diff --git a/src/Core.Client/Helpers/DiHelper.cs b/src/Core.Client/Helpers/DiHelper.cs index fef24446..f53f01b7 100644 --- a/src/Core.Client/Helpers/DiHelper.cs +++ b/src/Core.Client/Helpers/DiHelper.cs @@ -1,10 +1,12 @@ using System.Net; using System.Net.Http.Headers; +using Core.All; using Core.All.Enums; using Core.All.Helpers; using Core.All.Providers; using Core.Client.Api; using Core.Client.Config; +using Core.Client.Enums; using Core.Client.Interfaces; using Core.Client.Providers; using Core.Client.Tools; @@ -96,7 +98,7 @@ public static IServiceCollection WithFileLogging(this IServiceCollection contain { opt.Append = false; opt.FormatLogFileName = (fileName) => { return string.Format(fileName, DateTime.UtcNow); }; - opt.FormatLogEntry = (message) => { return $"[{DateTime.Now.ToLocalTime() + "]",-25} {message.LogLevel,-15} {message.Message}"; }; + opt.FormatLogEntry = (message) => { return $"[{DateTime.Now.ToLocalTime() + "]",-25} {message.LogLevel,-15} {message.Message} {message.Exception}"; }; }) .AddFilter("System.Net.Http.HttpClient", LogLevel.None) .AddFilter("Microsoft.EntityFrameworkCore", LogLevel.None) @@ -111,11 +113,35 @@ public static IServiceCollection WithClient(this IServiceCollection container) _ = container.AddSingleton(); _ = container.AddSingleton(); _ = container.AddSingleton(); - return container.AddSingleton(); + _ = container.AddSingleton(); + + return container; + } + + public static IServiceCollection WithChannels(this IServiceCollection container) + { + _ = container.AddSingleton>(); + + _ = container.AddKeyedSingleton>( + KeyedServicesEnum.LocalFilesChannel, + (sp, _) => sp.GetRequiredService>()); + + _ = container.AddKeyedSingleton>( + KeyedServicesEnum.LocalFilesChannel, + (sp, _) => sp.GetRequiredService>()); + + return container; + } + + + public sealed record LocalFileEvent + { + public IReadOnlyCollection Files { get; init; } + public bool IsAdded { get; init; } } - public sealed class FakeHttpMessageHandler : HttpMessageHandler + private sealed class FakeHttpMessageHandler : HttpMessageHandler { protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/src/Core.Client/Helpers/ParsedAddonFile.cs b/src/Core.Client/Helpers/ParsedAddonFile.cs new file mode 100644 index 00000000..05556294 --- /dev/null +++ b/src/Core.Client/Helpers/ParsedAddonFile.cs @@ -0,0 +1,36 @@ +using Core.All.Enums; +using Core.All.Serializable.Addon; + +namespace Core.Client.Helpers; + +/// +/// Combines a parsed manifest with its file metadata and image hashes. +/// +public sealed record ParsedAddonFile +{ + /// + /// File path wrapper that contains the addon folder or archive path and its main file name. + /// + public required AddonFilePathWrapper FileInfo { get; init; } + + /// + /// The deserialized addon manifest, or null if not available. + /// + /// The instance containing addon metadata. + public required AddonManifestJsonModel? Manifest { get; init; } + + /// + /// The game for which this addon file is intended or supported. + /// + public required GameEnum SupportedGame { get; init; } + + /// + /// Gets or initializes the hash of the grid image associated with the addon file. + /// + public required long? GridHash { get; init; } + + /// + /// Hash value for the preview image associated with the addon file. + /// + public required long? PreviewHash { get; init; } +} diff --git a/src/Core.Client/Providers/MetadataProvider.cs b/src/Core.Client/Providers/MetadataProvider.cs deleted file mode 100644 index 13b39f13..00000000 --- a/src/Core.Client/Providers/MetadataProvider.cs +++ /dev/null @@ -1,186 +0,0 @@ -using System.Text.Json; -using Core.All; -using Core.All.Enums; -using Core.All.Serializable.Addon; -using Core.Client.Helpers; -using Core.Client.Interfaces; -using Microsoft.Extensions.Logging; -using SharpCompress.Archives; -using SharpCompress.Archives.Zip; -using SharpCompress.Common; - -namespace Core.Client.Providers; - -public sealed class MetadataProvider -{ - public event EventHandler?>? MetadataUpdatedEvent; - - private readonly IApiInterface _apiInterface; - private readonly ILogger _logger; - - private readonly Dictionary> _updatesCache = []; - - public MetadataProvider( - IApiInterface apiInterface, - ILogger logger - ) - { - _apiInterface = apiInterface; - _logger = logger; - } - - public async Task InitializeAsync() - { - var metadata = await _apiInterface.GetMetadataAsync().ConfigureAwait(false); - - if (metadata is null) - { - return; - } - - var metaDict = metadata.ToDictionary(x => new AddonId(x.Id, x.Version)); - - var files = Directory.EnumerateFiles(ClientProperties.AddonsFolderPath, "*", SearchOption.AllDirectories); - - foreach (var file in files) - { - try - { - if (file.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) - { - using var archive = ArchiveFactory.OpenArchive(file); - var manifests = archive.Entries.Where(x => x.Key!.Contains("addon", StringComparison.OrdinalIgnoreCase) && x.Key.EndsWith(".json", StringComparison.OrdinalIgnoreCase)); - - foreach (var manifest in manifests) - { - using var stream = await manifest.OpenEntryStreamAsync().ConfigureAwait(false); - var originalManifest = await JsonSerializer.DeserializeAsync( - stream, - AddonManifestJsonContext.Default.AddonManifestJsonModel - ).ConfigureAwait(false); - - if (originalManifest is null) - { - continue; - } - - AddToCacheIfNewer(metaDict, file, originalManifest, manifest.Key); - } - } - else if (file.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) - { - using var originalManifestStr = File.OpenRead(file); - var originalManifest = await JsonSerializer.DeserializeAsync( - originalManifestStr, - AddonManifestJsonContext.Default.AddonManifestJsonModel - ).ConfigureAwait(false); - - if (originalManifest is null) - { - continue; - } - - AddToCacheIfNewer(metaDict, file, originalManifest, file); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while getting metadata from {FileName}.", file); - continue; - } - } - - if (_updatesCache.Count > 0) - { - MetadataUpdatedEvent?.Invoke(this, null); - } - } - - public bool IsMetadataUpdateAvailable(string path) => _updatesCache.TryGetValue(path, out _); - - public async Task> UpdateMetadataAsync(string path) - { - try - { - if (!_updatesCache.TryGetValue(path, out var updates)) - { - return new(ResultEnum.Error, false, string.Empty); - } - - if (path.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) - { - using (var archive = ZipArchive.OpenArchive(path)) - { - List streams = new(updates.Count); - - foreach (var update in updates) - { - var existing = archive.Entries.FirstOrDefault(x => x.Key.Equals(update.Key)); - - if (existing is not null) - { - archive.RemoveEntry(existing); - } - - var ms = new MemoryStream(); - streams.Add(ms); - await JsonSerializer.SerializeAsync(ms, update.Value, AddonManifestJsonContext.Default.AddonManifestJsonModel).ConfigureAwait(false); - - archive.AddEntry(update.Key, ms); - } - - archive.SaveTo(path + ".temp", new(CompressionType.None)); - streams.ForEach(x => x.Dispose()); - } - - File.Delete(path); - File.Move(path + ".temp", path); - - _updatesCache.Remove(path); - - MetadataUpdatedEvent?.Invoke(this, new( - updates.First().Value.SupportedGame.Game, - updates.First().Value.AddonType, - path - )); - } - else if (path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) - { - File.Delete(path); - var addonJson = JsonSerializer.Serialize(updates.First().Value, AddonManifestJsonContext.Default.AddonManifestJsonModel); - await File.WriteAllTextAsync(path, addonJson).ConfigureAwait(false); - - MetadataUpdatedEvent?.Invoke(this, new( - updates.First().Value.SupportedGame.Game, - updates.First().Value.AddonType, - path - )); - } - } - catch (Exception ex) - { - return new(ResultEnum.Error, false, ex.ToString()); - } - - return new(ResultEnum.Success, false, string.Empty); - } - - private void AddToCacheIfNewer(Dictionary metaDict, string file, AddonManifestJsonModel originalManifest, string jsonName) - { - if (!metaDict.TryGetValue(new(originalManifest.Id, originalManifest.Version), out var actualVersion)) - { - return; - } - - var newManifestStr = JsonSerializer.Serialize(actualVersion, AddonManifestJsonContext.Default.AddonManifestJsonModel); - var originalManifestStr = JsonSerializer.Serialize(originalManifest, AddonManifestJsonContext.Default.AddonManifestJsonModel); - - if (!originalManifestStr.Equals(newManifestStr)) - { - if (!_updatesCache.TryAdd(file, new() { { jsonName, actualVersion } })) - { - _updatesCache[file].Add(jsonName, actualVersion); - } - } - } -} diff --git a/src/Games/DiHelper.cs b/src/Games/DiHelper.cs index 15fd8be4..97633e37 100644 --- a/src/Games/DiHelper.cs +++ b/src/Games/DiHelper.cs @@ -11,6 +11,8 @@ public static class DiHelper public static IServiceCollection WithGames(this IServiceCollection container) { _ = container.AddSingleton(); - return container.AddSingleton(); + _ = container.AddSingleton(); + + return container; } } diff --git a/src/Games/Providers/InstalledGamesProvider.cs b/src/Games/Providers/InstalledGamesProvider.cs index 28936cd5..bd853f83 100644 --- a/src/Games/Providers/InstalledGamesProvider.cs +++ b/src/Games/Providers/InstalledGamesProvider.cs @@ -7,7 +7,7 @@ namespace Games.Providers; /// /// Class that provides singleton instances of game types /// -public sealed class InstalledGamesProvider +public class InstalledGamesProvider { public delegate void GameChanged(GameEnum game); public event GameChanged? GameChangedEvent; @@ -37,6 +37,9 @@ public sealed class InstalledGamesProvider public bool IsWitchavenInstalled => _witch.IsBaseGameInstalled || _witch.IsWitchaven2Installed; public bool IsTekWarInstalled => _tekwar.IsBaseGameInstalled; + public InstalledGamesProvider() + { + } public InstalledGamesProvider(IConfigProvider config) { @@ -128,6 +131,27 @@ public BaseGame GetGame(GameEnum gameEnum) }; } + /// + /// Gets a list of all game instances. + /// + public virtual IReadOnlyList GetGames() + { + return + [ + _blood, + _duke3d, + _wang, + _fury, + _slave, + _redneck, + _nam, + _ww2gi, + _standalone, + _tekwar, + _witch, + ]; + } + /// /// Update game instance when path to the game changes in the config diff --git a/src/Ports/Ports/BasePort.cs b/src/Ports/Ports/BasePort.cs index d27eab15..7d77e354 100644 --- a/src/Ports/Ports/BasePort.cs +++ b/src/Ports/Ports/BasePort.cs @@ -2,7 +2,6 @@ using System.Text; using Addons.Addons; using Addons.Helpers; -using Core.All; using Core.All.Enums; using Core.All.Enums.Addons; using Core.All.Helpers; @@ -94,7 +93,6 @@ public string Exe /// public string PortExeFilePath => Path.Combine(InstallFolderPath, Exe); - /// /// Name of the config file /// @@ -180,7 +178,6 @@ public string Exe /// public abstract bool IsSkillSelectionAvailable { get; } - protected BasePort() { if (!Directory.Exists(PortSavedGamesFolderPath)) @@ -189,13 +186,12 @@ protected BasePort() } } - /// /// Get path to addon's saved games folder /// /// Subfolder under port's saves folder /// Addon Id - public string GetPathToAddonSavedGamesFolder(string subFolder, string addonId) + protected string GetPathToAddonSavedGamesFolder(string subFolder, string addonId) { var folderName = addonId; @@ -207,7 +203,6 @@ public string GetPathToAddonSavedGamesFolder(string subFolder, string addonId) return Path.Combine(PortSavedGamesFolderPath, subFolder, folderName); } - /// /// Get command line parameters to start the game with selected campaign and autoload mods /// @@ -221,7 +216,7 @@ public string GetPathToAddonSavedGamesFolder(string subFolder, string addonId) public string GetStartGameArgs( BaseGame game, BaseAddon addon, - IReadOnlyDictionary mods, + IReadOnlyList mods, IReadOnlyList enabledOptions, bool skipIntro, bool skipStartup, @@ -257,7 +252,7 @@ public string GetStartGameArgs( return sb.ToString(); } - protected virtual void GetOptionsArgs( + protected void GetOptionsArgs( StringBuilder sb, BaseGame game, BaseAddon addon, @@ -279,8 +274,8 @@ IReadOnlyList enabledOptions { _ = sb.Append($@" {AddDefParam}""{option.Key}"""); } - else if (option.Value is OptionalParameterTypeEnum.INI - && game is BloodGame blood) + else if (option.Value is OptionalParameterTypeEnum.INI && + game is BloodGame) { _ = sb.Append($@" -ini ""{option.Key}"""); } @@ -292,16 +287,20 @@ IReadOnlyList enabledOptions } } - /// /// Get startup args for manifested maps /// - protected virtual void GetMapArgs(StringBuilder sb, BaseAddon camp) + protected void GetMapArgs(StringBuilder sb, BaseAddon camp) { + if (camp.FileInfo is null) + { + throw new InvalidOperationException(); + } + //TODO e#m# if (camp.StartMap is MapFileJsonModel mapFile) { - _ = sb.Append($@" {AddFileParam}""{camp.PathToFile}"""); + _ = sb.Append($@" {AddFileParam}""{camp.FileInfo.PathToFile}"""); _ = sb.Append($@" -map ""{mapFile.File}"""); } else @@ -310,7 +309,6 @@ protected virtual void GetMapArgs(StringBuilder sb, BaseAddon camp) } } - /// /// Get startup args for loose maps /// @@ -356,26 +354,24 @@ protected virtual void GetBloodArgs(StringBuilder sb, BloodGame game, BaseAddon _ = sb.Append($@" -ini ""{ClientConsts.CrypticIni}"""); } - - if (bCamp.FileName is null) + if (bCamp.FileInfo is null) { return; } - if (bCamp.Type is AddonTypeEnum.TC) { if (bCamp.Executables is not null) { //don't add addon dir if the port is overridden } - else if (bCamp.FileName.Equals("addon.json")) + else if (bCamp.FileInfo.IsFolder) { - _ = sb.Append($@" {AddGameDirParam}""{Path.GetDirectoryName(bCamp.PathToFile)}"""); + _ = sb.Append($@" {AddGameDirParam}""{bCamp.FileInfo.PathToFolder}"""); } else { - _ = sb.Append($@" {AddFileParam}""{bCamp.PathToFile}"""); + _ = sb.Append($@" {AddFileParam}""{bCamp.FileInfo.PathToFile}"""); } } else if (bCamp.Type is AddonTypeEnum.Map) @@ -387,20 +383,18 @@ protected virtual void GetBloodArgs(StringBuilder sb, BloodGame game, BaseAddon throw new NotSupportedException($"Mod type {bCamp.Type} is not supported"); } - if (bCamp.RFF is not null) { _ = sb.Append($@" {AddRffParam}""{bCamp.RFF}"""); } - if (bCamp.SND is not null) { _ = sb.Append($@" {AddSndParam}""{bCamp.SND}"""); } } - protected virtual void GetSlaveArgs(StringBuilder sb, SlaveGame game, BaseAddon addon) + protected void GetSlaveArgs(StringBuilder sb, SlaveGame game, BaseAddon addon) { if (addon is LooseMap) { @@ -413,14 +407,14 @@ protected virtual void GetSlaveArgs(StringBuilder sb, SlaveGame game, BaseAddon throw new InvalidCastException(); } - if (sCamp.FileName is null) + if (sCamp.FileInfo is null) { return; } if (sCamp.Type is AddonTypeEnum.TC) { - _ = sb.Append($@" {AddFileParam}""{sCamp.PathToFile}"""); + _ = sb.Append($@" {AddFileParam}""{sCamp.FileInfo.PathToFile}"""); } else if (sCamp.Type is AddonTypeEnum.Map) { @@ -447,7 +441,6 @@ protected virtual void GetNamWW2GIArgs(StringBuilder sb, BaseGame game, BaseAddo throw new NotSupportedException(); } - if (addon is LooseMap) { GetLooseMapArgs(sb, game, addon); @@ -468,13 +461,11 @@ protected virtual void GetNamWW2GIArgs(StringBuilder sb, BaseGame game, BaseAddo _ = sb.Append($" {MainConParam}GAME.CON"); } - - if (dCamp.FileName is null) + if (dCamp.FileInfo is null) { return; } - if (dCamp.MainCon is not null) { _ = sb.Append($@" {MainConParam}""{dCamp.MainCon}"""); @@ -488,10 +479,9 @@ protected virtual void GetNamWW2GIArgs(StringBuilder sb, BaseGame game, BaseAddo } } - if (dCamp.Type is AddonTypeEnum.TC) { - _ = sb.Append($@" {AddFileParam}""{dCamp.PathToFile}"""); + _ = sb.Append($@" {AddFileParam}""{dCamp.FileInfo.PathToFile}"""); } else if (dCamp.Type is AddonTypeEnum.Map) { @@ -510,7 +500,7 @@ protected virtual void GetNamWW2GIArgs(StringBuilder sb, BaseGame game, BaseAddo /// Game /// Campaign\map /// Autoload mods - protected virtual void GetAutoloadModsArgs(StringBuilder sb, BaseGame game, BaseAddon addon, IReadOnlyDictionary mods) + protected virtual void GetAutoloadModsArgs(StringBuilder sb, BaseGame game, BaseAddon addon, IReadOnlyList mods) { if (mods.Count == 0) { @@ -518,11 +508,10 @@ protected virtual void GetAutoloadModsArgs(StringBuilder sb, BaseGame game, Base } var enabledModsCount = 0; - HashSet addedModsFiles = []; foreach (var mod in mods) { - if (mod.Value is not AutoloadMod aMod) + if (mod is not AutoloadMod aMod) { continue; } @@ -532,12 +521,18 @@ protected virtual void GetAutoloadModsArgs(StringBuilder sb, BaseGame game, Base continue; } - if (!addedModsFiles.TryGetValue(mod.Value.FileName!, out _)) + if (aMod.FileInfo is null) + { + continue; + } + + if (aMod.FileInfo.IsFolder) { - _ = addedModsFiles.Add(mod.Value.FileName!); - _ = sb.Append($@" {AddFileParam}""{aMod.FileName}"""); + throw new InvalidOperationException(); } + _ = sb.Append($@" {AddFileParam}""{aMod.FileInfo.FileName}"""); + if (aMod.AdditionalDefs is not null) { foreach (var def in aMod.AdditionalDefs) @@ -565,7 +560,6 @@ protected virtual void GetAutoloadModsArgs(StringBuilder sb, BaseGame game, Base } } - /// /// Method to perform after port is finished /// @@ -600,8 +594,6 @@ protected virtual void GetAutoloadModsArgs(StringBuilder sb, BaseGame game, Base /// String builder for parameters protected abstract void GetSkipStartupParameter(StringBuilder sb); - - /// /// Remove route 66 art files overrides used for RedNukem /// @@ -676,7 +668,6 @@ protected void RestoreWtFiles(BaseGame game) var art4 = Path.Combine(game.GameInstallFolder, "TILES022.ART"); var art4r = Path.Combine(game.GameInstallFolder, "TILES022._ART"); - if (File.Exists(art1r)) { File.Move(art1r, art1, true); @@ -733,7 +724,12 @@ protected void RestoreWtFiles(BaseGame game) } } - protected virtual void MoveSaveFilesToGameFolder(BaseGame game, BaseAddon campaign) + /// + /// Moves save files from the addon's saved games storage folder to the game's install folder. + /// + /// The game instance containing the target install folder. + /// The addon campaign whose saves are to be moved. + protected virtual void MoveSaveFilesFromStorage(BaseGame game, BaseAddon campaign) { var saveFolder = GetPathToAddonSavedGamesFolder(game.ShortName, campaign.AddonId.Id); @@ -746,17 +742,22 @@ protected virtual void MoveSaveFilesToGameFolder(BaseGame game, BaseAddon campai foreach (var save in saves) { - string destFileName = Path.Combine(game.GameInstallFolder!, Path.GetFileName(save)!); + var destFileName = Path.Combine(game.GameInstallFolder!, Path.GetFileName(save)!); File.Move(save, destFileName, true); } } - protected virtual void MoveSaveFilesFromGameFolder(BaseGame game, BaseAddon campaign) + /// + /// Moves save files from the game installation folder to the addon's saved games folder. + /// + /// The game whose save files are to be moved. + /// The addon or campaign whose save folder is the destination. + protected virtual void MoveSaveFilesToStorage(BaseGame game, BaseAddon campaign) { - //copying saved games into separate folder var saveFolder = GetPathToAddonSavedGamesFolder(game.ShortName, campaign.AddonId.Id); - string path = game.GameInstallFolder ?? throw new NullReferenceException(nameof(game.GameInstallFolder)); + ArgumentNullException.ThrowIfNull(game.GameInstallFolder); + var path = game.GameInstallFolder; var files = from file in Directory.GetFiles(path) from ext in SaveFileExtensions diff --git a/src/Ports/Ports/BuildGDX.cs b/src/Ports/Ports/BuildGDX.cs index ed68212d..38d73860 100644 --- a/src/Ports/Ports/BuildGDX.cs +++ b/src/Ports/Ports/BuildGDX.cs @@ -1,6 +1,5 @@ using System.Text; using Addons.Addons; -using Core.All; using Core.All.Enums; using Core.All.Enums.Versions; using Games.Games; @@ -119,17 +118,15 @@ public override string? InstalledVersion /// public override void BeforeStart(BaseGame game, BaseAddon campaign) { - MoveSaveFilesToGameFolder(game, campaign); - + MoveSaveFilesFromStorage(game, campaign); RestoreRoute66Files(game); - RestoreWtFiles(game); } /// public override void AfterEnd(BaseGame game, BaseAddon campaign) { - MoveSaveFilesFromGameFolder(game, campaign); + MoveSaveFilesToStorage(game, campaign); } /// @@ -176,7 +173,7 @@ protected override void GetStartCampaignArgs(StringBuilder sb, BaseGame game, Ba } /// - protected override void GetAutoloadModsArgs(StringBuilder sb, BaseGame _, BaseAddon addon, IReadOnlyDictionary mods) { } + protected override void GetAutoloadModsArgs(StringBuilder sb, BaseGame _, BaseAddon addon, IReadOnlyList mods) { } /// protected override void GetSkipIntroParameter(StringBuilder sb) { } diff --git a/src/Ports/Ports/DosBox.cs b/src/Ports/Ports/DosBox.cs index cc71d25e..395c86a2 100644 --- a/src/Ports/Ports/DosBox.cs +++ b/src/Ports/Ports/DosBox.cs @@ -1,6 +1,5 @@ using System.Text; using Addons.Addons; -using Core.All; using Core.All.Enums; using Core.All.Enums.Addons; using Core.All.Enums.Versions; @@ -118,8 +117,7 @@ public override string? InstalledVersion /// public override void BeforeStart(BaseGame game, BaseAddon campaign) { - MoveSaveFilesToGameFolder(game, campaign); - + MoveSaveFilesFromStorage(game, campaign); RestoreRoute66Files(game); try @@ -158,7 +156,7 @@ public override void BeforeStart(BaseGame game, BaseAddon campaign) /// public override void AfterEnd(BaseGame game, BaseAddon campaign) { - MoveSaveFilesFromGameFolder(game, campaign); + MoveSaveFilesToStorage(game, campaign); } /// @@ -210,8 +208,13 @@ private static void GetDukeArgs(StringBuilder sb, DukeGame game, BaseAddon addon } else if (addon is LooseMap map) { + if (map.FileInfo is null) + { + throw new InvalidOperationException(); + } + _ = sb.Append($@" -c ""mount d \""{game.MapsFolderPath}"""""); - _ = sb.Append($@" -c ""DUKE3D.EXE -map d:\\{map.FileName}"""); + _ = sb.Append($@" -c ""DUKE3D.EXE -map d:\\{map.FileInfo.FileName}"""); } else { @@ -256,8 +259,11 @@ protected override void GetBloodArgs(StringBuilder sb, BloodGame game, BaseAddon return; } - if (addon is BloodCampaign bCamp && bCamp.Type is AddonTypeEnum.TC) + if (addon is BloodCampaign bCamp && + bCamp.Type is AddonTypeEnum.TC && + addon.FileInfo is not null) { + if (Directory.Exists(ClientProperties.TempFolderPath)) { Directory.Delete(ClientProperties.TempFolderPath, true); @@ -267,23 +273,23 @@ protected override void GetBloodArgs(StringBuilder sb, BloodGame game, BaseAddon foreach (var filePath in Directory.GetFiles(game.GameInstallFolder)) { - string fileName = Path.GetFileName(filePath); + var fileName = Path.GetFileName(filePath); if (fileName.EndsWith(".DEM", StringComparison.OrdinalIgnoreCase)) { continue; } - string destFile = Path.Combine(ClientProperties.TempFolderPath, fileName); + var destFile = Path.Combine(ClientProperties.TempFolderPath, fileName); File.Copy(filePath, destFile, overwrite: true); } - if (addon.IsUnpacked) + if (addon.FileInfo.IsFolder) { - foreach (var filePath in Directory.GetFiles(Path.GetDirectoryName(addon.PathToFile)!)) + foreach (var filePath in Directory.GetFiles(addon.FileInfo.PathToFolder)) { - string fileName = Path.GetFileName(filePath); - string destFile = Path.Combine(ClientProperties.TempFolderPath, fileName); + var fileName = Path.GetFileName(filePath); + var destFile = Path.Combine(ClientProperties.TempFolderPath, fileName); File.Copy(filePath, destFile, overwrite: true); } } @@ -294,7 +300,7 @@ protected override void GetBloodArgs(StringBuilder sb, BloodGame game, BaseAddon Directory.CreateDirectory(ClientProperties.TempFolderPath); } - using var archive = ArchiveFactory.OpenArchive(addon.PathToFile); + using var archive = ArchiveFactory.OpenArchive(addon.FileInfo.PathToFile); archive.WriteToDirectory(ClientProperties.TempFolderPath); } @@ -304,11 +310,11 @@ protected override void GetBloodArgs(StringBuilder sb, BloodGame game, BaseAddon return; } - if (addon is LooseMap map) + if (addon is LooseMap map && map.FileInfo is not null) { _ = sb.Append(@$" -c ""mount c \""{game.GameInstallFolder}"""" -c ""c:"""); _ = sb.Append(@$" -c ""mount d \""{game.MapsFolderPath}"""""); - _ = sb.Append(@$" -c ""BLOOD.EXE -map d:\\{map.FileName}"""); + _ = sb.Append(@$" -c ""BLOOD.EXE -map d:\\{map.FileInfo.FileName}"""); return; } @@ -318,7 +324,7 @@ protected override void GetBloodArgs(StringBuilder sb, BloodGame game, BaseAddon } /// - protected override void GetAutoloadModsArgs(StringBuilder sb, BaseGame _, BaseAddon addon, IReadOnlyDictionary mods) { } + protected override void GetAutoloadModsArgs(StringBuilder sb, BaseGame _, BaseAddon addon, IReadOnlyList mods) { } /// protected override void GetSkipIntroParameter(StringBuilder sb) { } diff --git a/src/Ports/Ports/EDuke32/EDuke32.cs b/src/Ports/Ports/EDuke32/EDuke32.cs index 1a4d05f8..52ea571f 100644 --- a/src/Ports/Ports/EDuke32/EDuke32.cs +++ b/src/Ports/Ports/EDuke32/EDuke32.cs @@ -163,17 +163,15 @@ private void CreateWTStopgapFolder() /// public override void BeforeStart(BaseGame game, BaseAddon campaign) { - MoveSaveFilesFromGameFolder(game, campaign); - + MoveSaveFilesFromStorage(game, campaign); FixConfig(); - FixWtFiles(game, campaign); } /// public override void AfterEnd(BaseGame game, BaseAddon campaign) { - MoveSaveFilesToGameFolder(game, campaign); + MoveSaveFilesToStorage(game, campaign); } /// @@ -297,7 +295,7 @@ protected void GetDukeArgs(StringBuilder sb, DukeGame game, BaseAddon addon) } } - if (addon.FileName is null) + if (addon.FileInfo is null) { return; } @@ -335,7 +333,7 @@ protected void GetDukeArgs(StringBuilder sb, DukeGame game, BaseAddon addon) } else { - _ = sb.Append($@" {AddFileParam}""{dCamp.PathToFile}"""); + _ = sb.Append($@" {AddFileParam}""{dCamp.FileInfo.PathToFile}"""); } } else if (dCamp.Type is AddonTypeEnum.Map) @@ -368,17 +366,14 @@ protected void FixConfig() if (contents[i].StartsWith("SelectedGRP", StringComparison.OrdinalIgnoreCase)) { contents[i] = @"SelectedGRP = """""; - continue; } else if (contents[i].StartsWith("LastINI", StringComparison.OrdinalIgnoreCase)) { contents[i] = string.Empty; - continue; } else if (contents[i].StartsWith("ModDir", StringComparison.OrdinalIgnoreCase)) { contents[i] = string.Empty; - continue; } } @@ -438,16 +433,38 @@ protected void FixWtFiles(BaseGame game, BaseAddon campaign) } } - protected override void MoveSaveFilesToGameFolder(BaseGame game, BaseAddon campaign) + /// + protected override void MoveSaveFilesFromStorage(BaseGame game, BaseAddon campaign) + { + var saveFolder = GetPathToAddonSavedGamesFolder(game.ShortName, campaign.AddonId.Id); + + if (!Directory.Exists(saveFolder)) + { + return; + } + + var saves = Directory.GetFiles(saveFolder); + + var firstPart = campaign.FileInfo is not null && campaign.FileInfo.IsFolder ? campaign.FileInfo.PathToFolder : InstallFolderPath; + + foreach (var save in saves) + { + var destFileName = Path.Combine(firstPart, Path.GetFileName(save)!); + File.Move(save, destFileName, true); + } + } + + /// + protected override void MoveSaveFilesToStorage(BaseGame game, BaseAddon campaign) { //copying saved games into separate folder var saveFolder = GetPathToAddonSavedGamesFolder(game.ShortName, campaign.AddonId.Id); string path; - if (campaign.IsUnpacked) + if (campaign.FileInfo is not null && campaign.FileInfo.IsFolder) { - path = Path.GetDirectoryName(campaign.PathToFile)!; + path = campaign.FileInfo.PathToFolder; } else { @@ -470,24 +487,4 @@ where file.EndsWith(ext) File.Move(file, destFileName, true); } } - - protected override void MoveSaveFilesFromGameFolder(BaseGame game, BaseAddon campaign) - { - var saveFolder = GetPathToAddonSavedGamesFolder(game.ShortName, campaign.AddonId.Id); - - if (!Directory.Exists(saveFolder)) - { - return; - } - - var saves = Directory.GetFiles(saveFolder); - - string firstPart = campaign.IsUnpacked ? Path.GetDirectoryName(campaign.PathToFile)! : InstallFolderPath; - - foreach (var save in saves) - { - var destFileName = Path.Combine(firstPart, Path.GetFileName(save)!); - File.Move(save, destFileName, true); - } - } } diff --git a/src/Ports/Ports/EDuke32/Fury.cs b/src/Ports/Ports/EDuke32/Fury.cs index f90d3b99..e271308f 100644 --- a/src/Ports/Ports/EDuke32/Fury.cs +++ b/src/Ports/Ports/EDuke32/Fury.cs @@ -63,8 +63,7 @@ public Fury(IConfigProvider config) /// public override void BeforeStart(BaseGame game, BaseAddon campaign) { - MoveSaveFilesFromGameFolder(game, campaign); - + MoveSaveFilesFromStorage(game, campaign); FixConfig(); } @@ -99,7 +98,7 @@ protected override void GetStartCampaignArgs(StringBuilder sb, BaseGame game, Ba private void GetFuryArgs(StringBuilder sb, FuryGame game, BaseAddon addon) { - if (addon.FileName is null) + if (addon.FileInfo is null) { return; } @@ -131,7 +130,7 @@ private void GetFuryArgs(StringBuilder sb, FuryGame game, BaseAddon addon) if (fCamp.Type is AddonTypeEnum.TC) { - _ = sb.Append($@" {AddFileParam}""{fCamp.PathToFile}"""); + _ = sb.Append($@" {AddFileParam}""{fCamp.FileInfo.PathToFile}"""); } else if (fCamp.Type is AddonTypeEnum.Map) { diff --git a/src/Ports/Ports/EDuke32/NBlood.cs b/src/Ports/Ports/EDuke32/NBlood.cs index 9d702f9d..5df33a95 100644 --- a/src/Ports/Ports/EDuke32/NBlood.cs +++ b/src/Ports/Ports/EDuke32/NBlood.cs @@ -50,8 +50,7 @@ public class NBlood : EDuke32 /// public override void BeforeStart(BaseGame game, BaseAddon campaign) { - MoveSaveFilesFromGameFolder(game, campaign); - + MoveSaveFilesFromStorage(game, campaign); FixConfig(); } diff --git a/src/Ports/Ports/EDuke32/NotBlood.cs b/src/Ports/Ports/EDuke32/NotBlood.cs index ed0659fe..a47d2df2 100644 --- a/src/Ports/Ports/EDuke32/NotBlood.cs +++ b/src/Ports/Ports/EDuke32/NotBlood.cs @@ -40,8 +40,7 @@ public sealed class NotBlood : NBlood /// public override void BeforeStart(BaseGame game, BaseAddon campaign) { - MoveSaveFilesFromGameFolder(game, campaign); - + MoveSaveFilesFromStorage(game, campaign); FixConfig(); } } diff --git a/src/Ports/Ports/EDuke32/PCExhumed.cs b/src/Ports/Ports/EDuke32/PCExhumed.cs index fa4fe660..c1b838a7 100644 --- a/src/Ports/Ports/EDuke32/PCExhumed.cs +++ b/src/Ports/Ports/EDuke32/PCExhumed.cs @@ -35,8 +35,7 @@ public sealed class PCExhumed : EDuke32 /// public override void BeforeStart(BaseGame game, BaseAddon campaign) { - MoveSaveFilesFromGameFolder(game, campaign); - + MoveSaveFilesFromStorage(game, campaign); FixConfig(); } diff --git a/src/Ports/Ports/EDuke32/RedNukem.cs b/src/Ports/Ports/EDuke32/RedNukem.cs index f9095000..9b17fa0d 100644 --- a/src/Ports/Ports/EDuke32/RedNukem.cs +++ b/src/Ports/Ports/EDuke32/RedNukem.cs @@ -61,15 +61,10 @@ public sealed class RedNukem : EDuke32 public override void BeforeStart(BaseGame game, BaseAddon campaign) { CreateBlankDemo(); - CreateOrDeleteBlankAnm(true); - - MoveSaveFilesFromGameFolder(game, campaign); - + MoveSaveFilesFromStorage(game, campaign); FixConfig(); - FixRoute66Files(game, campaign); - FixWtFiles(game, campaign); } @@ -202,8 +197,7 @@ private void GetRedneckArgs(StringBuilder sb, RedneckGame game, BaseAddon addon) _ = sb.Append($@" {AddDirectoryParam}""{game.GameInstallFolder}"""); } - - if (addon.FileName is null) + if (addon.FileInfo is null) { return; } @@ -241,7 +235,7 @@ private void GetRedneckArgs(StringBuilder sb, RedneckGame game, BaseAddon addon) } else { - _ = sb.Append($@" {AddFileParam}""{rCamp.PathToFile}"""); + _ = sb.Append($@" {AddFileParam}""{rCamp.FileInfo.PathToFile}"""); } } else if (rCamp.Type is AddonTypeEnum.Map) diff --git a/src/Ports/Ports/EDuke32/VoidSW.cs b/src/Ports/Ports/EDuke32/VoidSW.cs index 495bc9ef..14b893fa 100644 --- a/src/Ports/Ports/EDuke32/VoidSW.cs +++ b/src/Ports/Ports/EDuke32/VoidSW.cs @@ -66,8 +66,7 @@ public sealed class VoidSW : EDuke32 /// public override void BeforeStart(BaseGame game, BaseAddon campaign) { - MoveSaveFilesFromGameFolder(game, campaign); - + MoveSaveFilesFromStorage(game, campaign); FixConfig(); } @@ -138,16 +137,14 @@ private void GetWangArgs(StringBuilder sb, WangGame game, BaseAddon addon) AddWangMusicFolder(sb, game); - - if (wCamp.FileName is null) + if (wCamp.FileInfo is null) { return; } - if (wCamp.Type is AddonTypeEnum.TC) { - _ = sb.Append($@" {AddDirectoryParam}""{game.CampaignsFolderPath}"" {AddFileParam}""{wCamp.FileName}"""); + _ = sb.Append($@" {AddDirectoryParam}""{game.CampaignsFolderPath}"" {AddFileParam}""{wCamp.FileInfo.FileName}"""); } else if (wCamp.Type is AddonTypeEnum.Map) { @@ -181,7 +178,6 @@ private static void AddWangMusicFolder(StringBuilder sb, WangGame game) if (Directory.Exists(folder)) { _ = sb.Append(@$" -j""{folder}"""); - return; } } } diff --git a/src/Ports/Ports/PortStarter.cs b/src/Ports/Ports/PortStarter.cs index 1580e3d8..6671a39f 100644 --- a/src/Ports/Ports/PortStarter.cs +++ b/src/Ports/Ports/PortStarter.cs @@ -45,7 +45,8 @@ public async Task StartAsync( byte? skill, bool skipIntro, bool skipStartup, - string? pathToExe = null) + string? pathToExe = null + ) { var sw = Stopwatch.StartNew(); @@ -56,7 +57,10 @@ public async Task StartAsync( var args = port.GetStartGameArgs(game, addon, mods, enabledOptions, skipIntro, skipStartup, skill); - _ = addon.Executables?[OSEnum.Windows].TryGetValue(port.PortEnum, out pathToExe); + if (addon.Executables?.TryGetValue(OSEnum.Windows, out var winDict) is true) + { + winDict.TryGetValue(port.PortEnum, out pathToExe); + } _logger.LogInformation($"=== Starting addon {addon.AddonId.Id} for {game.FullName} ==="); _logger.LogInformation($"Path to port exe {pathToExe}"); @@ -82,23 +86,22 @@ public async Task StartAsync( /// Path to custom port's exe private async Task StartPortAsync(BasePort port, string args, string? pathToExe = null) { - string exe; - - if (pathToExe is not null) - { - exe = pathToExe; - } - else - { - exe = port.PortExeFilePath; - } + var exe = pathToExe ?? port.PortExeFilePath; - await Process.Start(new ProcessStartInfo + using var process = Process.Start(new ProcessStartInfo { FileName = Path.GetFileName(exe), UseShellExecute = true, Arguments = args, WorkingDirectory = Path.GetDirectoryName(exe) - })!.WaitForExitAsync().ConfigureAwait(false); + }); + + if (process is null) + { + _logger.LogError("Failed to start process: {Exe}", exe); + return; + } + + await process.WaitForExitAsync().ConfigureAwait(false); } } diff --git a/src/Ports/Ports/Raze.cs b/src/Ports/Ports/Raze.cs index bb2c2f6d..1a28392f 100644 --- a/src/Ports/Ports/Raze.cs +++ b/src/Ports/Ports/Raze.cs @@ -265,13 +265,11 @@ private void GetDukeArgs(StringBuilder sb, DukeGame game, BaseAddon addon) _ = sb.Append($" -addon {dukeAddon}"); } - - if (dCamp.FileName is null) + if (dCamp.FileInfo is null) { return; } - if (dCamp.MainCon is not null) { _ = sb.Append($@" {MainConParam}""{dCamp.MainCon}"""); @@ -288,7 +286,7 @@ private void GetDukeArgs(StringBuilder sb, DukeGame game, BaseAddon addon) if (dCamp.Type is AddonTypeEnum.TC) { - _ = sb.Append($@" {AddFileParam}""{dCamp.PathToFile}"""); + _ = sb.Append($@" {AddFileParam}""{dCamp.FileInfo.PathToFile}"""); } else if (dCamp.Type is AddonTypeEnum.Map) { @@ -325,16 +323,14 @@ private void GetWangArgs(StringBuilder sb, WangGame game, BaseAddon addon) _ = sb.Append($" {AddFileParam}TD.GRP"); } - - if (wCamp.FileName is null) + if (wCamp.FileInfo is null) { return; } - if (wCamp.Type is AddonTypeEnum.TC) { - _ = sb.Append($@" {AddFileParam}""{wCamp.PathToFile}"""); + _ = sb.Append($@" {AddFileParam}""{wCamp.FileInfo.PathToFile}"""); } else if (wCamp.Type is AddonTypeEnum.Map) { @@ -372,13 +368,11 @@ private void GetRedneckArgs(StringBuilder sb, RedneckGame game, BaseAddon addon) AddGamePathsToConfig(game, addon, game.AgainInstallPath!, pathToConfig); } - - if (rCamp.FileName is null) + if (rCamp.FileInfo is null) { return; } - if (rCamp.MainCon is not null) { _ = sb.Append($@" {MainConParam}""{rCamp.MainCon}"""); @@ -395,7 +389,7 @@ private void GetRedneckArgs(StringBuilder sb, RedneckGame game, BaseAddon addon) if (rCamp.Type is AddonTypeEnum.TC) { - _ = sb.Append($@" {AddFileParam}""{rCamp.PathToFile}"""); + _ = sb.Append($@" {AddFileParam}""{rCamp.FileInfo.PathToFile}"""); } else if (rCamp.Type is AddonTypeEnum.Map) { @@ -463,9 +457,10 @@ private static void AddGamePathsToConfig(BaseGame game, BaseAddon campaign, stri //blood unpacked addons if (campaign is BloodCampaign bCamp && - bCamp.IsUnpacked) + bCamp.FileInfo is not null && + bCamp.FileInfo.IsFolder) { - path = Path.GetDirectoryName(bCamp.PathToFile)!.Replace('\\', '/'); + path = bCamp.FileInfo.PathToFolder.Replace('\\', '/'); _ = sb.Append("Path=").AppendLine(path); } @@ -473,7 +468,7 @@ private static void AddGamePathsToConfig(BaseGame game, BaseAddon campaign, stri { i++; } - while (!string.IsNullOrWhiteSpace(contents[i])); + while (i < contents.Length && !string.IsNullOrWhiteSpace(contents[i])); _ = sb.AppendLine(); continue; diff --git a/src/Ports/Ports/StubPort.cs b/src/Ports/Ports/StubPort.cs index 3701520b..c0f1ae28 100644 --- a/src/Ports/Ports/StubPort.cs +++ b/src/Ports/Ports/StubPort.cs @@ -1,6 +1,5 @@ using System.Text; using Addons.Addons; -using Core.All; using Core.All.Enums; using Games.Games; @@ -58,7 +57,7 @@ public override void BeforeStart(BaseGame game, BaseAddon campaign) { } - protected override void GetAutoloadModsArgs(StringBuilder sb, BaseGame game, BaseAddon addon, IReadOnlyDictionary mods) + protected override void GetAutoloadModsArgs(StringBuilder sb, BaseGame game, BaseAddon addon, IReadOnlyList mods) { } diff --git a/src/Ports/Providers/PortsProvider.cs b/src/Ports/Providers/PortsProvider.cs index e4ca4f60..c3993f2d 100644 --- a/src/Ports/Providers/PortsProvider.cs +++ b/src/Ports/Providers/PortsProvider.cs @@ -132,7 +132,13 @@ private void UpdateCustomPortsList() foreach (var port in dbContext.CustomPorts.OrderBy(static x => x.Name)) { - _customPorts.Add(new() { Name = port.Name, Path = port.PathToExe, BasePort = _ports.Values.First(x => x.PortEnum == port.PortEnum) }); + var basePort = _ports.Values.FirstOrDefault(x => x.PortEnum == port.PortEnum); + if (basePort is null) + { + continue; + } + + _customPorts.Add(new() { Name = port.Name, Path = port.PathToExe, BasePort = basePort }); } } } diff --git a/src/Tests.Unit/AddonFilePathWrapperTests.cs b/src/Tests.Unit/AddonFilePathWrapperTests.cs new file mode 100644 index 00000000..07901e20 --- /dev/null +++ b/src/Tests.Unit/AddonFilePathWrapperTests.cs @@ -0,0 +1,226 @@ +using Core.Client.Helpers; +using Tests.Unit.Helpers; + +namespace Tests.Unit; + +public sealed class AddonFilePathWrapperTests +{ + [Fact] + public void Constructor_SetsProperties() + { + var wrapper = new AddonFilePathWrapper(@"C:\addons\myaddon", "manifest.json"); + + Assert.Equal(@"C:\addons\myaddon", NormalizerHelper.NormalizePath(wrapper.PathToFolder)); + Assert.Equal("manifest.json", NormalizerHelper.NormalizePath(wrapper.FileName)); + Assert.Equal(@"C:\addons\myaddon\manifest.json", NormalizerHelper.NormalizePath(wrapper.PathToFile)); + } + + [Fact] + public void Constructor_ZipPath_SetsFolderToParent() + { + var wrapper = new AddonFilePathWrapper(@"C:\addons\pack.zip", "addon.json"); + + Assert.Equal(@"C:\addons", NormalizerHelper.NormalizePath(wrapper.PathToFolder)); + Assert.Equal("pack.zip", NormalizerHelper.NormalizePath(wrapper.FileName)); + Assert.Equal(@"C:\addons\pack.zip", NormalizerHelper.NormalizePath(wrapper.PathToFile)); + } + + [Theory] + [InlineData(@"C:\addons\myaddon", true)] + [InlineData(@"C:\addons\myaddon.zip", false)] + [InlineData(@"C:\addons\myaddon.map", false)] + [InlineData(@"C:\addons\myaddon.json", false)] + public void IsFolder_ReturnsExpected(string path, bool expected) + { + var wrapper = new AddonFilePathWrapper(path, "manifest.json"); + + Assert.Equal(expected, wrapper.IsFolder); + } + + [Theory] + [InlineData(@"C:\addons\myaddon", "info.json", true)] + [InlineData(@"C:\addons\myaddon", "info.JSON", true)] + [InlineData(@"C:\addons\myaddon", "info.JsOn", true)] + [InlineData(@"C:\addons\myaddon", "info.xml", false)] + [InlineData(@"C:\addons\myaddon", "json", false)] + public void IsJson_ReturnsExpected(string path, string fileName, bool expected) + { + var wrapper = new AddonFilePathWrapper(path, fileName); + + Assert.Equal(expected, wrapper.IsJson); + } + + [Fact] + public void IsJson_ReturnsFalse_WhenPathIsNotFolder() + { + var wrapper = new AddonFilePathWrapper(@"C:\addons\myaddon.zip", "info.json"); + + Assert.False(wrapper.IsJson); + } + + [Theory] + [InlineData(@"C:\addons\myaddon.zip", true)] + [InlineData(@"C:\addons\myaddon.ZIP", true)] + [InlineData(@"C:\addons\myaddon.Zip", true)] + [InlineData(@"C:\addons\myaddon", false)] + [InlineData(@"C:\addons\myaddon.json", false)] + [InlineData(@"C:\addons\myaddon.map", false)] + public void IsZip_ReturnsExpected(string path, bool expected) + { + var wrapper = new AddonFilePathWrapper(path, "manifest.json"); + + Assert.Equal(expected, wrapper.IsZip); + } + + [Theory] + [InlineData(@"C:\addons\myaddon", "level.map", true)] + [InlineData(@"C:\addons\myaddon", "level.MAP", true)] + [InlineData(@"C:\addons\myaddon", "level.Map", true)] + [InlineData(@"C:\addons\myaddon", "level.txt", false)] + [InlineData(@"C:\addons\myaddon", "map", false)] + public void IsMap_ReturnsExpected(string path, string fileName, bool expected) + { + var wrapper = new AddonFilePathWrapper(path, fileName); + + Assert.Equal(expected, wrapper.IsMap); + } + + [Fact] + public void IsMap_ReturnsFalse_WhenPathIsNotFolder() + { + var wrapper = new AddonFilePathWrapper(@"C:\addons\myaddon.zip", "level.map"); + + Assert.False(wrapper.IsMap); + } + + [Theory] + [InlineData(@"C:\addons\myaddon", "info.grpinfo", true)] + [InlineData(@"C:\addons\myaddon", "info.GRPINFO", true)] + [InlineData(@"C:\addons\myaddon", "info.GrpInfo", true)] + [InlineData(@"C:\addons\myaddon", "info.txt", false)] + [InlineData(@"C:\addons\myaddon", "grpinfo", false)] + public void IsGrpInfo_ReturnsExpected(string path, string fileName, bool expected) + { + var wrapper = new AddonFilePathWrapper(path, fileName); + + Assert.Equal(expected, wrapper.IsGrpInfo); + } + + [Fact] + public void IsGrpInfo_ReturnsFalse_WhenPathIsNotFolder() + { + var wrapper = new AddonFilePathWrapper(@"C:\addons\myaddon.zip", "info.grpinfo"); + + Assert.False(wrapper.IsGrpInfo); + } + + [Theory] + [InlineData(@"C:\addons\myaddon", "manifest.json", @"C:\addons\myaddon\manifest.json")] + [InlineData(@"C:\addons\folder", "file.map", @"C:\addons\folder\file.map")] + public void PathToFile_ForFolder_ReturnsCombinedPath(string path, string fileName, string expected) + { + var wrapper = new AddonFilePathWrapper(path, fileName); + + Assert.Equal(expected, NormalizerHelper.NormalizePath(wrapper.PathToFile)); + } + + [Fact] + public void PathToFile_ForZip_ReturnsZipPath() + { + var wrapper = new AddonFilePathWrapper(@"C:\addons\pack.zip", "addon.json"); + + Assert.Equal(@"C:\addons\pack.zip", NormalizerHelper.NormalizePath(wrapper.PathToFile)); + } + + [Theory] + [InlineData(@"C:\addons\myaddon")] + [InlineData(@"C:\addons\folder")] + public void PathToFolder_ForFolder_ReturnsSamePath(string path) + { + var wrapper = new AddonFilePathWrapper(path, "manifest.json"); + + Assert.Equal(path, NormalizerHelper.NormalizePath(wrapper.PathToFolder)); + } + + [Fact] + public void PathToFolder_ForZip_ReturnsParentDirectory() + { + var wrapper = new AddonFilePathWrapper(@"C:\addons\pack.zip", "addon.json"); + + Assert.Equal(@"C:\addons", NormalizerHelper.NormalizePath(wrapper.PathToFolder)); + } + + [Fact] + public void FileName_ForFolder_ReturnsManifestName() + { + var wrapper = new AddonFilePathWrapper(@"C:\addons\myaddon", "manifest.json"); + + Assert.Equal("manifest.json", NormalizerHelper.NormalizePath(wrapper.FileName)); + } + + [Fact] + public void FileName_ForZip_ReturnsZipName() + { + var wrapper = new AddonFilePathWrapper(@"C:\addons\pack.zip", "addon.json"); + + Assert.Equal("pack.zip", NormalizerHelper.NormalizePath(wrapper.FileName)); + } + + [Fact] + public void WithChangedFolder_FolderPath_ReturnsNewWrapperWithUpdatedPath() + { + var wrapper = new AddonFilePathWrapper(@"C:\addons\old", "manifest.json"); + var updated = wrapper.WithChangedFolder(@"D:\new\path"); + + Assert.Equal(@"D:\new\path", NormalizerHelper.NormalizePath(updated.PathToFolder)); + Assert.Equal(@"D:\new\path\manifest.json", NormalizerHelper.NormalizePath(updated.PathToFile)); + Assert.Equal("manifest.json", NormalizerHelper.NormalizePath(updated.FileName)); + Assert.True(updated.IsFolder); + } + + [Fact] + public void WithChangedFolder_ZipPath_ReturnsNewWrapperWithUpdatedPath() + { + var wrapper = new AddonFilePathWrapper(@"C:\addons\pack.zip", "addon.json"); + var updated = wrapper.WithChangedFolder(@"D:\mods\newpack.zip"); + + Assert.Equal(@"D:\mods", NormalizerHelper.NormalizePath(updated.PathToFolder)); + Assert.Equal(@"D:\mods\newpack.zip", NormalizerHelper.NormalizePath(updated.PathToFile)); + Assert.Equal("newpack.zip", NormalizerHelper.NormalizePath(updated.FileName)); + Assert.True(updated.IsZip); + } + + [Fact] + public void WithChangedFolder_FromFolderToZip_UpdatesTypeFlags() + { + var folder = new AddonFilePathWrapper(@"C:\addons\myaddon", "manifest.json"); + var zipped = folder.WithChangedFolder(@"C:\addons\myaddon.zip"); + + Assert.True(zipped.IsZip); + Assert.False(zipped.IsFolder); + Assert.Equal("myaddon.zip", NormalizerHelper.NormalizePath(zipped.FileName)); + Assert.Equal(@"C:\addons", NormalizerHelper.NormalizePath(zipped.PathToFolder)); + } + + [Fact] + public void WithChangedFolder_OriginalWrapperIsUnchanged() + { + var original = new AddonFilePathWrapper(@"C:\addons\original", "manifest.json"); + _ = original.WithChangedFolder(@"D:\new"); + + Assert.Equal(@"C:\addons\original", NormalizerHelper.NormalizePath(original.PathToFolder)); + Assert.Equal("manifest.json", NormalizerHelper.NormalizePath(original.FileName)); + } + + [Fact] + public void RecordEquality_ByValue() + { + var a = new AddonFilePathWrapper(@"C:\addons\a", "m.json"); + var b = new AddonFilePathWrapper(@"C:\addons\a", "m.json"); + var c = new AddonFilePathWrapper(@"C:\addons\a", "n.json"); + + Assert.Equal(a, b); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + Assert.NotEqual(a, c); + } +} diff --git a/src/Tests.Unit/AddonFilesTests.cs b/src/Tests.Unit/AddonFilesTests.cs deleted file mode 100644 index 15e5385c..00000000 --- a/src/Tests.Unit/AddonFilesTests.cs +++ /dev/null @@ -1,161 +0,0 @@ -using Addons.Providers; -using Core.All.Enums; -using Core.Client.Api; -using Core.Client.Cache; -using Core.Client.Interfaces; -using Core.Client.Providers; -using Games.Games; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; - -namespace Tests.Unit; - -[Collection("Sync")] -public sealed class AddonFilesTests : IDisposable -{ - private readonly InstalledAddonsProvider _installedAddonsProvider; - - public AddonFilesTests() - { - var game = new Mock(); - _ = game.Setup(x => x.GameEnum).Returns(GameEnum.Blood); - _ = game.Setup(x => x.FullName).Returns("Blood"); - _ = game.Setup(x => x.ShortName).Returns("Blood"); - - var config = new Mock(); - _ = config.Setup(x => x.DisabledAutoloadMods).Returns([]); - _ = config.Setup(x => x.FavoriteAddons).Returns([]); - - var bmCache = new Mock>(); - MetadataProvider metadataProvider = new(new OfflineApiInterface(NullLogger.Instance), NullLogger.Instance); - OriginalCampaignsProvider originalCampaignsProvider = new(config.Object); - - _installedAddonsProvider = new( - game.Object, - config.Object, - bmCache.Object, - originalCampaignsProvider, - metadataProvider, - NullLogger.Instance - ); - } - - public void Dispose() - { - _installedAddonsProvider.Dispose(); - Directory.Delete("FilesTemp", true); - } - - - [Fact] - public async Task AddonArchiveTest() - { - _ = Directory.CreateDirectory("FilesTemp"); - File.Copy(Path.Combine("Files", "ZippedAddon.zip"), Path.Combine("FilesTemp", "ZippedAddon.zip")); - - var pathToFile = Path.Combine(Directory.GetCurrentDirectory(), "FilesTemp", "ZippedAddon.zip"); - - var result = await _installedAddonsProvider.GetAddonsFromFilesAsync([pathToFile]); - - Assert.Equal(2, result.Count); - - var a = result.First(); - var b = result.Last(); - - Assert.Equal("blood-voxel-pack", a.Key.Id); - Assert.Equal("p292", a.Key.Version); - Assert.Equal("Voxel Pack", a.Value.Title); - - Assert.Equal("blood-voxel-pack-2", b.Key.Id); - Assert.Equal("p292-2", b.Key.Version); - Assert.Equal("Voxel Pack 2", b.Value.Title); - - Assert.True(File.Exists(pathToFile)); - Assert.False(Directory.Exists(pathToFile.Replace(".zip", ""))); - } - - [Fact] - public async Task UnpackedAddonTest() - { - _ = Directory.CreateDirectory("FilesTemp"); - File.Copy(Path.Combine("Files", "UnpackedAddon.zip"), Path.Combine("FilesTemp", "UnpackedAddon.zip")); - - var pathToFile = Path.Combine(Directory.GetCurrentDirectory(), "FilesTemp", "UnpackedAddon.zip"); - - var result = await _installedAddonsProvider.GetAddonsFromFilesAsync([pathToFile]); - - Assert.Equal(2, result.Count); - - var a = result[new("blood-voxel-pack", "p292")]; - var b = result[new("blood-voxel-pack-2", "p292-2")]; - - Assert.Equal("blood-voxel-pack", a.AddonId.Id); - Assert.Equal("p292", a.AddonId.Version); - Assert.Equal("Voxel Pack", a.Title); - - Assert.Equal("blood-voxel-pack-2", b.AddonId.Id); - Assert.Equal("p292-2", b.AddonId.Version); - Assert.Equal("Voxel Pack 2", b.Title); - - Assert.False(File.Exists(pathToFile)); - Assert.True(Directory.Exists(pathToFile.Replace(".zip", ""))); - } - - [Fact] - public async Task LooseMapTest() - { - _ = Directory.CreateDirectory("FilesTemp"); - File.Copy(Path.Combine("Files", "TEST.MAP"), Path.Combine("FilesTemp", "TEST.MAP")); - - var pathToFile = Path.Combine(Directory.GetCurrentDirectory(), "FilesTemp", "TEST.MAP"); - - var result = await _installedAddonsProvider.GetAddonsFromFilesAsync([pathToFile]); - - var map = Assert.Single(result); - - Assert.Equal("TEST.MAP", map.Key.Id); - Assert.Null(map.Key.Version); - Assert.Equal("TEST.MAP", map.Value.Title); - - Assert.True(File.Exists(pathToFile)); - } - - [Fact] - public async Task GrpInfoTest() - { - _ = Directory.CreateDirectory("FilesTemp"); - File.Copy(Path.Combine("Files", "GrpInfoAddon.zip"), Path.Combine("FilesTemp", "GrpInfoAddon.zip")); - - var pathToFile = Path.Combine(Directory.GetCurrentDirectory(), "FilesTemp", "GrpInfoAddon.zip"); - - var result = await _installedAddonsProvider.GetAddonsFromFilesAsync([pathToFile]); - - Assert.Empty(result); - - Assert.False(File.Exists(pathToFile)); - Assert.True(Directory.Exists(pathToFile.Replace(".zip", ""))); - Assert.True(File.Exists(Path.Combine(pathToFile.Replace(".zip", ""), "addons.grpinfo"))); - } - - [Fact] - public async Task WhatLiesBeneathTest() - { - _ = Directory.CreateDirectory("FilesTemp"); - File.Copy(Path.Combine("Files", "WhatLiesBeneathAddon.zip"), Path.Combine("FilesTemp", "WhatLiesBeneathAddon.zip")); - - var pathToFile = Path.Combine(Directory.GetCurrentDirectory(), "FilesTemp", "WhatLiesBeneathAddon.zip"); - - var result = await _installedAddonsProvider.GetAddonsFromFilesAsync([pathToFile]); - - _ = Assert.Single(result); - - var a = result.First(); - - Assert.Equal("blood-what-lies-beneath", a.Key.Id); - Assert.Equal("1.1.7", a.Key.Version); - Assert.Equal("What Lies Beneath", a.Value.Title); - - Assert.False(File.Exists(pathToFile)); - Assert.True(Directory.Exists(pathToFile.Replace(".zip", ""))); - } -} diff --git a/src/Tests.Unit/AutoloadModsValidatorTests.cs b/src/Tests.Unit/AutoloadModsValidatorTests.cs new file mode 100644 index 00000000..d9cb1109 --- /dev/null +++ b/src/Tests.Unit/AutoloadModsValidatorTests.cs @@ -0,0 +1,474 @@ +using System.Collections.Immutable; +using Addons.Addons; +using Addons.Helpers; +using Core.All; +using Core.All.Enums; +using Core.All.Enums.Versions; +using Core.Client.Helpers; + +namespace Tests.Unit; + +public sealed class AutoloadModsValidatorTests +{ + private static readonly GameInfo DukeGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic); + private static readonly GameInfo DukeGameWT = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_WT); + private static readonly GameInfo BloodGame = new(GameEnum.Blood); + private static readonly GameInfo RedneckGame = new(GameEnum.Redneck); + private static readonly GameInfo RidesAgainGame = new(GameEnum.RidesAgain); + + private static AutoloadMod CreateMod( + string id, + string? version, + GameInfo game, + bool enabled = true, + IReadOnlyDictionary? dependentAddons = null, + IReadOnlyDictionary? incompatibleAddons = null, + ImmutableArray? requiredFeatures = null) + { + return new AutoloadMod + { + AddonId = new(id, version), + Type = AddonTypeEnum.Mod, + Title = id, + SupportedGame = game, + FileInfo = new AddonFilePathWrapper("D:\\Mods", $"{id}.zip"), + DependentAddons = dependentAddons, + IncompatibleAddons = incompatibleAddons, + RequiredFeatures = requiredFeatures, + IsEnabled = enabled, + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + } + + private static AutoloadMod CreateCampaign( + string id, + string? version, + GameInfo game, + IReadOnlyDictionary? incompatibleAddons = null, + IReadOnlyDictionary? dependentAddons = null) + { + return new AutoloadMod + { + AddonId = new(id, version), + Type = AddonTypeEnum.TC, + Title = id, + SupportedGame = game, + FileInfo = new AddonFilePathWrapper("D:\\Campaigns", $"{id}.zip"), + DependentAddons = dependentAddons, + IncompatibleAddons = incompatibleAddons, + RequiredFeatures = null, + IsEnabled = true, + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + } + + [Fact] + public void ValidateAutoloadMod_DisabledMod_ReturnsFalse() + { + var mod = CreateMod("someMod", "1.0", DukeGame, enabled: false); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_DifferentGame_ReturnsFalse() + { + var mod = CreateMod("dukeMod", "1.0", DukeGame); + var campaign = CreateCampaign("bloodCampaign", "1.0", BloodGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_SameGame_ReturnTrue() + { + var mod = CreateMod("dukeMod", "1.0", DukeGame); + var campaign = CreateCampaign("dukeCampaign", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_RedneckModWithRidesAgainCampaign_ReturnsTrue() + { + var mod = CreateMod("redneckMod", "1.0", RedneckGame); + var campaign = CreateCampaign("ridesAgainCampaign", "1.0", RidesAgainGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_RidesAgainModWithRedneckCampaign_ReturnsFalse() + { + var mod = CreateMod("ridesAgainMod", "1.0", RidesAgainGame); + var campaign = CreateCampaign("redneckCampaign", "1.0", RedneckGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_DifferentGameVersion_ReturnsFalse() + { + var mod = CreateMod("someMod", "1.0", DukeGameWT); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_NullGameVersion_ReturnTrue() + { + var mod = CreateMod("someMod", "1.0", new GameInfo(GameEnum.Duke3D)); + var campaign = CreateCampaign("campaign", "1.0", new GameInfo(GameEnum.Duke3D)); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_RequiredFeaturesNotSupported_ReturnsFalse() + { + var mod = CreateMod("featureMod", "1.0", DukeGame, requiredFeatures: [FeatureEnum.Models, FeatureEnum.Hightile]); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + var features = new List { FeatureEnum.EDuke32_CON }; + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], features); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_RequiredFeaturesSupported_ReturnTrue() + { + var mod = CreateMod("featureMod", "1.0", DukeGame, requiredFeatures: [FeatureEnum.Models, FeatureEnum.Hightile]); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + var features = new List { FeatureEnum.Models, FeatureEnum.Hightile, FeatureEnum.EDuke32_CON }; + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], features); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_NullRequiredFeatures_ReturnTrue() + { + var mod = CreateMod("simpleMod", "1.0", DukeGame); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_CampaignIncompatibleWildcard_ReturnsFalse() + { + var mod = CreateMod("anyMod", "1.0", DukeGame); + var campaign = CreateCampaign("campaign", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "*", null } }); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_CampaignIncompatibleWithModId_ReturnsFalse() + { + var mod = CreateMod("specificMod", "1.0", DukeGame); + var campaign = CreateCampaign("campaign", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "specificMod", null } }); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_CampaignIncompatibleWithDifferentModId_ReturnTrue() + { + var mod = CreateMod("myMod", "1.0", DukeGame); + var campaign = CreateCampaign("campaign", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "otherMod", null } }); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_CampaignIncompatibleWithMatchingVersion_ReturnsFalse() + { + var mod = CreateMod("versionedMod", "1.5", DukeGame); + var campaign = CreateCampaign("campaign", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "versionedMod", "1.5" } }); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_CampaignIncompatibleWithNonMatchingVersion_ReturnTrue() + { + var mod = CreateMod("versionedMod", "1.5", DukeGame); + var campaign = CreateCampaign("campaign", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "versionedMod", "1.0" } }); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_DependencyOnCampaignItself_ReturnTrue() + { + var mod = CreateMod("dependentMod", "1.0", DukeGame, dependentAddons: new Dictionary { { "campaign", null } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_DependencyOnCampaignDependentAddon_ReturnTrue() + { + var mod = CreateMod("dependentMod", "1.0", DukeGame, dependentAddons: new Dictionary { { "someAddon", null } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame, dependentAddons: new Dictionary { { "someAddon", "1.0" } }); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_DependencyOnOtherMod_ReturnTrue() + { + var mod = CreateMod("dependentMod", "1.0", DukeGame, dependentAddons: new Dictionary { { "helperMod", null } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + var helperMod = CreateMod("helperMod", "2.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [helperMod], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_DependencyUnsatisfied_ReturnsFalse() + { + var mod = CreateMod("needyMod", "1.0", DukeGame, dependentAddons: new Dictionary { { "missingMod", null } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_DependencyVersionConstraintSatisfied_ReturnTrue() + { + var mod = CreateMod("dependentMod", "1.0", DukeGame, dependentAddons: new Dictionary { { "helperMod", ">1.0" } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + var helperMod = CreateMod("helperMod", "2.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [helperMod], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_DependencyVersionConstraintUnsatisfied_ReturnsFalse() + { + var mod = CreateMod("dependentMod", "1.0", DukeGame, dependentAddons: new Dictionary { { "helperMod", "<=1.0" } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + var helperMod = CreateMod("helperMod", "2.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [helperMod], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_NullDependentAddons_ReturnTrue() + { + var mod = CreateMod("independentMod", "1.0", DukeGame); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_IncompatibleWithCampaign_ReturnsFalse() + { + var mod = CreateMod("conflictingMod", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "campaign", null } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_IncompatibleWithEnabledMod_ReturnsFalse() + { + var mod = CreateMod("conflictingMod", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "otherMod", null } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + var otherMod = CreateMod("otherMod", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [otherMod], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_IncompatibleWithDisabledMod_ReturnTrue() + { + var mod = CreateMod("conflictingMod", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "disabledMod", null } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + var disabledMod = CreateMod("disabledMod", "1.0", DukeGame, enabled: false); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [disabledMod], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_IncompatibleWithNonMatchingAddon_ReturnTrue() + { + var mod = CreateMod("conflictingMod", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "unrelatedMod", null } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + var otherMod = CreateMod("otherMod", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [otherMod], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_IncompatibleWithCampaignUsingOperatorPrefix_ReturnsFalse() + { + var mod = CreateMod("conflictingMod", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "campaign", "==1.0" } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_IncompatibleVersionDoesNotMatch_ReturnTrue() + { + var mod = CreateMod("conflictingMod", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "campaign", "==2.0" } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_NullIncompatibleAddons_ReturnTrue() + { + var mod = CreateMod("friendlyMod", "1.0", DukeGame); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_AllChecksPass_ReturnsTrue() + { + var mod = CreateMod("validMod", "1.0", DukeGame); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_DependencyOnDisabledMod_ReturnTrue() + { + var mod = CreateMod("dependentMod", "1.0", DukeGame, dependentAddons: new Dictionary { { "disabledSatisfier", null } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + var disabledMod = CreateMod("disabledSatisfier", "2.0", DukeGame, enabled: false); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [disabledMod], []); + + Assert.True(result); + } + + [Fact] + public void ValidateAutoloadMod_PartialDependenciesUnsatisfied_ReturnsFalse() + { + var mod = CreateMod("needyMod", "1.0", DukeGame, dependentAddons: new Dictionary + { + { "presentMod", null }, + { "missingMod", null } + }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + var presentMod = CreateMod("presentMod", "1.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [presentMod], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_IncompatibleWithEnabledModByVersion_ReturnsFalse() + { + var mod = CreateMod("conflictingMod", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "versionedMod", "==2.0" } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + var versionedMod = CreateMod("versionedMod", "2.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [versionedMod], []); + + Assert.False(result); + } + + [Fact] + public void ValidateAutoloadMod_IncompatibleWithEnabledModByDifferentVersion_ReturnTrue() + { + var mod = CreateMod("conflictingMod", "1.0", DukeGame, incompatibleAddons: new Dictionary { { "versionedMod", "==2.0" } }); + var campaign = CreateCampaign("campaign", "1.0", DukeGame); + var versionedMod = CreateMod("versionedMod", "3.0", DukeGame); + + var result = AutoloadModsValidator.ValidateAutoloadMod(mod, campaign, [versionedMod], []); + + Assert.True(result); + } +} diff --git a/src/Tests.Unit/BaseAddonTests.cs b/src/Tests.Unit/BaseAddonTests.cs new file mode 100644 index 00000000..7a699b2b --- /dev/null +++ b/src/Tests.Unit/BaseAddonTests.cs @@ -0,0 +1,170 @@ +using Addons.Addons; +using Core.All.Enums; + +namespace Tests.Unit; + +public sealed class BaseAddonTests +{ + private static DukeCampaign CreateAddon( + string? id = null, + string? version = null, + string? title = null, + string? author = null, + DateOnly? releaseDate = null, + string? description = null, + AddonTypeEnum type = AddonTypeEnum.Mod, + IReadOnlyDictionary? dependentAddons = null, + IReadOnlyDictionary? incompatibleAddons = null) + { + return new DukeCampaign + { + AddonId = new(id ?? "test-addon", version), + FileInfo = null, + Type = type, + SupportedGame = new(GameEnum.Duke3D), + Title = title ?? "Test Addon", + Author = author, + ReleaseDate = releaseDate, + Description = description, + RequiredFeatures = null, + DependentAddons = dependentAddons, + IncompatibleAddons = incompatibleAddons, + GridImageHash = null, + PreviewImageHash = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + Executables = null, + Options = null, + MainCon = null, + AdditionalCons = null, + RTS = null, + }; + } + + [Fact] + public void ToMarkdownString_Minimal_ReturnsOnlyTitle() + { + var addon = CreateAddon(title: "Minimal"); + + var result = addon.ToMarkdownString(); + + Assert.Equal("## Minimal", result); + } + + [Fact] + public void ToMarkdownString_WithVersion_IncludesVersion() + { + var addon = CreateAddon(version: "2.1"); + + var result = addon.ToMarkdownString(); + + Assert.Equal($"## Test Addon{Environment.NewLine}{Environment.NewLine}#### v2.1", result); + } + + [Fact] + public void ToMarkdownString_WithAuthorAndDate_IncludesBoth() + { + var addon = CreateAddon(title: "My Addon", version: "1.0", author: "Tester", releaseDate: new(2024, 3, 15)); + + var result = addon.ToMarkdownString(); + + var nl = Environment.NewLine; + Assert.Equal($"## My Addon{nl}{nl}#### v1.0{nl}{nl}*Released on:* 15.03.2024{nl}{nl}*by Tester*", result); + } + + [Fact] + public void ToMarkdownString_WithDescriptionWithoutUrls_JoinsLines() + { + var addon = CreateAddon(title: "Desc", version: "1.0", description: "Line one\nLine two\nLine three"); + + var result = addon.ToMarkdownString(); + + var nl = Environment.NewLine; + Assert.Equal($"## Desc{nl}{nl}#### v1.0{nl}{nl}Line one{nl}{nl}Line two{nl}{nl}Line three", result); + } + + [Fact] + public void ToMarkdownString_WithUrlLineInDescription_ConvertsToMarkdownLink() + { + var addon = CreateAddon(title: "Url", version: "1.0", description: "https://example.com"); + + var result = addon.ToMarkdownString(); + + var nl = Environment.NewLine; + Assert.Equal($"## Url{nl}{nl}#### v1.0{nl}{nl}[https://example.com](https://example.com)", result); + } + + [Fact] + public void ToMarkdownString_WithUrlAmongLines_ConvertsOnlyUrlLines() + { + var addon = CreateAddon(title: "Mix", version: "1.0", description: "Normal text\nhttps://example.com\nMore text"); + + var result = addon.ToMarkdownString(); + + var nl = Environment.NewLine; + Assert.Equal($"## Mix{nl}{nl}#### v1.0{nl}{nl}Normal text{nl}{nl}[https://example.com](https://example.com){nl}{nl}More text", result); + } + + [Fact] + public void ToMarkdownString_WithDependencies_IncludesRequiresSection() + { + var addon = CreateAddon( + type: AddonTypeEnum.TC, + dependentAddons: new Dictionary { { "dep-one", null }, { "dep-two", "1.5" } }); + + var result = addon.ToMarkdownString(); + + Assert.Contains("#### Requires:", result); + Assert.Contains("dep-one", result); + Assert.Contains("dep-two", result); + } + + [Fact] + public void ToMarkdownString_WithDependencies_OfficialType_OmitsRequiresSection() + { + var addon = CreateAddon( + type: AddonTypeEnum.Official, + dependentAddons: new Dictionary { { "some-dep", null } }); + + var result = addon.ToMarkdownString(); + + Assert.DoesNotContain("#### Requires:", result); + } + + [Fact] + public void ToMarkdownString_WithIncompatibleAddons_IncludesIncompatibleSection() + { + var addon = CreateAddon( + incompatibleAddons: new Dictionary { { "bad-mod", null }, { "old-mod", "<=1.0" } }); + + var result = addon.ToMarkdownString(); + + Assert.Contains("#### Incompatible with:", result); + Assert.Contains("bad-mod", result); + Assert.Contains("old-mod", result); + } + + [Fact] + public void ToMarkdownString_AllFields_ReturnsCompleteMarkdown() + { + var addon = CreateAddon( + id: "full-addon", version: "3.0", title: "Full Addon", + author: "Creator", releaseDate: new(2023, 12, 1), + description: "First line\nhttps://example.com\nLast line", + type: AddonTypeEnum.TC, + dependentAddons: new Dictionary { { "required-dep", null } }, + incompatibleAddons: new Dictionary { { "conflict-mod", null } }); + + var result = addon.ToMarkdownString(); + + var nl = Environment.NewLine; + Assert.StartsWith("## Full Addon", result); + Assert.Contains($"{nl}{nl}#### v3.0", result); + Assert.Contains($"{nl}{nl}*Released on:* 01.12.2023", result); + Assert.Contains($"{nl}{nl}*by Creator*", result); + Assert.Contains($"{nl}{nl}First line{nl}{nl}[https://example.com](https://example.com){nl}{nl}Last line", result); + Assert.Contains($"{nl}{nl}#### Requires:{nl}required-dep", result); + Assert.Contains($"{nl}{nl}#### Incompatible with:{nl}conflict-mod", result); + } +} diff --git a/src/Tests.Unit/CmdArguments/BloodCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/BloodCmdArgumentsTests.cs deleted file mode 100644 index 4d128b5b..00000000 --- a/src/Tests.Unit/CmdArguments/BloodCmdArgumentsTests.cs +++ /dev/null @@ -1,701 +0,0 @@ -using Addons.Addons; -using Core.All; -using Core.All.Enums; -using Core.All.Enums.Addons; -using Games.Games; -using Ports.Ports; -using Ports.Ports.EDuke32; - -namespace Tests.Unit.CmdArguments; - -[Collection("Sync")] -public sealed class BloodCmdArgumentsTests -{ - private readonly BloodGame _bloodGame; - private readonly BloodCampaign _bloodCamp; - private readonly BloodCampaign _bloodCampWithOptions; - private readonly BloodCampaign _bloodCpCamp; - private readonly BloodCampaign _bloodTc; - private readonly BloodCampaign _bloodTcFolder; - private readonly BloodCampaign _bloodTcExeOverride; - private readonly BloodCampaign _bloodTcIncompatibleWithEnabledMod; - private readonly BloodCampaign _bloodTcIncompatibleWithEverything; - - private readonly AutoloadModsProvider _modsProvider; - - public BloodCmdArgumentsTests() - { - _modsProvider = new(GameEnum.Blood); - - _bloodGame = new() - { - GameInstallFolder = Path.Combine("D:", "Games", "Blood"), - }; - - _bloodCamp = new() - { - AddonId = new(nameof(GameEnum.Blood).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Blood", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Blood), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainDef = null, - AdditionalDefs = null, - StartMap = null, - PreviewImageHash = null, - INI = null, - RFF = null, - SND = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - - _bloodCampWithOptions = new() - { - AddonId = new(nameof(GameEnum.Blood).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Blood", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Blood), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainDef = null, - AdditionalDefs = null, - StartMap = null, - PreviewImageHash = null, - INI = null, - RFF = null, - SND = null, - IsUnpacked = false, - Executables = null, - Options = new() { - { "option 1", new() { { "OPT.DEF", OptionalParameterTypeEnum.DEF } } }, - { "option 2", new() { { "OPT2.DEF", OptionalParameterTypeEnum.DEF }, { "OPT2_2.DEF", OptionalParameterTypeEnum.DEF } } }, - } - }; - - _bloodCpCamp = new() - { - AddonId = new(nameof(BloodAddonEnum.BloodCP).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Cryptic Passage", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Blood), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = new Dictionary() { { nameof(BloodAddonEnum.BloodCP), null } }, - IncompatibleAddons = null, - MainDef = null, - AdditionalDefs = null, - StartMap = null, - PreviewImageHash = null, - INI = null, - RFF = null, - SND = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - - _bloodTc = new() - { - AddonId = new("blood-tc", null), - Type = AddonTypeEnum.TC, - Title = "Blood TC", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Blood), - RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Games", "Blood", "blood_tc.zip"), - DependentAddons = null, - IncompatibleAddons = null, - MainDef = null, - AdditionalDefs = null, - StartMap = null, - PreviewImageHash = null, - INI = "TC.INI", - RFF = "TC.RFF", - SND = "TC.SND", - IsUnpacked = false, - Executables = null, - Options = null - }; - - _bloodTcFolder = new() - { - AddonId = new("blood-tc-folder", null), - Type = AddonTypeEnum.TC, - Title = "Blood TC", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Blood), - RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Games", "Blood", "blood_tc_folder", "addon.json"), - DependentAddons = null, - IncompatibleAddons = null, - MainDef = null, - AdditionalDefs = null, - StartMap = null, - PreviewImageHash = null, - INI = "TC.INI", - RFF = "TC.RFF", - SND = "TC.SND", - IsUnpacked = true, - Executables = null, - Options = null - }; - - _bloodTcExeOverride = new() - { - AddonId = new("blood-tc-exe-override", null), - Type = AddonTypeEnum.TC, - Title = "Blood TC", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Blood), - RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Games", "Blood", "blood_tc_folder", "addon.json"), - DependentAddons = null, - IncompatibleAddons = null, - MainDef = null, - AdditionalDefs = null, - StartMap = null, - PreviewImageHash = null, - INI = "TC.INI", - RFF = "TC.RFF", - SND = "TC.SND", - IsUnpacked = true, - Executables = new Dictionary>() { { OSEnum.Windows, new Dictionary() { { PortEnum.NBlood, "nblood.exe" } } } }, - Options = null - }; - - _bloodTcIncompatibleWithEnabledMod = new() - { - AddonId = new("blood-tc-imcompatible-with-enabled", null), - Type = AddonTypeEnum.TC, - Title = "Blood TC", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Blood), - RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Games", "Blood", "blood_tc_folder", "addon.json"), - DependentAddons = null, - IncompatibleAddons = new Dictionary() { { "enabledMod", null } }, - MainDef = null, - AdditionalDefs = null, - StartMap = null, - PreviewImageHash = null, - INI = "TC.INI", - RFF = "TC.RFF", - SND = "TC.SND", - IsUnpacked = true, - Executables = null, - Options = null - }; - - _bloodTcIncompatibleWithEverything = new() - { - AddonId = new("blood-tc-imcompatible-with-everything", null), - Type = AddonTypeEnum.TC, - Title = "Blood TC", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Blood), - RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Games", "Blood", "blood_tc_folder", "addon.json"), - DependentAddons = null, - IncompatibleAddons = new Dictionary() { { "*", null } }, - MainDef = null, - AdditionalDefs = null, - StartMap = null, - PreviewImageHash = null, - INI = "TC.INI", - RFF = "TC.RFF", - SND = "TC.SND", - IsUnpacked = true, - Executables = null, - Options = null - }; - - } - - [Fact] - public void RazeTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature, - _modsProvider.MultipleDependenciesMod - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_bloodGame, _bloodCamp); - var args = raze.GetStartGameArgs(_bloodGame, _bloodCamp, mods, [], true, true); - var expected = @$" -file ""enabled_mod.zip"" -adddef ""ENABLED1.DEF"" -adddef ""ENABLED2.DEF"" -file ""mod_incompatible_with_addon.zip"" -file ""incompatible_mod_with_compatible_version.zip"" -file ""dependent_mod.zip"" -file ""dependent_mod_with_compatible_version.zip"" -savedir ""{Directory.GetCurrentDirectory()}\Data\Saves\Raze\Blood\blood"" -def ""a"" -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Blood - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Blood/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void RazeCpTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_bloodGame, _bloodCamp); - var args = raze.GetStartGameArgs(_bloodGame, _bloodCpCamp, mods, [], true, true); - var expected = @$" -file ""enabled_mod.zip"" -adddef ""ENABLED1.DEF"" -adddef ""ENABLED2.DEF"" -file ""mod_requires_addon.zip"" -file ""incompatible_mod_with_compatible_version.zip"" -file ""dependent_mod.zip"" -file ""dependent_mod_with_compatible_version.zip"" -savedir ""{Directory.GetCurrentDirectory()}\Data\Saves\Raze\Blood\bloodcp"" -def ""a"" -ini ""CRYPTIC.INI"" -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Blood - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Blood/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void RazeTCTest() - { - Raze raze = new(); - - raze.BeforeStart(_bloodGame, _bloodTc); - var args = raze.GetStartGameArgs(_bloodGame, _bloodTc, new Dictionary(), [], true, true); - var expected = @$" -savedir ""{Directory.GetCurrentDirectory()}\Data\Saves\Raze\Blood\blood-tc"" -def ""a"" -ini ""TC.INI"" -file ""D:\Games\Blood\blood_tc.zip"" -file ""TC.RFF"" -file ""TC.SND"" -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Blood - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Blood/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void RazeTCFolderTest() - { - Raze raze = new(); - - raze.BeforeStart(_bloodGame, _bloodTcFolder); - var args = raze.GetStartGameArgs(_bloodGame, _bloodTcFolder, new Dictionary(), [], true, true); - var expected = @$" -savedir ""{Directory.GetCurrentDirectory()}\Data\Saves\Raze\Blood\blood-tc-folder"" -def ""a"" -ini ""TC.INI"" -file ""D:\Games\Blood\blood_tc_folder"" -file ""TC.RFF"" -file ""TC.SND"" -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Blood - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Blood/Mods - Path=D:/Games/Blood/blood_tc_folder - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void NBloodTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature, - _modsProvider.MultipleDependenciesMod - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - NBlood nblood = new(); - - var args = nblood.GetStartGameArgs(_bloodGame, _bloodCamp, mods, [], true, true, 2); - var expected = @$" -g ""enabled_mod.zip"" -mh ""ENABLED1.DEF"" -mh ""ENABLED2.DEF"" -g ""mod_incompatible_with_addon.zip"" -g ""incompatible_mod_with_compatible_version.zip"" -g ""dependent_mod.zip"" -g ""dependent_mod_with_compatible_version.zip"" -g ""feature_mod.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void NBloodCPTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - NBlood nblood = new(); - - var args = nblood.GetStartGameArgs(_bloodGame, _bloodCpCamp, mods, [], true, true, 2); - var expected = @$" -g ""enabled_mod.zip"" -mh ""ENABLED1.DEF"" -mh ""ENABLED2.DEF"" -g ""mod_requires_addon.zip"" -g ""incompatible_mod_with_compatible_version.zip"" -g ""dependent_mod.zip"" -g ""dependent_mod_with_compatible_version.zip"" -g ""feature_mod.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""CRYPTIC.INI"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void NBloodTCTest() - { - NBlood nblood = new(); - - var args = nblood.GetStartGameArgs(_bloodGame, _bloodTc, new Dictionary(), [], true, true, 2); - var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -g ""D:\Games\Blood\blood_tc.zip"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void NBloodTCFolderTest() - { - NBlood nblood = new(); - - var args = nblood.GetStartGameArgs(_bloodGame, _bloodTcFolder, new Dictionary(), [], true, true, 2); - var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -game_dir ""D:\Games\Blood\blood_tc_folder"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void NBloodTcExeOverride() - { - NBlood nblood = new(); - - var args = nblood.GetStartGameArgs(_bloodGame, _bloodTcExeOverride, new Dictionary(), [], true, true, 2); - var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void NotBloodTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - NotBlood notblood = new(); - - var args = notblood.GetStartGameArgs(_bloodGame, _bloodCamp, mods, [], true, true, 2); - var expected = @$" -g ""enabled_mod.zip"" -mh ""ENABLED1.DEF"" -mh ""ENABLED2.DEF"" -g ""mod_incompatible_with_addon.zip"" -g ""incompatible_mod_with_compatible_version.zip"" -g ""dependent_mod.zip"" -g ""dependent_mod_with_compatible_version.zip"" -g ""feature_mod.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void NotBloodCPTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - NotBlood notblood = new(); - - var args = notblood.GetStartGameArgs(_bloodGame, _bloodCpCamp, mods, [], true, true, 2); - var expected = @$" -g ""enabled_mod.zip"" -mh ""ENABLED1.DEF"" -mh ""ENABLED2.DEF"" -g ""mod_requires_addon.zip"" -g ""incompatible_mod_with_compatible_version.zip"" -g ""dependent_mod.zip"" -g ""dependent_mod_with_compatible_version.zip"" -g ""feature_mod.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""CRYPTIC.INI"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void NotBloodTCTest() - { - NotBlood notblood = new(); - - var args = notblood.GetStartGameArgs(_bloodGame, _bloodTc, new Dictionary(), [], true, true, 2); - var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -g ""D:\Games\Blood\blood_tc.zip"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void NotBloodTCFolderTest() - { - NotBlood notblood = new(); - - var args = notblood.GetStartGameArgs(_bloodGame, _bloodTcFolder, new Dictionary(), [], true, true, 2); - var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -game_dir ""D:\Games\Blood\blood_tc_folder"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void NotBloodTcExeOverride() - { - NotBlood notblood = new(); - - var args = notblood.GetStartGameArgs(_bloodGame, _bloodTcExeOverride, new Dictionary(), [], true, true, 2); - var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void NBloodIncompatibleWithEnabledModTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.ModThatIncompatibleWithAddon - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - NotBlood notblood = new(); - - var args = notblood.GetStartGameArgs(_bloodGame, _bloodTcIncompatibleWithEnabledMod, mods, [], true, true, 2); - var expected = @$" -g ""mod_incompatible_with_addon.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -game_dir ""D:\Games\Blood\blood_tc_folder"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void NBloodIncompatibleWithEverythingTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - NotBlood notblood = new(); - - var args = notblood.GetStartGameArgs(_bloodGame, _bloodTcIncompatibleWithEverything, mods, [], true, true, 2); - var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -game_dir ""D:\Games\Blood\blood_tc_folder"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void BloodWithOptionsTest() - { - NBlood nblood = new(); - - var args = nblood.GetStartGameArgs(_bloodGame, _bloodCampWithOptions, new Dictionary(), ["option 2"], true, true, 2); - var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -mh ""OPT2.DEF"" -mh ""OPT2_2.DEF"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } -} diff --git a/src/Tests.Unit/CmdArguments/BloodLooseMapCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/BloodLooseMapCmdArgumentsTests.cs deleted file mode 100644 index 64de79a1..00000000 --- a/src/Tests.Unit/CmdArguments/BloodLooseMapCmdArgumentsTests.cs +++ /dev/null @@ -1,160 +0,0 @@ -using Addons.Addons; -using Core.All.Enums; -using Core.All.Serializable.Addon; -using Games.Games; -using Ports.Ports; -using Ports.Ports.EDuke32; - -namespace Tests.Unit.CmdArguments; - -[Collection("Sync")] -public sealed class BloodLooseMapCmdArgumentsTests -{ - private readonly BloodGame _bloodGame; - private readonly LooseMap _looseMap; - - private readonly AutoloadModsProvider _modsProvider; - - public BloodLooseMapCmdArgumentsTests() - { - _modsProvider = new(GameEnum.Blood); - - _bloodGame = new() - { - GameInstallFolder = Path.Combine("D:", "Games", "Blood"), - }; - - _looseMap = new() - { - AddonId = new("loose-map", null), - Type = AddonTypeEnum.Map, - Title = "Loose map", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Blood), - RequiredFeatures = null, - PathToFile = Path.Combine("Maps", "LOOSE.MAP"), - DependentAddons = null, - IncompatibleAddons = null, - MainDef = null, - AdditionalDefs = null, - StartMap = new MapFileJsonModel() { File = "LOOSE.MAP" }, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - BloodIni = null, - Options = null - }; - } - - [Fact] - public void RazeTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_bloodGame, _looseMap); - var args = raze.GetStartGameArgs(_bloodGame, _looseMap, mods, [], true, true); - var expected = @$" -file ""enabled_mod.zip"" -adddef ""ENABLED1.DEF"" -adddef ""ENABLED2.DEF"" -file ""mod_incompatible_with_addon.zip"" -file ""incompatible_mod_with_compatible_version.zip"" -file ""dependent_mod.zip"" -file ""dependent_mod_with_compatible_version.zip"" -savedir ""{Directory.GetCurrentDirectory()}\Data\Saves\Raze\Blood\loose-map"" -def ""a"" -ini ""BLOOD.INI"" -file ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Maps"" -map ""LOOSE.MAP"" -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Blood - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Blood/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void NBloodTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - NBlood nblood = new(); - - var args = nblood.GetStartGameArgs(_bloodGame, _looseMap, mods, [], true, true, 2); - var expected = @$" -g ""enabled_mod.zip"" -mh ""ENABLED1.DEF"" -mh ""ENABLED2.DEF"" -g ""mod_incompatible_with_addon.zip"" -g ""incompatible_mod_with_compatible_version.zip"" -g ""dependent_mod.zip"" -g ""dependent_mod_with_compatible_version.zip"" -g ""feature_mod.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""BLOOD.INI"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Maps"" -map ""LOOSE.MAP"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void NotBloodTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - NotBlood notblood = new(); - - var args = notblood.GetStartGameArgs(_bloodGame, _looseMap, mods, [], true, true, 2); - var expected = @$" -g ""enabled_mod.zip"" -mh ""ENABLED1.DEF"" -mh ""ENABLED2.DEF"" -g ""mod_incompatible_with_addon.zip"" -g ""incompatible_mod_with_compatible_version.zip"" -g ""dependent_mod.zip"" -g ""dependent_mod_with_compatible_version.zip"" -g ""feature_mod.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""BLOOD.INI"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Maps"" -map ""LOOSE.MAP"" -s 2 -quick -nosetup"; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } -} diff --git a/src/Tests.Unit/CmdArguments/BuildGDXCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/BuildGDXCmdArgumentsTests.cs new file mode 100644 index 00000000..cb0e07b0 --- /dev/null +++ b/src/Tests.Unit/CmdArguments/BuildGDXCmdArgumentsTests.cs @@ -0,0 +1,138 @@ +using Addons.Addons; +using Games.Games; +using Ports.Ports; +using Tests.Unit.Helpers; + +namespace Tests.Unit.CmdArguments; + +public sealed class BuildGDXCmdArgumentsTests +{ + private readonly DukeGame _dukeGame; + private readonly DukeCampaign _dukeCamp; + private readonly BloodGame _bloodGame; + private readonly BloodCampaign _bloodCamp; + private readonly WangGame _wangGame; + private readonly GenericCampaign _wangCamp; + private readonly SlaveGame _slaveGame; + private readonly GenericCampaign _slaveCamp; + private readonly RedneckGame _redneckGame; + private readonly DukeCampaign _redneckCamp; + private readonly DukeCampaign _ridesAgainCamp; + private readonly NamGame _namGame; + private readonly DukeCampaign _namCamp; + private readonly WitchavenGame _witchavenGame; + private readonly GenericCampaign _witchavenCamp; + private readonly TekWarGame _tekWarGame; + private readonly GenericCampaign _tekWarCamp; + + public BuildGDXCmdArgumentsTests() + { + (_dukeGame, _dukeCamp, _, _, _, _, _, _, _, _, _) = PortTestSetups.Duke3D(); + (_bloodGame, _bloodCamp, _, _, _, _, _, _, _, _, _) = PortTestSetups.Blood(); + (_redneckGame, _redneckCamp, _ridesAgainCamp, _, _) = PortTestSetups.Redneck(); + (_wangGame, _wangCamp, _, _, _) = PortTestSetups.Wang(); + (_slaveGame, _slaveCamp, _) = PortTestSetups.Slave(); + (_namGame, _namCamp, _) = PortTestSetups.Nam(); + (_witchavenGame, _witchavenCamp) = PortTestSetups.Witchaven(); + (_tekWarGame, _tekWarCamp) = PortTestSetups.TekWar(); + } + + [Fact] + public void DukeTest() + { + BuildGDX buildGdx = new(); + var args = buildGdx.GetStartGameArgs(_dukeGame, _dukeCamp, [], [], true, true); + var expected = $" -jar ..\\..\\BuildGDX.jar -path \"{_dukeGame.GameInstallFolder}\" -game DUKE_NUKEM_3D"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void BloodTest() + { + BuildGDX buildGdx = new(); + var args = buildGdx.GetStartGameArgs(_bloodGame, _bloodCamp, [], [], true, true); + var expected = $" -jar ..\\..\\BuildGDX.jar -path \"{_bloodGame.GameInstallFolder}\" -game BLOOD"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void WangTest() + { + BuildGDX buildGdx = new(); + var args = buildGdx.GetStartGameArgs(_wangGame, _wangCamp, [], [], true, true); + var expected = $" -jar ..\\..\\BuildGDX.jar -path \"{_wangGame.GameInstallFolder}\" -game SHADOW_WARRIOR"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void SlaveTest() + { + BuildGDX buildGdx = new(); + var args = buildGdx.GetStartGameArgs(_slaveGame, _slaveCamp, [], [], true, true); + var expected = $" -jar ..\\..\\BuildGDX.jar -path \"{_slaveGame.GameInstallFolder}\" -game POWERSLAVE"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void RedneckTest() + { + BuildGDX buildGdx = new(); + var args = buildGdx.GetStartGameArgs(_redneckGame, _redneckCamp, [], [], true, true); + var expected = $" -jar ..\\..\\BuildGDX.jar -path \"{_redneckGame.GameInstallFolder}\" -game REDNECK_RAMPAGE"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void RidesAgainTest() + { + BuildGDX buildGdx = new(); + var args = buildGdx.GetStartGameArgs(_redneckGame, _ridesAgainCamp, [], [], true, true); + var expected = $" -jar ..\\..\\BuildGDX.jar -path \"{_redneckGame.AgainInstallPath}\" -game RR_RIDES_AGAIN"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void NamTest() + { + BuildGDX buildGdx = new(); + var args = buildGdx.GetStartGameArgs(_namGame, _namCamp, [], [], true, true); + var expected = $" -jar ..\\..\\BuildGDX.jar -path \"{_namGame.GameInstallFolder}\" -game NAM"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void WitchavenTest() + { + BuildGDX buildGdx = new(); + var args = buildGdx.GetStartGameArgs(_witchavenGame, _witchavenCamp, [], [], true, true); + var expected = $" -jar ..\\..\\BuildGDX.jar -path \"{_witchavenGame.GameInstallFolder}\" -game WITCHAVEN"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void TekWarTest() + { + BuildGDX buildGdx = new(); + var args = buildGdx.GetStartGameArgs(_tekWarGame, _tekWarCamp, [], [], true, true); + var expected = $" -jar ..\\..\\BuildGDX.jar -path \"{_tekWarGame.GameInstallFolder}\" -game TEKWAR"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } +} diff --git a/src/Tests.Unit/CmdArguments/DosBoxCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/DosBoxCmdArgumentsTests.cs new file mode 100644 index 00000000..fcd06b71 --- /dev/null +++ b/src/Tests.Unit/CmdArguments/DosBoxCmdArgumentsTests.cs @@ -0,0 +1,346 @@ +using Addons.Addons; +using Core.All.Enums; +using Core.All.Enums.Addons; +using Core.Client.Helpers; +using Games.Games; +using Ports.Ports; +using Tests.Unit.Helpers; + +namespace Tests.Unit.CmdArguments; + +public sealed class DosBoxCmdArgumentsTests +{ + private readonly DukeGame _dukeGame; + private readonly DukeCampaign _dukeCamp; + private readonly DukeCampaign _dukeVaca; + private readonly DukeCampaign _dukeDc; + private readonly DukeCampaign _dukeNw; + private readonly LooseMap _dukeLooseMap; + + private readonly BloodGame _bloodGame; + private readonly BloodCampaign _bloodCamp; + private readonly BloodCampaign _bloodCpCamp; + private readonly LooseMap _bloodLooseMap; + + private readonly RedneckGame _redneckGame; + private readonly DukeCampaign _redneckCamp; + private readonly DukeCampaign _ridesAgainCamp; + private readonly DukeCampaign _route66Camp; + + private readonly WangGame _wangGame; + private readonly GenericCampaign _wangCamp; + + public DosBoxCmdArgumentsTests() + { + (_dukeGame, _dukeCamp, _dukeVaca, _, _, _, _, _dukeDc, _dukeNw, _dukeLooseMap, _) = PortTestSetups.Duke3D(); + (_bloodGame, _bloodCamp, _, _bloodCpCamp, _, _, _, _, _, _bloodLooseMap, _) = PortTestSetups.Blood(); + (_redneckGame, _redneckCamp, _ridesAgainCamp, _route66Camp, _) = PortTestSetups.Redneck(); + (_wangGame, _wangCamp, _, _, _) = PortTestSetups.Wang(); + } + + [Fact] + public void DukeBaseGameTest() + { + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_dukeGame, _dukeCamp, [], [], true, true); + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_dukeGame.GameInstallFolder}\"\" -c \"c:\"" + + $" -c DUKE3D.EXE" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void DukeVacaTest() + { + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_dukeGame, _dukeVaca, [], [], true, true); + var vacaPath = _dukeGame.AddonsPaths[DukeAddonEnum.DukeVaca]; + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_dukeGame.GameInstallFolder}\"\" -c \"c:\"" + + $" -c \"mount d \\\"{vacaPath}\"\"" + + $" -c \"VACATION.EXE /gd:\\\\VACATION.GRP /xd:\\\\VACATION.CON\"" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void DukeDcTest() + { + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_dukeGame, _dukeDc, [], [], true, true); + var dcPath = _dukeGame.AddonsPaths[DukeAddonEnum.DukeDC]; + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_dukeGame.GameInstallFolder}\"\" -c \"c:\"" + + $" -c \"mount d \\\"{dcPath}\"\"" + + $" -c \"DUKE3D.EXE /gd:\\\\DUKEDC.GRP /xd:\\\\DUKEDC.CON\"" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void DukeNwTest() + { + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_dukeGame, _dukeNw, [], [], true, true); + var nwPath = _dukeGame.AddonsPaths[DukeAddonEnum.DukeNW]; + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_dukeGame.GameInstallFolder}\"\" -c \"c:\"" + + $" -c \"mount d \\\"{nwPath}\"\"" + + $" -c \"DUKE3D.EXE /gd:\\\\NWINTER.GRP /xd:\\\\NWINTER.CON\"" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void DukeLooseMapTest() + { + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_dukeGame, _dukeLooseMap, [], [], true, true); + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_dukeGame.GameInstallFolder}\"\" -c \"c:\"" + + $" -c \"mount d \\\"{_dukeGame.MapsFolderPath}\"\"" + + $" -c \"DUKE3D.EXE -map d:\\\\LOOSE.MAP\"" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void BloodBaseGameTest() + { + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_bloodGame, _bloodCamp, [], [], true, true); + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_bloodGame.GameInstallFolder}\"\" -c \"c:\"" + + $" -c BLOOD.EXE" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void BloodCPTest() + { + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_bloodGame, _bloodCpCamp, [], [], true, true); + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_bloodGame.GameInstallFolder}\"\" -c \"c:\"" + + $" -c CRYPTIC.EXE" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void BloodTCFolderTest() + { + var addonDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var gameDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + Directory.CreateDirectory(addonDir); + Directory.CreateDirectory(gameDir); + + var addonFile1 = Path.Combine(addonDir, "BLOOD.EXE"); + var addonFile2 = Path.Combine(addonDir, "BLOOD.RFF"); + + var originalFile1 = Path.Combine(gameDir, "BLOOD.EXE"); + var originalFile2 = Path.Combine(addonDir, "BLOOD.RFF"); + + File.WriteAllText(addonFile1, ""); + File.WriteAllText(addonFile2, ""); + File.WriteAllText(originalFile1, ""); + + try + { + var bloodGame = new BloodGame { GameInstallFolder = gameDir }; + var bloodTcFolder = new BloodCampaign + { + AddonId = new("blood-tc-folder", "1.0"), + Type = AddonTypeEnum.TC, + Title = "Blood TC Folder", + SupportedGame = new(GameEnum.Blood, null, null), + FileInfo = new(addonDir, "addon.json"), + GridImageHash = null, + PreviewImageHash = null, + Description = null, + Author = null, + ReleaseDate = null, + MainDef = null, + AdditionalDefs = null, + INI = "BLOODTC.INI", + RFF = "BLOODTC.RFF", + SND = "BLOODTC.SND", + StartMap = null, + DependentAddons = null, + IncompatibleAddons = null, + RequiredFeatures = null, + Executables = null, + Options = null, + }; + + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(bloodGame, bloodTcFolder, [], [], true, true); + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{ClientProperties.TempFolderPath}\"\" -c \"c:\"" + + $" -c \"BLOOD.EXE -ini BLOODTC.INI -RFF BLOODTC.RFF -snd BLOODTC.SND\"" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + + Assert.True(File.Exists(addonFile1)); + Assert.True(File.Exists(addonFile2)); + Assert.True(File.Exists(originalFile1)); + Assert.True(File.Exists(originalFile2)); + } + finally + { + if (Directory.Exists(addonDir)) + { + Directory.Delete(addonDir, true); + } + + if (Directory.Exists(gameDir)) + { + Directory.Delete(gameDir, true); + } + } + } + + [Fact] + public void BloodLooseMapTest() + { + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_bloodGame, _bloodLooseMap, [], [], true, true); + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_bloodGame.GameInstallFolder}\"\" -c \"c:\"" + + $" -c \"mount d \\\"{_bloodGame.MapsFolderPath}\"\"" + + $" -c \"BLOOD.EXE -map d:\\\\LOOSE.MAP\"" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void RedneckBaseTest() + { + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_redneckGame, _redneckCamp, [], [], true, true); + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_redneckGame.GameInstallFolder}\"\" -c \"c:\"" + + $" -c RR.EXE" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void RidesAgainTest() + { + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_redneckGame, _ridesAgainCamp, [], [], true, true); + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_redneckGame.AgainInstallPath}\"\" -c \"c:\"" + + $" -c RA.EXE" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void Route66Test() + { + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_redneckGame, _route66Camp, [], [], true, true); + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_redneckGame.GameInstallFolder}\"\" -c \"c:\"" + + $" -c ROUTE66.EXE" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void WangBaseTest() + { + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_wangGame, _wangCamp, [], [], true, true); + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_wangGame.GameInstallFolder}\"\" -c \"c:\"" + + $" -c Sw.EXE" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } + + [Fact] + public void BloodTC_NullFileInfo_FallsThroughToBaseGame() + { + var bloodTcNull = new BloodCampaign + { + AddonId = new("blood-tc", "1.0"), + Type = AddonTypeEnum.TC, + Title = "Blood TC", + SupportedGame = new(GameEnum.Blood, null, null), + FileInfo = null, + GridImageHash = null, + PreviewImageHash = null, + Description = null, + Author = null, + ReleaseDate = null, + MainDef = null, + AdditionalDefs = null, + INI = "TC.INI", + RFF = "TC.RFF", + SND = "TC.SND", + StartMap = null, + DependentAddons = null, + IncompatibleAddons = null, + RequiredFeatures = null, + Executables = null, + Options = null, + }; + + DosBox dosBox = new(); + var args = dosBox.GetStartGameArgs(_bloodGame, bloodTcNull, [], [], true, true); + // Should fall through to base Blood game args instead of NRE + var expected = $"" + + $" --noconsole -c \"cycles max\" -c \"core dynamic\"" + + $" -c \"mount c \\\"{_bloodGame.GameInstallFolder}\"\" -c \"c:\"" + + $" -c BLOOD.EXE" + + $" -c \"exit\""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + Assert.Equal(expected, args); + } +} diff --git a/src/Tests.Unit/CmdArguments/DukeCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/DukeCmdArgumentsTests.cs deleted file mode 100644 index fd2cd2e4..00000000 --- a/src/Tests.Unit/CmdArguments/DukeCmdArgumentsTests.cs +++ /dev/null @@ -1,680 +0,0 @@ -using Addons.Addons; -using Core.All; -using Core.All.Enums; -using Core.All.Enums.Addons; -using Core.All.Enums.Versions; -using Core.Client.Interfaces; -using Games.Games; -using Moq; -using Ports.Ports; -using Ports.Ports.EDuke32; - -namespace Tests.Unit.CmdArguments; - -[Collection("Sync")] -public sealed class DukeCmdArgumentsTests -{ - private readonly DukeGame _dukeGame; - private readonly DukeCampaign _dukeCamp; - private readonly DukeCampaign _dukeVaca; - private readonly DukeCampaign _dukeTcForVaca; - private readonly DukeCampaign _dukeWtCamp; - private readonly DukeCampaign _duke64Camp; - private readonly DukeCampaign _dukeZhCamp; - - private readonly AutoloadModsProvider _modsProvider; - - public DukeCmdArgumentsTests() - { - _modsProvider = new(GameEnum.Duke3D); - - _dukeGame = new() - { - Duke64RomPath = Path.Combine("D:", "Games", "Duke64", "rom.z64"), - DukeZHRomPath = Path.Combine("D:", "Games", "DukeZH", "rom.z64"), - DukeWTInstallPath = Path.Combine("D:", "Games", "DukeWT"), - GameInstallFolder = Path.Combine("D:", "Games", "Duke3D"), - AddonsPaths = new() { { DukeAddonEnum.DukeVaca, Path.Combine("D:", "Games", "Duke3D", "Vaca") } } - }; - - _dukeCamp = new() - { - AddonId = new(nameof(GameEnum.Duke3D).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Duke Nukem 3D", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainCon = null, - AdditionalCons = null, - MainDef = null, - AdditionalDefs = null, - RTS = null, - StartMap = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - - _dukeWtCamp = new() - { - AddonId = new(nameof(DukeVersionEnum.Duke3D_WT).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Duke Nukem 3D World Tour", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_WT), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainCon = null, - AdditionalCons = null, - MainDef = null, - AdditionalDefs = null, - RTS = null, - StartMap = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - - _duke64Camp = new() - { - AddonId = new(nameof(GameEnum.Duke64).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Duke Nukem 64", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Duke64), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainCon = null, - AdditionalCons = null, - MainDef = null, - AdditionalDefs = null, - RTS = null, - StartMap = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - - _dukeZhCamp = new() - { - AddonId = new(nameof(GameEnum.DukeZeroHour).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Duke Nukem ZeroHour", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.DukeZeroHour), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainCon = null, - AdditionalCons = null, - MainDef = null, - AdditionalDefs = null, - RTS = null, - StartMap = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - - _dukeVaca = new() - { - AddonId = new("dukevaca", null), - Type = AddonTypeEnum.Official, - Title = "Duke Nukem 3D Caribbean", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = new Dictionary() { { nameof(DukeAddonEnum.DukeVaca), null } }, - IncompatibleAddons = null, - MainCon = null, - AdditionalCons = null, - MainDef = null, - AdditionalDefs = null, - RTS = null, - StartMap = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - - _dukeTcForVaca = new() - { - AddonId = new("duke-tc", "1.1"), - Type = AddonTypeEnum.TC, - Title = "Duke Nukem 3D TC", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), - RequiredFeatures = null, - PathToFile = Path.Combine(Directory.GetCurrentDirectory(), "Data", "Duke3D", "Campaigns", "duke_tc.zip"), - DependentAddons = new Dictionary() { { nameof(DukeAddonEnum.DukeVaca), null } }, - IncompatibleAddons = null, - MainCon = "TC.CON", - RTS = "TC.RTS", - AdditionalCons = ["TC1.CON", "TC2.CON"], - MainDef = "TC.DEF", - AdditionalDefs = ["TC1.DEF", "TC2.DEF"], - StartMap = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - - } - - [Fact] - public void RazeTest() - { - var mods = new List() { - _modsProvider.EnabledModWithCons, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature, - _modsProvider.MultipleDependenciesMod - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_dukeGame, _dukeCamp); - var args = raze.GetStartGameArgs(_dukeGame, _dukeCamp, mods, [], true, true); - var expected = $"" + - $" -file \"enabled_mod.zip\"" + - $" -adddef \"ENABLED1.DEF\"" + - $" -adddef \"ENABLED2.DEF\"" + - $" -addcon \"ENABLED1.CON\"" + - $" -addcon \"ENABLED2.CON\"" + - $" -file \"mod_incompatible_with_addon.zip\"" + - $" -file \"incompatible_mod_with_compatible_version.zip\"" + - $" -file \"dependent_mod.zip\"" + - $" -file \"dependent_mod_with_compatible_version.zip\"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Duke3D\\duke3d\"" + - $" -def \"a\"" + - $" -addon 0" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Duke3D - Path=D:/Games/Duke3D/Vaca - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Duke3D/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void RazeWtTest() - { - Raze raze = new(); - - var args = raze.GetStartGameArgs(_dukeGame, _dukeWtCamp, new Dictionary(), [], true, true); - var expected = $"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Duke3D\\duke3d_wt\"" + - $" -def \"a\"" + - $" -addon 0" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/DukeWT - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Duke3D/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void RazeVacaTest() - { - var mods = new List() { - _modsProvider.EnabledModWithCons, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_dukeGame, _dukeVaca); - var args = raze.GetStartGameArgs(_dukeGame, _dukeVaca, mods, [], true, true); - var expected = $"" + - $" -file \"enabled_mod.zip\"" + - $" -adddef \"ENABLED1.DEF\"" + - $" -adddef \"ENABLED2.DEF\"" + - $" -addcon \"ENABLED1.CON\"" + - $" -addcon \"ENABLED2.CON\"" + - $" -file \"mod_requires_addon.zip\"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Duke3D\\dukevaca\"" + - $" -def \"a\"" + - $" -addon 3" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Duke3D - Path=D:/Games/Duke3D/Vaca - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Duke3D/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void RazeTCTest() - { - Raze raze = new(); - - var args = raze.GetStartGameArgs(_dukeGame, _dukeTcForVaca, new Dictionary(), [], true, true); - var expected = $"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Duke3D\\duke-tc\"" + - $" -def \"TC.DEF\"" + - $" -adddef \"TC1.DEF\"" + - $" -adddef \"TC2.DEF\"" + - $" -addon 3" + - $" -con \"TC.CON\"" + - $" -addcon \"TC1.CON\"" + - $" -addcon \"TC2.CON\"" + - $" -file \"{Path.Combine(Directory.GetCurrentDirectory(), "Data", "Duke3D", "Campaigns", "duke_tc.zip")}\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void EDuke32Test() - { - var mods = new List() { - _modsProvider.EnabledModWithCons, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature, - _modsProvider.MultipleDependenciesMod - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - EDuke32 eduke32 = new(); - - var args = eduke32.GetStartGameArgs(_dukeGame, _dukeCamp, mods, [], true, true, 3); - var expected = "" + - $" -g \"enabled_mod.zip\"" + - $" -mh \"ENABLED1.DEF\"" + - $" -mh \"ENABLED2.DEF\"" + - $" -mx \"ENABLED1.CON\"" + - $" -mx \"ENABLED2.CON\"" + - $" -g \"mod_incompatible_with_addon.zip\"" + - $" -g \"incompatible_mod_with_compatible_version.zip\"" + - $" -g \"dependent_mod.zip\"" + - $" -g \"dependent_mod_with_compatible_version.zip\"" + - $" -g \"feature_mod.zip\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Mods\"" + - $" -usecwd" + - $" -cachesize 262144" + - $" -h \"a\"" + - $" -j \"D:\\Games\\Duke3D\"" + - $" -s3" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void EDuke32WtTest() - { - EDuke32 eduke32 = new(); - - var args = eduke32.GetStartGameArgs(_dukeGame, _dukeWtCamp, new Dictionary(), [], true, true); - var expected = $"" + - $" -usecwd" + - $" -cachesize 262144" + - $" -h \"a\"" + - $" -j \"D:\\Games\\DukeWT\"" + - $" -addon 0" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Ports\\EDuke32\\WTStopgap\"" + - $" -gamegrp e32wt.grp" + - $" -mh e32wt.def" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void EDuke32VacaTest() - { - var mods = new List() { - _modsProvider.EnabledModWithCons, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - EDuke32 eduke32 = new(); - - var args = eduke32.GetStartGameArgs(_dukeGame, _dukeVaca, mods, [], true, true); - var expected = $"" + - $" -g \"enabled_mod.zip\"" + - $" -mh \"ENABLED1.DEF\"" + - $" -mh \"ENABLED2.DEF\"" + - $" -mx \"ENABLED1.CON\"" + - $" -mx \"ENABLED2.CON\"" + - $" -g \"mod_requires_addon.zip\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Mods\"" + - $" -usecwd" + - $" -cachesize 262144" + - $" -h \"a\"" + - $" -j \"D:\\Games\\Duke3D\"" + - $" -j \"D:\\Games\\Duke3D\\Vaca\"" + - $" -grp VACATION.GRP" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void EDuke32TCTest() - { - EDuke32 eduke32 = new(); - - var args = eduke32.GetStartGameArgs(_dukeGame, _dukeTcForVaca, new Dictionary(), [], true, true); - var expected = $"" + - $" -usecwd" + - $" -cachesize 262144" + - $" -h \"TC.DEF\"" + - $" -mh \"TC1.DEF\"" + - $" -mh \"TC2.DEF\"" + - $" -j \"D:\\Games\\Duke3D\"" + - $" -j \"D:\\Games\\Duke3D\\Vaca\"" + - $" -grp VACATION.GRP" + - $" -x \"TC.CON\"" + - $" -mx \"TC1.CON\"" + - $" -mx \"TC2.CON\"" + - $" -g \"{Path.Combine(Directory.GetCurrentDirectory(), "Data", "Duke3D", "Campaigns", "duke_tc.zip")}\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void RedNukem64Test() - { - RedNukem redNukem = new(); - - var args = redNukem.GetStartGameArgs(_dukeGame, _duke64Camp, new Dictionary(), [], true, true); - var expected = "" + - " -usecwd" + - " -d blank.edm" + - " -h \"a\"" + - " -j \"D:\\Games\\Duke64\"" + - " -gamegrp \"rom.z64\"" + - " -quick" + - " -nosetup" + - ""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void RedNukemTest() - { - var mods = new List() { - _modsProvider.EnabledModWithCons, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature, - _modsProvider.MultipleDependenciesMod - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - RedNukem redNukem = new(); - - var args = redNukem.GetStartGameArgs(_dukeGame, _dukeCamp, mods, [], true, true); - var expected = $"" + - $" -g \"enabled_mod.zip\"" + - $" -mh \"ENABLED1.DEF\"" + - $" -mh \"ENABLED2.DEF\"" + - $" -mx \"ENABLED1.CON\"" + - $" -mx \"ENABLED2.CON\"" + - $" -g \"mod_incompatible_with_addon.zip\"" + - $" -g \"incompatible_mod_with_compatible_version.zip\"" + - $" -g \"dependent_mod.zip\"" + - $" -g \"dependent_mod_with_compatible_version.zip\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Mods\"" + - $" -usecwd" + - " -d blank.edm" + - $" -h \"a\"" + - $" -j \"D:\\Games\\Duke3D\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void RedNukemVacaTest() - { - var mods = new List() { - _modsProvider.EnabledModWithCons, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - RedNukem eduke32 = new(); - - var args = eduke32.GetStartGameArgs(_dukeGame, _dukeVaca, mods, [], true, true); - var expected = $"" + - $" -g \"enabled_mod.zip\"" + - $" -mh \"ENABLED1.DEF\"" + - $" -mh \"ENABLED2.DEF\"" + - $" -mx \"ENABLED1.CON\"" + - $" -mx \"ENABLED2.CON\"" + - $" -g \"mod_requires_addon.zip\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Mods\"" + - $" -usecwd" + - " -d blank.edm" + - $" -h \"a\"" + - $" -j \"D:\\Games\\Duke3D\"" + - $" -j \"D:\\Games\\Duke3D\\Vaca\"" + - $" -g VACATION.GRP" + - $" -quick" + - $" -nosetup" - ; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void RedNukemTCTest() - { - RedNukem eduke32 = new(); - - var args = eduke32.GetStartGameArgs(_dukeGame, _dukeTcForVaca, new Dictionary(), [], true, true); - var expected = $"" + - $" -usecwd" + - " -d blank.edm" + - $" -h \"TC.DEF\"" + - $" -mh \"TC1.DEF\"" + - $" -mh \"TC2.DEF\"" + - $" -j \"D:\\Games\\Duke3D\"" + - $" -j \"D:\\Games\\Duke3D\\Vaca\"" + - $" -g VACATION.GRP" + - $" -x \"TC.CON\"" + - $" -mx \"TC1.CON\"" + - $" -mx \"TC2.CON\"" + - $" -g \"{Path.Combine(Directory.GetCurrentDirectory(), "Data", "Duke3D", "Campaigns", "duke_tc.zip")}\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void ZeroHourTest() - { - Mock _config = new(); - ZHRecomp redNukem = new(_config.Object); - - var args = redNukem.GetStartGameArgs(_dukeGame, _dukeZhCamp, new Dictionary(), [], true, true); - - Assert.Equal(string.Empty, args); - } -} diff --git a/src/Tests.Unit/CmdArguments/DukeLooseMapCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/DukeLooseMapCmdArgumentsTests.cs deleted file mode 100644 index 9df89072..00000000 --- a/src/Tests.Unit/CmdArguments/DukeLooseMapCmdArgumentsTests.cs +++ /dev/null @@ -1,220 +0,0 @@ -using Addons.Addons; -using Core.All.Enums; -using Core.All.Enums.Versions; -using Core.All.Serializable.Addon; -using Games.Games; -using Ports.Ports; -using Ports.Ports.EDuke32; - -namespace Tests.Unit.CmdArguments; - -[Collection("Sync")] -public sealed class DukeLooseMapCmdArgumentsTests -{ - private readonly DukeGame _dukeGame; - private readonly LooseMap _dukeLooseMap; - - private readonly AutoloadModsProvider _modsProvider; - - public DukeLooseMapCmdArgumentsTests() - { - _modsProvider = new(GameEnum.Duke3D); - - _dukeGame = new() - { - GameInstallFolder = Path.Combine("D:", "Games", "Duke3D"), - Duke64RomPath = null, - DukeZHRomPath = null, - DukeWTInstallPath = null, - }; - - _dukeLooseMap = new() - { - AddonId = new("loose-map", null), - Type = AddonTypeEnum.Map, - Title = "Loose map", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), - RequiredFeatures = null, - PathToFile = Path.Combine("Maps", "LOOSE.MAP"), - DependentAddons = null, - IncompatibleAddons = null, - MainDef = null, - AdditionalDefs = null, - StartMap = new MapFileJsonModel() { File = "LOOSE.MAP" }, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - BloodIni = null, - Options = null - }; - } - - [Fact] - public void RazeTest() - { - var mods = new List() { - _modsProvider.EnabledModWithCons, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_dukeGame, _dukeLooseMap); - var args = raze.GetStartGameArgs(_dukeGame, _dukeLooseMap, mods, [], true, true); - var expected = $"" + - $" -file \"enabled_mod.zip\"" + - $" -adddef \"ENABLED1.DEF\"" + - $" -adddef \"ENABLED2.DEF\"" + - $" -addcon \"ENABLED1.CON\"" + - $" -addcon \"ENABLED2.CON\"" + - $" -file \"mod_incompatible_with_addon.zip\"" + - $" -file \"incompatible_mod_with_compatible_version.zip\"" + - $" -file \"dependent_mod.zip\"" + - $" -file \"dependent_mod_with_compatible_version.zip\"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Duke3D\\loose-map\"" + - $" -def \"a\"" + - $" -file \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Maps\"" + - $" -map \"LOOSE.MAP\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Duke3D - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Duke3D/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void EDuke32Test() - { - var mods = new List() { - _modsProvider.EnabledModWithCons, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - EDuke32 eduke32 = new(); - - var args = eduke32.GetStartGameArgs(_dukeGame, _dukeLooseMap, mods, [], true, true, 3); - var expected = $"" + - $" -g \"enabled_mod.zip\"" + - $" -mh \"ENABLED1.DEF\"" + - $" -mh \"ENABLED2.DEF\"" + - $" -mx \"ENABLED1.CON\"" + - $" -mx \"ENABLED2.CON\"" + - $" -g \"mod_incompatible_with_addon.zip\"" + - $" -g \"incompatible_mod_with_compatible_version.zip\"" + - $" -g \"dependent_mod.zip\"" + - $" -g \"dependent_mod_with_compatible_version.zip\"" + - $" -g \"feature_mod.zip\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Mods\"" + - $" -usecwd" + - $" -cachesize 262144" + - $" -h \"a\"" + - $" -j \"D:\\Games\\Duke3D\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Maps\"" + - $" -map \"LOOSE.MAP\"" + - $" -s3" + - $" -quick" + - $" -nosetup" - ; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void RedNukemTest() - { - var mods = new List() { - _modsProvider.EnabledModWithCons, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - RedNukem redNukem = new(); - - var args = redNukem.GetStartGameArgs(_dukeGame, _dukeLooseMap, mods, [], true, true); - var expected = $"" + - $" -g \"enabled_mod.zip\"" + - $" -mh \"ENABLED1.DEF\"" + - $" -mh \"ENABLED2.DEF\"" + - $" -mx \"ENABLED1.CON\"" + - $" -mx \"ENABLED2.CON\"" + - $" -g \"mod_incompatible_with_addon.zip\"" + - $" -g \"incompatible_mod_with_compatible_version.zip\"" + - $" -g \"dependent_mod.zip\"" + - $" -g \"dependent_mod_with_compatible_version.zip\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Mods\"" + - $" -usecwd" + - " -d blank.edm" + - $" -h \"a\"" + - $" -j \"D:\\Games\\Duke3D\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Maps\"" + - $" -map \"LOOSE.MAP\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } -} diff --git a/src/Tests.Unit/CmdArguments/EDuke32CmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/EDuke32CmdArgumentsTests.cs new file mode 100644 index 00000000..a7480898 --- /dev/null +++ b/src/Tests.Unit/CmdArguments/EDuke32CmdArgumentsTests.cs @@ -0,0 +1,352 @@ +using Addons.Addons; +using Core.All.Enums; +using Games.Games; +using Ports.Ports.EDuke32; +using Tests.Unit.Helpers; + +namespace Tests.Unit.CmdArguments; + +public sealed class EDuke32CmdArgumentsTests +{ + private readonly DukeGame _dukeGame; + private readonly DukeCampaign _dukeCamp; + private readonly DukeCampaign _dukeVaca; + private readonly DukeCampaign _dukeTcForVaca; + private readonly DukeCampaign _dukeWtCamp; + private readonly LooseMap _dukeLooseMap; + private readonly AutoloadModsTestSetups _dukeMods; + + private readonly NamGame _namGame; + private readonly DukeCampaign _namCamp; + private readonly AutoloadModsTestSetups _namMods; + + private readonly WW2GIGame _ww2Game; + private readonly DukeCampaign _ww2Camp; + private readonly DukeCampaign _ww2PlatoonCamp; + private readonly AutoloadModsTestSetups _ww2Mods; + + public EDuke32CmdArgumentsTests() + { + (_dukeGame, _dukeCamp, _dukeVaca, _dukeTcForVaca, _dukeWtCamp, _, _, _, _, _dukeLooseMap, _dukeMods) = PortTestSetups.Duke3D(); + (_namGame, _namCamp, _namMods) = PortTestSetups.Nam(); + (_ww2Game, _ww2Camp, _ww2PlatoonCamp, _ww2Mods) = PortTestSetups.WW2GI(); + } + + [Fact] + public void DukeTest() + { + var mods = _dukeMods.StandardModsWithCons; + + EDuke32 eduke32 = new(); + + var args = eduke32.GetStartGameArgs(_dukeGame, _dukeCamp, mods, [], true, true, 3); + var expected = "" + + " -g \"enabled_mod.zip\"" + + " -mh \"ENABLED1.DEF\"" + + " -mh \"ENABLED2.DEF\"" + + " -mx \"ENABLED1.CON\"" + + " -mx \"ENABLED2.CON\"" + + " -g \"mod_incompatible_with_addon.zip\"" + + " -g \"incompatible_mod_with_compatible_version.zip\"" + + " -g \"dependent_mod.zip\"" + + " -g \"dependent_mod_with_compatible_version.zip\"" + + " -g \"feature_mod.zip\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Mods\"" + + " -usecwd" + + " -cachesize 262144" + + " -h \"a\"" + + " -j \"D:\\Games\\Duke3D\"" + + " -s3" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void DukeWtTest() + { + EDuke32 eduke32 = new(); + + var args = eduke32.GetStartGameArgs(_dukeGame, _dukeWtCamp, [], [], true, true); + var expected = $"" + + $" -usecwd" + + $" -cachesize 262144" + + $" -h \"a\"" + + $" -j \"D:\\Games\\DukeWT\"" + + $" -addon 0" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Ports\\EDuke32\\WTStopgap\"" + + $" -gamegrp e32wt.grp" + + $" -mh e32wt.def" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void DukeVacaTest() + { + var mods = _dukeMods.AddonModsWithCons; + + EDuke32 eduke32 = new(); + + var args = eduke32.GetStartGameArgs(_dukeGame, _dukeVaca, mods, [], true, true); + var expected = $"" + + $" -g \"enabled_mod.zip\"" + + $" -mh \"ENABLED1.DEF\"" + + $" -mh \"ENABLED2.DEF\"" + + $" -mx \"ENABLED1.CON\"" + + $" -mx \"ENABLED2.CON\"" + + $" -g \"mod_requires_addon.zip\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Mods\"" + + $" -usecwd" + + $" -cachesize 262144" + + $" -h \"a\"" + + $" -j \"D:\\Games\\Duke3D\"" + + $" -j \"D:\\Games\\Duke3D\\Vaca\"" + + $" -grp VACATION.GRP" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void DukeTCTest() + { + EDuke32 eduke32 = new(); + + var args = eduke32.GetStartGameArgs(_dukeGame, _dukeTcForVaca, [], [], true, true); + var expected = $"" + + $" -usecwd" + + $" -cachesize 262144" + + $" -h \"TC.DEF\"" + + $" -mh \"TC1.DEF\"" + + $" -mh \"TC2.DEF\"" + + $" -j \"D:\\Games\\Duke3D\"" + + $" -j \"D:\\Games\\Duke3D\\Vaca\"" + + $" -grp VACATION.GRP" + + $" -x \"TC.CON\"" + + $" -mx \"TC1.CON\"" + + $" -mx \"TC2.CON\"" + + $" -g \"{Path.Combine(Directory.GetCurrentDirectory(), "Data", "Duke3D", "Campaigns", "duke_tc.zip")}\"" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void DukePackedAddonTest() + { + EDuke32 eduke32 = new(); + + var packedCamp = PortTestSetups.PackedDukeAddonCampaign(); + var zipFilePath = packedCamp.FileInfo!.PathToFile; + + var args = eduke32.GetStartGameArgs(_dukeGame, packedCamp, [], [], true, true); + var expected = "" + + " -usecwd" + + " -cachesize 262144" + + " -h \"a\"" + + $" -j \"{_dukeGame.GameInstallFolder}\"" + + $" -g \"{zipFilePath}\"" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void DukeLooseMapTest() + { + var mods = _dukeMods.StandardModsWithCons; + + EDuke32 eduke32 = new(); + + var args = eduke32.GetStartGameArgs(_dukeGame, _dukeLooseMap, mods, [], true, true, 3); + var expected = $"" + + $" -g \"enabled_mod.zip\"" + + $" -mh \"ENABLED1.DEF\"" + + $" -mh \"ENABLED2.DEF\"" + + $" -mx \"ENABLED1.CON\"" + + $" -mx \"ENABLED2.CON\"" + + $" -g \"mod_incompatible_with_addon.zip\"" + + $" -g \"incompatible_mod_with_compatible_version.zip\"" + + $" -g \"dependent_mod.zip\"" + + $" -g \"dependent_mod_with_compatible_version.zip\"" + + $" -g \"feature_mod.zip\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Mods\"" + + $" -usecwd" + + $" -cachesize 262144" + + $" -h \"a\"" + + $" -j \"D:\\Games\\Duke3D\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Maps\"" + + $" -map \"LOOSE.MAP\"" + + $" -s3" + + $" -quick" + + $" -nosetup" + ; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void NamTest() + { + var mods = _namMods.MinimalMods; + + EDuke32 eduke32 = new(); + + var args = eduke32.GetStartGameArgs(_namGame, _namCamp, mods, [], true, true); + var expected = $"" + + $" -g \"enabled_mod.zip\"" + + $" -mh \"ENABLED1.DEF\"" + + $" -mh \"ENABLED2.DEF\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\NAM\\Mods\"" + + $" -usecwd" + + " -cachesize 262144" + + " -h \"a\"" + + " -j \"D:\\Games\\NAM\"" + + " -nam" + + " -gamegrp NAM.GRP" + + " -x GAME.CON" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void WW2GITest() + { + var mods = _ww2Mods.MinimalMods; + + EDuke32 eDuke = new(); + + var args = eDuke.GetStartGameArgs(_ww2Game, _ww2Camp, mods, [], true, true); + var expected = "" + + " -g \"enabled_mod.zip\"" + + " -mh \"ENABLED1.DEF\"" + + " -mh \"ENABLED2.DEF\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\WW2GI\\Mods\"" + + " -usecwd" + + " -cachesize 262144" + + " -h \"a\"" + + " -j \"D:\\Games\\WW2GI\"" + + " -ww2gi" + + " -gamegrp WW2GI.GRP" + + " -x GAME.CON" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void WW2GIPlatoonTest() + { + var mods = _ww2Mods.MinimalMods; + + EDuke32 eDuke = new(); + + var args = eDuke.GetStartGameArgs(_ww2Game, _ww2PlatoonCamp, mods, [], true, true); + var expected = "" + + " -g \"enabled_mod.zip\"" + + " -mh \"ENABLED1.DEF\"" + + " -mh \"ENABLED2.DEF\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\WW2GI\\Mods\"" + + " -usecwd" + + " -cachesize 262144" + + " -h \"a\"" + + " -j \"D:\\Games\\WW2GI\"" + + " -ww2gi -gamegrp WW2GI.GRP" + + " -grp PLATOONL.DAT" + + " -x PLATOONL.DEF" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BeforeStart_NullFileInfo_DoesNotThrow() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var game = new DukeGame + { + Duke64RomPath = null, + DukeZHRomPath = null, + DukeWTInstallPath = null, + GameInstallFolder = tempDir, + AddonsPaths = [], + }; + + var camp = new DukeCampaign + { + AddonId = new("test-camp", null), + Type = AddonTypeEnum.Official, + Title = "Test", + SupportedGame = new(GameEnum.Duke3D, null, null), + FileInfo = null, + GridImageHash = null, + PreviewImageHash = null, + Description = null, + Author = null, + ReleaseDate = null, + MainDef = null, + AdditionalDefs = null, + MainCon = null, + AdditionalCons = null, + RTS = null, + StartMap = null, + DependentAddons = null, + IncompatibleAddons = null, + RequiredFeatures = null, + Executables = null, + Options = null, + }; + + EDuke32 eduke32 = new(); + eduke32.BeforeStart(game, camp); + } + finally + { + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, true); + } + } + } +} diff --git a/src/Tests.Unit/CmdArguments/FuryCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/FuryCmdArgumentsTests.cs index dbf6725b..05416639 100644 --- a/src/Tests.Unit/CmdArguments/FuryCmdArgumentsTests.cs +++ b/src/Tests.Unit/CmdArguments/FuryCmdArgumentsTests.cs @@ -1,76 +1,30 @@ using Addons.Addons; -using Core.All.Enums; using Core.Client.Config; using Games.Games; using Ports.Ports.EDuke32; +using Tests.Unit.Helpers; namespace Tests.Unit.CmdArguments; -[Collection("Sync")] public sealed class FuryCmdArgumentsTests { - private readonly FuryGame _dukeGame; - private readonly DukeCampaign _dukeCamp; - - private readonly AutoloadModsProvider _modsProvider; + private readonly FuryGame _game; + private readonly DukeCampaign _camp; + private readonly AutoloadModsTestSetups _mods; public FuryCmdArgumentsTests() { - _modsProvider = new(GameEnum.Fury); - - _dukeGame = new() - { - GameInstallFolder = Path.Combine("D:", "Games", "Fury") - }; - - _dukeCamp = new() - { - AddonId = new(nameof(GameEnum.Fury).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Ion Fury", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Fury), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainCon = null, - AdditionalCons = null, - MainDef = null, - RTS = null, - AdditionalDefs = null, - StartMap = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null - }; + (_game, _camp, _mods) = PortTestSetups.Fury(); } [Fact] public void FuryTest() { - var mods = new List() { - _modsProvider.EnabledModWithCons, - _modsProvider.DisabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - _modsProvider.IncompatibleMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame, - _modsProvider.ModThatRequiresFeature - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); + var mods = _mods.StandardModsWithCons; Fury fury = new(new ConfigProviderFake()); - var args = fury.GetStartGameArgs(_dukeGame, _dukeCamp, mods, [], true, true, 3); + var args = fury.GetStartGameArgs(_game, _camp, mods, [], true, true, 3); var expected = $"" + $" -g \"enabled_mod.zip\"" + $" -mh \"ENABLED1.DEF\"" + @@ -88,11 +42,7 @@ public void FuryTest() $" -nosetup" + $""; - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); Assert.Equal(expected, args); } diff --git a/src/Tests.Unit/CmdArguments/NBloodCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/NBloodCmdArgumentsTests.cs new file mode 100644 index 00000000..327cc522 --- /dev/null +++ b/src/Tests.Unit/CmdArguments/NBloodCmdArgumentsTests.cs @@ -0,0 +1,121 @@ +using Addons.Addons; +using Games.Games; +using Ports.Ports.EDuke32; +using Tests.Unit.Helpers; + +namespace Tests.Unit.CmdArguments; + +public sealed class NBloodCmdArgumentsTests +{ + private readonly BloodGame _bloodGame; + private readonly BloodCampaign _bloodCamp; + private readonly BloodCampaign _bloodCampWithOptions; + private readonly BloodCampaign _bloodCpCamp; + private readonly BloodCampaign _bloodTc; + private readonly BloodCampaign _bloodTcFolder; + private readonly BloodCampaign _bloodTcExeOverride; + private readonly LooseMap _bloodLooseMap; + private readonly AutoloadModsTestSetups _bloodMods; + + public NBloodCmdArgumentsTests() + { + (_bloodGame, _bloodCamp, _bloodCampWithOptions, _bloodCpCamp, _bloodTc, _bloodTcFolder, _bloodTcExeOverride, _, _, _bloodLooseMap, _bloodMods) = PortTestSetups.Blood(); + } + + [Fact] + public void BloodTest() + { + var mods = _bloodMods.StandardMods; + + NBlood nblood = new(); + + var args = nblood.GetStartGameArgs(_bloodGame, _bloodCamp, mods, [], true, true, 2); + var expected = @$" -g ""enabled_mod.zip"" -mh ""ENABLED1.DEF"" -mh ""ENABLED2.DEF"" -g ""mod_incompatible_with_addon.zip"" -g ""incompatible_mod_with_compatible_version.zip"" -g ""dependent_mod.zip"" -g ""dependent_mod_with_compatible_version.zip"" -g ""feature_mod.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodCPTest() + { + var mods = _bloodMods.StandardMods; + + NBlood nblood = new(); + + var args = nblood.GetStartGameArgs(_bloodGame, _bloodCpCamp, mods, [], true, true, 2); + var expected = @$" -g ""enabled_mod.zip"" -mh ""ENABLED1.DEF"" -mh ""ENABLED2.DEF"" -g ""mod_requires_addon.zip"" -g ""incompatible_mod_with_compatible_version.zip"" -g ""dependent_mod.zip"" -g ""dependent_mod_with_compatible_version.zip"" -g ""feature_mod.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""CRYPTIC.INI"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodTCTest() + { + NBlood nblood = new(); + + var args = nblood.GetStartGameArgs(_bloodGame, _bloodTc, [], [], true, true, 2); + var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -g ""D:\Games\Blood\blood_tc.zip"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodTCFolderTest() + { + NBlood nblood = new(); + + var args = nblood.GetStartGameArgs(_bloodGame, _bloodTcFolder, [], [], true, true, 2); + var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -game_dir ""D:\Games\Blood\blood_tc_folder"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodTcExeOverride() + { + NBlood nblood = new(); + + var args = nblood.GetStartGameArgs(_bloodGame, _bloodTcExeOverride, [], [], true, true, 2); + var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodWithOptionsTest() + { + NBlood nblood = new(); + + var args = nblood.GetStartGameArgs(_bloodGame, _bloodCampWithOptions, [], ["option 2"], true, true, 2); + var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -mh ""OPT2.DEF"" -mh ""OPT2_2.DEF"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodLooseMapTest() + { + var mods = _bloodMods.StandardMods; + + NBlood nblood = new(); + + var args = nblood.GetStartGameArgs(_bloodGame, _bloodLooseMap, mods, [], true, true, 2); + var expected = @$" -g ""enabled_mod.zip"" -mh ""ENABLED1.DEF"" -mh ""ENABLED2.DEF"" -g ""mod_incompatible_with_addon.zip"" -g ""incompatible_mod_with_compatible_version.zip"" -g ""dependent_mod.zip"" -g ""dependent_mod_with_compatible_version.zip"" -g ""feature_mod.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""BLOOD.INI"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Maps"" -map ""LOOSE.MAP"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } +} diff --git a/src/Tests.Unit/CmdArguments/NamCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/NamCmdArgumentsTests.cs deleted file mode 100644 index 195c031b..00000000 --- a/src/Tests.Unit/CmdArguments/NamCmdArgumentsTests.cs +++ /dev/null @@ -1,170 +0,0 @@ -using Addons.Addons; -using Core.All.Enums; -using Games.Games; -using Ports.Ports; -using Ports.Ports.EDuke32; - -namespace Tests.Unit.CmdArguments; - -[Collection("Sync")] -public sealed class NamCmdArgumentsTests -{ - private readonly NamGame _namGame; - private readonly DukeCampaign _namCamp; - - private readonly AutoloadModsProvider _modsProvider; - - public NamCmdArgumentsTests() - { - _modsProvider = new(GameEnum.NAM); - - _namGame = new() - { - GameInstallFolder = Path.Combine("D:", "Games", "NAM") - }; - - _namCamp = new() - { - AddonId = new(nameof(GameEnum.NAM).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "NAM", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.NAM), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainCon = null, - AdditionalCons = null, - MainDef = null, - AdditionalDefs = null, - RTS = null, - StartMap = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - } - - [Fact] - public void RazeTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_namGame, _namCamp); - var args = raze.GetStartGameArgs(_namGame, _namCamp, mods, [], true, true); - var expected = $"" + - $" -file \"enabled_mod.zip\"" + - $" -adddef \"ENABLED1.DEF\"" + - $" -adddef \"ENABLED2.DEF\"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\NAM\\nam\"" + - $" -def \"a\"" + - $" -nam" + - $" -file NAM.GRP" + - $" -con GAME.CON" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/NAM - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/NAM/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void EDuke32Test() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - EDuke32 eduke32 = new(); - - var args = eduke32.GetStartGameArgs(_namGame, _namCamp, mods, [], true, true); - var expected = $"" + - $" -g \"enabled_mod.zip\"" + - $" -mh \"ENABLED1.DEF\"" + - $" -mh \"ENABLED2.DEF\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\NAM\\Mods\"" + - $" -usecwd" + - " -cachesize 262144" + - $" -h \"a\"" + - $" -j \"D:\\Games\\NAM\"" + - $" -nam" + - $" -gamegrp NAM.GRP" + - $" -x GAME.CON" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void RedNukemTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - RedNukem redNukem = new(); - - var args = redNukem.GetStartGameArgs(_namGame, _namCamp, mods, [], true, true); - var expected = $"" + - $" -g \"enabled_mod.zip\"" + - $" -mh \"ENABLED1.DEF\"" + - $" -mh \"ENABLED2.DEF\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\NAM\\Mods\"" + - $" -usecwd" + - " -d blank.edm" + - $" -h \"a\"" + - $" -j \"D:\\Games\\NAM\"" + - $" -nam" + - $" -gamegrp NAM.GRP" + - $" -x GAME.CON" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } -} diff --git a/src/Tests.Unit/CmdArguments/NotBloodCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/NotBloodCmdArgumentsTests.cs new file mode 100644 index 00000000..6abace07 --- /dev/null +++ b/src/Tests.Unit/CmdArguments/NotBloodCmdArgumentsTests.cs @@ -0,0 +1,139 @@ +using Addons.Addons; +using Games.Games; +using Ports.Ports.EDuke32; +using Tests.Unit.Helpers; + +namespace Tests.Unit.CmdArguments; + +public sealed class NotBloodCmdArgumentsTests +{ + private readonly BloodGame _bloodGame; + private readonly BloodCampaign _bloodCamp; + private readonly BloodCampaign _bloodCpCamp; + private readonly BloodCampaign _bloodTc; + private readonly BloodCampaign _bloodTcFolder; + private readonly BloodCampaign _bloodTcExeOverride; + private readonly BloodCampaign _bloodTcIncompatibleWithEnabledMod; + private readonly BloodCampaign _bloodTcIncompatibleWithEverything; + private readonly LooseMap _bloodLooseMap; + private readonly AutoloadModsTestSetups _bloodMods; + + public NotBloodCmdArgumentsTests() + { + (_bloodGame, _bloodCamp, _, _bloodCpCamp, _bloodTc, _bloodTcFolder, _bloodTcExeOverride, _bloodTcIncompatibleWithEnabledMod, _bloodTcIncompatibleWithEverything, _bloodLooseMap, _bloodMods) = PortTestSetups.Blood(); + } + + [Fact] + public void BloodTest() + { + var mods = _bloodMods.StandardMods; + + NotBlood notblood = new(); + + var args = notblood.GetStartGameArgs(_bloodGame, _bloodCamp, mods, [], true, true, 2); + var expected = @$" -g ""enabled_mod.zip"" -mh ""ENABLED1.DEF"" -mh ""ENABLED2.DEF"" -g ""mod_incompatible_with_addon.zip"" -g ""incompatible_mod_with_compatible_version.zip"" -g ""dependent_mod.zip"" -g ""dependent_mod_with_compatible_version.zip"" -g ""feature_mod.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodCPTest() + { + var mods = _bloodMods.StandardMods; + + NotBlood notblood = new(); + + var args = notblood.GetStartGameArgs(_bloodGame, _bloodCpCamp, mods, [], true, true, 2); + var expected = @$" -g ""enabled_mod.zip"" -mh ""ENABLED1.DEF"" -mh ""ENABLED2.DEF"" -g ""mod_requires_addon.zip"" -g ""incompatible_mod_with_compatible_version.zip"" -g ""dependent_mod.zip"" -g ""dependent_mod_with_compatible_version.zip"" -g ""feature_mod.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""CRYPTIC.INI"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodTCTest() + { + NotBlood notblood = new(); + + var args = notblood.GetStartGameArgs(_bloodGame, _bloodTc, [], [], true, true, 2); + var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -g ""D:\Games\Blood\blood_tc.zip"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodTCFolderTest() + { + NotBlood notblood = new(); + + var args = notblood.GetStartGameArgs(_bloodGame, _bloodTcFolder, [], [], true, true, 2); + var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -game_dir ""D:\Games\Blood\blood_tc_folder"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodTcExeOverride() + { + NotBlood notblood = new(); + + var args = notblood.GetStartGameArgs(_bloodGame, _bloodTcExeOverride, [], [], true, true, 2); + var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodIncompatibleWithEnabledModTest() + { + var mods = _bloodMods.Enabled; + + NotBlood notblood = new(); + + var args = notblood.GetStartGameArgs(_bloodGame, _bloodTcIncompatibleWithEnabledMod, mods, [], true, true, 2); + var expected = @$" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -game_dir ""D:\Games\Blood\blood_tc_folder"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodIncompatibleWithEverythingTest() + { + var mods = _bloodMods.StandardMods; + + NotBlood notblood = new(); + + var args = notblood.GetStartGameArgs(_bloodGame, _bloodTcIncompatibleWithEverything, mods, [], true, true, 2); + var expected = @" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""TC.INI"" -game_dir ""D:\Games\Blood\blood_tc_folder"" -rff ""TC.RFF"" -snd ""TC.SND"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void BloodLooseMapTest() + { + var mods = _bloodMods.StandardMods; + + NotBlood notblood = new(); + + var args = notblood.GetStartGameArgs(_bloodGame, _bloodLooseMap, mods, [], true, true, 2); + var expected = @$" -g ""enabled_mod.zip"" -mh ""ENABLED1.DEF"" -mh ""ENABLED2.DEF"" -g ""mod_incompatible_with_addon.zip"" -g ""incompatible_mod_with_compatible_version.zip"" -g ""dependent_mod.zip"" -g ""dependent_mod_with_compatible_version.zip"" -g ""feature_mod.zip"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Mods"" -usecwd -j ""D:\Games\Blood"" -h ""a"" -ini ""BLOOD.INI"" -j ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Maps"" -map ""LOOSE.MAP"" -s 2 -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } +} diff --git a/src/Tests.Unit/CmdArguments/PCExhumedCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/PCExhumedCmdArgumentsTests.cs new file mode 100644 index 00000000..98156102 --- /dev/null +++ b/src/Tests.Unit/CmdArguments/PCExhumedCmdArgumentsTests.cs @@ -0,0 +1,43 @@ +using Addons.Addons; +using Games.Games; +using Ports.Ports.EDuke32; +using Tests.Unit.Helpers; + +namespace Tests.Unit.CmdArguments; + +public sealed class PCExhumedCmdArgumentsTests +{ + private readonly SlaveGame _slaveGame; + private readonly GenericCampaign _slaveCamp; + private readonly AutoloadModsTestSetups _slaveMods; + + public PCExhumedCmdArgumentsTests() + { + (_slaveGame, _slaveCamp, _slaveMods) = PortTestSetups.Slave(); + } + + [Fact] + public void SlaveTest() + { + var mods = _slaveMods.MinimalMods; + + PCExhumed pcExhumed = new(); + + var args = pcExhumed.GetStartGameArgs(_slaveGame, _slaveCamp, mods, [], true, true); + var expected = $"" + + $" -g \"enabled_mod.zip\"" + + $" -mh \"ENABLED1.DEF\"" + + $" -mh \"ENABLED2.DEF\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Slave\\Mods\"" + + $" -usecwd" + + $" -j \"D:\\Games\\Slave\"" + + $" -h \"a\"" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } +} diff --git a/src/Tests.Unit/CmdArguments/RazeCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/RazeCmdArgumentsTests.cs new file mode 100644 index 00000000..ef334b31 --- /dev/null +++ b/src/Tests.Unit/CmdArguments/RazeCmdArgumentsTests.cs @@ -0,0 +1,764 @@ +using Addons.Addons; +using Games.Games; +using Ports.Ports; +using Tests.Unit.Helpers; + +namespace Tests.Unit.CmdArguments; + +public sealed class RazeCmdArgumentsTests +{ + private readonly BloodGame _bloodGame; + private readonly BloodCampaign _bloodCamp; + private readonly BloodCampaign _bloodCpCamp; + private readonly BloodCampaign _bloodTc; + private readonly BloodCampaign _bloodTcFolder; + private readonly LooseMap _bloodLooseMap; + private readonly AutoloadModsTestSetups _bloodMods; + + private readonly DukeGame _dukeGame; + private readonly DukeCampaign _dukeCamp; + private readonly DukeCampaign _dukeVaca; + private readonly DukeCampaign _dukeTcForVaca; + private readonly DukeCampaign _dukeWtCamp; + private readonly LooseMap _dukeLooseMap; + private readonly AutoloadModsTestSetups _dukeMods; + + private readonly NamGame _namGame; + private readonly DukeCampaign _namCamp; + private readonly AutoloadModsTestSetups _namMods; + + private readonly RedneckGame _redneckGame; + private readonly DukeCampaign _redneckCamp; + private readonly DukeCampaign _redneckAgainCamp; + private readonly AutoloadModsTestSetups _redneckMods; + + private readonly SlaveGame _slaveGame; + private readonly GenericCampaign _slaveCamp; + private readonly AutoloadModsTestSetups _slaveMods; + + private readonly WangGame _wangGame; + private readonly GenericCampaign _wangCamp; + private readonly GenericCampaign _wangTdCamp; + private readonly LooseMap _wangLooseMap; + private readonly AutoloadModsTestSetups _wangMods; + + private readonly WW2GIGame _ww2Game; + private readonly DukeCampaign _ww2Camp; + private readonly DukeCampaign _ww2PlatoonCamp; + private readonly AutoloadModsTestSetups _ww2Mods; + + public RazeCmdArgumentsTests() + { + (_bloodGame, _bloodCamp, _, _bloodCpCamp, _bloodTc, _bloodTcFolder, _, _, _, _bloodLooseMap, _bloodMods) = PortTestSetups.Blood(); + (_dukeGame, _dukeCamp, _dukeVaca, _dukeTcForVaca, _dukeWtCamp, _, _, _, _, _dukeLooseMap, _dukeMods) = PortTestSetups.Duke3D(); + (_namGame, _namCamp, _namMods) = PortTestSetups.Nam(); + (_redneckGame, _redneckCamp, _redneckAgainCamp, _, _redneckMods) = PortTestSetups.Redneck(); + (_slaveGame, _slaveCamp, _slaveMods) = PortTestSetups.Slave(); + (_wangGame, _wangCamp, _wangTdCamp, _wangLooseMap, _wangMods) = PortTestSetups.Wang(); + (_ww2Game, _ww2Camp, _ww2PlatoonCamp, _ww2Mods) = PortTestSetups.WW2GI(); + } + + [Fact] + public void BloodTest() + { + var mods = _bloodMods.StandardMods; + + Raze raze = new(); + + raze.BeforeStart(_bloodGame, _bloodCamp); + var args = raze.GetStartGameArgs(_bloodGame, _bloodCamp, mods, [], true, true); + var expected = @$" -file ""enabled_mod.zip"" -adddef ""ENABLED1.DEF"" -adddef ""ENABLED2.DEF"" -file ""mod_incompatible_with_addon.zip"" -file ""incompatible_mod_with_compatible_version.zip"" -file ""dependent_mod.zip"" -file ""dependent_mod_with_compatible_version.zip"" -savedir ""{Directory.GetCurrentDirectory()}\Data\Saves\Raze\Blood\blood"" -def ""a"" -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Blood + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Blood/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void BloodCPTest() + { + var mods = _bloodMods.StandardMods; + + Raze raze = new(); + + raze.BeforeStart(_bloodGame, _bloodCamp); + var args = raze.GetStartGameArgs(_bloodGame, _bloodCpCamp, mods, [], true, true); + var expected = @$" -file ""enabled_mod.zip"" -adddef ""ENABLED1.DEF"" -adddef ""ENABLED2.DEF"" -file ""mod_requires_addon.zip"" -file ""incompatible_mod_with_compatible_version.zip"" -file ""dependent_mod.zip"" -file ""dependent_mod_with_compatible_version.zip"" -savedir ""{Directory.GetCurrentDirectory()}\Data\Saves\Raze\Blood\bloodcp"" -def ""a"" -ini ""CRYPTIC.INI"" -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Blood + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Blood/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void BloodTCTest() + { + Raze raze = new(); + + raze.BeforeStart(_bloodGame, _bloodTc); + var args = raze.GetStartGameArgs(_bloodGame, _bloodTc, [], [], true, true); + var expected = @$" -savedir ""{Directory.GetCurrentDirectory()}\Data\Saves\Raze\Blood\blood-tc"" -def ""a"" -ini ""TC.INI"" -file ""D:\Games\Blood\blood_tc.zip"" -file ""TC.RFF"" -file ""TC.SND"" -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Blood + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Blood/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void BloodTCFolderTest() + { + Raze raze = new(); + + raze.BeforeStart(_bloodGame, _bloodTcFolder); + var args = raze.GetStartGameArgs(_bloodGame, _bloodTcFolder, [], [], true, true); + var expected = @$" -savedir ""{Directory.GetCurrentDirectory()}\Data\Saves\Raze\Blood\blood-tc-folder"" -def ""a"" -ini ""TC.INI"" -file ""D:\Games\Blood\blood_tc_folder"" -file ""TC.RFF"" -file ""TC.SND"" -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Blood + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Blood/Mods + Path=D:/Games/Blood/blood_tc_folder + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void BloodLooseMapTest() + { + var mods = _bloodMods.StandardMods; + + Raze raze = new(); + + raze.BeforeStart(_bloodGame, _bloodLooseMap); + var args = raze.GetStartGameArgs(_bloodGame, _bloodLooseMap, mods, [], true, true); + var expected = @$" -file ""enabled_mod.zip"" -adddef ""ENABLED1.DEF"" -adddef ""ENABLED2.DEF"" -file ""mod_incompatible_with_addon.zip"" -file ""incompatible_mod_with_compatible_version.zip"" -file ""dependent_mod.zip"" -file ""dependent_mod_with_compatible_version.zip"" -savedir ""{Directory.GetCurrentDirectory()}\Data\Saves\Raze\Blood\loose-map"" -def ""a"" -ini ""BLOOD.INI"" -file ""{Directory.GetCurrentDirectory()}\Data\Addons\Blood\Maps"" -map ""LOOSE.MAP"" -quick -nosetup"; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Blood + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Blood/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void DukeTest() + { + var mods = _dukeMods.StandardModsWithCons; + + Raze raze = new(); + + raze.BeforeStart(_dukeGame, _dukeCamp); + var args = raze.GetStartGameArgs(_dukeGame, _dukeCamp, mods, [], true, true); + var expected = $"" + + $" -file \"enabled_mod.zip\"" + + $" -adddef \"ENABLED1.DEF\"" + + $" -adddef \"ENABLED2.DEF\"" + + $" -addcon \"ENABLED1.CON\"" + + $" -addcon \"ENABLED2.CON\"" + + $" -file \"mod_incompatible_with_addon.zip\"" + + $" -file \"incompatible_mod_with_compatible_version.zip\"" + + $" -file \"dependent_mod.zip\"" + + $" -file \"dependent_mod_with_compatible_version.zip\"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Duke3D\\duke3d\"" + + $" -def \"a\"" + + $" -addon 0" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Duke3D + Path=D:/Games/Duke3D/Vaca + Path=D:/Games/Duke3D/DC + Path=D:/Games/Duke3D/NW + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Duke3D/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void DukeWtTest() + { + Raze raze = new(); + + var args = raze.GetStartGameArgs(_dukeGame, _dukeWtCamp, [], [], true, true); + var expected = $"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Duke3D\\duke3d_wt\"" + + $" -def \"a\"" + + $" -addon 0" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/DukeWT + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Duke3D/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void DukeVacaTest() + { + var mods = _dukeMods.AddonModsWithCons; + + Raze raze = new(); + + raze.BeforeStart(_dukeGame, _dukeVaca); + var args = raze.GetStartGameArgs(_dukeGame, _dukeVaca, mods, [], true, true); + var expected = $"" + + $" -file \"enabled_mod.zip\"" + + $" -adddef \"ENABLED1.DEF\"" + + $" -adddef \"ENABLED2.DEF\"" + + $" -addcon \"ENABLED1.CON\"" + + $" -addcon \"ENABLED2.CON\"" + + $" -file \"mod_requires_addon.zip\"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Duke3D\\dukevaca\"" + + $" -def \"a\"" + + $" -addon 3" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Duke3D + Path=D:/Games/Duke3D/Vaca + Path=D:/Games/Duke3D/DC + Path=D:/Games/Duke3D/NW + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Duke3D/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void DukeTCTest() + { + Raze raze = new(); + + var args = raze.GetStartGameArgs(_dukeGame, _dukeTcForVaca, [], [], true, true); + var expected = $"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Duke3D\\duke-tc\"" + + $" -def \"TC.DEF\"" + + $" -adddef \"TC1.DEF\"" + + $" -adddef \"TC2.DEF\"" + + $" -addon 3" + + $" -con \"TC.CON\"" + + $" -addcon \"TC1.CON\"" + + $" -addcon \"TC2.CON\"" + + $" -file \"{Path.Combine(Directory.GetCurrentDirectory(), "Data", "Duke3D", "Campaigns", "duke_tc.zip")}\"" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void DukePackedAddonTest() + { + Raze raze = new(); + + var packedCamp = PortTestSetups.PackedDukeAddonCampaign(); + var zipFilePath = packedCamp.FileInfo!.PathToFile; + + var args = raze.GetStartGameArgs(_dukeGame, packedCamp, [], [], true, true); + var expected = $"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Duke3D\\packed-camp\"" + + $" -def \"a\"" + + $" -addon 0" + + $" -file \"{zipFilePath}\"" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void DukeLooseMapTest() + { + var mods = _dukeMods.StandardModsWithCons; + + var dukeGame = new DukeGame + { + Duke64RomPath = null, + DukeZHRomPath = null, + DukeWTInstallPath = null, + GameInstallFolder = Path.Combine("D:", "Games", "Duke3D"), + AddonsPaths = [], + }; + Raze raze = new(); + + raze.BeforeStart(dukeGame, _dukeLooseMap); + var args = raze.GetStartGameArgs(dukeGame, _dukeLooseMap, mods, [], true, true); + var expected = $"" + + $" -file \"enabled_mod.zip\"" + + $" -adddef \"ENABLED1.DEF\"" + + $" -adddef \"ENABLED2.DEF\"" + + $" -addcon \"ENABLED1.CON\"" + + $" -addcon \"ENABLED2.CON\"" + + $" -file \"mod_incompatible_with_addon.zip\"" + + $" -file \"incompatible_mod_with_compatible_version.zip\"" + + $" -file \"dependent_mod.zip\"" + + $" -file \"dependent_mod_with_compatible_version.zip\"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Duke3D\\loose-map\"" + + $" -def \"a\"" + + $" -file \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Maps\"" + + $" -map \"LOOSE.MAP\"" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Duke3D + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Duke3D/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void NamTest() + { + var mods = _namMods.MinimalMods; + + Raze raze = new(); + + raze.BeforeStart(_namGame, _namCamp); + var args = raze.GetStartGameArgs(_namGame, _namCamp, mods, [], true, true); + var expected = $"" + + $" -file \"enabled_mod.zip\"" + + $" -adddef \"ENABLED1.DEF\"" + + $" -adddef \"ENABLED2.DEF\"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\NAM\\nam\"" + + $" -def \"a\"" + + $" -nam" + + $" -file NAM.GRP" + + $" -con GAME.CON" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/NAM + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/NAM/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void RedneckTest() + { + var mods = _redneckMods.MinimalMods; + + Raze raze = new(); + + raze.BeforeStart(_redneckGame, _redneckCamp); + var args = raze.GetStartGameArgs(_redneckGame, _redneckCamp, mods, [], true, true); + var expected = $"" + + $" -file \"enabled_mod.zip\"" + + $" -adddef \"ENABLED1.DEF\"" + + $" -adddef \"ENABLED2.DEF\"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Redneck\\redneck\"" + + $" -def \"a\"" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Redneck + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Redneck/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void RedneckAgainTest() + { + var mods = _redneckMods.MinimalMods; + + Raze raze = new(); + + raze.BeforeStart(_redneckGame, _redneckAgainCamp); + var args = raze.GetStartGameArgs(_redneckGame, _redneckAgainCamp, mods, [], true, true); + var expected = $"" + + $" -file \"enabled_mod.zip\"" + + $" -adddef \"ENABLED1.DEF\"" + + $" -adddef \"ENABLED2.DEF\"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Redneck\\ridesagain\"" + + $" -def \"a\"" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Again + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Redneck/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void SlaveTest() + { + var mods = _slaveMods.MinimalMods; + + Raze raze = new(); + + raze.BeforeStart(_slaveGame, _slaveCamp); + var args = raze.GetStartGameArgs(_slaveGame, _slaveCamp, mods, [], true, true); + var expected = $"" + + $" -file \"enabled_mod.zip\"" + + $" -adddef \"ENABLED1.DEF\"" + + $" -adddef \"ENABLED2.DEF\"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Slave\\slave\"" + + $" -def \"a\"" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Slave + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Slave/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void WangTest() + { + var mods = _wangMods.StandardMods; + + Raze raze = new(); + + raze.BeforeStart(_wangGame, _wangCamp); + var args = raze.GetStartGameArgs(_wangGame, _wangCamp, mods, [], true, true); + var expected = "" + + " -file \"enabled_mod.zip\"" + + " -adddef \"ENABLED1.DEF\"" + + " -adddef \"ENABLED2.DEF\"" + + " -file \"mod_incompatible_with_addon.zip\"" + + " -file \"incompatible_mod_with_compatible_version.zip\"" + + " -file \"dependent_mod.zip\"" + + " -file \"dependent_mod_with_compatible_version.zip\"" + + " -file \"feature_mod.zip\"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Wang\\wang\"" + + " -def \"a\"" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Wang + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Wang/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void WangTdTest() + { + var mods = _wangMods.AddonMods; + + Raze raze = new(); + + raze.BeforeStart(_wangGame, _wangTdCamp); + var args = raze.GetStartGameArgs(_wangGame, _wangTdCamp, mods, [], true, true); + var expected = "" + + " -file \"enabled_mod.zip\"" + + " -adddef \"ENABLED1.DEF\"" + + " -adddef \"ENABLED2.DEF\"" + + " -file \"mod_requires_addon.zip\"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Wang\\twindragon\"" + + " -def \"a\"" + + " -file \"D:\\Games\\Wang\\TD.zip\"" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Wang + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Wang/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void WangLooseMapTest() + { + var mods = _wangMods.StandardMods; + + Raze raze = new(); + + raze.BeforeStart(_wangGame, _wangLooseMap); + var args = raze.GetStartGameArgs(_wangGame, _wangLooseMap, mods, [], true, true); + var expected = $"" + + $" -file \"enabled_mod.zip\"" + + $" -adddef \"ENABLED1.DEF\"" + + $" -adddef \"ENABLED2.DEF\"" + + $" -file \"mod_incompatible_with_addon.zip\"" + + $" -file \"incompatible_mod_with_compatible_version.zip\"" + + $" -file \"dependent_mod.zip\"" + + $" -file \"dependent_mod_with_compatible_version.zip\"" + + $" -file \"feature_mod.zip\"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Wang\\loose-map\"" + + $" -def \"a\"" + + $" -file \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Wang\\Maps\"" + + $" -map \"LOOSE.MAP\"" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/Wang + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/Wang/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void WW2GITest() + { + var mods = _ww2Mods.MinimalMods; + + Raze raze = new(); + + raze.BeforeStart(_ww2Game, _ww2Camp); + var args = raze.GetStartGameArgs(_ww2Game, _ww2Camp, mods, [], true, true); + var expected = "" + + " -file \"enabled_mod.zip\"" + + " -adddef \"ENABLED1.DEF\"" + + " -adddef \"ENABLED2.DEF\"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\WW2GI\\ww2gi\"" + + " -def \"a\" -ww2gi" + + " -file WW2GI.GRP" + + " -con GAME.CON" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/WW2GI + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/WW2GI/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } + + [Fact] + public void WW2GIPlatoonTest() + { + var mods = _ww2Mods.MinimalMods; + + Raze raze = new(); + + raze.BeforeStart(_ww2Game, _ww2PlatoonCamp); + var args = raze.GetStartGameArgs(_ww2Game, _ww2PlatoonCamp, mods, [], true, true); + var expected = "" + + " -file \"enabled_mod.zip\"" + + " -adddef \"ENABLED1.DEF\"" + + " -adddef \"ENABLED2.DEF\"" + + $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\WW2GI\\platoon\"" + + " -def \"a\"" + + " -ww2gi" + + " -file WW2GI.GRP" + + " -file PLATOONL.DAT" + + " -con PLATOONL.DEF" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + + var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); + + Assert.StartsWith($""" + [GameSearch.Directories] + Path=D:/Games/WW2GI + + [FileSearch.Directories] + Path={Directory.GetCurrentDirectory()}/Data/Addons/WW2GI/Mods + + [SoundfontSearch.Directories] + """.Replace('\\', '/').Replace("\r\n", "\n"), config.Replace("\r\n", "\n")); + } +} diff --git a/src/Tests.Unit/CmdArguments/RedNukemCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/RedNukemCmdArgumentsTests.cs new file mode 100644 index 00000000..ffcebaec --- /dev/null +++ b/src/Tests.Unit/CmdArguments/RedNukemCmdArgumentsTests.cs @@ -0,0 +1,260 @@ +using Addons.Addons; +using Games.Games; +using Ports.Ports.EDuke32; +using Tests.Unit.Helpers; + +namespace Tests.Unit.CmdArguments; + +public sealed class RedNukemCmdArgumentsTests +{ + private readonly DukeGame _dukeGame; + private readonly DukeCampaign _dukeCamp; + private readonly DukeCampaign _dukeVaca; + private readonly DukeCampaign _dukeTcForVaca; + private readonly DukeCampaign _duke64Camp; + private readonly LooseMap _dukeLooseMap; + private readonly AutoloadModsTestSetups _dukeMods; + + private readonly NamGame _namGame; + private readonly DukeCampaign _namCamp; + private readonly AutoloadModsTestSetups _namMods; + + private readonly RedneckGame _redneckGame; + private readonly DukeCampaign _redneckCamp; + private readonly DukeCampaign _redneckAgainCamp; + private readonly AutoloadModsTestSetups _redneckMods; + + public RedNukemCmdArgumentsTests() + { + (_dukeGame, _dukeCamp, _dukeVaca, _dukeTcForVaca, _, _duke64Camp, _, _, _, _dukeLooseMap, _dukeMods) = PortTestSetups.Duke3D(); + (_namGame, _namCamp, _namMods) = PortTestSetups.Nam(); + (_redneckGame, _redneckCamp, _redneckAgainCamp, _, _redneckMods) = PortTestSetups.Redneck(); + } + + [Fact] + public void Duke64Test() + { + RedNukem redNukem = new(); + + var args = redNukem.GetStartGameArgs(_dukeGame, _duke64Camp, [], [], true, true); + var expected = "" + + " -usecwd" + + " -d blank.edm" + + " -h \"a\"" + + " -j \"D:\\Games\\Duke64\"" + + " -gamegrp \"rom.z64\"" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void DukeTest() + { + var mods = _dukeMods.StandardModsWithCons; + + RedNukem redNukem = new(); + + var args = redNukem.GetStartGameArgs(_dukeGame, _dukeCamp, mods, [], true, true); + var expected = $"" + + $" -g \"enabled_mod.zip\"" + + $" -mh \"ENABLED1.DEF\"" + + $" -mh \"ENABLED2.DEF\"" + + $" -mx \"ENABLED1.CON\"" + + $" -mx \"ENABLED2.CON\"" + + $" -g \"mod_incompatible_with_addon.zip\"" + + $" -g \"incompatible_mod_with_compatible_version.zip\"" + + $" -g \"dependent_mod.zip\"" + + $" -g \"dependent_mod_with_compatible_version.zip\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Mods\"" + + $" -usecwd" + + " -d blank.edm" + + " -h \"a\"" + + " -j \"D:\\Games\\Duke3D\"" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void DukeVacaTest() + { + var mods = _dukeMods.AddonModsWithCons; + + RedNukem eduke32 = new(); + + var args = eduke32.GetStartGameArgs(_dukeGame, _dukeVaca, mods, [], true, true); + var expected = $"" + + $" -g \"enabled_mod.zip\"" + + $" -mh \"ENABLED1.DEF\"" + + $" -mh \"ENABLED2.DEF\"" + + $" -mx \"ENABLED1.CON\"" + + $" -mx \"ENABLED2.CON\"" + + $" -g \"mod_requires_addon.zip\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Mods\"" + + $" -usecwd" + + " -d blank.edm" + + " -h \"a\"" + + " -j \"D:\\Games\\Duke3D\"" + + " -j \"D:\\Games\\Duke3D\\Vaca\"" + + " -g VACATION.GRP" + + " -quick" + + " -nosetup" + ; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void DukeTCTest() + { + RedNukem eduke32 = new(); + + var args = eduke32.GetStartGameArgs(_dukeGame, _dukeTcForVaca, [], [], true, true); + var expected = $"" + + $" -usecwd" + + " -d blank.edm" + + " -h \"TC.DEF\"" + + " -mh \"TC1.DEF\"" + + " -mh \"TC2.DEF\"" + + " -j \"D:\\Games\\Duke3D\"" + + " -j \"D:\\Games\\Duke3D\\Vaca\"" + + " -g VACATION.GRP" + + " -x \"TC.CON\"" + + " -mx \"TC1.CON\"" + + " -mx \"TC2.CON\"" + + $" -g \"{Path.Combine(Directory.GetCurrentDirectory(), "Data", "Duke3D", "Campaigns", "duke_tc.zip")}\"" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void DukeLooseMapTest() + { + var mods = _dukeMods.StandardModsWithCons; + + RedNukem redNukem = new(); + + var args = redNukem.GetStartGameArgs(_dukeGame, _dukeLooseMap, mods, [], true, true); + var expected = $"" + + $" -g \"enabled_mod.zip\"" + + $" -mh \"ENABLED1.DEF\"" + + $" -mh \"ENABLED2.DEF\"" + + $" -mx \"ENABLED1.CON\"" + + $" -mx \"ENABLED2.CON\"" + + $" -g \"mod_incompatible_with_addon.zip\"" + + $" -g \"incompatible_mod_with_compatible_version.zip\"" + + $" -g \"dependent_mod.zip\"" + + $" -g \"dependent_mod_with_compatible_version.zip\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Mods\"" + + $" -usecwd" + + " -d blank.edm" + + " -h \"a\"" + + " -j \"D:\\Games\\Duke3D\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Duke3D\\Maps\"" + + " -map \"LOOSE.MAP\"" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void NamTest() + { + var mods = _namMods.MinimalMods; + + RedNukem redNukem = new(); + + var args = redNukem.GetStartGameArgs(_namGame, _namCamp, mods, [], true, true); + var expected = $"" + + $" -g \"enabled_mod.zip\"" + + $" -mh \"ENABLED1.DEF\"" + + $" -mh \"ENABLED2.DEF\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\NAM\\Mods\"" + + $" -usecwd" + + " -d blank.edm" + + " -h \"a\"" + + " -j \"D:\\Games\\NAM\"" + + " -nam" + + " -gamegrp NAM.GRP" + + " -x GAME.CON" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void RedneckTest() + { + var mods = _redneckMods.MinimalMods; + + RedNukem redNukem = new(); + + var args = redNukem.GetStartGameArgs(_redneckGame, _redneckCamp, mods, [], true, true); + var expected = $"" + + $" -g \"enabled_mod.zip\"" + + $" -mh \"ENABLED1.DEF\"" + + $" -mh \"ENABLED2.DEF\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Redneck\\Mods\"" + + $" -usecwd" + + " -d blank.edm" + + " -h \"a\"" + + " -j \"D:\\Games\\Redneck\"" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void RedneckAgainTest() + { + var mods = _redneckMods.MinimalMods; + + RedNukem redNukem = new(); + + var args = redNukem.GetStartGameArgs(_redneckGame, _redneckAgainCamp, mods, [], true, true); + var expected = $"" + + $" -g \"enabled_mod.zip\"" + + $" -mh \"ENABLED1.DEF\"" + + $" -mh \"ENABLED2.DEF\"" + + $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Redneck\\Mods\"" + + $" -usecwd" + + " -d blank.edm" + + " -h \"a\"" + + " -j \"D:\\Games\\Again\"" + + " -quick" + + " -nosetup" + + ""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } +} diff --git a/src/Tests.Unit/CmdArguments/RedneckCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/RedneckCmdArgumentsTests.cs deleted file mode 100644 index 80542fea..00000000 --- a/src/Tests.Unit/CmdArguments/RedneckCmdArgumentsTests.cs +++ /dev/null @@ -1,232 +0,0 @@ -using Addons.Addons; -using Core.All.Enums; -using Games.Games; -using Ports.Ports; -using Ports.Ports.EDuke32; - -namespace Tests.Unit.CmdArguments; - -[Collection("Sync")] -public sealed class RedneckCmdArgumentsTests -{ - private readonly RedneckGame _redneckGame; - private readonly DukeCampaign _redneckCamp; - private readonly DukeCampaign _againCamp; - - private readonly AutoloadModsProvider _modsProvider; - - public RedneckCmdArgumentsTests() - { - _modsProvider = new(GameEnum.Redneck); - - _redneckGame = new() - { - GameInstallFolder = Path.Combine("D:", "Games", "Redneck"), - AgainInstallPath = Path.Combine("D:", "Games", "Again"), - }; - - _redneckCamp = new() - { - AddonId = new(nameof(GameEnum.Redneck).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Redneck Rampage", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Redneck), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainCon = null, - AdditionalCons = null, - MainDef = null, - AdditionalDefs = null, - RTS = null, - StartMap = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - - _againCamp = new() - { - AddonId = new(nameof(GameEnum.RidesAgain).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Rides Again", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.RidesAgain), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainCon = null, - AdditionalCons = null, - MainDef = null, - AdditionalDefs = null, - RTS = null, - StartMap = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - } - - [Fact] - public void RazeTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_redneckGame, _redneckCamp); - var args = raze.GetStartGameArgs(_redneckGame, _redneckCamp, mods, [], true, true); - var expected = $"" + - $" -file \"enabled_mod.zip\"" + - $" -adddef \"ENABLED1.DEF\"" + - $" -adddef \"ENABLED2.DEF\"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Redneck\\redneck\"" + - $" -def \"a\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Redneck - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Redneck/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void RazeAgainTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_redneckGame, _againCamp); - var args = raze.GetStartGameArgs(_redneckGame, _againCamp, mods, [], true, true); - var expected = $"" + - $" -file \"enabled_mod.zip\"" + - $" -adddef \"ENABLED1.DEF\"" + - $" -adddef \"ENABLED2.DEF\"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Redneck\\ridesagain\"" + - $" -def \"a\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Again - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Redneck/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void RedNukemTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - RedNukem redNukem = new(); - - var args = redNukem.GetStartGameArgs(_redneckGame, _redneckCamp, mods, [], true, true); - var expected = $"" + - $" -g \"enabled_mod.zip\"" + - $" -mh \"ENABLED1.DEF\"" + - $" -mh \"ENABLED2.DEF\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Redneck\\Mods\"" + - $" -usecwd" + - " -d blank.edm" + - $" -h \"a\"" + - $" -j \"D:\\Games\\Redneck\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void RedNukemAgainTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - RedNukem redNukem = new(); - - var args = redNukem.GetStartGameArgs(_redneckGame, _againCamp, mods, [], true, true); - var expected = $"" + - $" -g \"enabled_mod.zip\"" + - $" -mh \"ENABLED1.DEF\"" + - $" -mh \"ENABLED2.DEF\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Redneck\\Mods\"" + - $" -usecwd" + - " -d blank.edm" + - $" -h \"a\"" + - $" -j \"D:\\Games\\Again\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } -} diff --git a/src/Tests.Unit/CmdArguments/SlaveCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/SlaveCmdArgumentsTests.cs deleted file mode 100644 index 74acef3a..00000000 --- a/src/Tests.Unit/CmdArguments/SlaveCmdArgumentsTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -using Addons.Addons; -using Core.All.Enums; -using Games.Games; -using Ports.Ports; -using Ports.Ports.EDuke32; - -namespace Tests.Unit.CmdArguments; - -[Collection("Sync")] -public sealed class SlaveCmdArgumentsTests -{ - private readonly SlaveGame _slaveGame; - private readonly GenericCampaign _slaveCamp; - - private readonly AutoloadModsProvider _modsProvider; - - public SlaveCmdArgumentsTests() - { - _modsProvider = new(GameEnum.Slave); - - _slaveGame = new() - { - GameInstallFolder = Path.Combine("D:", "Games", "Slave"), - }; - - _slaveCamp = new() - { - AddonId = new(nameof(GameEnum.Slave).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Slave", - GridImageHash = null, - PreviewImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Slave), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainDef = null, - AdditionalDefs = null, - StartMap = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - } - - [Fact] - public void RazeTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_slaveGame, _slaveCamp); - var args = raze.GetStartGameArgs(_slaveGame, _slaveCamp, mods, [], true, true); - var expected = $"" + - $" -file \"enabled_mod.zip\"" + - $" -adddef \"ENABLED1.DEF\"" + - $" -adddef \"ENABLED2.DEF\"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Slave\\slave\"" + - $" -def \"a\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Slave - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Slave/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void PCExhumedTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - PCExhumed pcExhumed = new(); - - var args = pcExhumed.GetStartGameArgs(_slaveGame, _slaveCamp, mods, [], true, true); - var expected = $"" + - $" -g \"enabled_mod.zip\"" + - $" -mh \"ENABLED1.DEF\"" + - $" -mh \"ENABLED2.DEF\"" + - $" -j \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Slave\\Mods\"" + - $" -usecwd" + - $" -j \"D:\\Games\\Slave\"" + - $" -h \"a\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } -} diff --git a/src/Tests.Unit/CmdArguments/VoidSWCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/VoidSWCmdArgumentsTests.cs new file mode 100644 index 00000000..430cce63 --- /dev/null +++ b/src/Tests.Unit/CmdArguments/VoidSWCmdArgumentsTests.cs @@ -0,0 +1,114 @@ +using Addons.Addons; +using Games.Games; +using Ports.Ports.EDuke32; +using Tests.Unit.Helpers; + +namespace Tests.Unit.CmdArguments; + +public sealed class VoidSWCmdArgumentsTests +{ + private readonly WangGame _wangGame; + private readonly GenericCampaign _wangCamp; + private readonly GenericCampaign _wangTdCamp; + private readonly LooseMap _wangLooseMap; + private readonly AutoloadModsTestSetups _wangMods; + + public VoidSWCmdArgumentsTests() + { + (_wangGame, _wangCamp, _wangTdCamp, _wangLooseMap, _wangMods) = PortTestSetups.Wang(); + } + + [Fact] + public void WangTest() + { + var mods = _wangMods.StandardMods; + + VoidSW voidSw = new(); + + var args = voidSw.GetStartGameArgs(_wangGame, _wangCamp, mods, [], true, true, 3); + var expected = "" + + " -g\"enabled_mod.zip\"" + + " -mh\"ENABLED1.DEF\"" + + " -mh\"ENABLED2.DEF\"" + + " -g\"mod_incompatible_with_addon.zip\"" + + " -g\"incompatible_mod_with_compatible_version.zip\"" + + " -g\"dependent_mod.zip\"" + + " -g\"dependent_mod_with_compatible_version.zip\"" + + " -g\"feature_mod.zip\"" + + $" -j\"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Wang\\Mods\"" + + " -usecwd" + + " -j\"D:\\Games\\Wang\"" + + " -h\"a\"" + + " -addon0" + + " -s3" + + " -quick" + + " -nosetup" + ; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void WangTdTest() + { + var mods = _wangMods.AddonMods; + + VoidSW voidSw = new(); + + var args = voidSw.GetStartGameArgs(_wangGame, _wangTdCamp, mods, [], true, true, 3); + var expected = "" + + " -g\"enabled_mod.zip\"" + + " -mh\"ENABLED1.DEF\"" + + " -mh\"ENABLED2.DEF\"" + + " -g\"mod_requires_addon.zip\"" + + $" -j\"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Wang\\Mods\"" + + " -usecwd -j\"D:\\Games\\Wang\"" + + " -h\"a\"" + + " -addon0" + + $" -j\"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Wang\\Campaigns\"" + + " -g\"TD.zip\"" + + " -s3" + + " -quick" + + " -nosetup" + ; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } + + [Fact] + public void WangLooseMapTest() + { + var mods = _wangMods.StandardMods; + + VoidSW voidSw = new(); + + var args = voidSw.GetStartGameArgs(_wangGame, _wangLooseMap, mods, [], true, true, 3); + var expected = $"" + + $" -g\"enabled_mod.zip\"" + + $" -mh\"ENABLED1.DEF\"" + + $" -mh\"ENABLED2.DEF\"" + + $" -g\"mod_incompatible_with_addon.zip\"" + + $" -g\"incompatible_mod_with_compatible_version.zip\"" + + $" -g\"dependent_mod.zip\"" + + $" -g\"dependent_mod_with_compatible_version.zip\"" + + $" -g\"feature_mod.zip\"" + + $" -j\"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Wang\\Mods\"" + + $" -usecwd" + + $" -j\"D:\\Games\\Wang\"" + + $" -h\"a\"" + + $" -j\"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Wang\\Maps\"" + + $" -map \"LOOSE.MAP\"" + + $" -s3" + + $" -quick" + + $" -nosetup" + + $""; + + NormalizerHelper.NormalizeExpectedArgs(ref args, ref expected); + + Assert.Equal(expected, args); + } +} diff --git a/src/Tests.Unit/CmdArguments/WW2GICmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/WW2GICmdArgumentsTests.cs deleted file mode 100644 index 5e625702..00000000 --- a/src/Tests.Unit/CmdArguments/WW2GICmdArgumentsTests.cs +++ /dev/null @@ -1,230 +0,0 @@ -using Addons.Addons; -using Core.All.Enums; -using Core.All.Enums.Addons; -using Games.Games; -using Ports.Ports; -using Ports.Ports.EDuke32; - -namespace Tests.Unit.CmdArguments; - -[Collection("Sync")] -public sealed class WW2GICmdArgumentsTests -{ - private readonly WW2GIGame _ww2Game; - private readonly DukeCampaign _ww2Camp; - private readonly DukeCampaign _platoonCamp; - - private readonly AutoloadModsProvider _modsProvider; - - public WW2GICmdArgumentsTests() - { - _modsProvider = new(GameEnum.Redneck); - - _ww2Game = new() - { - GameInstallFolder = Path.Combine("D:", "Games", "WW2GI") - }; - - _ww2Camp = new() - { - AddonId = new(nameof(GameEnum.WW2GI).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "World War II GI", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.WW2GI), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainCon = null, - AdditionalCons = null, - MainDef = null, - AdditionalDefs = null, - RTS = null, - StartMap = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - - _platoonCamp = new() - { - AddonId = new(nameof(WW2GIAddonEnum.Platoon).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Platoon Leader", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.WW2GI), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainCon = null, - AdditionalCons = null, - MainDef = null, - AdditionalDefs = null, - RTS = null, - StartMap = null, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - } - - [Fact] - public void RazeTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_ww2Game, _ww2Camp); - var args = raze.GetStartGameArgs(_ww2Game, _ww2Camp, mods, [], true, true); - var expected = $"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\WW2GI\\ww2gi\"" + - $" -def \"a\" -ww2gi" + - $" -file WW2GI.GRP" + - $" -con GAME.CON" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/WW2GI - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/WW2GI/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void RazePlatoonTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_ww2Game, _platoonCamp); - var args = raze.GetStartGameArgs(_ww2Game, _platoonCamp, mods, [], true, true); - var expected = $"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\WW2GI\\platoon\"" + - $" -def \"a\"" + - $" -ww2gi" + - $" -file WW2GI.GRP" + - $" -file PLATOONL.DAT" + - $" -con PLATOONL.DEF" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/WW2GI - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/WW2GI/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void EDuke32Test() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - EDuke32 eDuke = new(); - - var args = eDuke.GetStartGameArgs(_ww2Game, _ww2Camp, mods, [], true, true); - var expected = "" + - " -usecwd" + - " -cachesize 262144" + - " -h \"a\"" + - " -j \"D:\\Games\\WW2GI\"" + - " -ww2gi" + - " -gamegrp WW2GI.GRP" + - " -x GAME.CON" + - " -quick" + - " -nosetup" + - ""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void EDuke32PlatoonTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.IncompatibleMod, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - EDuke32 eDuke = new(); - - var args = eDuke.GetStartGameArgs(_ww2Game, _platoonCamp, mods, [], true, true); - var expected = "" + - " -usecwd" + - " -cachesize 262144" + - " -h \"a\"" + - " -j \"D:\\Games\\WW2GI\"" + - " -ww2gi -gamegrp WW2GI.GRP" + - " -grp PLATOONL.DAT" + - " -x PLATOONL.DEF" + - " -quick" + - " -nosetup" + - ""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } -} diff --git a/src/Tests.Unit/CmdArguments/WangCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/WangCmdArgumentsTests.cs deleted file mode 100644 index ff8f2d09..00000000 --- a/src/Tests.Unit/CmdArguments/WangCmdArgumentsTests.cs +++ /dev/null @@ -1,256 +0,0 @@ -using Addons.Addons; -using Core.All.Enums; -using Core.All.Enums.Addons; -using Games.Games; -using Ports.Ports; -using Ports.Ports.EDuke32; - -namespace Tests.Unit.CmdArguments; - -[Collection("Sync")] -public sealed class WangCmdArgumentsTests -{ - private readonly WangGame _wangGame; - private readonly GenericCampaign _wangCamp; - private readonly GenericCampaign _tdCamp; - - private readonly AutoloadModsProvider _modsProvider; - - public WangCmdArgumentsTests() - { - _modsProvider = new(GameEnum.Wang); - - _wangGame = new() - { - GameInstallFolder = Path.Combine("D:", "Games", "Wang"), - }; - - _wangCamp = new() - { - AddonId = new(nameof(GameEnum.Wang).ToLower(), null), - Type = AddonTypeEnum.Official, - Title = "Shadow Warrior", - GridImageHash = null, - PreviewImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Wang), - RequiredFeatures = null, - PathToFile = null, - DependentAddons = null, - IncompatibleAddons = null, - MainDef = null, - AdditionalDefs = null, - StartMap = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - - _tdCamp = new() - { - AddonId = new(nameof(WangAddonEnum.TwinDragon).ToLower(), null), - Type = AddonTypeEnum.TC, - Title = "Twin Dragon", - GridImageHash = null, - PreviewImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Wang), - RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Games", "Wang", "TD.zip"), - DependentAddons = null, - IncompatibleAddons = null, - MainDef = null, - AdditionalDefs = null, - StartMap = null, - IsUnpacked = false, - Executables = null, - Options = null - }; - } - - [Fact] - public void RazeTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.IncompatibleMod, - _modsProvider.DisabledMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_wangGame, _wangCamp); - var args = raze.GetStartGameArgs(_wangGame, _wangCamp, mods, [], true, true); - var expected = "" + - " -file \"enabled_mod.zip\"" + - " -adddef \"ENABLED1.DEF\"" + - " -adddef \"ENABLED2.DEF\"" + - " -file \"incompatible_mod_with_compatible_version.zip\"" + - " -file \"dependent_mod.zip\"" + - " -file \"dependent_mod_with_compatible_version.zip\"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Wang\\wang\"" + - " -def \"a\"" + - " -quick" + - " -nosetup" + - ""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Wang - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Wang/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void RazeTdTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_wangGame, _tdCamp); - var args = raze.GetStartGameArgs(_wangGame, _tdCamp, mods, [], true, true); - var expected = "" + - " -file \"enabled_mod.zip\"" + - " -adddef \"ENABLED1.DEF\"" + - " -adddef \"ENABLED2.DEF\"" + - " -file \"mod_requires_addon.zip\"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Wang\\twindragon\"" + - " -def \"a\"" + - " -file \"D:\\Games\\Wang\\TD.zip\"" + - " -quick" + - " -nosetup" + - ""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Wang - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Wang/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void VoidSWTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.IncompatibleMod, - _modsProvider.DisabledMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - VoidSW voidSw = new(); - - var args = voidSw.GetStartGameArgs(_wangGame, _wangCamp, mods, [], true, true, 3); - var expected = "" + - " -g\"enabled_mod.zip\"" + - " -mh\"ENABLED1.DEF\"" + - " -mh\"ENABLED2.DEF\"" + - " -g\"incompatible_mod_with_compatible_version.zip\"" + - " -g\"dependent_mod.zip\"" + - " -g\"dependent_mod_with_compatible_version.zip\"" + - $" -j\"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Wang\\Mods\"" + - " -usecwd" + - " -j\"D:\\Games\\Wang\"" + - " -h\"a\"" + - " -addon0" + - " -s3" + - " -quick" + - " -nosetup" - ; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } - - [Fact] - public void VoidSWTdTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.ModThatIncompatibleWithAddon, - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - VoidSW voidSw = new(); - - var args = voidSw.GetStartGameArgs(_wangGame, _tdCamp, mods, [], true, true, 3); - var expected = "" + - " -g\"enabled_mod.zip\"" + - " -mh\"ENABLED1.DEF\"" + - " -mh\"ENABLED2.DEF\"" + - " -g\"mod_requires_addon.zip\"" + - $" -j\"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Wang\\Mods\"" + - " -usecwd -j\"D:\\Games\\Wang\"" + - " -h\"a\"" + - " -addon0" + - $" -j\"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Wang\\Campaigns\"" + - " -g\"TD.zip\"" + - " -s3" + - " -quick" + - " -nosetup" - ; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } -} diff --git a/src/Tests.Unit/CmdArguments/WangLooseMapsCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/WangLooseMapsCmdArgumentsTests.cs deleted file mode 100644 index dcc6c2ec..00000000 --- a/src/Tests.Unit/CmdArguments/WangLooseMapsCmdArgumentsTests.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Addons.Addons; -using Core.All.Enums; -using Core.All.Serializable.Addon; -using Games.Games; -using Ports.Ports; -using Ports.Ports.EDuke32; - -namespace Tests.Unit.CmdArguments; - -[Collection("Sync")] -public sealed class WangLooseMapsCmdArgumentsTests -{ - private readonly WangGame _wangGame; - private readonly LooseMap _looseMap; - - private readonly AutoloadModsProvider _modsProvider; - - public WangLooseMapsCmdArgumentsTests() - { - _modsProvider = new(GameEnum.Wang); - - _wangGame = new() - { - GameInstallFolder = Path.Combine("D:", "Games", "Wang"), - }; - - _looseMap = new() - { - AddonId = new("loose-map", null), - Type = AddonTypeEnum.Map, - Title = "Loose map", - GridImageHash = null, - Author = null, - ReleaseDate = null, - Description = null, - SupportedGame = new(GameEnum.Wang), - RequiredFeatures = null, - PathToFile = Path.Combine("Maps", "LOOSE.MAP"), - DependentAddons = null, - IncompatibleAddons = null, - MainDef = null, - AdditionalDefs = null, - StartMap = new MapFileJsonModel() { File = "LOOSE.MAP" }, - PreviewImageHash = null, - IsUnpacked = false, - Executables = null, - BloodIni = null, - Options = null - }; - } - - [Fact] - public void RazeTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.IncompatibleMod, - _modsProvider.DisabledMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - Raze raze = new(); - - raze.BeforeStart(_wangGame, _looseMap); - var args = raze.GetStartGameArgs(_wangGame, _looseMap, mods, [], true, true); - var expected = $"" + - $" -file \"enabled_mod.zip\"" + - $" -adddef \"ENABLED1.DEF\"" + - $" -adddef \"ENABLED2.DEF\"" + - $" -file \"incompatible_mod_with_compatible_version.zip\"" + - $" -file \"dependent_mod.zip\"" + - $" -file \"dependent_mod_with_compatible_version.zip\"" + - $" -savedir \"{Directory.GetCurrentDirectory()}\\Data\\Saves\\Raze\\Wang\\loose-map\"" + - $" -def \"a\"" + - $" -file \"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Wang\\Maps\"" + - $" -map \"LOOSE.MAP\"" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - - var config = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Ports", "Raze", "raze_portable.ini")); - - Assert.StartsWith($""" - [GameSearch.Directories] - Path=D:/Games/Wang - - [FileSearch.Directories] - Path={Directory.GetCurrentDirectory()}/Data/Addons/Wang/Mods - - [SoundfontSearch.Directories] - """.Replace('\\', '/'), config); - } - - [Fact] - public void VoidSWTest() - { - var mods = new List() { - _modsProvider.EnabledMod, - _modsProvider.ModThatRequiresOfficialAddon, - _modsProvider.IncompatibleMod, - _modsProvider.DisabledMod, - _modsProvider.IncompatibleModWithIncompatibleVersion, - _modsProvider.IncompatibleModWithCompatibleVersion, - _modsProvider.DependentMod, - _modsProvider.DependentModWithCompatibleVersion, - _modsProvider.DependentModWithIncompatibleVersion, - _modsProvider.ModForAnotherGame - }.ToDictionary(x => x.AddonId, x => (BaseAddon)x); - - VoidSW voidSw = new(); - - var args = voidSw.GetStartGameArgs(_wangGame, _looseMap, mods, [], true, true, 3); - var expected = $"" + - $" -g\"enabled_mod.zip\"" + - $" -mh\"ENABLED1.DEF\"" + - $" -mh\"ENABLED2.DEF\"" + - $" -g\"incompatible_mod_with_compatible_version.zip\"" + - $" -g\"dependent_mod.zip\"" + - $" -g\"dependent_mod_with_compatible_version.zip\"" + - $" -j\"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Wang\\Mods\"" + - $" -usecwd" + - $" -j\"D:\\Games\\Wang\"" + - $" -h\"a\"" + - $" -j\"{Directory.GetCurrentDirectory()}\\Data\\Addons\\Wang\\Maps\"" + - $" -map \"LOOSE.MAP\"" + - $" -s3" + - $" -quick" + - $" -nosetup" + - $""; - - if (OperatingSystem.IsLinux()) - { - args = args.Replace('\\', Path.DirectorySeparatorChar); - expected = expected.Replace('\\', Path.DirectorySeparatorChar); - } - - Assert.Equal(expected, args); - } -} diff --git a/src/Tests.Unit/CmdArguments/ZeroHourCmdArgumentsTests.cs b/src/Tests.Unit/CmdArguments/ZeroHourCmdArgumentsTests.cs new file mode 100644 index 00000000..babb2ca6 --- /dev/null +++ b/src/Tests.Unit/CmdArguments/ZeroHourCmdArgumentsTests.cs @@ -0,0 +1,30 @@ +using Addons.Addons; +using Core.Client.Interfaces; +using Games.Games; +using Moq; +using Ports.Ports; +using Tests.Unit.Helpers; + +namespace Tests.Unit.CmdArguments; + +public sealed class ZeroHourCmdArgumentsTests +{ + private readonly DukeGame _dukeGame; + private readonly DukeCampaign _dukeZhCamp; + + public ZeroHourCmdArgumentsTests() + { + (_dukeGame, _, _, _, _, _, _dukeZhCamp, _, _, _, _) = PortTestSetups.Duke3D(); + } + + [Fact] + public void DukeTest() + { + Mock _config = new(); + ZHRecomp redNukem = new(_config.Object); + + var args = redNukem.GetStartGameArgs(_dukeGame, _dukeZhCamp, [], [], true, true); + + Assert.Equal(string.Empty, args); + } +} diff --git a/src/Tests.Unit/Files/WhatLiesBeneathAddon.zip b/src/Tests.Unit/Files/WhatLiesBeneathAddon.zip deleted file mode 100644 index fe0d33e779ff1fdba27ce086be2aa99c3accfe6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 710 zcmWIWW@Zs#U|`^2*f^;=@>=5IJ{=|o1}7E<1}>mzVoFMWo?cdQeqL$t!MxiBJbT4o z*bCUH^HoV*IK5#Bcff+ziQE$oM~Q55J?@lpal(^K$BFgws;6fLFK9m^Cja%v50Cx# z?=gsYiG2?UndPD9to8guN{fg${O|Z$Waf;b@Ox9^iz0aTsa7wvdAs6t$B}1il`S($c>bSb`sV-o z)5G6pQ|n}frv}cMFyZ*~<2AR}Sr!kKx;F_TxfDP*0RoJmU22bq%QT$f(%0JXW=Tq|S)g#Bke z*REmKd?WbEu{xubS-&qsVbalEhmPD$6{JMm!82G^WZEBD8) zUX-C`oN|NFFZr1TXT}Mq7`u0AtNGUYPj9w3*vVEWwZCDPYLNu9n1;lQ<5FuLUpRNm zqe$v#{oZHOc6uJm38?wnVOu@3v}oaskk^gHH*Vg$)Hwb0#)%WBTk&;YdD8Re(y6wO zhNizJet8hThFxFltg(baP}Q2~#aRn)vOTQ0e`?PI%ddz1-bM1aN@()@)Y^G)=40`? z>f(Rw0p5&E_6)dEl?pIjfq(*-L{4K|P%%aZ2?pk*-)`p}ZZkp^!KnamRyGia5eQ>| LbO + public void Dispose() + { + if (File.Exists(_grpInfoFilePath)) + { + File.Delete(_grpInfoFilePath); + } + } + + [Fact] + public void Parse_ReturnTrue() + { + var result = GrpInfoProvider.Parse(_grpInfoFilePath); + + Assert.Equal(3, result.Count); + + var entry1 = result.First(x => x.Name == "1999/2000 TC"); + Assert.Equal("scripts/2000tc.con", entry1.MainCon); + Assert.Null(entry1.AddDef); + Assert.Equal(17147390, entry1.Size); + + var entry2 = result.First(x => x.Name == "25th Century Duke"); + Assert.Equal("scripts/25th_century.con", entry2.MainCon); + Assert.Equal("add.def", entry2.AddDef); + Assert.Equal(1385747, entry2.Size); + + var entry3 = result.First(x => x.Name == "A.Dream Trilogy"); + Assert.Null(entry3.MainCon); + Assert.Null(entry3.AddDef); + Assert.Equal(28245809, entry3.Size); + } + + [Fact] + public void Parse_EmptyFile_ReturnsEmptyList() + { + var emptyPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.grpinfo"); + File.WriteAllText(emptyPath, ""); + + try + { + var result = GrpInfoProvider.Parse(emptyPath); + + Assert.Empty(result); + } + finally + { + File.Delete(emptyPath); + } + } + + [Fact] + public void Parse_OnlyCommentsAndWhitespace_ReturnsEmptyList() + { + var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.grpinfo"); + File.WriteAllText(path, """ + // just a comment + + // another comment + """); + + try + { + var result = GrpInfoProvider.Parse(path); + + Assert.Empty(result); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void Parse_EntriesMissingNameOrSize_Skipped() + { + var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.grpinfo"); + File.WriteAllText(path, """ + grpinfo + { + name "Valid Entry" + size 100 + } + + grpinfo + { + // no name and no size + } + + grpinfo + { + name "No Size Entry" + // size missing + } + """); + + try + { + var result = GrpInfoProvider.Parse(path); + + Assert.Single(result); + Assert.Equal("Valid Entry", result[0].Name); + Assert.Equal(100, result[0].Size); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void Parse_SingleEntry_ReturnsOneEntry() + { + var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.grpinfo"); + File.WriteAllText(path, """ + grpinfo + { + name "Single Addon" + scriptname "scripts/main.con" + defname "add.def" + size 5000 + crc 0x12345678 + flags 16 + } + """); + + try + { + var result = GrpInfoProvider.Parse(path); + + var entry = Assert.Single(result); + Assert.Equal("Single Addon", entry.Name); + Assert.Equal("scripts/main.con", entry.MainCon); + Assert.Equal("add.def", entry.AddDef); + Assert.Equal(5000, entry.Size); + } + finally + { + File.Delete(path); + } + } +} diff --git a/src/Tests.Unit/CmdArguments/AutoloadModsProvider.cs b/src/Tests.Unit/Helpers/AutoloadModsTestSetups.cs similarity index 61% rename from src/Tests.Unit/CmdArguments/AutoloadModsProvider.cs rename to src/Tests.Unit/Helpers/AutoloadModsTestSetups.cs index ae107c94..fb5ec8d0 100644 --- a/src/Tests.Unit/CmdArguments/AutoloadModsProvider.cs +++ b/src/Tests.Unit/Helpers/AutoloadModsTestSetups.cs @@ -4,15 +4,16 @@ using Core.All.Enums.Addons; using Core.All.Enums.Versions; -namespace Tests.Unit.CmdArguments; +namespace Tests.Unit.Helpers; -internal sealed class AutoloadModsProvider +internal sealed class AutoloadModsTestSetups { private readonly GameInfo _game; private readonly string _addon; private readonly FeatureEnum _feature; + private readonly FeatureEnum _unsupportedFeature; - public AutoloadModsProvider(GameEnum gameEnum) + public AutoloadModsTestSetups(GameEnum gameEnum) { _game = gameEnum switch { @@ -25,6 +26,7 @@ public AutoloadModsProvider(GameEnum gameEnum) GameEnum.RidesAgain => new(GameEnum.RidesAgain), GameEnum.Fury => new(GameEnum.Fury), GameEnum.NAM => new(GameEnum.NAM), + GameEnum.WW2GI => new(GameEnum.WW2GI), _ => throw new NotSupportedException() }; @@ -38,6 +40,7 @@ public AutoloadModsProvider(GameEnum gameEnum) GameEnum.Duke64 => "", GameEnum.Slave => "", GameEnum.NAM => "", + GameEnum.WW2GI => nameof(WW2GIAddonEnum.Platoon).ToLower(), _ => throw new NotSupportedException() }; @@ -52,11 +55,27 @@ public AutoloadModsProvider(GameEnum gameEnum) GameEnum.RidesAgain => FeatureEnum.Models, GameEnum.Fury => FeatureEnum.EDuke32_CON, GameEnum.NAM => FeatureEnum.Models, + GameEnum.WW2GI => FeatureEnum.Models, + _ => throw new NotSupportedException() + }; + + _unsupportedFeature = gameEnum switch + { + GameEnum.Duke3D => FeatureEnum.Modern_Types, + GameEnum.Blood => FeatureEnum.EDuke32_CON, + GameEnum.Wang => FeatureEnum.Modern_Types, + GameEnum.Duke64 => FeatureEnum.Modern_Types, + GameEnum.Slave => FeatureEnum.Modern_Types, + GameEnum.Redneck => FeatureEnum.Modern_Types, + GameEnum.RidesAgain => FeatureEnum.Modern_Types, + GameEnum.Fury => FeatureEnum.Modern_Types, + GameEnum.NAM => FeatureEnum.Modern_Types, + GameEnum.WW2GI => FeatureEnum.Modern_Types, _ => throw new NotSupportedException() }; } - public AutoloadMod EnabledModWithCons => new() + private AutoloadMod EnabledModWithCons => new() { AddonId = new("enabledMod", "1.5"), Type = AddonTypeEnum.Mod, @@ -68,7 +87,7 @@ public AutoloadModsProvider(GameEnum gameEnum) SupportedGame = _game, IncompatibleAddons = null, RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "enabled_mod.zip"), + FileInfo = new(Path.Combine("D:", "Mods", "enabled_mod.zip"), "addon.json"), DependentAddons = null, AdditionalCons = ["ENABLED1.CON", "ENABLED2.CON"], MainDef = null, @@ -76,12 +95,11 @@ public AutoloadModsProvider(GameEnum gameEnum) StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod EnabledMod => new() + private AutoloadMod EnabledMod => new() { AddonId = new("enabledMod", "1.5"), Type = AddonTypeEnum.Mod, @@ -93,7 +111,7 @@ public AutoloadModsProvider(GameEnum gameEnum) SupportedGame = _game, IncompatibleAddons = null, RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "enabled_mod.zip"), + FileInfo = new(Path.Combine("D:", "Mods", "enabled_mod.zip"), "addon.json"), DependentAddons = null, AdditionalCons = null, MainDef = null, @@ -101,12 +119,11 @@ public AutoloadModsProvider(GameEnum gameEnum) StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod ModThatRequiresOfficialAddon => new() + private AutoloadMod ModThatRequiresOfficialAddon => new() { AddonId = new("modThatRequiresOfficialAddon", "1.5"), Type = AddonTypeEnum.Mod, @@ -118,45 +135,43 @@ public AutoloadModsProvider(GameEnum gameEnum) SupportedGame = _game, IncompatibleAddons = null, RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "mod_requires_addon.zip"), - DependentAddons = new Dictionary() { { _addon, null } }, + FileInfo = new(Path.Combine("D:", "Mods", "mod_requires_addon.zip"), "addon.json"), + DependentAddons = new Dictionary { { _addon, null } }, AdditionalCons = null, MainDef = null, AdditionalDefs = null, StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod ModThatIncompatibleWithAddon => new() + private AutoloadMod ModThatIncompatibleWithAddon => new() { - AddonId = new("modThatIncompatibleWithVaca", "1.5"), + AddonId = new("modThatIncompatibleWithOfficialAddon", "1.5"), Type = AddonTypeEnum.Mod, - Title = "modThatIncompatibleWithVaca", + Title = "modThatIncompatibleWithOfficialAddon", GridImageHash = null, Author = null, ReleaseDate = null, Description = null, SupportedGame = _game, - IncompatibleAddons = new Dictionary() { { _addon, null } }, + IncompatibleAddons = new Dictionary { { _addon, null } }, DependentAddons = null, RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "mod_incompatible_with_addon.zip"), + FileInfo = new(Path.Combine("D:", "Mods", "mod_incompatible_with_addon.zip"), "addon.json"), AdditionalCons = null, MainDef = null, AdditionalDefs = null, StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod IncompatibleMod => new() + private AutoloadMod IncompatibleWithEnabledMod => new() { AddonId = new("incompatibleMod", "1.0"), Type = AddonTypeEnum.Mod, @@ -167,21 +182,20 @@ public AutoloadModsProvider(GameEnum gameEnum) Description = null, SupportedGame = _game, RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), + FileInfo = new(Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), "!!!!!!!!!!NOPE!!!!!!!!!!"), DependentAddons = null, - IncompatibleAddons = new Dictionary() { { "EnAbLeDmOd", null } }, + IncompatibleAddons = new Dictionary { { "EnAbLeDmOd", null } }, AdditionalCons = null, MainDef = null, AdditionalDefs = null, StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod IncompatibleModWithCompatibleVersion => new() + private AutoloadMod IncompatibleModWithCompatibleVersion => new() { AddonId = new("incompatibleModWithCompatibleVersion", "1.0"), Type = AddonTypeEnum.Mod, @@ -192,21 +206,20 @@ public AutoloadModsProvider(GameEnum gameEnum) Description = null, SupportedGame = _game, RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "incompatible_mod_with_compatible_version.zip"), - DependentAddons = new Dictionary() { { "enabledMod", null } }, - IncompatibleAddons = new Dictionary() { { "enabledMod", "<=1.0" } }, + FileInfo = new(Path.Combine("D:", "Mods", "incompatible_mod_with_compatible_version.zip"), "addon.json"), + DependentAddons = new Dictionary { { "enabledMod", null } }, + IncompatibleAddons = new Dictionary { { "enabledMod", "<=1.0" } }, AdditionalCons = null, MainDef = null, AdditionalDefs = null, StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod IncompatibleModWithIncompatibleVersion => new() + private AutoloadMod IncompatibleModWithIncompatibleVersion => new() { AddonId = new("incompatibleModWithIncompatibleVersion", "1.0"), Type = AddonTypeEnum.Mod, @@ -217,21 +230,20 @@ public AutoloadModsProvider(GameEnum gameEnum) Description = null, SupportedGame = _game, RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), - DependentAddons = new Dictionary() { { "enabledMod", null } }, - IncompatibleAddons = new Dictionary() { { "enabledMod", ">1.1" } }, + FileInfo = new(Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), "!!!!!!!!!!NOPE!!!!!!!!!!"), + DependentAddons = new Dictionary { { "enabledMod", null } }, + IncompatibleAddons = new Dictionary { { "enabledMod", ">1.1" } }, AdditionalCons = null, MainDef = null, AdditionalDefs = null, StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod DependentMod => new() + private AutoloadMod ModThatDependsOnEnabled => new() { AddonId = new("dependentMod", "1.0"), Type = AddonTypeEnum.Mod, @@ -242,8 +254,8 @@ public AutoloadModsProvider(GameEnum gameEnum) Description = null, SupportedGame = _game, RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "dependent_mod.zip"), - DependentAddons = new Dictionary() { { "enabledMod", null } }, + FileInfo = new(Path.Combine("D:", "Mods", "dependent_mod.zip"), "addon.json"), + DependentAddons = new Dictionary { { "enabledMod", null } }, IncompatibleAddons = null, AdditionalCons = null, MainDef = null, @@ -251,13 +263,12 @@ public AutoloadModsProvider(GameEnum gameEnum) StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod MultipleDependenciesMod => new() + private AutoloadMod MultipleDependenciesMod => new() { AddonId = new("multipleDependenciesMod", "1.0"), Type = AddonTypeEnum.Mod, @@ -268,8 +279,8 @@ public AutoloadModsProvider(GameEnum gameEnum) Description = null, SupportedGame = _game, RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), - DependentAddons = new Dictionary() { { "enabledMod", null }, { "someMod", null } }, + FileInfo = new(Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), "!!!!!!!!!!NOPE!!!!!!!!!!"), + DependentAddons = new Dictionary { { "enabledMod", null }, { "someMod", null } }, IncompatibleAddons = null, AdditionalCons = null, MainDef = null, @@ -277,12 +288,11 @@ public AutoloadModsProvider(GameEnum gameEnum) StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod DependentModWithIncompatibleVersion => new() + private AutoloadMod DependentModWithIncompatibleVersion => new() { AddonId = new("dependentModWithIncompatibleVersion", "1.0"), Type = AddonTypeEnum.Mod, @@ -293,8 +303,8 @@ public AutoloadModsProvider(GameEnum gameEnum) Description = null, SupportedGame = _game, RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), - DependentAddons = new Dictionary() { { "enabledMod", "<=1.0" } }, + FileInfo = new(Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), "!!!!!!!!!!NOPE!!!!!!!!!!"), + DependentAddons = new Dictionary { { "enabledMod", "<=1.0" } }, IncompatibleAddons = null, AdditionalCons = null, MainDef = null, @@ -302,12 +312,11 @@ public AutoloadModsProvider(GameEnum gameEnum) StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod DependentModWithCompatibleVersion => new() + private AutoloadMod DependentModWithCompatibleVersion => new() { AddonId = new("dependentModWithCompatibleVersion", "1.0"), Type = AddonTypeEnum.Mod, @@ -318,8 +327,8 @@ public AutoloadModsProvider(GameEnum gameEnum) Description = null, SupportedGame = _game, RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "dependent_mod_with_compatible_version.zip"), - DependentAddons = new Dictionary() { { "enabledMod", ">1.1" } }, + FileInfo = new(Path.Combine("D:", "Mods", "dependent_mod_with_compatible_version.zip"), "addon.json"), + DependentAddons = new Dictionary { { "enabledMod", ">1.1" } }, IncompatibleAddons = null, AdditionalCons = null, MainDef = null, @@ -327,12 +336,11 @@ public AutoloadModsProvider(GameEnum gameEnum) StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod DisabledMod => new() + private AutoloadMod DisabledMod => new() { AddonId = new("disabledMod", "1.0"), Type = AddonTypeEnum.Mod, @@ -343,7 +351,7 @@ public AutoloadModsProvider(GameEnum gameEnum) Description = null, SupportedGame = _game, RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), + FileInfo = new(Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), "!!!!!!!!!!NOPE!!!!!!!!!!"), DependentAddons = null, IncompatibleAddons = null, AdditionalCons = null, @@ -352,23 +360,22 @@ public AutoloadModsProvider(GameEnum gameEnum) StartMap = null, PreviewImageHash = null, IsEnabled = false, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod ModThatRequiresFeature => new() + private AutoloadMod ModThatRequiresFeature => new() { - AddonId = new("eduke32mod", "1.0"), + AddonId = new("featureMod", "1.0"), Type = AddonTypeEnum.Mod, - Title = "eduke32mod", + Title = "featureMod", GridImageHash = null, Author = null, ReleaseDate = null, Description = null, SupportedGame = _game, RequiredFeatures = [_feature], - PathToFile = Path.Combine("D:", "Mods", "feature_mod.zip"), + FileInfo = new(Path.Combine("D:", "Mods", "feature_mod.zip"), "addon.json"), DependentAddons = null, IncompatibleAddons = null, AdditionalCons = null, @@ -377,12 +384,35 @@ public AutoloadModsProvider(GameEnum gameEnum) StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; - public AutoloadMod ModForAnotherGame => new() + private AutoloadMod ModThatRequiresUnsupportedFeature => new() + { + AddonId = new("unsupportedFeatureMod", "1.0"), + Type = AddonTypeEnum.Mod, + Title = "unsupportedFeatureMod", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = _game, + RequiredFeatures = [_unsupportedFeature], + FileInfo = new(Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), "!!!!!!!!!!NOPE!!!!!!!!!!"), + DependentAddons = null, + IncompatibleAddons = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + PreviewImageHash = null, + IsEnabled = true, + Executables = null, + Options = null + }; + + private static AutoloadMod ModForAnotherGame => new() { AddonId = new("somegame-mod", "1.0"), Type = AddonTypeEnum.Mod, @@ -393,7 +423,7 @@ public AutoloadModsProvider(GameEnum gameEnum) Description = null, SupportedGame = new(GameEnum.TekWar), RequiredFeatures = null, - PathToFile = Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), + FileInfo = new(Path.Combine("D:", "Mods", "!!!!!!!!!!NOPE!!!!!!!!!!"), "!!!!!!!!!!NOPE!!!!!!!!!!"), DependentAddons = null, IncompatibleAddons = null, AdditionalCons = null, @@ -402,8 +432,23 @@ public AutoloadModsProvider(GameEnum gameEnum) StartMap = null, PreviewImageHash = null, IsEnabled = true, - IsUnpacked = false, Executables = null, Options = null }; + + private List _standardMods => [DisabledMod, MultipleDependenciesMod, IncompatibleWithEnabledMod, IncompatibleModWithIncompatibleVersion, IncompatibleModWithCompatibleVersion, ModThatDependsOnEnabled, DependentModWithCompatibleVersion, DependentModWithIncompatibleVersion, ModForAnotherGame, ModThatRequiresFeature, ModThatRequiresUnsupportedFeature]; + + private List _addonMods => [ModThatRequiresOfficialAddon, ModThatIncompatibleWithAddon]; + + + public List Enabled => [EnabledMod]; + public List StandardMods => [EnabledMod, .._addonMods, .._standardMods]; + + public List StandardModsWithCons => [EnabledModWithCons, .._addonMods, .._standardMods]; + + public List MinimalMods => [EnabledMod, IncompatibleWithEnabledMod]; + + public List AddonMods => [EnabledMod, .._addonMods]; + + public List AddonModsWithCons => [EnabledModWithCons, .._addonMods]; } diff --git a/src/Tests.Unit/Helpers/FileCreationHelper.cs b/src/Tests.Unit/Helpers/FileCreationHelper.cs new file mode 100644 index 00000000..da9739ee --- /dev/null +++ b/src/Tests.Unit/Helpers/FileCreationHelper.cs @@ -0,0 +1,39 @@ +using Core.Client.Helpers; + +namespace Tests.Unit.Helpers; + +public static class FileCreationHelper +{ + /// + /// Creates a temporary folder on disk and returns an pointing to it. + /// The folder is not cleaned up automatically. + /// + /// Receives the created folder path. + public static AddonFilePathWrapper CreateFileInTempDir() + { + var pathToFolder = PathHelper.GetFakePath(); + Directory.CreateDirectory(pathToFolder); + return new(pathToFolder, "addon.json"); + } + + /// + /// Creates a temporary folder with an addon.json manifest file on disk. + /// + /// A tuple of (folderPath, jsonPath). + public static AddonFilePathWrapper CreateAddonManifestInTempFolder(string id, string addonType, string game, string title, string version) + { + var folderPath = PathHelper.GetFakePath(); + Directory.CreateDirectory(folderPath); + var jsonPath = Path.Combine(folderPath, "addon.json"); + File.WriteAllText(jsonPath, $$""" + { + "id": "{{id}}", + "type": "{{addonType}}", + "game": { "name": "{{game}}" }, + "title": "{{title}}", + "version": "{{version}}" + } + """); + return new(folderPath, "addon.json"); + } +} diff --git a/src/Tests.Unit/Helpers/GamesTestHelper.cs b/src/Tests.Unit/Helpers/GamesTestHelper.cs new file mode 100644 index 00000000..56f4113f --- /dev/null +++ b/src/Tests.Unit/Helpers/GamesTestHelper.cs @@ -0,0 +1,25 @@ +// using Core.Client.Interfaces; +// using Games.Games; +// using Games.Providers; +// using Moq; +// +// namespace Tests.Unit.Helpers; +// +// internal sealed class TestInstalledGamesProvider : InstalledGamesProvider +// { +// private readonly IReadOnlyList _games; +// +// public TestInstalledGamesProvider(IReadOnlyList games) +// : base(Mock.Of()) +// { +// _games = games; +// } +// +// public TestInstalledGamesProvider(IConfigProvider config, IReadOnlyList games) +// : base(config) +// { +// _games = games; +// } +// +// public override IReadOnlyList GetGames() => _games; +// } diff --git a/src/Tests.Unit/Helpers/NormalizerHelper.cs b/src/Tests.Unit/Helpers/NormalizerHelper.cs new file mode 100644 index 00000000..7df2610b --- /dev/null +++ b/src/Tests.Unit/Helpers/NormalizerHelper.cs @@ -0,0 +1,33 @@ +using Core.Client.Helpers; + +namespace Tests.Unit.Helpers; + +public static class NormalizerHelper +{ + /// + /// Normalizes path separators in both and + /// to the platform native separator on Linux, so that command-line argument tests pass cross-platform. + /// + internal static void NormalizeExpectedArgs(ref string args, ref string expected) + { + if (OperatingSystem.IsLinux()) + { + args = args.Replace('\\', Path.DirectorySeparatorChar); + expected = expected.Replace('\\', Path.DirectorySeparatorChar); + } + } + /// + /// Converts forward slashes to backslashes on Linux so that paths returned + /// by (which uses native separators) can + /// be compared against hardcoded Windows-style expected values in tests. + /// + internal static string NormalizePath(string path) + { + if (OperatingSystem.IsLinux()) + { + return path.Replace('/', '\\'); + } + + return path; + } +} diff --git a/src/Tests.Unit/Helpers/ObjectCreationHelper.cs b/src/Tests.Unit/Helpers/ObjectCreationHelper.cs new file mode 100644 index 00000000..3a9f0618 --- /dev/null +++ b/src/Tests.Unit/Helpers/ObjectCreationHelper.cs @@ -0,0 +1,50 @@ +using Addons.Providers; +using Core.All; +using Core.Client.Api; +using Core.Client.Cache; +using Core.Client.Helpers; +using Core.Client.Interfaces; +using Games.Games; +using Games.Providers; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; + +namespace Tests.Unit.Helpers; + +public static class ObjectCreationHelper +{ + /// + /// Creates an wired up with a fresh , + /// , and for the given game. + /// The caller must dispose the result. + /// + internal static (InstalledAddonsProvider installedAddonsProvider, LocalFilesProvider localFilesProvider) CreateInstalledAddonsProvider(BaseGame game, IConfigProvider config) + { + Mock> cache = new(); + ChannelBroadcaster channelBroadcaster = new(); + Mock gamesProvider = new(); + gamesProvider.Setup(x => x.GetGames()).Returns([game]); + + var localFilesProvider = new LocalFilesProvider(gamesProvider.Object, cache.Object, channelBroadcaster, NullLogger.Instance); + + var metadataProvider = new MetadataProvider( + localFilesProvider, + new OfflineApiInterface(NullLogger.Instance), + NullLogger.Instance + ); + + var originalCampaignsProvider = new OriginalCampaignsProvider(config); + + InstalledAddonsProvider installedAddonsProvider = new InstalledAddonsProvider( + game, + config, + originalCampaignsProvider, + metadataProvider, + localFilesProvider, + channelBroadcaster, + NullLogger.Instance + ); + + return (installedAddonsProvider, localFilesProvider); + } +} diff --git a/src/Tests.Unit/Helpers/ParsedAddonFileHelper.cs b/src/Tests.Unit/Helpers/ParsedAddonFileHelper.cs new file mode 100644 index 00000000..5c4162b7 --- /dev/null +++ b/src/Tests.Unit/Helpers/ParsedAddonFileHelper.cs @@ -0,0 +1,77 @@ +using Core.All.Enums; +using Core.All.Serializable.Addon; +using Core.Client.Helpers; + +namespace Tests.Unit.Helpers; + +public static class ParsedAddonFileHelper +{ + /// + /// Creates a with the given identity and a dummy file path. + /// + public static ParsedAddonFile CreateParsedAddonFile( + string id, + string title, + string version, + AddonTypeEnum addonType, + GameEnum game = GameEnum.Duke3D + ) + { + return new ParsedAddonFile + { + FileInfo = new AddonFilePathWrapper(PathHelper.GetFakePath(), "addon.json"), + SupportedGame = game, + Manifest = new AddonManifestJsonModel + { + Id = id, + Title = title, + Version = version, + AddonType = addonType, + SupportedGame = new SupportedGameJsonModel { Game = game }, + }, + GridHash = null, + PreviewHash = null, + }; + } + /// + /// Creates a with a Mod type, optional dependencies, and incompatibles. + /// + public static ParsedAddonFile CreateParsedModFile( + string id, + string title, + string version, + GameEnum game = GameEnum.Duke3D, + Dictionary? deps = null, + Dictionary? incompatibles = null) + { + return new ParsedAddonFile + { + FileInfo = new AddonFilePathWrapper(PathHelper.GetFakePath(), "addon.json"), + SupportedGame = game, + Manifest = new AddonManifestJsonModel + { + Id = id, + Title = title, + Version = version, + AddonType = AddonTypeEnum.Mod, + Author = "test author", + Description = "test description", + SupportedGame = new SupportedGameJsonModel { Game = game }, + Dependencies = deps is not null + ? new DependencyJsonModel + { + Addons = deps.Select(d => new DependantAddonJsonModel { Id = d.Key, Version = d.Value }).ToList() + } + : null, + Incompatibles = incompatibles is not null + ? new DependencyJsonModel + { + Addons = incompatibles.Select(d => new DependantAddonJsonModel { Id = d.Key, Version = d.Value }).ToList() + } + : null, + }, + GridHash = null, + PreviewHash = null, + }; + } +} diff --git a/src/Tests.Unit/Helpers/PathHelper.cs b/src/Tests.Unit/Helpers/PathHelper.cs new file mode 100644 index 00000000..4fce4e9c --- /dev/null +++ b/src/Tests.Unit/Helpers/PathHelper.cs @@ -0,0 +1,12 @@ +namespace Tests.Unit.Helpers; + +public static class PathHelper +{ + /// + /// Returns a non-existent fake path for use in test addon files. + /// + public static string GetFakePath() + { + return Path.Combine("TestData", Guid.NewGuid().ToString()); + } +} diff --git a/src/Tests.Unit/Helpers/PortTestSetups.cs b/src/Tests.Unit/Helpers/PortTestSetups.cs new file mode 100644 index 00000000..60544b72 --- /dev/null +++ b/src/Tests.Unit/Helpers/PortTestSetups.cs @@ -0,0 +1,926 @@ +using Addons.Addons; +using Core.All.Enums; +using Core.All.Enums.Addons; +using Core.All.Enums.Versions; +using Core.All.Serializable.Addon; +using Core.Client.Helpers; +using Games.Games; + +namespace Tests.Unit.Helpers; + +internal static class PortTestSetups +{ + internal static (BloodGame game, BloodCampaign baseCamp, BloodCampaign baseCampWithOptions, BloodCampaign cpCamp, BloodCampaign tcCamp, BloodCampaign tcFolderCamp, BloodCampaign tcExeOverride, BloodCampaign tcIncompatibleWithEnabled, BloodCampaign tcIncompatibleWithAll, LooseMap looseMap, AutoloadModsTestSetups mods) Blood() + { + var modsProvider = new AutoloadModsTestSetups(GameEnum.Blood); + + var game = new BloodGame + { + GameInstallFolder = Path.Combine("D:", "Games", "Blood"), + }; + + var baseCamp = new BloodCampaign + { + AddonId = new(nameof(GameEnum.Blood).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Blood", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Blood), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + PreviewImageHash = null, + INI = null, + RFF = null, + SND = null, + Executables = null, + Options = null + }; + + var baseCampWithOptions = new BloodCampaign + { + AddonId = new(nameof(GameEnum.Blood).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Blood", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Blood), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + PreviewImageHash = null, + INI = null, + RFF = null, + SND = null, + Executables = null, + Options = new() { + { "option 1", new() { { "OPT.DEF", OptionalParameterTypeEnum.DEF } } }, + { "option 2", new() { { "OPT2.DEF", OptionalParameterTypeEnum.DEF }, { "OPT2_2.DEF", OptionalParameterTypeEnum.DEF } } }, + } + }; + + var cpCamp = new BloodCampaign + { + AddonId = new(nameof(BloodAddonEnum.BloodCP).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Cryptic Passage", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Blood), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = new Dictionary { { nameof(BloodAddonEnum.BloodCP), null } }, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + PreviewImageHash = null, + INI = null, + RFF = null, + SND = null, + Executables = null, + Options = null + }; + + var tcCamp = new BloodCampaign + { + AddonId = new("blood-tc", null), + Type = AddonTypeEnum.TC, + Title = "Blood TC", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Blood), + RequiredFeatures = null, + FileInfo = new(Path.Combine("D:", "Games", "Blood", "blood_tc.zip"), "addon.json"), + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + PreviewImageHash = null, + INI = "TC.INI", + RFF = "TC.RFF", + SND = "TC.SND", + Executables = null, + Options = null + }; + + var tcFolderCamp = new BloodCampaign + { + AddonId = new("blood-tc-folder", null), + Type = AddonTypeEnum.TC, + Title = "Blood TC", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Blood), + RequiredFeatures = null, + FileInfo = new(Path.Combine("D:", "Games", "Blood", "blood_tc_folder"), "addon.json"), + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + PreviewImageHash = null, + INI = "TC.INI", + RFF = "TC.RFF", + SND = "TC.SND", + Executables = null, + Options = null + }; + + var tcExeOverride = new BloodCampaign + { + AddonId = new("blood-tc-exe-override", null), + Type = AddonTypeEnum.TC, + Title = "Blood TC", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Blood), + RequiredFeatures = null, + FileInfo = new(Path.Combine("D:", "Games", "Blood", "blood_tc_folder"), "addon.json"), + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + PreviewImageHash = null, + INI = "TC.INI", + RFF = "TC.RFF", + SND = "TC.SND", + Executables = new Dictionary> { { OSEnum.Windows, new Dictionary { { PortEnum.NBlood, "nblood.exe" } } } }, + Options = null + }; + + var tcIncompatibleWithEnabled = new BloodCampaign + { + AddonId = new("blood-tc-imcompatible-with-enabled", null), + Type = AddonTypeEnum.TC, + Title = "Blood TC", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Blood), + RequiredFeatures = null, + FileInfo = new(Path.Combine("D:", "Games", "Blood", "blood_tc_folder"), "addon.json"), + DependentAddons = null, + IncompatibleAddons = new Dictionary { { "enabledMod", null } }, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + PreviewImageHash = null, + INI = "TC.INI", + RFF = "TC.RFF", + SND = "TC.SND", + Executables = null, + Options = null + }; + + var tcIncompatibleWithAll = new BloodCampaign + { + AddonId = new("blood-tc-imcompatible-with-everything", null), + Type = AddonTypeEnum.TC, + Title = "Blood TC", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Blood), + RequiredFeatures = null, + FileInfo = new(Path.Combine("D:", "Games", "Blood", "blood_tc_folder"), "addon.json"), + DependentAddons = null, + IncompatibleAddons = new Dictionary { { "*", null } }, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + PreviewImageHash = null, + INI = "TC.INI", + RFF = "TC.RFF", + SND = "TC.SND", + Executables = null, + Options = null + }; + + var looseMap = new LooseMap + { + AddonId = new("loose-map", null), + Type = AddonTypeEnum.Map, + Title = "Loose map", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Blood), + RequiredFeatures = null, + FileInfo = new("Maps", "LOOSE.MAP"), + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = new MapFileJsonModel { File = "LOOSE.MAP" }, + PreviewImageHash = null, + Executables = null, + BloodIni = null, + Options = null + }; + + return (game, baseCamp, baseCampWithOptions, cpCamp, tcCamp, tcFolderCamp, tcExeOverride, tcIncompatibleWithEnabled, tcIncompatibleWithAll, looseMap, modsProvider); + } + + internal static (DukeGame game, DukeCampaign baseCamp, DukeCampaign vacaCamp, DukeCampaign tcCamp, DukeCampaign wtCamp, DukeCampaign duke64Camp, DukeCampaign zhCamp, DukeCampaign dcCamp, DukeCampaign nwCamp, LooseMap looseMap, AutoloadModsTestSetups mods) Duke3D() + { + var modsProvider = new AutoloadModsTestSetups(GameEnum.Duke3D); + + var game = new DukeGame + { + Duke64RomPath = Path.Combine("D:", "Games", "Duke64", "rom.z64"), + DukeZHRomPath = Path.Combine("D:", "Games", "DukeZH", "rom.z64"), + DukeWTInstallPath = Path.Combine("D:", "Games", "DukeWT"), + GameInstallFolder = Path.Combine("D:", "Games", "Duke3D"), + AddonsPaths = new() + { + { DukeAddonEnum.DukeVaca, Path.Combine("D:", "Games", "Duke3D", "Vaca") }, + { DukeAddonEnum.DukeDC, Path.Combine("D:", "Games", "Duke3D", "DC") }, + { DukeAddonEnum.DukeNW, Path.Combine("D:", "Games", "Duke3D", "NW") } + } + }; + + var baseCamp = new DukeCampaign + { + AddonId = new(nameof(GameEnum.Duke3D).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Duke Nukem 3D", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + var wtCamp = new DukeCampaign + { + AddonId = new(nameof(DukeVersionEnum.Duke3D_WT).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Duke Nukem 3D World Tour", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_WT), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + var duke64Camp = new DukeCampaign + { + AddonId = new(nameof(GameEnum.Duke64).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Duke Nukem 64", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Duke64), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + var zhCamp = new DukeCampaign + { + AddonId = new(nameof(GameEnum.DukeZeroHour).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Duke Nukem ZeroHour", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.DukeZeroHour), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + var dcCamp = new DukeCampaign + { + AddonId = new(nameof(DukeAddonEnum.DukeDC).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Duke: DC", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = new Dictionary { { nameof(DukeAddonEnum.DukeDC), null } }, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + var nwCamp = new DukeCampaign + { + AddonId = new(nameof(DukeAddonEnum.DukeNW).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Duke: NW", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = new Dictionary { { nameof(DukeAddonEnum.DukeNW), null } }, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + var vacaCamp = new DukeCampaign + { + AddonId = new("dukevaca", null), + Type = AddonTypeEnum.Official, + Title = "Duke Nukem 3D Caribbean", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = new Dictionary { { nameof(DukeAddonEnum.DukeVaca), null } }, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + var tcCamp = new DukeCampaign + { + AddonId = new("duke-tc", "1.1"), + Type = AddonTypeEnum.TC, + Title = "Duke Nukem 3D TC", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), + RequiredFeatures = null, + FileInfo = new(Path.Combine(Directory.GetCurrentDirectory(), "Data", "Duke3D", "Campaigns"), "duke_tc.zip"), + DependentAddons = new Dictionary { { nameof(DukeAddonEnum.DukeVaca), null } }, + IncompatibleAddons = null, + MainCon = "TC.CON", + AdditionalCons = ["TC1.CON", "TC2.CON"], + MainDef = "TC.DEF", + AdditionalDefs = ["TC1.DEF", "TC2.DEF"], + RTS = "TC.RTS", + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + var looseMap = new LooseMap + { + AddonId = new("loose-map", null), + Type = AddonTypeEnum.Map, + Title = "Loose map", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), + RequiredFeatures = null, + FileInfo = new("Maps", "LOOSE.MAP"), + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = new MapFileJsonModel { File = "LOOSE.MAP" }, + PreviewImageHash = null, + Executables = null, + BloodIni = null, + Options = null + }; + + return (game, baseCamp, vacaCamp, tcCamp, wtCamp, duke64Camp, zhCamp, dcCamp, nwCamp, looseMap, modsProvider); + } + + internal static (NamGame game, DukeCampaign camp, AutoloadModsTestSetups mods) Nam() + { + var modsProvider = new AutoloadModsTestSetups(GameEnum.NAM); + + var game = new NamGame + { + GameInstallFolder = Path.Combine("D:", "Games", "NAM") + }; + + var camp = new DukeCampaign + { + AddonId = new(nameof(GameEnum.NAM).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "NAM", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.NAM), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + return (game, camp, modsProvider); + } + + internal static (RedneckGame game, DukeCampaign redneckCamp, DukeCampaign againCamp, DukeCampaign route66Camp, AutoloadModsTestSetups mods) Redneck() + { + var modsProvider = new AutoloadModsTestSetups(GameEnum.Redneck); + + var game = new RedneckGame + { + GameInstallFolder = Path.Combine("D:", "Games", "Redneck"), + AgainInstallPath = Path.Combine("D:", "Games", "Again"), + }; + + var redneckCamp = new DukeCampaign + { + AddonId = new(nameof(GameEnum.Redneck).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Redneck Rampage", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Redneck), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + var againCamp = new DukeCampaign + { + AddonId = new(nameof(GameEnum.RidesAgain).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Rides Again", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.RidesAgain), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + var route66Camp = new DukeCampaign + { + AddonId = new(nameof(RedneckAddonEnum.Route66).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Redneck Rampage Route 66", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Redneck), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + return (game, redneckCamp, againCamp, route66Camp, modsProvider); + } + + internal static (SlaveGame game, GenericCampaign camp, AutoloadModsTestSetups mods) Slave() + { + var modsProvider = new AutoloadModsTestSetups(GameEnum.Slave); + + var game = new SlaveGame + { + GameInstallFolder = Path.Combine("D:", "Games", "Slave"), + }; + + var camp = new GenericCampaign + { + AddonId = new(nameof(GameEnum.Slave).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Slave", + GridImageHash = null, + PreviewImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Slave), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + Executables = null, + Options = null + }; + + return (game, camp, modsProvider); + } + + internal static (WangGame game, GenericCampaign wangCamp, GenericCampaign tdCamp, LooseMap looseMap, AutoloadModsTestSetups mods) Wang() + { + var modsProvider = new AutoloadModsTestSetups(GameEnum.Wang); + + var game = new WangGame + { + GameInstallFolder = Path.Combine("D:", "Games", "Wang"), + }; + + var wangCamp = new GenericCampaign + { + AddonId = new(nameof(GameEnum.Wang).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Shadow Warrior", + GridImageHash = null, + PreviewImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Wang), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + Executables = null, + Options = null + }; + + var tdCamp = new GenericCampaign + { + AddonId = new(nameof(WangAddonEnum.TwinDragon).ToLower(), null), + Type = AddonTypeEnum.TC, + Title = "Twin Dragon", + GridImageHash = null, + PreviewImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Wang), + RequiredFeatures = null, + FileInfo = new(Path.Combine("D:", "Games", "Wang"), "TD.zip"), + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + Executables = null, + Options = null + }; + + var looseMap = new LooseMap + { + AddonId = new("loose-map", null), + Type = AddonTypeEnum.Map, + Title = "Loose map", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Wang), + RequiredFeatures = null, + FileInfo = new("Maps", "LOOSE.MAP"), + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = new MapFileJsonModel { File = "LOOSE.MAP" }, + PreviewImageHash = null, + Executables = null, + BloodIni = null, + Options = null + }; + + return (game, wangCamp, tdCamp, looseMap, modsProvider); + } + + internal static (WW2GIGame game, DukeCampaign ww2Camp, DukeCampaign platoonCamp, AutoloadModsTestSetups mods) WW2GI() + { + var modsProvider = new AutoloadModsTestSetups(GameEnum.WW2GI); + + var game = new WW2GIGame + { + GameInstallFolder = Path.Combine("D:", "Games", "WW2GI") + }; + + var ww2Camp = new DukeCampaign + { + AddonId = new(nameof(GameEnum.WW2GI).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "World War II GI", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.WW2GI), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + var platoonCamp = new DukeCampaign + { + AddonId = new(nameof(WW2GIAddonEnum.Platoon).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Platoon Leader", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.WW2GI), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + return (game, ww2Camp, platoonCamp, modsProvider); + } + + internal static (FuryGame game, DukeCampaign camp, AutoloadModsTestSetups mods) Fury() + { + var modsProvider = new AutoloadModsTestSetups(GameEnum.Fury); + + var game = new FuryGame + { + GameInstallFolder = Path.Combine("D:", "Games", "Fury") + }; + + var camp = new DukeCampaign + { + AddonId = new(nameof(GameEnum.Fury).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Ion Fury", + GridImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Fury), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + RTS = null, + AdditionalDefs = null, + StartMap = null, + PreviewImageHash = null, + Executables = null, + Options = null + }; + + return (game, camp, modsProvider); + } + + internal static (WitchavenGame game, GenericCampaign camp) Witchaven() + { + var game = new WitchavenGame + { + GameInstallFolder = Path.Combine("D:", "Games", "Witchaven"), + Witchaven2InstallPath = Path.Combine("D:", "Games", "Witchaven", "WH2"), + }; + + var camp = new GenericCampaign + { + AddonId = new(nameof(GameEnum.Witchaven).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "Witchaven", + GridImageHash = null, + PreviewImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.Witchaven), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + Executables = null, + Options = null + }; + + return (game, camp); + } + + internal static (TekWarGame game, GenericCampaign camp) TekWar() + { + var game = new TekWarGame + { + GameInstallFolder = Path.Combine("D:", "Games", "TekWar"), + }; + + var camp = new GenericCampaign + { + AddonId = new(nameof(GameEnum.TekWar).ToLower(), null), + Type = AddonTypeEnum.Official, + Title = "TekWar", + GridImageHash = null, + PreviewImageHash = null, + Author = null, + ReleaseDate = null, + Description = null, + SupportedGame = new(GameEnum.TekWar), + RequiredFeatures = null, + FileInfo = null, + DependentAddons = null, + IncompatibleAddons = null, + MainDef = null, + AdditionalDefs = null, + StartMap = null, + Executables = null, + Options = null + }; + + return (game, camp); + } + + internal static DukeCampaign PackedDukeAddonCampaign() + { + var zipFilePath = Path.Combine(Directory.GetCurrentDirectory(), "Data", "Duke3D", "Campaigns", "packed_campaign.zip"); + + return new DukeCampaign + { + AddonId = new("packed-camp", "1.0"), + Type = AddonTypeEnum.TC, + Title = "Packed Campaign", + SupportedGame = new(GameEnum.Duke3D, DukeVersionEnum.Duke3D_Atomic), + FileInfo = new AddonFilePathWrapper(zipFilePath, "addon.json"), + DependentAddons = null, + IncompatibleAddons = null, + GridImageHash = null, + PreviewImageHash = null, + Description = null, + Author = null, + ReleaseDate = null, + MainCon = null, + AdditionalCons = null, + MainDef = null, + AdditionalDefs = null, + RTS = null, + StartMap = null, + RequiredFeatures = null, + Executables = null, + Options = null, + }; + } +} diff --git a/src/Tests.Unit/ResultTests.cs b/src/Tests.Unit/ResultTests.cs index d7b88ee9..56254306 100644 --- a/src/Tests.Unit/ResultTests.cs +++ b/src/Tests.Unit/ResultTests.cs @@ -45,19 +45,6 @@ public void Default_IsSuccessIsTrue() Assert.True(r.IsSuccess); } - [Fact] - public void Constructor_WithCancelled_IsNotSuccess() - { - var r = new Result(ResultEnum.Cancelled, "cancelled"); - Assert.False(r.IsSuccess); - } - - [Fact] - public void Constructor_WithError_IsNotSuccess() - { - var r = new Result(ResultEnum.Error, "error"); - Assert.False(r.IsSuccess); - } } public sealed class ResultGenericTests diff --git a/src/Tests.Unit/SaveFilesTestHelper.cs b/src/Tests.Unit/SaveFilesTestHelper.cs new file mode 100644 index 00000000..bd6644dc --- /dev/null +++ b/src/Tests.Unit/SaveFilesTestHelper.cs @@ -0,0 +1,60 @@ +using System.Text; +using Addons.Addons; +using Core.All.Enums; +using Games.Games; +using Ports.Ports; +using Ports.Ports.EDuke32; + +namespace Tests.Unit; + +internal sealed class BasePortTestProxy : BasePort +{ + public override PortEnum PortEnum => PortEnum.Stub; + protected override string WinExe => string.Empty; + protected override string LinExe => string.Empty; + public override string Name => string.Empty; + public override List SupportedGames => []; + public override List SupportedFeatures => []; + public override string? InstalledVersion => string.Empty; + public override bool IsSkillSelectionAvailable => false; + protected override string ConfigFile => string.Empty; + protected override string AddDirectoryParam => string.Empty; + protected override string MainGrpParam => string.Empty; + protected override string AddGrpParam => string.Empty; + protected override string AddFileParam => string.Empty; + protected override string AddDefParam => string.Empty; + protected override string AddConParam => string.Empty; + protected override string MainDefParam => string.Empty; + protected override string MainConParam => string.Empty; + protected override string SkillParam => string.Empty; + protected override string AddGameDirParam => string.Empty; + protected override string AddRffParam => string.Empty; + protected override string AddSndParam => string.Empty; + public override void AfterEnd(BaseGame game, BaseAddon campaign) { } + public override void BeforeStart(BaseGame game, BaseAddon campaign) { } + protected override void GetAutoloadModsArgs(StringBuilder sb, BaseGame game, BaseAddon addon, IReadOnlyList mods) { } + protected override void GetSkipIntroParameter(StringBuilder sb) { } + protected override void GetSkipStartupParameter(StringBuilder sb) { } + protected override void GetStartCampaignArgs(StringBuilder sb, BaseGame game, BaseAddon addon) { } + + public void CallMoveSaveFilesFromStorage(BaseGame game, BaseAddon campaign) + => MoveSaveFilesFromStorage(game, campaign); + + public void CallMoveSaveFilesToStorage(BaseGame game, BaseAddon campaign) + => MoveSaveFilesToStorage(game, campaign); + + public string CallGetPathToAddonSavedGamesFolder(string subFolder, string addonId) + => GetPathToAddonSavedGamesFolder(subFolder, addonId); +} + +internal sealed class EDuke32TestProxy : EDuke32 +{ + public void CallMoveSaveFilesFromStorage(BaseGame game, BaseAddon campaign) + => MoveSaveFilesFromStorage(game, campaign); + + public void CallMoveSaveFilesToStorage(BaseGame game, BaseAddon campaign) + => MoveSaveFilesToStorage(game, campaign); + + public string CallGetPathToAddonSavedGamesFolder(string subFolder, string addonId) + => GetPathToAddonSavedGamesFolder(subFolder, addonId); +} diff --git a/src/Tests.Unit/Sync/AddonDropHelperTests.cs b/src/Tests.Unit/Sync/AddonDropHelperTests.cs new file mode 100644 index 00000000..e715e710 --- /dev/null +++ b/src/Tests.Unit/Sync/AddonDropHelperTests.cs @@ -0,0 +1,222 @@ +using System.IO.Compression; +using Addons.Helpers; +using Addons.Providers; +using Core.All; +using Core.Client.Api; +using Core.Client.Cache; +using Core.Client.Helpers; +using Core.Client.Interfaces; +using Games.Games; +using Games.Providers; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Tests.Unit.Helpers; + +namespace Tests.Unit.Sync; + +[Collection("Sync")] +public sealed class AddonDropHelperTests : IDisposable +{ + private readonly BloodGame _game; + private readonly string _addonsFolder; + private readonly string _mapsFolder; + private readonly string _modsFolder; + private readonly AddonDropHelper _helper; + + public AddonDropHelperTests() + { + _game = new BloodGame(); + _addonsFolder = ClientProperties.AddonsFolderPath; + _mapsFolder = _game.MapsFolderPath; + _modsFolder = _game.ModsFolderPath; + + Directory.CreateDirectory(_addonsFolder); + Directory.CreateDirectory(_mapsFolder); + Directory.CreateDirectory(_modsFolder); + + Mock> cache = new(); + ChannelBroadcaster channelPubMock = new(); + Mock gamesProvider = new(); + gamesProvider.Setup(x => x.GetGames()).Returns([]); + LocalFilesProvider scanner = new(gamesProvider.Object, cache.Object, channelPubMock, NullLogger.Instance); + + Mock config = new(); + config.Setup(x => x.DisabledAutoloadMods).Returns([]); + config.Setup(x => x.FavoriteAddons).Returns([]); + + OriginalCampaignsProvider originalCampaignsProvider = new(config.Object); + + MetadataProvider metadataProvider = new( + scanner, + new OfflineApiInterface(NullLogger.Instance), + NullLogger.Instance + ); + + Mock> channelSubMock = new(); + var providerFactory = new InstalledAddonsProviderFactory( + config.Object, + originalCampaignsProvider, + metadataProvider, + scanner, + channelSubMock.Object, + NullLoggerFactory.Instance + ); + + _helper = new AddonDropHelper( + scanner, + NullLogger.Instance + ); + } + + public void Dispose() + { + if (Directory.Exists(_addonsFolder)) + { + Directory.Delete(_addonsFolder, true); + } + } + + private static void CreateZipArchive(string path, string? addonJsonContent = null) + { + using var fileStream = new FileStream(path, FileMode.Create); + using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create); + if (addonJsonContent is not null) + { + var entry = archive.CreateEntry("addon.json", CompressionLevel.NoCompression); + using var writer = new StreamWriter(entry.Open()); + writer.Write(addonJsonContent); + } + } + + [Fact] + public async Task AddAddonsAsync_EmptyList_ReturnsNull() + { + var result = await _helper.AddAddonsAsync([], _game); + + Assert.Null(result); + } + + [Fact] + public async Task AddAddonsAsync_UnsupportedExtension_ReturnsFailedName() + { + var txtFile = Path.Combine(_addonsFolder, "readme.txt"); + await File.WriteAllTextAsync(txtFile, "test"); + + var result = await _helper.AddAddonsAsync([txtFile], _game); + + Assert.NotNull(result); + Assert.Contains("readme.txt", result); + } + + [Fact] + public async Task AddAddonsAsync_ZipFileInstallsSuccessfully_ReturnsNull() + { + var sourceZip = Path.Combine("Files", "ZippedAddon.zip"); + + var result = await _helper.AddAddonsAsync([sourceZip], _game); + + Assert.Null(result); + Assert.True(File.Exists(Path.Combine(_modsFolder, "ZippedAddon.zip"))); + } + + [Fact] + public async Task AddAddonsAsync_MultipleFilesWithFailures_ReturnsOnlyFailedNames() + { + var sourceZip = Path.Combine("Files", "ZippedAddon.zip"); + var txtFile = Path.Combine(_addonsFolder, "readme.txt"); + var txtFile2 = Path.Combine(_addonsFolder, "notes.txt"); + + await File.WriteAllTextAsync(txtFile, "test1"); + await File.WriteAllTextAsync(txtFile2, "test2"); + + var result = await _helper.AddAddonsAsync([sourceZip, txtFile, txtFile2], _game); + + Assert.NotNull(result); + Assert.Equal(2, result.Count); + Assert.Contains("readme.txt", result); + Assert.Contains("notes.txt", result); + + Assert.True(File.Exists(Path.Combine(_modsFolder, "ZippedAddon.zip"))); + Assert.False(File.Exists(Path.Combine(_modsFolder, "readme.txt"))); + Assert.False(File.Exists(Path.Combine(_modsFolder, "notes.txt"))); + } + + [Fact] + public async Task AddAddonsAsync_AllFail_ReturnsAllNames() + { + var txtFile = Path.Combine(_addonsFolder, "readme.txt"); + var txtFile2 = Path.Combine(_addonsFolder, "notes.txt"); + + await File.WriteAllTextAsync(txtFile, "test1"); + await File.WriteAllTextAsync(txtFile2, "test2"); + + var result = await _helper.AddAddonsAsync([txtFile, txtFile2], _game); + + Assert.NotNull(result); + Assert.Equal(2, result.Count); + Assert.Contains("readme.txt", result); + Assert.Contains("notes.txt", result); + + Assert.False(File.Exists(Path.Combine(_modsFolder, "readme.txt"))); + Assert.False(File.Exists(Path.Combine(_modsFolder, "notes.txt"))); + } + + [Fact] + public async Task AddAddonsAsync_MapFileCopiedToMapsFolder_ReturnsNull() + { + var mapFile = Path.Combine(_addonsFolder, "TEST.MAP"); + File.Copy(Path.Combine("Files", "TEST.MAP"), mapFile, true); + + var result = await _helper.AddAddonsAsync([mapFile], _game); + + Assert.Null(result); + Assert.True(File.Exists(Path.Combine(_mapsFolder, "TEST.MAP"))); + } + + [Fact] + public async Task AddAddonsAsync_ArchiveWithNoManifest_ReturnsFailedName() + { + var emptyZip = Path.Combine(_addonsFolder, "empty.zip"); + CreateZipArchive(emptyZip); + + var result = await _helper.AddAddonsAsync([emptyZip], _game); + + Assert.NotNull(result); + Assert.Contains("empty.zip", result); + } + + [Fact] + public async Task AddAddonsAsync_WrongGameAddon_ReturnsFailedName() + { + var wrongGameZip = Path.Combine(_addonsFolder, "wrong_game.zip"); + CreateZipArchive(wrongGameZip, """ + { + "id": "duke-mod", + "type": "Mod", + "game": { "name": "Duke3D" }, + "title": "Duke Mod", + "version": "1.0" + } + """); + + var result = await _helper.AddAddonsAsync([wrongGameZip], _game); + + Assert.NotNull(result); + Assert.Contains("wrong_game.zip", result); + } + + [Fact] + public async Task AddAddonsAsync_DuplicateFile_OverwritesAndSucceeds() + { + var sourceZip = Path.Combine("Files", "ZippedAddon.zip"); + var destZip = Path.Combine(_modsFolder, "ZippedAddon.zip"); + + var firstResult = await _helper.AddAddonsAsync([sourceZip], _game); + Assert.Null(firstResult); + Assert.True(File.Exists(destZip)); + + var secondResult = await _helper.AddAddonsAsync([sourceZip], _game); + Assert.Null(secondResult); + Assert.True(File.Exists(destZip)); + } +} diff --git a/src/Tests.Unit/Sync/AddonFilesTests.cs b/src/Tests.Unit/Sync/AddonFilesTests.cs new file mode 100644 index 00000000..d5956d82 --- /dev/null +++ b/src/Tests.Unit/Sync/AddonFilesTests.cs @@ -0,0 +1,199 @@ +using Addons.Providers; +using Core.All.Enums; +using Core.Client.Helpers; +using Core.Client.Interfaces; +using Games.Games; +using Moq; +using Tests.Unit.Helpers; + +namespace Tests.Unit.Sync; + +[Collection("Sync")] +public sealed class AddonFilesTests : IDisposable +{ + private readonly BloodGame _game = new(); + private readonly string _addonsFolder = ClientProperties.AddonsFolderPath; + private readonly InstalledAddonsProvider _installedAddonsProvider; + private readonly LocalFilesProvider _localFilesProvider; + + public AddonFilesTests() + { + Mock configMock = new(); + configMock.Setup(x => x.DisabledAutoloadMods).Returns([]); + configMock.Setup(x => x.FavoriteAddons).Returns([]); + + (_installedAddonsProvider, _localFilesProvider) = ObjectCreationHelper.CreateInstalledAddonsProvider(_game, configMock.Object); + } + + public void Dispose() + { + _installedAddonsProvider.Dispose(); + if (Directory.Exists(_addonsFolder)) + { + Directory.Delete(_addonsFolder, true); + } + } + + private async Task InitializeDependencies(string? fileName, AddonTypeEnum addonType) + { + string? pathToFile = null; + + if (fileName is not null) + { + var addonFolder = addonType switch + { + AddonTypeEnum.TC => _game.CampaignsFolderPath, + AddonTypeEnum.Map => _game.MapsFolderPath, + AddonTypeEnum.Mod => _game.ModsFolderPath, + _ => _addonsFolder + }; + + pathToFile = Path.Combine(addonFolder, fileName); + Directory.CreateDirectory(addonFolder); + + File.Copy(Path.Combine("Files", fileName), pathToFile, true); + } + + await _localFilesProvider.InitializeAsync(); + + await _installedAddonsProvider.CreateCacheAsync(true, addonType); + + return pathToFile; + } + + [Fact] + public async Task GetAddonFromFile_ZippedAddon_ReturnsParsedAddons() + { + var pathToFile = await InitializeDependencies("ZippedAddon.zip", AddonTypeEnum.Mod); + + var addons = await _localFilesProvider.GetCachedAddonFilesAsync(); + Assert.Equal(2, addons.Count); + + var installedMods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + Assert.Equal(2, installedMods.Count); + + var voxel1 = installedMods.First(m => m.AddonId.Id == "blood-voxel-pack"); + Assert.Equal("p292", voxel1.AddonId.Version); + Assert.Equal("Voxel Pack", voxel1.Title); + Assert.Equal(GameEnum.Blood, voxel1.SupportedGame.GameEnum); + + var voxel2 = installedMods.First(m => m.AddonId.Id == "blood-voxel-pack-2"); + Assert.Equal("p292-2", voxel2.AddonId.Version); + Assert.Equal("Voxel Pack 2", voxel2.Title); + Assert.Equal(GameEnum.Blood, voxel2.SupportedGame.GameEnum); + + Assert.True(File.Exists(pathToFile)); + Assert.False(Directory.Exists(pathToFile.Replace(".zip", ""))); + } + + [Fact] + public async Task Init_GetAddonFromFile_UnpackedAddon_ReturnsParsedAddons() + { + var pathToFile = await InitializeDependencies("UnpackedAddon.zip", AddonTypeEnum.Mod); + + await Task.Delay(100); + + Assert.False(File.Exists(pathToFile)); + Assert.True(Directory.Exists(pathToFile.Replace(".zip", ""))); + + var addons = await _localFilesProvider.GetCachedAddonFilesAsync(); + Assert.Equal(2, addons.Count); + Assert.DoesNotContain(addons, x => x.FileInfo.PathToFile.Contains(".zip")); + + var installedMods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + Assert.Equal(2, installedMods.Count); + + var voxel1 = installedMods.First(m => m.AddonId.Id == "blood-voxel-pack"); + Assert.Equal("p292", voxel1.AddonId.Version); + Assert.Equal("Voxel Pack", voxel1.Title); + Assert.Equal(GameEnum.Blood, voxel1.SupportedGame.GameEnum); + + var voxel2 = installedMods.First(m => m.AddonId.Id == "blood-voxel-pack-2"); + Assert.Equal("p292-2", voxel2.AddonId.Version); + Assert.Equal("Voxel Pack 2", voxel2.Title); + Assert.Equal(GameEnum.Blood, voxel2.SupportedGame.GameEnum); + } + + [Fact] + public async Task AddFile_GetAddonFromFile_UnpackedAddon_ReturnsParsedAddons() + { + _ = await InitializeDependencies(null, AddonTypeEnum.Mod); + + var pathToFile = Path.Combine(_game.ModsFolderPath, "UnpackedAddon.zip"); + Directory.CreateDirectory(_game.ModsFolderPath); + + File.Copy(Path.Combine("Files", "UnpackedAddon.zip"), pathToFile, true); + + await _localFilesProvider.TryAddFileToCacheAsync(pathToFile, _game.GameEnum); + + await Task.Delay(1000); + + var addons = await _localFilesProvider.GetCachedAddonFilesAsync(); + Assert.Equal(2, addons.Count); + + Assert.False(File.Exists(pathToFile)); + Assert.True(Directory.Exists(pathToFile.Replace(".zip", ""))); + Assert.DoesNotContain(addons, x => x.FileInfo.PathToFile.Contains(".zip")); + + var installedMods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + Assert.Equal(2, installedMods.Count); + + var voxel1 = installedMods.First(m => m.AddonId.Id == "blood-voxel-pack"); + Assert.Equal("p292", voxel1.AddonId.Version); + Assert.Equal("Voxel Pack", voxel1.Title); + Assert.Equal(GameEnum.Blood, voxel1.SupportedGame.GameEnum); + + var voxel2 = installedMods.First(m => m.AddonId.Id == "blood-voxel-pack-2"); + Assert.Equal("p292-2", voxel2.AddonId.Version); + Assert.Equal("Voxel Pack 2", voxel2.Title); + Assert.Equal(GameEnum.Blood, voxel2.SupportedGame.GameEnum); + } + + [Fact] + public async Task GetInstalledAddonsByType_LooseMap_ReturnsSingleMap() + { + var pathToFile = await InitializeDependencies("TEST.MAP", AddonTypeEnum.Map); + + var addons = await _localFilesProvider.GetCachedAddonFilesAsync(); + var addon = Assert.Single(addons); + Assert.Equal(GameEnum.Blood, addon.SupportedGame); + + var installedMaps = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Map); + var map = Assert.Single(installedMaps); + + Assert.Equal("TEST.MAP", map.AddonId.Id); + Assert.Null(map.AddonId.Version); + Assert.Equal("TEST.MAP", map.Title); + Assert.Equal(GameEnum.Blood, map.SupportedGame.GameEnum); + + Assert.True(File.Exists(pathToFile)); + } + + [Fact] + public async Task GetCachedAddonFilesAsync_GrpInfoAddon_Extracts() + { + var pathToFile = await InitializeDependencies("GrpInfoAddon.zip", AddonTypeEnum.TC); + + var addons = await _localFilesProvider.GetCachedAddonFilesAsync(); + Assert.Single(addons); + + Assert.False(File.Exists(pathToFile)); + Assert.True(Directory.Exists(pathToFile.Replace(".zip", ""))); + Assert.True(File.Exists(Path.Combine(pathToFile.Replace(".zip", ""), "addons.grpinfo"))); + } + + [Fact] + public async Task CreateCacheAsync_Idempotent_DoesNotDuplicateMods() + { + await InitializeDependencies("ZippedAddon.zip", AddonTypeEnum.Mod); + + var before = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + Assert.Equal(2, before.Count); + + await _installedAddonsProvider.CreateCacheAsync(true, AddonTypeEnum.Mod); + + var after = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + Assert.Equal(2, after.Count); + Assert.Equal(before.Count, after.Count); + } +} diff --git a/src/Tests.Unit/Sync/InstalledAddonsProviderTests.cs b/src/Tests.Unit/Sync/InstalledAddonsProviderTests.cs new file mode 100644 index 00000000..5a2fb656 --- /dev/null +++ b/src/Tests.Unit/Sync/InstalledAddonsProviderTests.cs @@ -0,0 +1,832 @@ +using Addons.Addons; +using Addons.Providers; +using Core.All; +using Core.All.Enums; +using Core.All.Serializable.Addon; +using Core.Client.Helpers; +using Core.Client.Interfaces; +using Games.Games; +using Moq; +using Tests.Unit.Helpers; +using StandaloneGame = Games.Games.StandaloneGame; + +namespace Tests.Unit.Sync; + +[Collection("Sync")] +public sealed class InstalledAddonsProviderTests : IDisposable +{ + private readonly DukeGame _game; + private readonly Mock _configMock; + private readonly InstalledAddonsProvider _installedAddonsProvider; + private readonly LocalFilesProvider _localFilesProvider; + private readonly HashSet _disabledMods; + private readonly HashSet _favorites; + + public InstalledAddonsProviderTests() + { + _game = new DukeGame + { + Duke64RomPath = null, + DukeZHRomPath = null, + DukeWTInstallPath = null, + }; + _game.GameInstallFolder = null; + + _disabledMods = []; + _favorites = []; + _configMock = new Mock(); + _configMock.Setup(x => x.DisabledAutoloadMods).Returns(_disabledMods); + _configMock.Setup(x => x.FavoriteAddons).Returns(_favorites); + + (_installedAddonsProvider, _localFilesProvider) = ObjectCreationHelper.CreateInstalledAddonsProvider(_game, _configMock.Object); + } + + public void Dispose() + { + _installedAddonsProvider.Dispose(); + } + + [Fact] + public void AddAddon_Campaign_AddsToCampaignsCache() + { + var parsed = ParsedAddonFileHelper.CreateParsedAddonFile("test-camp", "Test Campaign", "1.0", AddonTypeEnum.TC); + + _installedAddonsProvider.AddAddon(parsed); + + var campaigns = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.TC); + Assert.Contains(campaigns, c => c.AddonId.Id == "test-camp"); + } + + [Fact] + public void AddAddon_Map_AddsToMapsCache() + { + var parsed = ParsedAddonFileHelper.CreateParsedAddonFile("test-map", "Test Map", "1.0", AddonTypeEnum.Map); + + _installedAddonsProvider.AddAddon(parsed); + + var maps = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Map); + Assert.Contains(maps, m => m.AddonId.Id == "test-map"); + } + + [Fact] + public void AddAddon_Mod_AddsToModsCache() + { + var parsed = ParsedAddonFileHelper.CreateParsedModFile("test-mod", "Test Mod", "1.0"); + + _installedAddonsProvider.AddAddon(parsed); + + var mods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + Assert.Contains(mods, m => m.AddonId.Id == "test-mod"); + } + + [Fact] + public void AddAddon_ModIsEnabledByDefault_WhenNotInDisabledList() + { + var parsed = ParsedAddonFileHelper.CreateParsedModFile("test-mod", "Test Mod", "1.0"); + + _installedAddonsProvider.AddAddon(parsed); + + var mods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + var mod = mods.OfType().First(m => m.AddonId.Id == "test-mod"); + Assert.True(mod.IsEnabled); + } + + [Fact] + public void AddAddon_ModIsDisabled_WhenInDisabledList() + { + _disabledMods.Add("test-mod"); + var parsed = ParsedAddonFileHelper.CreateParsedModFile("test-mod", "Test Mod", "1.0"); + + _installedAddonsProvider.AddAddon(parsed); + + var mods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + var mod = mods.OfType().First(m => m.AddonId.Id == "test-mod"); + Assert.False(mod.IsEnabled); + } + + [Fact] + public void AddAddon_ModWithMainDef_Throws() + { + var parsed = ParsedAddonFileHelper.CreateParsedModFile("test-mod", "Test Mod", "1.0"); + parsed.Manifest.MainDef = "GAME.CON"; + + Assert.Throws(() => _installedAddonsProvider.AddAddon(parsed)); + } + + [Fact] + public void AddAddon_FiresAddonsChangedEvent() + { + var parsed = ParsedAddonFileHelper.CreateParsedAddonFile("test", "Test", "1.0", AddonTypeEnum.TC); + GameEnum? firedGame = null; + AddonTypeEnum? firedType = null; + _installedAddonsProvider.AddonsChangedEvent += (g, t) => { firedGame = g; firedType = t; }; + + _installedAddonsProvider.AddAddon(parsed); + + Assert.Equal(GameEnum.Duke3D, firedGame); + Assert.Equal(AddonTypeEnum.TC, firedType); + } + + [Fact] + public void AddAddon_AddsFavorite_WhenInFavoritesList() + { + AddonId favId = new("fav-camp", "1.0"); + _favorites.Add(favId); + var parsed = ParsedAddonFileHelper.CreateParsedAddonFile("fav-camp", "Fav", "1.0", AddonTypeEnum.TC); + + _installedAddonsProvider.AddAddon(parsed); + + var campaigns = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.TC); + var camp = campaigns.First(c => c.AddonId.Id == "fav-camp"); + Assert.True(camp.IsFavorite); + } + + [Fact] + public void GetInstalledAddonsByType_UnknownType_Throws() + { + Assert.Throws(() => + _installedAddonsProvider.GetInstalledAddonsByType((AddonTypeEnum)99)); + } + + [Fact] + public void GetInstalledCampaigns_IncludesCustomCampaigns() + { + var parsed = ParsedAddonFileHelper.CreateParsedAddonFile("custom", "ZZ Custom", "1.0", AddonTypeEnum.TC); + _installedAddonsProvider.AddAddon(parsed); + + var campaigns = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.TC); + + Assert.Contains(campaigns, c => c.AddonId.Id == "custom"); + } + + [Fact] + public void GetInstalledCampaigns_CustomCampaignsAreSortedByTitle() + { + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedAddonFile("camp-b", "B Camp", "1.0", AddonTypeEnum.TC)); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedAddonFile("camp-a", "A Camp", "1.0", AddonTypeEnum.TC)); + + var campaigns = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.TC); + var titles = campaigns.Where(c => c.AddonId.Id is "camp-a" or "camp-b") + .Select(c => c.Title) + .ToList(); + + Assert.Equal(["A Camp", "B Camp"], titles); + } + + [Fact] + public void EnableAddon_NonExistentMod_DoesNothing() + { + _installedAddonsProvider.EnableAddon(new AddonId("ghost", "1.0")); + + _configMock.Verify(x => x.ChangeModState(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void EnableAddon_AlreadyEnabled_DoesNothing() + { + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("enabled-mod", "Enabled", "1.0")); + + _installedAddonsProvider.EnableAddon(new AddonId("enabled-mod", "1.0")); + + _configMock.Verify(x => x.ChangeModState(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void EnableAddon_DisabledMod_EnablesIt() + { + _disabledMods.Add("disabled-mod"); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("disabled-mod", "Disabled", "1.0")); + + _installedAddonsProvider.EnableAddon(new AddonId("disabled-mod", "1.0")); + + _configMock.Verify(x => x.ChangeModState(new AddonId("disabled-mod", "1.0"), true), Times.Once); + var mods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + var mod = mods.OfType().First(m => m.AddonId.Id == "disabled-mod"); + Assert.True(mod.IsEnabled); + } + + [Fact] + public void DisableAddon_NonExistentMod_DoesNothing() + { + _installedAddonsProvider.DisableAddon(new AddonId("ghost", "1.0")); + + _configMock.Verify(x => x.ChangeModState(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void DisableAddon_AlreadyDisabled_DoesNothing() + { + _disabledMods.Add("already-disabled"); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("already-disabled", "Disabled", "1.0")); + + _installedAddonsProvider.DisableAddon(new AddonId("already-disabled", "1.0")); + + _configMock.Verify(x => x.ChangeModState(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void DisableAddon_EnabledMod_DisablesIt() + { + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("enabled-mod", "Enabled", "1.0")); + + _installedAddonsProvider.DisableAddon(new AddonId("enabled-mod", "1.0")); + + _configMock.Verify(x => x.ChangeModState(new AddonId("enabled-mod", "1.0"), false), Times.Once); + var mods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + var mod = mods.OfType().First(m => m.AddonId.Id == "enabled-mod"); + Assert.False(mod.IsEnabled); + } + + [Fact] + public void EnableAddon_CascadesToDependencies() + { + _disabledMods.Add("dep-mod"); + _disabledMods.Add("main-mod"); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("main-mod", "Main", "1.0", + deps: new Dictionary { ["dep-mod"] = null })); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("dep-mod", "Dep", "1.0")); + + _installedAddonsProvider.EnableAddon(new AddonId("main-mod", "1.0")); + + _configMock.Verify(x => x.ChangeModState(new AddonId("main-mod", "1.0"), true), Times.Once); + _configMock.Verify(x => x.ChangeModState(new AddonId("dep-mod", "1.0"), true), Times.Once); + } + + [Fact] + public void EnableAddon_DisablesIncompatibleMods() + { + _disabledMods.Add("main-mod"); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("main-mod", "Main", "1.0", + incompatibles: new Dictionary { ["incompat-mod"] = null })); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("incompat-mod", "Incompat", "1.0")); + + _installedAddonsProvider.EnableAddon(new AddonId("main-mod", "1.0")); + + _configMock.Verify(x => x.ChangeModState(new AddonId("incompat-mod", "1.0"), false), Times.Once); + var mods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + var incompMod = mods.OfType().First(m => m.AddonId.Id == "incompat-mod"); + Assert.False(incompMod.IsEnabled); + } + + [Fact] + public void DisableAddon_CascadesToDependants() + { + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("dep-mod", "Dep", "1.0", + deps: new Dictionary { ["main-mod"] = null })); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("main-mod", "Main", "1.0")); + + _installedAddonsProvider.DisableAddon(new AddonId("main-mod", "1.0")); + + _configMock.Verify(x => x.ChangeModState(new AddonId("main-mod", "1.0"), false), Times.AtLeastOnce); + var mods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + var depMod = mods.OfType().First(m => m.AddonId.Id == "dep-mod"); + Assert.False(depMod.IsEnabled); + } + + [Fact] + public void EnableAddon_DisablesOtherVersionsOfSameMod() + { + _disabledMods.Add("my-mod"); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("my-mod", "MyMod v1", "1.0")); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("my-mod", "MyMod v2", "2.0")); + + _installedAddonsProvider.EnableAddon(new AddonId("my-mod", "1.0")); + + var mods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + var v1 = mods.OfType().First(m => m.AddonId.Version == "1.0"); + var v2 = mods.OfType().First(m => m.AddonId.Version == "2.0"); + Assert.True(v1.IsEnabled); + Assert.False(v2.IsEnabled); + } + + [Fact] + public void GetInstalledAddonsByType_Maps_ReturnsEmptyList_WhenNoMaps() + { + var maps = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Map); + + Assert.Empty(maps); + } + + [Fact] + public void GetInstalledAddonsByType_Mods_ReturnsEmptyList_WhenNoMods() + { + var mods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + + Assert.Empty(mods); + } + + [Fact] + public async Task FileRemovedEvent_CampaignAddon_RemovesFromCache() + { + await _localFilesProvider.InitializeAsync().ConfigureAwait(false); + + var fileInfo = FileCreationHelper.CreateAddonManifestInTempFolder("del-camp", "TC", "Duke3D", "Del Camp", "1.0"); + var addResult = await _localFilesProvider.TryAddFileToCacheAsync(fileInfo.PathToFile, null).ConfigureAwait(false); + Assert.NotNull(addResult); + Assert.NotEmpty(addResult); + + await Task.Delay(100); + var before = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.TC); + Assert.Contains(before, c => c.AddonId.Id == "del-camp"); + + await _localFilesProvider.TryRemoveFileFromCacheAsync(fileInfo.PathToFolder); + + await Task.Delay(100); + var after = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.TC); + Assert.DoesNotContain(after, c => c.AddonId.Id == "del-camp"); + } + + [Fact] + public async Task FileRemovedEvent_ModAddon_RemovesFromCache() + { + await _localFilesProvider.InitializeAsync().ConfigureAwait(false); + + var fileInfo = FileCreationHelper.CreateAddonManifestInTempFolder("del-mod", "Mod", "Duke3D", "Del Mod", "1.0"); + var addResult = await _localFilesProvider.TryAddFileToCacheAsync(fileInfo.PathToFile, null).ConfigureAwait(false); + Assert.NotNull(addResult); + Assert.NotEmpty(addResult); + + await Task.Delay(100); + var before = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + Assert.Contains(before, m => m.AddonId.Id == "del-mod"); + + await _localFilesProvider.TryRemoveFileFromCacheAsync(fileInfo.PathToFolder); + + await Task.Delay(100); + var after = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + Assert.DoesNotContain(after, m => m.AddonId.Id == "del-mod"); + } + + [Fact] + public async Task FileRemovedEvent_Map_RemovesFromCache() + { + await _localFilesProvider.InitializeAsync().ConfigureAwait(false); + + var fileInfo = FileCreationHelper.CreateAddonManifestInTempFolder("del-map", "Map", "Duke3D", "Del Map", "1.0"); + var addResult = await _localFilesProvider.TryAddFileToCacheAsync(fileInfo.PathToFile, null).ConfigureAwait(false); + Assert.NotNull(addResult); + Assert.NotEmpty(addResult); + + await Task.Delay(100); + var before = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Map); + Assert.Contains(before, m => m.AddonId.Id == "del-map"); + + await _localFilesProvider.TryRemoveFileFromCacheAsync(fileInfo.PathToFolder); + + await Task.Delay(100); + var after = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Map); + Assert.DoesNotContain(after, m => m.AddonId.Id == "del-map"); + } + + [Fact] + public void FileRemovedEvent_FiresAddonsChangedEvent() + { + var fileInfo = FileCreationHelper.CreateFileInTempDir(); + var parsed = ParsedAddonFileHelper.CreateParsedAddonFile("del-camp", "Del", "1.0", AddonTypeEnum.TC) with { FileInfo = fileInfo }; + + var jsonPath = Path.Combine(fileInfo.PathToFolder, "addon.json"); + File.WriteAllText(jsonPath, """ + { + "id": "del-camp", + "type": "TC", + "game": { "name": "Duke3D" }, + "title": "Del", + "version": "1.0" + } + """); + + GameEnum? firedGame = null; + AddonTypeEnum? firedType = null; + _installedAddonsProvider.AddonsChangedEvent += (g, t) => { firedGame = g; firedType = t; }; + + _installedAddonsProvider.AddAddon(parsed); + _installedAddonsProvider.DeleteAddon(parsed); + + Assert.Equal(GameEnum.Duke3D, firedGame); + Assert.Equal(AddonTypeEnum.TC, firedType); + } + + [Fact] + public void GetAddonFromFile_JsonManifest_ReturnsDukeCampaign() + { + var parsed = ParsedAddonFileHelper.CreateParsedAddonFile("duke-camp", "Duke Camp", "1.0", AddonTypeEnum.TC); + + var addon = _installedAddonsProvider.GetAddonFromFile(parsed); + + Assert.NotNull(addon); + Assert.IsType(addon); + Assert.Equal("duke-camp", addon.AddonId.Id); + Assert.Equal("1.0", addon.AddonId.Version); + Assert.Equal("Duke Camp", addon.Title); + Assert.Equal(AddonTypeEnum.TC, addon.Type); + } + + [Fact] + public void GetAddonFromFile_JsonManifestWithDependencies_AddsThem() + { + var deps = new Dictionary { ["dep-addon"] = "1.0" }; + var incomps = new Dictionary { ["bad-addon"] = "2.0" }; + var parsed = ParsedAddonFileHelper.CreateParsedModFile("test-mod", "Test Mod", "1.0", deps: deps, incompatibles: incomps); + + var addon = _installedAddonsProvider.GetAddonFromFile(parsed); + + Assert.NotNull(addon); + Assert.NotNull(addon.DependentAddons); + Assert.True(addon.DependentAddons.ContainsKey("dep-addon")); + Assert.NotNull(addon.IncompatibleAddons); + Assert.True(addon.IncompatibleAddons.ContainsKey("bad-addon")); + } + + [Fact] + public void GetAddonFromFile_JsonManifestWithExecutables_AddsThem() + { + var folder = PathHelper.GetFakePath(); + var parsed = new ParsedAddonFile + { + FileInfo = new AddonFilePathWrapper(folder, "addon.json"), + SupportedGame = _game.GameEnum, + Manifest = new AddonManifestJsonModel + { + Id = "exe-addon", + Title = "Executable Addon", + Version = "1.0", + AddonType = AddonTypeEnum.TC, + SupportedGame = new SupportedGameJsonModel { Game = GameEnum.Duke3D }, + Executables = new Dictionary> + { + [OSEnum.Windows] = new() + { + [PortEnum.EDuke32] = "custom.exe" + } + } + }, + GridHash = null, + PreviewHash = null, + }; + + var addon = _installedAddonsProvider.GetAddonFromFile(parsed); + + Assert.NotNull(addon); + Assert.NotNull(addon.Executables); + Assert.Contains(OSEnum.Windows, addon.Executables); + Assert.Contains(PortEnum.EDuke32, addon.Executables[OSEnum.Windows]); + Assert.EndsWith("custom.exe", addon.Executables[OSEnum.Windows][PortEnum.EDuke32]); + } + + [Fact] + public void GetAddonFromFile_JsonManifestWithOptions_AddsThem() + { + var parsed = new ParsedAddonFile + { + FileInfo = new AddonFilePathWrapper(PathHelper.GetFakePath(), "addon.json"), + SupportedGame = _game.GameEnum, + Manifest = new AddonManifestJsonModel + { + Id = "opt-addon", + Title = "Options Addon", + Version = "1.0", + AddonType = AddonTypeEnum.TC, + SupportedGame = new SupportedGameJsonModel { Game = GameEnum.Duke3D }, + Options = new List + { + new() + { + OptionName = "opt1", + Parameters = new Dictionary + { + ["test.def"] = OptionalParameterTypeEnum.DEF + } + } + } + }, + GridHash = null, + PreviewHash = null, + }; + + var addon = _installedAddonsProvider.GetAddonFromFile(parsed); + + Assert.NotNull(addon); + Assert.NotNull(addon.Options); + Assert.Contains("opt1", addon.Options); + Assert.Contains("test.def", addon.Options["opt1"]); + Assert.Equal(OptionalParameterTypeEnum.DEF, addon.Options["opt1"]["test.def"]); + } + + [Fact] + public void GetAddonFromFile_JsonManifestWithAddCons_AddsThem() + { + var parsed = new ParsedAddonFile + { + FileInfo = new AddonFilePathWrapper(PathHelper.GetFakePath(), "addon.json"), + SupportedGame = _game.GameEnum, + Manifest = new AddonManifestJsonModel + { + Id = "con-addon", + Title = "Con Addon", + Version = "1.0", + AddonType = AddonTypeEnum.TC, + SupportedGame = new SupportedGameJsonModel { Game = GameEnum.Duke3D }, + AdditionalCons = ["extra.con", "more.con"], + }, + GridHash = null, + PreviewHash = null, + }; + + var addon = _installedAddonsProvider.GetAddonFromFile(parsed); + + Assert.NotNull(addon); + var duke = Assert.IsType(addon); + Assert.NotNull(duke.AdditionalCons); + Assert.Contains("extra.con", duke.AdditionalCons); + Assert.Contains("more.con", duke.AdditionalCons); + } + + [Fact] + public void GetAddonFromFile_JsonManifest_FallsBackToPreviewForGrid() + { + var parsed = new ParsedAddonFile + { + FileInfo = new AddonFilePathWrapper(PathHelper.GetFakePath(), "addon.json"), + SupportedGame = _game.GameEnum, + Manifest = new AddonManifestJsonModel + { + Id = "prev-addon", + Title = "Preview Addon", + Version = "1.0", + AddonType = AddonTypeEnum.TC, + SupportedGame = new SupportedGameJsonModel { Game = GameEnum.Duke3D }, + }, + GridHash = null, + PreviewHash = 67890, + }; + + var addon = _installedAddonsProvider.GetAddonFromFile(parsed); + + Assert.NotNull(addon); + Assert.Equal(67890, addon.GridImageHash); + Assert.Equal(67890, addon.PreviewImageHash); + } + + [Fact] + public void GetAddonFromFile_NotJsonNotMapNotZip_ReturnsNull() + { + var parsed = new ParsedAddonFile + { + FileInfo = new AddonFilePathWrapper(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()), "addon.xyz"), + SupportedGame = _game.GameEnum, + Manifest = new AddonManifestJsonModel + { + Id = "bad", + Title = "Bad", + Version = "1.0", + AddonType = AddonTypeEnum.TC, + SupportedGame = new SupportedGameJsonModel { Game = GameEnum.Duke3D }, + }, + GridHash = null, + PreviewHash = null, + }; + + var addon = _installedAddonsProvider.GetAddonFromFile(parsed); + + Assert.Null(addon); + } + + [Fact] + public void GetInstalledCampaigns_WangCustomOrder_TwinDragonFirst() + { + WangGame wangGame = new(); + var (wangProvider, _) = ObjectCreationHelper.CreateInstalledAddonsProvider(wangGame, _configMock.Object); + + wangProvider.AddAddon(ParsedAddonFileHelper.CreateParsedAddonFile("Wanton", "Wanton", "1.0", AddonTypeEnum.TC, GameEnum.Wang)); + wangProvider.AddAddon(ParsedAddonFileHelper.CreateParsedAddonFile("TwinDragon", "TwinDragon", "1.0", AddonTypeEnum.TC, GameEnum.Wang)); + wangProvider.AddAddon(ParsedAddonFileHelper.CreateParsedAddonFile("Other", "Aaa Other", "1.0", AddonTypeEnum.TC, GameEnum.Wang)); + + var wangCampaigns = wangProvider.GetInstalledAddonsByType(AddonTypeEnum.TC) + .Where(c => c.SupportedGame.GameEnum == GameEnum.Wang) + .ToList(); + + var tdIndex = wangCampaigns.FindIndex(c => c.AddonId.Id == "TwinDragon"); + var wantonIndex = wangCampaigns.FindIndex(c => c.AddonId.Id == "Wanton"); + var otherIndex = wangCampaigns.FindIndex(c => c.AddonId.Id == "Other"); + + Assert.True(tdIndex >= 0 && wantonIndex >= 0 && otherIndex >= 0); + Assert.True(tdIndex < otherIndex, "TwinDragon should appear before other campaigns"); + Assert.True(wantonIndex < otherIndex, "Wanton should appear before other campaigns"); + } + + [Fact] + public void AddAddon_OfficialType_Throws() + { + var parsed = ParsedAddonFileHelper.CreateParsedAddonFile("official", "Official", "1.0", AddonTypeEnum.Official); + + Assert.Throws(() => _installedAddonsProvider.AddAddon(parsed)); + } + + [Fact] + public void GetAddonFromFile_NullManifest_Throws() + { + var parsed = new ParsedAddonFile + { + FileInfo = new AddonFilePathWrapper(PathHelper.GetFakePath(), "addon.json"), + SupportedGame = _game.GameEnum, + Manifest = null, + GridHash = null, + PreviewHash = null, + }; + + Assert.Throws(() => _installedAddonsProvider.GetAddonFromFile(parsed)); + } + + [Fact] + public void GetAddonFromFile_BloodGame_ReturnsBloodCampaign() + { + BloodGame bloodGame = new(); + var (bloodProvider, _) = ObjectCreationHelper.CreateInstalledAddonsProvider(bloodGame, _configMock.Object); + + var parsed = ParsedAddonFileHelper.CreateParsedAddonFile("blood-camp", "Blood Camp", "1.0", AddonTypeEnum.TC, GameEnum.Blood); + bloodProvider.AddAddon(parsed); + + var campaigns = bloodProvider.GetInstalledAddonsByType(AddonTypeEnum.TC); + var addon = campaigns.First(c => c.AddonId.Id == "blood-camp"); + + Assert.IsType(addon); + } + + [Fact] + public void GetAddonFromFile_StandaloneGame_ReturnsStandaloneGame() + { + var parsed = ParsedAddonFileHelper.CreateParsedAddonFile("standalone", "Standalone", "1.0", AddonTypeEnum.TC, GameEnum.Standalone); + StandaloneGame standaloneGame = new(); + var (standaloneProvider, _) = ObjectCreationHelper.CreateInstalledAddonsProvider(standaloneGame, _configMock.Object); + + standaloneProvider.AddAddon(parsed); + + var campaigns = standaloneProvider.GetInstalledAddonsByType(AddonTypeEnum.TC); + var addon = campaigns.First(c => c.AddonId.Id == "standalone"); + + Assert.IsType(addon); + } + + [Fact] + public void GetAddonFromFile_SlaveGame_ReturnsGenericCampaign() + { + var parsed = ParsedAddonFileHelper.CreateParsedAddonFile("slave-camp", "Slave Camp", "1.0", AddonTypeEnum.TC, GameEnum.Slave); + var slaveGame = new SlaveGame(); + var (slaveProvider, _) = ObjectCreationHelper.CreateInstalledAddonsProvider(slaveGame, _configMock.Object); + + slaveProvider.AddAddon(parsed); + + var campaigns = slaveProvider.GetInstalledAddonsByType(AddonTypeEnum.TC); + var addon = campaigns.First(c => c.AddonId.Id == "slave-camp"); + + Assert.IsType(addon); + } + + [Fact] + public void EnableAddon_WithMissingDependency_DoesNotThrow() + { + _disabledMods.Add("main-mod"); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("main-mod", "Main", "1.0", + deps: new Dictionary { ["missing-dep"] = null })); + + _installedAddonsProvider.EnableAddon(new AddonId("main-mod", "1.0")); + + _configMock.Verify(x => x.ChangeModState(new AddonId("main-mod", "1.0"), true), Times.Once); + } + + [Fact] + public void DisableAddon_TransitiveCascade_DisablesGrandchild() + { + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("leaf-mod", "Leaf", "1.0", + deps: new Dictionary { ["mid-mod"] = null })); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("mid-mod", "Mid", "1.0", + deps: new Dictionary { ["root-mod"] = null })); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("root-mod", "Root", "1.0")); + + _installedAddonsProvider.DisableAddon(new AddonId("root-mod", "1.0")); + + var mods = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Mod); + Assert.False(mods.OfType().First(m => m.AddonId.Id == "root-mod").IsEnabled); + Assert.False(mods.OfType().First(m => m.AddonId.Id == "mid-mod").IsEnabled); + Assert.False(mods.OfType().First(m => m.AddonId.Id == "leaf-mod").IsEnabled); + } + + [Fact] + public void AddAddon_LooseMapWithBloodIni_DetectsIniFile() + { + var mapsDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(mapsDir); + + try + { + var mapFile = Path.Combine(mapsDir, "TEST.MAP"); + var iniFile = Path.Combine(mapsDir, "TEST.INI"); + File.WriteAllText(mapFile, ""); + File.WriteAllText(iniFile, ""); + + var fileInfo = new AddonFilePathWrapper(mapsDir, "TEST.MAP"); + var parsed = new ParsedAddonFile + { + FileInfo = fileInfo, + SupportedGame = GameEnum.Duke3D, + Manifest = null, + GridHash = null, + PreviewHash = null, + }; + + _installedAddonsProvider.AddAddon(parsed); + + var maps = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Map); + var map = Assert.Single(maps); + var looseMap = Assert.IsType(map); + Assert.Equal("TEST.ini", looseMap.BloodIni, StringComparer.OrdinalIgnoreCase); + } + finally + { + if (Directory.Exists(mapsDir)) + { + Directory.Delete(mapsDir, true); + } + } + } + + [Fact] + public void AddAddon_LooseMapWithoutBloodIni_DoesNotSetIni() + { + var mapsDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(mapsDir); + + try + { + var mapFile = Path.Combine(mapsDir, "TEST2.MAP"); + File.WriteAllText(mapFile, ""); + + var fileInfo = new AddonFilePathWrapper(mapsDir, "TEST2.MAP"); + var parsed = new ParsedAddonFile + { + FileInfo = fileInfo, + SupportedGame = GameEnum.Duke3D, + Manifest = null, + GridHash = null, + PreviewHash = null, + }; + + _installedAddonsProvider.AddAddon(parsed); + + var maps = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.Map); + var map = Assert.Single(maps); + var looseMap = Assert.IsType(map); + Assert.Null(looseMap.BloodIni); + } + finally + { + if (Directory.Exists(mapsDir)) + { + Directory.Delete(mapsDir, true); + } + } + } + + [Fact] + public void EnableAddon_ModWithNullFileInfo_DoesNotThrow() + { + _disabledMods.Add("null-file-mod"); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("null-file-mod", "Null File", "1.0")); + _installedAddonsProvider.AddAddon(ParsedAddonFileHelper.CreateParsedModFile("other-mod", "Other", "1.0")); + + _installedAddonsProvider.EnableAddon(new AddonId("null-file-mod", "1.0")); + } + + [Fact] + public void AddAddon_SameCampaignTwice_ReplacesEntry() + { + var parsed = ParsedAddonFileHelper.CreateParsedAddonFile("dup-camp", "Original", "1.0", AddonTypeEnum.TC); + _installedAddonsProvider.AddAddon(parsed); + + var parsed2 = ParsedAddonFileHelper.CreateParsedAddonFile("dup-camp", "Updated", "1.0", AddonTypeEnum.TC); + _installedAddonsProvider.AddAddon(parsed2); + + var campaigns = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.TC); + Assert.Single(campaigns.Where(c => c.AddonId.Id == "dup-camp")); + Assert.Equal("Updated", campaigns.First(c => c.AddonId.Id == "dup-camp").Title); + } + + [Fact] + public async Task FileRemovedEvent_NonExistentFile_DoesNotThrow() + { + await _localFilesProvider.InitializeAsync().ConfigureAwait(false); + + var fileInfo = FileCreationHelper.CreateAddonManifestInTempFolder("ghost", "TC", "Duke3D", "Ghost", "1.0"); + var addResult = await _localFilesProvider.TryAddFileToCacheAsync(fileInfo.PathToFile, null).ConfigureAwait(false); + Assert.NotNull(addResult); + Assert.NotEmpty(addResult); + + File.Delete(fileInfo.PathToFile); + + await _localFilesProvider.TryRemoveFileFromCacheAsync(fileInfo.PathToFolder); + + await Task.Delay(100); + var after = _installedAddonsProvider.GetInstalledAddonsByType(AddonTypeEnum.TC); + Assert.DoesNotContain(after, c => c.AddonId.Id == "ghost"); + } +} diff --git a/src/Tests.Unit/Sync/LocalFilesProviderTests.cs b/src/Tests.Unit/Sync/LocalFilesProviderTests.cs new file mode 100644 index 00000000..8417a538 --- /dev/null +++ b/src/Tests.Unit/Sync/LocalFilesProviderTests.cs @@ -0,0 +1,366 @@ +using Addons.Providers; +using Core.All; +using Core.Client.Cache; +using Core.Client.Helpers; +using Games.Providers; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using SharpCompress.Archives; +using Tests.Unit.Helpers; + +namespace Tests.Unit.Sync; + +[Collection("Sync")] +public sealed class LocalFilesProviderTests : IDisposable +{ + private readonly string _addonsFolder; + + public LocalFilesProviderTests() + { + _addonsFolder = ClientProperties.AddonsFolderPath; + Directory.CreateDirectory(_addonsFolder); + File.Copy( + Path.Combine("Files", "ZippedAddon.zip"), + Path.Combine(_addonsFolder, "ZippedAddon.zip"), + true + ); + + var unpackToFolder = Path.Combine(_addonsFolder, "UnpackedAddon"); + Directory.CreateDirectory(unpackToFolder); + using var archive = ArchiveFactory.OpenArchive(Path.Combine("Files", "UnpackedAddon.zip")); + archive.WriteToDirectory(unpackToFolder); + } + + public void Dispose() + { + if (Directory.Exists(_addonsFolder)) + { + Directory.Delete(_addonsFolder, true); + } + } + + private static LocalFilesProvider CreateScanner() + { + Mock> cache = new(); + Mock> channelPubMock = new(); + Mock gamesProvider = new(); + gamesProvider.Setup(x => x.GetGames()).Returns([]); + return new(gamesProvider.Object, cache.Object, channelPubMock.Object, NullLogger.Instance); + } + + [Fact] + public async Task InitializeAsync_WhenAlreadyInitialized_ReturnsTrue() + { + var scanner = CreateScanner(); + + Assert.True(await scanner.InitializeAsync()); + Assert.True(await scanner.InitializeAsync()); + + var addons = await scanner.GetCachedAddonFilesAsync(); + Assert.Equal(4, addons.Count); + } + + [Fact] + public async Task InitializeAsync_WithEmptyAddonsFolder_ReturnsTrue() + { + foreach (var file in Directory.EnumerateFiles(_addonsFolder, "*", SearchOption.AllDirectories)) + { + File.Delete(file); + } + foreach (var dir in Directory.EnumerateDirectories(_addonsFolder)) + { + Directory.Delete(dir, true); + } + + var scanner = CreateScanner(); + + Assert.True(await scanner.InitializeAsync()); + Assert.True(scanner.IsInitialized); + + var addons = await scanner.GetCachedAddonFilesAsync(); + Assert.Empty(addons); + } + + [Fact] + public void IsInitialized_FalseBeforeInit() + { + var scanner = CreateScanner(); + + Assert.False(scanner.IsInitialized); + } + + [Fact] + public async Task IsInitialized_TrueAfterInit() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + Assert.True(scanner.IsInitialized); + } + + [Fact] + public async Task GetCachedAddonFilesAsync_AutoInitializes() + { + var scanner = CreateScanner(); + + var addons = await scanner.GetCachedAddonFilesAsync(); + + Assert.Equal(4, addons.Count); + Assert.True(scanner.IsInitialized); + } + + [Fact] + public async Task GetCachedAddonFilesAsync_ReturnsAllParsedAddonFiles() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + var addons = await scanner.GetCachedAddonFilesAsync(); + + Assert.All(addons, addon => + { + Assert.NotNull(addon.Manifest); + Assert.NotNull(addon.Manifest.Id); + Assert.NotNull(addon.Manifest.Title); + Assert.NotNull(addon.FileInfo); + }); + } + + [Fact] + public async Task TryGetCachedAddonFile_WhenExists_ReturnsTrueAndFile() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + var addons = await scanner.GetCachedAddonFilesAsync(); + var target = addons[0]; + + var found = scanner.TryGetCachedAddonFile(target.FileInfo, out var retrieved); + + Assert.True(found); + Assert.NotNull(retrieved); + Assert.Equal(target.Manifest.Id, retrieved.Manifest.Id); + Assert.Equal(target.Manifest.Title, retrieved.Manifest.Title); + } + + [Fact] + public async Task TryGetCachedAddonFile_WhenNotExists_ReturnsFalse() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + var notFound = new AddonFilePathWrapper(@"C:\nonexistent", "nonexistent.json"); + + Assert.False(scanner.TryGetCachedAddonFile(notFound, out var file)); + Assert.Null(file); + } + + [Fact] + public void TryGetCachedAddonFile_WhenNotInitialized_ReturnsFalse() + { + var scanner = CreateScanner(); + + var wrapper = new AddonFilePathWrapper(@"C:\test", "test.json"); + + Assert.False(scanner.TryGetCachedAddonFile(wrapper, out var file)); + Assert.Null(file); + } + + [Fact] + public async Task ReplacePath_WhenMatchExists_ReplacesPaths() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + var oldPath = Path.Combine(_addonsFolder, "UnpackedAddon"); + var newPath = Path.Combine(_addonsFolder, "MovedAddon"); + + Directory.Move(oldPath, newPath); + + var updated = await scanner.ReplacePathAsync(oldPath, newPath); + + Assert.Equal(2, updated.Count); + Assert.All(updated, x => Assert.Equal(newPath, x.FileInfo.PathToFolder)); + } + + [Fact] + public async Task ReplacePath_WhenNoMatch_ReturnsEmpty() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + var updated = await scanner.ReplacePathAsync(@"C:\nonexistent", @"C:\new"); + + Assert.Empty(updated); + } + + [Fact] + public async Task TryAddFileToCache_WithZip_AddsToCache() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + var additionalZip = Path.Combine(_addonsFolder, "AdditionalAddon.zip"); + File.Copy(Path.Combine("Files", "ZippedAddon.zip"), additionalZip, true); + + var result = await scanner.TryAddFileToCacheAsync(additionalZip, null); + + Assert.NotNull(result); + Assert.Equal(2, result.Count); + + var addons = await scanner.GetCachedAddonFilesAsync(); + Assert.Equal(6, addons.Count); + } + + [Fact] + public async Task TryAddFileToCache_WithUnsupportedExtension_ReturnsNull() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + var txtFile = Path.Combine(_addonsFolder, "readme.txt"); + await File.WriteAllTextAsync(txtFile, "hello"); + + var result = await scanner.TryAddFileToCacheAsync(txtFile, null); + + Assert.Null(result); + } + + [Fact] + public async Task Concurrency_DoesNotDeadlock() + { + var scanner = CreateScanner(); + + var t1 = scanner.InitializeAsync(); + var t2 = scanner.InitializeAsync(); + + var results = await Task.WhenAll(t1, t2); + + Assert.True(results[0]); + Assert.True(results[1]); + Assert.True(scanner.IsInitialized); + + var addons = await scanner.GetCachedAddonFilesAsync(); + Assert.Equal(4, addons.Count); + } + + [Fact] + public async Task TryAddFileToCache_JsonFile_AddsToCache() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + var folder = Path.Combine(_addonsFolder, "JsonAddon"); + Directory.CreateDirectory(folder); + var jsonPath = Path.Combine(folder, "addon.json"); + await File.WriteAllTextAsync(jsonPath, """ + { + "id": "json-addon", + "type": "Mod", + "game": { "name": "Duke3D" }, + "title": "Json Addon", + "version": "1.0" + } + """); + + var result = await scanner.TryAddFileToCacheAsync(jsonPath, null); + + Assert.NotNull(result); + var parsed = Assert.Single(result); + Assert.Equal("json-addon", parsed.Manifest!.Id); + } + + [Fact] + public async Task ReplacePath_WhenNotInitialized_Throws() + { + var scanner = CreateScanner(); + + var ex = await Assert.ThrowsAsync(() => scanner.ReplacePathAsync(@"C:\old", @"C:\new")); + + Assert.Contains("Cache is not initialized", ex.Message); + } + + [Fact] + public async Task TryRemoveFileFromCache_WhenNotInitialized_ReturnsFalse() + { + var scanner = CreateScanner(); + + Assert.False(await scanner.TryRemoveFileFromCacheAsync(@"C:\anything.zip")); + } + + [Fact] + public async Task TryRemoveFileFromCache_WhenNoMatch_ReturnsFalse() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + Assert.False(await scanner.TryRemoveFileFromCacheAsync(@"C:\nonexistent\file.zip")); + } + + [Fact] + public async Task TryRemoveFileFromCache_WithZipPath_RemovesAllZipEntries() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + var zipPath = Path.Combine(_addonsFolder, "ZippedAddon.zip"); + + var removed = await scanner.TryRemoveFileFromCacheAsync(zipPath); + + Assert.True(removed); + + var addons = await scanner.GetCachedAddonFilesAsync(); + Assert.Equal(2, addons.Count); + Assert.DoesNotContain(addons, x => x.FileInfo.PathToFile.Equals(zipPath, StringComparison.InvariantCultureIgnoreCase)); + } + + [Fact] + public async Task TryRemoveFileFromCache_WithFolderPath_RemovesFolderEntries() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + var folderPath = Path.Combine(_addonsFolder, "UnpackedAddon"); + + var removed = await scanner.TryRemoveFileFromCacheAsync(folderPath); + + Assert.True(removed); + + var addons = await scanner.GetCachedAddonFilesAsync(); + Assert.Equal(2, addons.Count); + Assert.DoesNotContain(addons, x => x.FileInfo.PathToFolder.Equals(folderPath, StringComparison.InvariantCultureIgnoreCase)); + } + + [Fact] + public async Task TryRemoveFileFromCache_AfterRemoval_EntryNoLongerFound() + { + var scanner = CreateScanner(); + await scanner.InitializeAsync(); + + var addonsBefore = await scanner.GetCachedAddonFilesAsync(); + var target = addonsBefore[0]; + + Assert.True(await scanner.TryRemoveFileFromCacheAsync(target.FileInfo.PathToFile)); + + Assert.False(scanner.TryGetCachedAddonFile(target.FileInfo, out _)); + } + + [Fact] + public async Task TryRemoveFileFromCache_FiresFileRemovedEvent() + { + Mock> cache = new(); + Mock> channelPubMock = new(); + Mock gamesProvider = new(); + gamesProvider.Setup(x => x.GetGames()).Returns([]); + var scanner = new LocalFilesProvider(gamesProvider.Object, cache.Object, channelPubMock.Object, NullLogger.Instance); + await scanner.InitializeAsync(); + + var zipPath = Path.Combine(_addonsFolder, "ZippedAddon.zip"); + + await scanner.TryRemoveFileFromCacheAsync(zipPath); + + channelPubMock.Verify(x => x.PublishAsync(It.Is( + e => !e.IsAdded && e.Files.Count == 2 + )), Times.Once); + } +} diff --git a/src/Tests.Unit/Sync/MetadataProviderTests.cs b/src/Tests.Unit/Sync/MetadataProviderTests.cs new file mode 100644 index 00000000..e8876dc6 --- /dev/null +++ b/src/Tests.Unit/Sync/MetadataProviderTests.cs @@ -0,0 +1,331 @@ +using Addons.Providers; +using Core.All; +using Core.All.Enums; +using Core.All.Serializable.Addon; +using Core.Client.Cache; +using Core.Client.Helpers; +using Core.Client.Interfaces; +using Games.Providers; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using SharpCompress.Archives; +using Tests.Unit.Helpers; + +namespace Tests.Unit.Sync; + +[Collection("Sync")] +public sealed class MetadataProviderTests : IDisposable +{ + private readonly Mock> _cacheMock; + private readonly LocalFilesProvider _scanner; + + public MetadataProviderTests() + { + _cacheMock = new Mock>(); + + Directory.CreateDirectory(ClientProperties.AddonsFolderPath); + File.Copy( + Path.Combine("Files", "ZippedAddon.zip"), + Path.Combine(ClientProperties.AddonsFolderPath, "ZippedAddon.zip"), + true + ); + + var unpackToFolder = Path.Combine(ClientProperties.AddonsFolderPath, "UnpackedAddon"); + Directory.CreateDirectory(unpackToFolder); + using var archive = ArchiveFactory.OpenArchive(Path.Combine("Files", "UnpackedAddon.zip")); + archive.WriteToDirectory(unpackToFolder); + + Mock> channelPubMock = new(); + Mock gamesProvider = new(); + gamesProvider.Setup(x => x.GetGames()).Returns([]); + _scanner = new(gamesProvider.Object, _cacheMock.Object, channelPubMock.Object, NullLogger.Instance); + } + + public void Dispose() + { + Directory.Delete(ClientProperties.AddonsFolderPath, true); + } + + private static AddonManifestJsonModel CreateRemoteManifest1() + { + return new() + { + AddonType = AddonTypeEnum.TC, + Id = "blood-voxel-pack", + Version = "p292", + SupportedGame = new() { Game = GameEnum.Blood }, + Title = "blood-voxel-pack", + Incompatibles = new() { Addons = [new() { Id = "blood-coagulated" }] }, + Description = "New description" + }; + } + + private static AddonManifestJsonModel CreateRemoteManifest2() + { + return new() + { + AddonType = AddonTypeEnum.TC, + Id = "blood-voxel-pack-2", + Version = "p292-2", + SupportedGame = new() { Game = GameEnum.Blood }, + Title = "blood-voxel-pack", + Incompatibles = new() { Addons = [new() { Id = "blood-coagulated" }] }, + Description = "Voxel Pack 2", + MainRff = "MAIN.RFF" + }; + } + + private static AddonManifestJsonModel CreateMatchingLocalManifest1() + { + return new() + { + AddonType = AddonTypeEnum.Mod, + Id = "blood-voxel-pack", + Version = "p292", + SupportedGame = new() { Game = GameEnum.Blood }, + Title = "Voxel Pack", + AdditionalDefs = ["blood_voxels.def"], + Incompatibles = new() { Addons = [new() { Id = "blood-coagulated" }] }, + Description = "https://github.com/fgsfds/Blood-Voxel-Pack/\r\n\r\nVoxel replacements for sprites in Blood" + }; + } + + private async Task<(MetadataProvider provider, Mock api)> CreateProviderAsync( + List? metadata) + { + Mock api = new(); + api.Setup(x => x.GetMetadataAsync()).ReturnsAsync(metadata); + + MetadataProvider provider = new( + _scanner, + api.Object, + NullLogger.Instance + ); + + await _scanner.InitializeAsync(); + await provider.InitializeAsync(); + + return (provider, api); + } + + [Fact] + public async Task UpdateAvailable_ZippedAddonWithDifferentManifest_ReturnsTrue() + { + var (provider, _) = await CreateProviderAsync(new List { CreateRemoteManifest1(), CreateRemoteManifest2() }); + + var result = provider.IsMetadataUpdateAvailable( + new("blood-voxel-pack", "p292"), + new(Path.Combine(ClientProperties.AddonsFolderPath, "ZippedAddon.zip"), "addon.json")); + + Assert.True(result); + } + + [Fact] + public async Task UpdateAvailable_UnpackedAddonWithDifferentManifest_ReturnsTrue() + { + var (provider, _) = await CreateProviderAsync(new List { CreateRemoteManifest1(), CreateRemoteManifest2() }); + + var result = provider.IsMetadataUpdateAvailable( + new("blood-voxel-pack-2", "p292-2"), + new(Path.Combine(ClientProperties.AddonsFolderPath, "UnpackedAddon"), "addon2.json")); + + Assert.True(result); + } + + [Fact] + public async Task IsMetadataUpdateAvailable_RemoteManifestMatchesLocal_ReturnsFalse() + { + var matchingManifest = CreateMatchingLocalManifest1(); + var (provider, _) = await CreateProviderAsync(new List { matchingManifest }); + + var result = provider.IsMetadataUpdateAvailable( + new("blood-voxel-pack", "p292"), + new(Path.Combine(ClientProperties.AddonsFolderPath, "ZippedAddon.zip"), "addon.json")); + + Assert.False(result); + } + + [Fact] + public async Task AddonNotInMetadata_ReturnsFalse() + { + var (provider, _) = await CreateProviderAsync(new List { CreateRemoteManifest1() }); + + var result = provider.IsMetadataUpdateAvailable( + new("unknown-addon", "v1.0"), + new(Path.Combine(ClientProperties.AddonsFolderPath, "ZippedAddon.zip"), "addon.json")); + + Assert.False(result); + } + + [Fact] + public async Task AlreadyCachedUpdate_ReturnsTrue() + { + var (provider, _) = await CreateProviderAsync(new List { CreateRemoteManifest1(), CreateRemoteManifest2() }); + + _ = provider.IsMetadataUpdateAvailable( + new("blood-voxel-pack", "p292"), + new(Path.Combine(ClientProperties.AddonsFolderPath, "ZippedAddon.zip"), "addon.json")); + + var result = provider.IsMetadataUpdateAvailable( + new("blood-voxel-pack", "p292"), + new(Path.Combine(ClientProperties.AddonsFolderPath, "ZippedAddon.zip"), "addon.json")); + + Assert.True(result); + } + + [Fact] + public async Task NotInitialized_ReturnsFalse() + { + Mock api = new(); + MetadataProvider provider = new( + _scanner, + api.Object, + NullLogger.Instance + ); + + await _scanner.InitializeAsync(); + + var result = provider.IsMetadataUpdateAvailable( + new("blood-voxel-pack", "p292"), + new(Path.Combine(ClientProperties.AddonsFolderPath, "ZippedAddon.zip"), "addon.json")); + + Assert.False(result); + } + + [Fact] + public async Task MetadataIsNull_ReturnsFalse() + { + var (provider, _) = await CreateProviderAsync(null); + + var result = provider.IsMetadataUpdateAvailable( + new("blood-voxel-pack", "p292"), + new(Path.Combine(ClientProperties.AddonsFolderPath, "ZippedAddon.zip"), "addon.json")); + + Assert.False(result); + } + + [Fact] + public void IsInitialized_FalseBeforeInit() + { + Mock api = new(); + MetadataProvider provider = new( + _scanner, + api.Object, + NullLogger.Instance + ); + + Assert.False(provider.IsInitialized); + } + + [Fact] + public async Task InitializeAsync_SetsIsInitialized() + { + Mock api = new(); + api.Setup(x => x.GetMetadataAsync()).ReturnsAsync([]); + + MetadataProvider provider = new( + _scanner, + api.Object, + NullLogger.Instance + ); + + await provider.InitializeAsync(); + + Assert.True(provider.IsInitialized); + } + + [Fact] + public async Task InitializeAsync_Twice_ReturnsTrue() + { + Mock api = new(); + api.Setup(x => x.GetMetadataAsync()).ReturnsAsync([]); + + MetadataProvider provider = new( + _scanner, + api.Object, + NullLogger.Instance + ); + + Assert.True(await provider.InitializeAsync()); + Assert.True(await provider.InitializeAsync()); + } + + [Fact] + public async Task MetadataInitializedEvent_FiresOnInit() + { + Mock api = new(); + api.Setup(x => x.GetMetadataAsync()).ReturnsAsync([]); + + MetadataProvider provider = new( + _scanner, + api.Object, + NullLogger.Instance + ); + + var eventFired = false; + provider.MetadataInitializedEvent += (_, _) => eventFired = true; + + await provider.InitializeAsync(); + + Assert.True(eventFired); + } + + [Fact] + public async Task MetadataUpdatedEvent_FiresOnFolderUpdate() + { + var (provider, _) = await CreateProviderAsync(new List { CreateRemoteManifest1(), CreateRemoteManifest2() }); + + _ = provider.IsMetadataUpdateAvailable( + new("blood-voxel-pack-2", "p292-2"), + new(Path.Combine(ClientProperties.AddonsFolderPath, "UnpackedAddon"), "addon2.json")); + + var eventFired = false; + provider.MetadataUpdatedEvent += (_, _) => eventFired = true; + + var result = await provider.UpdateMetadataAsync( + new(Path.Combine(ClientProperties.AddonsFolderPath, "UnpackedAddon"), "addon2.json")); + + Assert.True(result.IsSuccess); + Assert.True(eventFired); + } + + [Fact] + public async Task UpdateMetadataAsync_ForFolder_Success() + { + var (provider, _) = await CreateProviderAsync(new List { CreateRemoteManifest1(), CreateRemoteManifest2() }); + + _ = provider.IsMetadataUpdateAvailable( + new("blood-voxel-pack-2", "p292-2"), + new(Path.Combine(ClientProperties.AddonsFolderPath, "UnpackedAddon"), "addon2.json")); + + var result = await provider.UpdateMetadataAsync( + new(Path.Combine(ClientProperties.AddonsFolderPath, "UnpackedAddon"), "addon2.json")); + + Assert.True(result.IsSuccess); + } + + [Fact] + public async Task UpdateMetadataAsync_WhenNotCached_ReturnsError() + { + var (provider, _) = await CreateProviderAsync(new List { CreateRemoteManifest1(), CreateRemoteManifest2() }); + + var result = await provider.UpdateMetadataAsync( + new(Path.Combine(ClientProperties.AddonsFolderPath, "ZippedAddon.zip"), "addon.json")); + + Assert.False(result.IsSuccess); + } + + [Fact] + public async Task IsMetadataUpdateAvailable_WhenScannerMissingFile_ReturnsFalse() + { + var (provider, _) = await CreateProviderAsync(new List { CreateRemoteManifest1() }); + + var result = provider.IsMetadataUpdateAvailable( + new("blood-voxel-pack", "p292"), + new(Path.Combine(ClientProperties.AddonsFolderPath, "NonExistent.zip"), "addon.json")); + + Assert.False(result); + } + + +} diff --git a/src/Tests.Unit/Sync/SaveFilesTests.cs b/src/Tests.Unit/Sync/SaveFilesTests.cs new file mode 100644 index 00000000..03217b15 --- /dev/null +++ b/src/Tests.Unit/Sync/SaveFilesTests.cs @@ -0,0 +1,298 @@ +using Addons.Addons; +using Core.All.Enums; +using Core.Client.Helpers; +using Games.Games; + +namespace Tests.Unit.Sync; + +[Collection("Sync")] +public sealed class SaveFilesTests : IDisposable +{ + private readonly DukeGame _game; + private readonly DukeCampaign _camp; + private readonly string _gameDir; + private readonly List _tempDirs = []; + + public SaveFilesTests() + { + _gameDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(_gameDir); + + _game = new DukeGame + { + Duke64RomPath = null, + DukeZHRomPath = null, + DukeWTInstallPath = null, + GameInstallFolder = _gameDir, + AddonsPaths = [], + }; + + _camp = new DukeCampaign + { + AddonId = new("save-test", null), + Type = AddonTypeEnum.Official, + Title = "Save Test", + SupportedGame = new(GameEnum.Duke3D, null, null), + FileInfo = null, + GridImageHash = null, + PreviewImageHash = null, + Description = null, + Author = null, + ReleaseDate = null, + MainDef = null, + AdditionalDefs = null, + MainCon = null, + AdditionalCons = null, + RTS = null, + StartMap = null, + DependentAddons = null, + IncompatibleAddons = null, + RequiredFeatures = null, + Executables = null, + Options = null, + }; + } + + public void Dispose() + { + if (Directory.Exists(_gameDir)) + { + Directory.Delete(_gameDir, true); + } + + foreach (var dir in _tempDirs) + { + if (Directory.Exists(dir)) + { + try { Directory.Delete(dir, true); } catch { } + } + } + } + + private void RegisterTempDir(string path) + { + _tempDirs.Add(path); + } + + [Fact] + public void Base_MoveSaveFilesToStorage_NoSaves_DoesNotThrow() + { + var port = new BasePortTestProxy(); + port.CallMoveSaveFilesToStorage(_game, _camp); + } + + [Fact] + public void Base_MoveSaveFilesFromStorage_NoFolder_DoesNotThrow() + { + var port = new BasePortTestProxy(); + port.CallMoveSaveFilesFromStorage(_game, _camp); + } + + [Fact] + public void Base_MoveSaveFilesToStorage_MovesSaves() + { + var port = new BasePortTestProxy(); + var saveFolder = port.CallGetPathToAddonSavedGamesFolder(_game.ShortName, _camp.AddonId.Id); + CleanupParentDirs(saveFolder); + + var saveFile1 = Path.Combine(_gameDir, "savegame.sav"); + var saveFile2 = Path.Combine(_gameDir, "quick.esv"); + var nonSaveFile = Path.Combine(_gameDir, "readme.txt"); + File.WriteAllText(saveFile1, "save1"); + File.WriteAllText(saveFile2, "save2"); + File.WriteAllText(nonSaveFile, "don't move me"); + + port.CallMoveSaveFilesToStorage(_game, _camp); + + Assert.False(File.Exists(saveFile1)); + Assert.False(File.Exists(saveFile2)); + Assert.True(File.Exists(nonSaveFile)); + Assert.True(Directory.Exists(saveFolder)); + + var savedFiles = Directory.GetFiles(saveFolder); + Assert.Equal(2, savedFiles.Length); + Assert.Contains(savedFiles, f => f.EndsWith("savegame.sav")); + Assert.Contains(savedFiles, f => f.EndsWith("quick.esv")); + } + + [Fact] + public void Base_MoveSaveFilesFromStorage_RestoresSaves() + { + var port = new BasePortTestProxy(); + var saveFolder = port.CallGetPathToAddonSavedGamesFolder(_game.ShortName, _camp.AddonId.Id); + CleanupParentDirs(saveFolder); + + var saveFile = Path.Combine(_gameDir, "savegame.sav"); + File.WriteAllText(saveFile, "save data"); + + port.CallMoveSaveFilesToStorage(_game, _camp); + Assert.False(File.Exists(saveFile)); + Assert.True(Directory.Exists(saveFolder)); + + port.CallMoveSaveFilesFromStorage(_game, _camp); + + Assert.True(File.Exists(saveFile), "Save file should be restored"); + Assert.Equal("save data", File.ReadAllText(saveFile)); + } + + [Fact] + public void EDuke32_MoveSaveFilesFromStorage_NullFileInfo_UsesInstallFolder() + { + var port = new EDuke32TestProxy(); + var saveFolder = port.CallGetPathToAddonSavedGamesFolder(_game.ShortName, _camp.AddonId.Id); + CleanupParentDirs(saveFolder); + + // Create the install directory (EDuke32's PortsFolder/EDuke32) + var installDir = Path.Combine(ClientProperties.PortsFolderPath, "EDuke32"); + Directory.CreateDirectory(installDir); + RegisterTempDir(installDir); + + // EDuke32's MoveSaveFilesFromStorage copies FROM saveFolder TO installDir + Directory.CreateDirectory(saveFolder); + var saveFile = Path.Combine(saveFolder, "savegame.sav"); + File.WriteAllText(saveFile, "save data"); + + port.CallMoveSaveFilesFromStorage(_game, _camp); + + // Save should be in install directory + Assert.True(File.Exists(Path.Combine(installDir, "savegame.sav")), "Save should be restored to install dir"); + Assert.False(File.Exists(saveFile), "Save should be removed from save folder"); + } + + [Fact] + public void EDuke32_MoveSaveFilesToStorage_NullFileInfo_UsesInstallFolder() + { + var port = new EDuke32TestProxy(); + var saveFolder = port.CallGetPathToAddonSavedGamesFolder(_game.ShortName, _camp.AddonId.Id); + CleanupParentDirs(saveFolder); + + // Create the install directory (EDuke32's PortsFolder/EDuke32) + var installDir = Path.Combine(ClientProperties.PortsFolderPath, "EDuke32"); + Directory.CreateDirectory(installDir); + + // EDuke32's MoveSaveFilesToStorage copies FROM installDir TO saveFolder + var saveFile = Path.Combine(installDir, "savegame.sav"); + File.WriteAllText(saveFile, "save data"); + + port.CallMoveSaveFilesToStorage(_game, _camp); + + // Save should be in save folder + Assert.True(Directory.Exists(saveFolder), "Save folder should exist"); + Assert.True(File.Exists(Path.Combine(saveFolder, "savegame.sav")), "Save should be backed up to save folder"); + Assert.False(File.Exists(saveFile), "Save should be removed from install dir"); + } + + [Fact] + public void EDuke32_MoveSaveFilesFromStorage_FolderFileInfo_RestoresToAddonFolder() + { + var addonDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(addonDir); + RegisterTempDir(addonDir); + + var camp = new DukeCampaign + { + AddonId = new("folder-camp", null), + Type = AddonTypeEnum.TC, + Title = "Folder Camp", + SupportedGame = new(GameEnum.Duke3D, null, null), + FileInfo = new AddonFilePathWrapper(addonDir, "addon.json"), + GridImageHash = null, + PreviewImageHash = null, + Description = null, + Author = null, + ReleaseDate = null, + MainDef = null, + AdditionalDefs = null, + MainCon = null, + AdditionalCons = null, + RTS = null, + StartMap = null, + DependentAddons = null, + IncompatibleAddons = null, + RequiredFeatures = null, + Executables = null, + Options = null, + }; + + var port = new EDuke32TestProxy(); + var saveFolder = port.CallGetPathToAddonSavedGamesFolder(_game.ShortName, camp.AddonId.Id); + CleanupParentDirs(saveFolder); + + // FromStorage (RESTORE): saves from saveFolder → addon folder + Directory.CreateDirectory(saveFolder); + var saveFile = Path.Combine(saveFolder, "savegame.sav"); + File.WriteAllText(saveFile, "save data"); + + port.CallMoveSaveFilesFromStorage(_game, camp); + + Assert.True(File.Exists(Path.Combine(addonDir, "savegame.sav")), "Save should be restored to addon folder"); + Assert.False(File.Exists(saveFile), "Save should be removed from save folder"); + } + + [Fact] + public void EDuke32_MoveSaveFilesToStorage_FolderFileInfo_BacksUpFromAddonFolder() + { + var addonDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(addonDir); + RegisterTempDir(addonDir); + + var camp = new DukeCampaign + { + AddonId = new("folder-camp", null), + Type = AddonTypeEnum.TC, + Title = "Folder Camp", + SupportedGame = new(GameEnum.Duke3D, null, null), + FileInfo = new AddonFilePathWrapper(addonDir, "addon.json"), + GridImageHash = null, + PreviewImageHash = null, + Description = null, + Author = null, + ReleaseDate = null, + MainDef = null, + AdditionalDefs = null, + MainCon = null, + AdditionalCons = null, + RTS = null, + StartMap = null, + DependentAddons = null, + IncompatibleAddons = null, + RequiredFeatures = null, + Executables = null, + Options = null, + }; + + var port = new EDuke32TestProxy(); + var saveFolder = port.CallGetPathToAddonSavedGamesFolder(_game.ShortName, camp.AddonId.Id); + CleanupParentDirs(saveFolder); + + // ToStorage (BACKUP): saves from addon folder → saveFolder + var saveFile = Path.Combine(addonDir, "savegame.sav"); + File.WriteAllText(saveFile, "save data"); + + port.CallMoveSaveFilesToStorage(_game, camp); + + Assert.True(File.Exists(Path.Combine(saveFolder, "savegame.sav")), "Save should be backed up to save folder"); + Assert.False(File.Exists(saveFile), "Save should be removed from addon folder"); + } + + private void CleanupParentDirs(string path) + { + var dir = Path.GetDirectoryName(path); + while (dir is not null && Directory.Exists(dir)) + { + try + { + if (Directory.EnumerateFileSystemEntries(dir).Any()) + { + break; + } + Directory.Delete(dir); + dir = Path.GetDirectoryName(dir); + } + catch + { + break; + } + } + } +} diff --git a/src/Tests.Unit/Tests.Unit.csproj b/src/Tests.Unit/Tests.Unit.csproj index 9bec91b5..d4ecc25f 100644 --- a/src/Tests.Unit/Tests.Unit.csproj +++ b/src/Tests.Unit/Tests.Unit.csproj @@ -9,7 +9,6 @@ - @@ -44,9 +43,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest