diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 965dfa3a..c9b10f54 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -590,6 +590,7 @@ "DlcWindowTitle": "Manage Downloadable Content for {0} ({1})", "UpdateWindowTitle": "Title Update Manager", "CheatWindowHeading": "Cheats Available for {0} [{1}]", + "BuildId": "BuildId:", "DlcWindowHeading": "{0} Downloadable Content(s)", "UserProfilesEditProfile": "Edit Selected", "Cancel": "Cancel", diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs index 90c72e02..a9269386 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs @@ -10,6 +10,7 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common.Configuration; +using Ryujinx.Ui.App.Common; using Ryujinx.HLE.HOS; using Ryujinx.Ui.Common.Helper; using System; @@ -118,7 +119,11 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await new CheatWindow(viewModel.VirtualFileSystem, viewModel.SelectedApplication.TitleId, viewModel.SelectedApplication.TitleName).ShowDialog(viewModel.TopLevel as Window); + await new CheatWindow( + viewModel.VirtualFileSystem, + viewModel.SelectedApplication.TitleId, + viewModel.SelectedApplication.TitleName, + viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window); } } diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs index bdf2cf9f..557528eb 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs @@ -11,6 +11,7 @@ using Ryujinx.Common; using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS; using Ryujinx.Modules; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -176,7 +177,11 @@ namespace Ryujinx.Ava.UI.Views.Main string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString(); - await new CheatWindow(Window.VirtualFileSystem, ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText, name).ShowDialog(Window); + await new CheatWindow( + Window.VirtualFileSystem, + ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText, + name, + Window.ViewModel.SelectedApplication.Path).ShowDialog(Window); ViewModel.AppHost.Device.EnableCheats(); } diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml index 3557ed69..11e86211 100644 --- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml @@ -21,23 +21,52 @@ + + + + + - + + diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs index 241a6c34..f5bba7d2 100644 --- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs @@ -1,8 +1,10 @@ -using Avalonia.Collections; +using Avalonia; +using Avalonia.Collections; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Models; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; +using Ryujinx.Ui.App.Common; using System.Collections.Generic; using System.IO; using System.Linq; @@ -17,6 +19,7 @@ namespace Ryujinx.Ava.UI.Windows private AvaloniaList LoadedCheats { get; } public string Heading { get; } + public string BuildId { get; } public CheatWindow() { @@ -27,12 +30,13 @@ namespace Ryujinx.Ava.UI.Windows Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance[LocaleKeys.CheatWindowTitle]; } - public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) + public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath) { LoadedCheats = new AvaloniaList(); Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper()); - + BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath); + InitializeComponent(); string modsBasePath = ModLoader.GetModsBasePath(); diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs index ba430172..d9d3cf68 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs @@ -1,5 +1,16 @@ using LibHac.Common; using LibHac.Ns; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Loader; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.FileSystem; +using System; +using System.IO; namespace Ryujinx.Ui.App.Common { @@ -19,5 +30,122 @@ namespace Ryujinx.Ui.App.Common public double FileSizeBytes { get; set; } public string Path { get; set; } public BlitStruct ControlHolder { get; set; } + + public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath) + { + using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read); + + Nca mainNca = null; + Nca patchNca = null; + + if (!System.IO.Path.Exists(titleFilePath)) + { + Logger.Error?.Print(LogClass.Application, $"File does not exists. {titleFilePath}"); + return string.Empty; + } + + string extension = System.IO.Path.GetExtension(titleFilePath).ToLower(); + + if (extension is ".nsp" or ".xci") + { + PartitionFileSystem pfs; + + if (extension == ".xci") + { + Xci xci = new(virtualFileSystem.KeySet, file.AsStorage()); + + pfs = xci.OpenPartition(XciPartitionType.Secure); + } + else + { + pfs = new PartitionFileSystem(file.AsStorage()); + } + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + + if (nca.Header.ContentType != NcaContentType.Program) + { + continue; + } + + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + patchNca = nca; + } + else + { + mainNca = nca; + } + } + } + else if (extension == ".nca") + { + mainNca = new Nca(virtualFileSystem.KeySet, file.AsStorage()); + } + + if (mainNca == null) + { + Logger.Error?.Print(LogClass.Application, "Extraction failure. The main NCA was not present in the selected file"); + + return string.Empty; + } + + (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); + + if (updatePatchNca != null) + { + patchNca = updatePatchNca; + } + + IFileSystem codeFs = null; + + if (patchNca == null) + { + if (mainNca.CanOpenSection(NcaSectionType.Code)) + { + codeFs = mainNca.OpenFileSystem(NcaSectionType.Code, IntegrityCheckLevel.ErrorOnInvalid); + } + } + else + { + if (patchNca.CanOpenSection(NcaSectionType.Code)) + { + codeFs = mainNca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, IntegrityCheckLevel.ErrorOnInvalid); + } + } + + if (codeFs == null) + { + Logger.Error?.Print(LogClass.Loader, "No ExeFS found in NCA"); + + return string.Empty; + } + + const string mainExeFs = "main"; + + if (!codeFs.FileExists($"/{mainExeFs}")) + { + Logger.Error?.Print(LogClass.Loader, "No main binary ExeFS found in ExeFS"); + + return string.Empty; + } + + using var nsoFile = new UniqueRef(); + + codeFs.OpenFile(ref nsoFile.Ref, $"/{mainExeFs}".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + NsoReader reader = new NsoReader(); + reader.Initialize(nsoFile.Release().AsStorage().AsFile(OpenMode.Read)).ThrowIfFailure(); + + return BitConverter.ToString(reader.Header.ModuleId.ItemsRo.ToArray()).Replace("-", "").ToUpper()[..16]; + } } } \ No newline at end of file diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index 4911c900..b61855e4 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -1626,9 +1626,12 @@ namespace Ryujinx.Ui private void ManageCheats_Pressed(object sender, EventArgs args) { - var window = new CheatWindow(_virtualFileSystem, - _emulationContext.Processes.ActiveApplication.ProgramId, - _emulationContext.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString()); + var window = new CheatWindow( + _virtualFileSystem, + _emulationContext.Processes.ActiveApplication.ProgramId, + _emulationContext.Processes.ActiveApplication.ApplicationControlProperties + .Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(), + _currentEmulatedGamePath); window.Destroyed += CheatWindow_Destroyed; window.Show(); diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index 28ec5a43..74f6043d 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -461,7 +461,7 @@ namespace Ryujinx.Ui.Widgets private void ManageCheats_Clicked(object sender, EventArgs args) { - new CheatWindow(_virtualFileSystem, _titleId, _titleName).Show(); + new CheatWindow(_virtualFileSystem, _titleId, _titleName, _titleFilePath).Show(); } private void OpenTitleModDir_Clicked(object sender, EventArgs args) diff --git a/src/Ryujinx/Ui/Windows/CheatWindow.cs b/src/Ryujinx/Ui/Windows/CheatWindow.cs index 7dbea012..32df2c0c 100644 --- a/src/Ryujinx/Ui/Windows/CheatWindow.cs +++ b/src/Ryujinx/Ui/Windows/CheatWindow.cs @@ -1,6 +1,7 @@ using Gtk; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; +using Ryujinx.Ui.App.Common; using System; using System.Collections.Generic; using System.IO; @@ -17,16 +18,18 @@ namespace Ryujinx.Ui.Windows #pragma warning disable CS0649, IDE0044 [GUI] Label _baseTitleInfoLabel; + [GUI] TextView _buildIdTextView; [GUI] TreeView _cheatTreeView; [GUI] Button _saveButton; #pragma warning restore CS0649, IDE0044 - public CheatWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.CheatWindow.glade"), virtualFileSystem, titleId, titleName) { } + public CheatWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : this(new Builder("Ryujinx.Ui.Windows.CheatWindow.glade"), virtualFileSystem, titleId, titleName, titlePath) { } - private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) : base(builder.GetRawOwnedObject("_cheatWindow")) + private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow")) { builder.Autoconnect(this); _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]"; + _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath)}"; string modsBasePath = ModLoader.GetModsBasePath(); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16")); diff --git a/src/Ryujinx/Ui/Windows/CheatWindow.glade b/src/Ryujinx/Ui/Windows/CheatWindow.glade index 37b1cbe0..9a165f1a 100644 --- a/src/Ryujinx/Ui/Windows/CheatWindow.glade +++ b/src/Ryujinx/Ui/Windows/CheatWindow.glade @@ -31,6 +31,21 @@ 0 + + + True + 10 + center + 10 + False + False + + + False + True + 1 + + True @@ -57,7 +72,7 @@ True True - 1 + 2