mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-12-15 10:36:58 +00:00
Separate firmware avatar loading from the selector view model
This is going to be used for the setup wizard (probably)
This commit is contained in:
parent
c8c5b9bbfb
commit
ba9334e73d
2 changed files with 187 additions and 148 deletions
172
src/Ryujinx/UI/Models/FirmwareAvatarCache.cs
Normal file
172
src/Ryujinx/UI/Models/FirmwareAvatarCache.cs
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Ncm;
|
||||||
|
using LibHac.Tools.Fs;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Models
|
||||||
|
{
|
||||||
|
public class FirmwareAvatarCache : BaseModel, IReadOnlyDictionary<string, byte[]>
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, byte[]> _backing = new();
|
||||||
|
|
||||||
|
public FirmwareAvatarCache(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
|
||||||
|
{
|
||||||
|
string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data);
|
||||||
|
string avatarPath = VirtualFileSystem.SwitchPathToSystemPath(contentPath);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(avatarPath))
|
||||||
|
{
|
||||||
|
using IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open);
|
||||||
|
|
||||||
|
Nca nca = new(virtualFileSystem.KeySet, ncaFileStream);
|
||||||
|
IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
||||||
|
|
||||||
|
foreach (DirectoryEntryEx item in romfs.EnumerateEntries())
|
||||||
|
{
|
||||||
|
// TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy.
|
||||||
|
if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs"))
|
||||||
|
{
|
||||||
|
using UniqueRef<IFile> file = new();
|
||||||
|
|
||||||
|
romfs.OpenFile(ref file.Ref, ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
using MemoryStream stream = new();
|
||||||
|
using MemoryStream streamPng = new();
|
||||||
|
|
||||||
|
file.Get.AsStream().CopyTo(stream);
|
||||||
|
|
||||||
|
stream.Position = 0;
|
||||||
|
|
||||||
|
SKImage avatarImage = SKImage.FromPixelCopy(new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul), DecompressYaz0(stream));
|
||||||
|
|
||||||
|
using (SKData data = avatarImage.Encode(SKEncodedImageFormat.Png, 100))
|
||||||
|
{
|
||||||
|
data.SaveTo(streamPng);
|
||||||
|
}
|
||||||
|
|
||||||
|
_backing[item.FullPath] = streamPng.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ProfileImageModel> CreateProfileImageModels()
|
||||||
|
=> this.Select(x => new ProfileImageModel(x.Key, x.Value));
|
||||||
|
|
||||||
|
private static byte[] DecompressYaz0(MemoryStream stream)
|
||||||
|
{
|
||||||
|
using BinaryReader reader = new(stream);
|
||||||
|
|
||||||
|
reader.ReadInt32(); // Magic
|
||||||
|
|
||||||
|
uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
|
||||||
|
|
||||||
|
reader.ReadInt64(); // Padding
|
||||||
|
|
||||||
|
byte[] input = new byte[stream.Length - stream.Position];
|
||||||
|
stream.ReadExactly(input, 0, input.Length);
|
||||||
|
|
||||||
|
uint inputOffset = 0;
|
||||||
|
|
||||||
|
byte[] output = new byte[decodedLength];
|
||||||
|
uint outputOffset = 0;
|
||||||
|
|
||||||
|
ushort mask = 0;
|
||||||
|
byte header = 0;
|
||||||
|
|
||||||
|
while (outputOffset < decodedLength)
|
||||||
|
{
|
||||||
|
if ((mask >>= 1) == 0)
|
||||||
|
{
|
||||||
|
header = input[inputOffset++];
|
||||||
|
mask = 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((header & mask) != 0)
|
||||||
|
{
|
||||||
|
if (outputOffset == output.Length)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
output[outputOffset++] = input[inputOffset++];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
byte byte1 = input[inputOffset++];
|
||||||
|
byte byte2 = input[inputOffset++];
|
||||||
|
|
||||||
|
uint dist = (uint)((byte1 & 0xF) << 8) | byte2;
|
||||||
|
uint position = outputOffset - (dist + 1);
|
||||||
|
|
||||||
|
uint length = (uint)byte1 >> 4;
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
length = (uint)input[inputOffset++] + 0x12;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
length += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint gap = outputOffset - position;
|
||||||
|
uint nonOverlappingLength = length;
|
||||||
|
|
||||||
|
if (nonOverlappingLength > gap)
|
||||||
|
{
|
||||||
|
nonOverlappingLength = gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength);
|
||||||
|
outputOffset += nonOverlappingLength;
|
||||||
|
position += nonOverlappingLength;
|
||||||
|
length -= nonOverlappingLength;
|
||||||
|
|
||||||
|
while (length-- > 0)
|
||||||
|
{
|
||||||
|
output[outputOffset++] = output[position++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region dictionary impl
|
||||||
|
|
||||||
|
IEnumerator<KeyValuePair<string, byte[]>> IEnumerable<KeyValuePair<string, byte[]>>.GetEnumerator()
|
||||||
|
{
|
||||||
|
return (_backing as IEnumerable<KeyValuePair<string, byte[]>>).GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator GetEnumerator()
|
||||||
|
{
|
||||||
|
return ((IEnumerable)_backing).GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _backing.Count;
|
||||||
|
public bool ContainsKey(string key) => _backing.ContainsKey(key);
|
||||||
|
|
||||||
|
public bool TryGetValue(string key, out byte[] value) => _backing.TryGetValue(key, out value);
|
||||||
|
|
||||||
|
public byte[] this[string key] => _backing[key];
|
||||||
|
|
||||||
|
public IEnumerable<string> Keys => _backing.Keys;
|
||||||
|
public IEnumerable<byte[]> Values => _backing.Values;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,46 +1,36 @@
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using LibHac.Common;
|
using DynamicData;
|
||||||
using LibHac.Fs;
|
|
||||||
using LibHac.Fs.Fsa;
|
|
||||||
using LibHac.FsSystem;
|
|
||||||
using LibHac.Ncm;
|
|
||||||
using LibHac.Tools.Fs;
|
|
||||||
using LibHac.Tools.FsSystem;
|
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using SkiaSharp;
|
|
||||||
using System;
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
|
||||||
using Color = Avalonia.Media.Color;
|
using Color = Avalonia.Media.Color;
|
||||||
using Image = SkiaSharp.SKImage;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.ViewModels
|
namespace Ryujinx.Ava.UI.ViewModels
|
||||||
{
|
{
|
||||||
public partial class UserFirmwareAvatarSelectorViewModel : BaseModel
|
public partial class UserFirmwareAvatarSelectorViewModel : BaseModel
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, byte[]> _avatarStore = new();
|
private static FirmwareAvatarCache _avatarCache;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial ObservableCollection<ProfileImageModel> Images { get; set; }
|
public partial ObservableCollection<ProfileImageModel> Images { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
public Color BackgroundColor
|
||||||
public partial Color BackgroundColor { get; set; } = Colors.White;
|
{
|
||||||
|
get;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
field = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
ChangeImageBackground();
|
||||||
|
}
|
||||||
|
} = Colors.White;
|
||||||
|
|
||||||
public UserFirmwareAvatarSelectorViewModel()
|
public UserFirmwareAvatarSelectorViewModel()
|
||||||
{
|
{
|
||||||
Images = [];
|
Images = [];
|
||||||
|
|
||||||
LoadImagesFromStore();
|
LoadImagesFromStore();
|
||||||
PropertyChanged += (_, args) =>
|
|
||||||
{
|
|
||||||
if (args.PropertyName == nameof(BackgroundColor))
|
|
||||||
ChangeImageBackground();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int SelectedIndex
|
public int SelectedIndex
|
||||||
|
|
@ -65,10 +55,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
{
|
{
|
||||||
Images.Clear();
|
Images.Clear();
|
||||||
|
|
||||||
foreach (KeyValuePair<string, byte[]> image in _avatarStore)
|
Images.AddRange(_avatarCache.CreateProfileImageModels());
|
||||||
{
|
|
||||||
Images.Add(new ProfileImageModel(image.Key, image.Value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ChangeImageBackground()
|
private void ChangeImageBackground()
|
||||||
|
|
@ -81,127 +68,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
|
public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
|
||||||
{
|
{
|
||||||
if (_avatarStore.Count > 0)
|
_avatarCache ??= new FirmwareAvatarCache(contentManager, virtualFileSystem);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data);
|
|
||||||
string avatarPath = VirtualFileSystem.SwitchPathToSystemPath(contentPath);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(avatarPath))
|
|
||||||
{
|
|
||||||
using IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open);
|
|
||||||
|
|
||||||
Nca nca = new(virtualFileSystem.KeySet, ncaFileStream);
|
|
||||||
IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
|
||||||
|
|
||||||
foreach (DirectoryEntryEx item in romfs.EnumerateEntries())
|
|
||||||
{
|
|
||||||
// TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy.
|
|
||||||
if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs"))
|
|
||||||
{
|
|
||||||
using UniqueRef<IFile> file = new();
|
|
||||||
|
|
||||||
romfs.OpenFile(ref file.Ref, ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
using MemoryStream stream = new();
|
|
||||||
using MemoryStream streamPng = new();
|
|
||||||
|
|
||||||
file.Get.AsStream().CopyTo(stream);
|
|
||||||
|
|
||||||
stream.Position = 0;
|
|
||||||
|
|
||||||
Image avatarImage = Image.FromPixelCopy(new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul), DecompressYaz0(stream));
|
|
||||||
|
|
||||||
using (SKData data = avatarImage.Encode(SKEncodedImageFormat.Png, 100))
|
|
||||||
{
|
|
||||||
data.SaveTo(streamPng);
|
|
||||||
}
|
|
||||||
|
|
||||||
_avatarStore.Add(item.FullPath, streamPng.ToArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] DecompressYaz0(MemoryStream stream)
|
|
||||||
{
|
|
||||||
using BinaryReader reader = new(stream);
|
|
||||||
|
|
||||||
reader.ReadInt32(); // Magic
|
|
||||||
|
|
||||||
uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
|
|
||||||
|
|
||||||
reader.ReadInt64(); // Padding
|
|
||||||
|
|
||||||
byte[] input = new byte[stream.Length - stream.Position];
|
|
||||||
stream.ReadExactly(input, 0, input.Length);
|
|
||||||
|
|
||||||
uint inputOffset = 0;
|
|
||||||
|
|
||||||
byte[] output = new byte[decodedLength];
|
|
||||||
uint outputOffset = 0;
|
|
||||||
|
|
||||||
ushort mask = 0;
|
|
||||||
byte header = 0;
|
|
||||||
|
|
||||||
while (outputOffset < decodedLength)
|
|
||||||
{
|
|
||||||
if ((mask >>= 1) == 0)
|
|
||||||
{
|
|
||||||
header = input[inputOffset++];
|
|
||||||
mask = 0x80;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((header & mask) != 0)
|
|
||||||
{
|
|
||||||
if (outputOffset == output.Length)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
output[outputOffset++] = input[inputOffset++];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
byte byte1 = input[inputOffset++];
|
|
||||||
byte byte2 = input[inputOffset++];
|
|
||||||
|
|
||||||
uint dist = (uint)((byte1 & 0xF) << 8) | byte2;
|
|
||||||
uint position = outputOffset - (dist + 1);
|
|
||||||
|
|
||||||
uint length = (uint)byte1 >> 4;
|
|
||||||
if (length == 0)
|
|
||||||
{
|
|
||||||
length = (uint)input[inputOffset++] + 0x12;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
length += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint gap = outputOffset - position;
|
|
||||||
uint nonOverlappingLength = length;
|
|
||||||
|
|
||||||
if (nonOverlappingLength > gap)
|
|
||||||
{
|
|
||||||
nonOverlappingLength = gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength);
|
|
||||||
outputOffset += nonOverlappingLength;
|
|
||||||
position += nonOverlappingLength;
|
|
||||||
length -= nonOverlappingLength;
|
|
||||||
|
|
||||||
while (length-- > 0)
|
|
||||||
{
|
|
||||||
output[outputOffset++] = output[position++];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue