From ec6cb0abb4b7669895b6e96fd7581c93b5abd691 Mon Sep 17 00:00:00 2001 From: Mary Guillemard Date: Sat, 2 Mar 2024 12:51:05 +0100 Subject: infra: Make Avalonia the default UI (#6375) * misc: Move Ryujinx project to Ryujinx.Gtk3 This breaks release CI for now but that's fine. Signed-off-by: Mary Guillemard * misc: Move Ryujinx.Ava project to Ryujinx This breaks CI for now, but it's fine. Signed-off-by: Mary Guillemard * infra: Make Avalonia the default UI Should fix CI after the previous changes. GTK3 isn't build by the release job anymore, only by PR CI. This also ensure that the test-ava update package is still generated to allow update from the old testing channel. Signed-off-by: Mary Guillemard * Fix missing copy in create_app_bundle.sh Signed-off-by: Mary Guillemard * Fix syntax error Signed-off-by: Mary Guillemard --------- Signed-off-by: Mary Guillemard --- .../UI/Windows/AboutWindow.Designer.cs | 511 ++++ src/Ryujinx.Gtk3/UI/Windows/AboutWindow.cs | 85 + .../UI/Windows/AmiiboWindow.Designer.cs | 190 ++ src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.cs | 438 +++ src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs | 291 ++ src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs | 156 + src/Ryujinx.Gtk3/UI/Windows/CheatWindow.glade | 150 + src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs | 1230 ++++++++ src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.glade | 2241 ++++++++++++++ src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs | 280 ++ src/Ryujinx.Gtk3/UI/Windows/DlcWindow.glade | 202 ++ src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs | 847 +++++ src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade | 3221 ++++++++++++++++++++ src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs | 206 ++ .../UI/Windows/TitleUpdateWindow.glade | 214 ++ .../Windows/UserProfilesManagerWindow.Designer.cs | 255 ++ .../UI/Windows/UserProfilesManagerWindow.cs | 328 ++ 17 files changed, 10845 insertions(+) create mode 100644 src/Ryujinx.Gtk3/UI/Windows/AboutWindow.Designer.cs create mode 100644 src/Ryujinx.Gtk3/UI/Windows/AboutWindow.cs create mode 100644 src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.Designer.cs create mode 100644 src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.cs create mode 100644 src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs create mode 100644 src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs create mode 100644 src/Ryujinx.Gtk3/UI/Windows/CheatWindow.glade create mode 100644 src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs create mode 100644 src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.glade create mode 100644 src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs create mode 100644 src/Ryujinx.Gtk3/UI/Windows/DlcWindow.glade create mode 100644 src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs create mode 100644 src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade create mode 100644 src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs create mode 100644 src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.glade create mode 100644 src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.Designer.cs create mode 100644 src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.cs (limited to 'src/Ryujinx.Gtk3/UI/Windows') diff --git a/src/Ryujinx.Gtk3/UI/Windows/AboutWindow.Designer.cs b/src/Ryujinx.Gtk3/UI/Windows/AboutWindow.Designer.cs new file mode 100644 index 00000000..fd912ef9 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/AboutWindow.Designer.cs @@ -0,0 +1,511 @@ +using Gtk; +using Pango; +using Ryujinx.UI.Common.Configuration; +using System.Reflection; + +namespace Ryujinx.UI.Windows +{ + public partial class AboutWindow : Window + { + private Box _mainBox; + private Box _leftBox; + private Box _logoBox; + private Image _ryujinxLogo; + private Box _logoTextBox; + private Label _ryujinxLabel; + private Label _ryujinxPhoneticLabel; + private EventBox _ryujinxLink; + private Label _ryujinxLinkLabel; + private Label _versionLabel; + private Label _disclaimerLabel; + private EventBox _amiiboApiLink; + private Label _amiiboApiLinkLabel; + private Box _socialBox; + private EventBox _patreonEventBox; + private Box _patreonBox; + private Image _patreonLogo; + private Label _patreonLabel; + private EventBox _githubEventBox; + private Box _githubBox; + private Image _githubLogo; + private Label _githubLabel; + private Box _discordBox; + private EventBox _discordEventBox; + private Image _discordLogo; + private Label _discordLabel; + private EventBox _twitterEventBox; + private Box _twitterBox; + private Image _twitterLogo; + private Label _twitterLabel; + private Separator _separator; + private Box _rightBox; + private Label _aboutLabel; + private Label _aboutDescriptionLabel; + private Label _createdByLabel; + private TextView _createdByText; + private EventBox _contributorsEventBox; + private Label _contributorsLinkLabel; + private Label _patreonNamesLabel; + private ScrolledWindow _patreonNamesScrolled; + private TextView _patreonNamesText; + private EventBox _changelogEventBox; + private Label _changelogLinkLabel; + + private void InitializeComponent() + { + + // + // AboutWindow + // + CanFocus = false; + Resizable = false; + Modal = true; + WindowPosition = WindowPosition.Center; + DefaultWidth = 800; + DefaultHeight = 450; + TypeHint = Gdk.WindowTypeHint.Dialog; + + // + // _mainBox + // + _mainBox = new Box(Orientation.Horizontal, 0); + + // + // _leftBox + // + _leftBox = new Box(Orientation.Vertical, 0) + { + Margin = 15, + MarginStart = 30, + MarginEnd = 0, + }; + + // + // _logoBox + // + _logoBox = new Box(Orientation.Horizontal, 0); + + // + // _ryujinxLogo + // + _ryujinxLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png", 100, 100)) + { + Margin = 10, + MarginStart = 15, + }; + + // + // _logoTextBox + // + _logoTextBox = new Box(Orientation.Vertical, 0); + + // + // _ryujinxLabel + // + _ryujinxLabel = new Label("Ryujinx") + { + MarginTop = 15, + Justify = Justification.Center, + Attributes = new AttrList(), + }; + _ryujinxLabel.Attributes.Insert(new Pango.AttrScale(2.7f)); + + // + // _ryujinxPhoneticLabel + // + _ryujinxPhoneticLabel = new Label("(REE-YOU-JINX)") + { + Justify = Justification.Center, + }; + + // + // _ryujinxLink + // + _ryujinxLink = new EventBox() + { + Margin = 5 + }; + _ryujinxLink.ButtonPressEvent += RyujinxButton_Pressed; + + // + // _ryujinxLinkLabel + // + _ryujinxLinkLabel = new Label("www.ryujinx.org") + { + TooltipText = "Click to open the Ryujinx website in your default browser.", + Justify = Justification.Center, + Attributes = new AttrList(), + }; + _ryujinxLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); + + // + // _versionLabel + // + _versionLabel = new Label(Program.Version) + { + Expand = true, + Justify = Justification.Center, + Margin = 5, + }; + + // + // _changelogEventBox + // + _changelogEventBox = new EventBox(); + _changelogEventBox.ButtonPressEvent += ChangelogButton_Pressed; + + // + // _changelogLinkLabel + // + _changelogLinkLabel = new Label("View Changelog on GitHub") + { + TooltipText = "Click to open the changelog for this version in your default browser.", + Justify = Justification.Center, + Attributes = new AttrList(), + }; + _changelogLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); + + // + // _disclaimerLabel + // + _disclaimerLabel = new Label("Ryujinx is not affiliated with Nintendo™,\nor any of its partners, in any way.") + { + Expand = true, + Justify = Justification.Center, + Margin = 5, + Attributes = new AttrList(), + }; + _disclaimerLabel.Attributes.Insert(new Pango.AttrScale(0.8f)); + + // + // _amiiboApiLink + // + _amiiboApiLink = new EventBox() + { + Margin = 5, + }; + _amiiboApiLink.ButtonPressEvent += AmiiboApiButton_Pressed; + + // + // _amiiboApiLinkLabel + // + _amiiboApiLinkLabel = new Label("AmiiboAPI (www.amiiboapi.com) is used\nin our Amiibo emulation.") + { + TooltipText = "Click to open the AmiiboAPI website in your default browser.", + Justify = Justification.Center, + Attributes = new AttrList(), + }; + _amiiboApiLinkLabel.Attributes.Insert(new Pango.AttrScale(0.9f)); + + // + // _socialBox + // + _socialBox = new Box(Orientation.Horizontal, 0) + { + Margin = 25, + MarginBottom = 10, + }; + + // + // _patreonEventBox + // + _patreonEventBox = new EventBox() + { + TooltipText = "Click to open the Ryujinx Patreon page in your default browser.", + }; + _patreonEventBox.ButtonPressEvent += PatreonButton_Pressed; + + // + // _patreonBox + // + _patreonBox = new Box(Orientation.Vertical, 0); + + // + // _patreonLogo + // + _patreonLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Patreon_Light.png", 30, 30)) + { + Margin = 10, + }; + + // + // _patreonLabel + // + _patreonLabel = new Label("Patreon") + { + Justify = Justification.Center, + }; + + // + // _githubEventBox + // + _githubEventBox = new EventBox() + { + TooltipText = "Click to open the Ryujinx GitHub page in your default browser.", + }; + _githubEventBox.ButtonPressEvent += GitHubButton_Pressed; + + // + // _githubBox + // + _githubBox = new Box(Orientation.Vertical, 0); + + // + // _githubLogo + // + _githubLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_GitHub_Light.png", 30, 30)) + { + Margin = 10, + }; + + // + // _githubLabel + // + _githubLabel = new Label("GitHub") + { + Justify = Justification.Center, + }; + + // + // _discordBox + // + _discordBox = new Box(Orientation.Vertical, 0); + + // + // _discordEventBox + // + _discordEventBox = new EventBox() + { + TooltipText = "Click to open an invite to the Ryujinx Discord server in your default browser.", + }; + _discordEventBox.ButtonPressEvent += DiscordButton_Pressed; + + // + // _discordLogo + // + _discordLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Discord_Light.png", 30, 30)) + { + Margin = 10, + }; + + // + // _discordLabel + // + _discordLabel = new Label("Discord") + { + Justify = Justification.Center, + }; + + // + // _twitterEventBox + // + _twitterEventBox = new EventBox() + { + TooltipText = "Click to open the Ryujinx Twitter page in your default browser.", + }; + _twitterEventBox.ButtonPressEvent += TwitterButton_Pressed; + + // + // _twitterBox + // + _twitterBox = new Box(Orientation.Vertical, 0); + + // + // _twitterLogo + // + _twitterLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Twitter_Light.png", 30, 30)) + { + Margin = 10, + }; + + // + // _twitterLabel + // + _twitterLabel = new Label("Twitter") + { + Justify = Justification.Center, + }; + + // + // _separator + // + _separator = new Separator(Orientation.Vertical) + { + Margin = 15, + }; + + // + // _rightBox + // + _rightBox = new Box(Orientation.Vertical, 0) + { + Margin = 15, + MarginTop = 40, + }; + + // + // _aboutLabel + // + _aboutLabel = new Label("About :") + { + Halign = Align.Start, + Attributes = new AttrList(), + }; + _aboutLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); + _aboutLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); + + // + // _aboutDescriptionLabel + // + _aboutDescriptionLabel = new Label("Ryujinx is an emulator for the Nintendo Switch™.\n" + + "Please support us on Patreon.\n" + + "Get all the latest news on our Twitter or Discord.\n" + + "Developers interested in contributing can find out more on our GitHub or Discord.") + { + Margin = 15, + Halign = Align.Start, + }; + + // + // _createdByLabel + // + _createdByLabel = new Label("Maintained by :") + { + Halign = Align.Start, + Attributes = new AttrList(), + }; + _createdByLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); + _createdByLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); + + // + // _createdByText + // + _createdByText = new TextView() + { + WrapMode = Gtk.WrapMode.Word, + Editable = false, + CursorVisible = false, + Margin = 15, + MarginEnd = 30, + }; + _createdByText.Buffer.Text = "gdkchan, Ac_K, Thog, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, Xpl0itR, GoffyDude, »jD« and more..."; + + // + // _contributorsEventBox + // + _contributorsEventBox = new EventBox(); + _contributorsEventBox.ButtonPressEvent += ContributorsButton_Pressed; + + // + // _contributorsLinkLabel + // + _contributorsLinkLabel = new Label("See All Contributors...") + { + TooltipText = "Click to open the Contributors page in your default browser.", + MarginEnd = 30, + Halign = Align.End, + Attributes = new AttrList(), + }; + _contributorsLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); + + // + // _patreonNamesLabel + // + _patreonNamesLabel = new Label("Supported on Patreon by :") + { + Halign = Align.Start, + Attributes = new AttrList(), + }; + _patreonNamesLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); + _patreonNamesLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); + + // + // _patreonNamesScrolled + // + _patreonNamesScrolled = new ScrolledWindow() + { + Margin = 15, + MarginEnd = 30, + Expand = true, + ShadowType = ShadowType.In, + }; + _patreonNamesScrolled.SetPolicy(PolicyType.Never, PolicyType.Automatic); + + // + // _patreonNamesText + // + _patreonNamesText = new TextView() + { + WrapMode = Gtk.WrapMode.Word, + }; + _patreonNamesText.Buffer.Text = "Loading..."; + _patreonNamesText.SetProperty("editable", new GLib.Value(false)); + + ShowComponent(); + } + + private void ShowComponent() + { + _logoBox.Add(_ryujinxLogo); + + _ryujinxLink.Add(_ryujinxLinkLabel); + + _logoTextBox.Add(_ryujinxLabel); + _logoTextBox.Add(_ryujinxPhoneticLabel); + _logoTextBox.Add(_ryujinxLink); + + _logoBox.Add(_logoTextBox); + + _amiiboApiLink.Add(_amiiboApiLinkLabel); + + _patreonBox.Add(_patreonLogo); + _patreonBox.Add(_patreonLabel); + _patreonEventBox.Add(_patreonBox); + + _githubBox.Add(_githubLogo); + _githubBox.Add(_githubLabel); + _githubEventBox.Add(_githubBox); + + _discordBox.Add(_discordLogo); + _discordBox.Add(_discordLabel); + _discordEventBox.Add(_discordBox); + + _twitterBox.Add(_twitterLogo); + _twitterBox.Add(_twitterLabel); + _twitterEventBox.Add(_twitterBox); + + _socialBox.Add(_patreonEventBox); + _socialBox.Add(_githubEventBox); + _socialBox.Add(_discordEventBox); + _socialBox.Add(_twitterEventBox); + + _changelogEventBox.Add(_changelogLinkLabel); + + _leftBox.Add(_logoBox); + _leftBox.Add(_versionLabel); + _leftBox.Add(_changelogEventBox); + _leftBox.Add(_disclaimerLabel); + _leftBox.Add(_amiiboApiLink); + _leftBox.Add(_socialBox); + + _contributorsEventBox.Add(_contributorsLinkLabel); + _patreonNamesScrolled.Add(_patreonNamesText); + + _rightBox.Add(_aboutLabel); + _rightBox.Add(_aboutDescriptionLabel); + _rightBox.Add(_createdByLabel); + _rightBox.Add(_createdByText); + _rightBox.Add(_contributorsEventBox); + _rightBox.Add(_patreonNamesLabel); + _rightBox.Add(_patreonNamesScrolled); + + _mainBox.Add(_leftBox); + _mainBox.Add(_separator); + _mainBox.Add(_rightBox); + + Add(_mainBox); + + ShowAll(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/AboutWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/AboutWindow.cs new file mode 100644 index 00000000..f4bb533c --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/AboutWindow.cs @@ -0,0 +1,85 @@ +using Gtk; +using Ryujinx.Common.Utilities; +using Ryujinx.UI.Common.Helper; +using System.Net.Http; +using System.Net.NetworkInformation; +using System.Reflection; +using System.Threading.Tasks; + +namespace Ryujinx.UI.Windows +{ + public partial class AboutWindow : Window + { + public AboutWindow() : base($"Ryujinx {Program.Version} - About") + { + Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(OpenHelper)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png"); + InitializeComponent(); + + _ = DownloadPatronsJson(); + } + + private async Task DownloadPatronsJson() + { + if (!NetworkInterface.GetIsNetworkAvailable()) + { + _patreonNamesText.Buffer.Text = "Connection Error."; + } + + HttpClient httpClient = new(); + + try + { + string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/"); + + _patreonNamesText.Buffer.Text = string.Join(", ", JsonHelper.Deserialize(patreonJsonString, CommonJsonContext.Default.StringArray)); + } + catch + { + _patreonNamesText.Buffer.Text = "API Error."; + } + } + + // + // Events + // + private void RyujinxButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://ryujinx.org"); + } + + private void AmiiboApiButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://amiiboapi.com"); + } + + private void PatreonButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://www.patreon.com/ryujinx"); + } + + private void GitHubButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx"); + } + + private void DiscordButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://discordapp.com/invite/N2FmfVc"); + } + + private void TwitterButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://twitter.com/RyujinxEmu"); + } + + private void ContributorsButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a"); + } + + private void ChangelogButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx/wiki/Changelog#ryujinx-changelog"); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.Designer.cs b/src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.Designer.cs new file mode 100644 index 00000000..3bf73318 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.Designer.cs @@ -0,0 +1,190 @@ +using Gtk; + +namespace Ryujinx.UI.Windows +{ + public partial class AmiiboWindow : Window + { + private Box _mainBox; + private ButtonBox _buttonBox; + private Button _scanButton; + private Button _cancelButton; + private CheckButton _randomUuidCheckBox; + private Box _amiiboBox; + private Box _amiiboHeadBox; + private Box _amiiboSeriesBox; + private Label _amiiboSeriesLabel; + private ComboBoxText _amiiboSeriesComboBox; + private Box _amiiboCharsBox; + private Label _amiiboCharsLabel; + private ComboBoxText _amiiboCharsComboBox; + private CheckButton _showAllCheckBox; + private Image _amiiboImage; + private Label _gameUsageLabel; + + private void InitializeComponent() + { + // + // AmiiboWindow + // + CanFocus = false; + Resizable = false; + Modal = true; + WindowPosition = WindowPosition.Center; + DefaultWidth = 600; + DefaultHeight = 470; + TypeHint = Gdk.WindowTypeHint.Dialog; + + // + // _mainBox + // + _mainBox = new Box(Orientation.Vertical, 2); + + // + // _buttonBox + // + _buttonBox = new ButtonBox(Orientation.Horizontal) + { + Margin = 20, + LayoutStyle = ButtonBoxStyle.End, + }; + + // + // _scanButton + // + _scanButton = new Button() + { + Label = "Scan It!", + CanFocus = true, + ReceivesDefault = true, + MarginStart = 10, + }; + _scanButton.Clicked += ScanButton_Pressed; + + // + // _randomUuidCheckBox + // + _randomUuidCheckBox = new CheckButton() + { + Label = "Hack: Use Random Tag Uuid", + TooltipText = "This allows multiple scans of a single Amiibo.\n(used in The Legend of Zelda: Breath of the Wild)", + }; + + // + // _cancelButton + // + _cancelButton = new Button() + { + Label = "Cancel", + CanFocus = true, + ReceivesDefault = true, + MarginStart = 10, + }; + _cancelButton.Clicked += CancelButton_Pressed; + + // + // _amiiboBox + // + _amiiboBox = new Box(Orientation.Vertical, 0); + + // + // _amiiboHeadBox + // + _amiiboHeadBox = new Box(Orientation.Horizontal, 0) + { + Margin = 20, + Hexpand = true, + }; + + // + // _amiiboSeriesBox + // + _amiiboSeriesBox = new Box(Orientation.Horizontal, 0) + { + Hexpand = true, + }; + + // + // _amiiboSeriesLabel + // + _amiiboSeriesLabel = new Label("Amiibo Series:"); + + // + // _amiiboSeriesComboBox + // + _amiiboSeriesComboBox = new ComboBoxText(); + + // + // _amiiboCharsBox + // + _amiiboCharsBox = new Box(Orientation.Horizontal, 0) + { + Hexpand = true, + }; + + // + // _amiiboCharsLabel + // + _amiiboCharsLabel = new Label("Character:"); + + // + // _amiiboCharsComboBox + // + _amiiboCharsComboBox = new ComboBoxText(); + + // + // _showAllCheckBox + // + _showAllCheckBox = new CheckButton() + { + Label = "Show All Amiibo", + }; + + // + // _amiiboImage + // + _amiiboImage = new Image() + { + HeightRequest = 350, + WidthRequest = 350, + }; + + // + // _gameUsageLabel + // + _gameUsageLabel = new Label("") + { + MarginTop = 20, + }; + + ShowComponent(); + } + + private void ShowComponent() + { + _buttonBox.Add(_showAllCheckBox); + _buttonBox.Add(_randomUuidCheckBox); + _buttonBox.Add(_scanButton); + _buttonBox.Add(_cancelButton); + + _amiiboSeriesBox.Add(_amiiboSeriesLabel); + _amiiboSeriesBox.Add(_amiiboSeriesComboBox); + + _amiiboCharsBox.Add(_amiiboCharsLabel); + _amiiboCharsBox.Add(_amiiboCharsComboBox); + + _amiiboHeadBox.Add(_amiiboSeriesBox); + _amiiboHeadBox.Add(_amiiboCharsBox); + + _amiiboBox.PackStart(_amiiboHeadBox, true, true, 0); + _amiiboBox.PackEnd(_gameUsageLabel, false, false, 0); + _amiiboBox.PackEnd(_amiiboImage, false, false, 0); + + _mainBox.Add(_amiiboBox); + _mainBox.PackEnd(_buttonBox, false, false, 0); + + Add(_mainBox); + + ShowAll(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.cs new file mode 100644 index 00000000..d8c0b0c0 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.cs @@ -0,0 +1,438 @@ +using Gdk; +using Gtk; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Models.Amiibo; +using Ryujinx.UI.Widgets; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Window = Gtk.Window; + +namespace Ryujinx.UI.Windows +{ + public partial class AmiiboWindow : Window + { + private const string DefaultJson = "{ \"amiibo\": [] }"; + + public string AmiiboId { get; private set; } + + public int DeviceId { get; set; } + public string TitleId { get; set; } + public string LastScannedAmiiboId { get; set; } + public bool LastScannedAmiiboShowAll { get; set; } + + public ResponseType Response { get; private set; } + + public bool UseRandomUuid + { + get + { + return _randomUuidCheckBox.Active; + } + } + + private readonly HttpClient _httpClient; + private readonly string _amiiboJsonPath; + + private readonly byte[] _amiiboLogoBytes; + + private List _amiiboList; + + private static readonly AmiiboJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public AmiiboWindow() : base($"Ryujinx {Program.Version} - Amiibo") + { + Icon = new Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png"); + + InitializeComponent(); + + _httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(30), + }; + + Directory.CreateDirectory(System.IO.Path.Join(AppDataManager.BaseDirPath, "system", "amiibo")); + + _amiiboJsonPath = System.IO.Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json"); + _amiiboList = new List(); + + _amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.UI.Common/Resources/Logo_Amiibo.png"); + _amiiboImage.Pixbuf = new Pixbuf(_amiiboLogoBytes); + + _scanButton.Sensitive = false; + _randomUuidCheckBox.Sensitive = false; + + _ = LoadContentAsync(); + } + + private static bool TryGetAmiiboJson(string json, out AmiiboJson amiiboJson) + { + if (string.IsNullOrEmpty(json)) + { + amiiboJson = JsonHelper.Deserialize(DefaultJson, _serializerContext.AmiiboJson); + + return false; + } + + try + { + amiiboJson = JsonHelper.Deserialize(json, _serializerContext.AmiiboJson); + + return true; + } + catch (JsonException exception) + { + Logger.Error?.Print(LogClass.Application, $"Unable to deserialize amiibo data: {exception}"); + amiiboJson = JsonHelper.Deserialize(DefaultJson, _serializerContext.AmiiboJson); + + return false; + } + } + + private async Task GetMostRecentAmiiboListOrDefaultJson() + { + bool localIsValid = false; + bool remoteIsValid = false; + AmiiboJson amiiboJson = new(); + + try + { + try + { + if (File.Exists(_amiiboJsonPath)) + { + localIsValid = TryGetAmiiboJson(await File.ReadAllTextAsync(_amiiboJsonPath), out amiiboJson); + } + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"Unable to read data from '{_amiiboJsonPath}': {exception}"); + } + + if (!localIsValid || await NeedsUpdate(amiiboJson.LastUpdated)) + { + remoteIsValid = TryGetAmiiboJson(await DownloadAmiiboJson(), out amiiboJson); + } + } + catch (Exception exception) + { + if (!(localIsValid || remoteIsValid)) + { + Logger.Error?.Print(LogClass.Application, $"Couldn't get valid amiibo data: {exception}"); + + // Neither local or remote files are valid JSON, close window. + ShowInfoDialog(); + Close(); + } + else if (!remoteIsValid) + { + Logger.Warning?.Print(LogClass.Application, $"Couldn't update amiibo data: {exception}"); + + // Only the local file is valid, the local one should be used + // but the user should be warned. + ShowInfoDialog(); + } + } + + return amiiboJson; + } + + private async Task LoadContentAsync() + { + AmiiboJson amiiboJson = await GetMostRecentAmiiboListOrDefaultJson(); + + _amiiboList = amiiboJson.Amiibo.OrderBy(amiibo => amiibo.AmiiboSeries).ToList(); + + if (LastScannedAmiiboShowAll) + { + _showAllCheckBox.Click(); + } + + ParseAmiiboData(); + + _showAllCheckBox.Clicked += ShowAllCheckBox_Clicked; + } + + private void ParseAmiiboData() + { + List comboxItemList = new(); + + for (int i = 0; i < _amiiboList.Count; i++) + { + if (!comboxItemList.Contains(_amiiboList[i].AmiiboSeries)) + { + if (!_showAllCheckBox.Active) + { + foreach (var game in _amiiboList[i].GamesSwitch) + { + if (game != null) + { + if (game.GameId.Contains(TitleId)) + { + comboxItemList.Add(_amiiboList[i].AmiiboSeries); + _amiiboSeriesComboBox.Append(_amiiboList[i].AmiiboSeries, _amiiboList[i].AmiiboSeries); + + break; + } + } + } + } + else + { + comboxItemList.Add(_amiiboList[i].AmiiboSeries); + _amiiboSeriesComboBox.Append(_amiiboList[i].AmiiboSeries, _amiiboList[i].AmiiboSeries); + } + } + } + + _amiiboSeriesComboBox.Changed += SeriesComboBox_Changed; + _amiiboCharsComboBox.Changed += CharacterComboBox_Changed; + + if (LastScannedAmiiboId != "") + { + SelectLastScannedAmiibo(); + } + else + { + _amiiboSeriesComboBox.Active = 0; + } + } + + private void SelectLastScannedAmiibo() + { + bool isSet = _amiiboSeriesComboBox.SetActiveId(_amiiboList.Find(amiibo => amiibo.Head + amiibo.Tail == LastScannedAmiiboId).AmiiboSeries); + isSet = _amiiboCharsComboBox.SetActiveId(LastScannedAmiiboId); + + if (isSet == false) + { + _amiiboSeriesComboBox.Active = 0; + } + } + + private async Task NeedsUpdate(DateTime oldLastModified) + { + try + { + HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://amiibo.ryujinx.org/")); + + if (response.IsSuccessStatusCode) + { + return response.Content.Headers.LastModified != oldLastModified; + } + } + catch (HttpRequestException exception) + { + Logger.Error?.Print(LogClass.Application, $"Unable to check for amiibo data updates: {exception}"); + } + + return false; + } + + private async Task DownloadAmiiboJson() + { + try + { + HttpResponseMessage response = await _httpClient.GetAsync("https://amiibo.ryujinx.org/"); + + if (response.IsSuccessStatusCode) + { + string amiiboJsonString = await response.Content.ReadAsStringAsync(); + + try + { + using FileStream dlcJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough); + dlcJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString)); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"Couldn't write amiibo data to file '{_amiiboJsonPath}: {exception}'"); + } + + return amiiboJsonString; + } + + Logger.Error?.Print(LogClass.Application, $"Failed to download amiibo data. Response status code: {response.StatusCode}"); + } + catch (HttpRequestException exception) + { + Logger.Error?.Print(LogClass.Application, $"Failed to request amiibo data: {exception}"); + } + + GtkDialog.CreateInfoDialog("Amiibo API", "An error occured while fetching information from the API."); + + return null; + } + + private async Task UpdateAmiiboPreview(string imageUrl) + { + HttpResponseMessage response = await _httpClient.GetAsync(imageUrl); + + if (response.IsSuccessStatusCode) + { + byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync(); + Pixbuf amiiboPreview = new(amiiboPreviewBytes); + + float ratio = Math.Min((float)_amiiboImage.AllocatedWidth / amiiboPreview.Width, + (float)_amiiboImage.AllocatedHeight / amiiboPreview.Height); + + int resizeHeight = (int)(amiiboPreview.Height * ratio); + int resizeWidth = (int)(amiiboPreview.Width * ratio); + + _amiiboImage.Pixbuf = amiiboPreview.ScaleSimple(resizeWidth, resizeHeight, InterpType.Bilinear); + } + else + { + Logger.Error?.Print(LogClass.Application, $"Failed to get amiibo preview. Response status code: {response.StatusCode}"); + } + } + + private static void ShowInfoDialog() + { + GtkDialog.CreateInfoDialog("Amiibo API", "Unable to connect to Amiibo API server. The service may be down or you may need to verify your internet connection is online."); + } + + // + // Events + // + private void SeriesComboBox_Changed(object sender, EventArgs args) + { + _amiiboCharsComboBox.Changed -= CharacterComboBox_Changed; + + _amiiboCharsComboBox.RemoveAll(); + + List amiiboSortedList = _amiiboList.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeriesComboBox.ActiveId).OrderBy(amiibo => amiibo.Name).ToList(); + + List comboxItemList = new(); + + for (int i = 0; i < amiiboSortedList.Count; i++) + { + if (!comboxItemList.Contains(amiiboSortedList[i].Head + amiiboSortedList[i].Tail)) + { + if (!_showAllCheckBox.Active) + { + foreach (var game in amiiboSortedList[i].GamesSwitch) + { + if (game != null) + { + if (game.GameId.Contains(TitleId)) + { + comboxItemList.Add(amiiboSortedList[i].Head + amiiboSortedList[i].Tail); + _amiiboCharsComboBox.Append(amiiboSortedList[i].Head + amiiboSortedList[i].Tail, amiiboSortedList[i].Name); + + break; + } + } + } + } + else + { + comboxItemList.Add(amiiboSortedList[i].Head + amiiboSortedList[i].Tail); + _amiiboCharsComboBox.Append(amiiboSortedList[i].Head + amiiboSortedList[i].Tail, amiiboSortedList[i].Name); + } + } + } + + _amiiboCharsComboBox.Changed += CharacterComboBox_Changed; + + _amiiboCharsComboBox.Active = 0; + + _scanButton.Sensitive = true; + _randomUuidCheckBox.Sensitive = true; + } + + private void CharacterComboBox_Changed(object sender, EventArgs args) + { + AmiiboId = _amiiboCharsComboBox.ActiveId; + + _amiiboImage.Pixbuf = new Pixbuf(_amiiboLogoBytes); + + string imageUrl = _amiiboList.Find(amiibo => amiibo.Head + amiibo.Tail == _amiiboCharsComboBox.ActiveId).Image; + + var usageStringBuilder = new StringBuilder(); + + for (int i = 0; i < _amiiboList.Count; i++) + { + if (_amiiboList[i].Head + _amiiboList[i].Tail == _amiiboCharsComboBox.ActiveId) + { + bool writable = false; + + foreach (var item in _amiiboList[i].GamesSwitch) + { + if (item.GameId.Contains(TitleId)) + { + foreach (AmiiboApiUsage usageItem in item.AmiiboUsage) + { + usageStringBuilder.Append(Environment.NewLine); + usageStringBuilder.Append($"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}"); + + writable = usageItem.Write; + } + } + } + + if (usageStringBuilder.Length == 0) + { + usageStringBuilder.Append("Unknown."); + } + + _gameUsageLabel.Text = $"Usage{(writable ? " (Writable)" : "")} : {usageStringBuilder}"; + } + } + + _ = UpdateAmiiboPreview(imageUrl); + } + + private void ShowAllCheckBox_Clicked(object sender, EventArgs e) + { + _amiiboImage.Pixbuf = new Pixbuf(_amiiboLogoBytes); + + _amiiboSeriesComboBox.Changed -= SeriesComboBox_Changed; + _amiiboCharsComboBox.Changed -= CharacterComboBox_Changed; + + _amiiboSeriesComboBox.RemoveAll(); + _amiiboCharsComboBox.RemoveAll(); + + _scanButton.Sensitive = false; + _randomUuidCheckBox.Sensitive = false; + + new Task(ParseAmiiboData).Start(); + } + + private void ScanButton_Pressed(object sender, EventArgs args) + { + LastScannedAmiiboShowAll = _showAllCheckBox.Active; + + Response = ResponseType.Ok; + + Close(); + } + + private void CancelButton_Pressed(object sender, EventArgs args) + { + AmiiboId = ""; + LastScannedAmiiboId = ""; + LastScannedAmiiboShowAll = false; + + Response = ResponseType.Cancel; + + Close(); + } + + protected override void Dispose(bool disposing) + { + _httpClient.Dispose(); + + base.Dispose(disposing); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs new file mode 100644 index 00000000..7cddc362 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs @@ -0,0 +1,291 @@ +using Gtk; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ncm; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Memory; +using Ryujinx.HLE.FileSystem; +using Ryujinx.UI.Common.Configuration; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Image = SixLabors.ImageSharp.Image; + +namespace Ryujinx.UI.Windows +{ + public class AvatarWindow : Window + { + public byte[] SelectedProfileImage; + public bool NewUser; + + private static readonly Dictionary _avatarDict = new(); + + private readonly ListStore _listStore; + private readonly IconView _iconView; + private readonly Button _setBackgroungColorButton; + private Gdk.RGBA _backgroundColor; + + public AvatarWindow() : base($"Ryujinx {Program.Version} - Manage Accounts - Avatar") + { + Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png"); + + CanFocus = false; + Resizable = false; + Modal = true; + TypeHint = Gdk.WindowTypeHint.Dialog; + + SetDefaultSize(740, 400); + SetPosition(WindowPosition.Center); + + Box vbox = new(Orientation.Vertical, 0); + Add(vbox); + + ScrolledWindow scrolledWindow = new() + { + ShadowType = ShadowType.EtchedIn, + }; + scrolledWindow.SetPolicy(PolicyType.Automatic, PolicyType.Automatic); + + Box hbox = new(Orientation.Horizontal, 0); + + Button chooseButton = new() + { + Label = "Choose", + CanFocus = true, + ReceivesDefault = true, + }; + chooseButton.Clicked += ChooseButton_Pressed; + + _setBackgroungColorButton = new Button() + { + Label = "Set Background Color", + CanFocus = true, + }; + _setBackgroungColorButton.Clicked += SetBackgroungColorButton_Pressed; + + _backgroundColor.Red = 1; + _backgroundColor.Green = 1; + _backgroundColor.Blue = 1; + _backgroundColor.Alpha = 1; + + Button closeButton = new() + { + Label = "Close", + CanFocus = true, + }; + closeButton.Clicked += CloseButton_Pressed; + + vbox.PackStart(scrolledWindow, true, true, 0); + hbox.PackStart(chooseButton, true, true, 0); + hbox.PackStart(_setBackgroungColorButton, true, true, 0); + hbox.PackStart(closeButton, true, true, 0); + vbox.PackStart(hbox, false, false, 0); + + _listStore = new ListStore(typeof(string), typeof(Gdk.Pixbuf)); + _listStore.SetSortColumnId(0, SortType.Ascending); + + _iconView = new IconView(_listStore) + { + ItemWidth = 64, + ItemPadding = 10, + PixbufColumn = 1, + }; + + _iconView.SelectionChanged += IconView_SelectionChanged; + + scrolledWindow.Add(_iconView); + + _iconView.GrabFocus(); + + ProcessAvatars(); + + ShowAll(); + } + + public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem) + { + if (_avatarDict.Count > 0) + { + return; + } + + string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data); + string avatarPath = VirtualFileSystem.SwitchPathToSystemPath(contentPath); + + if (!string.IsNullOrWhiteSpace(avatarPath)) + { + using IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open); + + Nca nca = new(virtualFileSystem.KeySet, ncaFileStream); + IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid); + + foreach (var item in romfs.EnumerateEntries()) + { + // TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy. + + if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs")) + { + using var file = new UniqueRef(); + + romfs.OpenFile(ref file.Ref, ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); + using MemoryStream streamPng = MemoryStreamManager.Shared.GetStream(); + file.Get.AsStream().CopyTo(stream); + + stream.Position = 0; + + Image avatarImage = Image.LoadPixelData(DecompressYaz0(stream), 256, 256); + + avatarImage.SaveAsPng(streamPng); + + _avatarDict.Add(item.FullPath, streamPng.ToArray()); + } + } + } + } + + private void ProcessAvatars() + { + _listStore.Clear(); + + foreach (var avatar in _avatarDict) + { + _listStore.AppendValues(avatar.Key, new Gdk.Pixbuf(ProcessImage(avatar.Value), 96, 96)); + } + + _iconView.SelectPath(new TreePath(new[] { 0 })); + } + + private byte[] ProcessImage(byte[] data) + { + using MemoryStream streamJpg = MemoryStreamManager.Shared.GetStream(); + + Image avatarImage = Image.Load(data, new PngDecoder()); + + avatarImage.Mutate(x => x.BackgroundColor(new Rgba32( + (byte)(_backgroundColor.Red * 255), + (byte)(_backgroundColor.Green * 255), + (byte)(_backgroundColor.Blue * 255), + (byte)(_backgroundColor.Alpha * 255) + ))); + avatarImage.SaveAsJpeg(streamJpg); + + return streamJpg.ToArray(); + } + + private void CloseButton_Pressed(object sender, EventArgs e) + { + SelectedProfileImage = null; + + Close(); + } + + private void IconView_SelectionChanged(object sender, EventArgs e) + { + if (_iconView.SelectedItems.Length > 0) + { + _listStore.GetIter(out TreeIter iter, _iconView.SelectedItems[0]); + + SelectedProfileImage = ProcessImage(_avatarDict[(string)_listStore.GetValue(iter, 0)]); + } + } + + private void SetBackgroungColorButton_Pressed(object sender, EventArgs e) + { + using ColorChooserDialog colorChooserDialog = new("Set Background Color", this); + + colorChooserDialog.UseAlpha = false; + colorChooserDialog.Rgba = _backgroundColor; + + if (colorChooserDialog.Run() == (int)ResponseType.Ok) + { + _backgroundColor = colorChooserDialog.Rgba; + + ProcessAvatars(); + } + + colorChooserDialog.Hide(); + } + + private void ChooseButton_Pressed(object sender, EventArgs e) + { + Close(); + } + + private static byte[] DecompressYaz0(Stream stream) + { + using BinaryReader reader = new(stream); + + reader.ReadInt32(); // Magic + + uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32()); + + reader.ReadInt64(); // Padding + + byte[] input = new byte[stream.Length - stream.Position]; + stream.Read(input, 0, input.Length); + + long inputOffset = 0; + + byte[] output = new byte[decodedLength]; + long outputOffset = 0; + + ushort mask = 0; + byte header = 0; + + while (outputOffset < decodedLength) + { + if ((mask >>= 1) == 0) + { + header = input[inputOffset++]; + mask = 0x80; + } + + if ((header & mask) > 0) + { + if (outputOffset == output.Length) + { + break; + } + + output[outputOffset++] = input[inputOffset++]; + } + else + { + byte byte1 = input[inputOffset++]; + byte byte2 = input[inputOffset++]; + + int dist = ((byte1 & 0xF) << 8) | byte2; + int position = (int)outputOffset - (dist + 1); + + int length = byte1 >> 4; + if (length == 0) + { + length = input[inputOffset++] + 0x12; + } + else + { + length += 2; + } + + while (length-- > 0) + { + output[outputOffset++] = output[position++]; + } + } + } + + return output; + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs new file mode 100644 index 00000000..96ed0723 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs @@ -0,0 +1,156 @@ +using Gtk; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using Ryujinx.UI.App.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using GUI = Gtk.Builder.ObjectAttribute; + +namespace Ryujinx.UI.Windows +{ + public class CheatWindow : Window + { + private readonly string _enabledCheatsPath; + private readonly bool _noCheatsFound; + +#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier + [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, string titlePath) : this(new Builder("Ryujinx.Gtk3.UI.Windows.CheatWindow.glade"), virtualFileSystem, titleId, titleName, titlePath) { } + + 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.GetApplicationDir(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(); + 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 = Array.Empty(); + + 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).ToUpper(); + parentIter = ((TreeStore)_cheatTreeView.Model).AppendValues(false, buildId, parentPath, ""); + } + + string cleanName = cheat.Name[1..^7]; + ((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 enabledCheats = new(); + + 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/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.glade b/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.glade new file mode 100644 index 00000000..9a165f1a --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.glade @@ -0,0 +1,150 @@ + + + + + + False + Ryujinx - Cheat Manager + 440 + 550 + + + True + False + vertical + + + True + False + vertical + + + True + False + 10 + 10 + Available Cheats + + + False + True + 0 + + + + + True + 10 + center + 10 + False + False + + + False + True + 1 + + + + + True + True + 10 + 10 + in + + + True + False + + + True + True + + + + + + + + + + True + True + 2 + + + + + True + True + 0 + + + + + True + False + + + True + False + 10 + 10 + end + + + Save + True + True + True + 10 + 2 + 2 + + + + True + True + 0 + + + + + Cancel + True + True + True + 10 + 2 + 2 + + + + True + True + 1 + + + + + True + True + 1 + + + + + False + True + 1 + + + + + + + + + diff --git a/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs new file mode 100644 index 00000000..6712657f --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs @@ -0,0 +1,1230 @@ +using Gtk; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; +using Ryujinx.Input.GTK3; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Widgets; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text.Json; +using System.Threading; +using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; +using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; +using GUI = Gtk.Builder.ObjectAttribute; +using Key = Ryujinx.Common.Configuration.Hid.Key; + +namespace Ryujinx.UI.Windows +{ + public class ControllerWindow : Window + { + private readonly PlayerIndex _playerIndex; + private readonly InputConfig _inputConfig; + + private bool _isWaitingForInput; + +#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier + [GUI] Adjustment _controllerStrongRumble; + [GUI] Adjustment _controllerWeakRumble; + [GUI] Adjustment _controllerDeadzoneLeft; + [GUI] Adjustment _controllerDeadzoneRight; + [GUI] Adjustment _controllerRangeLeft; + [GUI] Adjustment _controllerRangeRight; + [GUI] Adjustment _controllerTriggerThreshold; + [GUI] Adjustment _slotNumber; + [GUI] Adjustment _altSlotNumber; + [GUI] Adjustment _sensitivity; + [GUI] Adjustment _gyroDeadzone; + [GUI] CheckButton _enableMotion; + [GUI] CheckButton _enableCemuHook; + [GUI] CheckButton _mirrorInput; + [GUI] Entry _dsuServerHost; + [GUI] Entry _dsuServerPort; + [GUI] ComboBoxText _inputDevice; + [GUI] ComboBoxText _profile; + [GUI] Box _settingsBox; + [GUI] Box _motionAltBox; + [GUI] Box _motionBox; + [GUI] Box _dsuServerHostBox; + [GUI] Box _dsuServerPortBox; + [GUI] Box _motionControllerSlot; + [GUI] Grid _leftStickKeyboard; + [GUI] Grid _leftStickController; + [GUI] Box _deadZoneLeftBox; + [GUI] Box _rangeLeftBox; + [GUI] Grid _rightStickKeyboard; + [GUI] Grid _rightStickController; + [GUI] Box _deadZoneRightBox; + [GUI] Box _rangeRightBox; + [GUI] Grid _leftSideTriggerBox; + [GUI] Grid _rightSideTriggerBox; + [GUI] Box _triggerThresholdBox; + [GUI] ComboBoxText _controllerType; + [GUI] ToggleButton _lStick; + [GUI] CheckButton _invertLStickX; + [GUI] CheckButton _invertLStickY; + [GUI] CheckButton _rotateL90CW; + [GUI] ToggleButton _lStickUp; + [GUI] ToggleButton _lStickDown; + [GUI] ToggleButton _lStickLeft; + [GUI] ToggleButton _lStickRight; + [GUI] ToggleButton _lStickButton; + [GUI] ToggleButton _dpadUp; + [GUI] ToggleButton _dpadDown; + [GUI] ToggleButton _dpadLeft; + [GUI] ToggleButton _dpadRight; + [GUI] ToggleButton _minus; + [GUI] ToggleButton _l; + [GUI] ToggleButton _zL; + [GUI] ToggleButton _rStick; + [GUI] CheckButton _invertRStickX; + [GUI] CheckButton _invertRStickY; + [GUI] CheckButton _rotateR90CW; + [GUI] ToggleButton _rStickUp; + [GUI] ToggleButton _rStickDown; + [GUI] ToggleButton _rStickLeft; + [GUI] ToggleButton _rStickRight; + [GUI] ToggleButton _rStickButton; + [GUI] ToggleButton _a; + [GUI] ToggleButton _b; + [GUI] ToggleButton _x; + [GUI] ToggleButton _y; + [GUI] ToggleButton _plus; + [GUI] ToggleButton _r; + [GUI] ToggleButton _zR; + [GUI] ToggleButton _lSl; + [GUI] ToggleButton _lSr; + [GUI] ToggleButton _rSl; + [GUI] ToggleButton _rSr; + [GUI] Image _controllerImage; + [GUI] CheckButton _enableRumble; + [GUI] Box _rumbleBox; +#pragma warning restore CS0649, IDE0044 + + private readonly MainWindow _mainWindow; + private readonly IGamepadDriver _gtk3KeyboardDriver; + private IGamepad _selectedGamepad; + private bool _mousePressed; + private bool _middleMousePressed; + + private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public ControllerWindow(MainWindow mainWindow, PlayerIndex controllerId) : this(mainWindow, new Builder("Ryujinx.Gtk3.UI.Windows.ControllerWindow.glade"), controllerId) { } + + private ControllerWindow(MainWindow mainWindow, Builder builder, PlayerIndex controllerId) : base(builder.GetRawOwnedObject("_controllerWin")) + { + _mainWindow = mainWindow; + _selectedGamepad = null; + + // NOTE: To get input in this window, we need to bind a custom keyboard driver instead of using the InputManager one as the main window isn't focused... + _gtk3KeyboardDriver = new GTK3KeyboardDriver(this); + + Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png"); + + builder.Autoconnect(this); + + _playerIndex = controllerId; + _inputConfig = ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.PlayerIndex == _playerIndex); + + Title = $"Ryujinx - Controller Settings - {_playerIndex}"; + + if (_playerIndex == PlayerIndex.Handheld) + { + _controllerType.Append(ControllerType.Handheld.ToString(), "Handheld"); + _controllerType.Sensitive = false; + } + else + { + _controllerType.Append(ControllerType.ProController.ToString(), "Pro Controller"); + _controllerType.Append(ControllerType.JoyconPair.ToString(), "Joycon Pair"); + _controllerType.Append(ControllerType.JoyconLeft.ToString(), "Joycon Left"); + _controllerType.Append(ControllerType.JoyconRight.ToString(), "Joycon Right"); + } + + _controllerType.Active = 0; // Set initial value to first in list. + + // Bind Events. + _lStick.Clicked += ButtonForStick_Pressed; + _lStickUp.Clicked += Button_Pressed; + _lStickDown.Clicked += Button_Pressed; + _lStickLeft.Clicked += Button_Pressed; + _lStickRight.Clicked += Button_Pressed; + _lStickButton.Clicked += Button_Pressed; + _dpadUp.Clicked += Button_Pressed; + _dpadDown.Clicked += Button_Pressed; + _dpadLeft.Clicked += Button_Pressed; + _dpadRight.Clicked += Button_Pressed; + _minus.Clicked += Button_Pressed; + _l.Clicked += Button_Pressed; + _zL.Clicked += Button_Pressed; + _lSl.Clicked += Button_Pressed; + _lSr.Clicked += Button_Pressed; + _rStick.Clicked += ButtonForStick_Pressed; + _rStickUp.Clicked += Button_Pressed; + _rStickDown.Clicked += Button_Pressed; + _rStickLeft.Clicked += Button_Pressed; + _rStickRight.Clicked += Button_Pressed; + _rStickButton.Clicked += Button_Pressed; + _a.Clicked += Button_Pressed; + _b.Clicked += Button_Pressed; + _x.Clicked += Button_Pressed; + _y.Clicked += Button_Pressed; + _plus.Clicked += Button_Pressed; + _r.Clicked += Button_Pressed; + _zR.Clicked += Button_Pressed; + _rSl.Clicked += Button_Pressed; + _rSr.Clicked += Button_Pressed; + _enableCemuHook.Clicked += CemuHookCheckButtonPressed; + + // Setup current values. + UpdateInputDeviceList(); + SetAvailableOptions(); + + ClearValues(); + if (_inputDevice.ActiveId != null) + { + SetCurrentValues(); + } + + mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; + mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; + + _mainWindow.RendererWidget?.NpadManager.BlockInputUpdates(); + } + + private void CemuHookCheckButtonPressed(object sender, EventArgs e) + { + UpdateCemuHookSpecificFieldsVisibility(); + } + + private void HandleOnGamepadDisconnected(string id) + { + Application.Invoke(delegate + { + UpdateInputDeviceList(); + }); + } + + private void HandleOnGamepadConnected(string id) + { + Application.Invoke(delegate + { + UpdateInputDeviceList(); + }); + } + + protected override void OnDestroyed() + { + _mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected; + _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected; + + _mainWindow.RendererWidget?.NpadManager.UnblockInputUpdates(); + + _selectedGamepad?.Dispose(); + + _gtk3KeyboardDriver.Dispose(); + } + + private static string GetShortGamepadName(string str) + { + const string ShrinkChars = "..."; + const int MaxSize = 50; + + if (str.Length > MaxSize) + { + return $"{str.AsSpan(0, MaxSize - ShrinkChars.Length)}{ShrinkChars}"; + } + + return str; + } + + private void UpdateInputDeviceList() + { + _inputDevice.RemoveAll(); + _inputDevice.Append("disabled", "Disabled"); + _inputDevice.SetActiveId("disabled"); + + foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds) + { + IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id); + + if (gamepad != null) + { + _inputDevice.Append($"keyboard/{id}", GetShortGamepadName($"{gamepad.Name} ({id})")); + + gamepad.Dispose(); + } + } + + foreach (string id in _mainWindow.InputManager.GamepadDriver.GamepadsIds) + { + IGamepad gamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id); + + if (gamepad != null) + { + _inputDevice.Append($"controller/{id}", GetShortGamepadName($"{gamepad.Name} ({id})")); + + gamepad.Dispose(); + } + } + + switch (_inputConfig) + { + case StandardKeyboardInputConfig keyboard: + _inputDevice.SetActiveId($"keyboard/{keyboard.Id}"); + break; + case StandardControllerInputConfig controller: + _inputDevice.SetActiveId($"controller/{controller.Id}"); + break; + } + } + + private void UpdateCemuHookSpecificFieldsVisibility() + { + if (_enableCemuHook.Active) + { + _dsuServerHostBox.Show(); + _dsuServerPortBox.Show(); + _motionControllerSlot.Show(); + _motionAltBox.Show(); + _mirrorInput.Show(); + } + else + { + _dsuServerHostBox.Hide(); + _dsuServerPortBox.Hide(); + _motionControllerSlot.Hide(); + _motionAltBox.Hide(); + _mirrorInput.Hide(); + } + } + + private void SetAvailableOptions() + { + if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("keyboard")) + { + ShowAll(); + _leftStickController.Hide(); + _rightStickController.Hide(); + _deadZoneLeftBox.Hide(); + _deadZoneRightBox.Hide(); + _rangeLeftBox.Hide(); + _rangeRightBox.Hide(); + _triggerThresholdBox.Hide(); + _motionBox.Hide(); + _rumbleBox.Hide(); + } + else if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("controller")) + { + ShowAll(); + _leftStickKeyboard.Hide(); + _rightStickKeyboard.Hide(); + + UpdateCemuHookSpecificFieldsVisibility(); + } + else + { + _settingsBox.Hide(); + } + + ClearValues(); + } + + private void SetCurrentValues() + { + SetControllerSpecificFields(); + + SetProfiles(); + + if (_inputDevice.ActiveId.StartsWith("keyboard") && _inputConfig is StandardKeyboardInputConfig) + { + SetValues(_inputConfig); + } + else if (_inputDevice.ActiveId.StartsWith("controller") && _inputConfig is StandardControllerInputConfig) + { + SetValues(_inputConfig); + } + } + + private void SetControllerSpecificFields() + { + _leftSideTriggerBox.Hide(); + _rightSideTriggerBox.Hide(); + _motionAltBox.Hide(); + + switch (_controllerType.ActiveId) + { + case "JoyconLeft": + _leftSideTriggerBox.Show(); + break; + case "JoyconRight": + _rightSideTriggerBox.Show(); + break; + case "JoyconPair": + _motionAltBox.Show(); + break; + } + + if (!OperatingSystem.IsMacOS()) + { + _controllerImage.Pixbuf = _controllerType.ActiveId switch + { + "ProController" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Controller_ProCon.svg", 400, 400), + "JoyconLeft" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Controller_JoyConLeft.svg", 400, 500), + "JoyconRight" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Controller_JoyConRight.svg", 400, 500), + _ => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Controller_JoyConPair.svg", 400, 500), + }; + } + } + + private void ClearValues() + { + _lStick.Label = "Unbound"; + _lStickUp.Label = "Unbound"; + _lStickDown.Label = "Unbound"; + _lStickLeft.Label = "Unbound"; + _lStickRight.Label = "Unbound"; + _lStickButton.Label = "Unbound"; + _dpadUp.Label = "Unbound"; + _dpadDown.Label = "Unbound"; + _dpadLeft.Label = "Unbound"; + _dpadRight.Label = "Unbound"; + _minus.Label = "Unbound"; + _l.Label = "Unbound"; + _zL.Label = "Unbound"; + _lSl.Label = "Unbound"; + _lSr.Label = "Unbound"; + _rStick.Label = "Unbound"; + _rStickUp.Label = "Unbound"; + _rStickDown.Label = "Unbound"; + _rStickLeft.Label = "Unbound"; + _rStickRight.Label = "Unbound"; + _rStickButton.Label = "Unbound"; + _a.Label = "Unbound"; + _b.Label = "Unbound"; + _x.Label = "Unbound"; + _y.Label = "Unbound"; + _plus.Label = "Unbound"; + _r.Label = "Unbound"; + _zR.Label = "Unbound"; + _rSl.Label = "Unbound"; + _rSr.Label = "Unbound"; + _controllerStrongRumble.Value = 1; + _controllerWeakRumble.Value = 1; + _controllerDeadzoneLeft.Value = 0; + _controllerDeadzoneRight.Value = 0; + _controllerRangeLeft.Value = 1; + _controllerRangeRight.Value = 1; + _controllerTriggerThreshold.Value = 0; + _mirrorInput.Active = false; + _enableMotion.Active = false; + _enableCemuHook.Active = false; + _slotNumber.Value = 0; + _altSlotNumber.Value = 0; + _sensitivity.Value = 100; + _gyroDeadzone.Value = 1; + _dsuServerHost.Buffer.Text = ""; + _dsuServerPort.Buffer.Text = ""; + _enableRumble.Active = false; + } + + private void SetValues(InputConfig config) + { + switch (config) + { + case StandardKeyboardInputConfig keyboardConfig: + if (!_controllerType.SetActiveId(keyboardConfig.ControllerType.ToString())) + { + _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld + ? ControllerType.Handheld.ToString() + : ControllerType.ProController.ToString()); + } + + _lStickUp.Label = keyboardConfig.LeftJoyconStick.StickUp.ToString(); + _lStickDown.Label = keyboardConfig.LeftJoyconStick.StickDown.ToString(); + _lStickLeft.Label = keyboardConfig.LeftJoyconStick.StickLeft.ToString(); + _lStickRight.Label = keyboardConfig.LeftJoyconStick.StickRight.ToString(); + _lStickButton.Label = keyboardConfig.LeftJoyconStick.StickButton.ToString(); + _dpadUp.Label = keyboardConfig.LeftJoycon.DpadUp.ToString(); + _dpadDown.Label = keyboardConfig.LeftJoycon.DpadDown.ToString(); + _dpadLeft.Label = keyboardConfig.LeftJoycon.DpadLeft.ToString(); + _dpadRight.Label = keyboardConfig.LeftJoycon.DpadRight.ToString(); + _minus.Label = keyboardConfig.LeftJoycon.ButtonMinus.ToString(); + _l.Label = keyboardConfig.LeftJoycon.ButtonL.ToString(); + _zL.Label = keyboardConfig.LeftJoycon.ButtonZl.ToString(); + _lSl.Label = keyboardConfig.LeftJoycon.ButtonSl.ToString(); + _lSr.Label = keyboardConfig.LeftJoycon.ButtonSr.ToString(); + _rStickUp.Label = keyboardConfig.RightJoyconStick.StickUp.ToString(); + _rStickDown.Label = keyboardConfig.RightJoyconStick.StickDown.ToString(); + _rStickLeft.Label = keyboardConfig.RightJoyconStick.StickLeft.ToString(); + _rStickRight.Label = keyboardConfig.RightJoyconStick.StickRight.ToString(); + _rStickButton.Label = keyboardConfig.RightJoyconStick.StickButton.ToString(); + _a.Label = keyboardConfig.RightJoycon.ButtonA.ToString(); + _b.Label = keyboardConfig.RightJoycon.ButtonB.ToString(); + _x.Label = keyboardConfig.RightJoycon.ButtonX.ToString(); + _y.Label = keyboardConfig.RightJoycon.ButtonY.ToString(); + _plus.Label = keyboardConfig.RightJoycon.ButtonPlus.ToString(); + _r.Label = keyboardConfig.RightJoycon.ButtonR.ToString(); + _zR.Label = keyboardConfig.RightJoycon.ButtonZr.ToString(); + _rSl.Label = keyboardConfig.RightJoycon.ButtonSl.ToString(); + _rSr.Label = keyboardConfig.RightJoycon.ButtonSr.ToString(); + break; + + case StandardControllerInputConfig controllerConfig: + if (!_controllerType.SetActiveId(controllerConfig.ControllerType.ToString())) + { + _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld + ? ControllerType.Handheld.ToString() + : ControllerType.ProController.ToString()); + } + + _lStick.Label = controllerConfig.LeftJoyconStick.Joystick.ToString(); + _invertLStickX.Active = controllerConfig.LeftJoyconStick.InvertStickX; + _invertLStickY.Active = controllerConfig.LeftJoyconStick.InvertStickY; + _rotateL90CW.Active = controllerConfig.LeftJoyconStick.Rotate90CW; + _lStickButton.Label = controllerConfig.LeftJoyconStick.StickButton.ToString(); + _dpadUp.Label = controllerConfig.LeftJoycon.DpadUp.ToString(); + _dpadDown.Label = controllerConfig.LeftJoycon.DpadDown.ToString(); + _dpadLeft.Label = controllerConfig.LeftJoycon.DpadLeft.ToString(); + _dpadRight.Label = controllerConfig.LeftJoycon.DpadRight.ToString(); + _minus.Label = controllerConfig.LeftJoycon.ButtonMinus.ToString(); + _l.Label = controllerConfig.LeftJoycon.ButtonL.ToString(); + _zL.Label = controllerConfig.LeftJoycon.ButtonZl.ToString(); + _lSl.Label = controllerConfig.LeftJoycon.ButtonSl.ToString(); + _lSr.Label = controllerConfig.LeftJoycon.ButtonSr.ToString(); + _rStick.Label = controllerConfig.RightJoyconStick.Joystick.ToString(); + _invertRStickX.Active = controllerConfig.RightJoyconStick.InvertStickX; + _invertRStickY.Active = controllerConfig.RightJoyconStick.InvertStickY; + _rotateR90CW.Active = controllerConfig.RightJoyconStick.Rotate90CW; + _rStickButton.Label = controllerConfig.RightJoyconStick.StickButton.ToString(); + _a.Label = controllerConfig.RightJoycon.ButtonA.ToString(); + _b.Label = controllerConfig.RightJoycon.ButtonB.ToString(); + _x.Label = controllerConfig.RightJoycon.ButtonX.ToString(); + _y.Label = controllerConfig.RightJoycon.ButtonY.ToString(); + _plus.Label = controllerConfig.RightJoycon.ButtonPlus.ToString(); + _r.Label = controllerConfig.RightJoycon.ButtonR.ToString(); + _zR.Label = controllerConfig.RightJoycon.ButtonZr.ToString(); + _rSl.Label = controllerConfig.RightJoycon.ButtonSl.ToString(); + _rSr.Label = controllerConfig.RightJoycon.ButtonSr.ToString(); + _controllerStrongRumble.Value = controllerConfig.Rumble.StrongRumble; + _controllerWeakRumble.Value = controllerConfig.Rumble.WeakRumble; + _enableRumble.Active = controllerConfig.Rumble.EnableRumble; + _controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft; + _controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight; + _controllerRangeLeft.Value = controllerConfig.RangeLeft; + _controllerRangeRight.Value = controllerConfig.RangeRight; + _controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold; + _sensitivity.Value = controllerConfig.Motion.Sensitivity; + _gyroDeadzone.Value = controllerConfig.Motion.GyroDeadzone; + _enableMotion.Active = controllerConfig.Motion.EnableMotion; + _enableCemuHook.Active = controllerConfig.Motion.MotionBackend == MotionInputBackendType.CemuHook; + + // If both stick ranges are 0 (usually indicative of an outdated profile load) then both sticks will be set to 1.0. + if (_controllerRangeLeft.Value <= 0.0 && _controllerRangeRight.Value <= 0.0) + { + _controllerRangeLeft.Value = 1.0; + _controllerRangeRight.Value = 1.0; + + Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} stick range reset. Save the profile now to update your configuration"); + } + + if (controllerConfig.Motion is CemuHookMotionConfigController cemuHookMotionConfig) + { + _slotNumber.Value = cemuHookMotionConfig.Slot; + _altSlotNumber.Value = cemuHookMotionConfig.AltSlot; + _mirrorInput.Active = cemuHookMotionConfig.MirrorInput; + _dsuServerHost.Buffer.Text = cemuHookMotionConfig.DsuServerHost; + _dsuServerPort.Buffer.Text = cemuHookMotionConfig.DsuServerPort.ToString(); + } + + break; + } + } + + private InputConfig GetValues() + { + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { +#pragma warning disable CA1806, IDE0055 // Disable formatting + Enum.TryParse(_lStickUp.Label, out Key lStickUp); + Enum.TryParse(_lStickDown.Label, out Key lStickDown); + Enum.TryParse(_lStickLeft.Label, out Key lStickLeft); + Enum.TryParse(_lStickRight.Label, out Key lStickRight); + Enum.TryParse(_lStickButton.Label, out Key lStickButton); + Enum.TryParse(_dpadUp.Label, out Key lDPadUp); + Enum.TryParse(_dpadDown.Label, out Key lDPadDown); + Enum.TryParse(_dpadLeft.Label, out Key lDPadLeft); + Enum.TryParse(_dpadRight.Label, out Key lDPadRight); + Enum.TryParse(_minus.Label, out Key lButtonMinus); + Enum.TryParse(_l.Label, out Key lButtonL); + Enum.TryParse(_zL.Label, out Key lButtonZl); + Enum.TryParse(_lSl.Label, out Key lButtonSl); + Enum.TryParse(_lSr.Label, out Key lButtonSr); + + Enum.TryParse(_rStickUp.Label, out Key rStickUp); + Enum.TryParse(_rStickDown.Label, out Key rStickDown); + Enum.TryParse(_rStickLeft.Label, out Key rStickLeft); + Enum.TryParse(_rStickRight.Label, out Key rStickRight); + Enum.TryParse(_rStickButton.Label, out Key rStickButton); + Enum.TryParse(_a.Label, out Key rButtonA); + Enum.TryParse(_b.Label, out Key rButtonB); + Enum.TryParse(_x.Label, out Key rButtonX); + Enum.TryParse(_y.Label, out Key rButtonY); + Enum.TryParse(_plus.Label, out Key rButtonPlus); + Enum.TryParse(_r.Label, out Key rButtonR); + Enum.TryParse(_zR.Label, out Key rButtonZr); + Enum.TryParse(_rSl.Label, out Key rButtonSl); + Enum.TryParse(_rSr.Label, out Key rButtonSr); +#pragma warning restore CA1806, IDE0055 + + return new StandardKeyboardInputConfig + { + Backend = InputBackendType.WindowKeyboard, + Version = InputConfig.CurrentVersion, + Id = _inputDevice.ActiveId.Split("/")[1], + ControllerType = Enum.Parse(_controllerType.ActiveId), + PlayerIndex = _playerIndex, + LeftJoycon = new LeftJoyconCommonConfig + { + ButtonMinus = lButtonMinus, + ButtonL = lButtonL, + ButtonZl = lButtonZl, + ButtonSl = lButtonSl, + ButtonSr = lButtonSr, + DpadUp = lDPadUp, + DpadDown = lDPadDown, + DpadLeft = lDPadLeft, + DpadRight = lDPadRight, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = lStickUp, + StickDown = lStickDown, + StickLeft = lStickLeft, + StickRight = lStickRight, + StickButton = lStickButton, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = rButtonA, + ButtonB = rButtonB, + ButtonX = rButtonX, + ButtonY = rButtonY, + ButtonPlus = rButtonPlus, + ButtonR = rButtonR, + ButtonZr = rButtonZr, + ButtonSl = rButtonSl, + ButtonSr = rButtonSr, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = rStickUp, + StickDown = rStickDown, + StickLeft = rStickLeft, + StickRight = rStickRight, + StickButton = rStickButton, + }, + }; + } + + if (_inputDevice.ActiveId.StartsWith("controller")) + { +#pragma warning disable CA1806, IDE0055 // Disable formatting + Enum.TryParse(_lStick.Label, out ConfigStickInputId lStick); + Enum.TryParse(_lStickButton.Label, out ConfigGamepadInputId lStickButton); + Enum.TryParse(_minus.Label, out ConfigGamepadInputId lButtonMinus); + Enum.TryParse(_l.Label, out ConfigGamepadInputId lButtonL); + Enum.TryParse(_zL.Label, out ConfigGamepadInputId lButtonZl); + Enum.TryParse(_lSl.Label, out ConfigGamepadInputId lButtonSl); + Enum.TryParse(_lSr.Label, out ConfigGamepadInputId lButtonSr); + Enum.TryParse(_dpadUp.Label, out ConfigGamepadInputId lDPadUp); + Enum.TryParse(_dpadDown.Label, out ConfigGamepadInputId lDPadDown); + Enum.TryParse(_dpadLeft.Label, out ConfigGamepadInputId lDPadLeft); + Enum.TryParse(_dpadRight.Label, out ConfigGamepadInputId lDPadRight); + + Enum.TryParse(_rStick.Label, out ConfigStickInputId rStick); + Enum.TryParse(_rStickButton.Label, out ConfigGamepadInputId rStickButton); + Enum.TryParse(_a.Label, out ConfigGamepadInputId rButtonA); + Enum.TryParse(_b.Label, out ConfigGamepadInputId rButtonB); + Enum.TryParse(_x.Label, out ConfigGamepadInputId rButtonX); + Enum.TryParse(_y.Label, out ConfigGamepadInputId rButtonY); + Enum.TryParse(_plus.Label, out ConfigGamepadInputId rButtonPlus); + Enum.TryParse(_r.Label, out ConfigGamepadInputId rButtonR); + Enum.TryParse(_zR.Label, out ConfigGamepadInputId rButtonZr); + Enum.TryParse(_rSl.Label, out ConfigGamepadInputId rButtonSl); + Enum.TryParse(_rSr.Label, out ConfigGamepadInputId rButtonSr); + + int.TryParse(_dsuServerPort.Buffer.Text, out int port); +#pragma warning restore CA1806, IDE0055 + + MotionConfigController motionConfig; + + if (_enableCemuHook.Active) + { + motionConfig = new CemuHookMotionConfigController + { + MotionBackend = MotionInputBackendType.CemuHook, + EnableMotion = _enableMotion.Active, + Sensitivity = (int)_sensitivity.Value, + GyroDeadzone = _gyroDeadzone.Value, + MirrorInput = _mirrorInput.Active, + Slot = (int)_slotNumber.Value, + AltSlot = (int)_altSlotNumber.Value, + DsuServerHost = _dsuServerHost.Buffer.Text, + DsuServerPort = port, + }; + } + else + { + motionConfig = new StandardMotionConfigController + { + MotionBackend = MotionInputBackendType.GamepadDriver, + EnableMotion = _enableMotion.Active, + Sensitivity = (int)_sensitivity.Value, + GyroDeadzone = _gyroDeadzone.Value, + }; + } + + return new StandardControllerInputConfig + { + Backend = InputBackendType.GamepadSDL2, + Version = InputConfig.CurrentVersion, + Id = _inputDevice.ActiveId.Split("/")[1].Split(" ")[0], + ControllerType = Enum.Parse(_controllerType.ActiveId), + PlayerIndex = _playerIndex, + DeadzoneLeft = (float)_controllerDeadzoneLeft.Value, + DeadzoneRight = (float)_controllerDeadzoneRight.Value, + RangeLeft = (float)_controllerRangeLeft.Value, + RangeRight = (float)_controllerRangeRight.Value, + TriggerThreshold = (float)_controllerTriggerThreshold.Value, + LeftJoycon = new LeftJoyconCommonConfig + { + ButtonMinus = lButtonMinus, + ButtonL = lButtonL, + ButtonZl = lButtonZl, + ButtonSl = lButtonSl, + ButtonSr = lButtonSr, + DpadUp = lDPadUp, + DpadDown = lDPadDown, + DpadLeft = lDPadLeft, + DpadRight = lDPadRight, + }, + LeftJoyconStick = new JoyconConfigControllerStick + { + InvertStickX = _invertLStickX.Active, + Joystick = lStick, + InvertStickY = _invertLStickY.Active, + StickButton = lStickButton, + Rotate90CW = _rotateL90CW.Active, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = rButtonA, + ButtonB = rButtonB, + ButtonX = rButtonX, + ButtonY = rButtonY, + ButtonPlus = rButtonPlus, + ButtonR = rButtonR, + ButtonZr = rButtonZr, + ButtonSl = rButtonSl, + ButtonSr = rButtonSr, + }, + RightJoyconStick = new JoyconConfigControllerStick + { + InvertStickX = _invertRStickX.Active, + Joystick = rStick, + InvertStickY = _invertRStickY.Active, + StickButton = rStickButton, + Rotate90CW = _rotateR90CW.Active, + }, + Motion = motionConfig, + Rumble = new RumbleConfigController + { + StrongRumble = (float)_controllerStrongRumble.Value, + WeakRumble = (float)_controllerWeakRumble.Value, + EnableRumble = _enableRumble.Active, + }, + }; + } + + if (!_inputDevice.ActiveId.StartsWith("disabled")) + { + GtkDialog.CreateErrorDialog("Invalid data detected in one or more fields; the configuration was not saved."); + } + + return null; + } + + private string GetProfileBasePath() + { + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + return System.IO.Path.Combine(AppDataManager.ProfilesDirPath, "keyboard"); + } + else if (_inputDevice.ActiveId.StartsWith("controller")) + { + return System.IO.Path.Combine(AppDataManager.ProfilesDirPath, "controller"); + } + + return AppDataManager.ProfilesDirPath; + } + + // + // Events + // + private void InputDevice_Changed(object sender, EventArgs args) + { + SetAvailableOptions(); + SetControllerSpecificFields(); + + _selectedGamepad?.Dispose(); + _selectedGamepad = null; + + if (_inputDevice.ActiveId != null) + { + SetProfiles(); + + string id = GetCurrentGamepadId(); + + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + if (_inputConfig is StandardKeyboardInputConfig) + { + SetValues(_inputConfig); + } + + if (_mainWindow.InputManager.KeyboardDriver is GTK3KeyboardDriver) + { + // NOTE: To get input in this window, we need to bind a custom keyboard driver instead of using the InputManager one as the main window isn't focused... + _selectedGamepad = _gtk3KeyboardDriver.GetGamepad(id); + } + else + { + _selectedGamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id); + } + } + else if (_inputDevice.ActiveId.StartsWith("controller")) + { + if (_inputConfig is StandardControllerInputConfig) + { + SetValues(_inputConfig); + } + + _selectedGamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id); + } + } + } + + private string GetCurrentGamepadId() + { + if (_inputDevice.ActiveId == null || _inputDevice.ActiveId == "disabled") + { + return null; + } + + return _inputDevice.ActiveId.Split("/")[1].Split(" ")[0]; + } + + private void Controller_Changed(object sender, EventArgs args) + { + SetControllerSpecificFields(); + } + + private IButtonAssigner CreateButtonAssigner(bool forStick) + { + IButtonAssigner assigner; + + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + assigner = new KeyboardKeyAssigner((IKeyboard)_selectedGamepad); + } + else if (_inputDevice.ActiveId.StartsWith("controller")) + { + assigner = new GamepadButtonAssigner(_selectedGamepad, (float)_controllerTriggerThreshold.Value, forStick); + } + else + { + throw new Exception("Controller not supported"); + } + + return assigner; + } + + private void HandleButtonPressed(ToggleButton button, bool forStick) + { + if (_isWaitingForInput) + { + button.Active = false; + + return; + } + + _mousePressed = false; + + ButtonPressEvent += MouseClick; + + IButtonAssigner assigner = CreateButtonAssigner(forStick); + + _isWaitingForInput = true; + + // Open GTK3 keyboard for cancel operations + IKeyboard keyboard = (IKeyboard)_gtk3KeyboardDriver.GetGamepad("0"); + + Thread inputThread = new(() => + { + assigner.Initialize(); + + while (true) + { + Thread.Sleep(10); + assigner.ReadInput(); + + if (_mousePressed || keyboard.IsPressed(Ryujinx.Input.Key.Escape) || assigner.HasAnyButtonPressed() || assigner.ShouldCancel()) + { + break; + } + } + + string pressedButton = assigner.GetPressedButton(); + + Application.Invoke(delegate + { + if (_middleMousePressed) + { + button.Label = "Unbound"; + } + else if (pressedButton != "") + { + button.Label = pressedButton; + } + + _middleMousePressed = false; + + ButtonPressEvent -= MouseClick; + keyboard.Dispose(); + + button.Active = false; + _isWaitingForInput = false; + }); + }) + { + Name = "GUI.InputThread", + IsBackground = true, + }; + inputThread.Start(); + } + + private void Button_Pressed(object sender, EventArgs args) + { + HandleButtonPressed((ToggleButton)sender, false); + } + + private void ButtonForStick_Pressed(object sender, EventArgs args) + { + HandleButtonPressed((ToggleButton)sender, true); + } + + private void MouseClick(object sender, ButtonPressEventArgs args) + { + _mousePressed = true; + _middleMousePressed = args.Event.Button == 2; + } + + private void SetProfiles() + { + _profile.RemoveAll(); + + string basePath = GetProfileBasePath(); + + if (!Directory.Exists(basePath)) + { + Directory.CreateDirectory(basePath); + } + + if (_inputDevice.ActiveId == null || _inputDevice.ActiveId.Equals("disabled")) + { + _profile.Append("default", "None"); + } + else + { + _profile.Append("default", "Default"); + + foreach (string profile in Directory.GetFiles(basePath, "*.*", SearchOption.AllDirectories)) + { + _profile.Append(System.IO.Path.GetFileName(profile), System.IO.Path.GetFileNameWithoutExtension(profile)); + } + } + + _profile.SetActiveId("default"); + } + + private void ProfileLoad_Activated(object sender, EventArgs args) + { + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + + if (_inputDevice.ActiveId == "disabled" || _profile.ActiveId == null) + { + return; + } + + InputConfig config = null; + int pos = _profile.Active; + + if (_profile.ActiveId == "default") + { + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + config = new StandardKeyboardInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.WindowKeyboard, + Id = null, + ControllerType = ControllerType.ProController, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = Key.Up, + DpadDown = Key.Down, + DpadLeft = Key.Left, + DpadRight = Key.Right, + ButtonMinus = Key.Minus, + ButtonL = Key.E, + ButtonZl = Key.Q, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.W, + StickDown = Key.S, + StickLeft = Key.A, + StickRight = Key.D, + StickButton = Key.F, + }, + + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = Key.Z, + ButtonB = Key.X, + ButtonX = Key.C, + ButtonY = Key.V, + ButtonPlus = Key.Plus, + ButtonR = Key.U, + ButtonZr = Key.O, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.I, + StickDown = Key.K, + StickLeft = Key.J, + StickRight = Key.L, + StickButton = Key.H, + }, + }; + } + else if (_inputDevice.ActiveId.StartsWith("controller")) + { + bool isNintendoStyle = _inputDevice.ActiveText.Contains("Nintendo"); + + config = new StandardControllerInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.GamepadSDL2, + Id = null, + ControllerType = ControllerType.JoyconPair, + DeadzoneLeft = 0.1f, + DeadzoneRight = 0.1f, + RangeLeft = 1.0f, + RangeRight = 1.0f, + TriggerThreshold = 0.5f, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = ConfigGamepadInputId.DpadUp, + DpadDown = ConfigGamepadInputId.DpadDown, + DpadLeft = ConfigGamepadInputId.DpadLeft, + DpadRight = ConfigGamepadInputId.DpadRight, + ButtonMinus = ConfigGamepadInputId.Minus, + ButtonL = ConfigGamepadInputId.LeftShoulder, + ButtonZl = ConfigGamepadInputId.LeftTrigger, + ButtonSl = ConfigGamepadInputId.Unbound, + ButtonSr = ConfigGamepadInputId.Unbound, + }, + + LeftJoyconStick = new JoyconConfigControllerStick + { + Joystick = ConfigStickInputId.Left, + StickButton = ConfigGamepadInputId.LeftStick, + InvertStickX = false, + InvertStickY = false, + Rotate90CW = false, + }, + + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B, + ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A, + ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y, + ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X, + ButtonPlus = ConfigGamepadInputId.Plus, + ButtonR = ConfigGamepadInputId.RightShoulder, + ButtonZr = ConfigGamepadInputId.RightTrigger, + ButtonSl = ConfigGamepadInputId.Unbound, + ButtonSr = ConfigGamepadInputId.Unbound, + }, + + RightJoyconStick = new JoyconConfigControllerStick + { + Joystick = ConfigStickInputId.Right, + StickButton = ConfigGamepadInputId.RightStick, + InvertStickX = false, + InvertStickY = false, + Rotate90CW = false, + }, + + Motion = new StandardMotionConfigController + { + MotionBackend = MotionInputBackendType.GamepadDriver, + EnableMotion = true, + Sensitivity = 100, + GyroDeadzone = 1, + }, + Rumble = new RumbleConfigController + { + StrongRumble = 1f, + WeakRumble = 1f, + EnableRumble = false, + }, + }; + } + } + else + { + string path = System.IO.Path.Combine(GetProfileBasePath(), _profile.ActiveId); + + if (!File.Exists(path)) + { + if (pos >= 0) + { + _profile.Remove(pos); + } + + return; + } + + try + { + config = JsonHelper.DeserializeFromFile(path, _serializerContext.InputConfig); + } + catch (JsonException) { } + } + + SetValues(config); + } + + private void ProfileAdd_Activated(object sender, EventArgs args) + { + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + + if (_inputDevice.ActiveId == "disabled") + { + return; + } + + InputConfig inputConfig = GetValues(); + ProfileDialog profileDialog = new(); + + if (inputConfig == null) + { + return; + } + + if (profileDialog.Run() == (int)ResponseType.Ok) + { + string path = System.IO.Path.Combine(GetProfileBasePath(), profileDialog.FileName); + string jsonString = JsonHelper.Serialize(inputConfig, _serializerContext.InputConfig); + + File.WriteAllText(path, jsonString); + } + + profileDialog.Dispose(); + + SetProfiles(); + } + + private void ProfileRemove_Activated(object sender, EventArgs args) + { + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + + if (_inputDevice.ActiveId == "disabled" || _profile.ActiveId == "default" || _profile.ActiveId == null) + { + return; + } + + MessageDialog confirmDialog = GtkDialog.CreateConfirmationDialog("Deleting Profile", "This action is irreversible, are you sure you want to continue?"); + + if (confirmDialog.Run() == (int)ResponseType.Yes) + { + string path = System.IO.Path.Combine(GetProfileBasePath(), _profile.ActiveId); + + if (File.Exists(path)) + { + File.Delete(path); + } + + SetProfiles(); + } + } + + private void SaveToggle_Activated(object sender, EventArgs args) + { + InputConfig inputConfig = GetValues(); + + var newConfig = new List(); + newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value); + + if (_inputConfig == null && inputConfig != null) + { + newConfig.Add(inputConfig); + } + else + { + if (_inputDevice.ActiveId == "disabled") + { + newConfig.Remove(_inputConfig); + } + else if (inputConfig != null) + { + int index = newConfig.IndexOf(_inputConfig); + + newConfig[index] = inputConfig; + } + } + + _mainWindow.RendererWidget?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); + + // Atomically replace and signal input change. + // NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event. + ConfigurationState.Instance.Hid.InputConfig.Value = newConfig; + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + Dispose(); + } + + private void CloseToggle_Activated(object sender, EventArgs args) + { + Dispose(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.glade b/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.glade new file mode 100644 index 00000000..e433f5cc --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.glade @@ -0,0 +1,2241 @@ + + + + + + 4 + 1 + 4 + + + 0.1 + 10 + 1.0 + 0.1 + 1.0 + + + 0.1 + 10 + 1.0 + 0.1 + 1.0 + + + 1 + 0.050000000000000003 + 0.01 + 0.10000000000000001 + + + 1 + 0.050000000000000003 + 0.01 + 0.10000000000000001 + + + 2 + 1.000000000000000003 + 0.01 + 0.10000000000000001 + + + 2 + 1.000000000000000003 + 0.01 + 0.10000000000000001 + + + 1 + 0.5 + 0.01 + 0.10000000000000001 + + + 100 + 0.01 + 0.01 + 0.10000000000000001 + 0.10000000000000001 + + + 1000 + 100 + 1 + 4 + + + 4 + 1 + 4 + + + False + Ryujinx - Controller Settings + True + center + 1200 + 720 + + + + + + True + False + vertical + + + True + True + in + + + True + False + + + True + False + vertical + + + True + False + 10 + 10 + 10 + + + True + False + + + True + False + 5 + Input Device + + + False + True + 0 + + + + + True + False + 0 + disabled + + Disabled + + + + + True + True + 1 + + + + + False + True + 0 + + + + + True + False + 20 + + + True + False + The controller's type + center + 5 + Controller Type: + + + False + True + 0 + + + + + True + False + The controller's type + 0 + + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 20 + + + True + False + 5 + Profile: + + + False + True + 0 + + + + + True + False + 5 + 0 + default + + + False + True + 1 + + + + + Load + 60 + True + True + True + 5 + + + + False + True + 2 + + + + + Add + 60 + True + True + True + 5 + + + + False + True + 3 + + + + + Remove + 60 + True + True + True + + + + False + True + 4 + + + + + False + True + 2 + + + + + False + True + 0 + + + + + True + False + + + True + False + vertical + + + True + False + 10 + 5 + + + 156 + True + False + 10 + vertical + + + True + False + 5 + 5 + Buttons + + + + + + False + True + 0 + + + + + True + False + 3 + 10 + + + 80 + True + False + A + + + 0 + 0 + + + + + 80 + True + False + B + + + 0 + 1 + + + + + 80 + True + False + X + + + 0 + 2 + + + + + 80 + True + False + Y + + + 0 + 3 + + + + + + 70 + True + True + True + + + 1 + 0 + + + + + + 70 + True + True + True + + + 1 + 1 + + + + + + 70 + True + True + True + + + 1 + 2 + + + + + + 70 + True + True + True + + + 1 + 3 + + + + + 80 + True + False + + + + + 0 + 4 + + + + + 80 + True + False + - + + + 0 + 5 + + + + + + 70 + True + True + True + + + 1 + 5 + + + + + + 70 + True + True + True + + + 1 + 4 + + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + + + False + True + 1 + + + + + 160 + True + False + 10 + 10 + vertical + + + True + False + 5 + 5 + Left Stick + + + + + + False + True + 0 + + + + + True + False + 5 + 3 + 10 + + + 80 + True + False + LStick Button + 0 + + + 0 + 0 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + False + True + 1 + + + + + True + False + 3 + 10 + + + + 65 + True + True + True + + + 1 + 1 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + + 65 + True + True + True + + + 1 + 2 + + + + + + 65 + True + True + True + + + 1 + 3 + + + + + 80 + True + False + LStick Down + 0 + + + 0 + 1 + + + + + 80 + True + False + LStick Up + 0 + + + 0 + 0 + + + + + 80 + True + False + LStick Right + 0 + + + 0 + 3 + + + + + 80 + True + False + LStick Left + 0 + + + 0 + 2 + + + + + False + True + 2 + + + + + True + False + 3 + 10 + + + 80 + True + False + LStick + 0 + + + 0 + 0 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + Invert Stick X + True + True + False + True + + + 2 + 0 + + + + + Invert Stick Y + True + True + False + True + + + 2 + 1 + + + + + Rotate 90° Clockwise + True + True + False + True + + + 2 + 2 + + + + + False + True + 3 + + + + + True + False + 10 + vertical + + + True + False + start + Deadzone Left + + + False + True + 0 + + + + + True + True + _controllerDeadzoneLeft + 2 + 2 + + + True + True + 1 + + + + + False + True + 4 + + + + + True + False + 10 + vertical + + + True + False + start + Range Left + + + False + True + 0 + + + + + True + True + _controllerRangeLeft + 2 + 2 + + + True + True + 1 + + + + + False + True + 5 + + + + + False + True + 2 + + + + + True + False + + + False + True + 3 + + + + + 150 + True + False + 10 + 10 + vertical + + + True + False + 5 + 5 + Triggers + + + + + + False + True + 0 + + + + + True + False + 3 + 10 + + + 80 + True + False + L + + + 0 + 0 + + + + + 80 + True + False + R + + + 0 + 1 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + + 65 + True + True + True + + + 1 + 1 + + + + + 80 + True + False + ZL + + + 0 + 2 + + + + + 80 + True + False + ZR + + + 0 + 3 + + + + + + 65 + True + True + True + + + 1 + 2 + + + + + + 65 + True + True + True + + + 1 + 3 + + + + + False + True + 1 + + + + + _sideTriggerBox + True + False + 5 + 3 + 10 + + + 80 + True + False + Left SL + + + 0 + 0 + + + + + 80 + True + False + Left SR + + + 0 + 1 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + + 65 + True + True + True + + + 1 + 1 + + + + + False + True + 2 + + + + + _sideTriggerBox + True + False + 5 + 3 + 10 + + + 80 + True + False + Right SL + + + 0 + 0 + + + + + 80 + True + False + Right SR + + + 0 + 1 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + + 65 + True + True + True + + + 1 + 1 + + + + + False + True + 3 + + + + + True + False + 10 + vertical + + + True + False + start + 10 + Trigger Threshold + + + False + True + 0 + + + + + True + True + _controllerTriggerThreshold + 2 + 2 + + + True + True + 1 + + + + + False + True + 4 + + + + + False + True + 4 + + + + + False + True + 0 + + + + + True + False + 10 + + + 156 + True + False + 10 + 10 + vertical + + + True + False + 5 + 5 + Directional Pad + + + + + + False + True + 0 + + + + + True + False + 3 + 10 + + + 80 + True + False + Dpad Up + 0 + + + 0 + 0 + + + + + 80 + True + False + Dpad Down + 0 + + + 0 + 1 + + + + + 80 + True + False + Dpad Left + 0 + + + 0 + 2 + + + + + 80 + True + False + Dpad Right + 0 + + + 0 + 3 + + + + + + 70 + True + True + True + + + 1 + 0 + + + + + + 70 + True + True + True + + + 1 + 1 + + + + + + 70 + True + True + True + + + 1 + 2 + + + + + + 70 + True + True + True + + + 1 + 3 + + + + + False + True + 1 + + + + + True + False + 10 + vertical + + + True + False + 10 + 5 + Rumble + + + + + + False + True + 0 + + + + + Enable + True + True + False + True + + + False + True + 1 + + + + + True + False + 10 + vertical + + + True + False + start + Strong rumble multiplier + + + False + True + 0 + + + + + True + True + _controllerStrongRumble + 1 + 1 + + + True + True + 1 + + + + + False + True + 2 + + + + + True + False + 10 + vertical + + + True + False + start + Weak rumble multiplier + + + False + True + 0 + + + + + True + True + _controllerWeakRumble + 1 + 1 + + + True + True + 1 + + + + + False + True + 3 + + + + + False + True + 2 + + + + + False + True + 0 + + + + + True + False + + + False + True + 1 + + + + + 160 + True + False + 10 + 10 + vertical + + + True + False + 5 + 5 + Right Stick + + + + + + False + True + 0 + + + + + True + False + 5 + 3 + 10 + + + 80 + True + False + RStick Button + 0 + + + 0 + 0 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + False + True + 1 + + + + + True + False + 3 + 10 + + + 80 + True + False + RStick Up + 0 + + + 0 + 0 + + + + + 80 + True + False + RStick Down + 0 + + + 0 + 1 + + + + + 80 + True + False + RStick Left + 0 + + + 0 + 2 + + + + + 80 + True + False + RStick Right + 0 + + + 0 + 3 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + + 65 + True + True + True + + + 1 + 1 + + + + + + 65 + True + True + True + + + 1 + 2 + + + + + + 65 + True + True + True + + + 1 + 3 + + + + + False + True + 2 + + + + + True + False + 3 + 10 + + + 80 + True + False + RStick + 0 + + + 0 + 0 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + Invert Stick X + True + True + False + True + + + 2 + 0 + + + + + Invert Stick Y + True + True + False + True + + + 2 + 1 + + + + + Rotate 90° Clockwise + True + True + False + True + + + 2 + 2 + + + + + False + True + 3 + + + + + True + False + 10 + vertical + + + True + False + start + Deadzone Right + + + False + True + 0 + + + + + True + True + _controllerDeadzoneRight + 2 + 2 + + + True + True + 1 + + + + + False + True + 4 + + + + + True + False + 10 + vertical + + + True + False + start + Range Right + + + False + True + 0 + + + + + True + True + _controllerRangeRight + 2 + 2 + + + True + True + 1 + + + + + False + True + 5 + + + + + False + True + 2 + + + + + True + False + + + False + True + 3 + + + + + True + False + 10 + 10 + vertical + 5 + + + True + False + 5 + 5 + Motion + + + + + + False + True + 0 + + + + + Enable Motion Controls + True + True + False + True + + + False + True + 1 + + + + + Use CemuHook compatible motion + True + True + False + True + + + False + True + 2 + + + + + True + False + 10 + + + True + False + 17 + Controller Slot + + + False + True + 5 + 0 + + + + + True + True + 10 + _slotNumber + 1 + True + True + + + False + True + 1 + + + + + False + True + 5 + 3 + + + + + True + False + 10 + + + True + False + 5 + Gyro Sensitivity % + + + False + True + 5 + 0 + + + + + True + True + 0 + _sensitivity + 1 + True + True + + + False + True + 1 + + + + + False + True + 5 + 4 + + + + + True + False + vertical + + + Mirror Input + True + True + False + True + + + False + True + 0 + + + + + True + False + 10 + + + True + False + Right JoyCon Slot + + + False + True + 5 + 0 + + + + + True + True + 0 + _altSlotNumber + 1 + True + True + + + False + True + 1 + + + + + False + True + 5 + 1 + + + + + False + True + 5 + + + + + True + False + 30 + + + True + False + Server Host + + + False + True + 5 + 0 + + + + + True + True + + + False + True + 1 + + + + + False + True + 5 + 6 + + + + + True + False + 30 + + + True + False + Server Port + + + False + True + 5 + 0 + + + + + True + True + + + False + True + 1 + + + + + False + True + 5 + 7 + + + + + True + False + start + Gyro Deadzone + + + False + True + 8 + + + + + True + True + _gyroDeadzone + 2 + 2 + + + True + True + 9 + + + + + False + True + 4 + + + + + False + True + 1 + + + + + True + True + 0 + + + + + True + False + 10 + 20 + 5 + 5 + + + True + True + 1 + + + + + True + True + 1 + + + + + + + + + True + True + 0 + + + + + True + False + 5 + 3 + 3 + end + + + Save + True + True + True + + + + False + True + 0 + + + + + Close + True + True + True + 4 + + + + False + True + 5 + 1 + + + + + False + False + 1 + + + + + + diff --git a/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs new file mode 100644 index 00000000..388f1108 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs @@ -0,0 +1,280 @@ +using Gtk; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.UI.Widgets; +using System; +using System.Collections.Generic; +using System.IO; +using GUI = Gtk.Builder.ObjectAttribute; + +namespace Ryujinx.UI.Windows +{ + public class DlcWindow : Window + { + private readonly VirtualFileSystem _virtualFileSystem; + private readonly string _titleId; + private readonly string _dlcJsonPath; + private readonly List _dlcContainerList; + + private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + +#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier + [GUI] Label _baseTitleInfoLabel; + [GUI] TreeView _dlcTreeView; + [GUI] TreeSelection _dlcTreeSelection; +#pragma warning restore CS0649, IDE0044 + + public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Gtk3.UI.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { } + + private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow")) + { + builder.Autoconnect(this); + + _titleId = titleId; + _virtualFileSystem = virtualFileSystem; + _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json"); + _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]"; + + try + { + _dlcContainerList = JsonHelper.DeserializeFromFile(_dlcJsonPath, _serializerContext.ListDownloadableContentContainer); + } + catch + { + _dlcContainerList = new List(); + } + + _dlcTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string)); + + CellRendererToggle enableToggle = new(); + enableToggle.Toggled += (sender, args) => + { + _dlcTreeView.Model.GetIter(out TreeIter treeIter, new TreePath(args.Path)); + bool newValue = !(bool)_dlcTreeView.Model.GetValue(treeIter, 0); + _dlcTreeView.Model.SetValue(treeIter, 0, newValue); + + if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, treeIter)) + { + do + { + _dlcTreeView.Model.SetValue(childIter, 0, newValue); + } + while (_dlcTreeView.Model.IterNext(ref childIter)); + } + }; + + _dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0); + _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1); + _dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2); + + foreach (DownloadableContentContainer dlcContainer in _dlcContainerList) + { + if (File.Exists(dlcContainer.ContainerPath)) + { + // The parent tree item has its own "enabled" check box, but it's the actual + // nca entries that store the enabled / disabled state. A bit of a UI inconsistency. + // Maybe a tri-state check box would be better, but for now we check the parent + // "enabled" box if all child NCAs are enabled. Usually fine since each nsp has only one nca. + bool areAllContentPacksEnabled = dlcContainer.DownloadableContentNcaList.TrueForAll((nca) => nca.Enabled); + TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(areAllContentPacksEnabled, "", dlcContainer.ContainerPath); + + using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath); + + PartitionFileSystem pfs = new(); + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + + _virtualFileSystem.ImportTickets(pfs); + + foreach (DownloadableContentNca dlcNca in dlcContainer.DownloadableContentNcaList) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, dlcNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.ContainerPath); + + if (nca != null) + { + ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter, dlcNca.Enabled, nca.Header.TitleId.ToString("X16"), dlcNca.FullPath); + } + } + } + else + { + // DLC file moved or renamed. Allow the user to remove it without crashing the whole dialog. + TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", $"(MISSING) {dlcContainer.ContainerPath}"); + } + } + } + + private Nca TryCreateNca(IStorage ncaStorage, string containerPath) + { + try + { + return new Nca(_virtualFileSystem.KeySet, ncaStorage); + } + catch (Exception exception) + { + GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {containerPath}"); + } + + return null; + } + + private void AddButton_Clicked(object sender, EventArgs args) + { + FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel") + { + SelectMultiple = true, + }; + + FileFilter filter = new() + { + Name = "Switch Game DLCs", + }; + filter.AddPattern("*.nsp"); + + fileChooser.AddFilter(filter); + + if (fileChooser.Run() == (int)ResponseType.Accept) + { + foreach (string containerPath in fileChooser.Filenames) + { + if (!File.Exists(containerPath)) + { + return; + } + + using FileStream containerFile = File.OpenRead(containerPath); + + PartitionFileSystem pfs = new(); + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + bool containsDlc = false; + + _virtualFileSystem.ImportTickets(pfs); + + TreeIter? parentIter = null; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + 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!"); + } + } + } + + fileChooser.Dispose(); + } + + private void RemoveButton_Clicked(object sender, EventArgs args) + { + if (_dlcTreeSelection.GetSelected(out ITreeModel treeModel, out TreeIter treeIter)) + { + if (_dlcTreeView.Model.IterParent(out TreeIter parentIter, treeIter) && _dlcTreeView.Model.IterNChildren(parentIter) <= 1) + { + ((TreeStore)treeModel).Remove(ref parentIter); + } + else + { + ((TreeStore)treeModel).Remove(ref treeIter); + } + } + } + + private void RemoveAllButton_Clicked(object sender, EventArgs args) + { + List toRemove = new(); + + if (_dlcTreeView.Model.GetIterFirst(out TreeIter iter)) + { + do + { + toRemove.Add(iter); + } + while (_dlcTreeView.Model.IterNext(ref iter)); + } + + foreach (TreeIter i in toRemove) + { + TreeIter j = i; + ((TreeStore)_dlcTreeView.Model).Remove(ref j); + } + } + + private void SaveButton_Clicked(object sender, EventArgs args) + { + _dlcContainerList.Clear(); + + if (_dlcTreeView.Model.GetIterFirst(out TreeIter parentIter)) + { + do + { + if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, parentIter)) + { + DownloadableContentContainer dlcContainer = new() + { + ContainerPath = (string)_dlcTreeView.Model.GetValue(parentIter, 2), + DownloadableContentNcaList = new List(), + }; + + do + { + dlcContainer.DownloadableContentNcaList.Add(new DownloadableContentNca + { + Enabled = (bool)_dlcTreeView.Model.GetValue(childIter, 0), + TitleId = Convert.ToUInt64(_dlcTreeView.Model.GetValue(childIter, 1).ToString(), 16), + FullPath = (string)_dlcTreeView.Model.GetValue(childIter, 2), + }); + } + while (_dlcTreeView.Model.IterNext(ref childIter)); + + _dlcContainerList.Add(dlcContainer); + } + } + while (_dlcTreeView.Model.IterNext(ref parentIter)); + } + + JsonHelper.SerializeToFile(_dlcJsonPath, _dlcContainerList, _serializerContext.ListDownloadableContentContainer); + + Dispose(); + } + + private void CancelButton_Clicked(object sender, EventArgs args) + { + Dispose(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.glade b/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.glade new file mode 100644 index 00000000..bdb0e647 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.glade @@ -0,0 +1,202 @@ + + + + + + False + Ryujinx - DLC Manager + True + center + 550 + 350 + + + True + False + vertical + + + True + False + vertical + + + True + False + 10 + 10 + 10 + 10 + Available DLC + + + False + True + 0 + + + + + True + True + 10 + 10 + in + + + True + False + + + True + True + False + + + + + + + + + + True + True + 1 + + + + + True + True + 0 + + + + + True + False + + + True + False + 10 + 10 + start + + + Add + True + True + True + Adds a DLC to this list + 10 + + + + True + True + 0 + + + + + Remove + True + True + True + Removes the selected DLC + 10 + + + + True + True + 1 + + + + + Remove All + True + True + True + Removes all DLCs + 10 + + + + True + True + 2 + + + + + True + True + 0 + + + + + True + False + 10 + 10 + end + + + Save + True + True + True + 10 + 2 + 2 + + + + True + True + 0 + + + + + Cancel + True + True + True + 10 + 2 + 2 + + + + True + True + 1 + + + + + True + True + 1 + + + + + False + True + 1 + + + + + + + + + diff --git a/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs new file mode 100644 index 00000000..dc467c0f --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs @@ -0,0 +1,847 @@ +using Gtk; +using LibHac.Tools.FsSystem; +using Ryujinx.Audio.Backends.OpenAL; +using Ryujinx.Audio.Backends.SDL2; +using Ryujinx.Audio.Backends.SoundIo; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.GraphicsDriver; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Time.TimeZone; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Helper; +using Ryujinx.UI.Widgets; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net.NetworkInformation; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using GUI = Gtk.Builder.ObjectAttribute; + +namespace Ryujinx.UI.Windows +{ + public class SettingsWindow : Window + { + private readonly MainWindow _parent; + private readonly ListStore _gameDirsBoxStore; + private readonly ListStore _audioBackendStore; + private readonly TimeZoneContentManager _timeZoneContentManager; + private readonly HashSet _validTzRegions; + + private long _systemTimeOffset; + private float _previousVolumeLevel; + private bool _directoryChanged = false; + +#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier + [GUI] CheckButton _traceLogToggle; + [GUI] CheckButton _errorLogToggle; + [GUI] CheckButton _warningLogToggle; + [GUI] CheckButton _infoLogToggle; + [GUI] CheckButton _stubLogToggle; + [GUI] CheckButton _debugLogToggle; + [GUI] CheckButton _fileLogToggle; + [GUI] CheckButton _guestLogToggle; + [GUI] CheckButton _fsAccessLogToggle; + [GUI] Adjustment _fsLogSpinAdjustment; + [GUI] ComboBoxText _graphicsDebugLevel; + [GUI] CheckButton _dockedModeToggle; + [GUI] CheckButton _discordToggle; + [GUI] CheckButton _checkUpdatesToggle; + [GUI] CheckButton _showConfirmExitToggle; + [GUI] RadioButton _hideCursorNever; + [GUI] RadioButton _hideCursorOnIdle; + [GUI] RadioButton _hideCursorAlways; + [GUI] CheckButton _vSyncToggle; + [GUI] CheckButton _shaderCacheToggle; + [GUI] CheckButton _textureRecompressionToggle; + [GUI] CheckButton _macroHLEToggle; + [GUI] CheckButton _ptcToggle; + [GUI] CheckButton _internetToggle; + [GUI] CheckButton _fsicToggle; + [GUI] RadioButton _mmSoftware; + [GUI] RadioButton _mmHost; + [GUI] RadioButton _mmHostUnsafe; + [GUI] CheckButton _expandRamToggle; + [GUI] CheckButton _ignoreToggle; + [GUI] CheckButton _directKeyboardAccess; + [GUI] CheckButton _directMouseAccess; + [GUI] ComboBoxText _systemLanguageSelect; + [GUI] ComboBoxText _systemRegionSelect; + [GUI] Entry _systemTimeZoneEntry; + [GUI] EntryCompletion _systemTimeZoneCompletion; + [GUI] Box _audioBackendBox; + [GUI] ComboBox _audioBackendSelect; + [GUI] Label _audioVolumeLabel; + [GUI] Scale _audioVolumeSlider; + [GUI] SpinButton _systemTimeYearSpin; + [GUI] SpinButton _systemTimeMonthSpin; + [GUI] SpinButton _systemTimeDaySpin; + [GUI] SpinButton _systemTimeHourSpin; + [GUI] SpinButton _systemTimeMinuteSpin; + [GUI] Adjustment _systemTimeYearSpinAdjustment; + [GUI] Adjustment _systemTimeMonthSpinAdjustment; + [GUI] Adjustment _systemTimeDaySpinAdjustment; + [GUI] Adjustment _systemTimeHourSpinAdjustment; + [GUI] Adjustment _systemTimeMinuteSpinAdjustment; + [GUI] ComboBoxText _multiLanSelect; + [GUI] ComboBoxText _multiModeSelect; + [GUI] CheckButton _custThemeToggle; + [GUI] Entry _custThemePath; + [GUI] ToggleButton _browseThemePath; + [GUI] Label _custThemePathLabel; + [GUI] TreeView _gameDirsBox; + [GUI] Entry _addGameDirBox; + [GUI] ComboBoxText _galThreading; + [GUI] Entry _graphicsShadersDumpPath; + [GUI] ComboBoxText _anisotropy; + [GUI] ComboBoxText _aspectRatio; + [GUI] ComboBoxText _antiAliasing; + [GUI] ComboBoxText _scalingFilter; + [GUI] ComboBoxText _graphicsBackend; + [GUI] ComboBoxText _preferredGpu; + [GUI] ComboBoxText _resScaleCombo; + [GUI] Entry _resScaleText; + [GUI] Adjustment _scalingFilterLevel; + [GUI] Scale _scalingFilterSlider; + [GUI] ToggleButton _configureController1; + [GUI] ToggleButton _configureController2; + [GUI] ToggleButton _configureController3; + [GUI] ToggleButton _configureController4; + [GUI] ToggleButton _configureController5; + [GUI] ToggleButton _configureController6; + [GUI] ToggleButton _configureController7; + [GUI] ToggleButton _configureController8; + [GUI] ToggleButton _configureControllerH; + +#pragma warning restore CS0649, IDE0044 + + public SettingsWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this(parent, new Builder("Ryujinx.Gtk3.UI.Windows.SettingsWindow.glade"), virtualFileSystem, contentManager) { } + + private SettingsWindow(MainWindow parent, Builder builder, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : base(builder.GetRawOwnedObject("_settingsWin")) + { + Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png"); + + _parent = parent; + + builder.Autoconnect(this); + + _timeZoneContentManager = new TimeZoneContentManager(); + _timeZoneContentManager.InitializeInstance(virtualFileSystem, contentManager, IntegrityCheckLevel.None); + + _validTzRegions = new HashSet(_timeZoneContentManager.LocationNameCache.Length, StringComparer.Ordinal); // Zone regions are identifiers. Must match exactly. + + // Bind Events. + _configureController1.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player1); + _configureController2.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player2); + _configureController3.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player3); + _configureController4.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player4); + _configureController5.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player5); + _configureController6.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player6); + _configureController7.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player7); + _configureController8.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player8); + _configureControllerH.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Handheld); + _systemTimeZoneEntry.FocusOutEvent += TimeZoneEntry_FocusOut; + + _resScaleCombo.Changed += (sender, args) => _resScaleText.Visible = _resScaleCombo.ActiveId == "-1"; + _scalingFilter.Changed += (sender, args) => _scalingFilterSlider.Visible = _scalingFilter.ActiveId == "2"; + _galThreading.Changed += (sender, args) => + { + if (_galThreading.ActiveId != ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString()) + { + GtkDialog.CreateInfoDialog("Warning - Backend Threading", "Ryujinx must be restarted after changing this option for it to apply fully. Depending on your platform, you may need to manually disable your driver's own multithreading when using Ryujinx's."); + } + }; + + // Setup Currents. + if (ConfigurationState.Instance.Logger.EnableTrace) + { + _traceLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableFileLog) + { + _fileLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableError) + { + _errorLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableWarn) + { + _warningLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableInfo) + { + _infoLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableStub) + { + _stubLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableDebug) + { + _debugLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableGuest) + { + _guestLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableFsAccessLog) + { + _fsAccessLogToggle.Click(); + } + + foreach (GraphicsDebugLevel level in Enum.GetValues()) + { + _graphicsDebugLevel.Append(level.ToString(), level.ToString()); + } + + _graphicsDebugLevel.SetActiveId(ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value.ToString()); + + if (ConfigurationState.Instance.System.EnableDockedMode) + { + _dockedModeToggle.Click(); + } + + if (ConfigurationState.Instance.EnableDiscordIntegration) + { + _discordToggle.Click(); + } + + if (ConfigurationState.Instance.CheckUpdatesOnStart) + { + _checkUpdatesToggle.Click(); + } + + if (ConfigurationState.Instance.ShowConfirmExit) + { + _showConfirmExitToggle.Click(); + } + + switch (ConfigurationState.Instance.HideCursor.Value) + { + case HideCursorMode.Never: + _hideCursorNever.Click(); + break; + case HideCursorMode.OnIdle: + _hideCursorOnIdle.Click(); + break; + case HideCursorMode.Always: + _hideCursorAlways.Click(); + break; + } + + if (ConfigurationState.Instance.Graphics.EnableVsync) + { + _vSyncToggle.Click(); + } + + if (ConfigurationState.Instance.Graphics.EnableShaderCache) + { + _shaderCacheToggle.Click(); + } + + if (ConfigurationState.Instance.Graphics.EnableTextureRecompression) + { + _textureRecompressionToggle.Click(); + } + + if (ConfigurationState.Instance.Graphics.EnableMacroHLE) + { + _macroHLEToggle.Click(); + } + + if (ConfigurationState.Instance.System.EnablePtc) + { + _ptcToggle.Click(); + } + + if (ConfigurationState.Instance.System.EnableInternetAccess) + { + _internetToggle.Click(); + } + + if (ConfigurationState.Instance.System.EnableFsIntegrityChecks) + { + _fsicToggle.Click(); + } + + switch (ConfigurationState.Instance.System.MemoryManagerMode.Value) + { + case MemoryManagerMode.SoftwarePageTable: + _mmSoftware.Click(); + break; + case MemoryManagerMode.HostMapped: + _mmHost.Click(); + break; + case MemoryManagerMode.HostMappedUnsafe: + _mmHostUnsafe.Click(); + break; + } + + if (ConfigurationState.Instance.System.ExpandRam) + { + _expandRamToggle.Click(); + } + + if (ConfigurationState.Instance.System.IgnoreMissingServices) + { + _ignoreToggle.Click(); + } + + if (ConfigurationState.Instance.Hid.EnableKeyboard) + { + _directKeyboardAccess.Click(); + } + + if (ConfigurationState.Instance.Hid.EnableMouse) + { + _directMouseAccess.Click(); + } + + if (ConfigurationState.Instance.UI.EnableCustomTheme) + { + _custThemeToggle.Click(); + } + + // Custom EntryCompletion Columns. If added to glade, need to override more signals + ListStore tzList = new(typeof(string), typeof(string), typeof(string)); + _systemTimeZoneCompletion.Model = tzList; + + CellRendererText offsetCol = new(); + CellRendererText abbrevCol = new(); + + _systemTimeZoneCompletion.PackStart(offsetCol, false); + _systemTimeZoneCompletion.AddAttribute(offsetCol, "text", 0); + _systemTimeZoneCompletion.TextColumn = 1; // Regions Column + _systemTimeZoneCompletion.PackStart(abbrevCol, false); + _systemTimeZoneCompletion.AddAttribute(abbrevCol, "text", 2); + + int maxLocationLength = 0; + + foreach (var (offset, location, abbr) in _timeZoneContentManager.ParseTzOffsets()) + { + var hours = Math.DivRem(offset, 3600, out int seconds); + var minutes = Math.Abs(seconds) / 60; + + var abbr2 = (abbr.StartsWith('+') || abbr.StartsWith('-')) ? string.Empty : abbr; + + tzList.AppendValues($"UTC{hours:+0#;-0#;+00}:{minutes:D2} ", location, abbr2); + _validTzRegions.Add(location); + + maxLocationLength = Math.Max(maxLocationLength, location.Length); + } + + _systemTimeZoneEntry.WidthChars = Math.Max(20, maxLocationLength + 1); // Ensure minimum Entry width + _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(ConfigurationState.Instance.System.TimeZone); + + _systemTimeZoneCompletion.MatchFunc = TimeZoneMatchFunc; + + _systemLanguageSelect.SetActiveId(ConfigurationState.Instance.System.Language.Value.ToString()); + _systemRegionSelect.SetActiveId(ConfigurationState.Instance.System.Region.Value.ToString()); + _galThreading.SetActiveId(ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString()); + _resScaleCombo.SetActiveId(ConfigurationState.Instance.Graphics.ResScale.Value.ToString()); + _anisotropy.SetActiveId(ConfigurationState.Instance.Graphics.MaxAnisotropy.Value.ToString()); + _aspectRatio.SetActiveId(((int)ConfigurationState.Instance.Graphics.AspectRatio.Value).ToString()); + _graphicsBackend.SetActiveId(((int)ConfigurationState.Instance.Graphics.GraphicsBackend.Value).ToString()); + _antiAliasing.SetActiveId(((int)ConfigurationState.Instance.Graphics.AntiAliasing.Value).ToString()); + _scalingFilter.SetActiveId(((int)ConfigurationState.Instance.Graphics.ScalingFilter.Value).ToString()); + + UpdatePreferredGpuComboBox(); + + _graphicsBackend.Changed += (sender, e) => UpdatePreferredGpuComboBox(); + PopulateNetworkInterfaces(); + _multiLanSelect.SetActiveId(ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value); + _multiModeSelect.SetActiveId(ConfigurationState.Instance.Multiplayer.Mode.Value.ToString()); + + _custThemePath.Buffer.Text = ConfigurationState.Instance.UI.CustomThemePath; + _resScaleText.Buffer.Text = ConfigurationState.Instance.Graphics.ResScaleCustom.Value.ToString(); + _scalingFilterLevel.Value = ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value; + _resScaleText.Visible = _resScaleCombo.ActiveId == "-1"; + _scalingFilterSlider.Visible = _scalingFilter.ActiveId == "2"; + _graphicsShadersDumpPath.Buffer.Text = ConfigurationState.Instance.Graphics.ShadersDumpPath; + _fsLogSpinAdjustment.Value = ConfigurationState.Instance.System.FsGlobalAccessLogMode; + _systemTimeOffset = ConfigurationState.Instance.System.SystemTimeOffset; + + _gameDirsBox.AppendColumn("", new CellRendererText(), "text", 0); + _gameDirsBoxStore = new ListStore(typeof(string)); + _gameDirsBox.Model = _gameDirsBoxStore; + + foreach (string gameDir in ConfigurationState.Instance.UI.GameDirs.Value) + { + _gameDirsBoxStore.AppendValues(gameDir); + } + + if (_custThemeToggle.Active == false) + { + _custThemePath.Sensitive = false; + _custThemePathLabel.Sensitive = false; + _browseThemePath.Sensitive = false; + } + + // Setup system time spinners + UpdateSystemTimeSpinners(); + + _audioBackendStore = new ListStore(typeof(string), typeof(AudioBackend)); + + TreeIter openAlIter = _audioBackendStore.AppendValues("OpenAL", AudioBackend.OpenAl); + TreeIter soundIoIter = _audioBackendStore.AppendValues("SoundIO", AudioBackend.SoundIo); + TreeIter sdl2Iter = _audioBackendStore.AppendValues("SDL2", AudioBackend.SDL2); + TreeIter dummyIter = _audioBackendStore.AppendValues("Dummy", AudioBackend.Dummy); + + _audioBackendSelect = ComboBox.NewWithModelAndEntry(_audioBackendStore); + _audioBackendSelect.EntryTextColumn = 0; + _audioBackendSelect.Entry.IsEditable = false; + + switch (ConfigurationState.Instance.System.AudioBackend.Value) + { + case AudioBackend.OpenAl: + _audioBackendSelect.SetActiveIter(openAlIter); + break; + case AudioBackend.SoundIo: + _audioBackendSelect.SetActiveIter(soundIoIter); + break; + case AudioBackend.SDL2: + _audioBackendSelect.SetActiveIter(sdl2Iter); + break; + case AudioBackend.Dummy: + _audioBackendSelect.SetActiveIter(dummyIter); + break; + default: + throw new InvalidOperationException($"{nameof(ConfigurationState.Instance.System.AudioBackend)} contains an invalid value: {ConfigurationState.Instance.System.AudioBackend.Value}"); + } + + _audioBackendBox.Add(_audioBackendSelect); + _audioBackendSelect.Show(); + + _previousVolumeLevel = ConfigurationState.Instance.System.AudioVolume; + _audioVolumeLabel = new Label("Volume: "); + _audioVolumeSlider = new Scale(Orientation.Horizontal, 0, 100, 1); + _audioVolumeLabel.MarginStart = 10; + _audioVolumeSlider.ValuePos = PositionType.Right; + _audioVolumeSlider.WidthRequest = 200; + + _audioVolumeSlider.Value = _previousVolumeLevel * 100; + _audioVolumeSlider.ValueChanged += VolumeSlider_OnChange; + _audioBackendBox.Add(_audioVolumeLabel); + _audioBackendBox.Add(_audioVolumeSlider); + _audioVolumeLabel.Show(); + _audioVolumeSlider.Show(); + + bool openAlIsSupported = false; + bool soundIoIsSupported = false; + bool sdl2IsSupported = false; + + Task.Run(() => + { + openAlIsSupported = OpenALHardwareDeviceDriver.IsSupported; + soundIoIsSupported = !OperatingSystem.IsMacOS() && SoundIoHardwareDeviceDriver.IsSupported; + sdl2IsSupported = SDL2HardwareDeviceDriver.IsSupported; + }); + + // This function runs whenever the dropdown is opened + _audioBackendSelect.SetCellDataFunc(_audioBackendSelect.Cells[0], (layout, cell, model, iter) => + { + cell.Sensitive = ((AudioBackend)_audioBackendStore.GetValue(iter, 1)) switch + { + AudioBackend.OpenAl => openAlIsSupported, + AudioBackend.SoundIo => soundIoIsSupported, + AudioBackend.SDL2 => sdl2IsSupported, + AudioBackend.Dummy => true, + _ => throw new InvalidOperationException($"{nameof(_audioBackendStore)} contains an invalid value for iteration {iter}: {_audioBackendStore.GetValue(iter, 1)}"), + }; + }); + + if (OperatingSystem.IsMacOS()) + { + var store = (_graphicsBackend.Model as ListStore); + store.GetIter(out TreeIter openglIter, new TreePath(new[] { 1 })); + store.Remove(ref openglIter); + + _graphicsBackend.Model = store; + } + } + + private void UpdatePreferredGpuComboBox() + { + _preferredGpu.RemoveAll(); + + if (Enum.Parse(_graphicsBackend.ActiveId) == GraphicsBackend.Vulkan) + { + var devices = Graphics.Vulkan.VulkanRenderer.GetPhysicalDevices(); + string preferredGpuIdFromConfig = ConfigurationState.Instance.Graphics.PreferredGpu.Value; + string preferredGpuId = preferredGpuIdFromConfig; + bool noGpuId = string.IsNullOrEmpty(preferredGpuIdFromConfig); + + foreach (var device in devices) + { + string dGpu = device.IsDiscrete ? " (dGPU)" : ""; + _preferredGpu.Append(device.Id, $"{device.Name}{dGpu}"); + + // If there's no GPU selected yet, we just pick the first GPU. + // If there's a discrete GPU available, we always prefer that over the previous selection, + // as it is likely to have better performance and more features. + // If the configuration file already has a GPU selection, we always prefer that instead. + if (noGpuId && (string.IsNullOrEmpty(preferredGpuId) || device.IsDiscrete)) + { + preferredGpuId = device.Id; + } + } + + if (!string.IsNullOrEmpty(preferredGpuId)) + { + _preferredGpu.SetActiveId(preferredGpuId); + } + } + } + + private void PopulateNetworkInterfaces() + { + NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces(); + + foreach (NetworkInterface nif in interfaces) + { + string guid = nif.Id; + string name = nif.Name; + + _multiLanSelect.Append(guid, name); + } + } + + private void UpdateSystemTimeSpinners() + { + //Bind system time events + _systemTimeYearSpin.ValueChanged -= SystemTimeSpin_ValueChanged; + _systemTimeMonthSpin.ValueChanged -= SystemTimeSpin_ValueChanged; + _systemTimeDaySpin.ValueChanged -= SystemTimeSpin_ValueChanged; + _systemTimeHourSpin.ValueChanged -= SystemTimeSpin_ValueChanged; + _systemTimeMinuteSpin.ValueChanged -= SystemTimeSpin_ValueChanged; + + //Apply actual system time + SystemTimeOffset to system time spin buttons + DateTime systemTime = DateTime.Now.AddSeconds(_systemTimeOffset); + + _systemTimeYearSpinAdjustment.Value = systemTime.Year; + _systemTimeMonthSpinAdjustment.Value = systemTime.Month; + _systemTimeDaySpinAdjustment.Value = systemTime.Day; + _systemTimeHourSpinAdjustment.Value = systemTime.Hour; + _systemTimeMinuteSpinAdjustment.Value = systemTime.Minute; + + //Format spin buttons text to include leading zeros + _systemTimeYearSpin.Text = systemTime.Year.ToString("0000"); + _systemTimeMonthSpin.Text = systemTime.Month.ToString("00"); + _systemTimeDaySpin.Text = systemTime.Day.ToString("00"); + _systemTimeHourSpin.Text = systemTime.Hour.ToString("00"); + _systemTimeMinuteSpin.Text = systemTime.Minute.ToString("00"); + + //Bind system time events + _systemTimeYearSpin.ValueChanged += SystemTimeSpin_ValueChanged; + _systemTimeMonthSpin.ValueChanged += SystemTimeSpin_ValueChanged; + _systemTimeDaySpin.ValueChanged += SystemTimeSpin_ValueChanged; + _systemTimeHourSpin.ValueChanged += SystemTimeSpin_ValueChanged; + _systemTimeMinuteSpin.ValueChanged += SystemTimeSpin_ValueChanged; + } + + private void SaveSettings() + { + if (_directoryChanged) + { + List gameDirs = new(); + + _gameDirsBoxStore.GetIterFirst(out TreeIter treeIter); + + for (int i = 0; i < _gameDirsBoxStore.IterNChildren(); i++) + { + gameDirs.Add((string)_gameDirsBoxStore.GetValue(treeIter, 0)); + + _gameDirsBoxStore.IterNext(ref treeIter); + } + + ConfigurationState.Instance.UI.GameDirs.Value = gameDirs; + + _directoryChanged = false; + } + + HideCursorMode hideCursor = HideCursorMode.Never; + + if (_hideCursorOnIdle.Active) + { + hideCursor = HideCursorMode.OnIdle; + } + + if (_hideCursorAlways.Active) + { + hideCursor = HideCursorMode.Always; + } + + if (!float.TryParse(_resScaleText.Buffer.Text, out float resScaleCustom) || resScaleCustom <= 0.0f) + { + resScaleCustom = 1.0f; + } + + if (_validTzRegions.Contains(_systemTimeZoneEntry.Text)) + { + ConfigurationState.Instance.System.TimeZone.Value = _systemTimeZoneEntry.Text; + } + + MemoryManagerMode memoryMode = MemoryManagerMode.SoftwarePageTable; + + if (_mmHost.Active) + { + memoryMode = MemoryManagerMode.HostMapped; + } + + if (_mmHostUnsafe.Active) + { + memoryMode = MemoryManagerMode.HostMappedUnsafe; + } + + BackendThreading backendThreading = Enum.Parse(_galThreading.ActiveId); + if (ConfigurationState.Instance.Graphics.BackendThreading != backendThreading) + { + DriverUtilities.ToggleOGLThreading(backendThreading == BackendThreading.Off); + } + + ConfigurationState.Instance.Logger.EnableError.Value = _errorLogToggle.Active; + ConfigurationState.Instance.Logger.EnableTrace.Value = _traceLogToggle.Active; + ConfigurationState.Instance.Logger.EnableWarn.Value = _warningLogToggle.Active; + ConfigurationState.Instance.Logger.EnableInfo.Value = _infoLogToggle.Active; + ConfigurationState.Instance.Logger.EnableStub.Value = _stubLogToggle.Active; + ConfigurationState.Instance.Logger.EnableDebug.Value = _debugLogToggle.Active; + ConfigurationState.Instance.Logger.EnableGuest.Value = _guestLogToggle.Active; + ConfigurationState.Instance.Logger.EnableFsAccessLog.Value = _fsAccessLogToggle.Active; + ConfigurationState.Instance.Logger.EnableFileLog.Value = _fileLogToggle.Active; + ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value = Enum.Parse(_graphicsDebugLevel.ActiveId); + ConfigurationState.Instance.System.EnableDockedMode.Value = _dockedModeToggle.Active; + ConfigurationState.Instance.EnableDiscordIntegration.Value = _discordToggle.Active; + ConfigurationState.Instance.CheckUpdatesOnStart.Value = _checkUpdatesToggle.Active; + ConfigurationState.Instance.ShowConfirmExit.Value = _showConfirmExitToggle.Active; + ConfigurationState.Instance.HideCursor.Value = hideCursor; + ConfigurationState.Instance.Graphics.EnableVsync.Value = _vSyncToggle.Active; + ConfigurationState.Instance.Graphics.EnableShaderCache.Value = _shaderCacheToggle.Active; + ConfigurationState.Instance.Graphics.EnableTextureRecompression.Value = _textureRecompressionToggle.Active; + ConfigurationState.Instance.Graphics.EnableMacroHLE.Value = _macroHLEToggle.Active; + ConfigurationState.Instance.System.EnablePtc.Value = _ptcToggle.Active; + ConfigurationState.Instance.System.EnableInternetAccess.Value = _internetToggle.Active; + ConfigurationState.Instance.System.EnableFsIntegrityChecks.Value = _fsicToggle.Active; + ConfigurationState.Instance.System.MemoryManagerMode.Value = memoryMode; + ConfigurationState.Instance.System.ExpandRam.Value = _expandRamToggle.Active; + ConfigurationState.Instance.System.IgnoreMissingServices.Value = _ignoreToggle.Active; + ConfigurationState.Instance.Hid.EnableKeyboard.Value = _directKeyboardAccess.Active; + ConfigurationState.Instance.Hid.EnableMouse.Value = _directMouseAccess.Active; + ConfigurationState.Instance.UI.EnableCustomTheme.Value = _custThemeToggle.Active; + ConfigurationState.Instance.System.Language.Value = Enum.Parse(_systemLanguageSelect.ActiveId); + ConfigurationState.Instance.System.Region.Value = Enum.Parse(_systemRegionSelect.ActiveId); + ConfigurationState.Instance.System.SystemTimeOffset.Value = _systemTimeOffset; + ConfigurationState.Instance.UI.CustomThemePath.Value = _custThemePath.Buffer.Text; + ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = _graphicsShadersDumpPath.Buffer.Text; + ConfigurationState.Instance.System.FsGlobalAccessLogMode.Value = (int)_fsLogSpinAdjustment.Value; + ConfigurationState.Instance.Graphics.MaxAnisotropy.Value = float.Parse(_anisotropy.ActiveId, CultureInfo.InvariantCulture); + ConfigurationState.Instance.Graphics.AspectRatio.Value = Enum.Parse(_aspectRatio.ActiveId); + ConfigurationState.Instance.Graphics.BackendThreading.Value = backendThreading; + ConfigurationState.Instance.Graphics.GraphicsBackend.Value = Enum.Parse(_graphicsBackend.ActiveId); + ConfigurationState.Instance.Graphics.PreferredGpu.Value = _preferredGpu.ActiveId; + ConfigurationState.Instance.Graphics.ResScale.Value = int.Parse(_resScaleCombo.ActiveId); + ConfigurationState.Instance.Graphics.ResScaleCustom.Value = resScaleCustom; + ConfigurationState.Instance.System.AudioVolume.Value = (float)_audioVolumeSlider.Value / 100.0f; + ConfigurationState.Instance.Graphics.AntiAliasing.Value = Enum.Parse(_antiAliasing.ActiveId); + ConfigurationState.Instance.Graphics.ScalingFilter.Value = Enum.Parse(_scalingFilter.ActiveId); + ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value = (int)_scalingFilterLevel.Value; + ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _multiLanSelect.ActiveId; + + _previousVolumeLevel = ConfigurationState.Instance.System.AudioVolume.Value; + + ConfigurationState.Instance.Multiplayer.Mode.Value = Enum.Parse(_multiModeSelect.ActiveId); + ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _multiLanSelect.ActiveId; + + if (_audioBackendSelect.GetActiveIter(out TreeIter activeIter)) + { + ConfigurationState.Instance.System.AudioBackend.Value = (AudioBackend)_audioBackendStore.GetValue(activeIter, 1); + } + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + _parent.UpdateInternetAccess(); + MainWindow.UpdateGraphicsConfig(); + ThemeHelper.ApplyTheme(); + } + + // + // Events + // + private void TimeZoneEntry_FocusOut(object sender, FocusOutEventArgs e) + { + if (!_validTzRegions.Contains(_systemTimeZoneEntry.Text)) + { + _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(ConfigurationState.Instance.System.TimeZone); + } + } + + private bool TimeZoneMatchFunc(EntryCompletion compl, string key, TreeIter iter) + { + key = key.Trim().Replace(' ', '_'); + + return ((string)compl.Model.GetValue(iter, 1)).Contains(key, StringComparison.OrdinalIgnoreCase) || // region + ((string)compl.Model.GetValue(iter, 2)).StartsWith(key, StringComparison.OrdinalIgnoreCase) || // abbr + ((string)compl.Model.GetValue(iter, 0))[3..].StartsWith(key); // offset + } + + private void SystemTimeSpin_ValueChanged(object sender, EventArgs e) + { + int year = _systemTimeYearSpin.ValueAsInt; + int month = _systemTimeMonthSpin.ValueAsInt; + int day = _systemTimeDaySpin.ValueAsInt; + int hour = _systemTimeHourSpin.ValueAsInt; + int minute = _systemTimeMinuteSpin.ValueAsInt; + + if (!DateTime.TryParse(year + "-" + month + "-" + day + " " + hour + ":" + minute, out DateTime newTime)) + { + UpdateSystemTimeSpinners(); + + return; + } + + newTime = newTime.AddSeconds(DateTime.Now.Second).AddMilliseconds(DateTime.Now.Millisecond); + + long systemTimeOffset = (long)Math.Ceiling((newTime - DateTime.Now).TotalMinutes) * 60L; + + if (_systemTimeOffset != systemTimeOffset) + { + _systemTimeOffset = systemTimeOffset; + UpdateSystemTimeSpinners(); + } + } + + private void AddDir_Pressed(object sender, EventArgs args) + { + if (Directory.Exists(_addGameDirBox.Buffer.Text)) + { + _gameDirsBoxStore.AppendValues(_addGameDirBox.Buffer.Text); + _directoryChanged = true; + } + else + { + FileChooserNative fileChooser = new("Choose the game directory to add to the list", this, FileChooserAction.SelectFolder, "Add", "Cancel") + { + SelectMultiple = true, + }; + + if (fileChooser.Run() == (int)ResponseType.Accept) + { + _directoryChanged = false; + foreach (string directory in fileChooser.Filenames) + { + if (_gameDirsBoxStore.GetIterFirst(out TreeIter treeIter)) + { + do + { + if (directory.Equals((string)_gameDirsBoxStore.GetValue(treeIter, 0))) + { + break; + } + } while (_gameDirsBoxStore.IterNext(ref treeIter)); + } + + if (!_directoryChanged) + { + _gameDirsBoxStore.AppendValues(directory); + } + } + + _directoryChanged = true; + } + + fileChooser.Dispose(); + } + + _addGameDirBox.Buffer.Text = ""; + + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + } + + private void RemoveDir_Pressed(object sender, EventArgs args) + { + TreeSelection selection = _gameDirsBox.Selection; + + if (selection.GetSelected(out TreeIter treeIter)) + { + _gameDirsBoxStore.Remove(ref treeIter); + + _directoryChanged = true; + } + + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + } + + private void CustThemeToggle_Activated(object sender, EventArgs args) + { + _custThemePath.Sensitive = _custThemeToggle.Active; + _custThemePathLabel.Sensitive = _custThemeToggle.Active; + _browseThemePath.Sensitive = _custThemeToggle.Active; + } + + private void BrowseThemeDir_Pressed(object sender, EventArgs args) + { + using (FileChooserNative fileChooser = new("Choose the theme to load", this, FileChooserAction.Open, "Select", "Cancel")) + { + FileFilter filter = new() + { + Name = "Theme Files", + }; + filter.AddPattern("*.css"); + + fileChooser.AddFilter(filter); + + if (fileChooser.Run() == (int)ResponseType.Accept) + { + _custThemePath.Buffer.Text = fileChooser.Filename; + } + } + + _browseThemePath.SetStateFlags(StateFlags.Normal, true); + } + + private void ConfigureController_Pressed(object sender, PlayerIndex playerIndex) + { + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + + ControllerWindow controllerWindow = new(_parent, playerIndex); + + controllerWindow.SetSizeRequest((int)(controllerWindow.DefaultWidth * Program.WindowScaleFactor), (int)(controllerWindow.DefaultHeight * Program.WindowScaleFactor)); + controllerWindow.Show(); + } + + private void VolumeSlider_OnChange(object sender, EventArgs args) + { + ConfigurationState.Instance.System.AudioVolume.Value = (float)(_audioVolumeSlider.Value / 100); + } + + private void SaveToggle_Activated(object sender, EventArgs args) + { + SaveSettings(); + Dispose(); + } + + private void ApplyToggle_Activated(object sender, EventArgs args) + { + SaveSettings(); + } + + private void CloseToggle_Activated(object sender, EventArgs args) + { + ConfigurationState.Instance.System.AudioVolume.Value = _previousVolumeLevel; + Dispose(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade new file mode 100644 index 00000000..f0dbd6b6 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade @@ -0,0 +1,3221 @@ + + + + + + 3 + 1 + 10 + + + 101 + 1 + 5 + 1 + + + 1 + 31 + 1 + 5 + + + 23 + 1 + 5 + + + 59 + 1 + 5 + + + 1 + 12 + 1 + 5 + + + 2000 + 2060 + 1 + 10 + + + 0 + True + True + + + False + Ryujinx - Settings + True + center + 650 + 650 + + + True + False + vertical + + + True + True + in + + + True + False + + + True + True + + + True + False + 5 + 10 + 5 + vertical + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + General + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + Enable Discord Rich Presence + True + True + False + Choose whether or not to display Ryujinx on your "currently playing" Discord activity + start + True + + + False + True + 5 + 0 + + + + + Check for Updates on Launch + True + True + False + start + True + + + False + True + 5 + 1 + + + + + Show "Confirm Exit" Dialog + True + True + False + start + True + + + False + True + 5 + 2 + + + + + True + False + + + True + False + end + Hide Cursor: + + + False + True + 5 + 2 + + + + + Never + True + True + False + start + 5 + 5 + True + True + + + False + True + 3 + + + + + On Idle + True + True + False + start + 5 + 5 + True + _hideCursorNever + + + False + True + 4 + + + + + Always + True + True + False + start + 5 + 5 + True + _hideCursorNever + + + False + True + 5 + + + + + False + True + 5 + 4 + + + + + True + True + 1 + + + + + False + True + 5 + 1 + + + + + True + False + 5 + 5 + + + False + True + 5 + 2 + + + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + Game Directories + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + True + True + 10 + in + + + True + True + False + False + + + + + + + + + True + True + 0 + + + + + True + False + + + True + True + Enter a game directory to add to the list + + + True + True + 0 + + + + + Add + 80 + True + True + True + Add a game directory to the list + 5 + + + + False + True + 1 + + + + + Remove + 80 + True + True + True + Remove selected game directory + 5 + + + + False + True + 3 + + + + + False + True + 1 + + + + + True + True + 1 + + + + + True + True + 5 + 4 + + + + + True + False + 5 + 5 + + + False + True + 5 + 5 + + + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + Themes + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + Use Custom Theme + True + True + False + Enable or disable custom themes in the GUI + start + True + + + + False + True + 5 + 1 + + + + + True + False + + + True + False + Path to custom GUI theme + Custom Theme Path: + + + False + True + 5 + 0 + + + + + True + True + Path to custom GUI theme + center + + + True + True + 1 + + + + + Browse... + 80 + True + True + True + Browse for a custom GUI theme + 5 + + + + False + True + 2 + + + + + False + True + 10 + 2 + + + + + False + True + 1 + + + + + False + True + 5 + 6 + + + + + + + True + False + General + + + False + + + + + True + False + 5 + 10 + 5 + vertical + + + True + False + 5 + 5 + + + Enable Docked Mode + True + True + False + Docked mode makes the emulated system behave as a docked Nintendo Switch. This improves graphical fidelity in most games. Conversely, disabling this will make the emulated system behave as a handheld Nintendo Switch, reducing graphics quality. Configure player 1 controls if planning to use docked mode; configure handheld controls if planning to use handheld mode. Leave ON if unsure. + True + + + False + True + 10 + 0 + + + + + Direct Keyboard Access + True + True + False + Direct keyboard access (HID) support. Provides games access to your keyboard as a text entry device. + True + + + False + False + 10 + 1 + + + + + Direct Mouse Access + True + True + False + Direct mouse access (HID) support. Provides games access to your mouse as a pointing device. + True + + + False + False + 10 + 2 + + + + + False + True + 5 + 0 + + + + + True + False + + + False + True + 1 + + + + + + True + False + center + center + 20 + + + True + False + vertical + + + True + False + 20 + 20 + Player 1 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 0 + 0 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 3 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 4 + 0 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 2 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 2 + 0 + + + + + True + False + vertical + + + True + False + 20 + 20 + Handheld + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 4 + 4 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 6 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 4 + 2 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 5 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 2 + 2 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 7 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 0 + 4 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 4 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 0 + 2 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 8 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 2 + 4 + + + + + True + False + + + 1 + 0 + + + + + True + False + + + 3 + 0 + + + + + True + False + + + 3 + 2 + + + + + True + False + + + 3 + 4 + + + + + True + False + + + 1 + 2 + + + + + True + False + + + 1 + 4 + + + + + True + False + + + 1 + 1 + + + + + True + False + + + 1 + 3 + + + + + True + False + + + 3 + 1 + + + + + True + False + + + 3 + 3 + + + + + True + False + + + 0 + 1 + + + + + True + False + + + 2 + 1 + + + + + True + False + + + 4 + 1 + + + + + True + False + + + 0 + 3 + + + + + True + False + + + 2 + 3 + + + + + True + False + + + 4 + 3 + + + + + True + True + 2 + + + + + True + False + + + False + True + 3 + + + + + 1 + + + + + True + False + Input + + + 1 + False + + + + + True + False + 5 + 10 + 5 + vertical + + + True + False + start + 5 + 5 + vertical + + + True + False + start + 5 + Core + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + True + False + + + True + False + Change System Region + end + System Region: + + + False + True + 5 + 2 + + + + + True + False + Change System Region + 5 + + Japan + USA + Europe + Australia + China + Korea + Taiwan + + + + False + True + 3 + + + + + False + True + 5 + 0 + + + + + True + False + + + True + False + Change System Language + end + System Language: + + + False + True + 5 + 0 + + + + + True + False + Change System Language + + American English + British English + Canadian French + Chinese + Dutch + French + German + Italian + Japanese + Korean + Latin American Spanish + Portuguese + Russian + Simplified Chinese + Spanish + Taiwanese + Traditional Chinese + Brazilian Portuguese + + + + False + True + 1 + + + + + False + True + 5 + 1 + + + + + True + False + + + True + False + Change System TimeZone + end + System TimeZone: + + + False + True + 5 + 1 + + + + + True + True + Change System TimeZone + 5 + _systemTimeZoneCompletion + + + False + True + 2 + + + + + False + True + 5 + 2 + + + + + True + False + + + True + False + Change System Time + end + System Time: + + + False + True + 5 + 0 + + + + + True + True + 2000 + vertical + _systemTimeYearSpinAdjustment + True + 2000 + + + False + True + 1 + + + + + True + False + end + - + + + False + True + 5 + 2 + + + + + True + True + 1 + vertical + _systemTimeMonthSpinAdjustment + True + 1 + + + False + True + 3 + + + + + True + False + end + - + + + False + True + 5 + 4 + + + + + True + True + 1 + vertical + _systemTimeDaySpinAdjustment + True + 1 + + + False + True + 5 + + + + + True + True + 0 + vertical + _systemTimeHourSpinAdjustment + True + + + False + True + 6 + + + + + True + False + end + : + + + False + True + 5 + 7 + + + + + True + True + 0 + vertical + _systemTimeMinuteSpinAdjustment + True + + + False + True + 8 + + + + + False + True + 5 + 3 + + + + + Enable VSync + True + True + False + Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck. Can be toggled in-game with a hotkey of your preference. We recommend doing this if you plan on disabling it. Leave ON if unsure. + start + 5 + 5 + True + + + False + True + 4 + + + + + Enable PPTC (Profiled Persistent Translation Cache) + True + True + False + Saves translated JIT functions so that they do not need to be translated every time the game loads. Reduces stuttering and significantly speeds up boot times after the first boot of a game. Leave ON if unsure. + start + 5 + 5 + True + + + False + True + 6 + + + + + Enable Guest Internet Access + True + True + False + Allows the emulated application to connect to the Internet. Games with a LAN mode can connect to each other when this is enabled and the systems are connected to the same access point. This includes real consoles as well. Does NOT allow connecting to Nintendo servers. May cause crashing in certain games that try to connect to the Internet. Leave OFF if unsure. + start + 5 + 5 + True + + + False + True + 7 + + + + + Enable FS Integrity Checks + True + True + False + Checks for corrupt files when booting a game, and if corrupt files are detected, displays a hash error in the log. Has no impact on performance and is meant to help troubleshooting. Leave ON if unsure. + start + 5 + 5 + True + + + False + True + 8 + + + + + True + True + 1 + + + + + True + False + + + + + + True + False + Changes the backend used to render audio. SDL2 is the preferred one, while OpenAL and SoundIO are used as fallbacks. Dummy will have no sound. Set to SDL2 if unsure. + end + 5 + Audio Backend: + + + False + True + 5 + 2 + + + + + False + True + 5 + 2 + + + + + True + False + + + + + + True + False + Change how guest memory is mapped and accessed. Greatly affects emulated CPU performance. Set to HOST UNCHECKED if unsure. + end + 5 + Memory Manager Mode: + + + False + True + 5 + 2 + + + + + Software + True + True + False + Use a software page table for address translation. Highest accuracy but slowest performance. + start + 5 + 5 + True + + + False + True + 3 + + + + + Host (fast) + True + True + False + Directly map memory in the host address space. Much faster JIT compilation and execution. + start + 5 + 5 + True + _mmSoftware + + + False + True + 4 + + + + + Host Unchecked (fastest, unsafe) + True + True + False + Directly map memory, but do not mask the address within the guest address space before access. Faster, but at the cost of safety. The guest application can access memory from anywhere in Ryujinx, so only run programs you trust with this mode. + start + 5 + 5 + True + _mmSoftware + + + False + True + 5 + + + + + False + True + 5 + 3 + + + + + False + True + 5 + 0 + + + + + True + False + 5 + 5 + + + False + True + 5 + 1 + + + + + True + False + start + 5 + 5 + vertical + + + True + False + + + True + False + start + 5 + Hacks + + + + + + False + True + 0 + + + + + True + False + start + 5 + (may cause instability) + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 10 + 10 + vertical + + + Use alternative memory layout (Developers) + True + True + False + Utilizes an alternative MemoryMode layout to mimic a Switch development model. This is only useful for higher-resolution texture packs or 4k resolution mods. Does NOT improve performance. Leave OFF if unsure. + start + 5 + 5 + True + + + False + True + 0 + + + + + Ignore Missing Services + True + True + False + Ignores unimplemented Horizon OS services. This may help in bypassing crashes when booting certain games. Leave OFF if unsure. + start + 5 + 5 + True + + + False + True + 1 + + + + + True + True + 2 + + + + + False + True + 5 + 4 + + + + + 2 + + + + + True + False + end + System + + + 2 + False + + + + + True + False + 5 + vertical + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + 5 + 5 + Features + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + True + False + 5 + 5 + + + True + False + Executes graphics backend commands on a second thread. Speeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading. Set to AUTO if unsure. + Graphics Backend Multithreading: + + + False + True + 5 + 0 + + + + + True + False + Executes graphics backend commands on a second thread. Speeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading. Set to AUTO if unsure. + -1 + + Auto + Off + On + + + + False + True + 1 + + + + + False + True + 5 + 0 + + + + + True + False + 5 + 5 + + + True + False + Graphics Backend to use + Graphics Backend: + + + False + True + 5 + 0 + + + + + True + False + Graphics Backend to use + -1 + + Vulkan + OpenGL + + + + False + True + 1 + + + + + False + True + 5 + 1 + + + + + True + False + 5 + 5 + + + True + False + Preferred GPU (Vulkan only) + Preferred GPU: + + + False + True + 5 + 0 + + + + + True + False + Preferred GPU (Vulkan only) + -1 + + + False + True + 1 + + + + + False + True + 5 + 2 + + + + + False + True + 2 + + + + + False + True + 5 + 0 + + + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + 5 + 5 + Enhancements + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + Enable Shader Cache + True + True + False + Saves a disk shader cache which reduces stuttering in subsequent runs. Leave ON if unsure. + start + 5 + 5 + True + + + False + True + 0 + + + + + Enable Texture Recompression + True + True + False + Enables or disables Texture Recompression. Reduces VRAM usage at the cost of texture quality, and may also increase stuttering + start + 5 + 5 + True + + + False + True + 1 + + + + + Enable Macro HLE + True + True + False + Enables or disables high-level emulation of Macro code. Improves performance but may cause graphical glitches in some games + start + 5 + 5 + True + + + False + True + 2 + + + + + True + False + 5 + 5 + + + True + False + Resolution Scale applied to applicable render targets. + Resolution Scale: + + + False + True + 5 + 0 + + + + + True + False + Resolution Scale applied to applicable render targets. + 1 + + Native (720p/1080p) + 2x (1440p/2160p) + 3x (2160p/3240p) + 4x (2880p/4320p) + Custom (not recommended) + + + + False + True + 1 + + + + + True + True + Floating point resolution scale, such as 1.5. Non-integral scales are more likely to cause issues or crash. + center + False + 1.0 + number + + + True + True + 2 + + + + + False + True + 5 + 3 + + + + + True + False + 5 + 5 + + + True + False + Applies a final effect to the game render + Post Processing Effect: + + + False + True + 5 + 0 + + + + + True + False + Applies anti-aliasing to the game render + 1 + + None + FXAA + SMAA Low + SMAA Medium + SMAA High + SMAA Ultra + + + + False + True + 1 + + + + + False + True + 5 + 4 + + + + + 100 + True + False + 5 + 5 + + + True + False + Enables Framebuffer Upscaling + Upscale: + + + False + True + 5 + 0 + + + + + True + False + Enables Framebuffer Upscaling + 1 + + Bilinear + Nearest + FSR + + + + False + True + 1 + + + + + 200 + True + True + 5 + _scalingFilterLevel + 1 + right + + + False + True + 3 + + + + + False + True + 5 + 5 + + + + + True + False + 5 + 5 + + + True + False + Level of Anisotropic Filtering (set to Auto to use the value requested by the game) + Anisotropic Filtering: + + + False + True + 5 + 0 + + + + + True + False + Level of Anisotropic Filtering (set to Auto to use the value requested by the game) + -1 + + Auto + 2x + 4x + 8x + 16x + + + + False + True + 1 + + + + + False + True + 5 + 6 + + + + + True + False + 5 + 5 + + + True + False + Aspect Ratio applied to the renderer window. + Aspect Ratio: + + + False + True + 5 + 0 + + + + + True + False + Aspect Ratio applied to the renderer window. + 1 + + 4:3 + 16:9 + 16:10 + 21:9 + 32:9 + Stretch to Fit Window + + + + False + True + 1 + + + + + False + True + 5 + 7 + + + + + False + True + 2 + + + + + False + True + 5 + 2 + + + + + True + False + + + False + True + 5 + 3 + + + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + 5 + 5 + Developer Options + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + True + False + 5 + 5 + + + True + False + Graphics Shaders Dump Path + Graphics Shaders Dump Path: + + + False + True + 5 + 0 + + + + + True + True + Graphics Shaders Dump Path + center + False + + + True + True + 1 + + + + + False + True + 5 + 0 + + + + + False + True + 1 + + + + + False + True + 5 + 4 + + + + + 3 + + + + + True + False + Graphics + + + 3 + False + + + + + True + False + 5 + 10 + 5 + vertical + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + Logging + + + + + + False + True + 0 + + + + + True + False + start + 10 + 10 + vertical + + + Enable Logging to File + True + True + False + Saves console logging to a log file on disk. Does not affect performance. + start + 5 + 5 + True + + + False + True + 0 + + + + + Enable Stub Logs + True + True + False + Prints stub log messages in the console. Does not affect performance. + start + 5 + 5 + True + + + False + True + 3 + + + + + Enable Info Logs + True + True + False + Prints info log messages in the console. Does not affect performance. + start + 5 + 5 + True + + + False + True + 4 + + + + + Enable Warning Logs + True + True + False + Prints warning log messages in the console. Does not affect performance. + start + 5 + 5 + True + + + False + True + 5 + + + + + Enable Error Logs + True + True + False + Prints error log messages in the console. Does not affect performance. + start + 5 + 5 + True + + + False + True + 6 + + + + + Enable Guest Logs + True + True + False + Prints guest log messages in the console. Does not affect performance. + start + 5 + 5 + True + + + False + True + 7 + + + + + Enable Fs Access Logs + True + True + False + Enables FS access log output to the console. Possible modes are 0-3 + start + 5 + 5 + True + + + False + True + 8 + + + + + True + False + + + True + False + Enables FS access log output to the console. Possible modes are 0-3 + Fs Global Access Log Mode: + + + False + True + 5 + 0 + + + + + True + True + Enables FS access log output to the console. Possible modes are 0-3 + 0 + _fsLogSpinAdjustment + + + True + True + 1 + + + + + False + True + 5 + 9 + + + + + True + True + 1 + + + + + False + True + 5 + 0 + + + + + True + False + 5 + 5 + 10 + vertical + + + True + False + Use with care + start + 5 + Developer Options (WARNING: Will reduce performance) + + + + + + False + True + 0 + + + + + True + False + start + 10 + 10 + vertical + + + True + False + 5 + + + True + False + Requires appropriate log levels enabled. + Graphics Backend Log Level + + + False + True + 5 + 22 + + + + + True + False + Requires appropriate log levels enabled. + 5 + + + False + True + 22 + + + + + False + True + 1 + + + + + Enable Debug Logs + True + True + False + Prints debug log messages in the console. Only use this if specifically instructed by a staff member, as it will make logs difficult to read and worsen emulator performance. + start + 5 + 5 + True + + + False + True + 21 + + + + + Enable Trace Logs + True + True + False + Prints trace log messages in the console. Does not affect performance. + start + 5 + 5 + True + + + False + True + 22 + + + + + False + True + 1 + + + + + False + True + 5 + 22 + + + + + 4 + + + + + True + False + Logging + + + 4 + False + + + + + True + False + 5 + 10 + 5 + vertical + + + True + False + start + 5 + 5 + vertical + + + True + False + start + 5 + Multiplayer + + + + + + False + True + 0 + + + + + True + False + start + 10 + 10 + vertical + + + True + False + + + True + False + Change Multiplayer Mode + end + Mode: + + + False + True + 5 + 0 + + + + + True + False + Change Multiplayer Mode + Disabled + + Disabled + ldn_mitm + + + + False + True + 1 + + + + + False + True + 3 + + + + + True + True + 2 + + + + + False + True + 5 + 0 + + + + + True + False + start + 5 + 5 + vertical + + + True + False + start + 5 + LAN Mode + + + + + + False + True + 0 + + + + + True + False + start + 10 + 10 + vertical + + + True + False + + + True + False + The network interface used for LAN/LDN features + end + Network Interface: + + + False + True + 5 + 0 + + + + + True + False + The network interface used for LAN/LDN features + 0 + + Default + + + + False + True + 1 + + + + + False + True + 5 + 1 + + + + + True + False + start + 5 + To use LAN functionality in games, Enable Guest Internet Access must be checked in System. + True + + + False + True + 1 + + + + + True + True + 2 + + + + + False + True + 5 + 1 + + + + + 5 + + + + + True + False + Multiplayer + + + 5 + False + + + + + + + + + True + True + 0 + + + + + True + False + 5 + 3 + 3 + 5 + end + + + Save + True + True + True + + + + False + False + 0 + + + + + Close + True + True + True + + + + False + False + 1 + + + + + Apply + True + True + True + + + + True + True + 2 + + + + + False + False + 1 + + + + + + diff --git a/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs new file mode 100644 index 00000000..74b2330e --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs @@ -0,0 +1,206 @@ +using Gtk; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ns; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Widgets; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using GUI = Gtk.Builder.ObjectAttribute; +using SpanHelpers = LibHac.Common.SpanHelpers; + +namespace Ryujinx.UI.Windows +{ + public class TitleUpdateWindow : Window + { + private readonly MainWindow _parent; + private readonly VirtualFileSystem _virtualFileSystem; + private readonly string _titleId; + private readonly string _updateJsonPath; + + private TitleUpdateMetadata _titleUpdateWindowData; + + private readonly Dictionary _radioButtonToPathDictionary; + private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + +#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier + [GUI] Label _baseTitleInfoLabel; + [GUI] Box _availableUpdatesBox; + [GUI] RadioButton _noUpdateRadioButton; +#pragma warning restore CS0649, IDE0044 + + public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Gtk3.UI.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { } + + private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) + { + _parent = parent; + + builder.Autoconnect(this); + + _titleId = titleId; + _virtualFileSystem = virtualFileSystem; + _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json"); + _radioButtonToPathDictionary = new Dictionary(); + + try + { + _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_updateJsonPath, _serializerContext.TitleUpdateMetadata); + } + catch + { + _titleUpdateWindowData = new TitleUpdateMetadata + { + Selected = "", + Paths = new List(), + }; + } + + _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]"; + + foreach (string path in _titleUpdateWindowData.Paths) + { + AddUpdate(path); + } + + if (_titleUpdateWindowData.Selected == "") + { + _noUpdateRadioButton.Active = true; + } + else + { + foreach ((RadioButton update, var _) in _radioButtonToPathDictionary.Where(keyValuePair => keyValuePair.Value == _titleUpdateWindowData.Selected)) + { + update.Active = true; + } + } + } + + private void AddUpdate(string path) + { + if (File.Exists(path)) + { + using FileStream file = new(path, FileMode.Open, FileAccess.Read); + + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); + + try + { + (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); + + if (controlNca != null && patchNca != null) + { + ApplicationControlProperty controlData = new(); + + using var nacpFile = new UniqueRef(); + + controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); + + RadioButton radioButton = new($"Version {controlData.DisplayVersionString.ToString()} - {path}"); + radioButton.JoinGroup(_noUpdateRadioButton); + + _availableUpdatesBox.Add(radioButton); + _radioButtonToPathDictionary.Add(radioButton, path); + + radioButton.Show(); + radioButton.Active = true; + } + else + { + GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); + } + } + catch (Exception exception) + { + GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}"); + } + } + } + + private void RemoveUpdates(bool removeSelectedOnly = false) + { + foreach (RadioButton radioButton in _noUpdateRadioButton.Group) + { + if (radioButton.Label != "No Update" && (!removeSelectedOnly || radioButton.Active)) + { + _availableUpdatesBox.Remove(radioButton); + _radioButtonToPathDictionary.Remove(radioButton); + radioButton.Dispose(); + } + } + } + + private void AddButton_Clicked(object sender, EventArgs args) + { + using FileChooserNative fileChooser = new("Select update files", this, FileChooserAction.Open, "Add", "Cancel"); + + fileChooser.SelectMultiple = true; + + FileFilter filter = new() + { + Name = "Switch Game Updates", + }; + filter.AddPattern("*.nsp"); + + fileChooser.AddFilter(filter); + + if (fileChooser.Run() == (int)ResponseType.Accept) + { + foreach (string path in fileChooser.Filenames) + { + AddUpdate(path); + } + } + } + + private void RemoveButton_Clicked(object sender, EventArgs args) + { + RemoveUpdates(true); + } + + private void RemoveAllButton_Clicked(object sender, EventArgs args) + { + RemoveUpdates(); + } + + private void SaveButton_Clicked(object sender, EventArgs args) + { + _titleUpdateWindowData.Paths.Clear(); + _titleUpdateWindowData.Selected = ""; + + foreach (string paths in _radioButtonToPathDictionary.Values) + { + _titleUpdateWindowData.Paths.Add(paths); + } + + foreach (RadioButton radioButton in _noUpdateRadioButton.Group) + { + if (radioButton.Active) + { + _titleUpdateWindowData.Selected = _radioButtonToPathDictionary.TryGetValue(radioButton, out string updatePath) ? updatePath : ""; + } + } + + JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, _serializerContext.TitleUpdateMetadata); + + _parent.UpdateGameTable(); + + Dispose(); + } + + private void CancelButton_Clicked(object sender, EventArgs args) + { + Dispose(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.glade b/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.glade new file mode 100644 index 00000000..cfbac86d --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.glade @@ -0,0 +1,214 @@ + + + + + + False + Ryujinx - Title Update Manager + True + center + 550 + 250 + + + True + False + vertical + + + True + False + vertical + + + True + False + 10 + 10 + 10 + 10 + Available Updates + + + False + True + 0 + + + + + True + True + 10 + 10 + in + + + True + False + + + True + False + vertical + + + No Update + True + True + False + True + True + + + False + True + 0 + + + + + + + + + True + True + 1 + + + + + True + True + 0 + + + + + True + False + + + True + False + 10 + 10 + start + + + Add + True + True + True + Adds an update to this list + 10 + + + + True + True + 0 + + + + + Remove + True + True + True + Removes the selected update + 10 + + + + True + True + 1 + + + + + Remove All + True + True + True + Removes all the updates + 10 + + + + True + True + 2 + + + + + True + True + 0 + + + + + True + False + 10 + 10 + end + + + Save + True + True + True + 10 + 2 + 2 + + + + True + True + 0 + + + + + Cancel + True + True + True + 10 + 2 + 2 + + + + True + True + 1 + + + + + True + True + 1 + + + + + False + True + 1 + + + + + + + + + diff --git a/src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.Designer.cs b/src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.Designer.cs new file mode 100644 index 00000000..bc5a18f9 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.Designer.cs @@ -0,0 +1,255 @@ +using Gtk; +using Pango; +using System; + +namespace Ryujinx.UI.Windows +{ + public partial class UserProfilesManagerWindow : Window + { + private Box _mainBox; + private Label _selectedLabel; + private Box _selectedUserBox; + private Image _selectedUserImage; + private Box _selectedUserInfoBox; + private Entry _selectedUserNameEntry; + private Label _selectedUserIdLabel; + private Box _selectedUserButtonsBox; + private Button _saveProfileNameButton; + private Button _changeProfileImageButton; + private Box _usersTreeViewBox; + private Label _availableUsersLabel; + private ScrolledWindow _usersTreeViewWindow; + private ListStore _tableStore; + private TreeView _usersTreeView; + private Box _bottomBox; + private Button _addButton; + private Button _deleteButton; + private Button _closeButton; + + private void InitializeComponent() + { + // + // UserProfilesManagerWindow + // + CanFocus = false; + Resizable = false; + Modal = true; + WindowPosition = WindowPosition.Center; + DefaultWidth = 620; + DefaultHeight = 548; + TypeHint = Gdk.WindowTypeHint.Dialog; + + // + // _mainBox + // + _mainBox = new Box(Orientation.Vertical, 0); + + // + // _selectedLabel + // + _selectedLabel = new Label("Selected User Profile:") + { + Margin = 15, + Attributes = new AttrList(), + Halign = Align.Start, + }; + _selectedLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); + + // + // _viewBox + // + _usersTreeViewBox = new Box(Orientation.Vertical, 0); + + // + // _SelectedUserBox + // + _selectedUserBox = new Box(Orientation.Horizontal, 0) + { + MarginStart = 30, + }; + + // + // _selectedUserImage + // + _selectedUserImage = new Image(); + + // + // _selectedUserInfoBox + // + _selectedUserInfoBox = new Box(Orientation.Vertical, 0) + { + Homogeneous = true, + }; + + // + // _selectedUserNameEntry + // + _selectedUserNameEntry = new Entry("") + { + MarginStart = 15, + MaxLength = (int)MaxProfileNameLength, + }; + _selectedUserNameEntry.KeyReleaseEvent += SelectedUserNameEntry_KeyReleaseEvent; + + // + // _selectedUserIdLabel + // + _selectedUserIdLabel = new Label("") + { + MarginTop = 15, + MarginStart = 15, + }; + + // + // _selectedUserButtonsBox + // + _selectedUserButtonsBox = new Box(Orientation.Vertical, 0) + { + MarginEnd = 30, + }; + + // + // _saveProfileNameButton + // + _saveProfileNameButton = new Button() + { + Label = "Save Profile Name", + CanFocus = true, + ReceivesDefault = true, + Sensitive = false, + }; + _saveProfileNameButton.Clicked += EditProfileNameButton_Pressed; + + // + // _changeProfileImageButton + // + _changeProfileImageButton = new Button() + { + Label = "Change Profile Image", + CanFocus = true, + ReceivesDefault = true, + MarginTop = 10, + }; + _changeProfileImageButton.Clicked += ChangeProfileImageButton_Pressed; + + // + // _availableUsersLabel + // + _availableUsersLabel = new Label("Available User Profiles:") + { + Margin = 15, + Attributes = new AttrList(), + Halign = Align.Start, + }; + _availableUsersLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); + + // + // _usersTreeViewWindow + // + _usersTreeViewWindow = new ScrolledWindow() + { + ShadowType = ShadowType.In, + CanFocus = true, + Expand = true, + MarginStart = 30, + MarginEnd = 30, + MarginBottom = 15, + }; + + // + // _tableStore + // + _tableStore = new ListStore(typeof(bool), typeof(Gdk.Pixbuf), typeof(string), typeof(Gdk.RGBA)); + + // + // _usersTreeView + // + _usersTreeView = new TreeView(_tableStore) + { + HoverSelection = true, + HeadersVisible = false, + }; + _usersTreeView.RowActivated += UsersTreeView_Activated; + + // + // _bottomBox + // + _bottomBox = new Box(Orientation.Horizontal, 0) + { + MarginStart = 30, + MarginEnd = 30, + MarginBottom = 15, + }; + + // + // _addButton + // + _addButton = new Button() + { + Label = "Add New Profile", + CanFocus = true, + ReceivesDefault = true, + HeightRequest = 35, + }; + _addButton.Clicked += AddButton_Pressed; + + // + // _deleteButton + // + _deleteButton = new Button() + { + Label = "Delete Selected Profile", + CanFocus = true, + ReceivesDefault = true, + HeightRequest = 35, + MarginStart = 10, + }; + _deleteButton.Clicked += DeleteButton_Pressed; + + // + // _closeButton + // + _closeButton = new Button() + { + Label = "Close", + CanFocus = true, + ReceivesDefault = true, + HeightRequest = 35, + WidthRequest = 80, + }; + _closeButton.Clicked += CloseButton_Pressed; + + ShowComponent(); + } + + private void ShowComponent() + { + _usersTreeViewWindow.Add(_usersTreeView); + + _usersTreeViewBox.Add(_usersTreeViewWindow); + _bottomBox.PackStart(_addButton, false, false, 0); + _bottomBox.PackStart(_deleteButton, false, false, 0); + _bottomBox.PackEnd(_closeButton, false, false, 0); + + _selectedUserInfoBox.Add(_selectedUserNameEntry); + _selectedUserInfoBox.Add(_selectedUserIdLabel); + + _selectedUserButtonsBox.Add(_saveProfileNameButton); + _selectedUserButtonsBox.Add(_changeProfileImageButton); + + _selectedUserBox.Add(_selectedUserImage); + _selectedUserBox.PackStart(_selectedUserInfoBox, false, false, 0); + _selectedUserBox.PackEnd(_selectedUserButtonsBox, false, false, 0); + + _mainBox.PackStart(_selectedLabel, false, false, 0); + _mainBox.PackStart(_selectedUserBox, false, true, 0); + _mainBox.PackStart(_availableUsersLabel, false, false, 0); + _mainBox.Add(_usersTreeViewBox); + _mainBox.Add(_bottomBox); + + Add(_mainBox); + + ShowAll(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.cs new file mode 100644 index 00000000..d1e5fa9f --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.cs @@ -0,0 +1,328 @@ +using Gtk; +using Ryujinx.Common.Memory; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Widgets; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Image = SixLabors.ImageSharp.Image; + +namespace Ryujinx.UI.Windows +{ + public partial class UserProfilesManagerWindow : Window + { + private const uint MaxProfileNameLength = 0x20; + + private readonly AccountManager _accountManager; + private readonly ContentManager _contentManager; + + private byte[] _bufferImageProfile; + private string _tempNewProfileName; + + private Gdk.RGBA _selectedColor; + + private readonly ManualResetEvent _avatarsPreloadingEvent = new(false); + + public UserProfilesManagerWindow(AccountManager accountManager, ContentManager contentManager, VirtualFileSystem virtualFileSystem) : base($"Ryujinx {Program.Version} - Manage User Profiles") + { + Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png"); + + InitializeComponent(); + + _selectedColor.Red = 0.212; + _selectedColor.Green = 0.843; + _selectedColor.Blue = 0.718; + _selectedColor.Alpha = 1; + + _accountManager = accountManager; + _contentManager = contentManager; + + CellRendererToggle userSelectedToggle = new(); + userSelectedToggle.Toggled += UserSelectedToggle_Toggled; + + // NOTE: Uncomment following line when multiple selection of user profiles is supported. + //_usersTreeView.AppendColumn("Selected", userSelectedToggle, "active", 0); + _usersTreeView.AppendColumn("User Icon", new CellRendererPixbuf(), "pixbuf", 1); + _usersTreeView.AppendColumn("User Info", new CellRendererText(), "text", 2, "background-rgba", 3); + + _tableStore.SetSortColumnId(0, SortType.Descending); + + RefreshList(); + + if (_contentManager.GetCurrentFirmwareVersion() != null) + { + Task.Run(() => + { + AvatarWindow.PreloadAvatars(contentManager, virtualFileSystem); + _avatarsPreloadingEvent.Set(); + }); + } + } + + public void RefreshList() + { + _tableStore.Clear(); + + foreach (UserProfile userProfile in _accountManager.GetAllUsers()) + { + _tableStore.AppendValues(userProfile.AccountState == AccountState.Open, new Gdk.Pixbuf(userProfile.Image, 96, 96), $"{userProfile.Name}\n{userProfile.UserId}", Gdk.RGBA.Zero); + + if (userProfile.AccountState == AccountState.Open) + { + _selectedUserImage.Pixbuf = new Gdk.Pixbuf(userProfile.Image, 96, 96); + _selectedUserIdLabel.Text = userProfile.UserId.ToString(); + _selectedUserNameEntry.Text = userProfile.Name; + + _deleteButton.Sensitive = userProfile.UserId != AccountManager.DefaultUserId; + + _usersTreeView.Model.GetIterFirst(out TreeIter firstIter); + _tableStore.SetValue(firstIter, 3, _selectedColor); + } + } + } + + // + // Events + // + + private void UsersTreeView_Activated(object o, RowActivatedArgs args) + { + SelectUserTreeView(); + } + + private void UserSelectedToggle_Toggled(object o, ToggledArgs args) + { + SelectUserTreeView(); + } + + private void SelectUserTreeView() + { + // Get selected item informations. + _usersTreeView.Selection.GetSelected(out TreeIter selectedIter); + + Gdk.Pixbuf userPicture = (Gdk.Pixbuf)_tableStore.GetValue(selectedIter, 1); + + string userName = _tableStore.GetValue(selectedIter, 2).ToString().Split("\n")[0]; + string userId = _tableStore.GetValue(selectedIter, 2).ToString().Split("\n")[1]; + + // Unselect the first user. + _usersTreeView.Model.GetIterFirst(out TreeIter firstIter); + _tableStore.SetValue(firstIter, 0, false); + _tableStore.SetValue(firstIter, 3, Gdk.RGBA.Zero); + + // Set new informations. + _tableStore.SetValue(selectedIter, 0, true); + + _selectedUserImage.Pixbuf = userPicture; + _selectedUserNameEntry.Text = userName; + _selectedUserIdLabel.Text = userId; + _saveProfileNameButton.Sensitive = false; + + // Open the selected one. + _accountManager.OpenUser(new UserId(userId)); + + _deleteButton.Sensitive = userId != AccountManager.DefaultUserId.ToString(); + + _tableStore.SetValue(selectedIter, 3, _selectedColor); + } + + private void SelectedUserNameEntry_KeyReleaseEvent(object o, KeyReleaseEventArgs args) + { + if (_saveProfileNameButton.Sensitive == false) + { + _saveProfileNameButton.Sensitive = true; + } + } + + private void AddButton_Pressed(object sender, EventArgs e) + { + _tempNewProfileName = GtkDialog.CreateInputDialog(this, "Choose the Profile Name", "Please Enter a Profile Name", MaxProfileNameLength); + + if (_tempNewProfileName != "") + { + SelectProfileImage(true); + + if (_bufferImageProfile != null) + { + AddUser(); + } + } + } + + private void DeleteButton_Pressed(object sender, EventArgs e) + { + if (GtkDialog.CreateChoiceDialog("Delete User Profile", "Are you sure you want to delete the profile ?", "Deleting this profile will also delete all associated save data.")) + { + _accountManager.DeleteUser(GetSelectedUserId()); + + RefreshList(); + } + } + + private void EditProfileNameButton_Pressed(object sender, EventArgs e) + { + _saveProfileNameButton.Sensitive = false; + + _accountManager.SetUserName(GetSelectedUserId(), _selectedUserNameEntry.Text); + + RefreshList(); + } + + private void ProcessProfileImage(byte[] buffer) + { + using Image image = Image.Load(buffer); + + image.Mutate(x => x.Resize(256, 256)); + + using MemoryStream streamJpg = MemoryStreamManager.Shared.GetStream(); + + image.SaveAsJpeg(streamJpg); + + _bufferImageProfile = streamJpg.ToArray(); + } + + private void ProfileImageFileChooser() + { + FileChooserNative fileChooser = new("Import Custom Profile Image", this, FileChooserAction.Open, "Import", "Cancel") + { + SelectMultiple = false, + }; + + FileFilter filter = new() + { + Name = "Custom Profile Images", + }; + filter.AddPattern("*.jpg"); + filter.AddPattern("*.jpeg"); + filter.AddPattern("*.png"); + filter.AddPattern("*.bmp"); + + fileChooser.AddFilter(filter); + + if (fileChooser.Run() == (int)ResponseType.Accept) + { + ProcessProfileImage(File.ReadAllBytes(fileChooser.Filename)); + } + + fileChooser.Dispose(); + } + + private void SelectProfileImage(bool newUser = false) + { + if (_contentManager.GetCurrentFirmwareVersion() == null) + { + ProfileImageFileChooser(); + } + else + { + Dictionary buttons = new() + { + { 0, "Import Image File" }, + { 1, "Select Firmware Avatar" }, + }; + + ResponseType responseDialog = GtkDialog.CreateCustomDialog("Profile Image Selection", + "Choose a Profile Image", + "You may import a custom profile image, or select an avatar from the system firmware.", + buttons, MessageType.Question); + + if (responseDialog == 0) + { + ProfileImageFileChooser(); + } + else if (responseDialog == (ResponseType)1) + { + AvatarWindow avatarWindow = new() + { + NewUser = newUser, + }; + + avatarWindow.DeleteEvent += AvatarWindow_DeleteEvent; + + avatarWindow.SetSizeRequest((int)(avatarWindow.DefaultWidth * Program.WindowScaleFactor), (int)(avatarWindow.DefaultHeight * Program.WindowScaleFactor)); + avatarWindow.Show(); + } + } + } + + private void ChangeProfileImageButton_Pressed(object sender, EventArgs e) + { + if (_contentManager.GetCurrentFirmwareVersion() != null) + { + _avatarsPreloadingEvent.WaitOne(); + } + + SelectProfileImage(); + + if (_bufferImageProfile != null) + { + SetUserImage(); + } + } + + private void AvatarWindow_DeleteEvent(object sender, DeleteEventArgs args) + { + _bufferImageProfile = ((AvatarWindow)sender).SelectedProfileImage; + + if (_bufferImageProfile != null) + { + if (((AvatarWindow)sender).NewUser) + { + AddUser(); + } + else + { + SetUserImage(); + } + } + } + + private void AddUser() + { + _accountManager.AddUser(_tempNewProfileName, _bufferImageProfile); + + _bufferImageProfile = null; + _tempNewProfileName = ""; + + RefreshList(); + } + + private void SetUserImage() + { + _accountManager.SetUserImage(GetSelectedUserId(), _bufferImageProfile); + + _bufferImageProfile = null; + + RefreshList(); + } + + private UserId GetSelectedUserId() + { + if (_usersTreeView.Model.GetIterFirst(out TreeIter iter)) + { + do + { + if ((bool)_tableStore.GetValue(iter, 0)) + { + break; + } + } + while (_usersTreeView.Model.IterNext(ref iter)); + } + + return new UserId(_tableStore.GetValue(iter, 2).ToString().Split("\n")[1]); + } + + private void CloseButton_Pressed(object sender, EventArgs e) + { + Close(); + } + } +} -- cgit v1.2.3