Compare commits

...

7 Commits

Author SHA1 Message Date
TSRBerry
5c3cfb84c0 Add support for multi game XCIs (#5638)
* Add default values to ApplicationData directly

* Refactor application loading

It should now be possible to load multi game XCIs.
Included updates won't be detected for now.
Opening a game from the command line currently only opens the first one.

* Only include program NCAs where at least one tuple item is not null

* Get application data by title id and add programIndex check back

* Refactor application loading again and remove duplicate code

* Actually use patch ncas for updates

* Fix number of applications found with multi game xcis

* Don't load bundled updates from multi game xcis

* Change ApplicationData.TitleId type to ulong & Add TitleIdString property

* Use cnmt files and ContentCollection to load programs

* Ava: Add updates and DLCs from gamecarts

* Get the cnmt file from its NCA

* Ava: Identify bundled updates in updater window

* Fix the (hopefully) last few bugs

* Add idOffset parameter to GetNcaByType

* Handle missing file for dlc.json

* Ava: Shorten error message for invalid files

* Gtk: Add additional string for bundled updates in TitleUpdateWindow

* Hopefully fix DLC issues

* Apply formatting

* Finally fix DLC issues

* Adjust property names and fileSize field

* Read the correct update file

* Fix wrong casing for application id strings

* Rename TitleId to ApplicationId

* Address review comments

* Fix formatting issues

* Apply suggestions from code review

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* Gracefully fail when loading pfs for update and dlc window

* Fix applications with multiple programs

* Fix DLCWindow crash on GTK

* Fix some GUI issues

* Remove IsXci again

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
2023-11-11 21:56:57 +01:00
NitroTears
55557525b1 Create Desktop Shortcut fixes (#5852)
* remove duplicate basePath arg, add --fullscreen arg

* Changing FriendlyName to set "Ryujinx" text

* Fix GetArgsString using the base path

* Change desktop path to the Applications folder when creating shortcut on Mac

Co-authored-by: Nicko Anastassiu <134955950+nickoanastassiu@users.noreply.github.com>

* Move Create Shortcut button to top of context menu

---------

Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
Co-authored-by: Nicko Anastassiu <134955950+nickoanastassiu@users.noreply.github.com>
2023-11-11 16:08:42 +01:00
Isaac Marovitz
7e6342e44d Add accelerator keys for Options and Help (#5884) 2023-11-11 15:57:15 +01:00
jcm
c3555cb5d6 UI: Change default hide cursor mode to OnIdle (#5906)
Co-authored-by: jcm <butt@butts.com>
2023-11-11 15:27:53 +01:00
gdkchan
815819767c Force all exclusive memory accesses to be ordered on AppleHv (#5898)
* Implement reprotect method on virtual memory manager (currently stubbed)

* Force all exclusive memory accesses to be ordered on AppleHv

* Format whitespace

* Fix test build

* Fix comment copy/paste

* Fix wrong bit for isLoad

* Update src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs

Co-authored-by: riperiperi <rhy3756547@hotmail.com>

---------

Co-authored-by: riperiperi <rhy3756547@hotmail.com>
2023-11-07 13:24:10 -03:00
SamusAranX
623604c391 Overhaul of string formatting/parsing/sorting logic for TimeSpans, DateTimes, and file sizes (#4956)
* Fixed formatting/parsing issues with ApplicationData properties TimePlayed, LastPlayed, and FileSize

Replaced double-based TimePlayed property with TimeSpan?-based one in ApplicationData and ApplicationMetadata
Added a migration for TimePlayed, just like in #4861
Consolidated ApplicationData's FileSize* properties into one FileSize property
Added a formatting/parsing helper class ValueFormatUtils for TimeSpans, DateTimes, and file sizes
Added new value converters for TimeSpans and file sizes for the Avalonia UI
Added TimePlayedSortComparer
Fixed sort order in LastPlayedSortComparer
Fixed sort order for ApplicationData fields TimePlayed, LastPlayed, and FileSize
Fixed crashes caused by SortHelper
Replaced SystemInfo.ToMiBString with ToGiBString backed by ValueFormatUtils
Replaced SaveModel.GetSizeString() with ValueFormatUtils

* Additional ApplicationLibrary changes that got lost in the last commit

* Removed unneeded usings

* Removed converters as they are no longer needed

* Updated comment on FormatDateTime

* Removed base10 parameter from ValueFormatUtils

FormatFileSize now always returns base 2 values with base 10 units
Made ParseFileSize capable of parsing both base 2 and base 10 units

* Removed nullable attribute from TimePlayed property

Centralized TimePlayed update code into ApplicationMetadata

* Changed UpdateTimePlayed() to use TimeSpan logic

* Removed JsonIgnore attributes from ApplicationData

* Implemented requested format changes

* Fixed mistakes in method documentation comments

* Made it so the Last Played value "Never" is localized in the Avalonia UI

* Implemented suggestions

* Remove unused import

* Did a comment refinement pass in ValueFormatUtils.cs

* Reordered ValueFormatUtils methods and sorted them into #regions

* Integrated functionality from #5056

Also removed Logger print from last_played migration code

* Implemented suggestions

* Moved ValueFormatUtils and SystemInfo to namespace Ryujinx.Ui.Common

* common: Respect proper value format convention and use base10 by default

This could be discuss again in another issue/PR, for now revert to the previous behavior.

Signed-off-by: Mary Guillemard <mary@mary.zone>

---------

Signed-off-by: Mary Guillemard <mary@mary.zone>
Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
Co-authored-by: Mary Guillemard <mary@mary.zone>
2023-11-06 22:47:44 +01:00
Somebody Whoisbored
617c5700ca Better handle instruction aborts when hitting unmapped memory (#5869)
* Adjust ARMeilleure to better handle instruction aborts when hitting unmapped memory

* Update src/ARMeilleure/Decoders/Decoder.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
2023-11-05 12:32:17 +01:00
61 changed files with 1736 additions and 1118 deletions

View File

@@ -38,7 +38,9 @@ namespace ARMeilleure.Decoders
{ {
block = new Block(blkAddress); 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.Exit = true;
block.EndAddress = blkAddress; block.EndAddress = blkAddress;

View File

@@ -54,8 +54,6 @@ using System.Threading.Tasks;
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop; using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing; using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
using Image = SixLabors.ImageSharp.Image; using Image = SixLabors.ImageSharp.Image;
using InputManager = Ryujinx.Input.HLE.InputManager;
using IRenderer = Ryujinx.Graphics.GAL.IRenderer;
using Key = Ryujinx.Input.Key; using Key = Ryujinx.Input.Key;
using MouseButton = Ryujinx.Input.MouseButton; using MouseButton = Ryujinx.Input.MouseButton;
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
@@ -123,12 +121,14 @@ namespace Ryujinx.Ava
public int Width { get; private set; } public int Width { get; private set; }
public int Height { get; private set; } public int Height { get; private set; }
public string ApplicationPath { get; private set; } public string ApplicationPath { get; private set; }
public ulong ApplicationId { get; private set; }
public bool ScreenshotRequested { get; set; } public bool ScreenshotRequested { get; set; }
public AppHost( public AppHost(
RendererHost renderer, RendererHost renderer,
InputManager inputManager, InputManager inputManager,
string applicationPath, string applicationPath,
ulong applicationId,
VirtualFileSystem virtualFileSystem, VirtualFileSystem virtualFileSystem,
ContentManager contentManager, ContentManager contentManager,
AccountManager accountManager, AccountManager accountManager,
@@ -152,6 +152,7 @@ namespace Ryujinx.Ava
NpadManager = _inputManager.CreateNpadManager(); NpadManager = _inputManager.CreateNpadManager();
TouchScreenManager = _inputManager.CreateTouchScreenManager(); TouchScreenManager = _inputManager.CreateTouchScreenManager();
ApplicationPath = applicationPath; ApplicationPath = applicationPath;
ApplicationId = applicationId;
VirtualFileSystem = virtualFileSystem; VirtualFileSystem = virtualFileSystem;
ContentManager = contentManager; ContentManager = contentManager;
@@ -641,7 +642,7 @@ namespace Ryujinx.Ava
{ {
Logger.Info?.Print(LogClass.Application, "Loading as XCI."); Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
if (!Device.LoadXci(ApplicationPath)) if (!Device.LoadXci(ApplicationPath, ApplicationId))
{ {
Device.Dispose(); Device.Dispose();
@@ -668,7 +669,7 @@ namespace Ryujinx.Ava
{ {
Logger.Info?.Print(LogClass.Application, "Loading as NSP."); Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
if (!Device.LoadNsp(ApplicationPath)) if (!Device.LoadNsp(ApplicationPath, ApplicationId))
{ {
Device.Dispose(); Device.Dispose();
@@ -716,7 +717,7 @@ namespace Ryujinx.Ava
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
{ {
appMetadata.LastPlayed = DateTime.UtcNow; appMetadata.UpdatePreGame();
}); });
return true; return true;

View File

@@ -14,7 +14,7 @@
"MenuBarFileOpenEmuFolder": "Open Ryujinx Folder", "MenuBarFileOpenEmuFolder": "Open Ryujinx Folder",
"MenuBarFileOpenLogsFolder": "Open Logs Folder", "MenuBarFileOpenLogsFolder": "Open Logs Folder",
"MenuBarFileExit": "_Exit", "MenuBarFileExit": "_Exit",
"MenuBarOptions": "Options", "MenuBarOptions": "_Options",
"MenuBarOptionsToggleFullscreen": "Toggle Fullscreen", "MenuBarOptionsToggleFullscreen": "Toggle Fullscreen",
"MenuBarOptionsStartGamesInFullscreen": "Start Games in Fullscreen Mode", "MenuBarOptionsStartGamesInFullscreen": "Start Games in Fullscreen Mode",
"MenuBarOptionsStopEmulation": "Stop Emulation", "MenuBarOptionsStopEmulation": "Stop Emulation",
@@ -30,7 +30,7 @@
"MenuBarToolsManageFileTypes": "Manage file types", "MenuBarToolsManageFileTypes": "Manage file types",
"MenuBarToolsInstallFileTypes": "Install file types", "MenuBarToolsInstallFileTypes": "Install file types",
"MenuBarToolsUninstallFileTypes": "Uninstall file types", "MenuBarToolsUninstallFileTypes": "Uninstall file types",
"MenuBarHelp": "Help", "MenuBarHelp": "_Help",
"MenuBarHelpCheckForUpdates": "Check for Updates", "MenuBarHelpCheckForUpdates": "Check for Updates",
"MenuBarHelpAbout": "About", "MenuBarHelpAbout": "About",
"MenuSearch": "Search...", "MenuSearch": "Search...",
@@ -539,6 +539,8 @@
"OpenSetupGuideMessage": "Open the Setup Guide", "OpenSetupGuideMessage": "Open the Setup Guide",
"NoUpdate": "No Update", "NoUpdate": "No Update",
"TitleUpdateVersionLabel": "Version {0}", "TitleUpdateVersionLabel": "Version {0}",
"TitleBundledUpdateVersionLabel": "Bundled: Version {0}",
"TitleBundledDlcLabel": "Bundled:",
"RyujinxInfo": "Ryujinx - Info", "RyujinxInfo": "Ryujinx - Info",
"RyujinxConfirm": "Ryujinx - Confirmation", "RyujinxConfirm": "Ryujinx - Confirmation",
"FileDialogAllTypes": "All types", "FileDialogAllTypes": "All types",

View File

@@ -18,7 +18,8 @@ using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Ui.App.Common; using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
using System; using System;
using System.Buffers; using System.Buffers;
@@ -226,7 +227,11 @@ namespace Ryujinx.Ava.Common
return; 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) if (updatePatchNca != null)
{ {
patchNca = updatePatchNca; patchNca = updatePatchNca;

View File

@@ -6,13 +6,13 @@ using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.SystemInfo;
using Ryujinx.Common.SystemInterop; using Ryujinx.Common.SystemInterop;
using Ryujinx.Modules; using Ryujinx.Modules;
using Ryujinx.SDL2.Common; using Ryujinx.SDL2.Common;
using Ryujinx.Ui.Common; using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
using Ryujinx.Ui.Common.SystemInfo;
using System; using System;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;

View File

@@ -12,6 +12,11 @@
Click="ToggleFavorite_Click" Click="ToggleFavorite_Click"
Header="{locale:Locale GameListContextMenuToggleFavorite}" Header="{locale:Locale GameListContextMenuToggleFavorite}"
ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" /> ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
<MenuItem
Click="CreateApplicationShortcut_Click"
Header="{locale:Locale GameListContextMenuCreateShortcut}"
IsEnabled="{Binding CreateShortcutEnabled}"
ToolTip.Tip="{locale:Locale GameListContextMenuCreateShortcutToolTip}" />
<Separator /> <Separator />
<MenuItem <MenuItem
Click="OpenUserSaveDirectory_Click" Click="OpenUserSaveDirectory_Click"
@@ -82,9 +87,4 @@
Header="{locale:Locale GameListContextMenuExtractDataLogo}" Header="{locale:Locale GameListContextMenuExtractDataLogo}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" /> ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
</MenuItem> </MenuItem>
<MenuItem
Click="CreateApplicationShortcut_Click"
Header="{locale:Locale GameListContextMenuCreateShortcut}"
IsEnabled="{Binding CreateShortcutEnabled}"
ToolTip.Tip="{locale:Locale GameListContextMenuCreateShortcutToolTip}" />
</MenuFlyout> </MenuFlyout>

View File

@@ -1,7 +1,6 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using LibHac.Fs; using LibHac.Fs;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common; using Ryujinx.Ava.Common;
@@ -15,7 +14,6 @@ using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using Path = System.IO.Path; using Path = System.IO.Path;
@@ -41,7 +39,7 @@ namespace Ryujinx.Ava.UI.Controls
{ {
viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite; viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite;
ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata => ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata =>
{ {
appMetadata.Favorite = viewModel.SelectedApplication.Favorite; appMetadata.Favorite = viewModel.SelectedApplication.Favorite;
}); });
@@ -76,19 +74,9 @@ namespace Ryujinx.Ava.UI.Controls
{ {
if (viewModel?.SelectedApplication != null) if (viewModel?.SelectedApplication != null)
{ {
if (!ulong.TryParse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default);
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
});
return; ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name);
}
var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default);
ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName);
} }
} }
@@ -98,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null) 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) 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( await new CheatWindow(
viewModel.VirtualFileSystem, viewModel.VirtualFileSystem,
viewModel.SelectedApplication.TitleId, viewModel.SelectedApplication.IdString,
viewModel.SelectedApplication.TitleName, viewModel.SelectedApplication.Name,
viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window); viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window);
} }
} }
@@ -133,7 +121,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null) if (viewModel?.SelectedApplication != null)
{ {
string modsBasePath = ModLoader.GetModsBasePath(); string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.TitleId); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.IdString);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }
@@ -146,7 +134,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null) if (viewModel?.SelectedApplication != null)
{ {
string sdModsBasePath = ModLoader.GetSdModsBasePath(); string sdModsBasePath = ModLoader.GetSdModsBasePath();
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.TitleId); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.IdString);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }
@@ -160,15 +148,15 @@ namespace Ryujinx.Ava.UI.Controls
{ {
UserResult result = await ContentDialogHelper.CreateConfirmationDialog( UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance[LocaleKeys.DialogWarning], LocaleManager.Instance[LocaleKeys.DialogWarning],
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName), LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name),
LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result == UserResult.Yes) if (result == UserResult.Yes)
{ {
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0")); DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0"));
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1")); DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1"));
List<FileInfo> cacheFiles = new(); List<FileInfo> cacheFiles = new();
@@ -208,14 +196,14 @@ namespace Ryujinx.Ava.UI.Controls
{ {
UserResult result = await ContentDialogHelper.CreateConfirmationDialog( UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance[LocaleKeys.DialogWarning], LocaleManager.Instance[LocaleKeys.DialogWarning],
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName), LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name),
LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result == UserResult.Yes) if (result == UserResult.Yes)
{ {
DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader")); DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader"));
List<DirectoryInfo> oldCacheDirectories = new(); List<DirectoryInfo> oldCacheDirectories = new();
List<FileInfo> newCacheFiles = new(); List<FileInfo> newCacheFiles = new();
@@ -263,7 +251,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null) 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 mainDir = Path.Combine(ptcDir, "0");
string backupDir = Path.Combine(ptcDir, "1"); string backupDir = Path.Combine(ptcDir, "1");
@@ -284,7 +272,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null) 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)) if (!Directory.Exists(shaderCacheDir))
{ {
@@ -305,7 +293,7 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.StorageProvider, viewModel.StorageProvider,
NcaSectionType.Code, NcaSectionType.Code,
viewModel.SelectedApplication.Path, viewModel.SelectedApplication.Path,
viewModel.SelectedApplication.TitleName); viewModel.SelectedApplication.Name);
} }
} }
@@ -319,7 +307,7 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.StorageProvider, viewModel.StorageProvider,
NcaSectionType.Data, NcaSectionType.Data,
viewModel.SelectedApplication.Path, viewModel.SelectedApplication.Path,
viewModel.SelectedApplication.TitleName); viewModel.SelectedApplication.Name);
} }
} }
@@ -333,7 +321,7 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.StorageProvider, viewModel.StorageProvider,
NcaSectionType.Logo, NcaSectionType.Logo,
viewModel.SelectedApplication.Path, viewModel.SelectedApplication.Path,
viewModel.SelectedApplication.TitleName); viewModel.SelectedApplication.Name);
} }
} }
@@ -344,7 +332,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null) if (viewModel?.SelectedApplication != null)
{ {
ApplicationData selectedApplication = viewModel.SelectedApplication; ApplicationData selectedApplication = viewModel.SelectedApplication;
ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.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) if (viewModel?.SelectedApplication != null)
{ {
await viewModel.LoadApplication(viewModel.SelectedApplication.Path); await viewModel.LoadApplication(viewModel.SelectedApplication);
} }
} }
} }

View File

@@ -82,7 +82,7 @@
<TextBlock <TextBlock
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{Binding TitleName}" Text="{Binding Name}"
TextAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</Panel> </Panel>

View File

@@ -85,7 +85,7 @@
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
FontWeight="Bold" FontWeight="Bold"
Text="{Binding TitleName}" Text="{Binding Name}"
TextAlignment="Left" TextAlignment="Left"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
@@ -109,7 +109,7 @@
Spacing="5"> Spacing="5">
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Text="{Binding TitleId}" Text="{Binding Id, StringFormat=X16}"
TextAlignment="Left" TextAlignment="Left"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
@@ -126,17 +126,17 @@
Spacing="5"> Spacing="5">
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Text="{Binding TimePlayed}" Text="{Binding TimePlayedString}"
TextAlignment="Right" TextAlignment="Right"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Text="{Binding LastPlayed, Converter={helpers:NullableDateTimeConverter}}" Text="{Binding LastPlayedString, Converter={helpers:LocalizedNeverConverter}}"
TextAlignment="Right" TextAlignment="Right"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Text="{Binding FileSize}" Text="{Binding FileSizeString}"
TextAlignment="Right" TextAlignment="Right"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</StackPanel> </StackPanel>

View 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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,4 +1,5 @@
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels;
using System.IO; using System.IO;
namespace Ryujinx.Ava.UI.Models namespace Ryujinx.Ava.UI.Models
@@ -24,6 +25,9 @@ namespace Ryujinx.Ava.UI.Models
public string FileName => Path.GetFileName(ContainerPath); public string FileName => Path.GetFileName(ContainerPath);
public string Label =>
Path.GetExtension(FileName)?.ToLower() == ".xci" ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {FileName}" : FileName;
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled) public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
{ {
TitleId = titleId; TitleId = titleId;

View File

@@ -13,20 +13,19 @@ namespace Ryujinx.Ava.UI.Models.Generic
public int Compare(ApplicationData x, ApplicationData y) public int Compare(ApplicationData x, ApplicationData y)
{ {
var aValue = x.LastPlayed; DateTime aValue = DateTime.UnixEpoch, bValue = DateTime.UnixEpoch;
var bValue = y.LastPlayed;
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);
} }
} }
} }

View 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);
}
}
}

View File

@@ -4,7 +4,7 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.Ui.App.Common; using Ryujinx.Ui.App.Common;
using System; using Ryujinx.Ui.Common.Helper;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -38,26 +38,7 @@ namespace Ryujinx.Ava.UI.Models
public bool SizeAvailable { get; set; } public bool SizeAvailable { get; set; }
public string SizeString => GetSizeString(); public string SizeString => ValueFormatUtils.FormatFileSize(Size);
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 SaveModel(SaveDataInfo info) public SaveModel(SaveDataInfo info)
{ {
@@ -65,14 +46,14 @@ namespace Ryujinx.Ava.UI.Models
TitleId = info.ProgramId; TitleId = info.ProgramId;
UserId = info.UserId; UserId = info.UserId;
var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString); var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.ToUpper() == TitleIdString);
InGameList = appData != null; InGameList = appData != null;
if (InGameList) if (InGameList)
{ {
Icon = appData.Icon; Icon = appData.Icon;
Title = appData.TitleName; Title = appData.Name;
} }
else else
{ {

View File

@@ -8,7 +8,10 @@ namespace Ryujinx.Ava.UI.Models
public ApplicationControlProperty Control { get; } public ApplicationControlProperty Control { get; }
public string Path { get; } public string Path { get; }
public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(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) public TitleUpdateModel(ApplicationControlProperty control, string path)
{ {

View File

@@ -17,11 +17,12 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.Ui.App.Common;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Application = Avalonia.Application; using Application = Avalonia.Application;
using Path = System.IO.Path; using Path = System.IO.Path;
@@ -38,7 +39,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new(); private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
private string _search; private string _search;
private readonly ulong _titleId; private readonly ApplicationData _applicationData;
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
@@ -92,18 +93,25 @@ namespace Ryujinx.Ava.UI.ViewModels
public IStorageProvider StorageProvider; public IStorageProvider StorageProvider;
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
{ {
_virtualFileSystem = virtualFileSystem; _virtualFileSystem = virtualFileSystem;
_titleId = titleId; _applicationData = applicationData;
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
StorageProvider = desktop.MainWindow.StorageProvider; StorageProvider = desktop.MainWindow.StorageProvider;
} }
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json"); _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "dlc.json");
if (!File.Exists(_downloadableContentJsonPath))
{
_downloadableContentContainerList = new List<DownloadableContentContainer>();
Save();
}
try try
{ {
@@ -120,6 +128,9 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadDownloadableContents() private void LoadDownloadableContents()
{ {
// NOTE: Try to load downloadable contents from PFS first.
AddDownloadableContent(_applicationData.Path);
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList) foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
{ {
if (File.Exists(downloadableContentContainer.ContainerPath)) if (File.Exists(downloadableContentContainer.ContainerPath))
@@ -127,7 +138,11 @@ namespace Ryujinx.Ava.UI.ViewModels
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath); using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
PartitionFileSystem partitionFileSystem = new(); PartitionFileSystem partitionFileSystem = new();
partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
if (partitionFileSystem.Initialize(containerFile.AsStorage()).IsFailure())
{
continue;
}
_virtualFileSystem.ImportTickets(partitionFileSystem); _virtualFileSystem.ImportTickets(partitionFileSystem);
@@ -220,22 +235,34 @@ namespace Ryujinx.Ava.UI.ViewModels
foreach (var file in result) 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) if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
{ {
return; return true;
} }
using FileStream containerFile = File.OpenRead(path); using FileStream containerFile = File.OpenRead(path);
PartitionFileSystem partitionFileSystem = new(); IFileSystem partitionFileSystem;
partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
bool containsDownloadableContent = false; 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); _virtualFileSystem.ImportTickets(partitionFileSystem);
@@ -253,7 +280,7 @@ namespace Ryujinx.Ava.UI.ViewModels
if (nca.Header.ContentType == NcaContentType.PublicData) if (nca.Header.ContentType == NcaContentType.PublicData)
{ {
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId) if (nca.GetProgramIdBase() != _applicationData.IdBase)
{ {
break; break;
} }
@@ -265,14 +292,11 @@ namespace Ryujinx.Ava.UI.ViewModels
OnPropertyChanged(nameof(UpdateCount)); OnPropertyChanged(nameof(UpdateCount));
Sort(); Sort();
containsDownloadableContent = true; return true;
} }
} }
if (!containsDownloadableContent) return false;
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
}
} }
public void Remove(DownloadableContentModel model) public void Remove(DownloadableContentModel model)

View File

@@ -95,7 +95,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private bool _canUpdate = true; private bool _canUpdate = true;
private Cursor _cursor; private Cursor _cursor;
private string _title; private string _title;
private string _currentEmulatedGamePath; private ApplicationData _currentApplicationData;
private readonly AutoResetEvent _rendererWaitEvent; private readonly AutoResetEvent _rendererWaitEvent;
private WindowState _windowState; private WindowState _windowState;
private double _windowWidth; private double _windowWidth;
@@ -106,7 +106,6 @@ namespace Ryujinx.Ava.UI.ViewModels
public ApplicationData ListSelectedApplication; public ApplicationData ListSelectedApplication;
public ApplicationData GridSelectedApplication; public ApplicationData GridSelectedApplication;
private string TitleName { get; set; }
internal AppHost AppHost { get; set; } internal AppHost AppHost { get; set; }
public MainWindowViewModel() public MainWindowViewModel()
@@ -930,21 +929,20 @@ namespace Ryujinx.Ava.UI.ViewModels
return SortMode switch return SortMode switch
{ {
#pragma warning disable IDE0055 // Disable formatting #pragma warning disable IDE0055 // Disable formatting
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending), ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Name)
ApplicationSort.FileSize => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileSizeBytes) : SortExpressionComparer<ApplicationData>.Descending(app => app.Name),
: 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.Favorite => !IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Favorite)
: SortExpressionComparer<ApplicationData>.Descending(app => app.Favorite),
ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer) ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer)
: SortExpressionComparer<ApplicationData>.Descending(app => app.Developer), : SortExpressionComparer<ApplicationData>.Descending(app => app.Developer),
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
ApplicationSort.TotalTimePlayed => new TimePlayedSortComparer(IsAscending),
ApplicationSort.FileType => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileExtension) ApplicationSort.FileType => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileExtension)
: SortExpressionComparer<ApplicationData>.Descending(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) ApplicationSort.Path => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Path)
: SortExpressionComparer<ApplicationData>.Descending(app => app.Path), : SortExpressionComparer<ApplicationData>.Descending(app => app.Path),
ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Favorite)
: SortExpressionComparer<ApplicationData>.Descending(app => app.Favorite),
_ => null, _ => null,
#pragma warning restore IDE0055 #pragma warning restore IDE0055
}; };
@@ -969,7 +967,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
if (arg is ApplicationData app) 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; return false;
@@ -1098,7 +1096,7 @@ namespace Ryujinx.Ava.UI.ViewModels
IsLoadingIndeterminate = false; IsLoadingIndeterminate = false;
break; break;
case LoadState.Loaded: case LoadState.Loaded:
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName); LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
IsLoadingIndeterminate = true; IsLoadingIndeterminate = true;
CacheLoadStatus = ""; CacheLoadStatus = "";
break; break;
@@ -1118,7 +1116,7 @@ namespace Ryujinx.Ava.UI.ViewModels
IsLoadingIndeterminate = false; IsLoadingIndeterminate = false;
break; break;
case ShaderCacheLoadingState.Loaded: case ShaderCacheLoadingState.Loaded:
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName); LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
IsLoadingIndeterminate = true; IsLoadingIndeterminate = true;
CacheLoadStatus = ""; CacheLoadStatus = "";
break; break;
@@ -1169,13 +1167,13 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
UserChannelPersistence.ShouldRestart = false; UserChannelPersistence.ShouldRestart = false;
await LoadApplication(_currentEmulatedGamePath); await LoadApplication(_currentApplicationData);
} }
else else
{ {
// Otherwise, clear state. // Otherwise, clear state.
UserChannelPersistence = new UserChannelPersistence(); UserChannelPersistence = new UserChannelPersistence();
_currentEmulatedGamePath = null; _currentApplicationData = null;
} }
} }
@@ -1452,7 +1450,12 @@ namespace Ryujinx.Ava.UI.ViewModels
if (result.Count > 0) 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) 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) if (AppHost != null)
{ {
@@ -1490,7 +1499,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Logger.RestartTime(); Logger.RestartTime();
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language); SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id);
PrepareLoadScreen(); PrepareLoadScreen();
@@ -1499,7 +1508,8 @@ namespace Ryujinx.Ava.UI.ViewModels
AppHost = new AppHost( AppHost = new AppHost(
RendererHostControl, RendererHostControl,
InputManager, InputManager,
path, application.Path,
application.Id,
VirtualFileSystem, VirtualFileSystem,
ContentManager, ContentManager,
AccountManager, AccountManager,
@@ -1517,17 +1527,17 @@ namespace Ryujinx.Ava.UI.ViewModels
CanUpdate = false; 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); 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); SwitchToRenderer(startFullscreen);
_currentEmulatedGamePath = path; _currentApplicationData = application;
Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" }; Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
gameThread.Start(); gameThread.Start();
@@ -1549,13 +1559,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
{ {
if (appMetadata.LastPlayed.HasValue) appMetadata.UpdatePostGame();
{
double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds;
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
}
appMetadata.LastPlayed = DateTime.UtcNow;
}); });
} }

View File

@@ -1,4 +1,3 @@
using Avalonia;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
@@ -8,6 +7,7 @@ using LibHac.Fs;
using LibHac.Fs.Fsa; using LibHac.Fs.Fsa;
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.Ns; using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
@@ -17,12 +17,16 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.Ui.App.Common; using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common.Configuration;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Application = Avalonia.Application;
using ContentType = LibHac.Ncm.ContentType;
using Path = System.IO.Path; using Path = System.IO.Path;
using SpanHelpers = LibHac.Common.SpanHelpers; using SpanHelpers = LibHac.Common.SpanHelpers;
@@ -33,7 +37,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public TitleUpdateMetadata TitleUpdateWindowData; public TitleUpdateMetadata TitleUpdateWindowData;
public readonly string TitleUpdateJsonPath; public readonly string TitleUpdateJsonPath;
private VirtualFileSystem VirtualFileSystem { get; } private VirtualFileSystem VirtualFileSystem { get; }
private ulong TitleId { get; } private ApplicationData ApplicationData { get; }
private AvaloniaList<TitleUpdateModel> _titleUpdates = new(); private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
private AvaloniaList<object> _views = new(); private AvaloniaList<object> _views = new();
@@ -73,18 +77,18 @@ namespace Ryujinx.Ava.UI.ViewModels
public IStorageProvider StorageProvider; public IStorageProvider StorageProvider;
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
{ {
VirtualFileSystem = virtualFileSystem; VirtualFileSystem = virtualFileSystem;
TitleId = titleId; ApplicationData = applicationData;
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
StorageProvider = desktop.MainWindow.StorageProvider; StorageProvider = desktop.MainWindow.StorageProvider;
} }
TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json"); TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdString, "updates.json");
try try
{ {
@@ -92,7 +96,7 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
catch 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 TitleUpdateWindowData = new TitleUpdateMetadata
{ {
@@ -108,6 +112,9 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadUpdates() private void LoadUpdates()
{ {
// Try to load updates from PFS first
AddUpdate(ApplicationData.Path, true);
foreach (string path in TitleUpdateWindowData.Paths) foreach (string path in TitleUpdateWindowData.Paths)
{ {
AddUpdate(path); AddUpdate(path);
@@ -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)) if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
{ {
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
using FileStream file = new(path, FileMode.Open, FileAccess.Read); using FileStream file = new(path, FileMode.Open, FileAccess.Read);
IFileSystem pfs;
try try
{ {
var pfs = new PartitionFileSystem(); if (Path.GetExtension(path).ToLower() == ".xci")
pfs.Initialize(file.AsStorage()).ThrowIfFailure(); {
(Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0); pfs = new Xci(VirtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
}
else
{
var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
pfs = pfsTemp;
}
Dictionary<ulong, ContentCollection> updates = pfs.GetUpdateData(VirtualFileSystem, checkLevel);
Nca patchNca = null;
Nca controlNca = null;
if (updates.TryGetValue(ApplicationData.Id, out ContentCollection content))
{
patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program);
controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control);
}
if (controlNca != null && patchNca != null) if (controlNca != null && patchNca != null)
{ {
@@ -186,10 +217,13 @@ namespace Ryujinx.Ava.UI.ViewModels
TitleUpdates.Add(new TitleUpdateModel(controlData, path)); TitleUpdates.Add(new TitleUpdateModel(controlData, path));
} }
else else
{
if (!ignoreNotFound)
{ {
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
} }
} }
}
catch (Exception ex) catch (Exception ex)
{ {
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, path))); Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, path)));

View File

@@ -10,6 +10,7 @@ using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Modules; using Ryujinx.Modules;
using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common; using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
@@ -131,7 +132,14 @@ namespace Ryujinx.Ava.UI.Views.Main
if (!string.IsNullOrEmpty(contentPath)) 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);
} }
} }

View File

@@ -104,7 +104,7 @@
Content="{locale:Locale GameListHeaderApplication}" Content="{locale:Locale GameListHeaderApplication}"
GroupName="Sort" GroupName="Sort"
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}" IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
Tag="Title" /> Tag="Application" />
<RadioButton <RadioButton
Checked="Sort_Checked" Checked="Sort_Checked"
Content="{locale:Locale GameListHeaderDeveloper}" Content="{locale:Locale GameListHeaderDeveloper}"

View File

@@ -1,9 +1,11 @@
using Avalonia.Collections; using Avalonia.Collections;
using LibHac.Tools.FsSystem;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.Ui.App.Common; using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common.Configuration;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
@@ -34,9 +36,12 @@ namespace Ryujinx.Ava.UI.Windows
public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath) public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath)
{ {
LoadedCheats = new AvaloniaList<CheatsList>(); LoadedCheats = new AvaloniaList<CheatsList>();
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper()); Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper());
BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath); BuildId = ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath);
InitializeComponent(); InitializeComponent();

View File

@@ -97,7 +97,7 @@
MaxLines="2" MaxLines="2"
TextWrapping="Wrap" TextWrapping="Wrap"
TextTrimming="CharacterEllipsis" TextTrimming="CharacterEllipsis"
Text="{Binding FileName}" /> Text="{Binding Label}" />
<TextBlock <TextBlock
Grid.Column="1" Grid.Column="1"
Margin="10 0" Margin="10 0"

View File

@@ -7,9 +7,9 @@ using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
using System.Threading.Tasks; using System.Threading.Tasks;
using Button = Avalonia.Controls.Button;
namespace Ryujinx.Ava.UI.Windows namespace Ryujinx.Ava.UI.Windows
{ {
@@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent(); InitializeComponent();
} }
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId) public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
{ {
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, titleId); DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, applicationData);
InitializeComponent(); InitializeComponent();
} }
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
{ {
ContentDialog contentDialog = new() ContentDialog contentDialog = new()
{ {
PrimaryButtonText = "", PrimaryButtonText = "",
SecondaryButtonText = "", SecondaryButtonText = "",
CloseButtonText = "", CloseButtonText = "",
Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId), Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationData),
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16")), Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdString),
}; };
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>()); Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());

View File

@@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Threading; using Avalonia.Threading;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using LibHac.Tools.FsSystem;
using Ryujinx.Ava.Common; using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input; using Ryujinx.Ava.Input;
@@ -23,7 +24,6 @@ using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
using System; using System;
using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -139,9 +139,7 @@ namespace Ryujinx.Ava.UI.Windows
{ {
ViewModel.SelectedIcon = args.Application.Icon; ViewModel.SelectedIcon = args.Application.Icon;
string path = new FileInfo(args.Application.Path).FullName; ViewModel.LoadApplication(args.Application).Wait();
ViewModel.LoadApplication(path).Wait();
} }
args.Handled = true; args.Handled = true;
@@ -190,7 +188,11 @@ namespace Ryujinx.Ava.UI.Windows
LibHacHorizonManager.InitializeBcatServer(); LibHacHorizonManager.InitializeBcatServer();
LibHacHorizonManager.InitializeSystemClients(); 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 // Save data created before we supported extra data in directory save data will not work properly if
// given empty extra data. Luckily some of that extra data can be created using the data from the // given empty extra data. Luckily some of that extra data can be created using the data from the
@@ -297,7 +299,12 @@ namespace Ryujinx.Ava.UI.Windows
{ {
_deferLoad = false; _deferLoad = false;
ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait(); ApplicationData applicationData = new()
{
Path = _launchPath,
};
ViewModel.LoadApplication(applicationData, _startFullscreen).Wait();
} }
} }
else else

View File

@@ -7,15 +7,15 @@ using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
using System.Threading.Tasks; using System.Threading.Tasks;
using Button = Avalonia.Controls.Button;
namespace Ryujinx.Ava.UI.Windows namespace Ryujinx.Ava.UI.Windows
{ {
public partial class TitleUpdateWindow : UserControl public partial class TitleUpdateWindow : UserControl
{ {
public TitleUpdateViewModel ViewModel; public readonly TitleUpdateViewModel ViewModel;
public TitleUpdateWindow() public TitleUpdateWindow()
{ {
@@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent(); 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(); InitializeComponent();
} }
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
{ {
ContentDialog contentDialog = new() ContentDialog contentDialog = new()
{ {
PrimaryButtonText = "", PrimaryButtonText = "",
SecondaryButtonText = "", SecondaryButtonText = "",
CloseButtonText = "", CloseButtonText = "",
Content = new TitleUpdateWindow(virtualFileSystem, titleId), Content = new TitleUpdateWindow(virtualFileSystem, applicationData),
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, titleName, titleId.ToString("X16")), Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdString),
}; };
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>()); Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());

View 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;
}
}
}
}
}
}

View File

@@ -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/> /// <inheritdoc/>
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags) public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
{ {
@@ -736,6 +721,24 @@ namespace Ryujinx.Cpu.AppleHv
return (int)(vaSpan / PageSize); 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/> /// <inheritdoc/>
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
{ {

View File

@@ -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) private ulong GetPhysicalAddressInternal(ulong va)
{ {
return PteToPa(_pageTable.Read<ulong>((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask); return PteToPa(_pageTable.Read<ulong>((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask);
} }
/// <inheritdoc/>
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
{
// TODO
}
/// <inheritdoc/> /// <inheritdoc/>
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) 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. /// Disposes of resources used by the memory manager.
/// </summary> /// </summary>
protected override void Destroy() => _pageTable.Dispose(); 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
} }
} }

View File

@@ -615,6 +615,12 @@ namespace Ryujinx.Cpu.Jit
return (int)(vaSpan / PageSize); return (int)(vaSpan / PageSize);
} }
/// <inheritdoc/>
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
{
// TODO
}
/// <inheritdoc/> /// <inheritdoc/>
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
{ {

View 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;
}
}
}

View 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;
}
}
}

View File

@@ -203,15 +203,17 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
/// <inheritdoc/> /// <inheritdoc/>
protected override Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission) protected override Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission)
{ {
// TODO. _cpuMemory.Reprotect(address, pagesCount * PageSize, permission.Convert());
return Result.Success; return Result.Success;
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission) protected override Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission)
{ {
// TODO. // TODO: Flush JIT cache.
return Result.Success;
return Reprotect(address, pagesCount, permission);
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -1255,7 +1255,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
if ((oldPermission & KMemoryPermission.Execute) != 0) if ((oldPermission & KMemoryPermission.Execute) != 0)
{ {
result = ReprotectWithAttributes(address, pagesCount, permission); result = ReprotectAndFlush(address, pagesCount, permission);
} }
else else
{ {
@@ -3036,13 +3036,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
protected abstract Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission); protected abstract Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission);
/// <summary> /// <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> /// </summary>
/// <param name="address">Virtual address of the region to have the permission changes</param> /// <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="pagesCount">Number of pages to have their permissions changed</param>
/// <param name="permission">New permission</param> /// <param name="permission">New permission</param>
/// <returns>Result of the permission change operation</returns> /// <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> /// <summary>
/// Alerts the memory tracking that a given region has been read from or written to. /// Alerts the memory tracking that a given region has been read from or written to.

View File

@@ -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,
}
}

View File

@@ -2,21 +2,31 @@
using LibHac.Common; using LibHac.Common;
using LibHac.Fs; using LibHac.Fs;
using LibHac.Fs.Fsa; using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Loader; using LibHac.Loader;
using LibHac.Ncm; using LibHac.Ncm;
using LibHac.Ns; using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using LibHac.Tools.Ncm;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using ApplicationId = LibHac.Ncm.ApplicationId; using ApplicationId = LibHac.Ncm.ApplicationId;
using ContentType = LibHac.Ncm.ContentType;
using Path = System.IO.Path;
namespace Ryujinx.HLE.Loaders.Processes.Extensions namespace Ryujinx.HLE.Loaders.Processes.Extensions
{ {
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) public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
{ {
// Extract RomFs and ExeFs from NCA. // Extract RomFs and ExeFs from NCA.
@@ -47,7 +57,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
nacpData = controlNca.GetNacp(device); nacpData = controlNca.GetNacp(device);
} }
/* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" 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. // Load program 0 control NCA as we are going to need it for display version.
(_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); (_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
@@ -86,6 +96,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return processResult; return processResult;
} }
public static ulong GetProgramIdBase(this Nca nca)
{
return nca.Header.TitleId & ~0x1FFFUL;
}
public static int GetProgramIndex(this Nca nca) public static int GetProgramIndex(this Nca nca)
{ {
return (int)(nca.Header.TitleId & 0xF); return (int)(nca.Header.TitleId & 0xF);
@@ -96,6 +111,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return nca.Header.ContentType == NcaContentType.Program; return nca.Header.ContentType == NcaContentType.Program;
} }
public static bool IsMain(this Nca nca)
{
return nca.IsProgram() && !nca.IsPatch();
}
public static bool IsPatch(this Nca nca) public static bool IsPatch(this Nca nca)
{ {
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
@@ -108,6 +128,56 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return nca.Header.ContentType == NcaContentType.Control; return nca.Header.ContentType == NcaContentType.Control;
} }
public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath)
{
updatePath = "(unknown)";
// Load Update NCAs.
Nca updatePatchNca = null;
Nca updateControlNca = null;
// Clear the program index part.
ulong titleIdBase = mainNca.GetProgramIdBase();
// Load update information if exists.
string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json");
if (File.Exists(titleUpdateMetadataPath))
{
updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
if (File.Exists(updatePath))
{
var updateFile = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
IFileSystem updatePartitionFileSystem;
if (Path.GetExtension(updatePath).ToLower() == ".xci")
{
updatePartitionFileSystem = new Xci(fileSystem.KeySet, updateFile.AsStorage()).OpenPartition(XciPartitionType.Secure);
}
else
{
PartitionFileSystem pfsTemp = new();
pfsTemp.Initialize(updateFile.AsStorage()).ThrowIfFailure();
updatePartitionFileSystem = pfsTemp;
}
foreach ((ulong updateTitleId, ContentCollection content) in updatePartitionFileSystem.GetUpdateData(fileSystem, checkLevel))
{
if ((updateTitleId & ~0x1FFFUL) != titleIdBase)
{
continue;
}
updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex);
updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex);
break;
}
}
}
return (updatePatchNca, updateControlNca);
}
public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null) public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null)
{ {
IFileSystem exeFs = null; IFileSystem exeFs = null;
@@ -172,5 +242,31 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return nacpData; return nacpData;
} }
public static Cnmt GetCnmt(this Nca cnmtNca, IntegrityCheckLevel checkLevel, ContentMetaType metaType)
{
string path = $"/{metaType}_{cnmtNca.Header.TitleId:x16}.cnmt";
using var cnmtFile = new UniqueRef<IFile>();
try
{
Result result = cnmtNca.OpenFileSystem(0, checkLevel)
.OpenFile(ref cnmtFile.Ref, path.ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
return new Cnmt(cnmtFile.Release().AsStream());
}
}
catch (HorizonResultException ex)
{
if (!ResultFs.PathNotFound.Includes(ex.ResultValue))
{
Logger.Warning?.Print(LogClass.Application, $"Failed get cnmt for '{cnmtNca.Header.TitleId:x16}' from nca: {ex.Message}");
}
}
return null;
}
} }
} }

View File

@@ -1,26 +1,87 @@
using LibHac.Common; using LibHac.Common;
using LibHac.Common.Keys;
using LibHac.Fs; using LibHac.Fs;
using LibHac.Fs.Fsa; using LibHac.Fs.Fsa;
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.Ncm;
using LibHac.Tools.Fs; using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using LibHac.Tools.Ncm;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using ContentType = LibHac.Ncm.ContentType;
namespace Ryujinx.HLE.Loaders.Processes.Extensions namespace Ryujinx.HLE.Loaders.Processes.Extensions
{ {
public static class PartitionFileSystemExtensions public static class PartitionFileSystemExtensions
{ {
private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
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 TMetaData : PartitionFileSystemMetaCore<TFormat, THeader, TEntry>, new()
where TFormat : IPartitionFileSystemFormat where TFormat : IPartitionFileSystemFormat
where THeader : unmanaged, IPartitionFileSystemHeader where THeader : unmanaged, IPartitionFileSystemHeader
@@ -35,30 +96,21 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
try 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. if (titleId == 0)
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
{ {
Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath); foreach ((ulong _, ContentCollection content) in applications)
if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
{ {
continue; 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;
} }
if (nca.IsPatch())
{
patchNca = nca;
} }
else if (nca.IsProgram()) else if (applications.TryGetValue(titleId, out ContentCollection content))
{ {
mainNca = 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);
else if (nca.IsControl())
{
controlNca = nca;
}
} }
ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure(); ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure();
@@ -79,54 +131,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return (false, ProcessResult.Failed); return (false, ProcessResult.Failed);
} }
// Load Update NCAs. (Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _);
Nca updatePatchNca = null;
Nca updateControlNca = null;
if (ulong.TryParse(mainNca.Header.TitleId.ToString("x16"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase))
{
// Clear the program index part.
titleIdBase &= ~0xFUL;
// Load update information if exists.
string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
if (File.Exists(titleUpdateMetadataPath))
{
string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
if (File.Exists(updatePath))
{
PartitionFileSystem updatePartitionFileSystem = new();
updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure();
device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);
// TODO: This should use CNMT NCA instead.
foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca"))
{
Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath);
if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
{
continue;
}
if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16"))
{
break;
}
if (nca.IsProgram())
{
updatePatchNca = nca;
}
else if (nca.IsControl())
{
updateControlNca = nca;
}
}
}
}
}
if (updatePatchNca != null) if (updatePatchNca != null)
{ {
@@ -168,18 +173,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return (true, mainNca.Load(device, patchNca, controlNca)); return (true, mainNca.Load(device, patchNca, controlNca));
} }
errorMessage = "Unable to load: Could not find Main NCA"; errorMessage = $"Unable to load: Could not find Main NCA for title \"{titleId:X16}\"";
return (false, ProcessResult.Failed); 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>(); using var ncaFile = new UniqueRef<IFile>();
fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure(); fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
return new Nca(device.Configuration.VirtualFileSystem.KeySet, ncaFile.Release().AsStorage()); return new Nca(keySet, ncaFile.Release().AsStorage());
} }
} }
} }

View File

@@ -32,7 +32,7 @@ namespace Ryujinx.HLE.Loaders.Processes
_processesByPid = new ConcurrentDictionary<ulong, ProcessResult>(); _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); FileStream stream = new(path, FileMode.Open, FileAccess.Read);
Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage()); Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
@@ -44,7 +44,7 @@ namespace Ryujinx.HLE.Loaders.Processes
return false; return false;
} }
(bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, out string errorMessage); (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, titleId, out string errorMessage);
if (!success) if (!success)
{ {
@@ -66,13 +66,13 @@ namespace Ryujinx.HLE.Loaders.Processes
return false; return false;
} }
public bool LoadNsp(string path) public bool LoadNsp(string path, ulong titleId)
{ {
FileStream file = new(path, FileMode.Open, FileAccess.Read); FileStream file = new(path, FileMode.Open, FileAccess.Read);
PartitionFileSystem partitionFileSystem = new(); PartitionFileSystem partitionFileSystem = new();
partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure(); partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure();
(bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage); (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, titleId, out string errorMessage);
if (processResult.ProcessId == 0) if (processResult.ProcessId == 0)
{ {

View File

@@ -42,15 +42,14 @@ namespace Ryujinx.HLE.Loaders.Processes
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) 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; continue;
} }
ulong currentProgramId = nca.Header.TitleId; ulong currentMainProgramId = nca.GetProgramIdBase();
ulong currentMainProgramId = currentProgramId & ~0xFFFul;
if (applicationId == 0 && currentMainProgramId != 0) if (applicationId == 0 && currentMainProgramId != 0)
{ {
@@ -67,7 +66,7 @@ namespace Ryujinx.HLE.Loaders.Processes
break; break;
} }
hasIndex[(int)(currentProgramId & 0xF)] = true; hasIndex[nca.GetProgramIndex()] = true;
} }
if (programCount == 0) if (programCount == 0)

View File

@@ -72,9 +72,9 @@ namespace Ryujinx.HLE
return Processes.LoadUnpackedNca(exeFsDir, romFsFile); 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) public bool LoadNca(string ncaFile)
@@ -82,9 +82,9 @@ namespace Ryujinx.HLE
return Processes.LoadNca(ncaFile); return Processes.LoadNca(ncaFile);
} }
public bool LoadNsp(string nspFile) public bool LoadNsp(string nspFile, ulong titleId = 0)
{ {
return Processes.LoadNsp(nspFile); return Processes.LoadNsp(nspFile, titleId);
} }
public bool LoadProgram(string fileName) public bool LoadProgram(string fileName)

View File

@@ -455,6 +455,11 @@ namespace Ryujinx.Memory
return _pageTable.Read(va) + (nuint)(va & PageMask); return _pageTable.Read(va) + (nuint)(va & PageMask);
} }
/// <inheritdoc/>
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
{
}
/// <inheritdoc/> /// <inheritdoc/>
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
{ {

View File

@@ -104,6 +104,12 @@ namespace Ryujinx.Memory
/// <returns>True if the data was changed, false otherwise</returns> /// <returns>True if the data was changed, false otherwise</returns>
bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data); 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) void Fill(ulong va, ulong size, byte value)
{ {
const int MaxChunkSize = 1 << 24; 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> /// <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); 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> /// <summary>
/// Reprotect a region of virtual memory for tracking. /// Reprotect a region of virtual memory for tracking.
/// </summary> /// </summary>

View File

@@ -102,6 +102,11 @@ namespace Ryujinx.Tests.Memory
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
{
throw new NotImplementedException();
}
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
{ {
OnProtect?.Invoke(va, size, protection); OnProtect?.Invoke(va, size, protection);

View File

@@ -9,8 +9,9 @@ using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.Ui.Common.Helper;
using System; using System;
using System.Globalization;
using System.IO; using System.IO;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@@ -20,35 +21,28 @@ namespace Ryujinx.Ui.App.Common
{ {
public bool Favorite { get; set; } public bool Favorite { get; set; }
public byte[] Icon { get; set; } public byte[] Icon { get; set; }
public string TitleName { get; set; } public string Name { get; set; } = "Unknown";
public string TitleId { get; set; } public ulong Id { get; set; }
public string Developer { get; set; } public string Developer { get; set; } = "Unknown";
public string Version { get; set; } public string Version { get; set; } = "0";
public string TimePlayed { get; set; } public TimeSpan TimePlayed { get; set; }
public double TimePlayedNum { get; set; }
public DateTime? LastPlayed { get; set; } public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; } public string FileExtension { get; set; }
public string FileSize { get; set; } public long FileSize { get; set; }
public double FileSizeBytes { get; set; }
public string Path { get; set; } public string Path { get; set; }
public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; } public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; }
[JsonIgnore] public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
public string LastPlayedString
{
get
{
if (!LastPlayed.HasValue)
{
// TODO: maybe put localized string here instead of just "Never"
return "Never";
}
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); using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
@@ -117,7 +111,7 @@ namespace Ryujinx.Ui.App.Common
return string.Empty; 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) if (updatePatchNca != null)
{ {

File diff suppressed because it is too large Load Diff

View File

@@ -7,13 +7,45 @@ namespace Ryujinx.Ui.App.Common
{ {
public string Title { get; set; } public string Title { get; set; }
public bool Favorite { 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")] [JsonPropertyName("last_played_utc")]
public DateTime? LastPlayed { get; set; } = null; public DateTime? LastPlayed { get; set; } = null;
[JsonPropertyName("time_played")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public double TimePlayedOld { get; set; }
[JsonPropertyName("last_played")] [JsonPropertyName("last_played")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string LastPlayedOld { get; set; } 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));
}
} }
} }

View File

@@ -784,7 +784,7 @@ namespace Ryujinx.Ui.Common.Configuration
EnableDiscordIntegration.Value = true; EnableDiscordIntegration.Value = true;
CheckUpdatesOnStart.Value = true; CheckUpdatesOnStart.Value = true;
ShowConfirmExit.Value = true; ShowConfirmExit.Value = true;
HideCursor.Value = HideCursorMode.Never; HideCursor.Value = HideCursorMode.OnIdle;
Graphics.EnableVsync.Value = true; Graphics.EnableVsync.Value = true;
Graphics.EnableShaderCache.Value = true; Graphics.EnableShaderCache.Value = true;
Graphics.EnableTextureRecompression.Value = false; Graphics.EnableTextureRecompression.Value = false;

View File

@@ -30,7 +30,7 @@ namespace Ryujinx.Ui.Common.Helper
graphic.DrawImage(image, 0, 0, 128, 128); graphic.DrawImage(image, 0, 0, 128, 128);
SaveBitmapAsIcon(bitmap, iconPath); 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.StringData.NameString = cleanedAppName;
shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk")); shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk"));
} }
@@ -46,16 +46,16 @@ namespace Ryujinx.Ui.Common.Helper
image.SaveAsPng(iconPath); image.SaveAsPng(iconPath);
using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop")); 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")] [SupportedOSPlatform("macos")]
private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName) 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"); var plistFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.plist");
// Macos .App folder // 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"); string scriptFolderPath = Path.Combine(contentFolderPath, "MacOS");
if (!Directory.Exists(scriptFolderPath)) if (!Directory.Exists(scriptFolderPath))
@@ -69,7 +69,7 @@ namespace Ryujinx.Ui.Common.Helper
using StreamWriter scriptFile = new(scriptPath); using StreamWriter scriptFile = new(scriptPath);
scriptFile.WriteLine("#!/bin/sh"); scriptFile.WriteLine("#!/bin/sh");
scriptFile.WriteLine(GetArgsString(basePath, appFilePath)); scriptFile.WriteLine($"{basePath} {GetArgsString(appFilePath)}");
// Set execute permission // Set execute permission
FileInfo fileInfo = new(scriptPath); 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."); 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 // args are first defined as a list, for easier adjustments in the future
var argsList = new List<string> var argsList = new List<string>();
{
basePath,
};
if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg)) if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg))
{ {
@@ -141,7 +138,6 @@ namespace Ryujinx.Ui.Common.Helper
argsList.Add($"\"{appFilePath}\""); argsList.Add($"\"{appFilePath}\"");
return String.Join(" ", argsList); return String.Join(" ", argsList);
} }

View 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
}
}

View File

@@ -5,7 +5,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
namespace Ryujinx.Common.SystemInfo namespace Ryujinx.Ui.Common.SystemInfo
{ {
[SupportedOSPlatform("linux")] [SupportedOSPlatform("linux")]
class LinuxSystemInfo : SystemInfo class LinuxSystemInfo : SystemInfo

View File

@@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Text; using System.Text;
namespace Ryujinx.Common.SystemInfo namespace Ryujinx.Ui.Common.SystemInfo
{ {
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
partial class MacOSSystemInfo : SystemInfo partial class MacOSSystemInfo : SystemInfo

View File

@@ -1,10 +1,11 @@
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Ui.Common.Helper;
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
using System.Text; using System.Text;
namespace Ryujinx.Common.SystemInfo namespace Ryujinx.Ui.Common.SystemInfo
{ {
public class SystemInfo public class SystemInfo
{ {
@@ -20,13 +21,13 @@ namespace Ryujinx.Common.SystemInfo
CpuName = "Unknown"; 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() public void Print()
{ {
Logger.Notice.Print(LogClass.Application, $"Operating System: {OsDescription}"); Logger.Notice.Print(LogClass.Application, $"Operating System: {OsDescription}");
Logger.Notice.Print(LogClass.Application, $"CPU: {CpuName}"); 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() public static SystemInfo Gather()

View File

@@ -4,7 +4,7 @@ using System.Management;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
namespace Ryujinx.Common.SystemInfo namespace Ryujinx.Ui.Common.SystemInfo
{ {
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
partial class WindowsSystemInfo : SystemInfo partial class WindowsSystemInfo : SystemInfo

View File

@@ -3,14 +3,15 @@ using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.SystemInfo;
using Ryujinx.Common.SystemInterop; using Ryujinx.Common.SystemInterop;
using Ryujinx.Modules; using Ryujinx.Modules;
using Ryujinx.SDL2.Common; using Ryujinx.SDL2.Common;
using Ryujinx.Ui; using Ryujinx.Ui;
using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common; using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
using Ryujinx.Ui.Common.SystemInfo;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using System; using System;
@@ -332,7 +333,12 @@ namespace Ryujinx
if (CommandLineState.LaunchPathArg != null) 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)) if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))

View File

@@ -1,4 +1,5 @@
using Gtk; using Gtk;
using Ryujinx.Ui.Common.Helper;
using System; using System;
namespace Ryujinx.Ui.Helper namespace Ryujinx.Ui.Helper
@@ -7,88 +8,26 @@ namespace Ryujinx.Ui.Helper
{ {
public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b) public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b)
{ {
static string ReverseFormat(string time) TimeSpan aTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(a, 5).ToString());
{ TimeSpan bTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(b, 5).ToString());
if (time == "Never")
{
return "00";
}
var numbers = time.Split(new char[] { 'd', 'h', 'm' }); return TimeSpan.Compare(aTimeSpan, bTimeSpan);
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));
} }
public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b) public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b)
{ {
string aValue = model.GetValue(a, 6).ToString(); DateTime aDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(a, 6).ToString());
string bValue = model.GetValue(b, 6).ToString(); DateTime bDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(b, 6).ToString());
if (aValue == "Never") return DateTime.Compare(aDateTime, bDateTime);
{
aValue = DateTime.UnixEpoch.ToString();
}
if (bValue == "Never")
{
bValue = DateTime.UnixEpoch.ToString();
}
return DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue));
} }
public static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b) public static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b)
{ {
string aValue = model.GetValue(a, 8).ToString(); long aSize = ValueFormatUtils.ParseFileSize(model.GetValue(a, 8).ToString());
string bValue = model.GetValue(b, 8).ToString(); long bSize = ValueFormatUtils.ParseFileSize(model.GetValue(b, 8).ToString());
if (aValue[^3..] == "GiB") return aSize.CompareTo(bSize);
{
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;
}
} }
} }
} }

View File

@@ -39,6 +39,7 @@ using Silk.NET.Vulkan;
using SPB.Graphics.Vulkan; using SPB.Graphics.Vulkan;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
@@ -70,7 +71,7 @@ namespace Ryujinx.Ui
private bool _gameLoaded; private bool _gameLoaded;
private bool _ending; private bool _ending;
private string _currentEmulatedGamePath = null; private ApplicationData _currentApplicationData = null;
private string _lastScannedAmiiboId = ""; private string _lastScannedAmiiboId = "";
private bool _lastScannedAmiiboShowAll = false; private bool _lastScannedAmiiboShowAll = false;
@@ -181,8 +182,12 @@ namespace Ryujinx.Ui
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile); _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile);
_userChannelPersistence = new UserChannelPersistence(); _userChannelPersistence = new UserChannelPersistence();
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
// Instantiate GUI objects. // Instantiate GUI objects.
_applicationLibrary = new ApplicationLibrary(_virtualFileSystem); _applicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel);
_uiHandler = new GtkHostUiHandler(this); _uiHandler = new GtkHostUiHandler(this);
_deviceExitStatus = new AutoResetEvent(false); _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(); SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
@@ -858,7 +863,7 @@ namespace Ryujinx.Ui
case ".xci": case ".xci":
Logger.Info?.Print(LogClass.Application, "Loading as XCI."); Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
return _emulationContext.LoadXci(path); return _emulationContext.LoadXci(path, titleId);
case ".nca": case ".nca":
Logger.Info?.Print(LogClass.Application, "Loading as NCA."); Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
@@ -867,7 +872,7 @@ namespace Ryujinx.Ui
case ".pfs0": case ".pfs0":
Logger.Info?.Print(LogClass.Application, "Loading as NSP."); Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
return _emulationContext.LoadNsp(path); return _emulationContext.LoadNsp(path, titleId);
default: default:
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew."); Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
try try
@@ -888,7 +893,7 @@ namespace Ryujinx.Ui
return false; return false;
} }
public void RunApplication(string path, bool startFullscreen = false) public void RunApplication(ApplicationData application, bool startFullscreen = false)
{ {
if (_gameLoaded) if (_gameLoaded)
{ {
@@ -910,14 +915,14 @@ namespace Ryujinx.Ui
bool isFirmwareTitle = false; bool isFirmwareTitle = false;
if (path.StartsWith("@SystemContent")) if (application.Path.StartsWith("@SystemContent"))
{ {
path = VirtualFileSystem.SwitchPathToSystemPath(path); application.Path = VirtualFileSystem.SwitchPathToSystemPath(application.Path);
isFirmwareTitle = true; isFirmwareTitle = true;
} }
if (!LoadApplication(path, isFirmwareTitle)) if (!LoadApplication(application.Path, application.Id, isFirmwareTitle))
{ {
_emulationContext.Dispose(); _emulationContext.Dispose();
SwitchToGameTable(); SwitchToGameTable();
@@ -927,7 +932,7 @@ namespace Ryujinx.Ui
SetupProgressUiHandlers(); SetupProgressUiHandlers();
_currentEmulatedGamePath = path; _currentApplicationData = application;
_deviceExitStatus.Reset(); _deviceExitStatus.Reset();
@@ -954,7 +959,7 @@ namespace Ryujinx.Ui
ApplicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata => ApplicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata =>
{ {
appMetadata.LastPlayed = DateTime.UtcNow; appMetadata.UpdatePreGame();
}); });
} }
} }
@@ -1097,13 +1102,7 @@ namespace Ryujinx.Ui
{ {
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
{ {
if (appMetadata.LastPlayed.HasValue) appMetadata.UpdatePostGame();
{
double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds;
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
}
appMetadata.LastPlayed = DateTime.UtcNow;
}); });
} }
} }
@@ -1174,13 +1173,13 @@ namespace Ryujinx.Ui
_tableStore.AppendValues( _tableStore.AppendValues(
args.AppData.Favorite, args.AppData.Favorite,
new Gdk.Pixbuf(args.AppData.Icon, 75, 75), new Gdk.Pixbuf(args.AppData.Icon, 75, 75),
$"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}", $"{args.AppData.Name}\n{args.AppData.IdString.ToUpper()}",
args.AppData.Developer, args.AppData.Developer,
args.AppData.Version, args.AppData.Version,
args.AppData.TimePlayed, args.AppData.TimePlayedString,
args.AppData.LastPlayedString, args.AppData.LastPlayedString,
args.AppData.FileExtension, args.AppData.FileExtension,
args.AppData.FileSize, args.AppData.FileSizeString,
args.AppData.Path, args.AppData.Path,
args.AppData.ControlHolder); args.AppData.ControlHolder);
}); });
@@ -1262,9 +1261,22 @@ namespace Ryujinx.Ui
{ {
_gameTableSelection.GetSelected(out TreeIter treeIter); _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) private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args)
@@ -1322,13 +1334,22 @@ namespace Ryujinx.Ui
return; return;
} }
string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString(); ApplicationData application = new()
string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0]; {
string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower(); 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, application);
_ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData);
} }
private void Load_Application_File(object sender, EventArgs args) private void Load_Application_File(object sender, EventArgs args)
@@ -1350,7 +1371,12 @@ namespace Ryujinx.Ui
if (fileChooser.Run() == (int)ResponseType.Accept) 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) 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); 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) private void Open_Ryu_Folder(object sender, EventArgs args)
@@ -1651,13 +1690,13 @@ namespace Ryujinx.Ui
{ {
_userChannelPersistence.ShouldRestart = false; _userChannelPersistence.ShouldRestart = false;
RunApplication(_currentEmulatedGamePath); RunApplication(_currentApplicationData);
} }
else else
{ {
// otherwise, clear state. // otherwise, clear state.
_userChannelPersistence = new UserChannelPersistence(); _userChannelPersistence = new UserChannelPersistence();
_currentEmulatedGamePath = null; _currentApplicationData = null;
_actionMenu.Sensitive = false; _actionMenu.Sensitive = false;
_firmwareInstallFile.Sensitive = true; _firmwareInstallFile.Sensitive = true;
_firmwareInstallDirectory.Sensitive = true; _firmwareInstallDirectory.Sensitive = true;
@@ -1719,7 +1758,7 @@ namespace Ryujinx.Ui
_emulationContext.Processes.ActiveApplication.ProgramId, _emulationContext.Processes.ActiveApplication.ProgramId,
_emulationContext.Processes.ActiveApplication.ApplicationControlProperties _emulationContext.Processes.ActiveApplication.ApplicationControlProperties
.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(), .Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(),
_currentEmulatedGamePath); _currentApplicationData.Path);
window.Destroyed += CheatWindow_Destroyed; window.Destroyed += CheatWindow_Destroyed;
window.Show(); window.Show();

View File

@@ -211,6 +211,8 @@ namespace Ryujinx.Ui.Widgets
_manageSubMenu.Append(_openPtcDirMenuItem); _manageSubMenu.Append(_openPtcDirMenuItem);
_manageSubMenu.Append(_openShaderCacheDirMenuItem); _manageSubMenu.Append(_openShaderCacheDirMenuItem);
Add(_createShortcutMenuItem);
Add(new SeparatorMenuItem());
Add(_openSaveUserDirMenuItem); Add(_openSaveUserDirMenuItem);
Add(_openSaveDeviceDirMenuItem); Add(_openSaveDeviceDirMenuItem);
Add(_openSaveBcatDirMenuItem); Add(_openSaveBcatDirMenuItem);
@@ -223,7 +225,6 @@ namespace Ryujinx.Ui.Widgets
Add(new SeparatorMenuItem()); Add(new SeparatorMenuItem());
Add(_manageCacheMenuItem); Add(_manageCacheMenuItem);
Add(_extractMenuItem); Add(_extractMenuItem);
Add(_createShortcutMenuItem);
ShowAll(); ShowAll();
} }

View File

@@ -16,6 +16,7 @@ using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.Ui.App.Common; using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
@@ -23,7 +24,6 @@ using Ryujinx.Ui.Windows;
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
@@ -36,17 +36,13 @@ namespace Ryujinx.Ui.Widgets
private readonly VirtualFileSystem _virtualFileSystem; private readonly VirtualFileSystem _virtualFileSystem;
private readonly AccountManager _accountManager; private readonly AccountManager _accountManager;
private readonly HorizonClient _horizonClient; private readonly HorizonClient _horizonClient;
private readonly BlitStruct<ApplicationControlProperty> _controlData;
private readonly string _titleFilePath; private readonly ApplicationData _title;
private readonly string _titleName;
private readonly string _titleIdText;
private readonly ulong _titleId;
private MessageDialog _dialog; private MessageDialog _dialog;
private bool _cancel; private bool _cancel;
public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct<ApplicationControlProperty> controlData) public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, ApplicationData applicationData)
{ {
_parent = parent; _parent = parent;
@@ -55,23 +51,13 @@ namespace Ryujinx.Ui.Widgets
_virtualFileSystem = virtualFileSystem; _virtualFileSystem = virtualFileSystem;
_accountManager = accountManager; _accountManager = accountManager;
_horizonClient = horizonClient; _horizonClient = horizonClient;
_titleFilePath = titleFilePath; _title = applicationData;
_titleName = titleName;
_titleIdText = titleId;
_controlData = controlData;
if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId)) _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.UserAccountSaveDataSize > 0;
{ _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.DeviceSaveDataSize > 0;
GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id"); _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
return; string fileExt = System.IO.Path.GetExtension(_title.Path).ToLower();
}
_openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0;
_openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0;
_openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0;
string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower();
bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci"; bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci";
_extractRomFsMenuItem.Sensitive = hasNca; _extractRomFsMenuItem.Sensitive = hasNca;
@@ -137,7 +123,7 @@ namespace Ryujinx.Ui.Widgets
private void OpenSaveDir(in SaveDataFilter saveDataFilter) 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; return;
} }
@@ -190,7 +176,7 @@ namespace Ryujinx.Ui.Widgets
{ {
Title = "Ryujinx - NCA Section Extractor", Title = "Ryujinx - NCA Section Extractor",
Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"), Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"),
SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...", SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_title.Path)}...",
WindowPosition = WindowPosition.Center, 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 mainNca = null;
Nca patchNca = null; Nca patchNca = null;
if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") || if ((System.IO.Path.GetExtension(_title.Path).ToLower() == ".nsp") ||
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") || (System.IO.Path.GetExtension(_title.Path).ToLower() == ".pfs0") ||
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci")) (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci"))
{ {
IFileSystem pfs; 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()); 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()); mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
} }
@@ -266,7 +252,11 @@ namespace Ryujinx.Ui.Widgets
return; 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) if (updatePatchNca != null)
{ {
@@ -460,44 +450,44 @@ namespace Ryujinx.Ui.Widgets
private void OpenSaveUserDir_Clicked(object sender, EventArgs args) private void OpenSaveUserDir_Clicked(object sender, EventArgs args)
{ {
var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low); var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
var saveDataFilter = SaveDataFilter.Make(_titleId, saveType: default, userId, saveDataId: default, index: default); var saveDataFilter = SaveDataFilter.Make(_title.Id, saveType: default, userId, saveDataId: default, index: default);
OpenSaveDir(in saveDataFilter); OpenSaveDir(in saveDataFilter);
} }
private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args) private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args)
{ {
var saveDataFilter = SaveDataFilter.Make(_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); OpenSaveDir(in saveDataFilter);
} }
private void OpenSaveBcatDir_Clicked(object sender, EventArgs args) 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); OpenSaveDir(in saveDataFilter);
} }
private void ManageTitleUpdates_Clicked(object sender, EventArgs args) 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) 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) 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) private void OpenTitleModDir_Clicked(object sender, EventArgs args)
{ {
string modsBasePath = ModLoader.GetModsBasePath(); string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _title.IdString);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }
@@ -505,7 +495,7 @@ namespace Ryujinx.Ui.Widgets
private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
{ {
string sdModsBasePath = ModLoader.GetSdModsBasePath(); string sdModsBasePath = ModLoader.GetSdModsBasePath();
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _title.IdString);
OpenHelper.OpenFolder(titleModsPath); OpenHelper.OpenFolder(titleModsPath);
} }
@@ -527,7 +517,7 @@ namespace Ryujinx.Ui.Widgets
private void OpenPtcDir_Clicked(object sender, EventArgs args) private void OpenPtcDir_Clicked(object sender, EventArgs args)
{ {
string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu"); string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu");
string mainPath = System.IO.Path.Combine(ptcDir, "0"); string mainPath = System.IO.Path.Combine(ptcDir, "0");
string backupPath = System.IO.Path.Combine(ptcDir, "1"); string backupPath = System.IO.Path.Combine(ptcDir, "1");
@@ -544,7 +534,7 @@ namespace Ryujinx.Ui.Widgets
private void OpenShaderCacheDir_Clicked(object sender, EventArgs args) private void OpenShaderCacheDir_Clicked(object sender, EventArgs args)
{ {
string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"); string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader");
if (!Directory.Exists(shaderCacheDir)) if (!Directory.Exists(shaderCacheDir))
{ {
@@ -556,10 +546,10 @@ namespace Ryujinx.Ui.Widgets
private void PurgePtcCache_Clicked(object sender, EventArgs args) private void PurgePtcCache_Clicked(object sender, EventArgs args)
{ {
DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0")); DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "0"));
DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1")); 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(); List<FileInfo> cacheFiles = new();
@@ -593,9 +583,9 @@ namespace Ryujinx.Ui.Widgets
private void PurgeShaderCache_Clicked(object sender, EventArgs args) private void PurgeShaderCache_Clicked(object sender, EventArgs args)
{ {
DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _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<DirectoryInfo> oldCacheDirectories = new();
List<FileInfo> newCacheFiles = new(); List<FileInfo> newCacheFiles = new();
@@ -637,8 +627,11 @@ namespace Ryujinx.Ui.Widgets
private void CreateShortcut_Clicked(object sender, EventArgs args) private void CreateShortcut_Clicked(object sender, EventArgs args)
{ {
byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language); IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon); ? 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);
} }
} }
} }

View File

@@ -1,7 +1,9 @@
using Gtk; using Gtk;
using LibHac.Tools.FsSystem;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.Ui.App.Common; using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common.Configuration;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@@ -27,8 +29,13 @@ namespace Ryujinx.Ui.Windows
private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow")) private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow"))
{ {
builder.Autoconnect(this); builder.Autoconnect(this);
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
_baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]"; _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]";
_buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath)}"; _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath)}";
string modsBasePath = ModLoader.GetModsBasePath(); string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16")); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16"));

View File

@@ -9,9 +9,12 @@ using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using GUI = Gtk.Builder.ObjectAttribute; using GUI = Gtk.Builder.ObjectAttribute;
@@ -20,7 +23,7 @@ namespace Ryujinx.Ui.Windows
public class DlcWindow : Window public class DlcWindow : Window
{ {
private readonly VirtualFileSystem _virtualFileSystem; private readonly VirtualFileSystem _virtualFileSystem;
private readonly string _titleId; private readonly string _applicationId;
private readonly string _dlcJsonPath; private readonly string _dlcJsonPath;
private readonly List<DownloadableContentContainer> _dlcContainerList; private readonly List<DownloadableContentContainer> _dlcContainerList;
@@ -32,16 +35,16 @@ namespace Ryujinx.Ui.Windows
[GUI] TreeSelection _dlcTreeSelection; [GUI] TreeSelection _dlcTreeSelection;
#pragma warning restore CS0649, IDE0044 #pragma warning restore CS0649, IDE0044
public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, 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); builder.Autoconnect(this);
_titleId = titleId; _applicationId = applicationId;
_virtualFileSystem = virtualFileSystem; _virtualFileSystem = virtualFileSystem;
_dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json"); _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationId, "dlc.json");
_baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]"; _baseTitleInfoLabel.Text = $"DLC Available for {title.Name} [{applicationId.ToUpper()}]";
try try
{ {
@@ -72,9 +75,12 @@ namespace Ryujinx.Ui.Windows
}; };
_dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0); _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); _dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
// NOTE: Try to load downloadable contents from PFS first.
AddDlc(title.Path, true);
foreach (DownloadableContentContainer dlcContainer in _dlcContainerList) foreach (DownloadableContentContainer dlcContainer in _dlcContainerList)
{ {
if (File.Exists(dlcContainer.ContainerPath)) if (File.Exists(dlcContainer.ContainerPath))
@@ -89,7 +95,10 @@ namespace Ryujinx.Ui.Windows
using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath); using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
PartitionFileSystem pfs = new(); PartitionFileSystem pfs = new();
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); if (pfs.Initialize(containerFile.AsStorage()).IsFailure())
{
continue;
}
_virtualFileSystem.ImportTickets(pfs); _virtualFileSystem.ImportTickets(pfs);
@@ -128,6 +137,57 @@ namespace Ryujinx.Ui.Windows
return null; return null;
} }
private void AddDlc(string path, bool ignoreNotFound = false)
{
if (!File.Exists(path))
{
return;
}
using FileStream containerFile = File.OpenRead(path);
PartitionFileSystem pfs = new();
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
bool containsDlc = false;
_virtualFileSystem.ImportTickets(pfs);
TreeIter? parentIter = null;
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
if (nca == null)
{
continue;
}
if (nca.Header.ContentType == NcaContentType.PublicData)
{
if (nca.GetProgramIdBase() != (ulong.Parse(_applicationId, NumberStyles.HexNumber) & ~0x1FFFUL))
{
break;
}
parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", path);
((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
containsDlc = true;
}
}
if (!containsDlc && !ignoreNotFound)
{
GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
}
}
private void AddButton_Clicked(object sender, EventArgs args) private void AddButton_Clicked(object sender, EventArgs args)
{ {
FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel") FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel")
@@ -147,52 +207,7 @@ namespace Ryujinx.Ui.Windows
{ {
foreach (string containerPath in fileChooser.Filenames) foreach (string containerPath in fileChooser.Filenames)
{ {
if (!File.Exists(containerPath)) AddDlc(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!");
}
} }
} }

View File

@@ -4,12 +4,15 @@ using LibHac.Fs;
using LibHac.Fs.Fsa; using LibHac.Fs.Fsa;
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.Ns; using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.Ui.App.Common; using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -24,7 +27,7 @@ namespace Ryujinx.Ui.Windows
{ {
private readonly MainWindow _parent; private readonly MainWindow _parent;
private readonly VirtualFileSystem _virtualFileSystem; private readonly VirtualFileSystem _virtualFileSystem;
private readonly string _titleId; private readonly ApplicationData _title;
private readonly string _updateJsonPath; private readonly string _updateJsonPath;
private TitleUpdateMetadata _titleUpdateWindowData; private TitleUpdateMetadata _titleUpdateWindowData;
@@ -38,17 +41,17 @@ namespace Ryujinx.Ui.Windows
[GUI] RadioButton _noUpdateRadioButton; [GUI] RadioButton _noUpdateRadioButton;
#pragma warning restore CS0649, IDE0044 #pragma warning restore CS0649, IDE0044
public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, 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; _parent = parent;
builder.Autoconnect(this); builder.Autoconnect(this);
_titleId = titleId; _title = applicationData;
_virtualFileSystem = virtualFileSystem; _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>(); _radioButtonToPathDictionary = new Dictionary<RadioButton, string>();
try 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) 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)) if (File.Exists(path))
{ {
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
using FileStream file = new(path, FileMode.Open, FileAccess.Read); using FileStream file = new(path, FileMode.Open, FileAccess.Read);
PartitionFileSystem nsp = new(); IFileSystem pfs;
nsp.Initialize(file.AsStorage()).ThrowIfFailure();
try 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) 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(); controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
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); radioButton.JoinGroup(_noUpdateRadioButton);
_availableUpdatesBox.Add(radioButton); _availableUpdatesBox.Add(radioButton);
@@ -116,10 +152,13 @@ namespace Ryujinx.Ui.Windows
radioButton.Active = true; radioButton.Active = true;
} }
else else
{
if (!ignoreNotFound)
{ {
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
} }
} }
}
catch (Exception exception) catch (Exception exception)
{ {
GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}"); GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}");