/ src / Ryujinx / UI / ViewModels / TitleUpdateViewModel.cs
TitleUpdateViewModel.cs
  1  using Avalonia.Collections;
  2  using Avalonia.Controls.ApplicationLifetimes;
  3  using Avalonia.Platform.Storage;
  4  using Avalonia.Threading;
  5  using LibHac.Common;
  6  using LibHac.Fs;
  7  using LibHac.Fs.Fsa;
  8  using LibHac.Ncm;
  9  using LibHac.Ns;
 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 Ryujinx.UI.Common.Configuration;
 23  using System;
 24  using System.Collections.Generic;
 25  using System.IO;
 26  using System.Linq;
 27  using System.Threading.Tasks;
 28  using Application = Avalonia.Application;
 29  using ContentType = LibHac.Ncm.ContentType;
 30  using Path = System.IO.Path;
 31  using SpanHelpers = LibHac.Common.SpanHelpers;
 32  
 33  namespace Ryujinx.Ava.UI.ViewModels
 34  {
 35      public class TitleUpdateViewModel : BaseModel
 36      {
 37          public TitleUpdateMetadata TitleUpdateWindowData;
 38          public readonly string TitleUpdateJsonPath;
 39          private VirtualFileSystem VirtualFileSystem { get; }
 40          private ApplicationData ApplicationData { get; }
 41  
 42          private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
 43          private AvaloniaList<object> _views = new();
 44          private object _selectedUpdate;
 45  
 46          private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 47  
 48          public AvaloniaList<TitleUpdateModel> TitleUpdates
 49          {
 50              get => _titleUpdates;
 51              set
 52              {
 53                  _titleUpdates = value;
 54                  OnPropertyChanged();
 55              }
 56          }
 57  
 58          public AvaloniaList<object> Views
 59          {
 60              get => _views;
 61              set
 62              {
 63                  _views = value;
 64                  OnPropertyChanged();
 65              }
 66          }
 67  
 68          public object SelectedUpdate
 69          {
 70              get => _selectedUpdate;
 71              set
 72              {
 73                  _selectedUpdate = value;
 74                  OnPropertyChanged();
 75              }
 76          }
 77  
 78          public IStorageProvider StorageProvider;
 79  
 80          public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
 81          {
 82              VirtualFileSystem = virtualFileSystem;
 83  
 84              ApplicationData = applicationData;
 85  
 86              if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
 87              {
 88                  StorageProvider = desktop.MainWindow.StorageProvider;
 89              }
 90  
 91              TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdBaseString, "updates.json");
 92  
 93              try
 94              {
 95                  TitleUpdateWindowData = JsonHelper.DeserializeFromFile(TitleUpdateJsonPath, _serializerContext.TitleUpdateMetadata);
 96              }
 97              catch
 98              {
 99                  Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {ApplicationData.IdBaseString} at {TitleUpdateJsonPath}");
100  
101                  TitleUpdateWindowData = new TitleUpdateMetadata
102                  {
103                      Selected = "",
104                      Paths = new List<string>(),
105                  };
106  
107                  Save();
108              }
109  
110              LoadUpdates();
111          }
112  
113          private void LoadUpdates()
114          {
115              // Try to load updates from PFS first
116              AddUpdate(ApplicationData.Path, true);
117  
118              foreach (string path in TitleUpdateWindowData.Paths)
119              {
120                  AddUpdate(path);
121              }
122  
123              TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == TitleUpdateWindowData.Selected, null);
124  
125              SelectedUpdate = selected;
126  
127              // NOTE: Save the list again to remove leftovers.
128              Save();
129              SortUpdates();
130          }
131  
132          public void SortUpdates()
133          {
134              var sortedUpdates = TitleUpdates.OrderByDescending(update => update.Version);
135  
136              Views.Clear();
137              Views.Add(new BaseModel());
138              Views.AddRange(sortedUpdates);
139  
140              if (SelectedUpdate == null)
141              {
142                  SelectedUpdate = Views[0];
143              }
144              else if (!TitleUpdates.Contains(SelectedUpdate))
145              {
146                  if (Views.Count > 1)
147                  {
148                      SelectedUpdate = Views[1];
149                  }
150                  else
151                  {
152                      SelectedUpdate = Views[0];
153                  }
154              }
155          }
156  
157          private void AddUpdate(string path, bool ignoreNotFound = false, bool selected = false)
158          {
159              if (!File.Exists(path) || TitleUpdates.Any(x => x.Path == path))
160              {
161                  return;
162              }
163  
164              IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
165                  ? IntegrityCheckLevel.ErrorOnInvalid
166                  : IntegrityCheckLevel.None;
167  
168              try
169              {
170                  using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, VirtualFileSystem);
171  
172                  Dictionary<ulong, ContentMetaData> updates = pfs.GetContentData(ContentMetaType.Patch, VirtualFileSystem, checkLevel);
173  
174                  Nca patchNca = null;
175                  Nca controlNca = null;
176  
177                  if (updates.TryGetValue(ApplicationData.Id, out ContentMetaData content))
178                  {
179                      patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program);
180                      controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control);
181                  }
182  
183                  if (controlNca != null && patchNca != null)
184                  {
185                      ApplicationControlProperty controlData = new();
186  
187                      using UniqueRef<IFile> nacpFile = new();
188  
189                      controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
190                      nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
191  
192                      var displayVersion = controlData.DisplayVersionString.ToString();
193                      var update = new TitleUpdateModel(content.Version.Version, displayVersion, path);
194  
195                      TitleUpdates.Add(update);
196  
197                      if (selected)
198                      {
199                          Dispatcher.UIThread.InvokeAsync(() => SelectedUpdate = update);
200                      }
201                  }
202                  else
203                  {
204                      if (!ignoreNotFound)
205                      {
206                          Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
207                      }
208                  }
209              }
210              catch (Exception ex)
211              {
212                  Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path)));
213              }
214          }
215  
216          public void RemoveUpdate(TitleUpdateModel update)
217          {
218              TitleUpdates.Remove(update);
219  
220              SortUpdates();
221          }
222  
223          public async Task Add()
224          {
225              var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
226              {
227                  AllowMultiple = true,
228                  FileTypeFilter = new List<FilePickerFileType>
229                  {
230                      new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
231                      {
232                          Patterns = new[] { "*.nsp" },
233                          AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" },
234                          MimeTypes = new[] { "application/x-nx-nsp" },
235                      },
236                  },
237              });
238  
239              foreach (var file in result)
240              {
241                  AddUpdate(file.Path.LocalPath, selected: true);
242              }
243  
244              SortUpdates();
245          }
246  
247          public void Save()
248          {
249              TitleUpdateWindowData.Paths.Clear();
250              TitleUpdateWindowData.Selected = "";
251  
252              foreach (TitleUpdateModel update in TitleUpdates)
253              {
254                  TitleUpdateWindowData.Paths.Add(update.Path);
255  
256                  if (update == SelectedUpdate)
257                  {
258                      TitleUpdateWindowData.Selected = update.Path;
259                  }
260              }
261  
262              JsonHelper.SerializeToFile(TitleUpdateJsonPath, TitleUpdateWindowData, _serializerContext.TitleUpdateMetadata);
263          }
264      }
265  }