/ src / Ryujinx / UI / ViewModels / UserFirmwareAvatarSelectorViewModel.cs
UserFirmwareAvatarSelectorViewModel.cs
  1  using Avalonia.Media;
  2  using LibHac.Common;
  3  using LibHac.Fs;
  4  using LibHac.Fs.Fsa;
  5  using LibHac.FsSystem;
  6  using LibHac.Ncm;
  7  using LibHac.Tools.Fs;
  8  using LibHac.Tools.FsSystem;
  9  using LibHac.Tools.FsSystem.NcaUtils;
 10  using Ryujinx.Ava.UI.Models;
 11  using Ryujinx.HLE.FileSystem;
 12  using SkiaSharp;
 13  using System;
 14  using System.Buffers.Binary;
 15  using System.Collections.Generic;
 16  using System.Collections.ObjectModel;
 17  using System.IO;
 18  using Color = Avalonia.Media.Color;
 19  using Image = SkiaSharp.SKImage;
 20  
 21  namespace Ryujinx.Ava.UI.ViewModels
 22  {
 23      internal class UserFirmwareAvatarSelectorViewModel : BaseModel
 24      {
 25          private static readonly Dictionary<string, byte[]> _avatarStore = new();
 26  
 27          private ObservableCollection<ProfileImageModel> _images;
 28          private Color _backgroundColor = Colors.White;
 29  
 30          private int _selectedIndex;
 31  
 32          public UserFirmwareAvatarSelectorViewModel()
 33          {
 34              _images = new ObservableCollection<ProfileImageModel>();
 35  
 36              LoadImagesFromStore();
 37          }
 38  
 39          public Color BackgroundColor
 40          {
 41              get => _backgroundColor;
 42              set
 43              {
 44                  _backgroundColor = value;
 45                  OnPropertyChanged();
 46                  ChangeImageBackground();
 47              }
 48          }
 49  
 50          public ObservableCollection<ProfileImageModel> Images
 51          {
 52              get => _images;
 53              set
 54              {
 55                  _images = value;
 56                  OnPropertyChanged();
 57              }
 58          }
 59  
 60          public int SelectedIndex
 61          {
 62              get => _selectedIndex;
 63              set
 64              {
 65                  _selectedIndex = value;
 66  
 67                  if (_selectedIndex == -1)
 68                  {
 69                      SelectedImage = null;
 70                  }
 71                  else
 72                  {
 73                      SelectedImage = _images[_selectedIndex].Data;
 74                  }
 75  
 76                  OnPropertyChanged();
 77              }
 78          }
 79  
 80          public byte[] SelectedImage { get; private set; }
 81  
 82          private void LoadImagesFromStore()
 83          {
 84              Images.Clear();
 85  
 86              foreach (var image in _avatarStore)
 87              {
 88                  Images.Add(new ProfileImageModel(image.Key, image.Value));
 89              }
 90          }
 91  
 92          private void ChangeImageBackground()
 93          {
 94              foreach (var image in Images)
 95              {
 96                  image.BackgroundColor = new SolidColorBrush(BackgroundColor);
 97              }
 98          }
 99  
100          public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
101          {
102              if (_avatarStore.Count > 0)
103              {
104                  return;
105              }
106  
107              string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data);
108              string avatarPath = VirtualFileSystem.SwitchPathToSystemPath(contentPath);
109  
110              if (!string.IsNullOrWhiteSpace(avatarPath))
111              {
112                  using IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open);
113  
114                  Nca nca = new(virtualFileSystem.KeySet, ncaFileStream);
115                  IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
116  
117                  foreach (DirectoryEntryEx item in romfs.EnumerateEntries())
118                  {
119                      // TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy.
120                      if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs"))
121                      {
122                          using var file = new UniqueRef<IFile>();
123  
124                          romfs.OpenFile(ref file.Ref, ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure();
125  
126                          using MemoryStream stream = new();
127                          using MemoryStream streamPng = new();
128  
129                          file.Get.AsStream().CopyTo(stream);
130  
131                          stream.Position = 0;
132  
133                          Image avatarImage = Image.FromPixelCopy(new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul), DecompressYaz0(stream));
134  
135                          using (SKData data = avatarImage.Encode(SKEncodedImageFormat.Png, 100))
136                          {
137                              data.SaveTo(streamPng);
138                          }
139  
140                          _avatarStore.Add(item.FullPath, streamPng.ToArray());
141                      }
142                  }
143              }
144          }
145  
146          private static byte[] DecompressYaz0(Stream stream)
147          {
148              using BinaryReader reader = new(stream);
149  
150              reader.ReadInt32(); // Magic
151  
152              uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
153  
154              reader.ReadInt64(); // Padding
155  
156              byte[] input = new byte[stream.Length - stream.Position];
157              stream.ReadExactly(input, 0, input.Length);
158  
159              uint inputOffset = 0;
160  
161              byte[] output = new byte[decodedLength];
162              uint outputOffset = 0;
163  
164              ushort mask = 0;
165              byte header = 0;
166  
167              while (outputOffset < decodedLength)
168              {
169                  if ((mask >>= 1) == 0)
170                  {
171                      header = input[inputOffset++];
172                      mask = 0x80;
173                  }
174  
175                  if ((header & mask) != 0)
176                  {
177                      if (outputOffset == output.Length)
178                      {
179                          break;
180                      }
181  
182                      output[outputOffset++] = input[inputOffset++];
183                  }
184                  else
185                  {
186                      byte byte1 = input[inputOffset++];
187                      byte byte2 = input[inputOffset++];
188  
189                      uint dist = (uint)((byte1 & 0xF) << 8) | byte2;
190                      uint position = outputOffset - (dist + 1);
191  
192                      uint length = (uint)byte1 >> 4;
193                      if (length == 0)
194                      {
195                          length = (uint)input[inputOffset++] + 0x12;
196                      }
197                      else
198                      {
199                          length += 2;
200                      }
201  
202                      uint gap = outputOffset - position;
203                      uint nonOverlappingLength = length;
204  
205                      if (nonOverlappingLength > gap)
206                      {
207                          nonOverlappingLength = gap;
208                      }
209  
210                      Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength);
211                      outputOffset += nonOverlappingLength;
212                      position += nonOverlappingLength;
213                      length -= nonOverlappingLength;
214  
215                      while (length-- > 0)
216                      {
217                          output[outputOffset++] = output[position++];
218                      }
219                  }
220              }
221  
222              return output;
223          }
224      }
225  }