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 }