This reverts commit 5c3cfb84c0
.
This commit is contained in:
parent
6228331fd1
commit
51065d9129
33 changed files with 819 additions and 1171 deletions
|
@ -54,6 +54,8 @@ using System.Threading.Tasks;
|
||||||
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
|
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
|
||||||
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
|
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
|
||||||
using Image = SixLabors.ImageSharp.Image;
|
using Image = SixLabors.ImageSharp.Image;
|
||||||
|
using InputManager = Ryujinx.Input.HLE.InputManager;
|
||||||
|
using IRenderer = Ryujinx.Graphics.GAL.IRenderer;
|
||||||
using Key = Ryujinx.Input.Key;
|
using Key = Ryujinx.Input.Key;
|
||||||
using MouseButton = Ryujinx.Input.MouseButton;
|
using MouseButton = Ryujinx.Input.MouseButton;
|
||||||
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
|
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
|
||||||
|
@ -121,14 +123,12 @@ namespace Ryujinx.Ava
|
||||||
public int Width { get; private set; }
|
public int Width { get; private set; }
|
||||||
public int Height { get; private set; }
|
public int Height { get; private set; }
|
||||||
public string ApplicationPath { get; private set; }
|
public string ApplicationPath { get; private set; }
|
||||||
public ulong ApplicationId { get; private set; }
|
|
||||||
public bool ScreenshotRequested { get; set; }
|
public bool ScreenshotRequested { get; set; }
|
||||||
|
|
||||||
public AppHost(
|
public AppHost(
|
||||||
RendererHost renderer,
|
RendererHost renderer,
|
||||||
InputManager inputManager,
|
InputManager inputManager,
|
||||||
string applicationPath,
|
string applicationPath,
|
||||||
ulong applicationId,
|
|
||||||
VirtualFileSystem virtualFileSystem,
|
VirtualFileSystem virtualFileSystem,
|
||||||
ContentManager contentManager,
|
ContentManager contentManager,
|
||||||
AccountManager accountManager,
|
AccountManager accountManager,
|
||||||
|
@ -152,7 +152,6 @@ namespace Ryujinx.Ava
|
||||||
NpadManager = _inputManager.CreateNpadManager();
|
NpadManager = _inputManager.CreateNpadManager();
|
||||||
TouchScreenManager = _inputManager.CreateTouchScreenManager();
|
TouchScreenManager = _inputManager.CreateTouchScreenManager();
|
||||||
ApplicationPath = applicationPath;
|
ApplicationPath = applicationPath;
|
||||||
ApplicationId = applicationId;
|
|
||||||
VirtualFileSystem = virtualFileSystem;
|
VirtualFileSystem = virtualFileSystem;
|
||||||
ContentManager = contentManager;
|
ContentManager = contentManager;
|
||||||
|
|
||||||
|
@ -642,7 +641,7 @@ namespace Ryujinx.Ava
|
||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
||||||
|
|
||||||
if (!Device.LoadXci(ApplicationPath, ApplicationId))
|
if (!Device.LoadXci(ApplicationPath))
|
||||||
{
|
{
|
||||||
Device.Dispose();
|
Device.Dispose();
|
||||||
|
|
||||||
|
@ -669,7 +668,7 @@ namespace Ryujinx.Ava
|
||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
||||||
|
|
||||||
if (!Device.LoadNsp(ApplicationPath, ApplicationId))
|
if (!Device.LoadNsp(ApplicationPath))
|
||||||
{
|
{
|
||||||
Device.Dispose();
|
Device.Dispose();
|
||||||
|
|
||||||
|
|
|
@ -539,8 +539,6 @@
|
||||||
"OpenSetupGuideMessage": "Open the Setup Guide",
|
"OpenSetupGuideMessage": "Open the Setup Guide",
|
||||||
"NoUpdate": "No Update",
|
"NoUpdate": "No Update",
|
||||||
"TitleUpdateVersionLabel": "Version {0}",
|
"TitleUpdateVersionLabel": "Version {0}",
|
||||||
"TitleBundledUpdateVersionLabel": "Bundled: Version {0}",
|
|
||||||
"TitleBundledDlcLabel": "Bundled:",
|
|
||||||
"RyujinxInfo": "Ryujinx - Info",
|
"RyujinxInfo": "Ryujinx - Info",
|
||||||
"RyujinxConfirm": "Ryujinx - Confirmation",
|
"RyujinxConfirm": "Ryujinx - Confirmation",
|
||||||
"FileDialogAllTypes": "All types",
|
"FileDialogAllTypes": "All types",
|
||||||
|
|
|
@ -18,8 +18,7 @@ using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
using Ryujinx.Ui.App.Common;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
@ -227,11 +226,7 @@ namespace Ryujinx.Ava.Common
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
(Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
|
||||||
? IntegrityCheckLevel.ErrorOnInvalid
|
|
||||||
: IntegrityCheckLevel.None;
|
|
||||||
|
|
||||||
(Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _);
|
|
||||||
if (updatePatchNca != null)
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
patchNca = updatePatchNca;
|
patchNca = updatePatchNca;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.Threading;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
|
@ -14,6 +15,7 @@ using Ryujinx.Ui.App.Common;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
|
@ -39,7 +41,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
{
|
{
|
||||||
viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite;
|
viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite;
|
||||||
|
|
||||||
ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata =>
|
ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata =>
|
||||||
{
|
{
|
||||||
appMetadata.Favorite = viewModel.SelectedApplication.Favorite;
|
appMetadata.Favorite = viewModel.SelectedApplication.Favorite;
|
||||||
});
|
});
|
||||||
|
@ -74,9 +76,19 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
{
|
{
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default);
|
if (!ulong.TryParse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
|
||||||
|
});
|
||||||
|
|
||||||
ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default);
|
||||||
|
|
||||||
|
ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +98,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication);
|
await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +108,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication);
|
await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,8 +120,8 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
{
|
{
|
||||||
await new CheatWindow(
|
await new CheatWindow(
|
||||||
viewModel.VirtualFileSystem,
|
viewModel.VirtualFileSystem,
|
||||||
viewModel.SelectedApplication.IdString,
|
viewModel.SelectedApplication.TitleId,
|
||||||
viewModel.SelectedApplication.Name,
|
viewModel.SelectedApplication.TitleName,
|
||||||
viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window);
|
viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,7 +133,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
string modsBasePath = ModLoader.GetModsBasePath();
|
string modsBasePath = ModLoader.GetModsBasePath();
|
||||||
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.IdString);
|
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.TitleId);
|
||||||
|
|
||||||
OpenHelper.OpenFolder(titleModsPath);
|
OpenHelper.OpenFolder(titleModsPath);
|
||||||
}
|
}
|
||||||
|
@ -134,7 +146,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
||||||
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.IdString);
|
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.TitleId);
|
||||||
|
|
||||||
OpenHelper.OpenFolder(titleModsPath);
|
OpenHelper.OpenFolder(titleModsPath);
|
||||||
}
|
}
|
||||||
|
@ -148,15 +160,15 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
{
|
{
|
||||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||||
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
||||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name),
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName),
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||||
|
|
||||||
if (result == UserResult.Yes)
|
if (result == UserResult.Yes)
|
||||||
{
|
{
|
||||||
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0"));
|
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0"));
|
||||||
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1"));
|
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1"));
|
||||||
|
|
||||||
List<FileInfo> cacheFiles = new();
|
List<FileInfo> cacheFiles = new();
|
||||||
|
|
||||||
|
@ -196,14 +208,14 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
{
|
{
|
||||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||||
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
||||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name),
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName),
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||||
|
|
||||||
if (result == UserResult.Yes)
|
if (result == UserResult.Yes)
|
||||||
{
|
{
|
||||||
DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader"));
|
DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader"));
|
||||||
|
|
||||||
List<DirectoryInfo> oldCacheDirectories = new();
|
List<DirectoryInfo> oldCacheDirectories = new();
|
||||||
List<FileInfo> newCacheFiles = new();
|
List<FileInfo> newCacheFiles = new();
|
||||||
|
@ -251,7 +263,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu");
|
string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu");
|
||||||
string mainDir = Path.Combine(ptcDir, "0");
|
string mainDir = Path.Combine(ptcDir, "0");
|
||||||
string backupDir = Path.Combine(ptcDir, "1");
|
string backupDir = Path.Combine(ptcDir, "1");
|
||||||
|
|
||||||
|
@ -272,7 +284,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader");
|
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader");
|
||||||
|
|
||||||
if (!Directory.Exists(shaderCacheDir))
|
if (!Directory.Exists(shaderCacheDir))
|
||||||
{
|
{
|
||||||
|
@ -293,7 +305,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
viewModel.StorageProvider,
|
viewModel.StorageProvider,
|
||||||
NcaSectionType.Code,
|
NcaSectionType.Code,
|
||||||
viewModel.SelectedApplication.Path,
|
viewModel.SelectedApplication.Path,
|
||||||
viewModel.SelectedApplication.Name);
|
viewModel.SelectedApplication.TitleName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,7 +319,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
viewModel.StorageProvider,
|
viewModel.StorageProvider,
|
||||||
NcaSectionType.Data,
|
NcaSectionType.Data,
|
||||||
viewModel.SelectedApplication.Path,
|
viewModel.SelectedApplication.Path,
|
||||||
viewModel.SelectedApplication.Name);
|
viewModel.SelectedApplication.TitleName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,7 +333,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
viewModel.StorageProvider,
|
viewModel.StorageProvider,
|
||||||
NcaSectionType.Logo,
|
NcaSectionType.Logo,
|
||||||
viewModel.SelectedApplication.Path,
|
viewModel.SelectedApplication.Path,
|
||||||
viewModel.SelectedApplication.Name);
|
viewModel.SelectedApplication.TitleName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,7 +344,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
ApplicationData selectedApplication = viewModel.SelectedApplication;
|
ApplicationData selectedApplication = viewModel.SelectedApplication;
|
||||||
ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.Name, selectedApplication.IdString, selectedApplication.Icon);
|
ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,7 +354,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
await viewModel.LoadApplication(viewModel.SelectedApplication);
|
await viewModel.LoadApplication(viewModel.SelectedApplication.Path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{Binding Name}"
|
Text="{Binding TitleName}"
|
||||||
TextAlignment="Center"
|
TextAlignment="Center"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
Text="{Binding Name}"
|
Text="{Binding TitleName}"
|
||||||
TextAlignment="Left"
|
TextAlignment="Left"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
|
@ -109,7 +109,7 @@
|
||||||
Spacing="5">
|
Spacing="5">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Text="{Binding Id, StringFormat=X16}"
|
Text="{Binding TitleId}"
|
||||||
TextAlignment="Left"
|
TextAlignment="Left"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Models
|
namespace Ryujinx.Ava.UI.Models
|
||||||
|
@ -25,9 +24,6 @@ namespace Ryujinx.Ava.UI.Models
|
||||||
|
|
||||||
public string FileName => Path.GetFileName(ContainerPath);
|
public string FileName => Path.GetFileName(ContainerPath);
|
||||||
|
|
||||||
public string Label =>
|
|
||||||
Path.GetExtension(FileName)?.ToLower() == ".xci" ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {FileName}" : FileName;
|
|
||||||
|
|
||||||
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
|
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
|
||||||
{
|
{
|
||||||
TitleId = titleId;
|
TitleId = titleId;
|
||||||
|
|
|
@ -46,14 +46,14 @@ namespace Ryujinx.Ava.UI.Models
|
||||||
TitleId = info.ProgramId;
|
TitleId = info.ProgramId;
|
||||||
UserId = info.UserId;
|
UserId = info.UserId;
|
||||||
|
|
||||||
var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.ToUpper() == TitleIdString);
|
var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString);
|
||||||
|
|
||||||
InGameList = appData != null;
|
InGameList = appData != null;
|
||||||
|
|
||||||
if (InGameList)
|
if (InGameList)
|
||||||
{
|
{
|
||||||
Icon = appData.Icon;
|
Icon = appData.Icon;
|
||||||
Title = appData.Name;
|
Title = appData.TitleName;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,10 +8,7 @@ namespace Ryujinx.Ava.UI.Models
|
||||||
public ApplicationControlProperty Control { get; }
|
public ApplicationControlProperty Control { get; }
|
||||||
public string Path { get; }
|
public string Path { get; }
|
||||||
|
|
||||||
public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(
|
public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleUpdateVersionLabel, Control.DisplayVersionString.ToString());
|
||||||
System.IO.Path.GetExtension(Path)?.ToLower() == ".xci" ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel,
|
|
||||||
Control.DisplayVersionString.ToString()
|
|
||||||
);
|
|
||||||
|
|
||||||
public TitleUpdateModel(ApplicationControlProperty control, string path)
|
public TitleUpdateModel(ApplicationControlProperty control, string path)
|
||||||
{
|
{
|
||||||
|
|
|
@ -17,12 +17,11 @@ using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
|
||||||
using Ryujinx.Ui.App.Common;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Application = Avalonia.Application;
|
using Application = Avalonia.Application;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
|
@ -39,7 +38,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
|
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
|
||||||
|
|
||||||
private string _search;
|
private string _search;
|
||||||
private readonly ApplicationData _applicationData;
|
private readonly ulong _titleId;
|
||||||
|
|
||||||
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
|
|
||||||
|
@ -93,25 +92,18 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
public IStorageProvider StorageProvider;
|
public IStorageProvider StorageProvider;
|
||||||
|
|
||||||
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
|
||||||
{
|
{
|
||||||
_virtualFileSystem = virtualFileSystem;
|
_virtualFileSystem = virtualFileSystem;
|
||||||
|
|
||||||
_applicationData = applicationData;
|
_titleId = titleId;
|
||||||
|
|
||||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
StorageProvider = desktop.MainWindow.StorageProvider;
|
StorageProvider = desktop.MainWindow.StorageProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "dlc.json");
|
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
||||||
|
|
||||||
if (!File.Exists(_downloadableContentJsonPath))
|
|
||||||
{
|
|
||||||
_downloadableContentContainerList = new List<DownloadableContentContainer>();
|
|
||||||
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -128,9 +120,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
private void LoadDownloadableContents()
|
private void LoadDownloadableContents()
|
||||||
{
|
{
|
||||||
// NOTE: Try to load downloadable contents from PFS first.
|
|
||||||
AddDownloadableContent(_applicationData.Path);
|
|
||||||
|
|
||||||
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
|
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
|
||||||
{
|
{
|
||||||
if (File.Exists(downloadableContentContainer.ContainerPath))
|
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||||
|
@ -138,11 +127,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
||||||
|
|
||||||
PartitionFileSystem partitionFileSystem = new();
|
PartitionFileSystem partitionFileSystem = new();
|
||||||
|
partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
||||||
if (partitionFileSystem.Initialize(containerFile.AsStorage()).IsFailure())
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||||
|
|
||||||
|
@ -235,34 +220,22 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
foreach (var file in result)
|
foreach (var file in result)
|
||||||
{
|
{
|
||||||
if (!AddDownloadableContent(file.Path.LocalPath))
|
await AddDownloadableContent(file.Path.LocalPath);
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool AddDownloadableContent(string path)
|
private async Task AddDownloadableContent(string path)
|
||||||
{
|
{
|
||||||
if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
||||||
{
|
{
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using FileStream containerFile = File.OpenRead(path);
|
using FileStream containerFile = File.OpenRead(path);
|
||||||
|
|
||||||
IFileSystem partitionFileSystem;
|
PartitionFileSystem partitionFileSystem = new();
|
||||||
|
partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
||||||
if (Path.GetExtension(path).ToLower() == ".xci")
|
bool containsDownloadableContent = false;
|
||||||
{
|
|
||||||
partitionFileSystem = new Xci(_virtualFileSystem.KeySet, containerFile.AsStorage()).OpenPartition(XciPartitionType.Secure);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var pfsTemp = new PartitionFileSystem();
|
|
||||||
pfsTemp.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
|
||||||
partitionFileSystem = pfsTemp;
|
|
||||||
}
|
|
||||||
|
|
||||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||||
|
|
||||||
|
@ -280,7 +253,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||||
{
|
{
|
||||||
if (nca.GetProgramIdBase() != _applicationData.IdBase)
|
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -292,11 +265,14 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
OnPropertyChanged(nameof(UpdateCount));
|
OnPropertyChanged(nameof(UpdateCount));
|
||||||
Sort();
|
Sort();
|
||||||
|
|
||||||
return true;
|
containsDownloadableContent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
if (!containsDownloadableContent)
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(DownloadableContentModel model)
|
public void Remove(DownloadableContentModel model)
|
||||||
|
|
|
@ -95,7 +95,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
private bool _canUpdate = true;
|
private bool _canUpdate = true;
|
||||||
private Cursor _cursor;
|
private Cursor _cursor;
|
||||||
private string _title;
|
private string _title;
|
||||||
private ApplicationData _currentApplicationData;
|
private string _currentEmulatedGamePath;
|
||||||
private readonly AutoResetEvent _rendererWaitEvent;
|
private readonly AutoResetEvent _rendererWaitEvent;
|
||||||
private WindowState _windowState;
|
private WindowState _windowState;
|
||||||
private double _windowWidth;
|
private double _windowWidth;
|
||||||
|
@ -106,6 +106,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
public ApplicationData ListSelectedApplication;
|
public ApplicationData ListSelectedApplication;
|
||||||
public ApplicationData GridSelectedApplication;
|
public ApplicationData GridSelectedApplication;
|
||||||
|
|
||||||
|
private string TitleName { get; set; }
|
||||||
internal AppHost AppHost { get; set; }
|
internal AppHost AppHost { get; set; }
|
||||||
|
|
||||||
public MainWindowViewModel()
|
public MainWindowViewModel()
|
||||||
|
@ -929,8 +930,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
return SortMode switch
|
return SortMode switch
|
||||||
{
|
{
|
||||||
#pragma warning disable IDE0055 // Disable formatting
|
#pragma warning disable IDE0055 // Disable formatting
|
||||||
ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Name)
|
ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TitleName)
|
||||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Name),
|
: SortExpressionComparer<ApplicationData>.Descending(app => app.TitleName),
|
||||||
ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer)
|
ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer)
|
||||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Developer),
|
: SortExpressionComparer<ApplicationData>.Descending(app => app.Developer),
|
||||||
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
|
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
|
||||||
|
@ -967,7 +968,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
{
|
{
|
||||||
if (arg is ApplicationData app)
|
if (arg is ApplicationData app)
|
||||||
{
|
{
|
||||||
return string.IsNullOrWhiteSpace(_searchText) || app.Name.ToLower().Contains(_searchText.ToLower());
|
return string.IsNullOrWhiteSpace(_searchText) || app.TitleName.ToLower().Contains(_searchText.ToLower());
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -1096,7 +1097,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
IsLoadingIndeterminate = false;
|
IsLoadingIndeterminate = false;
|
||||||
break;
|
break;
|
||||||
case LoadState.Loaded:
|
case LoadState.Loaded:
|
||||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
|
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
|
||||||
IsLoadingIndeterminate = true;
|
IsLoadingIndeterminate = true;
|
||||||
CacheLoadStatus = "";
|
CacheLoadStatus = "";
|
||||||
break;
|
break;
|
||||||
|
@ -1116,7 +1117,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
IsLoadingIndeterminate = false;
|
IsLoadingIndeterminate = false;
|
||||||
break;
|
break;
|
||||||
case ShaderCacheLoadingState.Loaded:
|
case ShaderCacheLoadingState.Loaded:
|
||||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
|
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
|
||||||
IsLoadingIndeterminate = true;
|
IsLoadingIndeterminate = true;
|
||||||
CacheLoadStatus = "";
|
CacheLoadStatus = "";
|
||||||
break;
|
break;
|
||||||
|
@ -1167,13 +1168,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
{
|
{
|
||||||
UserChannelPersistence.ShouldRestart = false;
|
UserChannelPersistence.ShouldRestart = false;
|
||||||
|
|
||||||
await LoadApplication(_currentApplicationData);
|
await LoadApplication(_currentEmulatedGamePath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Otherwise, clear state.
|
// Otherwise, clear state.
|
||||||
UserChannelPersistence = new UserChannelPersistence();
|
UserChannelPersistence = new UserChannelPersistence();
|
||||||
_currentApplicationData = null;
|
_currentEmulatedGamePath = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1450,12 +1451,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
if (result.Count > 0)
|
if (result.Count > 0)
|
||||||
{
|
{
|
||||||
ApplicationData applicationData = new()
|
await LoadApplication(result[0].Path.LocalPath);
|
||||||
{
|
|
||||||
Path = result[0].Path.LocalPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
await LoadApplication(applicationData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1469,17 +1465,11 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
if (result.Count > 0)
|
if (result.Count > 0)
|
||||||
{
|
{
|
||||||
ApplicationData applicationData = new()
|
await LoadApplication(result[0].Path.LocalPath);
|
||||||
{
|
|
||||||
Name = Path.GetFileNameWithoutExtension(result[0].Path.LocalPath),
|
|
||||||
Path = result[0].Path.LocalPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
await LoadApplication(applicationData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadApplication(ApplicationData application, bool startFullscreen = false)
|
public async Task LoadApplication(string path, bool startFullscreen = false, string titleName = "")
|
||||||
{
|
{
|
||||||
if (AppHost != null)
|
if (AppHost != null)
|
||||||
{
|
{
|
||||||
|
@ -1499,7 +1489,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
Logger.RestartTime();
|
Logger.RestartTime();
|
||||||
|
|
||||||
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id);
|
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language);
|
||||||
|
|
||||||
PrepareLoadScreen();
|
PrepareLoadScreen();
|
||||||
|
|
||||||
|
@ -1508,8 +1498,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
AppHost = new AppHost(
|
AppHost = new AppHost(
|
||||||
RendererHostControl,
|
RendererHostControl,
|
||||||
InputManager,
|
InputManager,
|
||||||
application.Path,
|
path,
|
||||||
application.Id,
|
|
||||||
VirtualFileSystem,
|
VirtualFileSystem,
|
||||||
ContentManager,
|
ContentManager,
|
||||||
AccountManager,
|
AccountManager,
|
||||||
|
@ -1527,17 +1516,17 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
CanUpdate = false;
|
CanUpdate = false;
|
||||||
|
|
||||||
LoadHeading = application.Name;
|
LoadHeading = TitleName = titleName;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(application.Name))
|
if (string.IsNullOrWhiteSpace(titleName))
|
||||||
{
|
{
|
||||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name);
|
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name);
|
||||||
application.Name = AppHost.Device.Processes.ActiveApplication.Name;
|
TitleName = AppHost.Device.Processes.ActiveApplication.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
SwitchToRenderer(startFullscreen);
|
SwitchToRenderer(startFullscreen);
|
||||||
|
|
||||||
_currentApplicationData = application;
|
_currentEmulatedGamePath = path;
|
||||||
|
|
||||||
Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
|
Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
|
||||||
gameThread.Start();
|
gameThread.Start();
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
|
@ -7,7 +8,6 @@ using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
using LibHac.Tools.Fs;
|
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
@ -17,16 +17,12 @@ using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
|
||||||
using Ryujinx.Ui.App.Common;
|
using Ryujinx.Ui.App.Common;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Application = Avalonia.Application;
|
|
||||||
using ContentType = LibHac.Ncm.ContentType;
|
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
using SpanHelpers = LibHac.Common.SpanHelpers;
|
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||||
|
|
||||||
|
@ -37,7 +33,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
public TitleUpdateMetadata TitleUpdateWindowData;
|
public TitleUpdateMetadata TitleUpdateWindowData;
|
||||||
public readonly string TitleUpdateJsonPath;
|
public readonly string TitleUpdateJsonPath;
|
||||||
private VirtualFileSystem VirtualFileSystem { get; }
|
private VirtualFileSystem VirtualFileSystem { get; }
|
||||||
private ApplicationData ApplicationData { get; }
|
private ulong TitleId { get; }
|
||||||
|
|
||||||
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
|
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
|
||||||
private AvaloniaList<object> _views = new();
|
private AvaloniaList<object> _views = new();
|
||||||
|
@ -77,18 +73,18 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
public IStorageProvider StorageProvider;
|
public IStorageProvider StorageProvider;
|
||||||
|
|
||||||
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
|
||||||
{
|
{
|
||||||
VirtualFileSystem = virtualFileSystem;
|
VirtualFileSystem = virtualFileSystem;
|
||||||
|
|
||||||
ApplicationData = applicationData;
|
TitleId = titleId;
|
||||||
|
|
||||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
StorageProvider = desktop.MainWindow.StorageProvider;
|
StorageProvider = desktop.MainWindow.StorageProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdString, "updates.json");
|
TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -96,7 +92,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {ApplicationData.IdString} at {TitleUpdateJsonPath}");
|
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {TitleId} at {TitleUpdateJsonPath}");
|
||||||
|
|
||||||
TitleUpdateWindowData = new TitleUpdateMetadata
|
TitleUpdateWindowData = new TitleUpdateMetadata
|
||||||
{
|
{
|
||||||
|
@ -112,9 +108,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
private void LoadUpdates()
|
private void LoadUpdates()
|
||||||
{
|
{
|
||||||
// Try to load updates from PFS first
|
|
||||||
AddUpdate(ApplicationData.Path, true);
|
|
||||||
|
|
||||||
foreach (string path in TitleUpdateWindowData.Paths)
|
foreach (string path in TitleUpdateWindowData.Paths)
|
||||||
{
|
{
|
||||||
AddUpdate(path);
|
AddUpdate(path);
|
||||||
|
@ -169,41 +162,17 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddUpdate(string path, bool ignoreNotFound = false)
|
private void AddUpdate(string path)
|
||||||
{
|
{
|
||||||
if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
|
if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
|
||||||
{
|
{
|
||||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
|
||||||
? IntegrityCheckLevel.ErrorOnInvalid
|
|
||||||
: IntegrityCheckLevel.None;
|
|
||||||
|
|
||||||
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
IFileSystem pfs;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Path.GetExtension(path).ToLower() == ".xci")
|
var pfs = new PartitionFileSystem();
|
||||||
{
|
pfs.Initialize(file.AsStorage()).ThrowIfFailure();
|
||||||
pfs = new Xci(VirtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
|
(Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var pfsTemp = new PartitionFileSystem();
|
|
||||||
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
|
|
||||||
pfs = pfsTemp;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dictionary<ulong, ContentCollection> updates = pfs.GetUpdateData(VirtualFileSystem, checkLevel);
|
|
||||||
|
|
||||||
Nca patchNca = null;
|
|
||||||
Nca controlNca = null;
|
|
||||||
|
|
||||||
if (updates.TryGetValue(ApplicationData.Id, out ContentCollection content))
|
|
||||||
{
|
|
||||||
patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program);
|
|
||||||
controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controlNca != null && patchNca != null)
|
if (controlNca != null && patchNca != null)
|
||||||
{
|
{
|
||||||
|
@ -218,10 +187,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!ignoreNotFound)
|
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
|
||||||
{
|
|
||||||
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
@ -10,7 +10,6 @@ using Ryujinx.Ava.UI.Windows;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.Modules;
|
using Ryujinx.Modules;
|
||||||
using Ryujinx.Ui.App.Common;
|
|
||||||
using Ryujinx.Ui.Common;
|
using Ryujinx.Ui.Common;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
|
@ -132,14 +131,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(contentPath))
|
if (!string.IsNullOrEmpty(contentPath))
|
||||||
{
|
{
|
||||||
ApplicationData applicationData = new()
|
await ViewModel.LoadApplication(contentPath, false, "Mii Applet");
|
||||||
{
|
|
||||||
Name = "miiEdit",
|
|
||||||
Id = 0x0100000000001009,
|
|
||||||
Path = contentPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -104,7 +104,7 @@
|
||||||
Content="{locale:Locale GameListHeaderApplication}"
|
Content="{locale:Locale GameListHeaderApplication}"
|
||||||
GroupName="Sort"
|
GroupName="Sort"
|
||||||
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
|
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
|
||||||
Tag="Application" />
|
Tag="Title" />
|
||||||
<RadioButton
|
<RadioButton
|
||||||
Checked="Sort_Checked"
|
Checked="Sort_Checked"
|
||||||
Content="{locale:Locale GameListHeaderDeveloper}"
|
Content="{locale:Locale GameListHeaderDeveloper}"
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using LibHac.Tools.FsSystem;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
using Ryujinx.Ui.App.Common;
|
using Ryujinx.Ui.App.Common;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
@ -36,12 +34,9 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath)
|
public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath)
|
||||||
{
|
{
|
||||||
LoadedCheats = new AvaloniaList<CheatsList>();
|
LoadedCheats = new AvaloniaList<CheatsList>();
|
||||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
|
||||||
? IntegrityCheckLevel.ErrorOnInvalid
|
|
||||||
: IntegrityCheckLevel.None;
|
|
||||||
|
|
||||||
Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper());
|
Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper());
|
||||||
BuildId = ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath);
|
BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath);
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@
|
||||||
MaxLines="2"
|
MaxLines="2"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
Text="{Binding Label}" />
|
Text="{Binding FileName}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="10 0"
|
Margin="10 0"
|
||||||
|
|
|
@ -7,9 +7,9 @@ using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.Ui.App.Common;
|
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Button = Avalonia.Controls.Button;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Windows
|
namespace Ryujinx.Ava.UI.Windows
|
||||||
{
|
{
|
||||||
|
@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId)
|
||||||
{
|
{
|
||||||
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, applicationData);
|
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, titleId);
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||||
{
|
{
|
||||||
ContentDialog contentDialog = new()
|
ContentDialog contentDialog = new()
|
||||||
{
|
{
|
||||||
PrimaryButtonText = "",
|
PrimaryButtonText = "",
|
||||||
SecondaryButtonText = "",
|
SecondaryButtonText = "",
|
||||||
CloseButtonText = "",
|
CloseButtonText = "",
|
||||||
Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationData),
|
Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId),
|
||||||
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdString),
|
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16")),
|
||||||
};
|
};
|
||||||
|
|
||||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||||
|
|
|
@ -4,7 +4,6 @@ using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using LibHac.Tools.FsSystem;
|
|
||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Input;
|
using Ryujinx.Ava.Input;
|
||||||
|
@ -24,6 +23,7 @@ using Ryujinx.Ui.Common;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -139,7 +139,9 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
{
|
{
|
||||||
ViewModel.SelectedIcon = args.Application.Icon;
|
ViewModel.SelectedIcon = args.Application.Icon;
|
||||||
|
|
||||||
ViewModel.LoadApplication(args.Application).Wait();
|
string path = new FileInfo(args.Application.Path).FullName;
|
||||||
|
|
||||||
|
ViewModel.LoadApplication(path).Wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
|
@ -188,11 +190,7 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
LibHacHorizonManager.InitializeBcatServer();
|
LibHacHorizonManager.InitializeBcatServer();
|
||||||
LibHacHorizonManager.InitializeSystemClients();
|
LibHacHorizonManager.InitializeSystemClients();
|
||||||
|
|
||||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem);
|
||||||
? IntegrityCheckLevel.ErrorOnInvalid
|
|
||||||
: IntegrityCheckLevel.None;
|
|
||||||
|
|
||||||
ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel);
|
|
||||||
|
|
||||||
// Save data created before we supported extra data in directory save data will not work properly if
|
// Save data created before we supported extra data in directory save data will not work properly if
|
||||||
// given empty extra data. Luckily some of that extra data can be created using the data from the
|
// given empty extra data. Luckily some of that extra data can be created using the data from the
|
||||||
|
@ -299,12 +297,7 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
{
|
{
|
||||||
_deferLoad = false;
|
_deferLoad = false;
|
||||||
|
|
||||||
ApplicationData applicationData = new()
|
ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait();
|
||||||
{
|
|
||||||
Path = _launchPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
ViewModel.LoadApplication(applicationData, _startFullscreen).Wait();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
@ -7,15 +7,15 @@ using Ryujinx.Ava.UI.Helpers;
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.Ui.App.Common;
|
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Button = Avalonia.Controls.Button;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Windows
|
namespace Ryujinx.Ava.UI.Windows
|
||||||
{
|
{
|
||||||
public partial class TitleUpdateWindow : UserControl
|
public partial class TitleUpdateWindow : UserControl
|
||||||
{
|
{
|
||||||
public readonly TitleUpdateViewModel ViewModel;
|
public TitleUpdateViewModel ViewModel;
|
||||||
|
|
||||||
public TitleUpdateWindow()
|
public TitleUpdateWindow()
|
||||||
{
|
{
|
||||||
|
@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId)
|
||||||
{
|
{
|
||||||
DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationData);
|
DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId);
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||||
{
|
{
|
||||||
ContentDialog contentDialog = new()
|
ContentDialog contentDialog = new()
|
||||||
{
|
{
|
||||||
PrimaryButtonText = "",
|
PrimaryButtonText = "",
|
||||||
SecondaryButtonText = "",
|
SecondaryButtonText = "",
|
||||||
CloseButtonText = "",
|
CloseButtonText = "",
|
||||||
Content = new TitleUpdateWindow(virtualFileSystem, applicationData),
|
Content = new TitleUpdateWindow(virtualFileSystem, titleId),
|
||||||
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdString),
|
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, titleName, titleId.ToString("X16")),
|
||||||
};
|
};
|
||||||
|
|
||||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
using LibHac.Common.Keys;
|
|
||||||
using LibHac.Fs.Fsa;
|
|
||||||
using LibHac.Ncm;
|
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
|
||||||
using LibHac.Tools.Ncm;
|
|
||||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Ryujinx.HLE.FileSystem
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Thin wrapper around <see cref="Cnmt"/>
|
|
||||||
/// </summary>
|
|
||||||
public class ContentCollection
|
|
||||||
{
|
|
||||||
private readonly IFileSystem _pfs;
|
|
||||||
private readonly Cnmt _cnmt;
|
|
||||||
|
|
||||||
public ulong Id => _cnmt.TitleId;
|
|
||||||
public TitleVersion Version => _cnmt.TitleVersion;
|
|
||||||
public ContentMetaType Type => _cnmt.Type;
|
|
||||||
public ulong ApplicationId => _cnmt.ApplicationTitleId;
|
|
||||||
public ulong PatchId => _cnmt.PatchTitleId;
|
|
||||||
public TitleVersion RequiredSystemVersion => _cnmt.MinimumSystemVersion;
|
|
||||||
public TitleVersion RequiredApplicationVersion => _cnmt.MinimumApplicationVersion;
|
|
||||||
public byte[] Digest => _cnmt.Hash;
|
|
||||||
|
|
||||||
public ulong ProgramBaseId => Id & ~0x1FFFUL;
|
|
||||||
public bool IsSystemTitle => _cnmt.Type < ContentMetaType.Application;
|
|
||||||
|
|
||||||
public ContentCollection(IFileSystem pfs, Cnmt cnmt)
|
|
||||||
{
|
|
||||||
_pfs = pfs;
|
|
||||||
_cnmt = cnmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Nca GetNcaByType(KeySet keySet, ContentType type, int programIndex = 0)
|
|
||||||
{
|
|
||||||
// TODO: Replace this with a check for IdOffset as soon as LibHac supports it:
|
|
||||||
// && entry.IdOffset == programIndex
|
|
||||||
|
|
||||||
foreach (var entry in _cnmt.ContentEntries)
|
|
||||||
{
|
|
||||||
if (entry.Type != type)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
string ncaId = BitConverter.ToString(entry.NcaId).Replace("-", null).ToLower();
|
|
||||||
Nca nca = _pfs.GetNca(keySet, $"/{ncaId}.nca");
|
|
||||||
|
|
||||||
if (nca.GetProgramIndex() == programIndex)
|
|
||||||
{
|
|
||||||
return nca;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,31 +2,21 @@
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
|
||||||
using LibHac.Loader;
|
using LibHac.Loader;
|
||||||
using LibHac.Ncm;
|
using LibHac.Ncm;
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
using LibHac.Tools.Fs;
|
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using LibHac.Tools.Ncm;
|
|
||||||
using Ryujinx.Common.Configuration;
|
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
|
||||||
using Ryujinx.HLE.FileSystem;
|
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using ApplicationId = LibHac.Ncm.ApplicationId;
|
using ApplicationId = LibHac.Ncm.ApplicationId;
|
||||||
using ContentType = LibHac.Ncm.ContentType;
|
|
||||||
using Path = System.IO.Path;
|
|
||||||
|
|
||||||
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
{
|
{
|
||||||
public static class NcaExtensions
|
static class NcaExtensions
|
||||||
{
|
{
|
||||||
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
|
||||||
|
|
||||||
public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
|
public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
|
||||||
{
|
{
|
||||||
// Extract RomFs and ExeFs from NCA.
|
// Extract RomFs and ExeFs from NCA.
|
||||||
|
@ -57,7 +47,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
nacpData = controlNca.GetNacp(device);
|
nacpData = controlNca.GetNacp(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update.
|
/* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" inexistant update.
|
||||||
|
|
||||||
// Load program 0 control NCA as we are going to need it for display version.
|
// Load program 0 control NCA as we are going to need it for display version.
|
||||||
(_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
|
(_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
|
||||||
|
@ -96,11 +86,6 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
return processResult;
|
return processResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ulong GetProgramIdBase(this Nca nca)
|
|
||||||
{
|
|
||||||
return nca.Header.TitleId & ~0x1FFFUL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int GetProgramIndex(this Nca nca)
|
public static int GetProgramIndex(this Nca nca)
|
||||||
{
|
{
|
||||||
return (int)(nca.Header.TitleId & 0xF);
|
return (int)(nca.Header.TitleId & 0xF);
|
||||||
|
@ -111,11 +96,6 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
return nca.Header.ContentType == NcaContentType.Program;
|
return nca.Header.ContentType == NcaContentType.Program;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsMain(this Nca nca)
|
|
||||||
{
|
|
||||||
return nca.IsProgram() && !nca.IsPatch();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsPatch(this Nca nca)
|
public static bool IsPatch(this Nca nca)
|
||||||
{
|
{
|
||||||
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
||||||
|
@ -128,56 +108,6 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
return nca.Header.ContentType == NcaContentType.Control;
|
return nca.Header.ContentType == NcaContentType.Control;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath)
|
|
||||||
{
|
|
||||||
updatePath = "(unknown)";
|
|
||||||
|
|
||||||
// Load Update NCAs.
|
|
||||||
Nca updatePatchNca = null;
|
|
||||||
Nca updateControlNca = null;
|
|
||||||
|
|
||||||
// Clear the program index part.
|
|
||||||
ulong titleIdBase = mainNca.GetProgramIdBase();
|
|
||||||
|
|
||||||
// Load update information if exists.
|
|
||||||
string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json");
|
|
||||||
if (File.Exists(titleUpdateMetadataPath))
|
|
||||||
{
|
|
||||||
updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
|
|
||||||
if (File.Exists(updatePath))
|
|
||||||
{
|
|
||||||
var updateFile = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
|
|
||||||
|
|
||||||
IFileSystem updatePartitionFileSystem;
|
|
||||||
|
|
||||||
if (Path.GetExtension(updatePath).ToLower() == ".xci")
|
|
||||||
{
|
|
||||||
updatePartitionFileSystem = new Xci(fileSystem.KeySet, updateFile.AsStorage()).OpenPartition(XciPartitionType.Secure);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PartitionFileSystem pfsTemp = new();
|
|
||||||
pfsTemp.Initialize(updateFile.AsStorage()).ThrowIfFailure();
|
|
||||||
updatePartitionFileSystem = pfsTemp;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ((ulong updateTitleId, ContentCollection content) in updatePartitionFileSystem.GetUpdateData(fileSystem, checkLevel))
|
|
||||||
{
|
|
||||||
if ((updateTitleId & ~0x1FFFUL) != titleIdBase)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex);
|
|
||||||
updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (updatePatchNca, updateControlNca);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null)
|
public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null)
|
||||||
{
|
{
|
||||||
IFileSystem exeFs = null;
|
IFileSystem exeFs = null;
|
||||||
|
@ -242,31 +172,5 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
|
|
||||||
return nacpData;
|
return nacpData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Cnmt GetCnmt(this Nca cnmtNca, IntegrityCheckLevel checkLevel, ContentMetaType metaType)
|
|
||||||
{
|
|
||||||
string path = $"/{metaType}_{cnmtNca.Header.TitleId:x16}.cnmt";
|
|
||||||
using var cnmtFile = new UniqueRef<IFile>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Result result = cnmtNca.OpenFileSystem(0, checkLevel)
|
|
||||||
.OpenFile(ref cnmtFile.Ref, path.ToU8Span(), OpenMode.Read);
|
|
||||||
|
|
||||||
if (result.IsSuccess())
|
|
||||||
{
|
|
||||||
return new Cnmt(cnmtFile.Release().AsStream());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (HorizonResultException ex)
|
|
||||||
{
|
|
||||||
if (!ResultFs.PathNotFound.Includes(ex.ResultValue))
|
|
||||||
{
|
|
||||||
Logger.Warning?.Print(LogClass.Application, $"Failed get cnmt for '{cnmtNca.Header.TitleId:x16}' from nca: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,87 +1,26 @@
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Common.Keys;
|
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using LibHac.Ncm;
|
|
||||||
using LibHac.Tools.Fs;
|
using LibHac.Tools.Fs;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using LibHac.Tools.Ncm;
|
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using ContentType = LibHac.Ncm.ContentType;
|
|
||||||
|
|
||||||
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
{
|
{
|
||||||
public static class PartitionFileSystemExtensions
|
public static class PartitionFileSystemExtensions
|
||||||
{
|
{
|
||||||
private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
|
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
|
|
||||||
public static Dictionary<ulong, ContentCollection> GetApplicationData(this IFileSystem partitionFileSystem,
|
internal static (bool, ProcessResult) TryLoad<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> partitionFileSystem, Switch device, string path, out string errorMessage)
|
||||||
VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel)
|
|
||||||
{
|
|
||||||
fileSystem.ImportTickets(partitionFileSystem);
|
|
||||||
|
|
||||||
var programs = new Dictionary<ulong, ContentCollection>();
|
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca"))
|
|
||||||
{
|
|
||||||
Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Application);
|
|
||||||
|
|
||||||
if (cnmt == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentCollection content = new(partitionFileSystem, cnmt);
|
|
||||||
|
|
||||||
if (content.Type != ContentMetaType.Application)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
programs.TryAdd(content.ApplicationId, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
return programs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Dictionary<ulong, ContentCollection> GetUpdateData(this IFileSystem partitionFileSystem,
|
|
||||||
VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel)
|
|
||||||
{
|
|
||||||
fileSystem.ImportTickets(partitionFileSystem);
|
|
||||||
|
|
||||||
var programs = new Dictionary<ulong, ContentCollection>();
|
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca"))
|
|
||||||
{
|
|
||||||
Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Patch);
|
|
||||||
|
|
||||||
if (cnmt == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentCollection content = new(partitionFileSystem, cnmt);
|
|
||||||
|
|
||||||
if (content.Type != ContentMetaType.Patch)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
programs.TryAdd(content.ApplicationId, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
return programs;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static (bool, ProcessResult) TryLoad<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> partitionFileSystem, Switch device, string path, ulong titleId, out string errorMessage)
|
|
||||||
where TMetaData : PartitionFileSystemMetaCore<TFormat, THeader, TEntry>, new()
|
where TMetaData : PartitionFileSystemMetaCore<TFormat, THeader, TEntry>, new()
|
||||||
where TFormat : IPartitionFileSystemFormat
|
where TFormat : IPartitionFileSystemFormat
|
||||||
where THeader : unmanaged, IPartitionFileSystemHeader
|
where THeader : unmanaged, IPartitionFileSystemHeader
|
||||||
|
@ -96,21 +35,30 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Dictionary<ulong, ContentCollection> applications = partitionFileSystem.GetApplicationData(device.FileSystem, device.System.FsIntegrityCheckLevel);
|
device.Configuration.VirtualFileSystem.ImportTickets(partitionFileSystem);
|
||||||
|
|
||||||
if (titleId == 0)
|
// TODO: To support multi-games container, this should use CNMT NCA instead.
|
||||||
|
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||||
{
|
{
|
||||||
foreach ((ulong _, ContentCollection content) in applications)
|
Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
|
||||||
|
|
||||||
|
if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
|
||||||
{
|
{
|
||||||
mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index);
|
continue;
|
||||||
controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index);
|
}
|
||||||
break;
|
|
||||||
|
if (nca.IsPatch())
|
||||||
|
{
|
||||||
|
patchNca = nca;
|
||||||
|
}
|
||||||
|
else if (nca.IsProgram())
|
||||||
|
{
|
||||||
|
mainNca = nca;
|
||||||
|
}
|
||||||
|
else if (nca.IsControl())
|
||||||
|
{
|
||||||
|
controlNca = nca;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (applications.TryGetValue(titleId, out ContentCollection content))
|
|
||||||
{
|
|
||||||
mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index);
|
|
||||||
controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure();
|
ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure();
|
||||||
|
@ -131,7 +79,54 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
return (false, ProcessResult.Failed);
|
return (false, ProcessResult.Failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
(Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _);
|
// Load Update NCAs.
|
||||||
|
Nca updatePatchNca = null;
|
||||||
|
Nca updateControlNca = null;
|
||||||
|
|
||||||
|
if (ulong.TryParse(mainNca.Header.TitleId.ToString("x16"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase))
|
||||||
|
{
|
||||||
|
// Clear the program index part.
|
||||||
|
titleIdBase &= ~0xFUL;
|
||||||
|
|
||||||
|
// Load update information if exists.
|
||||||
|
string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
|
||||||
|
if (File.Exists(titleUpdateMetadataPath))
|
||||||
|
{
|
||||||
|
string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
|
||||||
|
if (File.Exists(updatePath))
|
||||||
|
{
|
||||||
|
PartitionFileSystem updatePartitionFileSystem = new();
|
||||||
|
updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure();
|
||||||
|
|
||||||
|
device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);
|
||||||
|
|
||||||
|
// TODO: This should use CNMT NCA instead.
|
||||||
|
foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||||
|
{
|
||||||
|
Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath);
|
||||||
|
|
||||||
|
if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16"))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nca.IsProgram())
|
||||||
|
{
|
||||||
|
updatePatchNca = nca;
|
||||||
|
}
|
||||||
|
else if (nca.IsControl())
|
||||||
|
{
|
||||||
|
updateControlNca = nca;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (updatePatchNca != null)
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
|
@ -173,18 +168,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
return (true, mainNca.Load(device, patchNca, controlNca));
|
return (true, mainNca.Load(device, patchNca, controlNca));
|
||||||
}
|
}
|
||||||
|
|
||||||
errorMessage = $"Unable to load: Could not find Main NCA for title \"{titleId:X16}\"";
|
errorMessage = "Unable to load: Could not find Main NCA";
|
||||||
|
|
||||||
return (false, ProcessResult.Failed);
|
return (false, ProcessResult.Failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Nca GetNca(this IFileSystem fileSystem, KeySet keySet, string path)
|
public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path)
|
||||||
{
|
{
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
return new Nca(keySet, ncaFile.Release().AsStorage());
|
return new Nca(device.Configuration.VirtualFileSystem.KeySet, ncaFile.Release().AsStorage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||||
_processesByPid = new ConcurrentDictionary<ulong, ProcessResult>();
|
_processesByPid = new ConcurrentDictionary<ulong, ProcessResult>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadXci(string path, ulong titleId)
|
public bool LoadXci(string path)
|
||||||
{
|
{
|
||||||
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
|
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
|
||||||
Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
|
Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
|
||||||
|
@ -44,7 +44,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
(bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, titleId, out string errorMessage);
|
(bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, out string errorMessage);
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
|
@ -66,13 +66,13 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadNsp(string path, ulong titleId)
|
public bool LoadNsp(string path)
|
||||||
{
|
{
|
||||||
FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
||||||
PartitionFileSystem partitionFileSystem = new();
|
PartitionFileSystem partitionFileSystem = new();
|
||||||
partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure();
|
partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure();
|
||||||
|
|
||||||
(bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, titleId, out string errorMessage);
|
(bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage);
|
||||||
|
|
||||||
if (processResult.ProcessId == 0)
|
if (processResult.ProcessId == 0)
|
||||||
{
|
{
|
||||||
|
|
|
@ -42,14 +42,15 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||||
{
|
{
|
||||||
Nca nca = partitionFileSystem.GetNca(device.FileSystem.KeySet, fileEntry.FullPath);
|
Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
|
||||||
|
|
||||||
if (!nca.IsProgram())
|
if (!nca.IsProgram() && nca.IsPatch())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong currentMainProgramId = nca.GetProgramIdBase();
|
ulong currentProgramId = nca.Header.TitleId;
|
||||||
|
ulong currentMainProgramId = currentProgramId & ~0xFFFul;
|
||||||
|
|
||||||
if (applicationId == 0 && currentMainProgramId != 0)
|
if (applicationId == 0 && currentMainProgramId != 0)
|
||||||
{
|
{
|
||||||
|
@ -66,7 +67,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasIndex[nca.GetProgramIndex()] = true;
|
hasIndex[(int)(currentProgramId & 0xF)] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (programCount == 0)
|
if (programCount == 0)
|
||||||
|
|
|
@ -72,9 +72,9 @@ namespace Ryujinx.HLE
|
||||||
return Processes.LoadUnpackedNca(exeFsDir, romFsFile);
|
return Processes.LoadUnpackedNca(exeFsDir, romFsFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadXci(string xciFile, ulong titleId = 0)
|
public bool LoadXci(string xciFile)
|
||||||
{
|
{
|
||||||
return Processes.LoadXci(xciFile, titleId);
|
return Processes.LoadXci(xciFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadNca(string ncaFile)
|
public bool LoadNca(string ncaFile)
|
||||||
|
@ -82,9 +82,9 @@ namespace Ryujinx.HLE
|
||||||
return Processes.LoadNca(ncaFile);
|
return Processes.LoadNca(ncaFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadNsp(string nspFile, ulong titleId = 0)
|
public bool LoadNsp(string nspFile)
|
||||||
{
|
{
|
||||||
return Processes.LoadNsp(nspFile, titleId);
|
return Processes.LoadNsp(nspFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadProgram(string fileName)
|
public bool LoadProgram(string fileName)
|
||||||
|
|
|
@ -9,11 +9,9 @@ using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ui.App.Common
|
namespace Ryujinx.Ui.App.Common
|
||||||
{
|
{
|
||||||
|
@ -21,10 +19,10 @@ namespace Ryujinx.Ui.App.Common
|
||||||
{
|
{
|
||||||
public bool Favorite { get; set; }
|
public bool Favorite { get; set; }
|
||||||
public byte[] Icon { get; set; }
|
public byte[] Icon { get; set; }
|
||||||
public string Name { get; set; } = "Unknown";
|
public string TitleName { get; set; }
|
||||||
public ulong Id { get; set; }
|
public string TitleId { get; set; }
|
||||||
public string Developer { get; set; } = "Unknown";
|
public string Developer { get; set; }
|
||||||
public string Version { get; set; } = "0";
|
public string Version { get; set; }
|
||||||
public TimeSpan TimePlayed { get; set; }
|
public TimeSpan TimePlayed { get; set; }
|
||||||
public DateTime? LastPlayed { get; set; }
|
public DateTime? LastPlayed { get; set; }
|
||||||
public string FileExtension { get; set; }
|
public string FileExtension { get; set; }
|
||||||
|
@ -38,11 +36,7 @@ namespace Ryujinx.Ui.App.Common
|
||||||
|
|
||||||
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
|
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
|
||||||
|
|
||||||
[JsonIgnore] public string IdString => Id.ToString("x16");
|
public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath)
|
||||||
|
|
||||||
[JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL;
|
|
||||||
|
|
||||||
public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath)
|
|
||||||
{
|
{
|
||||||
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
|
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
|
@ -111,7 +105,7 @@ namespace Ryujinx.Ui.App.Common
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
(Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _);
|
(Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
|
||||||
|
|
||||||
if (updatePatchNca != null)
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,7 +7,6 @@ using Ryujinx.Common.SystemInterop;
|
||||||
using Ryujinx.Modules;
|
using Ryujinx.Modules;
|
||||||
using Ryujinx.SDL2.Common;
|
using Ryujinx.SDL2.Common;
|
||||||
using Ryujinx.Ui;
|
using Ryujinx.Ui;
|
||||||
using Ryujinx.Ui.App.Common;
|
|
||||||
using Ryujinx.Ui.Common;
|
using Ryujinx.Ui.Common;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
|
@ -333,12 +332,7 @@ namespace Ryujinx
|
||||||
|
|
||||||
if (CommandLineState.LaunchPathArg != null)
|
if (CommandLineState.LaunchPathArg != null)
|
||||||
{
|
{
|
||||||
ApplicationData applicationData = new()
|
mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
|
||||||
{
|
|
||||||
Path = CommandLineState.LaunchPathArg,
|
|
||||||
};
|
|
||||||
|
|
||||||
mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))
|
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))
|
||||||
|
|
|
@ -39,7 +39,6 @@ using Silk.NET.Vulkan;
|
||||||
using SPB.Graphics.Vulkan;
|
using SPB.Graphics.Vulkan;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -71,7 +70,7 @@ namespace Ryujinx.Ui
|
||||||
private bool _gameLoaded;
|
private bool _gameLoaded;
|
||||||
private bool _ending;
|
private bool _ending;
|
||||||
|
|
||||||
private ApplicationData _currentApplicationData = null;
|
private string _currentEmulatedGamePath = null;
|
||||||
|
|
||||||
private string _lastScannedAmiiboId = "";
|
private string _lastScannedAmiiboId = "";
|
||||||
private bool _lastScannedAmiiboShowAll = false;
|
private bool _lastScannedAmiiboShowAll = false;
|
||||||
|
@ -182,12 +181,8 @@ namespace Ryujinx.Ui
|
||||||
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile);
|
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile);
|
||||||
_userChannelPersistence = new UserChannelPersistence();
|
_userChannelPersistence = new UserChannelPersistence();
|
||||||
|
|
||||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
|
||||||
? IntegrityCheckLevel.ErrorOnInvalid
|
|
||||||
: IntegrityCheckLevel.None;
|
|
||||||
|
|
||||||
// Instantiate GUI objects.
|
// Instantiate GUI objects.
|
||||||
_applicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel);
|
_applicationLibrary = new ApplicationLibrary(_virtualFileSystem);
|
||||||
_uiHandler = new GtkHostUiHandler(this);
|
_uiHandler = new GtkHostUiHandler(this);
|
||||||
_deviceExitStatus = new AutoResetEvent(false);
|
_deviceExitStatus = new AutoResetEvent(false);
|
||||||
|
|
||||||
|
@ -789,7 +784,7 @@ namespace Ryujinx.Ui
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool LoadApplication(string path, ulong titleId, bool isFirmwareTitle)
|
private bool LoadApplication(string path, bool isFirmwareTitle)
|
||||||
{
|
{
|
||||||
SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
|
SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
|
||||||
|
|
||||||
|
@ -863,7 +858,7 @@ namespace Ryujinx.Ui
|
||||||
case ".xci":
|
case ".xci":
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
||||||
|
|
||||||
return _emulationContext.LoadXci(path, titleId);
|
return _emulationContext.LoadXci(path);
|
||||||
case ".nca":
|
case ".nca":
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
|
Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
|
||||||
|
|
||||||
|
@ -872,7 +867,7 @@ namespace Ryujinx.Ui
|
||||||
case ".pfs0":
|
case ".pfs0":
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
||||||
|
|
||||||
return _emulationContext.LoadNsp(path, titleId);
|
return _emulationContext.LoadNsp(path);
|
||||||
default:
|
default:
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
|
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
|
||||||
try
|
try
|
||||||
|
@ -893,7 +888,7 @@ namespace Ryujinx.Ui
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RunApplication(ApplicationData application, bool startFullscreen = false)
|
public void RunApplication(string path, bool startFullscreen = false)
|
||||||
{
|
{
|
||||||
if (_gameLoaded)
|
if (_gameLoaded)
|
||||||
{
|
{
|
||||||
|
@ -915,14 +910,14 @@ namespace Ryujinx.Ui
|
||||||
|
|
||||||
bool isFirmwareTitle = false;
|
bool isFirmwareTitle = false;
|
||||||
|
|
||||||
if (application.Path.StartsWith("@SystemContent"))
|
if (path.StartsWith("@SystemContent"))
|
||||||
{
|
{
|
||||||
application.Path = VirtualFileSystem.SwitchPathToSystemPath(application.Path);
|
path = VirtualFileSystem.SwitchPathToSystemPath(path);
|
||||||
|
|
||||||
isFirmwareTitle = true;
|
isFirmwareTitle = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!LoadApplication(application.Path, application.Id, isFirmwareTitle))
|
if (!LoadApplication(path, isFirmwareTitle))
|
||||||
{
|
{
|
||||||
_emulationContext.Dispose();
|
_emulationContext.Dispose();
|
||||||
SwitchToGameTable();
|
SwitchToGameTable();
|
||||||
|
@ -932,7 +927,7 @@ namespace Ryujinx.Ui
|
||||||
|
|
||||||
SetupProgressUiHandlers();
|
SetupProgressUiHandlers();
|
||||||
|
|
||||||
_currentApplicationData = application;
|
_currentEmulatedGamePath = path;
|
||||||
|
|
||||||
_deviceExitStatus.Reset();
|
_deviceExitStatus.Reset();
|
||||||
|
|
||||||
|
@ -1173,7 +1168,7 @@ namespace Ryujinx.Ui
|
||||||
_tableStore.AppendValues(
|
_tableStore.AppendValues(
|
||||||
args.AppData.Favorite,
|
args.AppData.Favorite,
|
||||||
new Gdk.Pixbuf(args.AppData.Icon, 75, 75),
|
new Gdk.Pixbuf(args.AppData.Icon, 75, 75),
|
||||||
$"{args.AppData.Name}\n{args.AppData.IdString.ToUpper()}",
|
$"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}",
|
||||||
args.AppData.Developer,
|
args.AppData.Developer,
|
||||||
args.AppData.Version,
|
args.AppData.Version,
|
||||||
args.AppData.TimePlayedString,
|
args.AppData.TimePlayedString,
|
||||||
|
@ -1261,22 +1256,9 @@ namespace Ryujinx.Ui
|
||||||
{
|
{
|
||||||
_gameTableSelection.GetSelected(out TreeIter treeIter);
|
_gameTableSelection.GetSelected(out TreeIter treeIter);
|
||||||
|
|
||||||
ApplicationData application = new()
|
string path = (string)_tableStore.GetValue(treeIter, 9);
|
||||||
{
|
|
||||||
Favorite = (bool)_tableStore.GetValue(treeIter, 0),
|
|
||||||
Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0],
|
|
||||||
Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber),
|
|
||||||
Developer = (string)_tableStore.GetValue(treeIter, 3),
|
|
||||||
Version = (string)_tableStore.GetValue(treeIter, 4),
|
|
||||||
TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)),
|
|
||||||
LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)),
|
|
||||||
FileExtension = (string)_tableStore.GetValue(treeIter, 7),
|
|
||||||
FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)),
|
|
||||||
Path = (string)_tableStore.GetValue(treeIter, 9),
|
|
||||||
ControlHolder = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10),
|
|
||||||
};
|
|
||||||
|
|
||||||
RunApplication(application);
|
RunApplication(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args)
|
private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args)
|
||||||
|
@ -1334,22 +1316,13 @@ namespace Ryujinx.Ui
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplicationData application = new()
|
string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString();
|
||||||
{
|
string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0];
|
||||||
Favorite = (bool)_tableStore.GetValue(treeIter, 0),
|
string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower();
|
||||||
Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0],
|
|
||||||
Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber),
|
|
||||||
Developer = (string)_tableStore.GetValue(treeIter, 3),
|
|
||||||
Version = (string)_tableStore.GetValue(treeIter, 4),
|
|
||||||
TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)),
|
|
||||||
LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)),
|
|
||||||
FileExtension = (string)_tableStore.GetValue(treeIter, 7),
|
|
||||||
FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)),
|
|
||||||
Path = (string)_tableStore.GetValue(treeIter, 9),
|
|
||||||
ControlHolder = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10),
|
|
||||||
};
|
|
||||||
|
|
||||||
_ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, application);
|
BlitStruct<ApplicationControlProperty> controlData = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10);
|
||||||
|
|
||||||
|
_ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Load_Application_File(object sender, EventArgs args)
|
private void Load_Application_File(object sender, EventArgs args)
|
||||||
|
@ -1371,12 +1344,7 @@ namespace Ryujinx.Ui
|
||||||
|
|
||||||
if (fileChooser.Run() == (int)ResponseType.Accept)
|
if (fileChooser.Run() == (int)ResponseType.Accept)
|
||||||
{
|
{
|
||||||
ApplicationData applicationData = new()
|
RunApplication(fileChooser.Filename);
|
||||||
{
|
|
||||||
Path = fileChooser.Filename,
|
|
||||||
};
|
|
||||||
|
|
||||||
RunApplication(applicationData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1386,13 +1354,7 @@ namespace Ryujinx.Ui
|
||||||
|
|
||||||
if (fileChooser.Run() == (int)ResponseType.Accept)
|
if (fileChooser.Run() == (int)ResponseType.Accept)
|
||||||
{
|
{
|
||||||
ApplicationData applicationData = new()
|
RunApplication(fileChooser.Filename);
|
||||||
{
|
|
||||||
Name = System.IO.Path.GetFileNameWithoutExtension(fileChooser.Filename),
|
|
||||||
Path = fileChooser.Filename,
|
|
||||||
};
|
|
||||||
|
|
||||||
RunApplication(applicationData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1407,14 +1369,7 @@ namespace Ryujinx.Ui
|
||||||
{
|
{
|
||||||
string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
|
string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
|
||||||
|
|
||||||
ApplicationData applicationData = new()
|
RunApplication(contentPath);
|
||||||
{
|
|
||||||
Name = "miiEdit",
|
|
||||||
Id = 0x0100000000001009ul,
|
|
||||||
Path = contentPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
RunApplication(applicationData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Open_Ryu_Folder(object sender, EventArgs args)
|
private void Open_Ryu_Folder(object sender, EventArgs args)
|
||||||
|
@ -1690,13 +1645,13 @@ namespace Ryujinx.Ui
|
||||||
{
|
{
|
||||||
_userChannelPersistence.ShouldRestart = false;
|
_userChannelPersistence.ShouldRestart = false;
|
||||||
|
|
||||||
RunApplication(_currentApplicationData);
|
RunApplication(_currentEmulatedGamePath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// otherwise, clear state.
|
// otherwise, clear state.
|
||||||
_userChannelPersistence = new UserChannelPersistence();
|
_userChannelPersistence = new UserChannelPersistence();
|
||||||
_currentApplicationData = null;
|
_currentEmulatedGamePath = null;
|
||||||
_actionMenu.Sensitive = false;
|
_actionMenu.Sensitive = false;
|
||||||
_firmwareInstallFile.Sensitive = true;
|
_firmwareInstallFile.Sensitive = true;
|
||||||
_firmwareInstallDirectory.Sensitive = true;
|
_firmwareInstallDirectory.Sensitive = true;
|
||||||
|
@ -1758,7 +1713,7 @@ namespace Ryujinx.Ui
|
||||||
_emulationContext.Processes.ActiveApplication.ProgramId,
|
_emulationContext.Processes.ActiveApplication.ProgramId,
|
||||||
_emulationContext.Processes.ActiveApplication.ApplicationControlProperties
|
_emulationContext.Processes.ActiveApplication.ApplicationControlProperties
|
||||||
.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(),
|
.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(),
|
||||||
_currentApplicationData.Path);
|
_currentEmulatedGamePath);
|
||||||
|
|
||||||
window.Destroyed += CheatWindow_Destroyed;
|
window.Destroyed += CheatWindow_Destroyed;
|
||||||
window.Show();
|
window.Show();
|
||||||
|
|
|
@ -16,7 +16,6 @@ using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
|
||||||
using Ryujinx.Ui.App.Common;
|
using Ryujinx.Ui.App.Common;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
|
@ -24,6 +23,7 @@ using Ryujinx.Ui.Windows;
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -36,13 +36,17 @@ namespace Ryujinx.Ui.Widgets
|
||||||
private readonly VirtualFileSystem _virtualFileSystem;
|
private readonly VirtualFileSystem _virtualFileSystem;
|
||||||
private readonly AccountManager _accountManager;
|
private readonly AccountManager _accountManager;
|
||||||
private readonly HorizonClient _horizonClient;
|
private readonly HorizonClient _horizonClient;
|
||||||
|
private readonly BlitStruct<ApplicationControlProperty> _controlData;
|
||||||
|
|
||||||
private readonly ApplicationData _title;
|
private readonly string _titleFilePath;
|
||||||
|
private readonly string _titleName;
|
||||||
|
private readonly string _titleIdText;
|
||||||
|
private readonly ulong _titleId;
|
||||||
|
|
||||||
private MessageDialog _dialog;
|
private MessageDialog _dialog;
|
||||||
private bool _cancel;
|
private bool _cancel;
|
||||||
|
|
||||||
public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, ApplicationData applicationData)
|
public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct<ApplicationControlProperty> controlData)
|
||||||
{
|
{
|
||||||
_parent = parent;
|
_parent = parent;
|
||||||
|
|
||||||
|
@ -51,13 +55,23 @@ namespace Ryujinx.Ui.Widgets
|
||||||
_virtualFileSystem = virtualFileSystem;
|
_virtualFileSystem = virtualFileSystem;
|
||||||
_accountManager = accountManager;
|
_accountManager = accountManager;
|
||||||
_horizonClient = horizonClient;
|
_horizonClient = horizonClient;
|
||||||
_title = applicationData;
|
_titleFilePath = titleFilePath;
|
||||||
|
_titleName = titleName;
|
||||||
|
_titleIdText = titleId;
|
||||||
|
_controlData = controlData;
|
||||||
|
|
||||||
_openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.UserAccountSaveDataSize > 0;
|
if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId))
|
||||||
_openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.DeviceSaveDataSize > 0;
|
{
|
||||||
_openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id");
|
||||||
|
|
||||||
string fileExt = System.IO.Path.GetExtension(_title.Path).ToLower();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0;
|
||||||
|
_openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0;
|
||||||
|
_openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0;
|
||||||
|
|
||||||
|
string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower();
|
||||||
bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci";
|
bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci";
|
||||||
|
|
||||||
_extractRomFsMenuItem.Sensitive = hasNca;
|
_extractRomFsMenuItem.Sensitive = hasNca;
|
||||||
|
@ -123,7 +137,7 @@ namespace Ryujinx.Ui.Widgets
|
||||||
|
|
||||||
private void OpenSaveDir(in SaveDataFilter saveDataFilter)
|
private void OpenSaveDir(in SaveDataFilter saveDataFilter)
|
||||||
{
|
{
|
||||||
if (!TryFindSaveData(_title.Name, _title.Id, _title.ControlHolder, in saveDataFilter, out ulong saveDataId))
|
if (!TryFindSaveData(_titleName, _titleId, _controlData, in saveDataFilter, out ulong saveDataId))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -176,7 +190,7 @@ namespace Ryujinx.Ui.Widgets
|
||||||
{
|
{
|
||||||
Title = "Ryujinx - NCA Section Extractor",
|
Title = "Ryujinx - NCA Section Extractor",
|
||||||
Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"),
|
Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"),
|
||||||
SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_title.Path)}...",
|
SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...",
|
||||||
WindowPosition = WindowPosition.Center,
|
WindowPosition = WindowPosition.Center,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -188,18 +202,18 @@ namespace Ryujinx.Ui.Widgets
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
using FileStream file = new(_title.Path, FileMode.Open, FileAccess.Read);
|
using FileStream file = new(_titleFilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
Nca mainNca = null;
|
Nca mainNca = null;
|
||||||
Nca patchNca = null;
|
Nca patchNca = null;
|
||||||
|
|
||||||
if ((System.IO.Path.GetExtension(_title.Path).ToLower() == ".nsp") ||
|
if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") ||
|
||||||
(System.IO.Path.GetExtension(_title.Path).ToLower() == ".pfs0") ||
|
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") ||
|
||||||
(System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci"))
|
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci"))
|
||||||
{
|
{
|
||||||
IFileSystem pfs;
|
IFileSystem pfs;
|
||||||
|
|
||||||
if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci")
|
if (System.IO.Path.GetExtension(_titleFilePath) == ".xci")
|
||||||
{
|
{
|
||||||
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
|
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
|
||||||
|
|
||||||
|
@ -235,7 +249,7 @@ namespace Ryujinx.Ui.Widgets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".nca")
|
else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca")
|
||||||
{
|
{
|
||||||
mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
|
mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
|
||||||
}
|
}
|
||||||
|
@ -252,11 +266,7 @@ namespace Ryujinx.Ui.Widgets
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
(Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
|
||||||
? IntegrityCheckLevel.ErrorOnInvalid
|
|
||||||
: IntegrityCheckLevel.None;
|
|
||||||
|
|
||||||
(Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _);
|
|
||||||
|
|
||||||
if (updatePatchNca != null)
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
|
@ -450,44 +460,44 @@ namespace Ryujinx.Ui.Widgets
|
||||||
private void OpenSaveUserDir_Clicked(object sender, EventArgs args)
|
private void OpenSaveUserDir_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
|
var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
|
||||||
var saveDataFilter = SaveDataFilter.Make(_title.Id, saveType: default, userId, saveDataId: default, index: default);
|
var saveDataFilter = SaveDataFilter.Make(_titleId, saveType: default, userId, saveDataId: default, index: default);
|
||||||
|
|
||||||
OpenSaveDir(in saveDataFilter);
|
OpenSaveDir(in saveDataFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args)
|
private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
var saveDataFilter = SaveDataFilter.Make(_title.Id, SaveDataType.Device, userId: default, saveDataId: default, index: default);
|
var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Device, userId: default, saveDataId: default, index: default);
|
||||||
|
|
||||||
OpenSaveDir(in saveDataFilter);
|
OpenSaveDir(in saveDataFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenSaveBcatDir_Clicked(object sender, EventArgs args)
|
private void OpenSaveBcatDir_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
var saveDataFilter = SaveDataFilter.Make(_title.Id, SaveDataType.Bcat, userId: default, saveDataId: default, index: default);
|
var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Bcat, userId: default, saveDataId: default, index: default);
|
||||||
|
|
||||||
OpenSaveDir(in saveDataFilter);
|
OpenSaveDir(in saveDataFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ManageTitleUpdates_Clicked(object sender, EventArgs args)
|
private void ManageTitleUpdates_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
new TitleUpdateWindow(_parent, _virtualFileSystem, _title).Show();
|
new TitleUpdateWindow(_parent, _virtualFileSystem, _titleIdText, _titleName).Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ManageDlc_Clicked(object sender, EventArgs args)
|
private void ManageDlc_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
new DlcWindow(_virtualFileSystem, _title.IdString, _title).Show();
|
new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ManageCheats_Clicked(object sender, EventArgs args)
|
private void ManageCheats_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
new CheatWindow(_virtualFileSystem, _title.Id, _title.Name, _title.Path).Show();
|
new CheatWindow(_virtualFileSystem, _titleId, _titleName, _titleFilePath).Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenTitleModDir_Clicked(object sender, EventArgs args)
|
private void OpenTitleModDir_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
string modsBasePath = ModLoader.GetModsBasePath();
|
string modsBasePath = ModLoader.GetModsBasePath();
|
||||||
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _title.IdString);
|
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText);
|
||||||
|
|
||||||
OpenHelper.OpenFolder(titleModsPath);
|
OpenHelper.OpenFolder(titleModsPath);
|
||||||
}
|
}
|
||||||
|
@ -495,7 +505,7 @@ namespace Ryujinx.Ui.Widgets
|
||||||
private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
|
private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
||||||
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _title.IdString);
|
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText);
|
||||||
|
|
||||||
OpenHelper.OpenFolder(titleModsPath);
|
OpenHelper.OpenFolder(titleModsPath);
|
||||||
}
|
}
|
||||||
|
@ -517,7 +527,7 @@ namespace Ryujinx.Ui.Widgets
|
||||||
|
|
||||||
private void OpenPtcDir_Clicked(object sender, EventArgs args)
|
private void OpenPtcDir_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu");
|
string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu");
|
||||||
|
|
||||||
string mainPath = System.IO.Path.Combine(ptcDir, "0");
|
string mainPath = System.IO.Path.Combine(ptcDir, "0");
|
||||||
string backupPath = System.IO.Path.Combine(ptcDir, "1");
|
string backupPath = System.IO.Path.Combine(ptcDir, "1");
|
||||||
|
@ -534,7 +544,7 @@ namespace Ryujinx.Ui.Widgets
|
||||||
|
|
||||||
private void OpenShaderCacheDir_Clicked(object sender, EventArgs args)
|
private void OpenShaderCacheDir_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader");
|
string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader");
|
||||||
|
|
||||||
if (!Directory.Exists(shaderCacheDir))
|
if (!Directory.Exists(shaderCacheDir))
|
||||||
{
|
{
|
||||||
|
@ -546,10 +556,10 @@ namespace Ryujinx.Ui.Widgets
|
||||||
|
|
||||||
private void PurgePtcCache_Clicked(object sender, EventArgs args)
|
private void PurgePtcCache_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "0"));
|
DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0"));
|
||||||
DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "1"));
|
DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1"));
|
||||||
|
|
||||||
MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n<b>{_title.Name}</b>\n\nAre you sure you want to proceed?");
|
MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n<b>{_titleName}</b>\n\nAre you sure you want to proceed?");
|
||||||
|
|
||||||
List<FileInfo> cacheFiles = new();
|
List<FileInfo> cacheFiles = new();
|
||||||
|
|
||||||
|
@ -583,9 +593,9 @@ namespace Ryujinx.Ui.Widgets
|
||||||
|
|
||||||
private void PurgeShaderCache_Clicked(object sender, EventArgs args)
|
private void PurgeShaderCache_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader"));
|
DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"));
|
||||||
|
|
||||||
using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n<b>{_title.Name}</b>\n\nAre you sure you want to proceed?");
|
using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n<b>{_titleName}</b>\n\nAre you sure you want to proceed?");
|
||||||
|
|
||||||
List<DirectoryInfo> oldCacheDirectories = new();
|
List<DirectoryInfo> oldCacheDirectories = new();
|
||||||
List<FileInfo> newCacheFiles = new();
|
List<FileInfo> newCacheFiles = new();
|
||||||
|
@ -627,11 +637,8 @@ namespace Ryujinx.Ui.Widgets
|
||||||
|
|
||||||
private void CreateShortcut_Clicked(object sender, EventArgs args)
|
private void CreateShortcut_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language);
|
||||||
? IntegrityCheckLevel.ErrorOnInvalid
|
ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon);
|
||||||
: IntegrityCheckLevel.None;
|
|
||||||
byte[] appIcon = new ApplicationLibrary(_virtualFileSystem, checkLevel).GetApplicationIcon(_title.Path, ConfigurationState.Instance.System.Language, _title.Id);
|
|
||||||
ShortcutHelper.CreateAppShortcut(_title.Path, _title.Name, _title.IdString, appIcon);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
using Gtk;
|
using Gtk;
|
||||||
using LibHac.Tools.FsSystem;
|
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
using Ryujinx.Ui.App.Common;
|
using Ryujinx.Ui.App.Common;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -29,13 +27,8 @@ namespace Ryujinx.Ui.Windows
|
||||||
private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow"))
|
private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow"))
|
||||||
{
|
{
|
||||||
builder.Autoconnect(this);
|
builder.Autoconnect(this);
|
||||||
|
|
||||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
|
||||||
? IntegrityCheckLevel.ErrorOnInvalid
|
|
||||||
: IntegrityCheckLevel.None;
|
|
||||||
|
|
||||||
_baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]";
|
_baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]";
|
||||||
_buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath)}";
|
_buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath)}";
|
||||||
|
|
||||||
string modsBasePath = ModLoader.GetModsBasePath();
|
string modsBasePath = ModLoader.GetModsBasePath();
|
||||||
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16"));
|
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16"));
|
||||||
|
|
|
@ -9,12 +9,9 @@ using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
|
||||||
using Ryujinx.Ui.App.Common;
|
|
||||||
using Ryujinx.Ui.Widgets;
|
using Ryujinx.Ui.Widgets;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using GUI = Gtk.Builder.ObjectAttribute;
|
using GUI = Gtk.Builder.ObjectAttribute;
|
||||||
|
|
||||||
|
@ -23,7 +20,7 @@ namespace Ryujinx.Ui.Windows
|
||||||
public class DlcWindow : Window
|
public class DlcWindow : Window
|
||||||
{
|
{
|
||||||
private readonly VirtualFileSystem _virtualFileSystem;
|
private readonly VirtualFileSystem _virtualFileSystem;
|
||||||
private readonly string _applicationId;
|
private readonly string _titleId;
|
||||||
private readonly string _dlcJsonPath;
|
private readonly string _dlcJsonPath;
|
||||||
private readonly List<DownloadableContentContainer> _dlcContainerList;
|
private readonly List<DownloadableContentContainer> _dlcContainerList;
|
||||||
|
|
||||||
|
@ -35,16 +32,16 @@ namespace Ryujinx.Ui.Windows
|
||||||
[GUI] TreeSelection _dlcTreeSelection;
|
[GUI] TreeSelection _dlcTreeSelection;
|
||||||
#pragma warning restore CS0649, IDE0044
|
#pragma warning restore CS0649, IDE0044
|
||||||
|
|
||||||
public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, ApplicationData title) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, title) { }
|
public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { }
|
||||||
|
|
||||||
private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string applicationId, ApplicationData title) : base(builder.GetRawOwnedObject("_dlcWindow"))
|
private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow"))
|
||||||
{
|
{
|
||||||
builder.Autoconnect(this);
|
builder.Autoconnect(this);
|
||||||
|
|
||||||
_applicationId = applicationId;
|
_titleId = titleId;
|
||||||
_virtualFileSystem = virtualFileSystem;
|
_virtualFileSystem = virtualFileSystem;
|
||||||
_dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationId, "dlc.json");
|
_dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json");
|
||||||
_baseTitleInfoLabel.Text = $"DLC Available for {title.Name} [{applicationId.ToUpper()}]";
|
_baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]";
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -75,12 +72,9 @@ namespace Ryujinx.Ui.Windows
|
||||||
};
|
};
|
||||||
|
|
||||||
_dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0);
|
_dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0);
|
||||||
_dlcTreeView.AppendColumn("ApplicationId", new CellRendererText(), "text", 1);
|
_dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1);
|
||||||
_dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
|
_dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
|
||||||
|
|
||||||
// NOTE: Try to load downloadable contents from PFS first.
|
|
||||||
AddDlc(title.Path, true);
|
|
||||||
|
|
||||||
foreach (DownloadableContentContainer dlcContainer in _dlcContainerList)
|
foreach (DownloadableContentContainer dlcContainer in _dlcContainerList)
|
||||||
{
|
{
|
||||||
if (File.Exists(dlcContainer.ContainerPath))
|
if (File.Exists(dlcContainer.ContainerPath))
|
||||||
|
@ -95,10 +89,7 @@ namespace Ryujinx.Ui.Windows
|
||||||
using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
|
using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
|
||||||
|
|
||||||
PartitionFileSystem pfs = new();
|
PartitionFileSystem pfs = new();
|
||||||
if (pfs.Initialize(containerFile.AsStorage()).IsFailure())
|
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_virtualFileSystem.ImportTickets(pfs);
|
_virtualFileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
|
@ -137,57 +128,6 @@ namespace Ryujinx.Ui.Windows
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddDlc(string path, bool ignoreNotFound = false)
|
|
||||||
{
|
|
||||||
if (!File.Exists(path))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using FileStream containerFile = File.OpenRead(path);
|
|
||||||
|
|
||||||
PartitionFileSystem pfs = new();
|
|
||||||
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
|
||||||
|
|
||||||
bool containsDlc = false;
|
|
||||||
|
|
||||||
_virtualFileSystem.ImportTickets(pfs);
|
|
||||||
|
|
||||||
TreeIter? parentIter = null;
|
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
|
||||||
{
|
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
|
||||||
|
|
||||||
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
|
|
||||||
|
|
||||||
if (nca == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
|
||||||
{
|
|
||||||
if (nca.GetProgramIdBase() != (ulong.Parse(_applicationId, NumberStyles.HexNumber) & ~0x1FFFUL))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", path);
|
|
||||||
|
|
||||||
((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
|
|
||||||
containsDlc = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!containsDlc && !ignoreNotFound)
|
|
||||||
{
|
|
||||||
GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddButton_Clicked(object sender, EventArgs args)
|
private void AddButton_Clicked(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel")
|
FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel")
|
||||||
|
@ -207,7 +147,52 @@ namespace Ryujinx.Ui.Windows
|
||||||
{
|
{
|
||||||
foreach (string containerPath in fileChooser.Filenames)
|
foreach (string containerPath in fileChooser.Filenames)
|
||||||
{
|
{
|
||||||
AddDlc(containerPath);
|
if (!File.Exists(containerPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using FileStream containerFile = File.OpenRead(containerPath);
|
||||||
|
|
||||||
|
PartitionFileSystem pfs = new();
|
||||||
|
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
||||||
|
bool containsDlc = false;
|
||||||
|
|
||||||
|
_virtualFileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
|
TreeIter? parentIter = null;
|
||||||
|
|
||||||
|
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||||
|
{
|
||||||
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath);
|
||||||
|
|
||||||
|
if (nca == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||||
|
{
|
||||||
|
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath);
|
||||||
|
|
||||||
|
((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
|
||||||
|
containsDlc = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!containsDlc)
|
||||||
|
{
|
||||||
|
GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,12 @@ using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
using LibHac.Tools.Fs;
|
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
|
||||||
using Ryujinx.Ui.App.Common;
|
using Ryujinx.Ui.App.Common;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
|
||||||
using Ryujinx.Ui.Widgets;
|
using Ryujinx.Ui.Widgets;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -27,7 +24,7 @@ namespace Ryujinx.Ui.Windows
|
||||||
{
|
{
|
||||||
private readonly MainWindow _parent;
|
private readonly MainWindow _parent;
|
||||||
private readonly VirtualFileSystem _virtualFileSystem;
|
private readonly VirtualFileSystem _virtualFileSystem;
|
||||||
private readonly ApplicationData _title;
|
private readonly string _titleId;
|
||||||
private readonly string _updateJsonPath;
|
private readonly string _updateJsonPath;
|
||||||
|
|
||||||
private TitleUpdateMetadata _titleUpdateWindowData;
|
private TitleUpdateMetadata _titleUpdateWindowData;
|
||||||
|
@ -41,17 +38,17 @@ namespace Ryujinx.Ui.Windows
|
||||||
[GUI] RadioButton _noUpdateRadioButton;
|
[GUI] RadioButton _noUpdateRadioButton;
|
||||||
#pragma warning restore CS0649, IDE0044
|
#pragma warning restore CS0649, IDE0044
|
||||||
|
|
||||||
public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, applicationData) { }
|
public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { }
|
||||||
|
|
||||||
private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : base(builder.GetRawOwnedObject("_titleUpdateWindow"))
|
private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow"))
|
||||||
{
|
{
|
||||||
_parent = parent;
|
_parent = parent;
|
||||||
|
|
||||||
builder.Autoconnect(this);
|
builder.Autoconnect(this);
|
||||||
|
|
||||||
_title = applicationData;
|
_titleId = titleId;
|
||||||
_virtualFileSystem = virtualFileSystem;
|
_virtualFileSystem = virtualFileSystem;
|
||||||
_updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "updates.json");
|
_updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json");
|
||||||
_radioButtonToPathDictionary = new Dictionary<RadioButton, string>();
|
_radioButtonToPathDictionary = new Dictionary<RadioButton, string>();
|
||||||
|
|
||||||
try
|
try
|
||||||
|
@ -67,10 +64,7 @@ namespace Ryujinx.Ui.Windows
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_baseTitleInfoLabel.Text = $"Updates Available for {applicationData.Name} [{applicationData.IdString}]";
|
_baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]";
|
||||||
|
|
||||||
// Try to get updates from PFS first
|
|
||||||
AddUpdate(_title.Path, true);
|
|
||||||
|
|
||||||
foreach (string path in _titleUpdateWindowData.Paths)
|
foreach (string path in _titleUpdateWindowData.Paths)
|
||||||
{
|
{
|
||||||
|
@ -90,41 +84,18 @@ namespace Ryujinx.Ui.Windows
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddUpdate(string path, bool ignoreNotFound = false)
|
private void AddUpdate(string path)
|
||||||
{
|
{
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
|
||||||
? IntegrityCheckLevel.ErrorOnInvalid
|
|
||||||
: IntegrityCheckLevel.None;
|
|
||||||
|
|
||||||
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
IFileSystem pfs;
|
PartitionFileSystem nsp = new();
|
||||||
|
nsp.Initialize(file.AsStorage()).ThrowIfFailure();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (System.IO.Path.GetExtension(path).ToLower() == ".xci")
|
(Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0);
|
||||||
{
|
|
||||||
pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var pfsTemp = new PartitionFileSystem();
|
|
||||||
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
|
|
||||||
pfs = pfsTemp;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dictionary<ulong, ContentCollection> updates = pfs.GetUpdateData(_virtualFileSystem, checkLevel);
|
|
||||||
|
|
||||||
Nca patchNca = null;
|
|
||||||
Nca controlNca = null;
|
|
||||||
|
|
||||||
if (updates.TryGetValue(_title.Id, out ContentCollection update))
|
|
||||||
{
|
|
||||||
patchNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Program);
|
|
||||||
controlNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Control);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controlNca != null && patchNca != null)
|
if (controlNca != null && patchNca != null)
|
||||||
{
|
{
|
||||||
|
@ -135,14 +106,7 @@ namespace Ryujinx.Ui.Windows
|
||||||
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
||||||
|
|
||||||
string radioLabel = $"Version {controlData.DisplayVersionString.ToString()} - {path}";
|
RadioButton radioButton = new($"Version {controlData.DisplayVersionString.ToString()} - {path}");
|
||||||
|
|
||||||
if (System.IO.Path.GetExtension(path).ToLower() == ".xci")
|
|
||||||
{
|
|
||||||
radioLabel = "Bundled: " + radioLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
RadioButton radioButton = new(radioLabel);
|
|
||||||
radioButton.JoinGroup(_noUpdateRadioButton);
|
radioButton.JoinGroup(_noUpdateRadioButton);
|
||||||
|
|
||||||
_availableUpdatesBox.Add(radioButton);
|
_availableUpdatesBox.Add(radioButton);
|
||||||
|
@ -153,10 +117,7 @@ namespace Ryujinx.Ui.Windows
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!ignoreNotFound)
|
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
|
||||||
{
|
|
||||||
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
|
|
Loading…
Reference in a new issue