/ src / Ryujinx / UI / ViewModels / DownloadableContentManagerViewModel.cs
DownloadableContentManagerViewModel.cs
  1  using Avalonia.Collections;
  2  using Avalonia.Controls.ApplicationLifetimes;
  3  using Avalonia.Platform.Storage;
  4  using Avalonia.Threading;
  5  using DynamicData;
  6  using LibHac.Common;
  7  using LibHac.Fs;
  8  using LibHac.Fs.Fsa;
  9  using LibHac.Tools.Fs;
 10  using LibHac.Tools.FsSystem;
 11  using LibHac.Tools.FsSystem.NcaUtils;
 12  using Ryujinx.Ava.Common.Locale;
 13  using Ryujinx.Ava.UI.Helpers;
 14  using Ryujinx.Ava.UI.Models;
 15  using Ryujinx.Common.Configuration;
 16  using Ryujinx.Common.Logging;
 17  using Ryujinx.Common.Utilities;
 18  using Ryujinx.HLE.FileSystem;
 19  using Ryujinx.HLE.Loaders.Processes.Extensions;
 20  using Ryujinx.HLE.Utilities;
 21  using Ryujinx.UI.App.Common;
 22  using System;
 23  using System.Collections.Generic;
 24  using System.IO;
 25  using System.Linq;
 26  using Application = Avalonia.Application;
 27  using Path = System.IO.Path;
 28  
 29  namespace Ryujinx.Ava.UI.ViewModels
 30  {
 31      public class DownloadableContentManagerViewModel : BaseModel
 32      {
 33          private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
 34          private readonly string _downloadableContentJsonPath;
 35  
 36          private readonly VirtualFileSystem _virtualFileSystem;
 37          private AvaloniaList<DownloadableContentModel> _downloadableContents = new();
 38          private AvaloniaList<DownloadableContentModel> _views = new();
 39          private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
 40  
 41          private string _search;
 42          private readonly ApplicationData _applicationData;
 43          private readonly IStorageProvider _storageProvider;
 44  
 45          private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 46  
 47          public AvaloniaList<DownloadableContentModel> DownloadableContents
 48          {
 49              get => _downloadableContents;
 50              set
 51              {
 52                  _downloadableContents = value;
 53                  OnPropertyChanged();
 54                  OnPropertyChanged(nameof(UpdateCount));
 55                  Sort();
 56              }
 57          }
 58  
 59          public AvaloniaList<DownloadableContentModel> Views
 60          {
 61              get => _views;
 62              set
 63              {
 64                  _views = value;
 65                  OnPropertyChanged();
 66              }
 67          }
 68  
 69          public AvaloniaList<DownloadableContentModel> SelectedDownloadableContents
 70          {
 71              get => _selectedDownloadableContents;
 72              set
 73              {
 74                  _selectedDownloadableContents = value;
 75                  OnPropertyChanged();
 76              }
 77          }
 78  
 79          public string Search
 80          {
 81              get => _search;
 82              set
 83              {
 84                  _search = value;
 85                  OnPropertyChanged();
 86                  Sort();
 87              }
 88          }
 89  
 90          public string UpdateCount
 91          {
 92              get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count);
 93          }
 94  
 95          public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
 96          {
 97              _virtualFileSystem = virtualFileSystem;
 98  
 99              _applicationData = applicationData;
100  
101              if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
102              {
103                  _storageProvider = desktop.MainWindow.StorageProvider;
104              }
105  
106              _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdBaseString, "dlc.json");
107  
108              if (!File.Exists(_downloadableContentJsonPath))
109              {
110                  _downloadableContentContainerList = new List<DownloadableContentContainer>();
111  
112                  Save();
113              }
114  
115              try
116              {
117                  _downloadableContentContainerList = JsonHelper.DeserializeFromFile(_downloadableContentJsonPath, _serializerContext.ListDownloadableContentContainer);
118              }
119              catch
120              {
121                  Logger.Error?.Print(LogClass.Configuration, "Downloadable Content JSON failed to deserialize.");
122                  _downloadableContentContainerList = new List<DownloadableContentContainer>();
123              }
124  
125              LoadDownloadableContents();
126          }
127  
128          private void LoadDownloadableContents()
129          {
130              foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
131              {
132                  if (File.Exists(downloadableContentContainer.ContainerPath))
133                  {
134                      using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(downloadableContentContainer.ContainerPath, _virtualFileSystem);
135  
136                      foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
137                      {
138                          using UniqueRef<IFile> ncaFile = new();
139  
140                          partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
141  
142                          Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
143                          if (nca != null)
144                          {
145                              var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"),
146                                  downloadableContentContainer.ContainerPath,
147                                  downloadableContentNca.FullPath,
148                                  downloadableContentNca.Enabled);
149  
150                              DownloadableContents.Add(content);
151  
152                              if (content.Enabled)
153                              {
154                                  SelectedDownloadableContents.Add(content);
155                              }
156  
157                              OnPropertyChanged(nameof(UpdateCount));
158                          }
159                      }
160                  }
161              }
162  
163              // NOTE: Try to load downloadable contents from PFS last to preserve enabled state.
164              AddDownloadableContent(_applicationData.Path);
165  
166              // NOTE: Save the list again to remove leftovers.
167              Save();
168              Sort();
169          }
170  
171          public void Sort()
172          {
173              DownloadableContents.AsObservableChangeSet()
174                  .Filter(Filter)
175                  .Bind(out var view).AsObservableList();
176  
177              _views.Clear();
178              _views.AddRange(view);
179              OnPropertyChanged(nameof(Views));
180          }
181  
182          private bool Filter(object arg)
183          {
184              if (arg is DownloadableContentModel content)
185              {
186                  return string.IsNullOrWhiteSpace(_search) || content.FileName.ToLower().Contains(_search.ToLower()) || content.TitleId.ToLower().Contains(_search.ToLower());
187              }
188  
189              return false;
190          }
191  
192          private Nca TryOpenNca(IStorage ncaStorage, string containerPath)
193          {
194              try
195              {
196                  return new Nca(_virtualFileSystem.KeySet, ncaStorage);
197              }
198              catch (Exception ex)
199              {
200                  Dispatcher.UIThread.InvokeAsync(async () =>
201                  {
202                      await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogLoadFileErrorMessage], ex.Message, containerPath));
203                  });
204              }
205  
206              return null;
207          }
208  
209          public async void Add()
210          {
211              var result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
212              {
213                  Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle],
214                  AllowMultiple = true,
215                  FileTypeFilter = new List<FilePickerFileType>
216                  {
217                      new("NSP")
218                      {
219                          Patterns = new[] { "*.nsp" },
220                          AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" },
221                          MimeTypes = new[] { "application/x-nx-nsp" },
222                      },
223                  },
224              });
225  
226              foreach (var file in result)
227              {
228                  if (!AddDownloadableContent(file.Path.LocalPath))
229                  {
230                      await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
231                  }
232              }
233          }
234  
235          private bool AddDownloadableContent(string path)
236          {
237              if (!File.Exists(path) || _downloadableContentContainerList.Any(x => x.ContainerPath == path))
238              {
239                  return true;
240              }
241  
242              using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem);
243  
244              bool success = false;
245              foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
246              {
247                  using var ncaFile = new UniqueRef<IFile>();
248  
249                  partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
250  
251                  Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path);
252                  if (nca == null)
253                  {
254                      continue;
255                  }
256  
257                  if (nca.Header.ContentType == NcaContentType.PublicData)
258                  {
259                      if (nca.GetProgramIdBase() != _applicationData.IdBase)
260                      {
261                          continue;
262                      }
263  
264                      var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true);
265                      DownloadableContents.Add(content);
266                      Dispatcher.UIThread.InvokeAsync(() => SelectedDownloadableContents.Add(content));
267  
268                      success = true;
269                  }
270              }
271  
272              if (success)
273              {
274                  OnPropertyChanged(nameof(UpdateCount));
275                  Sort();
276              }
277  
278              return success;
279          }
280  
281          public void Remove(DownloadableContentModel model)
282          {
283              DownloadableContents.Remove(model);
284              OnPropertyChanged(nameof(UpdateCount));
285              Sort();
286          }
287  
288          public void RemoveAll()
289          {
290              DownloadableContents.Clear();
291              OnPropertyChanged(nameof(UpdateCount));
292              Sort();
293          }
294  
295          public void EnableAll()
296          {
297              SelectedDownloadableContents = new(DownloadableContents);
298          }
299  
300          public void DisableAll()
301          {
302              SelectedDownloadableContents.Clear();
303          }
304  
305          public void Save()
306          {
307              _downloadableContentContainerList.Clear();
308  
309              DownloadableContentContainer container = default;
310  
311              foreach (DownloadableContentModel downloadableContent in DownloadableContents)
312              {
313                  if (container.ContainerPath != downloadableContent.ContainerPath)
314                  {
315                      if (!string.IsNullOrWhiteSpace(container.ContainerPath))
316                      {
317                          _downloadableContentContainerList.Add(container);
318                      }
319  
320                      container = new DownloadableContentContainer
321                      {
322                          ContainerPath = downloadableContent.ContainerPath,
323                          DownloadableContentNcaList = new List<DownloadableContentNca>(),
324                      };
325                  }
326  
327                  container.DownloadableContentNcaList.Add(new DownloadableContentNca
328                  {
329                      Enabled = downloadableContent.Enabled,
330                      TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16),
331                      FullPath = downloadableContent.FullPath,
332                  });
333              }
334  
335              if (!string.IsNullOrWhiteSpace(container.ContainerPath))
336              {
337                  _downloadableContentContainerList.Add(container);
338              }
339  
340              JsonHelper.SerializeToFile(_downloadableContentJsonPath, _downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer);
341          }
342  
343      }
344  }