From dde208b480f3e2aeb0e3abc15857d031ceb22bf4 Mon Sep 17 00:00:00 2001
From: gnisman <gleb.nisman@gmail.com>
Date: Sun, 7 May 2023 17:36:44 +0300
Subject: [PATCH] UI: Expose games build ID for cheat management (#4340)

* Ava UI: Expose games build ID for cheat management

* Fix bad merge

* Change integrity check level to error on invalid

* Add support for GDK

* Remove whitespace

* Add BID identifier

* PR Comments fix

* Restore title id in cheats GTK window

* use halign center instead of margin_left

* Merge

* fix after merge

* PR comments fix - design AVA

* PR fix - Move GetApplicationBuildId to ApplicationData class

* PR comment fix - Add empty line before method

* Align with PR #4755

* PR comments fix

* Change BuildId label to support translation

* Comments fix

* Remove unused BuildIdLabel property
---
 src/Ryujinx.Ava/Assets/Locales/en_US.json     |   1 +
 .../Controls/ApplicationContextMenu.axaml.cs  |   7 +-
 .../UI/Views/Main/MainMenuBarView.axaml.cs    |   7 +-
 src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml  |  37 ++++-
 .../UI/Windows/CheatWindow.axaml.cs           |  10 +-
 src/Ryujinx.Ui.Common/App/ApplicationData.cs  | 128 ++++++++++++++++++
 src/Ryujinx/Ui/MainWindow.cs                  |   9 +-
 .../Ui/Widgets/GameTableContextMenu.cs        |   2 +-
 src/Ryujinx/Ui/Windows/CheatWindow.cs         |   7 +-
 src/Ryujinx/Ui/Windows/CheatWindow.glade      |  17 ++-
 10 files changed, 210 insertions(+), 15 deletions(-)

diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json
index 965dfa3a52..c9b10f54a5 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 90c72e0289..a9269386ac 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 bdf2cf9f80..557528eb16 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 3557ed696f..11e86211e0 100644
--- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml
+++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml
@@ -21,23 +21,52 @@
     </Window.Styles>
     <Grid Name="CheatGrid" Margin="15">
         <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
             <RowDefinition Height="Auto" />
             <RowDefinition Height="Auto" />
             <RowDefinition Height="*" />
             <RowDefinition Height="Auto" />
         </Grid.RowDefinitions>
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition Width="*" />
+            <ColumnDefinition Width="*" />
+        </Grid.ColumnDefinitions>
         <TextBlock
             Grid.Row="1"
+            Grid.Column="0"
+            Grid.ColumnSpan="2"
             MaxWidth="500"
-            Margin="20,15,20,20"
+            Margin="20,15,20,5"
             HorizontalAlignment="Center"
             VerticalAlignment="Center"
             LineHeight="18"
             Text="{Binding Heading}"
             TextAlignment="Center"
             TextWrapping="Wrap" />
-        <Border
+        <TextBlock
             Grid.Row="2"
+            Grid.Column="0"
+            MaxWidth="500"
+            Margin="140,15,20,5"
+            HorizontalAlignment="Center"
+            VerticalAlignment="Center"
+            LineHeight="30"
+            Text="{locale:Locale BuildId}"
+            TextAlignment="Center"
+            TextWrapping="Wrap" />
+        <TextBox
+            Grid.Row="2"
+            Grid.Column="1"
+            Margin="0,5,110,5"
+            MinWidth="160"
+            HorizontalAlignment="Center"
+            VerticalAlignment="Center"
+            Text="{Binding BuildId}"
+            IsReadOnly="True" />
+        <Border
+            Grid.Row="3"
+            Grid.Column="0"
+            Grid.ColumnSpan="2"
             Margin="5"
             HorizontalAlignment="Stretch"
             VerticalAlignment="Stretch"
@@ -81,7 +110,9 @@
             </TreeView>
         </Border>
         <DockPanel
-            Grid.Row="3"
+            Grid.Row="4"
+            Grid.Column="0"
+            Grid.ColumnSpan="2"
             Margin="0"
             HorizontalAlignment="Stretch">
             <DockPanel Margin="0" HorizontalAlignment="Right">
diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs
index 241a6c346c..f5bba7d2d9 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<CheatsList> 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<CheatsList>();
 
             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 ba430172fd..d9d3cf6853 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<ApplicationControlProperty> 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<IFile>();
+
+                    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<IFile>();
+
+            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 4911c90063..b61855e4c9 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 28ec5a43c7..74f6043d40 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 7dbea01289..32df2c0c20 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 37b1cbe078..9a165f1a82 100644
--- a/src/Ryujinx/Ui/Windows/CheatWindow.glade
+++ b/src/Ryujinx/Ui/Windows/CheatWindow.glade
@@ -31,6 +31,21 @@
                 <property name="position">0</property>
               </packing>
             </child>
+            <child>
+              <object class="GtkTextView" id="_buildIdTextView">
+                <property name="visible">True</property>
+                <property name="margin_top">10</property>
+                <property name="halign">center</property>
+                <property name="margin_bottom">10</property>
+                <property name="editable">False</property>
+                <property name="cursor_visible">False</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
             <child>
               <object class="GtkScrolledWindow">
                 <property name="visible">True</property>
@@ -57,7 +72,7 @@
               <packing>
                 <property name="expand">True</property>
                 <property name="fill">True</property>
-                <property name="position">1</property>
+                <property name="position">2</property>
               </packing>
             </child>
           </object>