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