Skip to content

Commit

Permalink
Added: Library code to create 'mod-packs' for downloading multiple mods.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sewer56 committed Sep 26, 2022
1 parent f42e0b3 commit df2b56f
Show file tree
Hide file tree
Showing 11 changed files with 467 additions and 0 deletions.
5 changes: 5 additions & 0 deletions source/Reloaded.Mod.Loader.Tests/Assets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ internal class Assets
/// Path to the folder containing the index.
/// </summary>
public static readonly string IndexAssetsFolder = Path.Combine(AssetsFolder, "Index");

/// <summary>
/// Path to the folder containing the r2 bundle package files.
/// </summary>
public static readonly string PackAssetsFolder = Path.Combine(AssetsFolder, "R2Pack");

/// <summary>
/// Path to the folder containing the sample index.
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
</ItemGroup>

<ItemGroup>
<Folder Include="Assets\R2Pack" />
<Folder Include="Utility\" />
</ItemGroup>

Expand Down
98 changes: 98 additions & 0 deletions source/Reloaded.Mod.Loader.Tests/Update/Pack/PackagePackerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using Reloaded.Mod.Loader.Update.Packs;
using Routes = Reloaded.Mod.Loader.Update.Packs.Routes;

namespace Reloaded.Mod.Loader.Tests.Update.Pack;

public class PackagePackerTests
{
public string ImageFilePath = Path.Combine(Assets.PackAssetsFolder, "heroes-preview.jxl");
public const string ModId = "test.mod";

[Fact]
public void Pack_Unpack_WithoutImages()
{
// Arrange
var builder = BuildBaselinePack();

// Act
var result = builder.Build(out var pack);
result.Position = 0;
var reader = new ReloadedPackReader(result);
var packCopy = reader.GetPack();

// Assert
Assert.Equal(pack, packCopy);
}

[Fact]
public void Pack_Unpack_WithMainImage()
{
// Arrange
var builder = BuildBaselinePack();
var imageBytes = File.ReadAllBytes(ImageFilePath);
using var fs = new FileStream(ImageFilePath, FileMode.Open);
builder.AddImage(fs, Path.GetExtension(ImageFilePath)!, "Sample Image");

// Act
var result = builder.Build(out var pack);
result.Position = 0;
var reader = new ReloadedPackReader(result);

// Assert
var newImage = reader.GetImage(reader.Pack.ImageFiles[0].Path);
Assert.Equal(imageBytes, newImage);
}

[Fact]
public void Pack_Unpack_WithMod()
{
// Arrange
var builder = BuildBaselinePack();
AddSampleMod(builder);

// Act
var result = builder.Build(out var pack);
result.Position = 0;
var reader = new ReloadedPackReader(result);
var packCopy = reader.GetPack();

// Assert
Assert.Equal(pack, packCopy);
}

[Fact]
public void Pack_Unpack_WithModImage()
{
// Arrange
var builder = BuildBaselinePack();
var modBuilder = AddSampleMod(builder);
var imageBytes = File.ReadAllBytes(ImageFilePath);
using var fs = new FileStream(ImageFilePath, FileMode.Open);
modBuilder.AddImage(fs, Path.GetExtension(ImageFilePath)!, "Sample Image");

// Act
var result = builder.Build(out var pack);
result.Position = 0;
var reader = new ReloadedPackReader(result);

// Assert
var newImage = reader.GetImage(reader.Pack.Items[0].ImageFiles[0].Path);
Assert.Equal(imageBytes, newImage);
}

private ReloadedPackBuilder BuildBaselinePack()
{
var builder = new ReloadedPackBuilder();
builder.SetName("Sample Package for Testing");
builder.SetReadme("## Sample Readme");
return builder;
}

private ReloadedPackItemBuilder AddSampleMod(ReloadedPackBuilder builder)
{
var modBuilder = builder.AddModItem(ModId);
modBuilder.SetName("Sample Mod");
modBuilder.SetReadme("Sample Readme");
return modBuilder;
}
}
29 changes: 29 additions & 0 deletions source/Reloaded.Mod.Loader.Update/Packs/ReloadedPack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Reloaded.Mod.Loader.Update.Packs;

/// <summary>
/// Represents a pack that contains a collection of mods to be installed.
/// </summary>
[Equals(DoNotAddEqualityOperators = true)]
public class ReloadedPack : IConfig
{
/// <summary>
/// Name of the pack.
/// </summary>
public string Name { get; set; } = String.Empty;

/// <summary>
/// Readme for the pack, in markdown format.
/// </summary>
public string Readme { get; set; } = String.Empty;

/// <summary>
/// List of preview image files belonging to the pack.
/// May be PNG, JPEG & JXL (JPEG XL).
/// </summary>
public List<ReloadedPackImage> ImageFiles { get; set; } = new();

/// <summary>
/// Items associated with this pack.
/// </summary>
public List<ReloadedPackItem> Items { get; set; } = new();
}
97 changes: 97 additions & 0 deletions source/Reloaded.Mod.Loader.Update/Packs/ReloadedPackBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
namespace Reloaded.Mod.Loader.Update.Packs;

/// <summary>
/// Class that can be used to help build Reloaded packs.
/// Uses a fluent API.
/// </summary>
public class ReloadedPackBuilder
{
private string _name;
private string _readme;
private List<(Stream stream, string name, string cap)> _images = new();
private int _imageIndex = 0;
private List<ReloadedPackItemBuilder> _itemBuilders = new();

/// <summary>
/// Sets the name for this Reloaded pack.
/// </summary>
public ReloadedPackBuilder SetName(string name)
{
_name = name;
return this;
}

/// <summary>
/// Sets the markdown readme for this Reloaded pack.
/// </summary>
public ReloadedPackBuilder SetReadme(string readme)
{
_readme = readme;
return this;
}

/// <summary>
/// Adds an image to the pack.
/// </summary>
/// <param name="imageData">Stream containing the image data.</param>
/// <param name="extension">Extension of the image, with the dot.</param>
/// <param name="caption">Caption for the image.</param>
public ReloadedPackBuilder AddImage(Stream imageData, string extension, string? caption)
{
_images.Add((imageData, $"Main_{_imageIndex++}{extension}", caption));
return this;
}

/// <summary>
/// Adds a mod to this package, returning a builder that allows customization of this mod.
/// </summary>
/// <param name="modId">ID of the mod.</param>
/// <returns>Builder for the individual pack item.</returns>
public ReloadedPackItemBuilder AddModItem(string modId)
{
var itemBuilder = new ReloadedPackItemBuilder(modId);
_itemBuilders.Add(itemBuilder);
return itemBuilder;
}

/// <summary>
/// Creates the Reloaded package.
/// </summary>
/// <returns>Stream containing the final ZIP archive.</returns>
public MemoryStream Build(out ReloadedPack package)
{
// Note: zips by standard should use forward slash.
var memStream = new MemoryStream();
using var archive = new ZipArchive(memStream, ZipArchiveMode.Create, true);

// Create Main Package
package = new ReloadedPack();
package.Name = _name;
package.Readme = _readme;
foreach (var image in _images)
{
package.ImageFiles.Add(new ReloadedPackImage()
{
Path = image.name,
Caption = image.cap,
});

// We use no compression assuming images are already compressed well.
var entry = archive.CreateEntry(Routes.GetImagePath(image.name), CompressionLevel.NoCompression);
using var entryStream = entry.Open();
image.stream.CopyTo(entryStream);
}

// Create Mod Entries
foreach (var builder in _itemBuilders)
builder.Build(package, archive);

// But compress the config
var configEntry = archive.CreateEntry(Routes.Config, CompressionLevel.Optimal);
using var configEntryStream = configEntry.Open();
var writer = new Utf8JsonWriter(configEntryStream);
JsonSerializer.Serialize(writer, package);

return memStream;
}
}
20 changes: 20 additions & 0 deletions source/Reloaded.Mod.Loader.Update/Packs/ReloadedPackImage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Reloaded.Mod.Loader.Update.Packs;

/// <summary>
/// Represents a singular image stored inside the Reloaded package.
/// </summary>
[Equals(DoNotAddEqualityOperators = true)]
public struct ReloadedPackImage
{
// TODO: [NET7] Restore constructor with guarantee of non-null username. Right now we can't because it breaks built-in System.Text.Json. This is fixed in newer versions.

/// <summary>
/// Path of the image inside the archive.
/// </summary>
public string Path { get; set; }

/// <summary>
/// Caption of the image.
/// </summary>
public string Caption { get; set; }
}
36 changes: 36 additions & 0 deletions source/Reloaded.Mod.Loader.Update/Packs/ReloadedPackItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace Reloaded.Mod.Loader.Update.Packs;

/// <summary>
/// Represents an individual item contained in a Reloaded pack.
/// </summary>
[Equals(DoNotAddEqualityOperators = true)]
public class ReloadedPackItem
{
/// <summary>
/// Name of the mod represented by this item.
/// [Shown in UI]
/// </summary>
public string Name { get; set; } = String.Empty;

/// <summary>
/// ID of the mod contained.
/// [Shown in UI]
/// </summary>
public string ModId { get; set; } = String.Empty;

/// <summary>
/// Readme for this mod, in markdown format.
/// </summary>
public string Readme { get; set; } = String.Empty;

/// <summary>
/// List of preview image files belonging to this item.
/// May be PNG, JPEG & JXL (JPEG XL).
/// </summary>
public List<ReloadedPackImage> ImageFiles { get; set; } = new();

/// <summary>
/// Copied from <see cref="ModConfig.PluginData"/>. Contains info on how to download the mod.
/// </summary>
public Dictionary<string, object> PluginData { get; set; } = new();
}
Loading

0 comments on commit df2b56f

Please sign in to comment.