Skip to content

Commit cfffe79

Browse files
committed
Automatically resize and copy custom game icon/banner
1 parent 61436cf commit cfffe79

7 files changed

Lines changed: 151 additions & 26 deletions

File tree

‎src/RayCarrot.RCP.Metro/App/Constants/AppFilePaths.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public static class AppFilePaths
4949
/// </summary>
5050
public static FileSystemPath IconsPath => UserDataBaseDir + "Icons";
5151

52+
/// <summary>
53+
/// The game assets path
54+
/// </summary>
55+
public static FileSystemPath GameAssetsPath => UserDataBaseDir + "GameAssets";
56+
5257
/// <summary>
5358
/// The temporary files path
5459
/// </summary>

‎src/RayCarrot.RCP.Metro/Games/GameInstallationExtensions.cs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ public static LocalizedString GetDisplayName(this GameInstallation gameInstallat
1717
: gameInstallation.GameDescriptor.DisplayName;
1818
}
1919

20-
public static string GetIconAssetSource(this GameInstallation gameInstallation)
20+
public static object GetIconAssetSource(this GameInstallation gameInstallation)
2121
{
2222
// Get the custom icon image
2323
if (gameInstallation.GetValue<string?>(GameDataKey.RCP_IconImage) is { } iconImage)
2424
{
2525
// Return if it exists
2626
if (File.Exists(iconImage))
27-
return iconImage;
27+
return BitmapImageHelpers.CreateFromFile(iconImage);
2828

2929
// Remove if it does not exist
3030
gameInstallation.SetValue<string?>(GameDataKey.RCP_IconImage, null);
@@ -35,14 +35,14 @@ public static string GetIconAssetSource(this GameInstallation gameInstallation)
3535
return gameInstallation.GameDescriptor.Icon.GetAssetPath();
3636
}
3737

38-
public static string GetBannerAssetSource(this GameInstallation gameInstallation)
38+
public static object GetBannerAssetSource(this GameInstallation gameInstallation)
3939
{
4040
// Get the custom banner image
4141
if (gameInstallation.GetValue<string?>(GameDataKey.RCP_BannerImage) is { } bannerImage)
4242
{
4343
// Return if it exists
4444
if (File.Exists(bannerImage))
45-
return bannerImage;
45+
return BitmapImageHelpers.CreateFromFile(bannerImage);
4646

4747
// Remove if it does not exist
4848
gameInstallation.SetValue<string?>(GameDataKey.RCP_BannerImage, null);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Windows.Media.Imaging;
2+
3+
namespace RayCarrot.RCP.Metro;
4+
5+
public static class BitmapImageHelpers
6+
{
7+
public static BitmapImage CreateFromFile(string filePath)
8+
{
9+
BitmapImage img = new();
10+
img.BeginInit();
11+
img.CreateOptions |= BitmapCreateOptions.IgnoreImageCache;
12+
img.CacheOption = BitmapCacheOption.OnLoad; // Required to allow the file to be deleted, such as if a temp file
13+
img.UriSource = new Uri(filePath);
14+
img.EndInit();
15+
16+
if (img.CanFreeze)
17+
img.Freeze();
18+
19+
return img;
20+
}
21+
}

‎src/RayCarrot.RCP.Metro/ModLoader/Dialogs/ModLoader/DownloadedModViewModel.cs‎

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System.Windows.Input;
22
using System.Windows.Media;
3-
using System.Windows.Media.Imaging;
43
using RayCarrot.RCP.Metro.ModLoader.Library;
54
using RayCarrot.RCP.Metro.ModLoader.Metadata;
65
using RayCarrot.RCP.Metro.ModLoader.Sources;
@@ -103,17 +102,7 @@ public void LoadThumbnail()
103102
return;
104103
}
105104

106-
BitmapImage thumb = new();
107-
thumb.BeginInit();
108-
thumb.CreateOptions |= BitmapCreateOptions.IgnoreImageCache;
109-
thumb.CacheOption = BitmapCacheOption.OnLoad; // Required to allow the file to be deleted, such as if a temp file
110-
thumb.UriSource = new Uri(thumbFilePath);
111-
thumb.EndInit();
112-
113-
if (thumb.CanFreeze)
114-
thumb.Freeze();
115-
116-
Thumbnail = thumb;
105+
Thumbnail = BitmapImageHelpers.CreateFromFile(thumbFilePath);
117106
}
118107
catch (Exception ex)
119108
{

‎src/RayCarrot.RCP.Metro/Pages/Games/GameHubControl.xaml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@
267267
</Button>
268268
<!-- TODO-LOC -->
269269
<Button Content="Replace icon image"
270+
ToolTip="The recommended image size is 128x128"
270271
Command="{Binding Path=ReplaceIconImageCommand, Mode=OneTime}"
271272
rcp:Icon.Kind="ImageOutline"
272273
rcp:Icon.Brush="{StaticResource RCP.Brushes.Info}"
@@ -278,6 +279,7 @@
278279
rcp:Icon.Brush="{StaticResource RCP.Brushes.Info}"
279280
HorizontalContentAlignment="Stretch" />
280281
<Button Content="Replace banner image"
282+
ToolTip="The recommended image size is 1024x512"
281283
Command="{Binding Path=ReplaceBannerImageCommand, Mode=OneTime}"
282284
rcp:Icon.Kind="ImageOutline"
283285
rcp:Icon.Brush="{StaticResource RCP.Brushes.Info}"

‎src/RayCarrot.RCP.Metro/Pages/Games/InstalledGameViewModel.cs‎

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Diagnostics.CodeAnalysis;
22
using System.IO;
33
using System.Windows.Input;
4+
using ImageMagick;
45
using Nito.AsyncEx;
56
using RayCarrot.RCP.Metro.Games.Components;
67
using RayCarrot.RCP.Metro.Games.Data;
@@ -109,9 +110,9 @@ public InstalledGameViewModel(GameInstallation gameInstallation, GameCategoryVie
109110
public LocalizedString PlatformDisplayName { get; }
110111
public GamePlatformIconAsset PlatformIcon { get; }
111112

112-
public string GameIcon { get; set; }
113+
public object GameIcon { get; set; }
113114
public bool HasCustomGameIcon { get; set; }
114-
public string GameBanner { get; set; }
115+
public object GameBanner { get; set; }
115116
public bool HasCustomGameBanner { get; set; }
116117

117118
public ObservableCollection<GamePanelViewModel> GamePanels { get; }
@@ -360,6 +361,29 @@ private void RefreshGameBanner()
360361
HasCustomGameBanner = GameInstallation.GetValue<string?>(GameDataKey.RCP_BannerImage) != null;
361362
}
362363

364+
private void CropToAspectRatio(MagickImage img, uint width, uint height)
365+
{
366+
double aspectRatio = width / (double)height;
367+
368+
uint newWidth;
369+
uint newHeight;
370+
if (img.Width >= aspectRatio * img.Height)
371+
{
372+
newHeight = img.Height;
373+
newWidth = (uint)(aspectRatio * newHeight);
374+
}
375+
else
376+
{
377+
newWidth = img.Width;
378+
newHeight = Math.Max(1, newWidth / 2);
379+
}
380+
381+
int cropX = (int)((img.Width - newWidth) / 2);
382+
int cropY = (int)((img.Height - newHeight) / 2);
383+
384+
img.Crop(new MagickGeometry(cropX, cropY, newWidth, newHeight));
385+
}
386+
363387
#endregion
364388

365389
#region Public Methods
@@ -588,13 +612,55 @@ public async Task ReplaceIconImageAsync()
588612
if (result.CanceledByUser)
589613
return;
590614

591-
GameInstallation.SetValue<string>(GameDataKey.RCP_IconImage, result.SelectedFile);
592-
Services.Messenger.Send(new ModifiedGameIconMessage(GameInstallation));
593-
RefreshGameIcon();
615+
try
616+
{
617+
FileSystemPath dstFilePath = AppFilePaths.GameAssetsPath + GameInstallation.InstallationId + "GameIcon.png";
618+
619+
using MagickImage img = new(result.SelectedFile);
620+
CropToAspectRatio(img, 128, 128);
621+
img.Resize(128, 128);
622+
623+
// Save the modified file to the game assets directory
624+
Directory.CreateDirectory(dstFilePath.Parent);
625+
await img.WriteAsync(dstFilePath);
626+
627+
// Store as a modified file so it automatically gets removed with the game
628+
GameInstallation.ModifyObject<AddedGameFiles>(GameDataKey.RCP_AddedFiles, x => x.Files.Add(dstFilePath));
629+
630+
GameInstallation.SetValue<string>(GameDataKey.RCP_IconImage, dstFilePath);
631+
Services.Messenger.Send(new ModifiedGameIconMessage(GameInstallation));
632+
RefreshGameIcon();
633+
}
634+
catch (Exception ex)
635+
{
636+
Logger.Error(ex, "Replacing icon image for {0}", GameInstallation.FullId);
637+
638+
// TODO-LOC
639+
await Services.MessageUI.DisplayExceptionMessageAsync(ex, "An error occurred when replacing the icon image. Make sure the image file is valid and try again.");
640+
}
594641
}
595642

596643
public void RemoveCustomIconImage()
597644
{
645+
// Delete the icon if it was added
646+
FileSystemPath iconPath = GameInstallation.GetValue<string?>(GameDataKey.RCP_IconImage);
647+
if (iconPath.FileExists)
648+
{
649+
AddedGameFiles? addedFiles = GameInstallation.GetObject<AddedGameFiles>(GameDataKey.RCP_AddedFiles);
650+
if (addedFiles != null && addedFiles.Files.Contains(iconPath))
651+
{
652+
try
653+
{
654+
Services.File.DeleteFile(iconPath);
655+
addedFiles.Files.Remove(iconPath);
656+
}
657+
catch (Exception ex)
658+
{
659+
Logger.Error(ex, "Deleting custom game icon file");
660+
}
661+
}
662+
}
663+
598664
GameInstallation.SetValue<string?>(GameDataKey.RCP_IconImage, null);
599665
Services.Messenger.Send(new ModifiedGameIconMessage(GameInstallation));
600666
RefreshGameIcon();
@@ -612,12 +678,54 @@ public async Task ReplaceBannerImageAsync()
612678
if (result.CanceledByUser)
613679
return;
614680

615-
GameInstallation.SetValue<string>(GameDataKey.RCP_BannerImage, result.SelectedFile);
616-
RefreshGameBanner();
681+
try
682+
{
683+
FileSystemPath dstFilePath = AppFilePaths.GameAssetsPath + GameInstallation.InstallationId + "GameBanner.png";
684+
685+
using MagickImage img = new(result.SelectedFile);
686+
CropToAspectRatio(img, 1024, 512);
687+
img.Resize(1024, 512);
688+
689+
// Save the modified file to the game assets directory
690+
Directory.CreateDirectory(dstFilePath.Parent);
691+
await img.WriteAsync(dstFilePath);
692+
693+
// Store as a modified file so it automatically gets removed with the game
694+
GameInstallation.ModifyObject<AddedGameFiles>(GameDataKey.RCP_AddedFiles, x => x.Files.Add(dstFilePath));
695+
696+
GameInstallation.SetValue<string>(GameDataKey.RCP_BannerImage, dstFilePath);
697+
RefreshGameBanner();
698+
}
699+
catch (Exception ex)
700+
{
701+
Logger.Error(ex, "Replacing banner image for {0}", GameInstallation.FullId);
702+
703+
// TODO-LOC
704+
await Services.MessageUI.DisplayExceptionMessageAsync(ex, "An error occurred when replacing the banner image. Make sure the image file is valid and try again.");
705+
}
617706
}
618707

619708
public void RemoveCustomBannerImage()
620709
{
710+
// Delete the banner if it was added
711+
FileSystemPath bannerPath = GameInstallation.GetValue<string?>(GameDataKey.RCP_BannerImage);
712+
if (bannerPath.FileExists)
713+
{
714+
AddedGameFiles? addedFiles = GameInstallation.GetObject<AddedGameFiles>(GameDataKey.RCP_AddedFiles);
715+
if (addedFiles != null && addedFiles.Files.Contains(bannerPath))
716+
{
717+
try
718+
{
719+
Services.File.DeleteFile(bannerPath);
720+
addedFiles.Files.Remove(bannerPath);
721+
}
722+
catch (Exception ex)
723+
{
724+
Logger.Error(ex, "Deleting custom game banner file");
725+
}
726+
}
727+
}
728+
621729
GameInstallation.SetValue<string?>(GameDataKey.RCP_BannerImage, null);
622730
RefreshGameBanner();
623731
}

‎src/RayCarrot.RCP.Metro/UI/Controls/GameTitle/GameTitle.cs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,14 @@ public GameIconSize GameIconSize
121121

122122
#region GameIcon
123123

124-
public string? GameIcon
124+
public object? GameIcon
125125
{
126-
get => (string?)GetValue(GameIconProperty);
126+
get => (object?)GetValue(GameIconProperty);
127127
private set => SetValue(GameIconPropertyKey, value);
128128
}
129129

130130
private static readonly DependencyPropertyKey GameIconPropertyKey =
131-
DependencyProperty.RegisterReadOnly(nameof(GameIcon), typeof(string), typeof(GameTitle), new FrameworkPropertyMetadata());
131+
DependencyProperty.RegisterReadOnly(nameof(GameIcon), typeof(object), typeof(GameTitle), new FrameworkPropertyMetadata());
132132

133133
public static readonly DependencyProperty GameIconProperty = GameIconPropertyKey.DependencyProperty;
134134

0 commit comments

Comments
 (0)