DownloadableContentManagerViewModel.cs 12 KB

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