diff --git a/Ryujinx.HLE/HOS/ModLoader.cs b/Ryujinx.HLE/HOS/ModLoader.cs index 54a9755690..b31798b887 100644 --- a/Ryujinx.HLE/HOS/ModLoader.cs +++ b/Ryujinx.HLE/HOS/ModLoader.cs @@ -664,7 +664,20 @@ namespace Ryujinx.HLE.HOS Logger.Info?.Print(LogClass.ModLoader, $"Installing cheat '{cheat.Name}'"); - tamperMachine.InstallAtmosphereCheat(cheat.Name, cheat.Instructions, tamperInfo, exeAddress); + tamperMachine.InstallAtmosphereCheat(cheat.Name, cheatId, cheat.Instructions, tamperInfo, exeAddress); + } + + EnableCheats(titleId, tamperMachine); + } + + internal void EnableCheats(ulong titleId, TamperMachine tamperMachine) + { + var contentDirectory = FindTitleDir(new DirectoryInfo(Path.Combine(GetModsBasePath(), AmsContentsDir)), $"{titleId:x16}"); + string enabledCheatsPath = Path.Combine(contentDirectory.FullName, CheatDir, "enabled.txt"); + + if (File.Exists(enabledCheatsPath)) + { + tamperMachine.EnableCheats(File.ReadAllLines(enabledCheatsPath)); } } diff --git a/Ryujinx.HLE/HOS/Tamper/AtmosphereProgram.cs b/Ryujinx.HLE/HOS/Tamper/AtmosphereProgram.cs index dac445b0d0..a2aa73a4fa 100644 --- a/Ryujinx.HLE/HOS/Tamper/AtmosphereProgram.cs +++ b/Ryujinx.HLE/HOS/Tamper/AtmosphereProgram.cs @@ -11,6 +11,7 @@ namespace Ryujinx.HLE.HOS.Tamper public string Name { get; } public bool TampersCodeMemory { get; set; } = false; public ITamperedProcess Process { get; } + public bool IsEnabled { get; set; } public AtmosphereProgram(string name, ITamperedProcess process, Parameter<long> pressedKeys, IOperation entryPoint) { @@ -22,8 +23,11 @@ namespace Ryujinx.HLE.HOS.Tamper public void Execute(ControllerKeys pressedKeys) { - _pressedKeys.Value = (long)pressedKeys; - _entryPoint.Execute(); + if (IsEnabled) + { + _pressedKeys.Value = (long)pressedKeys; + _entryPoint.Execute(); + } } } } diff --git a/Ryujinx.HLE/HOS/Tamper/ITamperProgram.cs b/Ryujinx.HLE/HOS/Tamper/ITamperProgram.cs index 63702bf75b..8458d95d66 100644 --- a/Ryujinx.HLE/HOS/Tamper/ITamperProgram.cs +++ b/Ryujinx.HLE/HOS/Tamper/ITamperProgram.cs @@ -4,6 +4,7 @@ namespace Ryujinx.HLE.HOS.Tamper { interface ITamperProgram { + bool IsEnabled { get; set; } string Name { get; } bool TampersCodeMemory { get; set; } ITamperedProcess Process { get; } diff --git a/Ryujinx.HLE/HOS/TamperMachine.cs b/Ryujinx.HLE/HOS/TamperMachine.cs index 6044368e94..016f326f1b 100644 --- a/Ryujinx.HLE/HOS/TamperMachine.cs +++ b/Ryujinx.HLE/HOS/TamperMachine.cs @@ -20,6 +20,7 @@ namespace Ryujinx.HLE.HOS private Thread _tamperThread = null; private ConcurrentQueue<ITamperProgram> _programs = new ConcurrentQueue<ITamperProgram>(); private long _pressedKeys = 0; + private Dictionary<string, ITamperProgram> _programDictionary = new Dictionary<string, ITamperProgram>(); private void Activate() { @@ -31,7 +32,7 @@ namespace Ryujinx.HLE.HOS } } - internal void InstallAtmosphereCheat(string name, IEnumerable<string> rawInstructions, ProcessTamperInfo info, ulong exeAddress) + internal void InstallAtmosphereCheat(string name, string buildId, IEnumerable<string> rawInstructions, ProcessTamperInfo info, ulong exeAddress) { if (!CanInstallOnPid(info.Process.Pid)) { @@ -47,6 +48,7 @@ namespace Ryujinx.HLE.HOS program.TampersCodeMemory = false; _programs.Enqueue(program); + _programDictionary.TryAdd($"{buildId}-{name}", program); } Activate(); @@ -65,6 +67,22 @@ namespace Ryujinx.HLE.HOS return true; } + public void EnableCheats(string[] enabledCheats) + { + foreach (var program in _programDictionary.Values) + { + program.IsEnabled = false; + } + + foreach (var cheat in enabledCheats) + { + if (_programDictionary.TryGetValue(cheat, out var program)) + { + program.IsEnabled = true; + } + } + } + private bool IsProcessValid(ITamperedProcess process) { return process.State != ProcessState.Crashed && process.State != ProcessState.Exiting && process.State != ProcessState.Exited; @@ -105,6 +123,8 @@ namespace Ryujinx.HLE.HOS if (!_programs.TryDequeue(out ITamperProgram program)) { // No more programs in the queue. + _programDictionary.Clear(); + return false; } diff --git a/Ryujinx.HLE/Switch.cs b/Ryujinx.HLE/Switch.cs index ac7d461127..0dcbc7ec50 100644 --- a/Ryujinx.HLE/Switch.cs +++ b/Ryujinx.HLE/Switch.cs @@ -156,6 +156,11 @@ namespace Ryujinx.HLE return System.GetVolume(); } + public void EnableCheats() + { + FileSystem.ModLoader.EnableCheats(Application.TitleId, TamperMachine); + } + public bool IsAudioMuted() { return System.GetVolume() == 0; diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj index 2e1bcf52c6..9374df102e 100644 --- a/Ryujinx/Ryujinx.csproj +++ b/Ryujinx/Ryujinx.csproj @@ -81,6 +81,7 @@ <None Remove="Ui\Resources\Logo_Ryujinx.png" /> <None Remove="Ui\Resources\Logo_Twitter.png" /> <None Remove="Ui\Widgets\ProfileDialog.glade" /> + <None Remove="Ui\Windows\CheatWindow.glade" /> <None Remove="Ui\Windows\ControllerWindow.glade" /> <None Remove="Ui\Windows\DlcWindow.glade" /> <None Remove="Ui\Windows\SettingsWindow.glade" /> @@ -106,6 +107,7 @@ <EmbeddedResource Include="Ui\Resources\Logo_Ryujinx.png" /> <EmbeddedResource Include="Ui\Resources\Logo_Twitter.png" /> <EmbeddedResource Include="Ui\Widgets\ProfileDialog.glade" /> + <EmbeddedResource Include="Ui\Windows\CheatWindow.glade" /> <EmbeddedResource Include="Ui\Windows\ControllerWindow.glade" /> <EmbeddedResource Include="Ui\Windows\DlcWindow.glade" /> <EmbeddedResource Include="Ui\Windows\SettingsWindow.glade" /> diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index e78d1a46d4..b955dc73b1 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -1553,6 +1553,20 @@ namespace Ryujinx.Ui ToggleExtraWidgets(false); } + private void ManageCheats_Pressed(object sender, EventArgs args) + { + var window = new CheatWindow(_virtualFileSystem, _emulationContext.Application.TitleId, _emulationContext.Application.TitleName); + + window.Destroyed += CheatWindow_Destroyed; + window.Show(); + } + + private void CheatWindow_Destroyed(object sender, EventArgs e) + { + _emulationContext.EnableCheats(); + (sender as CheatWindow).Destroyed -= CheatWindow_Destroyed; + } + private void ManageUserProfiles_Pressed(object sender, EventArgs args) { UserProfilesManagerWindow userProfilesManagerWindow = new UserProfilesManagerWindow(_accountManager, _contentManager, _virtualFileSystem); diff --git a/Ryujinx/Ui/MainWindow.glade b/Ryujinx/Ui/MainWindow.glade index a9ab43e1e3..595786a325 100644 --- a/Ryujinx/Ui/MainWindow.glade +++ b/Ryujinx/Ui/MainWindow.glade @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.38.2 --> +<!-- Generated with glade 3.21.0 --> <interface> <requires lib="gtk+" version="3.20"/> <object class="GtkApplicationWindow" id="_mainWin"> @@ -364,7 +364,15 @@ <property name="can_focus">False</property> <property name="label" translatable="yes">Hide UI (SHOWUIKEY to show)</property> <property name="use_underline">True</property> - <signal name="activate" handler="HideUi_Pressed" swapped="no" /> + <signal name="activate" handler="HideUi_Pressed" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="_manageCheats"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Manage Cheats</property> + <signal name="activate" handler="ManageCheats_Pressed" swapped="no"/> </object> </child> </object> @@ -485,7 +493,7 @@ <property name="can_focus">True</property> <property name="reorderable">True</property> <property name="hover_selection">True</property> - <signal name="row_activated" handler="Row_Activated" swapped="no"/> + <signal name="row-activated" handler="Row_Activated" swapped="no"/> <child internal-child="selection"> <object class="GtkTreeSelection" id="_gameTableSelection"/> </child> @@ -519,7 +527,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="margin_left">5</property> - <signal name="button_release_event" handler="RefreshList_Pressed" swapped="no"/> + <signal name="button-release-event" handler="RefreshList_Pressed" swapped="no"/> <child> <object class="GtkImage"> <property name="name">RefreshList</property> @@ -582,7 +590,7 @@ <object class="GtkEventBox"> <property name="visible">True</property> <property name="can_focus">False</property> - <signal name="button_release_event" handler="VSyncStatus_Clicked" swapped="no"/> + <signal name="button-release-event" handler="VSyncStatus_Clicked" swapped="no"/> <child> <object class="GtkLabel" id="_vSyncStatus"> <property name="visible">True</property> @@ -615,7 +623,7 @@ <object class="GtkEventBox"> <property name="visible">True</property> <property name="can_focus">False</property> - <signal name="button_release_event" handler="DockedMode_Clicked" swapped="no"/> + <signal name="button-release-event" handler="DockedMode_Clicked" swapped="no"/> <child> <object class="GtkLabel" id="_dockedMode"> <property name="visible">True</property> @@ -647,7 +655,7 @@ <object class="GtkEventBox"> <property name="visible">True</property> <property name="can_focus">False</property> - <signal name="button_release_event" handler="VolumeStatus_Clicked" swapped="no"/> + <signal name="button-release-event" handler="VolumeStatus_Clicked" swapped="no"/> <child> <object class="GtkLabel" id="_volumeStatus"> <property name="visible">True</property> @@ -655,7 +663,6 @@ <property name="halign">start</property> <property name="margin_left">5</property> <property name="margin_right">5</property> - <property name="label" translatable="yes"></property> </object> </child> </object> @@ -680,7 +687,7 @@ <object class="GtkEventBox"> <property name="visible">True</property> <property name="can_focus">False</property> - <signal name="button_release_event" handler="AspectRatio_Clicked" swapped="no"/> + <signal name="button-release-event" handler="AspectRatio_Clicked" swapped="no"/> <child> <object class="GtkLabel" id="_aspectRatio"> <property name="visible">True</property> @@ -862,5 +869,8 @@ </child> </object> </child> + <child type="titlebar"> + <placeholder/> + </child> </object> </interface> diff --git a/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs index 4b903d6cf1..190efd4947 100644 --- a/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs +++ b/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs @@ -9,6 +9,7 @@ namespace Ryujinx.Ui.Widgets private MenuItem _openSaveBcatDirMenuItem; private MenuItem _manageTitleUpdatesMenuItem; private MenuItem _manageDlcMenuItem; + private MenuItem _manageCheatMenuItem; private MenuItem _openTitleModDirMenuItem; private Menu _extractSubMenu; private MenuItem _extractMenuItem; @@ -69,6 +70,15 @@ namespace Ryujinx.Ui.Widgets }; _manageDlcMenuItem.Activated += ManageDlc_Clicked; + // + // _manageCheatMenuItem + // + _manageCheatMenuItem = new MenuItem("Manage Cheats") + { + TooltipText = "Open the Cheat management window" + }; + _manageCheatMenuItem.Activated += ManageCheats_Clicked; + // // _openTitleModDirMenuItem // @@ -187,6 +197,7 @@ namespace Ryujinx.Ui.Widgets Add(new SeparatorMenuItem()); Add(_manageTitleUpdatesMenuItem); Add(_manageDlcMenuItem); + Add(_manageCheatMenuItem); Add(_openTitleModDirMenuItem); Add(new SeparatorMenuItem()); Add(_manageCacheMenuItem); diff --git a/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index 5ad6e35fe0..c54e16a60a 100644 --- a/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -469,6 +469,11 @@ namespace Ryujinx.Ui.Widgets new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show(); } + private void ManageCheats_Clicked(object sender, EventArgs args) + { + new CheatWindow(_virtualFileSystem, _titleId, _titleName).Show(); + } + private void OpenTitleModDir_Clicked(object sender, EventArgs args) { string modsBasePath = _virtualFileSystem.ModLoader.GetModsBasePath(); diff --git a/Ryujinx/Ui/Windows/CheatWindow.cs b/Ryujinx/Ui/Windows/CheatWindow.cs new file mode 100644 index 0000000000..e4f6c44ebf --- /dev/null +++ b/Ryujinx/Ui/Windows/CheatWindow.cs @@ -0,0 +1,155 @@ +using Gtk; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using GUI = Gtk.Builder.ObjectAttribute; +using JsonHelper = Ryujinx.Common.Utilities.JsonHelper; + +namespace Ryujinx.Ui.Windows +{ + public class CheatWindow : Window + { + private readonly string _enabledCheatsPath; + private readonly bool _noCheatsFound; + +#pragma warning disable CS0649, IDE0044 + [GUI] Label _baseTitleInfoLabel; + [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) { } + + private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) : base(builder.GetObject("_cheatWindow").Handle) + { + builder.Autoconnect(this); + _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]"; + + string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath(); + string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16")); + + _enabledCheatsPath = System.IO.Path.Combine(titleModsPath, "cheats", "enabled.txt"); + + _cheatTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string), typeof(string)); + + CellRendererToggle enableToggle = new CellRendererToggle(); + enableToggle.Toggled += (sender, args) => + { + _cheatTreeView.Model.GetIter(out TreeIter treeIter, new TreePath(args.Path)); + bool newValue = !(bool)_cheatTreeView.Model.GetValue(treeIter, 0); + _cheatTreeView.Model.SetValue(treeIter, 0, newValue); + + if (_cheatTreeView.Model.IterChildren(out TreeIter childIter, treeIter)) + { + do + { + _cheatTreeView.Model.SetValue(childIter, 0, newValue); + } + while (_cheatTreeView.Model.IterNext(ref childIter)); + } + }; + + _cheatTreeView.AppendColumn("Enabled", enableToggle, "active", 0); + _cheatTreeView.AppendColumn("Name", new CellRendererText(), "text", 1); + _cheatTreeView.AppendColumn("Path", new CellRendererText(), "text", 2); + + var buildIdColumn = _cheatTreeView.AppendColumn("Build Id", new CellRendererText(), "text", 3); + buildIdColumn.Visible = false; + + string[] enabled = { }; + + if (File.Exists(_enabledCheatsPath)) + { + enabled = File.ReadAllLines(_enabledCheatsPath); + } + + int cheatAdded = 0; + + var mods = new ModLoader.ModCache(); + + ModLoader.QueryContentsDir(mods, new DirectoryInfo(System.IO.Path.Combine(modsBasePath, "contents")), titleId); + + string currentCheatFile = string.Empty; + string buildId = string.Empty; + TreeIter parentIter = default; + + foreach (var cheat in mods.Cheats) + { + if (cheat.Path.FullName != currentCheatFile) + { + currentCheatFile = cheat.Path.FullName; + string parentPath = currentCheatFile.Replace(titleModsPath, ""); + + buildId = System.IO.Path.GetFileNameWithoutExtension(currentCheatFile); + parentIter = ((TreeStore)_cheatTreeView.Model).AppendValues(false, buildId, parentPath, ""); + } + + string cleanName = cheat.Name.Substring(1, cheat.Name.Length - 8); + ((TreeStore)_cheatTreeView.Model).AppendValues(parentIter, enabled.Contains($"{buildId}-{cheat.Name}"), cleanName, "", buildId); + + cheatAdded++; + } + + if (cheatAdded == 0) + { + ((TreeStore)_cheatTreeView.Model).AppendValues(false, "No Cheats Found", "", ""); + _cheatTreeView.GetColumn(0).Visible = false; + + _noCheatsFound = true; + + _saveButton.Visible = false; + } + + _cheatTreeView.ExpandAll(); + } + + private void SaveButton_Clicked(object sender, EventArgs args) + { + if (_noCheatsFound) + { + return; + } + + List<string> enabledCheats = new List<string>(); + + if (_cheatTreeView.Model.GetIterFirst(out TreeIter parentIter)) + { + do + { + if (_cheatTreeView.Model.IterChildren(out TreeIter childIter, parentIter)) + { + do + { + var enabled = (bool)_cheatTreeView.Model.GetValue(childIter, 0); + + if (enabled) + { + var name = _cheatTreeView.Model.GetValue(childIter, 1).ToString(); + var buildId = _cheatTreeView.Model.GetValue(childIter, 3).ToString(); + + enabledCheats.Add($"{buildId}-<{name} Cheat>"); + } + } + while (_cheatTreeView.Model.IterNext(ref childIter)); + } + } + while (_cheatTreeView.Model.IterNext(ref parentIter)); + } + + Directory.CreateDirectory(System.IO.Path.GetDirectoryName(_enabledCheatsPath)); + + File.WriteAllLines(_enabledCheatsPath, enabledCheats); + + Dispose(); + } + + private void CancelButton_Clicked(object sender, EventArgs args) + { + Dispose(); + } + } +} diff --git a/Ryujinx/Ui/Windows/CheatWindow.glade b/Ryujinx/Ui/Windows/CheatWindow.glade new file mode 100644 index 0000000000..37b1cbe078 --- /dev/null +++ b/Ryujinx/Ui/Windows/CheatWindow.glade @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.21.0 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <object class="GtkWindow" id="_cheatWindow"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Ryujinx - Cheat Manager</property> + <property name="default_width">440</property> + <property name="default_height">550</property> + <child> + <object class="GtkBox" id="MainBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="CheatBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="_baseTitleInfoLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="margin_bottom">10</property> + <property name="label" translatable="yes">Available Cheats</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="margin_left">10</property> + <property name="margin_right">10</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkViewport"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkTreeView" id="_cheatTreeView"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child internal-child="selection"> + <object class="GtkTreeSelection"/> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkButtonBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="margin_bottom">10</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="_saveButton"> + <property name="label" translatable="yes">Save</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="margin_right">10</property> + <property name="margin_top">2</property> + <property name="margin_bottom">2</property> + <signal name="clicked" handler="SaveButton_Clicked" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="_cancelButton"> + <property name="label" translatable="yes">Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="margin_right">10</property> + <property name="margin_top">2</property> + <property name="margin_bottom">2</property> + <signal name="clicked" handler="CancelButton_Clicked" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="titlebar"> + <placeholder/> + </child> + </object> +</interface>