forked from Reloaded-Project/Reloaded-II
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added: Automatic Package Creator, Including some Simple Tests
- Loading branch information
Showing
8 changed files
with
272 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
source/Reloaded.Mod.Launcher.Lib/Utility/JxlImageConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
using PhotoSauce.MagicScaler; | ||
using PhotoSauce.NativeCodecs.Libjxl; | ||
|
||
namespace Reloaded.Mod.Launcher.Lib.Utility; | ||
|
||
/// <summary> | ||
/// Converts image to JPEG XL format. | ||
/// </summary> | ||
public class JxlImageConverter : IImageConverter | ||
{ | ||
private static readonly IEncoderOptions EncoderOptions = new JxlLosslessEncoderOptions(JxlEncodeSpeed.Squirrel, JxlDecodeSpeed.Slowest); | ||
|
||
static JxlImageConverter() | ||
{ | ||
CodecManager.Configure(codecs => { codecs.UseLibjxl(); }); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public MemoryStream Convert(Span<byte> source, out string extension) | ||
{ | ||
var settings = new ProcessImageSettings(); | ||
settings.TrySetEncoderFormat(ImageMimeTypes.Jxl); | ||
settings.EncoderOptions = EncoderOptions; | ||
|
||
var output = new MemoryStream(); | ||
MagicImageProcessor.ProcessImage(source, output, settings); | ||
|
||
output.Position = 0; | ||
extension = ".jxl"; | ||
return output; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
source/Reloaded.Mod.Loader.Tests/Update/Pack/AutoPackageCreatorTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
using Reloaded.Mod.Launcher.Lib.Utility; | ||
using Reloaded.Mod.Loader.Tests.Update.Pack.Mocks; | ||
using Reloaded.Mod.Loader.Update.Packs; | ||
using Sewer56.Update.Resolvers.GameBanana; | ||
|
||
namespace Reloaded.Mod.Loader.Tests.Update.Pack; | ||
|
||
public class AutoPackageCreatorTests | ||
{ | ||
[Fact] | ||
public void CannotCreate_WithMissingUpdateInfo() | ||
{ | ||
using var tempFolderAlloc = new TemporaryFolderAllocation(); | ||
var conf = CreateTestConfig(tempFolderAlloc.FolderPath); | ||
|
||
Assert.False(AutoPackCreator.ValidateCanCreate(new []{ conf }, out var incompatible)); | ||
Assert.Single(incompatible); | ||
} | ||
|
||
[Fact] | ||
public void CanCreate_WithUpdateInfo() | ||
{ | ||
using var tempFolderAlloc = new TemporaryFolderAllocation(); | ||
var conf = CreateTestConfig(tempFolderAlloc.FolderPath); | ||
AddGitHubUpdateResolver(conf); | ||
|
||
Assert.True(AutoPackCreator.ValidateCanCreate(new []{ conf }, out var incompatible)); | ||
Assert.Empty(incompatible); | ||
} | ||
|
||
[Fact] | ||
public async Task AutoPackCreator_CanImportFromModSearch() | ||
{ | ||
// Arrange | ||
using var tempMod = new TemporaryFolderAllocation(); | ||
var modConf = CreateTestConfig(tempMod.FolderPath); | ||
AddGitHubUpdateResolver(modConf); | ||
|
||
var provider = GetGameBananaPackageProvider(); | ||
|
||
// Act | ||
var pack = await AutoPackCreator.CreateAsync(new ModConfig[] { modConf.Config }, | ||
new DummyImageConverter(), | ||
new List<IDownloadablePackageProvider>() { provider}); | ||
var built = pack.Build(out var package); | ||
|
||
// Assert | ||
File.WriteAllBytes("build.zip", built.ToArray()); | ||
Assert.Single(package.Items); | ||
Assert.True(package.Items[0].ImageFiles.Count > 1); | ||
} | ||
|
||
private PathTuple<ModConfig> CreateTestConfig(string folder) | ||
{ | ||
return new PathTuple<ModConfig>(Path.Combine(folder, ModConfig.ConfigFileName), new ModConfig() | ||
{ | ||
ModId = "sonicheroes.essentials.graphics", | ||
ModName = "Graphics Essentials" // so it finds on GameBanana | ||
}); | ||
} | ||
|
||
private void AddGitHubUpdateResolver(PathTuple<ModConfig> modTuple) | ||
{ | ||
var factory = new GitHubReleasesUpdateResolverFactory(); | ||
factory.SetConfiguration(modTuple, new GitHubReleasesUpdateResolverFactory.GitHubConfig() | ||
{ | ||
RepositoryName = "Heroes.Graphics.Essentials.ReloadedII", | ||
UserName = "Sewer56" | ||
}); | ||
} | ||
|
||
private GameBananaPackageProvider GetGameBananaPackageProvider() | ||
{ | ||
return new GameBananaPackageProvider(6061); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
source/Reloaded.Mod.Loader.Tests/Update/Pack/Mocks/DummyImageConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
namespace Reloaded.Mod.Loader.Tests.Update.Pack.Mocks; | ||
|
||
public class DummyImageConverter : IImageConverter | ||
{ | ||
public MemoryStream Convert(Span<byte> source, out string extension) | ||
{ | ||
extension = ".org"; | ||
return new MemoryStream(source.ToArray()); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
source/Reloaded.Mod.Loader.Update/Interfaces/IImageConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
namespace Reloaded.Mod.Loader.Update.Interfaces; | ||
|
||
/// <summary> | ||
/// Interface that can be used to provide support for converting images. | ||
/// </summary> | ||
public interface IImageConverter | ||
{ | ||
/// <summary> | ||
/// Converts the image to new format. | ||
/// </summary> | ||
/// <param name="source">Span containing the source image to be converted.</param> | ||
/// <param name="extension">The extension for the image.</param> | ||
/// <returns>The converted image in a stream. Stream should have position 0 and end at end of file.</returns> | ||
public MemoryStream Convert(Span<byte> source, out string extension); | ||
} |
135 changes: 135 additions & 0 deletions
135
source/Reloaded.Mod.Loader.Update/Packs/AutoPackCreator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
namespace Reloaded.Mod.Loader.Update.Packs; | ||
|
||
/// <summary> | ||
/// Utility class for automatic creation of packages based on a given sample input of mods. | ||
/// </summary> | ||
public static class AutoPackCreator | ||
{ | ||
/// <summary> | ||
/// Checks if all mods in given list can be used in the pack by verifying they have enabled updates. | ||
/// </summary> | ||
/// <param name="configurations">List of mod configurations to check.</param> | ||
/// <param name="incompatibleMods">List of mods to check for compatibility.</param> | ||
/// <returns>True if mods can be packed, else false.</returns> | ||
public static bool ValidateCanCreate(IEnumerable<PathTuple<ModConfig>> configurations, out List<PathTuple<ModConfig>> incompatibleMods) | ||
{ | ||
incompatibleMods = new List<PathTuple<ModConfig>>(); | ||
foreach (var config in configurations) | ||
{ | ||
if (!PackageResolverFactory.HasAnyConfiguredResolver(config)) | ||
incompatibleMods.Add(config); | ||
} | ||
|
||
return incompatibleMods.Count <= 0; | ||
} | ||
|
||
/// <summary> | ||
/// Automatically creates a package. | ||
/// </summary> | ||
/// <param name="configurations">The configurations used to create the config. Must at least have update data, ModId and ModName.</param> | ||
/// <param name="imageConverter">Used for converting images.</param> | ||
/// <param name="packageProviders">Providers that can be used to search for packages.</param> | ||
/// <param name="token">Token to cancel the operation.</param> | ||
public static async Task<ReloadedPackBuilder> CreateAsync(IEnumerable<ModConfig> configurations, IImageConverter imageConverter, IList<IDownloadablePackageProvider> packageProviders, CancellationToken token = default) | ||
{ | ||
var builder = new ReloadedPackBuilder(); | ||
var imageDownloader = new ImageCacheService(); | ||
|
||
builder.SetName("My Autogenerated Package"); | ||
builder.SetReadme("You should probably add description here."); | ||
|
||
foreach (var config in configurations) | ||
{ | ||
var itemBuilder = builder.AddModItem(config.ModId); | ||
itemBuilder.SetName(config.ModName); | ||
itemBuilder.SetPluginData(config.PluginData); | ||
|
||
var bestPkg = await GetBestPackageForTemplateAsync(config.ModId, config.ModName, packageProviders, token); | ||
|
||
// Add images if possible | ||
if (bestPkg.images != null) | ||
{ | ||
foreach (var image in bestPkg.images) | ||
{ | ||
var file = await imageDownloader.GetOrDownloadFileFromUrl(image.Uri, imageDownloader.ModPreviewExpiration, false, token); | ||
var converted = imageConverter.Convert(file, out string ext); | ||
itemBuilder.AddImage(converted, ext, image.Caption); | ||
} | ||
} | ||
|
||
// Add readme if possible | ||
if (!string.IsNullOrEmpty(bestPkg.markdownReadme)) | ||
itemBuilder.SetReadme(bestPkg.markdownReadme); | ||
} | ||
|
||
return builder; | ||
} | ||
|
||
private static async Task<(DownloadableImage[]? images, string markdownReadme)> GetBestPackageForTemplateAsync(string modId, string modName, | ||
IList<IDownloadablePackageProvider> downloadablePackageProviders, CancellationToken cancellationToken) | ||
{ | ||
(DownloadableImage[]? images, string markdownReadme) result = new(); | ||
|
||
// We will get all packages with highest version and copy images & readme down the road. | ||
NuGetVersion? highestVersion = null; | ||
List<IDownloadablePackage> itemsForHighestVersion = new List<IDownloadablePackage>(); | ||
|
||
// Note: Some sources don't support e.g. images. | ||
// We prioritise sources based on whether they contain images, then by readme. | ||
// If preferred source does not contain both readme and image, we stitch them together from sources that do. | ||
foreach (var provider in downloadablePackageProviders) | ||
{ | ||
// Override if better than current best. | ||
var candidates = await provider.SearchForModAsync(modId, modName, 50, 4, true, cancellationToken); | ||
foreach (var candidate in candidates) | ||
{ | ||
// Find items for highest version | ||
if (candidate.Version != null) | ||
{ | ||
if (highestVersion == null) | ||
highestVersion = candidate.Version; | ||
|
||
if (candidate.Version == highestVersion) | ||
itemsForHighestVersion.Add(candidate); | ||
|
||
if (candidate.Version > highestVersion) | ||
{ | ||
itemsForHighestVersion.Clear(); | ||
highestVersion = candidate.Version; | ||
itemsForHighestVersion.Add(candidate); | ||
} | ||
} | ||
|
||
// Set images if more than existing best result. | ||
if (candidate.Images != null) | ||
{ | ||
// Assign images if unassigned | ||
result.images ??= candidate.Images; | ||
|
||
// Prefer source with more images. | ||
if (candidate.Images.Length > result.images.Length) | ||
result.images = candidate.Images; | ||
} | ||
|
||
// Set description if unassigned. | ||
if (candidate.MarkdownReadme != null) | ||
result.markdownReadme ??= candidate.MarkdownReadme; | ||
} | ||
} | ||
|
||
// Override supported items from | ||
foreach (var item in itemsForHighestVersion) | ||
{ | ||
// Set description if unassigned. | ||
if (item.MarkdownReadme != null) | ||
result.markdownReadme = item.MarkdownReadme; | ||
|
||
// Set images to ones from newest version in case older version had more images. | ||
// But only if more than 1 image. We want to filter out entries with only thumbnail. | ||
if (item.Images is { Length: > 1 }) | ||
result.images = item.Images; | ||
} | ||
|
||
return result; | ||
} | ||
} |