Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5c3cfb84c0 | ||
|
55557525b1 | ||
|
7e6342e44d | ||
|
c3555cb5d6 | ||
|
815819767c | ||
|
623604c391 | ||
|
617c5700ca |
@@ -38,7 +38,9 @@ namespace ARMeilleure.Decoders
|
||||
{
|
||||
block = new Block(blkAddress);
|
||||
|
||||
if ((dMode != DecoderMode.MultipleBlocks && visited.Count >= 1) || opsCount > instructionLimit || !memory.IsMapped(blkAddress))
|
||||
if ((dMode != DecoderMode.MultipleBlocks && visited.Count >= 1) ||
|
||||
opsCount > instructionLimit ||
|
||||
(visited.Count > 0 && !memory.IsMapped(blkAddress)))
|
||||
{
|
||||
block.Exit = true;
|
||||
block.EndAddress = blkAddress;
|
||||
|
@@ -54,8 +54,6 @@ using System.Threading.Tasks;
|
||||
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
|
||||
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
using InputManager = Ryujinx.Input.HLE.InputManager;
|
||||
using IRenderer = Ryujinx.Graphics.GAL.IRenderer;
|
||||
using Key = Ryujinx.Input.Key;
|
||||
using MouseButton = Ryujinx.Input.MouseButton;
|
||||
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
|
||||
@@ -123,12 +121,14 @@ namespace Ryujinx.Ava
|
||||
public int Width { get; private set; }
|
||||
public int Height { get; private set; }
|
||||
public string ApplicationPath { get; private set; }
|
||||
public ulong ApplicationId { get; private set; }
|
||||
public bool ScreenshotRequested { get; set; }
|
||||
|
||||
public AppHost(
|
||||
RendererHost renderer,
|
||||
InputManager inputManager,
|
||||
string applicationPath,
|
||||
ulong applicationId,
|
||||
VirtualFileSystem virtualFileSystem,
|
||||
ContentManager contentManager,
|
||||
AccountManager accountManager,
|
||||
@@ -152,6 +152,7 @@ namespace Ryujinx.Ava
|
||||
NpadManager = _inputManager.CreateNpadManager();
|
||||
TouchScreenManager = _inputManager.CreateTouchScreenManager();
|
||||
ApplicationPath = applicationPath;
|
||||
ApplicationId = applicationId;
|
||||
VirtualFileSystem = virtualFileSystem;
|
||||
ContentManager = contentManager;
|
||||
|
||||
@@ -641,7 +642,7 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
||||
|
||||
if (!Device.LoadXci(ApplicationPath))
|
||||
if (!Device.LoadXci(ApplicationPath, ApplicationId))
|
||||
{
|
||||
Device.Dispose();
|
||||
|
||||
@@ -668,7 +669,7 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
||||
|
||||
if (!Device.LoadNsp(ApplicationPath))
|
||||
if (!Device.LoadNsp(ApplicationPath, ApplicationId))
|
||||
{
|
||||
Device.Dispose();
|
||||
|
||||
@@ -716,7 +717,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
|
||||
{
|
||||
appMetadata.LastPlayed = DateTime.UtcNow;
|
||||
appMetadata.UpdatePreGame();
|
||||
});
|
||||
|
||||
return true;
|
||||
|
@@ -14,7 +14,7 @@
|
||||
"MenuBarFileOpenEmuFolder": "Open Ryujinx Folder",
|
||||
"MenuBarFileOpenLogsFolder": "Open Logs Folder",
|
||||
"MenuBarFileExit": "_Exit",
|
||||
"MenuBarOptions": "Options",
|
||||
"MenuBarOptions": "_Options",
|
||||
"MenuBarOptionsToggleFullscreen": "Toggle Fullscreen",
|
||||
"MenuBarOptionsStartGamesInFullscreen": "Start Games in Fullscreen Mode",
|
||||
"MenuBarOptionsStopEmulation": "Stop Emulation",
|
||||
@@ -30,7 +30,7 @@
|
||||
"MenuBarToolsManageFileTypes": "Manage file types",
|
||||
"MenuBarToolsInstallFileTypes": "Install file types",
|
||||
"MenuBarToolsUninstallFileTypes": "Uninstall file types",
|
||||
"MenuBarHelp": "Help",
|
||||
"MenuBarHelp": "_Help",
|
||||
"MenuBarHelpCheckForUpdates": "Check for Updates",
|
||||
"MenuBarHelpAbout": "About",
|
||||
"MenuSearch": "Search...",
|
||||
@@ -539,6 +539,8 @@
|
||||
"OpenSetupGuideMessage": "Open the Setup Guide",
|
||||
"NoUpdate": "No Update",
|
||||
"TitleUpdateVersionLabel": "Version {0}",
|
||||
"TitleBundledUpdateVersionLabel": "Bundled: Version {0}",
|
||||
"TitleBundledDlcLabel": "Bundled:",
|
||||
"RyujinxInfo": "Ryujinx - Info",
|
||||
"RyujinxConfirm": "Ryujinx - Confirmation",
|
||||
"FileDialogAllTypes": "All types",
|
||||
|
@@ -18,7 +18,8 @@ using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
@@ -226,7 +227,11 @@ namespace Ryujinx.Ava.Common
|
||||
return;
|
||||
}
|
||||
|
||||
(Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
|
||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||
? IntegrityCheckLevel.ErrorOnInvalid
|
||||
: IntegrityCheckLevel.None;
|
||||
|
||||
(Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _);
|
||||
if (updatePatchNca != null)
|
||||
{
|
||||
patchNca = updatePatchNca;
|
||||
|
@@ -6,13 +6,13 @@ using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.GraphicsDriver;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.SystemInfo;
|
||||
using Ryujinx.Common.SystemInterop;
|
||||
using Ryujinx.Modules;
|
||||
using Ryujinx.SDL2.Common;
|
||||
using Ryujinx.Ui.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using Ryujinx.Ui.Common.SystemInfo;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
@@ -12,6 +12,11 @@
|
||||
Click="ToggleFavorite_Click"
|
||||
Header="{locale:Locale GameListContextMenuToggleFavorite}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
|
||||
<MenuItem
|
||||
Click="CreateApplicationShortcut_Click"
|
||||
Header="{locale:Locale GameListContextMenuCreateShortcut}"
|
||||
IsEnabled="{Binding CreateShortcutEnabled}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuCreateShortcutToolTip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Click="OpenUserSaveDirectory_Click"
|
||||
@@ -82,9 +87,4 @@
|
||||
Header="{locale:Locale GameListContextMenuExtractDataLogo}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
Click="CreateApplicationShortcut_Click"
|
||||
Header="{locale:Locale GameListContextMenuCreateShortcut}"
|
||||
IsEnabled="{Binding CreateShortcutEnabled}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuCreateShortcutToolTip}" />
|
||||
</MenuFlyout>
|
||||
|
@@ -1,7 +1,6 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Ava.Common;
|
||||
@@ -15,7 +14,6 @@ using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
@@ -41,7 +39,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite;
|
||||
|
||||
ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata =>
|
||||
ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata =>
|
||||
{
|
||||
appMetadata.Favorite = viewModel.SelectedApplication.Favorite;
|
||||
});
|
||||
@@ -76,19 +74,9 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
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]);
|
||||
});
|
||||
var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default);
|
||||
|
||||
ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName);
|
||||
ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
|
||||
await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +96,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
|
||||
await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,8 +108,8 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
await new CheatWindow(
|
||||
viewModel.VirtualFileSystem,
|
||||
viewModel.SelectedApplication.TitleId,
|
||||
viewModel.SelectedApplication.TitleName,
|
||||
viewModel.SelectedApplication.IdString,
|
||||
viewModel.SelectedApplication.Name,
|
||||
viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window);
|
||||
}
|
||||
}
|
||||
@@ -133,7 +121,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
string modsBasePath = ModLoader.GetModsBasePath();
|
||||
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.TitleId);
|
||||
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.IdString);
|
||||
|
||||
OpenHelper.OpenFolder(titleModsPath);
|
||||
}
|
||||
@@ -146,7 +134,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
||||
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.TitleId);
|
||||
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.IdString);
|
||||
|
||||
OpenHelper.OpenFolder(titleModsPath);
|
||||
}
|
||||
@@ -160,15 +148,15 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName),
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name),
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||
|
||||
if (result == UserResult.Yes)
|
||||
{
|
||||
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0"));
|
||||
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1"));
|
||||
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0"));
|
||||
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1"));
|
||||
|
||||
List<FileInfo> cacheFiles = new();
|
||||
|
||||
@@ -208,14 +196,14 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName),
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name),
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||
|
||||
if (result == UserResult.Yes)
|
||||
{
|
||||
DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader"));
|
||||
DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader"));
|
||||
|
||||
List<DirectoryInfo> oldCacheDirectories = new();
|
||||
List<FileInfo> newCacheFiles = new();
|
||||
@@ -263,7 +251,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu");
|
||||
string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu");
|
||||
string mainDir = Path.Combine(ptcDir, "0");
|
||||
string backupDir = Path.Combine(ptcDir, "1");
|
||||
|
||||
@@ -284,7 +272,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader");
|
||||
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader");
|
||||
|
||||
if (!Directory.Exists(shaderCacheDir))
|
||||
{
|
||||
@@ -305,7 +293,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
viewModel.StorageProvider,
|
||||
NcaSectionType.Code,
|
||||
viewModel.SelectedApplication.Path,
|
||||
viewModel.SelectedApplication.TitleName);
|
||||
viewModel.SelectedApplication.Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +307,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
viewModel.StorageProvider,
|
||||
NcaSectionType.Data,
|
||||
viewModel.SelectedApplication.Path,
|
||||
viewModel.SelectedApplication.TitleName);
|
||||
viewModel.SelectedApplication.Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +321,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
viewModel.StorageProvider,
|
||||
NcaSectionType.Logo,
|
||||
viewModel.SelectedApplication.Path,
|
||||
viewModel.SelectedApplication.TitleName);
|
||||
viewModel.SelectedApplication.Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +332,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
ApplicationData selectedApplication = viewModel.SelectedApplication;
|
||||
ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon);
|
||||
ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.Name, selectedApplication.IdString, selectedApplication.Icon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +342,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
|
||||
if (viewModel?.SelectedApplication != null)
|
||||
{
|
||||
await viewModel.LoadApplication(viewModel.SelectedApplication.Path);
|
||||
await viewModel.LoadApplication(viewModel.SelectedApplication);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -82,7 +82,7 @@
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding TitleName}"
|
||||
Text="{Binding Name}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</Panel>
|
||||
|
@@ -85,7 +85,7 @@
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
FontWeight="Bold"
|
||||
Text="{Binding TitleName}"
|
||||
Text="{Binding Name}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
@@ -109,7 +109,7 @@
|
||||
Spacing="5">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding TitleId}"
|
||||
Text="{Binding Id, StringFormat=X16}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
@@ -126,17 +126,17 @@
|
||||
Spacing="5">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding TimePlayed}"
|
||||
Text="{Binding TimePlayedString}"
|
||||
TextAlignment="Right"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding LastPlayed, Converter={helpers:NullableDateTimeConverter}}"
|
||||
Text="{Binding LastPlayedString, Converter={helpers:LocalizedNeverConverter}}"
|
||||
TextAlignment="Right"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding FileSize}"
|
||||
Text="{Binding FileSizeString}"
|
||||
TextAlignment="Right"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
43
src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs
Normal file
43
src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// This <see cref="IValueConverter"/> makes sure that the string "Never" that's returned by <see cref="ValueFormatUtils.FormatDateTime"/> is properly localized in the Avalonia UI.
|
||||
/// After the Avalonia UI has been made the default and the GTK UI is removed, <see cref="ValueFormatUtils"/> should be updated to directly return a localized string.
|
||||
/// </summary>
|
||||
internal class LocalizedNeverConverter : MarkupExtension, IValueConverter
|
||||
{
|
||||
private static readonly LocalizedNeverConverter _instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not string valStr)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (valStr == "Never")
|
||||
{
|
||||
return LocaleManager.Instance[LocaleKeys.Never];
|
||||
}
|
||||
|
||||
return valStr;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class NullableDateTimeConverter : MarkupExtension, IValueConverter
|
||||
{
|
||||
private static readonly NullableDateTimeConverter _instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return LocaleManager.Instance[LocaleKeys.Never];
|
||||
}
|
||||
|
||||
if (value is DateTime dateTime)
|
||||
{
|
||||
return dateTime.ToLocalTime().ToString(culture);
|
||||
}
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
@@ -24,6 +25,9 @@ namespace Ryujinx.Ava.UI.Models
|
||||
|
||||
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)
|
||||
{
|
||||
TitleId = titleId;
|
||||
|
@@ -13,20 +13,19 @@ namespace Ryujinx.Ava.UI.Models.Generic
|
||||
|
||||
public int Compare(ApplicationData x, ApplicationData y)
|
||||
{
|
||||
var aValue = x.LastPlayed;
|
||||
var bValue = y.LastPlayed;
|
||||
DateTime aValue = DateTime.UnixEpoch, bValue = DateTime.UnixEpoch;
|
||||
|
||||
if (!aValue.HasValue)
|
||||
if (x?.LastPlayed != null)
|
||||
{
|
||||
aValue = DateTime.UnixEpoch;
|
||||
aValue = x.LastPlayed.Value;
|
||||
}
|
||||
|
||||
if (!bValue.HasValue)
|
||||
if (y?.LastPlayed != null)
|
||||
{
|
||||
bValue = DateTime.UnixEpoch;
|
||||
bValue = y.LastPlayed.Value;
|
||||
}
|
||||
|
||||
return (IsAscending ? 1 : -1) * DateTime.Compare(bValue.Value, aValue.Value);
|
||||
return (IsAscending ? 1 : -1) * DateTime.Compare(aValue, bValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
31
src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs
Normal file
31
src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models.Generic
|
||||
{
|
||||
internal class TimePlayedSortComparer : IComparer<ApplicationData>
|
||||
{
|
||||
public TimePlayedSortComparer() { }
|
||||
public TimePlayedSortComparer(bool isAscending) { IsAscending = isAscending; }
|
||||
|
||||
public bool IsAscending { get; }
|
||||
|
||||
public int Compare(ApplicationData x, ApplicationData y)
|
||||
{
|
||||
TimeSpan aValue = TimeSpan.Zero, bValue = TimeSpan.Zero;
|
||||
|
||||
if (x?.TimePlayed != null)
|
||||
{
|
||||
aValue = x.TimePlayed;
|
||||
}
|
||||
|
||||
if (y?.TimePlayed != null)
|
||||
{
|
||||
bValue = y.TimePlayed;
|
||||
}
|
||||
|
||||
return (IsAscending ? 1 : -1) * TimeSpan.Compare(aValue, bValue);
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@ using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using System;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -38,26 +38,7 @@ namespace Ryujinx.Ava.UI.Models
|
||||
|
||||
public bool SizeAvailable { get; set; }
|
||||
|
||||
public string SizeString => GetSizeString();
|
||||
|
||||
private string GetSizeString()
|
||||
{
|
||||
const int Scale = 1024;
|
||||
string[] orders = { "GiB", "MiB", "KiB" };
|
||||
long max = (long)Math.Pow(Scale, orders.Length);
|
||||
|
||||
foreach (string order in orders)
|
||||
{
|
||||
if (Size > max)
|
||||
{
|
||||
return $"{decimal.Divide(Size, max):##.##} {order}";
|
||||
}
|
||||
|
||||
max /= Scale;
|
||||
}
|
||||
|
||||
return "0 KiB";
|
||||
}
|
||||
public string SizeString => ValueFormatUtils.FormatFileSize(Size);
|
||||
|
||||
public SaveModel(SaveDataInfo info)
|
||||
{
|
||||
@@ -65,14 +46,14 @@ namespace Ryujinx.Ava.UI.Models
|
||||
TitleId = info.ProgramId;
|
||||
UserId = info.UserId;
|
||||
|
||||
var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString);
|
||||
var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.ToUpper() == TitleIdString);
|
||||
|
||||
InGameList = appData != null;
|
||||
|
||||
if (InGameList)
|
||||
{
|
||||
Icon = appData.Icon;
|
||||
Title = appData.TitleName;
|
||||
Title = appData.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@@ -8,7 +8,10 @@ namespace Ryujinx.Ava.UI.Models
|
||||
public ApplicationControlProperty Control { get; }
|
||||
public string Path { get; }
|
||||
|
||||
public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleUpdateVersionLabel, Control.DisplayVersionString.ToString());
|
||||
public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
System.IO.Path.GetExtension(Path)?.ToLower() == ".xci" ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel,
|
||||
Control.DisplayVersionString.ToString()
|
||||
);
|
||||
|
||||
public TitleUpdateModel(ApplicationControlProperty control, string path)
|
||||
{
|
||||
|
@@ -17,11 +17,12 @@ using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Application = Avalonia.Application;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
@@ -38,7 +39,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
|
||||
|
||||
private string _search;
|
||||
private readonly ulong _titleId;
|
||||
private readonly ApplicationData _applicationData;
|
||||
|
||||
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
@@ -92,18 +93,25 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public IStorageProvider StorageProvider;
|
||||
|
||||
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
|
||||
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||
{
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
|
||||
_titleId = titleId;
|
||||
_applicationData = applicationData;
|
||||
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
StorageProvider = desktop.MainWindow.StorageProvider;
|
||||
}
|
||||
|
||||
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
||||
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "dlc.json");
|
||||
|
||||
if (!File.Exists(_downloadableContentJsonPath))
|
||||
{
|
||||
_downloadableContentContainerList = new List<DownloadableContentContainer>();
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -120,6 +128,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private void LoadDownloadableContents()
|
||||
{
|
||||
// NOTE: Try to load downloadable contents from PFS first.
|
||||
AddDownloadableContent(_applicationData.Path);
|
||||
|
||||
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
|
||||
{
|
||||
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||
@@ -127,7 +138,11 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
||||
|
||||
PartitionFileSystem partitionFileSystem = new();
|
||||
partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
||||
|
||||
if (partitionFileSystem.Initialize(containerFile.AsStorage()).IsFailure())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
@@ -220,22 +235,34 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
foreach (var file in result)
|
||||
{
|
||||
await AddDownloadableContent(file.Path.LocalPath);
|
||||
if (!AddDownloadableContent(file.Path.LocalPath))
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddDownloadableContent(string path)
|
||||
private bool AddDownloadableContent(string path)
|
||||
{
|
||||
if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
||||
{
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
using FileStream containerFile = File.OpenRead(path);
|
||||
|
||||
PartitionFileSystem partitionFileSystem = new();
|
||||
partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
||||
bool containsDownloadableContent = false;
|
||||
IFileSystem partitionFileSystem;
|
||||
|
||||
if (Path.GetExtension(path).ToLower() == ".xci")
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -253,7 +280,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||
{
|
||||
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
|
||||
if (nca.GetProgramIdBase() != _applicationData.IdBase)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -265,14 +292,11 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
OnPropertyChanged(nameof(UpdateCount));
|
||||
Sort();
|
||||
|
||||
containsDownloadableContent = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!containsDownloadableContent)
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Remove(DownloadableContentModel model)
|
||||
|
@@ -95,7 +95,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
private bool _canUpdate = true;
|
||||
private Cursor _cursor;
|
||||
private string _title;
|
||||
private string _currentEmulatedGamePath;
|
||||
private ApplicationData _currentApplicationData;
|
||||
private readonly AutoResetEvent _rendererWaitEvent;
|
||||
private WindowState _windowState;
|
||||
private double _windowWidth;
|
||||
@@ -106,7 +106,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
public ApplicationData ListSelectedApplication;
|
||||
public ApplicationData GridSelectedApplication;
|
||||
|
||||
private string TitleName { get; set; }
|
||||
internal AppHost AppHost { get; set; }
|
||||
|
||||
public MainWindowViewModel()
|
||||
@@ -930,21 +929,20 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
return SortMode switch
|
||||
{
|
||||
#pragma warning disable IDE0055 // Disable formatting
|
||||
ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Name)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Name),
|
||||
ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Developer),
|
||||
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
|
||||
ApplicationSort.FileSize => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileSizeBytes)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.FileSizeBytes),
|
||||
ApplicationSort.TotalTimePlayed => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TimePlayedNum)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.TimePlayedNum),
|
||||
ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TitleName)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.TitleName),
|
||||
ApplicationSort.TotalTimePlayed => new TimePlayedSortComparer(IsAscending),
|
||||
ApplicationSort.FileType => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileExtension)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.FileExtension),
|
||||
ApplicationSort.FileSize => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileSize)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.FileSize),
|
||||
ApplicationSort.Path => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Path)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Path),
|
||||
ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Favorite)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Favorite),
|
||||
ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Developer),
|
||||
ApplicationSort.FileType => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileExtension)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.FileExtension),
|
||||
ApplicationSort.Path => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Path)
|
||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Path),
|
||||
_ => null,
|
||||
#pragma warning restore IDE0055
|
||||
};
|
||||
@@ -969,7 +967,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
if (arg is ApplicationData app)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(_searchText) || app.TitleName.ToLower().Contains(_searchText.ToLower());
|
||||
return string.IsNullOrWhiteSpace(_searchText) || app.Name.ToLower().Contains(_searchText.ToLower());
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -1098,7 +1096,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
IsLoadingIndeterminate = false;
|
||||
break;
|
||||
case LoadState.Loaded:
|
||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
|
||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
|
||||
IsLoadingIndeterminate = true;
|
||||
CacheLoadStatus = "";
|
||||
break;
|
||||
@@ -1118,7 +1116,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
IsLoadingIndeterminate = false;
|
||||
break;
|
||||
case ShaderCacheLoadingState.Loaded:
|
||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
|
||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
|
||||
IsLoadingIndeterminate = true;
|
||||
CacheLoadStatus = "";
|
||||
break;
|
||||
@@ -1169,13 +1167,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
UserChannelPersistence.ShouldRestart = false;
|
||||
|
||||
await LoadApplication(_currentEmulatedGamePath);
|
||||
await LoadApplication(_currentApplicationData);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, clear state.
|
||||
UserChannelPersistence = new UserChannelPersistence();
|
||||
_currentEmulatedGamePath = null;
|
||||
_currentApplicationData = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1452,7 +1450,12 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
if (result.Count > 0)
|
||||
{
|
||||
await LoadApplication(result[0].Path.LocalPath);
|
||||
ApplicationData applicationData = new()
|
||||
{
|
||||
Path = result[0].Path.LocalPath,
|
||||
};
|
||||
|
||||
await LoadApplication(applicationData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1466,11 +1469,17 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
if (result.Count > 0)
|
||||
{
|
||||
await LoadApplication(result[0].Path.LocalPath);
|
||||
ApplicationData applicationData = new()
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(result[0].Path.LocalPath),
|
||||
Path = result[0].Path.LocalPath,
|
||||
};
|
||||
|
||||
await LoadApplication(applicationData);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadApplication(string path, bool startFullscreen = false, string titleName = "")
|
||||
public async Task LoadApplication(ApplicationData application, bool startFullscreen = false)
|
||||
{
|
||||
if (AppHost != null)
|
||||
{
|
||||
@@ -1490,7 +1499,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
Logger.RestartTime();
|
||||
|
||||
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language);
|
||||
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id);
|
||||
|
||||
PrepareLoadScreen();
|
||||
|
||||
@@ -1499,7 +1508,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
AppHost = new AppHost(
|
||||
RendererHostControl,
|
||||
InputManager,
|
||||
path,
|
||||
application.Path,
|
||||
application.Id,
|
||||
VirtualFileSystem,
|
||||
ContentManager,
|
||||
AccountManager,
|
||||
@@ -1517,17 +1527,17 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
CanUpdate = false;
|
||||
|
||||
LoadHeading = TitleName = titleName;
|
||||
LoadHeading = application.Name;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(titleName))
|
||||
if (string.IsNullOrWhiteSpace(application.Name))
|
||||
{
|
||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name);
|
||||
TitleName = AppHost.Device.Processes.ActiveApplication.Name;
|
||||
application.Name = AppHost.Device.Processes.ActiveApplication.Name;
|
||||
}
|
||||
|
||||
SwitchToRenderer(startFullscreen);
|
||||
|
||||
_currentEmulatedGamePath = path;
|
||||
_currentApplicationData = application;
|
||||
|
||||
Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
|
||||
gameThread.Start();
|
||||
@@ -1549,13 +1559,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
|
||||
{
|
||||
if (appMetadata.LastPlayed.HasValue)
|
||||
{
|
||||
double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds;
|
||||
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
appMetadata.LastPlayed = DateTime.UtcNow;
|
||||
appMetadata.UpdatePostGame();
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Platform.Storage;
|
||||
@@ -8,6 +7,7 @@ using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Ns;
|
||||
using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
@@ -17,12 +17,16 @@ using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Application = Avalonia.Application;
|
||||
using ContentType = LibHac.Ncm.ContentType;
|
||||
using Path = System.IO.Path;
|
||||
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||
|
||||
@@ -33,7 +37,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
public TitleUpdateMetadata TitleUpdateWindowData;
|
||||
public readonly string TitleUpdateJsonPath;
|
||||
private VirtualFileSystem VirtualFileSystem { get; }
|
||||
private ulong TitleId { get; }
|
||||
private ApplicationData ApplicationData { get; }
|
||||
|
||||
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
|
||||
private AvaloniaList<object> _views = new();
|
||||
@@ -73,18 +77,18 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public IStorageProvider StorageProvider;
|
||||
|
||||
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
|
||||
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||
{
|
||||
VirtualFileSystem = virtualFileSystem;
|
||||
|
||||
TitleId = titleId;
|
||||
ApplicationData = applicationData;
|
||||
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
StorageProvider = desktop.MainWindow.StorageProvider;
|
||||
}
|
||||
|
||||
TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
|
||||
TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdString, "updates.json");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -92,7 +96,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {TitleId} at {TitleUpdateJsonPath}");
|
||||
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {ApplicationData.IdString} at {TitleUpdateJsonPath}");
|
||||
|
||||
TitleUpdateWindowData = new TitleUpdateMetadata
|
||||
{
|
||||
@@ -108,6 +112,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private void LoadUpdates()
|
||||
{
|
||||
// Try to load updates from PFS first
|
||||
AddUpdate(ApplicationData.Path, true);
|
||||
|
||||
foreach (string path in TitleUpdateWindowData.Paths)
|
||||
{
|
||||
AddUpdate(path);
|
||||
@@ -162,17 +169,41 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void AddUpdate(string path)
|
||||
private void AddUpdate(string path, bool ignoreNotFound = false)
|
||||
{
|
||||
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);
|
||||
|
||||
IFileSystem pfs;
|
||||
|
||||
try
|
||||
{
|
||||
var pfs = new PartitionFileSystem();
|
||||
pfs.Initialize(file.AsStorage()).ThrowIfFailure();
|
||||
(Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0);
|
||||
if (Path.GetExtension(path).ToLower() == ".xci")
|
||||
{
|
||||
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(ApplicationData.Id, out ContentCollection content))
|
||||
{
|
||||
patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program);
|
||||
controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control);
|
||||
}
|
||||
|
||||
if (controlNca != null && patchNca != null)
|
||||
{
|
||||
@@ -187,7 +218,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
|
||||
if (!ignoreNotFound)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@@ -10,6 +10,7 @@ using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Modules;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
@@ -131,7 +132,14 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
|
||||
if (!string.IsNullOrEmpty(contentPath))
|
||||
{
|
||||
await ViewModel.LoadApplication(contentPath, false, "Mii Applet");
|
||||
ApplicationData applicationData = new()
|
||||
{
|
||||
Name = "miiEdit",
|
||||
Id = 0x0100000000001009,
|
||||
Path = contentPath,
|
||||
};
|
||||
|
||||
await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -104,7 +104,7 @@
|
||||
Content="{locale:Locale GameListHeaderApplication}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
|
||||
Tag="Title" />
|
||||
Tag="Application" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderDeveloper}"
|
||||
|
@@ -1,9 +1,11 @@
|
||||
using Avalonia.Collections;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
@@ -34,9 +36,12 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath)
|
||||
{
|
||||
LoadedCheats = new AvaloniaList<CheatsList>();
|
||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||
? IntegrityCheckLevel.ErrorOnInvalid
|
||||
: IntegrityCheckLevel.None;
|
||||
|
||||
Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper());
|
||||
BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath);
|
||||
BuildId = ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
|
@@ -97,7 +97,7 @@
|
||||
MaxLines="2"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Text="{Binding FileName}" />
|
||||
Text="{Binding Label}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="10 0"
|
||||
|
@@ -7,9 +7,9 @@ using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System.Threading.Tasks;
|
||||
using Button = Avalonia.Controls.Button;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
@@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId)
|
||||
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||
{
|
||||
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, titleId);
|
||||
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, applicationData);
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||
{
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
PrimaryButtonText = "",
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = "",
|
||||
Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId),
|
||||
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16")),
|
||||
Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationData),
|
||||
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdString),
|
||||
};
|
||||
|
||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||
|
@@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Ryujinx.Ava.Common;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Input;
|
||||
@@ -23,7 +24,6 @@ using Ryujinx.Ui.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -139,9 +139,7 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
ViewModel.SelectedIcon = args.Application.Icon;
|
||||
|
||||
string path = new FileInfo(args.Application.Path).FullName;
|
||||
|
||||
ViewModel.LoadApplication(path).Wait();
|
||||
ViewModel.LoadApplication(args.Application).Wait();
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
@@ -190,7 +188,11 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
LibHacHorizonManager.InitializeBcatServer();
|
||||
LibHacHorizonManager.InitializeSystemClients();
|
||||
|
||||
ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem);
|
||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||
? 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
|
||||
// given empty extra data. Luckily some of that extra data can be created using the data from the
|
||||
@@ -297,7 +299,12 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
_deferLoad = false;
|
||||
|
||||
ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait();
|
||||
ApplicationData applicationData = new()
|
||||
{
|
||||
Path = _launchPath,
|
||||
};
|
||||
|
||||
ViewModel.LoadApplication(applicationData, _startFullscreen).Wait();
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@@ -7,15 +7,15 @@ using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System.Threading.Tasks;
|
||||
using Button = Avalonia.Controls.Button;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class TitleUpdateWindow : UserControl
|
||||
{
|
||||
public TitleUpdateViewModel ViewModel;
|
||||
public readonly TitleUpdateViewModel ViewModel;
|
||||
|
||||
public TitleUpdateWindow()
|
||||
{
|
||||
@@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId)
|
||||
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||
{
|
||||
DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId);
|
||||
DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationData);
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||
{
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
PrimaryButtonText = "",
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = "",
|
||||
Content = new TitleUpdateWindow(virtualFileSystem, titleId),
|
||||
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, titleName, titleId.ToString("X16")),
|
||||
Content = new TitleUpdateWindow(virtualFileSystem, applicationData),
|
||||
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdString),
|
||||
};
|
||||
|
||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||
|
62
src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs
Normal file
62
src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
|
||||
namespace Ryujinx.Cpu.AppleHv
|
||||
{
|
||||
static class HvCodePatcher
|
||||
{
|
||||
private const uint XMask = 0x3f808000u;
|
||||
private const uint XValue = 0x8000000u;
|
||||
|
||||
private const uint ZrIndex = 31u;
|
||||
|
||||
public static void RewriteUnorderedExclusiveInstructions(Span<byte> code)
|
||||
{
|
||||
Span<uint> codeUint = MemoryMarshal.Cast<byte, uint>(code);
|
||||
Span<Vector128<uint>> codeVector = MemoryMarshal.Cast<byte, Vector128<uint>>(code);
|
||||
|
||||
Vector128<uint> mask = Vector128.Create(XMask);
|
||||
Vector128<uint> value = Vector128.Create(XValue);
|
||||
|
||||
for (int index = 0; index < codeVector.Length; index++)
|
||||
{
|
||||
Vector128<uint> v = codeVector[index];
|
||||
|
||||
if (Vector128.EqualsAny(Vector128.BitwiseAnd(v, mask), value))
|
||||
{
|
||||
int baseIndex = index * 4;
|
||||
|
||||
for (int instIndex = baseIndex; instIndex < baseIndex + 4; instIndex++)
|
||||
{
|
||||
ref uint inst = ref codeUint[instIndex];
|
||||
|
||||
if ((inst & XMask) != XValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool isPair = (inst & (1u << 21)) != 0;
|
||||
bool isLoad = (inst & (1u << 22)) != 0;
|
||||
|
||||
uint rt2 = (inst >> 10) & 0x1fu;
|
||||
uint rs = (inst >> 16) & 0x1fu;
|
||||
|
||||
if (isLoad && rs != ZrIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isPair && rt2 != ZrIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set the ordered flag.
|
||||
inst |= 1u << 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -128,21 +128,6 @@ namespace Ryujinx.Cpu.AppleHv
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable IDE0051 // Remove unused private member
|
||||
/// <summary>
|
||||
/// Ensures the combination of virtual address and size is part of the addressable space and fully mapped.
|
||||
/// </summary>
|
||||
/// <param name="va">Virtual address of the range</param>
|
||||
/// <param name="size">Size of the range in bytes</param>
|
||||
private void AssertMapped(ulong va, ulong size)
|
||||
{
|
||||
if (!ValidateAddressAndSize(va, size) || !IsRangeMappedImpl(va, size))
|
||||
{
|
||||
throw new InvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
|
||||
}
|
||||
}
|
||||
#pragma warning restore IDE0051
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
|
||||
{
|
||||
@@ -736,6 +721,24 @@ namespace Ryujinx.Cpu.AppleHv
|
||||
return (int)(vaSpan / PageSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
if (protection.HasFlag(MemoryPermission.Execute))
|
||||
{
|
||||
// Some applications use unordered exclusive memory access instructions
|
||||
// where it is not valid to do so, leading to memory re-ordering that
|
||||
// makes the code behave incorrectly on some CPUs.
|
||||
// To work around this, we force all such accesses to be ordered.
|
||||
|
||||
using WritableRegion writableRegion = GetWritableRegion(va, (int)size);
|
||||
|
||||
HvCodePatcher.RewriteUnorderedExclusiveInstructions(writableRegion.Memory.Span);
|
||||
}
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
|
@@ -575,24 +575,17 @@ namespace Ryujinx.Cpu.Jit
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable IDE0051 // Remove unused private member
|
||||
private ulong GetPhysicalAddress(ulong va)
|
||||
{
|
||||
// We return -1L if the virtual address is invalid or unmapped.
|
||||
if (!ValidateAddress(va) || !IsMapped(va))
|
||||
{
|
||||
return ulong.MaxValue;
|
||||
}
|
||||
|
||||
return GetPhysicalAddressInternal(va);
|
||||
}
|
||||
#pragma warning restore IDE0051
|
||||
|
||||
private ulong GetPhysicalAddressInternal(ulong va)
|
||||
{
|
||||
return PteToPa(_pageTable.Read<ulong>((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
@@ -698,9 +691,5 @@ namespace Ryujinx.Cpu.Jit
|
||||
/// Disposes of resources used by the memory manager.
|
||||
/// </summary>
|
||||
protected override void Destroy() => _pageTable.Dispose();
|
||||
|
||||
#pragma warning disable IDE0051 // Remove unused private member
|
||||
private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message);
|
||||
#pragma warning restore IDE0051
|
||||
}
|
||||
}
|
||||
|
@@ -615,6 +615,12 @@ namespace Ryujinx.Cpu.Jit
|
||||
return (int)(vaSpan / PageSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
|
61
src/Ryujinx.HLE/FileSystem/ContentCollection.cs
Normal file
61
src/Ryujinx.HLE/FileSystem/ContentCollection.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
46
src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs
Normal file
46
src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||
{
|
||||
[Flags]
|
||||
enum KMemoryPermission : uint
|
||||
{
|
||||
None = 0,
|
||||
UserMask = Read | Write | Execute,
|
||||
Mask = uint.MaxValue,
|
||||
|
||||
Read = 1 << 0,
|
||||
Write = 1 << 1,
|
||||
Execute = 1 << 2,
|
||||
DontCare = 1 << 28,
|
||||
|
||||
ReadAndWrite = Read | Write,
|
||||
ReadAndExecute = Read | Execute,
|
||||
}
|
||||
|
||||
static class KMemoryPermissionExtensions
|
||||
{
|
||||
public static MemoryPermission Convert(this KMemoryPermission permission)
|
||||
{
|
||||
MemoryPermission output = MemoryPermission.None;
|
||||
|
||||
if (permission.HasFlag(KMemoryPermission.Read))
|
||||
{
|
||||
output = MemoryPermission.Read;
|
||||
}
|
||||
|
||||
if (permission.HasFlag(KMemoryPermission.Write))
|
||||
{
|
||||
output |= MemoryPermission.Write;
|
||||
}
|
||||
|
||||
if (permission.HasFlag(KMemoryPermission.Execute))
|
||||
{
|
||||
output |= MemoryPermission.Execute;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
}
|
@@ -203,15 +203,17 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||
/// <inheritdoc/>
|
||||
protected override Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission)
|
||||
{
|
||||
// TODO.
|
||||
_cpuMemory.Reprotect(address, pagesCount * PageSize, permission.Convert());
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission)
|
||||
protected override Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission)
|
||||
{
|
||||
// TODO.
|
||||
return Result.Success;
|
||||
// TODO: Flush JIT cache.
|
||||
|
||||
return Reprotect(address, pagesCount, permission);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@@ -1255,7 +1255,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||
|
||||
if ((oldPermission & KMemoryPermission.Execute) != 0)
|
||||
{
|
||||
result = ReprotectWithAttributes(address, pagesCount, permission);
|
||||
result = ReprotectAndFlush(address, pagesCount, permission);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -3036,13 +3036,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||
protected abstract Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission);
|
||||
|
||||
/// <summary>
|
||||
/// Changes the permissions of a given virtual memory region.
|
||||
/// Changes the permissions of a given virtual memory region, while also flushing the cache.
|
||||
/// </summary>
|
||||
/// <param name="address">Virtual address of the region to have the permission changes</param>
|
||||
/// <param name="pagesCount">Number of pages to have their permissions changed</param>
|
||||
/// <param name="permission">New permission</param>
|
||||
/// <returns>Result of the permission change operation</returns>
|
||||
protected abstract Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission);
|
||||
protected abstract Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission);
|
||||
|
||||
/// <summary>
|
||||
/// Alerts the memory tracking that a given region has been read from or written to.
|
||||
|
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||
{
|
||||
[Flags]
|
||||
enum KMemoryPermission : uint
|
||||
{
|
||||
None = 0,
|
||||
UserMask = Read | Write | Execute,
|
||||
Mask = uint.MaxValue,
|
||||
|
||||
Read = 1 << 0,
|
||||
Write = 1 << 1,
|
||||
Execute = 1 << 2,
|
||||
DontCare = 1 << 28,
|
||||
|
||||
ReadAndWrite = Read | Write,
|
||||
ReadAndExecute = Read | Execute,
|
||||
}
|
||||
}
|
@@ -2,21 +2,31 @@
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Loader;
|
||||
using LibHac.Ncm;
|
||||
using LibHac.Ns;
|
||||
using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using LibHac.Tools.Ncm;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ApplicationId = LibHac.Ncm.ApplicationId;
|
||||
using ContentType = LibHac.Ncm.ContentType;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
{
|
||||
static class NcaExtensions
|
||||
public static class NcaExtensions
|
||||
{
|
||||
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
|
||||
{
|
||||
// Extract RomFs and ExeFs from NCA.
|
||||
@@ -47,7 +57,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
nacpData = controlNca.GetNacp(device);
|
||||
}
|
||||
|
||||
/* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" inexistant update.
|
||||
/* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update.
|
||||
|
||||
// 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 _);
|
||||
@@ -86,6 +96,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
return processResult;
|
||||
}
|
||||
|
||||
public static ulong GetProgramIdBase(this Nca nca)
|
||||
{
|
||||
return nca.Header.TitleId & ~0x1FFFUL;
|
||||
}
|
||||
|
||||
public static int GetProgramIndex(this Nca nca)
|
||||
{
|
||||
return (int)(nca.Header.TitleId & 0xF);
|
||||
@@ -96,6 +111,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
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)
|
||||
{
|
||||
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
||||
@@ -108,6 +128,56 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
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)
|
||||
{
|
||||
IFileSystem exeFs = null;
|
||||
@@ -172,5 +242,31 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
|
||||
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,26 +1,87 @@
|
||||
using LibHac.Common;
|
||||
using LibHac.Common.Keys;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Ncm;
|
||||
using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using LibHac.Tools.Ncm;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using ContentType = LibHac.Ncm.ContentType;
|
||||
|
||||
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
{
|
||||
public static class PartitionFileSystemExtensions
|
||||
{
|
||||
private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
internal static (bool, ProcessResult) TryLoad<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> partitionFileSystem, Switch device, string path, out string errorMessage)
|
||||
public static Dictionary<ulong, ContentCollection> GetApplicationData(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.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 TFormat : IPartitionFileSystemFormat
|
||||
where THeader : unmanaged, IPartitionFileSystemHeader
|
||||
@@ -35,31 +96,22 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
|
||||
try
|
||||
{
|
||||
device.Configuration.VirtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
Dictionary<ulong, ContentCollection> applications = partitionFileSystem.GetApplicationData(device.FileSystem, device.System.FsIntegrityCheckLevel);
|
||||
|
||||
// TODO: To support multi-games container, this should use CNMT NCA instead.
|
||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||
if (titleId == 0)
|
||||
{
|
||||
Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
|
||||
|
||||
if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
|
||||
foreach ((ulong _, ContentCollection content) in applications)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nca.IsPatch())
|
||||
{
|
||||
patchNca = nca;
|
||||
}
|
||||
else if (nca.IsProgram())
|
||||
{
|
||||
mainNca = nca;
|
||||
}
|
||||
else if (nca.IsControl())
|
||||
{
|
||||
controlNca = nca;
|
||||
mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index);
|
||||
controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
@@ -79,54 +131,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
return (false, ProcessResult.Failed);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _);
|
||||
|
||||
if (updatePatchNca != null)
|
||||
{
|
||||
@@ -168,18 +173,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
return (true, mainNca.Load(device, patchNca, controlNca));
|
||||
}
|
||||
|
||||
errorMessage = "Unable to load: Could not find Main NCA";
|
||||
errorMessage = $"Unable to load: Could not find Main NCA for title \"{titleId:X16}\"";
|
||||
|
||||
return (false, ProcessResult.Failed);
|
||||
}
|
||||
|
||||
public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path)
|
||||
public static Nca GetNca(this IFileSystem fileSystem, KeySet keySet, string path)
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
return new Nca(device.Configuration.VirtualFileSystem.KeySet, ncaFile.Release().AsStorage());
|
||||
return new Nca(keySet, ncaFile.Release().AsStorage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -32,7 +32,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
_processesByPid = new ConcurrentDictionary<ulong, ProcessResult>();
|
||||
}
|
||||
|
||||
public bool LoadXci(string path)
|
||||
public bool LoadXci(string path, ulong titleId)
|
||||
{
|
||||
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
|
||||
Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
|
||||
@@ -44,7 +44,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
return false;
|
||||
}
|
||||
|
||||
(bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, out string errorMessage);
|
||||
(bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, titleId, out string errorMessage);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@@ -66,13 +66,13 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool LoadNsp(string path)
|
||||
public bool LoadNsp(string path, ulong titleId)
|
||||
{
|
||||
FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
||||
PartitionFileSystem partitionFileSystem = new();
|
||||
partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure();
|
||||
|
||||
(bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage);
|
||||
(bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, titleId, out string errorMessage);
|
||||
|
||||
if (processResult.ProcessId == 0)
|
||||
{
|
||||
|
@@ -42,15 +42,14 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
|
||||
Nca nca = partitionFileSystem.GetNca(device.FileSystem.KeySet, fileEntry.FullPath);
|
||||
|
||||
if (!nca.IsProgram() && nca.IsPatch())
|
||||
if (!nca.IsProgram())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ulong currentProgramId = nca.Header.TitleId;
|
||||
ulong currentMainProgramId = currentProgramId & ~0xFFFul;
|
||||
ulong currentMainProgramId = nca.GetProgramIdBase();
|
||||
|
||||
if (applicationId == 0 && currentMainProgramId != 0)
|
||||
{
|
||||
@@ -67,7 +66,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
break;
|
||||
}
|
||||
|
||||
hasIndex[(int)(currentProgramId & 0xF)] = true;
|
||||
hasIndex[nca.GetProgramIndex()] = true;
|
||||
}
|
||||
|
||||
if (programCount == 0)
|
||||
|
@@ -72,9 +72,9 @@ namespace Ryujinx.HLE
|
||||
return Processes.LoadUnpackedNca(exeFsDir, romFsFile);
|
||||
}
|
||||
|
||||
public bool LoadXci(string xciFile)
|
||||
public bool LoadXci(string xciFile, ulong titleId = 0)
|
||||
{
|
||||
return Processes.LoadXci(xciFile);
|
||||
return Processes.LoadXci(xciFile, titleId);
|
||||
}
|
||||
|
||||
public bool LoadNca(string ncaFile)
|
||||
@@ -82,9 +82,9 @@ namespace Ryujinx.HLE
|
||||
return Processes.LoadNca(ncaFile);
|
||||
}
|
||||
|
||||
public bool LoadNsp(string nspFile)
|
||||
public bool LoadNsp(string nspFile, ulong titleId = 0)
|
||||
{
|
||||
return Processes.LoadNsp(nspFile);
|
||||
return Processes.LoadNsp(nspFile, titleId);
|
||||
}
|
||||
|
||||
public bool LoadProgram(string fileName)
|
||||
|
@@ -455,6 +455,11 @@ namespace Ryujinx.Memory
|
||||
return _pageTable.Read(va) + (nuint)(va & PageMask);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
|
@@ -104,6 +104,12 @@ namespace Ryujinx.Memory
|
||||
/// <returns>True if the data was changed, false otherwise</returns>
|
||||
bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data);
|
||||
|
||||
/// <summary>
|
||||
/// Fills the specified memory region with the value specified in <paramref name="value"/>.
|
||||
/// </summary>
|
||||
/// <param name="va">Virtual address to fill the value into</param>
|
||||
/// <param name="size">Size of the memory region to fill</param>
|
||||
/// <param name="value">Value to fill with</param>
|
||||
void Fill(ulong va, ulong size, byte value)
|
||||
{
|
||||
const int MaxChunkSize = 1 << 24;
|
||||
@@ -194,6 +200,14 @@ namespace Ryujinx.Memory
|
||||
/// <param name="exemptId">Optional ID of the handles that should not be signalled</param>
|
||||
void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Reprotect a region of virtual memory for guest access.
|
||||
/// </summary>
|
||||
/// <param name="va">Virtual address base</param>
|
||||
/// <param name="size">Size of the region to protect</param>
|
||||
/// <param name="protection">Memory protection to set</param>
|
||||
void Reprotect(ulong va, ulong size, MemoryPermission protection);
|
||||
|
||||
/// <summary>
|
||||
/// Reprotect a region of virtual memory for tracking.
|
||||
/// </summary>
|
||||
|
@@ -102,6 +102,11 @@ namespace Ryujinx.Tests.Memory
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||
{
|
||||
OnProtect?.Invoke(va, size, protection);
|
||||
|
@@ -9,8 +9,9 @@ using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -20,35 +21,28 @@ namespace Ryujinx.Ui.App.Common
|
||||
{
|
||||
public bool Favorite { get; set; }
|
||||
public byte[] Icon { get; set; }
|
||||
public string TitleName { get; set; }
|
||||
public string TitleId { get; set; }
|
||||
public string Developer { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string TimePlayed { get; set; }
|
||||
public double TimePlayedNum { get; set; }
|
||||
public string Name { get; set; } = "Unknown";
|
||||
public ulong Id { get; set; }
|
||||
public string Developer { get; set; } = "Unknown";
|
||||
public string Version { get; set; } = "0";
|
||||
public TimeSpan TimePlayed { get; set; }
|
||||
public DateTime? LastPlayed { get; set; }
|
||||
public string FileExtension { get; set; }
|
||||
public string FileSize { get; set; }
|
||||
public double FileSizeBytes { get; set; }
|
||||
public long FileSize { get; set; }
|
||||
public string Path { get; set; }
|
||||
public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string LastPlayedString
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!LastPlayed.HasValue)
|
||||
{
|
||||
// TODO: maybe put localized string here instead of just "Never"
|
||||
return "Never";
|
||||
}
|
||||
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
|
||||
|
||||
return LastPlayed.Value.ToLocalTime().ToString(CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed);
|
||||
|
||||
public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath)
|
||||
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
|
||||
|
||||
[JsonIgnore] public string IdString => Id.ToString("x16");
|
||||
|
||||
[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);
|
||||
|
||||
@@ -117,7 +111,7 @@ namespace Ryujinx.Ui.App.Common
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
(Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
|
||||
(Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _);
|
||||
|
||||
if (updatePatchNca != null)
|
||||
{
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -7,13 +7,45 @@ namespace Ryujinx.Ui.App.Common
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public double TimePlayed { get; set; }
|
||||
|
||||
[JsonPropertyName("timespan_played")]
|
||||
public TimeSpan TimePlayed { get; set; } = TimeSpan.Zero;
|
||||
|
||||
[JsonPropertyName("last_played_utc")]
|
||||
public DateTime? LastPlayed { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("time_played")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public double TimePlayedOld { get; set; }
|
||||
|
||||
[JsonPropertyName("last_played")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public string LastPlayedOld { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="LastPlayed"/>. Call this before launching a game.
|
||||
/// </summary>
|
||||
public void UpdatePreGame()
|
||||
{
|
||||
LastPlayed = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="LastPlayed"/> and <see cref="TimePlayed"/>. Call this after a game ends.
|
||||
/// </summary>
|
||||
public void UpdatePostGame()
|
||||
{
|
||||
DateTime? prevLastPlayed = LastPlayed;
|
||||
UpdatePreGame();
|
||||
|
||||
if (!prevLastPlayed.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value;
|
||||
double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds;
|
||||
TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -784,7 +784,7 @@ namespace Ryujinx.Ui.Common.Configuration
|
||||
EnableDiscordIntegration.Value = true;
|
||||
CheckUpdatesOnStart.Value = true;
|
||||
ShowConfirmExit.Value = true;
|
||||
HideCursor.Value = HideCursorMode.Never;
|
||||
HideCursor.Value = HideCursorMode.OnIdle;
|
||||
Graphics.EnableVsync.Value = true;
|
||||
Graphics.EnableShaderCache.Value = true;
|
||||
Graphics.EnableTextureRecompression.Value = false;
|
||||
|
@@ -30,7 +30,7 @@ namespace Ryujinx.Ui.Common.Helper
|
||||
graphic.DrawImage(image, 0, 0, 128, 128);
|
||||
SaveBitmapAsIcon(bitmap, iconPath);
|
||||
|
||||
var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(basePath, applicationFilePath), iconPath, 0);
|
||||
var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath), iconPath, 0);
|
||||
shortcut.StringData.NameString = cleanedAppName;
|
||||
shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk"));
|
||||
}
|
||||
@@ -46,16 +46,16 @@ namespace Ryujinx.Ui.Common.Helper
|
||||
image.SaveAsPng(iconPath);
|
||||
|
||||
using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop"));
|
||||
outputFile.Write(desktopFile, cleanedAppName, iconPath, GetArgsString(basePath, applicationFilePath));
|
||||
outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath)}");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("macos")]
|
||||
private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName)
|
||||
{
|
||||
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName);
|
||||
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx");
|
||||
var plistFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.plist");
|
||||
// Macos .App folder
|
||||
string contentFolderPath = Path.Combine(desktopPath, cleanedAppName + ".app", "Contents");
|
||||
string contentFolderPath = Path.Combine("/Applications", cleanedAppName + ".app", "Contents");
|
||||
string scriptFolderPath = Path.Combine(contentFolderPath, "MacOS");
|
||||
|
||||
if (!Directory.Exists(scriptFolderPath))
|
||||
@@ -69,7 +69,7 @@ namespace Ryujinx.Ui.Common.Helper
|
||||
using StreamWriter scriptFile = new(scriptPath);
|
||||
|
||||
scriptFile.WriteLine("#!/bin/sh");
|
||||
scriptFile.WriteLine(GetArgsString(basePath, appFilePath));
|
||||
scriptFile.WriteLine($"{basePath} {GetArgsString(appFilePath)}");
|
||||
|
||||
// Set execute permission
|
||||
FileInfo fileInfo = new(scriptPath);
|
||||
@@ -125,13 +125,10 @@ namespace Ryujinx.Ui.Common.Helper
|
||||
throw new NotImplementedException("Shortcut support has not been implemented yet for this OS.");
|
||||
}
|
||||
|
||||
private static string GetArgsString(string basePath, string appFilePath)
|
||||
private static string GetArgsString(string appFilePath)
|
||||
{
|
||||
// args are first defined as a list, for easier adjustments in the future
|
||||
var argsList = new List<string>
|
||||
{
|
||||
basePath,
|
||||
};
|
||||
var argsList = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg))
|
||||
{
|
||||
@@ -141,7 +138,6 @@ namespace Ryujinx.Ui.Common.Helper
|
||||
|
||||
argsList.Add($"\"{appFilePath}\"");
|
||||
|
||||
|
||||
return String.Join(" ", argsList);
|
||||
}
|
||||
|
||||
|
219
src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs
Normal file
219
src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ui.Common.Helper
|
||||
{
|
||||
public static class ValueFormatUtils
|
||||
{
|
||||
private static readonly string[] _fileSizeUnitStrings =
|
||||
{
|
||||
"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", // Base 10 units, used for formatting and parsing
|
||||
"KB", "MB", "GB", "TB", "PB", "EB", // Base 2 units, used for parsing legacy values
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Used by <see cref="FormatFileSize"/>.
|
||||
/// </summary>
|
||||
public enum FileSizeUnits
|
||||
{
|
||||
Auto = -1,
|
||||
Bytes = 0,
|
||||
Kibibytes = 1,
|
||||
Mebibytes = 2,
|
||||
Gibibytes = 3,
|
||||
Tebibytes = 4,
|
||||
Pebibytes = 5,
|
||||
Exbibytes = 6,
|
||||
Kilobytes = 7,
|
||||
Megabytes = 8,
|
||||
Gigabytes = 9,
|
||||
Terabytes = 10,
|
||||
Petabytes = 11,
|
||||
Exabytes = 12,
|
||||
}
|
||||
|
||||
private const double SizeBase10 = 1000;
|
||||
private const double SizeBase2 = 1024;
|
||||
private const int UnitEBIndex = 6;
|
||||
|
||||
#region Value formatters
|
||||
|
||||
/// <summary>
|
||||
/// Creates a human-readable string from a <see cref="TimeSpan"/>.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The <see cref="TimeSpan"/> to be formatted.</param>
|
||||
/// <returns>A formatted string that can be displayed in the UI.</returns>
|
||||
public static string FormatTimeSpan(TimeSpan? timeSpan)
|
||||
{
|
||||
if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1)
|
||||
{
|
||||
// Game was never played
|
||||
return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (timeSpan.Value.TotalDays < 1)
|
||||
{
|
||||
// Game was played for less than a day
|
||||
return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
// Game was played for more than a day
|
||||
TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days));
|
||||
string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture);
|
||||
|
||||
return $"{timeSpan.Value.Days}d, {onlyTimeString}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a human-readable string from a <see cref="DateTime"/>.
|
||||
/// </summary>
|
||||
/// <param name="utcDateTime">The <see cref="DateTime"/> to be formatted. This is expected to be UTC-based.</param>
|
||||
/// <param name="culture">The <see cref="CultureInfo"/> that's used in formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.</param>
|
||||
/// <returns>A formatted string that can be displayed in the UI.</returns>
|
||||
public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null)
|
||||
{
|
||||
culture ??= CultureInfo.CurrentCulture;
|
||||
|
||||
if (!utcDateTime.HasValue)
|
||||
{
|
||||
// In the Avalonia UI, this is turned into a localized version of "Never" by LocalizedNeverConverter.
|
||||
return "Never";
|
||||
}
|
||||
|
||||
return utcDateTime.Value.ToLocalTime().ToString(culture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a human-readable file size string.
|
||||
/// </summary>
|
||||
/// <param name="size">The file size in bytes.</param>
|
||||
/// <param name="forceUnit">Formats the passed size value as this unit, bypassing the automatic unit choice.</param>
|
||||
/// <returns>A human-readable file size string.</returns>
|
||||
public static string FormatFileSize(long size, FileSizeUnits forceUnit = FileSizeUnits.Auto)
|
||||
{
|
||||
if (size <= 0)
|
||||
{
|
||||
return $"0 {_fileSizeUnitStrings[0]}";
|
||||
}
|
||||
|
||||
int unitIndex = (int)forceUnit;
|
||||
if (forceUnit == FileSizeUnits.Auto)
|
||||
{
|
||||
unitIndex = Convert.ToInt32(Math.Floor(Math.Log(size, SizeBase10)));
|
||||
|
||||
// Apply an upper bound so that exabytes are the biggest unit used when formatting.
|
||||
if (unitIndex > UnitEBIndex)
|
||||
{
|
||||
unitIndex = UnitEBIndex;
|
||||
}
|
||||
}
|
||||
|
||||
double sizeRounded;
|
||||
|
||||
if (unitIndex > UnitEBIndex)
|
||||
{
|
||||
sizeRounded = Math.Round(size / Math.Pow(SizeBase10, unitIndex - UnitEBIndex), 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
sizeRounded = Math.Round(size / Math.Pow(SizeBase2, unitIndex), 1);
|
||||
}
|
||||
|
||||
string sizeFormatted = sizeRounded.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
return $"{sizeFormatted} {_fileSizeUnitStrings[unitIndex]}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Value parsers
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string generated by <see cref="FormatTimeSpan"/> and returns the original <see cref="TimeSpan"/>.
|
||||
/// </summary>
|
||||
/// <param name="timeSpanString">A string representing a <see cref="TimeSpan"/>.</param>
|
||||
/// <returns>A <see cref="TimeSpan"/> object. If the input string couldn't been parsed, <see cref="TimeSpan.Zero"/> is returned.</returns>
|
||||
public static TimeSpan ParseTimeSpan(string timeSpanString)
|
||||
{
|
||||
TimeSpan returnTimeSpan = TimeSpan.Zero;
|
||||
|
||||
// An input string can either look like "01:23:45" or "1d, 01:23:45" if the timespan represents a duration of more than a day.
|
||||
// Here, we split the input string to check if it's the former or the latter.
|
||||
var valueSplit = timeSpanString.Split(", ");
|
||||
if (valueSplit.Length > 1)
|
||||
{
|
||||
var dayPart = valueSplit[0].Split("d")[0];
|
||||
if (int.TryParse(dayPart, out int days))
|
||||
{
|
||||
returnTimeSpan = returnTimeSpan.Add(TimeSpan.FromDays(days));
|
||||
}
|
||||
}
|
||||
|
||||
if (TimeSpan.TryParse(valueSplit.Last(), out TimeSpan parsedTimeSpan))
|
||||
{
|
||||
returnTimeSpan = returnTimeSpan.Add(parsedTimeSpan);
|
||||
}
|
||||
|
||||
return returnTimeSpan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string generated by <see cref="FormatDateTime"/> and returns the original <see cref="DateTime"/>.
|
||||
/// </summary>
|
||||
/// <param name="dateTimeString">The string representing a <see cref="DateTime"/>.</param>
|
||||
/// <returns>A <see cref="DateTime"/> object. If the input string couldn't be parsed, <see cref="DateTime.UnixEpoch"/> is returned.</returns>
|
||||
public static DateTime ParseDateTime(string dateTimeString)
|
||||
{
|
||||
if (!DateTime.TryParse(dateTimeString, CultureInfo.CurrentCulture, out DateTime parsedDateTime))
|
||||
{
|
||||
// Games that were never played are supposed to appear before the oldest played games in the list,
|
||||
// so returning DateTime.UnixEpoch here makes sense.
|
||||
return DateTime.UnixEpoch;
|
||||
}
|
||||
|
||||
return parsedDateTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string generated by <see cref="FormatFileSize"/> and returns a <see cref="long"/> representing a number of bytes.
|
||||
/// </summary>
|
||||
/// <param name="sizeString">A string representing a file size formatted with <see cref="FormatFileSize"/>.</param>
|
||||
/// <returns>A <see cref="long"/> representing a number of bytes.</returns>
|
||||
public static long ParseFileSize(string sizeString)
|
||||
{
|
||||
// Enumerating over the units backwards because otherwise, sizeString.EndsWith("B") would exit the loop in the first iteration.
|
||||
for (int i = _fileSizeUnitStrings.Length - 1; i >= 0; i--)
|
||||
{
|
||||
string unit = _fileSizeUnitStrings[i];
|
||||
if (!sizeString.EndsWith(unit))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string numberString = sizeString.Split(" ")[0];
|
||||
if (!double.TryParse(numberString, CultureInfo.InvariantCulture, out double number))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
double sizeBase = SizeBase2;
|
||||
|
||||
// If the unit index is one that points to a base 10 unit in the FileSizeUnitStrings array, subtract 6 to arrive at a usable power value.
|
||||
if (i > UnitEBIndex)
|
||||
{
|
||||
i -= UnitEBIndex;
|
||||
sizeBase = SizeBase10;
|
||||
}
|
||||
|
||||
number *= Math.Pow(sizeBase, i);
|
||||
|
||||
return Convert.ToInt64(number);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@@ -5,7 +5,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Common.SystemInfo
|
||||
namespace Ryujinx.Ui.Common.SystemInfo
|
||||
{
|
||||
[SupportedOSPlatform("linux")]
|
||||
class LinuxSystemInfo : SystemInfo
|
@@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Common.SystemInfo
|
||||
namespace Ryujinx.Ui.Common.SystemInfo
|
||||
{
|
||||
[SupportedOSPlatform("macos")]
|
||||
partial class MacOSSystemInfo : SystemInfo
|
@@ -1,10 +1,11 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Common.SystemInfo
|
||||
namespace Ryujinx.Ui.Common.SystemInfo
|
||||
{
|
||||
public class SystemInfo
|
||||
{
|
||||
@@ -20,13 +21,13 @@ namespace Ryujinx.Common.SystemInfo
|
||||
CpuName = "Unknown";
|
||||
}
|
||||
|
||||
private static string ToMiBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : $"{bytesValue / 1024 / 1024} MiB";
|
||||
private static string ToGBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : ValueFormatUtils.FormatFileSize((long)bytesValue, ValueFormatUtils.FileSizeUnits.Gibibytes);
|
||||
|
||||
public void Print()
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Operating System: {OsDescription}");
|
||||
Logger.Notice.Print(LogClass.Application, $"CPU: {CpuName}");
|
||||
Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToMiBString(RamTotal)} ; Available {ToMiBString(RamAvailable)}");
|
||||
Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToGBString(RamTotal)} ; Available {ToGBString(RamAvailable)}");
|
||||
}
|
||||
|
||||
public static SystemInfo Gather()
|
@@ -4,7 +4,7 @@ using System.Management;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Common.SystemInfo
|
||||
namespace Ryujinx.Ui.Common.SystemInfo
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
partial class WindowsSystemInfo : SystemInfo
|
@@ -3,14 +3,15 @@ using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.GraphicsDriver;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.SystemInfo;
|
||||
using Ryujinx.Common.SystemInterop;
|
||||
using Ryujinx.Modules;
|
||||
using Ryujinx.SDL2.Common;
|
||||
using Ryujinx.Ui;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using Ryujinx.Ui.Common.SystemInfo;
|
||||
using Ryujinx.Ui.Widgets;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using System;
|
||||
@@ -332,7 +333,12 @@ namespace Ryujinx
|
||||
|
||||
if (CommandLineState.LaunchPathArg != null)
|
||||
{
|
||||
mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
|
||||
ApplicationData applicationData = new()
|
||||
{
|
||||
Path = CommandLineState.LaunchPathArg,
|
||||
};
|
||||
|
||||
mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg);
|
||||
}
|
||||
|
||||
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using Gtk;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ui.Helper
|
||||
@@ -7,88 +8,26 @@ namespace Ryujinx.Ui.Helper
|
||||
{
|
||||
public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b)
|
||||
{
|
||||
static string ReverseFormat(string time)
|
||||
{
|
||||
if (time == "Never")
|
||||
{
|
||||
return "00";
|
||||
}
|
||||
TimeSpan aTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(a, 5).ToString());
|
||||
TimeSpan bTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(b, 5).ToString());
|
||||
|
||||
var numbers = time.Split(new char[] { 'd', 'h', 'm' });
|
||||
|
||||
time = time.Replace(" ", "").Replace("d", ".").Replace("h", ":").Replace("m", "");
|
||||
|
||||
if (numbers.Length == 2)
|
||||
{
|
||||
return $"00.00:{time}";
|
||||
}
|
||||
else if (numbers.Length == 3)
|
||||
{
|
||||
return $"00.{time}";
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
string aValue = ReverseFormat(model.GetValue(a, 5).ToString());
|
||||
string bValue = ReverseFormat(model.GetValue(b, 5).ToString());
|
||||
|
||||
return TimeSpan.Compare(TimeSpan.Parse(aValue), TimeSpan.Parse(bValue));
|
||||
return TimeSpan.Compare(aTimeSpan, bTimeSpan);
|
||||
}
|
||||
|
||||
public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b)
|
||||
{
|
||||
string aValue = model.GetValue(a, 6).ToString();
|
||||
string bValue = model.GetValue(b, 6).ToString();
|
||||
DateTime aDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(a, 6).ToString());
|
||||
DateTime bDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(b, 6).ToString());
|
||||
|
||||
if (aValue == "Never")
|
||||
{
|
||||
aValue = DateTime.UnixEpoch.ToString();
|
||||
}
|
||||
|
||||
if (bValue == "Never")
|
||||
{
|
||||
bValue = DateTime.UnixEpoch.ToString();
|
||||
}
|
||||
|
||||
return DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue));
|
||||
return DateTime.Compare(aDateTime, bDateTime);
|
||||
}
|
||||
|
||||
public static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b)
|
||||
{
|
||||
string aValue = model.GetValue(a, 8).ToString();
|
||||
string bValue = model.GetValue(b, 8).ToString();
|
||||
long aSize = ValueFormatUtils.ParseFileSize(model.GetValue(a, 8).ToString());
|
||||
long bSize = ValueFormatUtils.ParseFileSize(model.GetValue(b, 8).ToString());
|
||||
|
||||
if (aValue[^3..] == "GiB")
|
||||
{
|
||||
aValue = (float.Parse(aValue[0..^3]) * 1024).ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
aValue = aValue[0..^3];
|
||||
}
|
||||
|
||||
if (bValue[^3..] == "GiB")
|
||||
{
|
||||
bValue = (float.Parse(bValue[0..^3]) * 1024).ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
bValue = bValue[0..^3];
|
||||
}
|
||||
|
||||
if (float.Parse(aValue) > float.Parse(bValue))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (float.Parse(bValue) > float.Parse(aValue))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return aSize.CompareTo(bSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -39,6 +39,7 @@ using Silk.NET.Vulkan;
|
||||
using SPB.Graphics.Vulkan;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
@@ -70,7 +71,7 @@ namespace Ryujinx.Ui
|
||||
private bool _gameLoaded;
|
||||
private bool _ending;
|
||||
|
||||
private string _currentEmulatedGamePath = null;
|
||||
private ApplicationData _currentApplicationData = null;
|
||||
|
||||
private string _lastScannedAmiiboId = "";
|
||||
private bool _lastScannedAmiiboShowAll = false;
|
||||
@@ -181,8 +182,12 @@ namespace Ryujinx.Ui
|
||||
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile);
|
||||
_userChannelPersistence = new UserChannelPersistence();
|
||||
|
||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||
? IntegrityCheckLevel.ErrorOnInvalid
|
||||
: IntegrityCheckLevel.None;
|
||||
|
||||
// Instantiate GUI objects.
|
||||
_applicationLibrary = new ApplicationLibrary(_virtualFileSystem);
|
||||
_applicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel);
|
||||
_uiHandler = new GtkHostUiHandler(this);
|
||||
_deviceExitStatus = new AutoResetEvent(false);
|
||||
|
||||
@@ -784,7 +789,7 @@ namespace Ryujinx.Ui
|
||||
}
|
||||
}
|
||||
|
||||
private bool LoadApplication(string path, bool isFirmwareTitle)
|
||||
private bool LoadApplication(string path, ulong titleId, bool isFirmwareTitle)
|
||||
{
|
||||
SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
|
||||
|
||||
@@ -858,7 +863,7 @@ namespace Ryujinx.Ui
|
||||
case ".xci":
|
||||
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
||||
|
||||
return _emulationContext.LoadXci(path);
|
||||
return _emulationContext.LoadXci(path, titleId);
|
||||
case ".nca":
|
||||
Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
|
||||
|
||||
@@ -867,7 +872,7 @@ namespace Ryujinx.Ui
|
||||
case ".pfs0":
|
||||
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
||||
|
||||
return _emulationContext.LoadNsp(path);
|
||||
return _emulationContext.LoadNsp(path, titleId);
|
||||
default:
|
||||
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
|
||||
try
|
||||
@@ -888,7 +893,7 @@ namespace Ryujinx.Ui
|
||||
return false;
|
||||
}
|
||||
|
||||
public void RunApplication(string path, bool startFullscreen = false)
|
||||
public void RunApplication(ApplicationData application, bool startFullscreen = false)
|
||||
{
|
||||
if (_gameLoaded)
|
||||
{
|
||||
@@ -910,14 +915,14 @@ namespace Ryujinx.Ui
|
||||
|
||||
bool isFirmwareTitle = false;
|
||||
|
||||
if (path.StartsWith("@SystemContent"))
|
||||
if (application.Path.StartsWith("@SystemContent"))
|
||||
{
|
||||
path = VirtualFileSystem.SwitchPathToSystemPath(path);
|
||||
application.Path = VirtualFileSystem.SwitchPathToSystemPath(application.Path);
|
||||
|
||||
isFirmwareTitle = true;
|
||||
}
|
||||
|
||||
if (!LoadApplication(path, isFirmwareTitle))
|
||||
if (!LoadApplication(application.Path, application.Id, isFirmwareTitle))
|
||||
{
|
||||
_emulationContext.Dispose();
|
||||
SwitchToGameTable();
|
||||
@@ -927,7 +932,7 @@ namespace Ryujinx.Ui
|
||||
|
||||
SetupProgressUiHandlers();
|
||||
|
||||
_currentEmulatedGamePath = path;
|
||||
_currentApplicationData = application;
|
||||
|
||||
_deviceExitStatus.Reset();
|
||||
|
||||
@@ -954,7 +959,7 @@ namespace Ryujinx.Ui
|
||||
|
||||
ApplicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata =>
|
||||
{
|
||||
appMetadata.LastPlayed = DateTime.UtcNow;
|
||||
appMetadata.UpdatePreGame();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1097,13 +1102,7 @@ namespace Ryujinx.Ui
|
||||
{
|
||||
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
|
||||
{
|
||||
if (appMetadata.LastPlayed.HasValue)
|
||||
{
|
||||
double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds;
|
||||
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
appMetadata.LastPlayed = DateTime.UtcNow;
|
||||
appMetadata.UpdatePostGame();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1174,13 +1173,13 @@ namespace Ryujinx.Ui
|
||||
_tableStore.AppendValues(
|
||||
args.AppData.Favorite,
|
||||
new Gdk.Pixbuf(args.AppData.Icon, 75, 75),
|
||||
$"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}",
|
||||
$"{args.AppData.Name}\n{args.AppData.IdString.ToUpper()}",
|
||||
args.AppData.Developer,
|
||||
args.AppData.Version,
|
||||
args.AppData.TimePlayed,
|
||||
args.AppData.TimePlayedString,
|
||||
args.AppData.LastPlayedString,
|
||||
args.AppData.FileExtension,
|
||||
args.AppData.FileSize,
|
||||
args.AppData.FileSizeString,
|
||||
args.AppData.Path,
|
||||
args.AppData.ControlHolder);
|
||||
});
|
||||
@@ -1262,9 +1261,22 @@ namespace Ryujinx.Ui
|
||||
{
|
||||
_gameTableSelection.GetSelected(out TreeIter treeIter);
|
||||
|
||||
string path = (string)_tableStore.GetValue(treeIter, 9);
|
||||
ApplicationData application = new()
|
||||
{
|
||||
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(path);
|
||||
RunApplication(application);
|
||||
}
|
||||
|
||||
private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args)
|
||||
@@ -1322,13 +1334,22 @@ namespace Ryujinx.Ui
|
||||
return;
|
||||
}
|
||||
|
||||
string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString();
|
||||
string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0];
|
||||
string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower();
|
||||
ApplicationData application = new()
|
||||
{
|
||||
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),
|
||||
};
|
||||
|
||||
BlitStruct<ApplicationControlProperty> controlData = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10);
|
||||
|
||||
_ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData);
|
||||
_ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, application);
|
||||
}
|
||||
|
||||
private void Load_Application_File(object sender, EventArgs args)
|
||||
@@ -1350,7 +1371,12 @@ namespace Ryujinx.Ui
|
||||
|
||||
if (fileChooser.Run() == (int)ResponseType.Accept)
|
||||
{
|
||||
RunApplication(fileChooser.Filename);
|
||||
ApplicationData applicationData = new()
|
||||
{
|
||||
Path = fileChooser.Filename,
|
||||
};
|
||||
|
||||
RunApplication(applicationData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1360,7 +1386,13 @@ namespace Ryujinx.Ui
|
||||
|
||||
if (fileChooser.Run() == (int)ResponseType.Accept)
|
||||
{
|
||||
RunApplication(fileChooser.Filename);
|
||||
ApplicationData applicationData = new()
|
||||
{
|
||||
Name = System.IO.Path.GetFileNameWithoutExtension(fileChooser.Filename),
|
||||
Path = fileChooser.Filename,
|
||||
};
|
||||
|
||||
RunApplication(applicationData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1375,7 +1407,14 @@ namespace Ryujinx.Ui
|
||||
{
|
||||
string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
|
||||
|
||||
RunApplication(contentPath);
|
||||
ApplicationData applicationData = new()
|
||||
{
|
||||
Name = "miiEdit",
|
||||
Id = 0x0100000000001009ul,
|
||||
Path = contentPath,
|
||||
};
|
||||
|
||||
RunApplication(applicationData);
|
||||
}
|
||||
|
||||
private void Open_Ryu_Folder(object sender, EventArgs args)
|
||||
@@ -1651,13 +1690,13 @@ namespace Ryujinx.Ui
|
||||
{
|
||||
_userChannelPersistence.ShouldRestart = false;
|
||||
|
||||
RunApplication(_currentEmulatedGamePath);
|
||||
RunApplication(_currentApplicationData);
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise, clear state.
|
||||
_userChannelPersistence = new UserChannelPersistence();
|
||||
_currentEmulatedGamePath = null;
|
||||
_currentApplicationData = null;
|
||||
_actionMenu.Sensitive = false;
|
||||
_firmwareInstallFile.Sensitive = true;
|
||||
_firmwareInstallDirectory.Sensitive = true;
|
||||
@@ -1719,7 +1758,7 @@ namespace Ryujinx.Ui
|
||||
_emulationContext.Processes.ActiveApplication.ProgramId,
|
||||
_emulationContext.Processes.ActiveApplication.ApplicationControlProperties
|
||||
.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(),
|
||||
_currentEmulatedGamePath);
|
||||
_currentApplicationData.Path);
|
||||
|
||||
window.Destroyed += CheatWindow_Destroyed;
|
||||
window.Show();
|
||||
|
@@ -211,6 +211,8 @@ namespace Ryujinx.Ui.Widgets
|
||||
_manageSubMenu.Append(_openPtcDirMenuItem);
|
||||
_manageSubMenu.Append(_openShaderCacheDirMenuItem);
|
||||
|
||||
Add(_createShortcutMenuItem);
|
||||
Add(new SeparatorMenuItem());
|
||||
Add(_openSaveUserDirMenuItem);
|
||||
Add(_openSaveDeviceDirMenuItem);
|
||||
Add(_openSaveBcatDirMenuItem);
|
||||
@@ -223,7 +225,6 @@ namespace Ryujinx.Ui.Widgets
|
||||
Add(new SeparatorMenuItem());
|
||||
Add(_manageCacheMenuItem);
|
||||
Add(_extractMenuItem);
|
||||
Add(_createShortcutMenuItem);
|
||||
|
||||
ShowAll();
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
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;
|
||||
@@ -23,7 +24,6 @@ using Ryujinx.Ui.Windows;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
@@ -36,17 +36,13 @@ namespace Ryujinx.Ui.Widgets
|
||||
private readonly VirtualFileSystem _virtualFileSystem;
|
||||
private readonly AccountManager _accountManager;
|
||||
private readonly HorizonClient _horizonClient;
|
||||
private readonly BlitStruct<ApplicationControlProperty> _controlData;
|
||||
|
||||
private readonly string _titleFilePath;
|
||||
private readonly string _titleName;
|
||||
private readonly string _titleIdText;
|
||||
private readonly ulong _titleId;
|
||||
private readonly ApplicationData _title;
|
||||
|
||||
private MessageDialog _dialog;
|
||||
private bool _cancel;
|
||||
|
||||
public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct<ApplicationControlProperty> controlData)
|
||||
public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, ApplicationData applicationData)
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
@@ -55,23 +51,13 @@ namespace Ryujinx.Ui.Widgets
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_accountManager = accountManager;
|
||||
_horizonClient = horizonClient;
|
||||
_titleFilePath = titleFilePath;
|
||||
_titleName = titleName;
|
||||
_titleIdText = titleId;
|
||||
_controlData = controlData;
|
||||
_title = applicationData;
|
||||
|
||||
if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId))
|
||||
{
|
||||
GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id");
|
||||
_openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.UserAccountSaveDataSize > 0;
|
||||
_openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.DeviceSaveDataSize > 0;
|
||||
_openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
||||
|
||||
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();
|
||||
string fileExt = System.IO.Path.GetExtension(_title.Path).ToLower();
|
||||
bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci";
|
||||
|
||||
_extractRomFsMenuItem.Sensitive = hasNca;
|
||||
@@ -137,7 +123,7 @@ namespace Ryujinx.Ui.Widgets
|
||||
|
||||
private void OpenSaveDir(in SaveDataFilter saveDataFilter)
|
||||
{
|
||||
if (!TryFindSaveData(_titleName, _titleId, _controlData, in saveDataFilter, out ulong saveDataId))
|
||||
if (!TryFindSaveData(_title.Name, _title.Id, _title.ControlHolder, in saveDataFilter, out ulong saveDataId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -190,7 +176,7 @@ namespace Ryujinx.Ui.Widgets
|
||||
{
|
||||
Title = "Ryujinx - NCA Section Extractor",
|
||||
Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"),
|
||||
SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...",
|
||||
SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_title.Path)}...",
|
||||
WindowPosition = WindowPosition.Center,
|
||||
};
|
||||
|
||||
@@ -202,18 +188,18 @@ namespace Ryujinx.Ui.Widgets
|
||||
}
|
||||
});
|
||||
|
||||
using FileStream file = new(_titleFilePath, FileMode.Open, FileAccess.Read);
|
||||
using FileStream file = new(_title.Path, FileMode.Open, FileAccess.Read);
|
||||
|
||||
Nca mainNca = null;
|
||||
Nca patchNca = null;
|
||||
|
||||
if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") ||
|
||||
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") ||
|
||||
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci"))
|
||||
if ((System.IO.Path.GetExtension(_title.Path).ToLower() == ".nsp") ||
|
||||
(System.IO.Path.GetExtension(_title.Path).ToLower() == ".pfs0") ||
|
||||
(System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci"))
|
||||
{
|
||||
IFileSystem pfs;
|
||||
|
||||
if (System.IO.Path.GetExtension(_titleFilePath) == ".xci")
|
||||
if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci")
|
||||
{
|
||||
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
|
||||
|
||||
@@ -249,7 +235,7 @@ namespace Ryujinx.Ui.Widgets
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca")
|
||||
else if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".nca")
|
||||
{
|
||||
mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
|
||||
}
|
||||
@@ -266,7 +252,11 @@ namespace Ryujinx.Ui.Widgets
|
||||
return;
|
||||
}
|
||||
|
||||
(Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
|
||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||
? IntegrityCheckLevel.ErrorOnInvalid
|
||||
: IntegrityCheckLevel.None;
|
||||
|
||||
(Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _);
|
||||
|
||||
if (updatePatchNca != null)
|
||||
{
|
||||
@@ -460,44 +450,44 @@ namespace Ryujinx.Ui.Widgets
|
||||
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 saveDataFilter = SaveDataFilter.Make(_titleId, saveType: default, userId, saveDataId: default, index: default);
|
||||
var saveDataFilter = SaveDataFilter.Make(_title.Id, saveType: default, userId, saveDataId: default, index: default);
|
||||
|
||||
OpenSaveDir(in saveDataFilter);
|
||||
}
|
||||
|
||||
private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Device, userId: default, saveDataId: default, index: default);
|
||||
var saveDataFilter = SaveDataFilter.Make(_title.Id, SaveDataType.Device, userId: default, saveDataId: default, index: default);
|
||||
|
||||
OpenSaveDir(in saveDataFilter);
|
||||
}
|
||||
|
||||
private void OpenSaveBcatDir_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Bcat, userId: default, saveDataId: default, index: default);
|
||||
var saveDataFilter = SaveDataFilter.Make(_title.Id, SaveDataType.Bcat, userId: default, saveDataId: default, index: default);
|
||||
|
||||
OpenSaveDir(in saveDataFilter);
|
||||
}
|
||||
|
||||
private void ManageTitleUpdates_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
new TitleUpdateWindow(_parent, _virtualFileSystem, _titleIdText, _titleName).Show();
|
||||
new TitleUpdateWindow(_parent, _virtualFileSystem, _title).Show();
|
||||
}
|
||||
|
||||
private void ManageDlc_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show();
|
||||
new DlcWindow(_virtualFileSystem, _title.IdString, _title).Show();
|
||||
}
|
||||
|
||||
private void ManageCheats_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
new CheatWindow(_virtualFileSystem, _titleId, _titleName, _titleFilePath).Show();
|
||||
new CheatWindow(_virtualFileSystem, _title.Id, _title.Name, _title.Path).Show();
|
||||
}
|
||||
|
||||
private void OpenTitleModDir_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
string modsBasePath = ModLoader.GetModsBasePath();
|
||||
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText);
|
||||
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _title.IdString);
|
||||
|
||||
OpenHelper.OpenFolder(titleModsPath);
|
||||
}
|
||||
@@ -505,7 +495,7 @@ namespace Ryujinx.Ui.Widgets
|
||||
private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
||||
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText);
|
||||
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _title.IdString);
|
||||
|
||||
OpenHelper.OpenFolder(titleModsPath);
|
||||
}
|
||||
@@ -527,7 +517,7 @@ namespace Ryujinx.Ui.Widgets
|
||||
|
||||
private void OpenPtcDir_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu");
|
||||
string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu");
|
||||
|
||||
string mainPath = System.IO.Path.Combine(ptcDir, "0");
|
||||
string backupPath = System.IO.Path.Combine(ptcDir, "1");
|
||||
@@ -544,7 +534,7 @@ namespace Ryujinx.Ui.Widgets
|
||||
|
||||
private void OpenShaderCacheDir_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader");
|
||||
string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader");
|
||||
|
||||
if (!Directory.Exists(shaderCacheDir))
|
||||
{
|
||||
@@ -556,10 +546,10 @@ namespace Ryujinx.Ui.Widgets
|
||||
|
||||
private void PurgePtcCache_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0"));
|
||||
DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1"));
|
||||
DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "0"));
|
||||
DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "1"));
|
||||
|
||||
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?");
|
||||
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?");
|
||||
|
||||
List<FileInfo> cacheFiles = new();
|
||||
|
||||
@@ -593,9 +583,9 @@ namespace Ryujinx.Ui.Widgets
|
||||
|
||||
private void PurgeShaderCache_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"));
|
||||
DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader"));
|
||||
|
||||
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?");
|
||||
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?");
|
||||
|
||||
List<DirectoryInfo> oldCacheDirectories = new();
|
||||
List<FileInfo> newCacheFiles = new();
|
||||
@@ -637,8 +627,11 @@ namespace Ryujinx.Ui.Widgets
|
||||
|
||||
private void CreateShortcut_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language);
|
||||
ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon);
|
||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||
? IntegrityCheckLevel.ErrorOnInvalid
|
||||
: 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,7 +1,9 @@
|
||||
using Gtk;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -27,8 +29,13 @@ namespace Ryujinx.Ui.Windows
|
||||
private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow"))
|
||||
{
|
||||
builder.Autoconnect(this);
|
||||
|
||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||
? IntegrityCheckLevel.ErrorOnInvalid
|
||||
: IntegrityCheckLevel.None;
|
||||
|
||||
_baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]";
|
||||
_buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath)}";
|
||||
_buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath)}";
|
||||
|
||||
string modsBasePath = ModLoader.GetModsBasePath();
|
||||
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16"));
|
||||
|
@@ -9,9 +9,12 @@ using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Widgets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using GUI = Gtk.Builder.ObjectAttribute;
|
||||
|
||||
@@ -20,7 +23,7 @@ namespace Ryujinx.Ui.Windows
|
||||
public class DlcWindow : Window
|
||||
{
|
||||
private readonly VirtualFileSystem _virtualFileSystem;
|
||||
private readonly string _titleId;
|
||||
private readonly string _applicationId;
|
||||
private readonly string _dlcJsonPath;
|
||||
private readonly List<DownloadableContentContainer> _dlcContainerList;
|
||||
|
||||
@@ -32,16 +35,16 @@ namespace Ryujinx.Ui.Windows
|
||||
[GUI] TreeSelection _dlcTreeSelection;
|
||||
#pragma warning restore CS0649, IDE0044
|
||||
|
||||
public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { }
|
||||
public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, ApplicationData title) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, title) { }
|
||||
|
||||
private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow"))
|
||||
private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string applicationId, ApplicationData title) : base(builder.GetRawOwnedObject("_dlcWindow"))
|
||||
{
|
||||
builder.Autoconnect(this);
|
||||
|
||||
_titleId = titleId;
|
||||
_applicationId = applicationId;
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json");
|
||||
_baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]";
|
||||
_dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationId, "dlc.json");
|
||||
_baseTitleInfoLabel.Text = $"DLC Available for {title.Name} [{applicationId.ToUpper()}]";
|
||||
|
||||
try
|
||||
{
|
||||
@@ -72,9 +75,12 @@ namespace Ryujinx.Ui.Windows
|
||||
};
|
||||
|
||||
_dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0);
|
||||
_dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1);
|
||||
_dlcTreeView.AppendColumn("ApplicationId", new CellRendererText(), "text", 1);
|
||||
_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)
|
||||
{
|
||||
if (File.Exists(dlcContainer.ContainerPath))
|
||||
@@ -89,7 +95,10 @@ namespace Ryujinx.Ui.Windows
|
||||
using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
|
||||
|
||||
PartitionFileSystem pfs = new();
|
||||
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
||||
if (pfs.Initialize(containerFile.AsStorage()).IsFailure())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_virtualFileSystem.ImportTickets(pfs);
|
||||
|
||||
@@ -128,6 +137,57 @@ namespace Ryujinx.Ui.Windows
|
||||
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)
|
||||
{
|
||||
FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel")
|
||||
@@ -147,52 +207,7 @@ namespace Ryujinx.Ui.Windows
|
||||
{
|
||||
foreach (string containerPath in fileChooser.Filenames)
|
||||
{
|
||||
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!");
|
||||
}
|
||||
AddDlc(containerPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,12 +4,15 @@ using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Ns;
|
||||
using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Widgets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -24,7 +27,7 @@ namespace Ryujinx.Ui.Windows
|
||||
{
|
||||
private readonly MainWindow _parent;
|
||||
private readonly VirtualFileSystem _virtualFileSystem;
|
||||
private readonly string _titleId;
|
||||
private readonly ApplicationData _title;
|
||||
private readonly string _updateJsonPath;
|
||||
|
||||
private TitleUpdateMetadata _titleUpdateWindowData;
|
||||
@@ -38,17 +41,17 @@ namespace Ryujinx.Ui.Windows
|
||||
[GUI] RadioButton _noUpdateRadioButton;
|
||||
#pragma warning restore CS0649, IDE0044
|
||||
|
||||
public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { }
|
||||
public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, applicationData) { }
|
||||
|
||||
private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow"))
|
||||
private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : base(builder.GetRawOwnedObject("_titleUpdateWindow"))
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
builder.Autoconnect(this);
|
||||
|
||||
_titleId = titleId;
|
||||
_title = applicationData;
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json");
|
||||
_updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "updates.json");
|
||||
_radioButtonToPathDictionary = new Dictionary<RadioButton, string>();
|
||||
|
||||
try
|
||||
@@ -64,7 +67,10 @@ namespace Ryujinx.Ui.Windows
|
||||
};
|
||||
}
|
||||
|
||||
_baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]";
|
||||
_baseTitleInfoLabel.Text = $"Updates Available for {applicationData.Name} [{applicationData.IdString}]";
|
||||
|
||||
// Try to get updates from PFS first
|
||||
AddUpdate(_title.Path, true);
|
||||
|
||||
foreach (string path in _titleUpdateWindowData.Paths)
|
||||
{
|
||||
@@ -84,18 +90,41 @@ namespace Ryujinx.Ui.Windows
|
||||
}
|
||||
}
|
||||
|
||||
private void AddUpdate(string path)
|
||||
private void AddUpdate(string path, bool ignoreNotFound = false)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||
? IntegrityCheckLevel.ErrorOnInvalid
|
||||
: IntegrityCheckLevel.None;
|
||||
|
||||
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
||||
|
||||
PartitionFileSystem nsp = new();
|
||||
nsp.Initialize(file.AsStorage()).ThrowIfFailure();
|
||||
IFileSystem pfs;
|
||||
|
||||
try
|
||||
{
|
||||
(Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0);
|
||||
if (System.IO.Path.GetExtension(path).ToLower() == ".xci")
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -106,7 +135,14 @@ namespace Ryujinx.Ui.Windows
|
||||
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();
|
||||
|
||||
RadioButton radioButton = new($"Version {controlData.DisplayVersionString.ToString()} - {path}");
|
||||
string radioLabel = $"Version {controlData.DisplayVersionString.ToString()} - {path}";
|
||||
|
||||
if (System.IO.Path.GetExtension(path).ToLower() == ".xci")
|
||||
{
|
||||
radioLabel = "Bundled: " + radioLabel;
|
||||
}
|
||||
|
||||
RadioButton radioButton = new(radioLabel);
|
||||
radioButton.JoinGroup(_noUpdateRadioButton);
|
||||
|
||||
_availableUpdatesBox.Add(radioButton);
|
||||
@@ -117,7 +153,10 @@ namespace Ryujinx.Ui.Windows
|
||||
}
|
||||
else
|
||||
{
|
||||
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
|
||||
if (!ignoreNotFound)
|
||||
{
|
||||
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
|
Reference in New Issue
Block a user