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 }