Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions WheelWizard.Test/WheelWizard.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0-preview.3.25171.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0"/>
<PackageReference Include="NSubstitute" Version="5.3.0"/>
<PackageReference Include="Testably.Abstractions.Testing" Version="4.0.1" />
Expand Down
25 changes: 25 additions & 0 deletions WheelWizard/Features/MiiImages/Domain/IMiiIMagesApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Avalonia.Media.Imaging;
using Refit;

namespace WheelWizard.MiiImages.Domain;

public interface IMiiIMagesApi
{
[Get("/miis/image.png")]
Task<Stream> GetImageAsync(
string data,
string type,
string expression,
int width,
int characterXRotate = 0,
int characterYRotate = 0,
int characterZRotate = 0,
string bgColor = "FFFFFF00",
int instanceCount = 1,
int cameraXRotate = 0,
int cameraYRotate = 0,
int cameraZRotate = 0,
string lightDirectionMode = "none",
string instanceRotationMode = "model"
);
}
56 changes: 56 additions & 0 deletions WheelWizard/Features/MiiImages/Domain/MiiImageSpecifications.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Numerics;
using Avalonia.Media;
using Microsoft.Extensions.Caching.Memory;

namespace WheelWizard.MiiImages.Domain;

public class MiiImageSpecifications
{
public string Name { get; set; } = string.Empty;
public ImageSize Size { get; set; } = ImageSize.small;
public FaceExpression Expression { get; set; } = FaceExpression.normal;
public BodyType Type { get; set; } = BodyType.face;
public string BackgroundColor = "FFFFFF00";
public int InstanceCount { get; set; } = 1;

// All between 0 and 360, obviously
public Vector3 CharacterRotate { get; set; } = Vector3.Zero;
public Vector3 CameraRotate { get; set; } = Vector3.Zero;

public TimeSpan? ExpirationSeconds { get; set; } = TimeSpan.FromMinutes(30);
public CacheItemPriority CachePriority { get; set; } = CacheItemPriority.Normal;

public override string ToString()
{
// If we put all the things in this string, then the Key at least is unique
return $"{Name}_{Size}{Expression}{Type}_{BackgroundColor}{InstanceCount}_{CharacterRotate}{CameraRotate}_{CachePriority}";
}

#region Enums

// IMPORTANT: keep these enums lowercase and with underscores
public enum FaceExpression
{
normal,
smile,
frustrated,
anger,
blink,
sorrow,
surprise,
}

public enum ImageSize
{
small = 270,
medium = 512,
}

public enum BodyType
{
face,
all_body,
}

#endregion
}
51 changes: 51 additions & 0 deletions WheelWizard/Features/MiiImages/Domain/MiiImageVariants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Numerics;
using Microsoft.Extensions.Caching.Memory;

namespace WheelWizard.MiiImages.Domain;

public static class MiiImageVariants
{
public static readonly MiiImageSpecifications CurrentUserSmall = new()
{
Name = "CurrentUserSmall",
Expression = MiiImageSpecifications.FaceExpression.normal,
Type = MiiImageSpecifications.BodyType.face,
Size = MiiImageSpecifications.ImageSize.small,
CachePriority = CacheItemPriority.High,
};

public static readonly MiiImageSpecifications OnlinePlayerSmall = new()
{
Name = "OnlinePlayerSmall",
Expression = MiiImageSpecifications.FaceExpression.normal,
Type = MiiImageSpecifications.BodyType.face,
Size = MiiImageSpecifications.ImageSize.small,
CachePriority = CacheItemPriority.Low,
};

public static readonly MiiImageSpecifications CurrentUserSideProfile = new()
{
Expression = MiiImageSpecifications.FaceExpression.normal,
Type = MiiImageSpecifications.BodyType.face,
Size = MiiImageSpecifications.ImageSize.medium,
CharacterRotate = new(350, 15, 355),
CameraRotate = new(12, 0, 0),
};
public static readonly MiiImageSpecifications FriendsSideProfile = new()
{
Expression = MiiImageSpecifications.FaceExpression.normal,
Type = MiiImageSpecifications.BodyType.face,
Size = MiiImageSpecifications.ImageSize.medium,
CharacterRotate = new(350, 15, 355),
CameraRotate = new(12, 0, 0),
ExpirationSeconds = TimeSpan.FromMinutes(60),
};

public static readonly MiiImageSpecifications FullBodyCarousel = new()
{
Expression = MiiImageSpecifications.FaceExpression.normal,
Type = MiiImageSpecifications.BodyType.all_body,
Size = MiiImageSpecifications.ImageSize.medium,
InstanceCount = 8,
};
}
16 changes: 16 additions & 0 deletions WheelWizard/Features/MiiImages/MiiImagesExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using WheelWizard.MiiImages.Domain;
using WheelWizard.Services;

namespace WheelWizard.MiiImages;

public static class MiiImagesExtensions
{
public static IServiceCollection AddMiiImages(this IServiceCollection services)
{
services.AddWhWzRefitApi<IMiiIMagesApi>(Endpoints.MiiImageAddress);

services.AddSingleton<IMiiImagesSingletonService, MiiImagesSingletonService>();

return services;
}
}
69 changes: 69 additions & 0 deletions WheelWizard/Features/MiiImages/MiiImagesSingletonService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Avalonia.Media.Imaging;
using Microsoft.Extensions.Caching.Memory;
using WheelWizard.MiiImages.Domain;
using WheelWizard.Shared.Services;
using WheelWizard.WiiManagement.Domain.Mii;

namespace WheelWizard.MiiImages;

public interface IMiiImagesSingletonService
{
Task<OperationResult<Bitmap>> GetImageAsync(Mii? mii, MiiImageSpecifications specifications);
}

public class MiiImagesSingletonService(IApiCaller<IMiiIMagesApi> apiCaller, IMemoryCache cache) : IMiiImagesSingletonService
{
public async Task<OperationResult<Bitmap>> GetImageAsync(Mii? mii, MiiImageSpecifications specifications)
{
var data = MiiStudioDataSerializer.Serialize(mii);
if (data.IsFailure)
return data.Error;

var miiConfigKey = data.Value + specifications;
var isCached = cache.TryGetValue(miiConfigKey, out Bitmap? cachedValue);
if (isCached)
return cachedValue ?? Fail<Bitmap>("Cached image is null.");

var newImageResult = await apiCaller.CallApiAsync(api => GetBitmapAsync(api, data.Value, specifications));
Bitmap? newImage = null;
if (newImageResult.IsSuccess)
newImage = newImageResult.Value;

using (var entry = cache.CreateEntry(miiConfigKey))
{
entry.Value = newImage;
entry.SlidingExpiration = specifications.ExpirationSeconds;
entry.Priority = specifications.CachePriority;
}

return newImage ?? Fail<Bitmap>("Failed to get new image.");
}

private static async Task<Bitmap> GetBitmapAsync(IMiiIMagesApi api, string data, MiiImageSpecifications specifications)
{
var result = await api.GetImageAsync(
data,
specifications.Type.ToString(),
specifications.Expression.ToString(),
(int)specifications.Size,
characterXRotate: (int)specifications.CharacterRotate.X,
characterYRotate: (int)specifications.CharacterRotate.Y,
characterZRotate: (int)specifications.CharacterRotate.Z,
bgColor: specifications.BackgroundColor,
instanceCount: specifications.InstanceCount,
cameraXRotate: (int)specifications.CameraRotate.X,
cameraYRotate: (int)specifications.CameraRotate.Y,
cameraZRotate: (int)specifications.CameraRotate.Z
);

using var memoryStream = new MemoryStream();
await result.CopyToAsync(memoryStream);
memoryStream.Position = 0; // Reset stream position for Bitmap constructor

if (memoryStream.Length == 0)
throw new InvalidOperationException("Received empty image stream.");

var bitmap = new Bitmap(memoryStream);
return bitmap;
}
}
Loading
Loading