From 4aae82bad15187005add218f000f4ea1d03a123f Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 15:34:14 -0600 Subject: [PATCH 01/20] misc: Small cleanups --- src/ARMeilleure/Translation/Dominance.cs | 2 +- src/ARMeilleure/Translation/PTC/Ptc.cs | 20 +++++++++---------- .../Logging/Targets/AsyncLogTargetWrapper.cs | 4 ++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/ARMeilleure/Translation/Dominance.cs b/src/ARMeilleure/Translation/Dominance.cs index e2185bd85..b62714fdf 100644 --- a/src/ARMeilleure/Translation/Dominance.cs +++ b/src/ARMeilleure/Translation/Dominance.cs @@ -77,7 +77,7 @@ namespace ARMeilleure.Translation { continue; } - + for (int pBlkIndex = 0; pBlkIndex < block.Predecessors.Count; pBlkIndex++) { BasicBlock current = block.Predecessors[pBlkIndex]; diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs index 6dd38ed89..8236150fe 100644 --- a/src/ARMeilleure/Translation/PTC/Ptc.cs +++ b/src/ARMeilleure/Translation/PTC/Ptc.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; +using System.Linq; using System.Runtime; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -848,18 +849,15 @@ namespace ARMeilleure.Translation.PTC } } - List threads = new(); - for (int i = 0; i < degreeOfParallelism; i++) - { - Thread thread = new(TranslateFuncs) - { - IsBackground = true, - Name = "Ptc.TranslateThread." + i - }; - - threads.Add(thread); - } + List threads = Enumerable.Range(0, degreeOfParallelism) + .Select(idx => + new Thread(TranslateFuncs) + { + IsBackground = true, + Name = "Ptc.TranslateThread." + idx + } + ).ToList(); Stopwatch sw = Stopwatch.StartNew(); diff --git a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs index 02c6dc97b..a9dbe646a 100644 --- a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs +++ b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs @@ -30,10 +30,10 @@ namespace Ryujinx.Common.Logging.Targets string ILogTarget.Name { get => _target.Name; } public AsyncLogTargetWrapper(ILogTarget target) - : this(target, -1, AsyncLogTargetOverflowAction.Block) + : this(target, -1) { } - public AsyncLogTargetWrapper(ILogTarget target, int queueLimit, AsyncLogTargetOverflowAction overflowAction) + public AsyncLogTargetWrapper(ILogTarget target, int queueLimit = -1, AsyncLogTargetOverflowAction overflowAction = AsyncLogTargetOverflowAction.Block) { _target = target; _messageQueue = new BlockingCollection(queueLimit); From 9c82d98ec4ec4ab17c056dc2ec54ccff07c2afdb Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 15:48:07 -0600 Subject: [PATCH 02/20] headless: Add Ignore Controller Applet as a configurable option --- src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs | 5 +++-- src/Ryujinx.Headless.SDL2/Options.cs | 3 +++ src/Ryujinx.Headless.SDL2/Program.cs | 7 +++---- src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs | 5 +++-- src/Ryujinx.Headless.SDL2/WindowBase.cs | 7 ++++++- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs index ce52f84d9..8c4854a11 100644 --- a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs +++ b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs @@ -117,8 +117,9 @@ namespace Ryujinx.Headless.SDL2.OpenGL GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse, - HideCursorMode hideCursorMode) - : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode) + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) + : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { _glLogLevel = glLogLevel; } diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs index 2f86f3ebf..8078ca5e4 100644 --- a/src/Ryujinx.Headless.SDL2/Options.cs +++ b/src/Ryujinx.Headless.SDL2/Options.cs @@ -225,6 +225,9 @@ namespace Ryujinx.Headless.SDL2 [Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")] public bool IgnoreMissingServices { get; set; } + + [Option("ignore-controller-applet", Required = false, Default = false, HelpText = "Enable ignoring the controller applet when your game loses connection to your controller.")] + public bool IgnoreControllerApplet { get; set; } // Values diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index b6ccb2ac4..78cdb7718 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -444,8 +444,7 @@ namespace Ryujinx.Headless.SDL2 { Logger.AddTarget(new AsyncLogTargetWrapper( new FileLogTarget("file", logFile), - 1000, - AsyncLogTargetOverflowAction.Block + 1000 )); } else @@ -506,8 +505,8 @@ namespace Ryujinx.Headless.SDL2 private static WindowBase CreateWindow(Options options) { return options.GraphicsBackend == GraphicsBackend.Vulkan - ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode) - : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode); + ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet) + : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet); } private static IRenderer CreateRenderer(Options options, WindowBase window) diff --git a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs index fb73ca335..b88e0fe83 100644 --- a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs +++ b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs @@ -17,8 +17,9 @@ namespace Ryujinx.Headless.SDL2.Vulkan GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse, - HideCursorMode hideCursorMode) - : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode) + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) + : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { _glLogLevel = glLogLevel; } diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs index bd47dfd5d..6d681e100 100644 --- a/src/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs @@ -86,13 +86,15 @@ namespace Ryujinx.Headless.SDL2 private readonly AspectRatio _aspectRatio; private readonly bool _enableMouse; + private readonly bool _ignoreControllerApplet; public WindowBase( InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse, - HideCursorMode hideCursorMode) + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) { MouseDriver = new SDL2MouseDriver(hideCursorMode); _inputManager = inputManager; @@ -108,6 +110,7 @@ namespace Ryujinx.Headless.SDL2 _gpuDoneEvent = new ManualResetEvent(false); _aspectRatio = aspectRatio; _enableMouse = enableMouse; + _ignoreControllerApplet = ignoreControllerApplet; HostUITheme = new HeadlessHostUiTheme(); SDL2Driver.Instance.Initialize(); @@ -484,6 +487,8 @@ namespace Ryujinx.Headless.SDL2 public bool DisplayMessageDialog(ControllerAppletUIArgs args) { + if (_ignoreControllerApplet) return false; + string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}"; string message = $"Application requests {playerCount} {"player".ToQuantity(args.PlayerCountMin + args.PlayerCountMax, ShowQuantityAs.None)} with:\n\n" From e26625dfd5bacfed91871ca759d5f7e673072aa1 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 16:17:36 -0600 Subject: [PATCH 03/20] UI: Disable XCI trimmer button when in-game --- src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 03b7cfbe4..0a41b3458 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -269,7 +269,7 @@ - + From e01a30016e600d2b41e209a2c8809713719b9c21 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 17:01:47 -0600 Subject: [PATCH 04/20] RPC: Add Mario & Luigi Brothership image. --- src/Ryujinx.UI.Common/DiscordIntegrationModule.cs | 1 + src/Ryujinx/Program.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs index 7bfb1c95b..c21a46e29 100644 --- a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs +++ b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs @@ -163,6 +163,7 @@ namespace Ryujinx.UI.Common "010036b0034e4000", // Super Mario Party "01006fe013472000", // Mario Party Superstars "0100965017338000", // Super Mario Party Jamboree + "01006d0017f7a000", // Mario & Luigi: Brothership "010067300059a000", // Mario + Rabbids: Kingdom Battle "0100317013770000", // Mario + Rabbids: Sparks of Hope "0100a3900c3e2000", // Paper Mario: The Origami King diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 31db628d3..cdce023cb 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -101,7 +101,7 @@ namespace Ryujinx.Ava // Delete backup files after updating. Task.Run(Updater.CleanupUpdate); - Console.Title = $"Ryujinx Console {Version}"; + Console.Title = $"{App.FullAppName} Console {Version}"; // Hook unhandled exception and process exit events. AppDomain.CurrentDomain.UnhandledException += (sender, e) From 10c8d73b60c8390bdb9c8b43ecde3fa183ba8b33 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 19:10:02 -0600 Subject: [PATCH 05/20] UI: Ryujinx Canary title in NCA extractor --- src/Ryujinx/Assets/Locales/ar_SA.json | 2 +- src/Ryujinx/Assets/Locales/de_DE.json | 2 +- src/Ryujinx/Assets/Locales/el_GR.json | 2 +- src/Ryujinx/Assets/Locales/en_US.json | 4 ++-- src/Ryujinx/Assets/Locales/es_ES.json | 2 +- src/Ryujinx/Assets/Locales/fr_FR.json | 2 +- src/Ryujinx/Assets/Locales/he_IL.json | 2 +- src/Ryujinx/Assets/Locales/it_IT.json | 2 +- src/Ryujinx/Assets/Locales/ja_JP.json | 2 +- src/Ryujinx/Assets/Locales/ko_KR.json | 2 +- src/Ryujinx/Assets/Locales/pl_PL.json | 2 +- src/Ryujinx/Assets/Locales/pt_BR.json | 2 +- src/Ryujinx/Assets/Locales/ru_RU.json | 2 +- src/Ryujinx/Assets/Locales/th_TH.json | 2 +- src/Ryujinx/Assets/Locales/tr_TR.json | 2 +- src/Ryujinx/Assets/Locales/uk_UA.json | 2 +- src/Ryujinx/Assets/Locales/zh_CN.json | 2 +- src/Ryujinx/Assets/Locales/zh_TW.json | 2 +- src/Ryujinx/Common/ApplicationHelper.cs | 4 ++-- src/Ryujinx/Updater.cs | 2 +- 20 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Ryujinx/Assets/Locales/ar_SA.json b/src/Ryujinx/Assets/Locales/ar_SA.json index f2720b669..129558395 100644 --- a/src/Ryujinx/Assets/Locales/ar_SA.json +++ b/src/Ryujinx/Assets/Locales/ar_SA.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "حدث خطأ أثناء البحث عن بيانات الحفظ المحددة: {0}", "FolderDialogExtractTitle": "اختر المجلد الذي تريد الاستخراج إليه", "DialogNcaExtractionMessage": "استخراج قسم {0} من {1}...", - "DialogNcaExtractionTitle": "ريوجينكس - مستخرج قسم NCA", + "DialogNcaExtractionTitle": "مستخرج قسم NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "فشل الاستخراج. لم يكن NCA الرئيسي موجودا في الملف المحدد.", "DialogNcaExtractionCheckLogErrorMessage": "فشل الاستخراج. اقرأ ملف التسجيل لمزيد من المعلومات.", "DialogNcaExtractionSuccessMessage": "تم الاستخراج بنجاح.", diff --git a/src/Ryujinx/Assets/Locales/de_DE.json b/src/Ryujinx/Assets/Locales/de_DE.json index 4004ec325..7b25336c3 100644 --- a/src/Ryujinx/Assets/Locales/de_DE.json +++ b/src/Ryujinx/Assets/Locales/de_DE.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "Es ist ein Fehler beim Suchen der angegebenen Speicherdaten aufgetreten: {0}", "FolderDialogExtractTitle": "Wähle den Ordner, in welchen die Dateien entpackt werden sollen", "DialogNcaExtractionMessage": "Extrahiert {0} abschnitt von {1}...", - "DialogNcaExtractionTitle": "Ryujinx - NCA-Abschnitt-Extraktor", + "DialogNcaExtractionTitle": "NCA-Abschnitt-Extraktor", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Extraktion fehlgeschlagen. Der Hauptheader der NCA war in der ausgewählten Datei nicht vorhanden.", "DialogNcaExtractionCheckLogErrorMessage": "Extraktion fehlgeschlagen. Überprüfe die Logs für weitere Informationen.", "DialogNcaExtractionSuccessMessage": "Extraktion erfolgreich abgeschlossen.", diff --git a/src/Ryujinx/Assets/Locales/el_GR.json b/src/Ryujinx/Assets/Locales/el_GR.json index 56940ffc9..95c2062b0 100644 --- a/src/Ryujinx/Assets/Locales/el_GR.json +++ b/src/Ryujinx/Assets/Locales/el_GR.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "Σφάλμα κατά την εύρεση των αποθηκευμένων δεδομένων: {0}", "FolderDialogExtractTitle": "Επιλέξτε τον φάκελο στον οποίο θέλετε να εξαγάγετε", "DialogNcaExtractionMessage": "Εξαγωγή ενότητας {0} από {1}...", - "DialogNcaExtractionTitle": "Ryujinx - NCA Εξαγωγέας Τμημάτων", + "DialogNcaExtractionTitle": "NCA Εξαγωγέας Τμημάτων", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Αποτυχία εξαγωγής. Η κύρια NCA δεν υπήρχε στο επιλεγμένο αρχείο.", "DialogNcaExtractionCheckLogErrorMessage": "Αποτυχία εξαγωγής. Διαβάστε το αρχείο καταγραφής για περισσότερες πληροφορίες.", "DialogNcaExtractionSuccessMessage": "Η εξαγωγή ολοκληρώθηκε με επιτυχία.", diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 30c84ab14..88eb43ce4 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -444,7 +444,7 @@ "DialogMessageFindSaveErrorMessage": "There was an error finding the specified savedata: {0}", "FolderDialogExtractTitle": "Choose the folder to extract into", "DialogNcaExtractionMessage": "Extracting {0} section from {1}...", - "DialogNcaExtractionTitle": "Ryujinx - NCA Section Extractor", + "DialogNcaExtractionTitle": "NCA Section Extractor", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Extraction failure. The main NCA was not present in the selected file.", "DialogNcaExtractionCheckLogErrorMessage": "Extraction failed. Please check the log file for more details.", "DialogNcaExtractionSuccessMessage": "Extraction completed successfully.", @@ -461,7 +461,7 @@ "DialogUpdaterRestartMessage": "Do you want to restart Ryujinx now?", "DialogUpdaterNoInternetMessage": "You are not connected to the Internet!", "DialogUpdaterNoInternetSubMessage": "Please verify that you have a working Internet connection!", - "DialogUpdaterDirtyBuildMessage": "You Cannot update a Dirty build of Ryujinx!", + "DialogUpdaterDirtyBuildMessage": "You cannot update a Dirty build of Ryujinx!", "DialogUpdaterDirtyBuildSubMessage": "Please download Ryujinx at https://github.com/GreemDev/Ryujinx/releases/ if you are looking for a supported version.", "DialogRestartRequiredMessage": "Restart Required", "DialogThemeRestartMessage": "Theme has been saved. A restart is needed to apply the theme.", diff --git a/src/Ryujinx/Assets/Locales/es_ES.json b/src/Ryujinx/Assets/Locales/es_ES.json index bbc86ed0c..183addade 100644 --- a/src/Ryujinx/Assets/Locales/es_ES.json +++ b/src/Ryujinx/Assets/Locales/es_ES.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "Hubo un error encontrando los datos de guardado especificados: {0}", "FolderDialogExtractTitle": "Elige la carpeta en la que deseas extraer", "DialogNcaExtractionMessage": "Extrayendo {0} sección de {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Extractor de sección NCA", + "DialogNcaExtractionTitle": "Extractor de sección NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Fallo de extracción. El NCA principal no estaba presente en el archivo seleccionado.", "DialogNcaExtractionCheckLogErrorMessage": "Fallo de extracción. Lee el registro para más información.", "DialogNcaExtractionSuccessMessage": "Se completó la extracción con éxito.", diff --git a/src/Ryujinx/Assets/Locales/fr_FR.json b/src/Ryujinx/Assets/Locales/fr_FR.json index 0c673f719..34d53e16b 100644 --- a/src/Ryujinx/Assets/Locales/fr_FR.json +++ b/src/Ryujinx/Assets/Locales/fr_FR.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "Une erreur s'est produite lors de la recherche de la sauvegarde spécifiée : {0}", "FolderDialogExtractTitle": "Choisissez le dossier dans lequel extraire", "DialogNcaExtractionMessage": "Extraction de la section {0} depuis {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Extracteur de la section NCA", + "DialogNcaExtractionTitle": "Extracteur de la section NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Échec de l'extraction. Le NCA principal n'était pas présent dans le fichier sélectionné.", "DialogNcaExtractionCheckLogErrorMessage": "Échec de l'extraction. Lisez le fichier journal pour plus d'informations.", "DialogNcaExtractionSuccessMessage": "Extraction terminée avec succès.", diff --git a/src/Ryujinx/Assets/Locales/he_IL.json b/src/Ryujinx/Assets/Locales/he_IL.json index cac3fbf53..6bd8f0470 100644 --- a/src/Ryujinx/Assets/Locales/he_IL.json +++ b/src/Ryujinx/Assets/Locales/he_IL.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "אירעה שגיאה במציאת שמור המשחק שצויין: {0}", "FolderDialogExtractTitle": "בחרו את התיקייה לחילוץ", "DialogNcaExtractionMessage": "מלחץ {0} ממקטע {1}...", - "DialogNcaExtractionTitle": "ריוג'ינקס - מחלץ מקטע NCA", + "DialogNcaExtractionTitle": "מחלץ מקטע NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "כשל בחילוץ. ה-NCA הראשי לא היה קיים בקובץ שנבחר.", "DialogNcaExtractionCheckLogErrorMessage": "כשל בחילוץ. קרא את קובץ הרישום למידע נוסף.", "DialogNcaExtractionSuccessMessage": "החילוץ הושלם בהצלחה.", diff --git a/src/Ryujinx/Assets/Locales/it_IT.json b/src/Ryujinx/Assets/Locales/it_IT.json index 419a5dd2b..0d0edb42c 100644 --- a/src/Ryujinx/Assets/Locales/it_IT.json +++ b/src/Ryujinx/Assets/Locales/it_IT.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "C'è stato un errore durante la ricerca dei dati di salvataggio: {0}", "FolderDialogExtractTitle": "Scegli una cartella in cui estrarre", "DialogNcaExtractionMessage": "Estrazione della sezione {0} da {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Estrazione sezione NCA", + "DialogNcaExtractionTitle": "Estrazione sezione NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "L'estrazione è fallita. L'NCA principale non era presente nel file selezionato.", "DialogNcaExtractionCheckLogErrorMessage": "L'estrazione è fallita. Consulta il file di log per maggiori informazioni.", "DialogNcaExtractionSuccessMessage": "Estrazione completata con successo.", diff --git a/src/Ryujinx/Assets/Locales/ja_JP.json b/src/Ryujinx/Assets/Locales/ja_JP.json index b2c0a506e..3eade3e6e 100644 --- a/src/Ryujinx/Assets/Locales/ja_JP.json +++ b/src/Ryujinx/Assets/Locales/ja_JP.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "セーブデータ: {0} の検索中にエラーが発生しました", "FolderDialogExtractTitle": "展開フォルダを選択", "DialogNcaExtractionMessage": "{1} から {0} セクションを展開中...", - "DialogNcaExtractionTitle": "Ryujinx - NCA セクション展開", + "DialogNcaExtractionTitle": "NCA セクション展開", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "展開に失敗しました. 選択されたファイルにはメイン NCA が存在しません.", "DialogNcaExtractionCheckLogErrorMessage": "展開に失敗しました. 詳細はログを確認してください.", "DialogNcaExtractionSuccessMessage": "展開が正常終了しました", diff --git a/src/Ryujinx/Assets/Locales/ko_KR.json b/src/Ryujinx/Assets/Locales/ko_KR.json index bce762d19..bd5bed962 100644 --- a/src/Ryujinx/Assets/Locales/ko_KR.json +++ b/src/Ryujinx/Assets/Locales/ko_KR.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "지정된 저장 데이터를 찾는 중에 오류 발생: {0}", "FolderDialogExtractTitle": "추출할 폴더 선택", "DialogNcaExtractionMessage": "{1}에서 {0} 섹션을 추출하는 중...", - "DialogNcaExtractionTitle": "Ryujinx - NCA 섹션 추출기", + "DialogNcaExtractionTitle": "NCA 섹션 추출기", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "추출 실패하였습니다. 선택한 파일에 기본 NCA가 없습니다.", "DialogNcaExtractionCheckLogErrorMessage": "추출 실패하였습니다. 자세한 내용은 로그 파일을 읽으세요.", "DialogNcaExtractionSuccessMessage": "추출이 성공적으로 완료되었습니다.", diff --git a/src/Ryujinx/Assets/Locales/pl_PL.json b/src/Ryujinx/Assets/Locales/pl_PL.json index 5a276a7c5..6eb1118be 100644 --- a/src/Ryujinx/Assets/Locales/pl_PL.json +++ b/src/Ryujinx/Assets/Locales/pl_PL.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "Wystąpił błąd podczas próby znalezienia określonych zapisanych danych: {0}", "FolderDialogExtractTitle": "Wybierz folder, do którego chcesz rozpakować", "DialogNcaExtractionMessage": "Wypakowywanie sekcji {0} z {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Asystent wypakowania sekcji NCA", + "DialogNcaExtractionTitle": "Asystent wypakowania sekcji NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Niepowodzenie podczas wypakowywania. W wybranym pliku nie było głównego NCA.", "DialogNcaExtractionCheckLogErrorMessage": "Niepowodzenie podczas wypakowywania. Przeczytaj plik dziennika, aby uzyskać więcej informacji.", "DialogNcaExtractionSuccessMessage": "Wypakowywanie zakończone pomyślnie.", diff --git a/src/Ryujinx/Assets/Locales/pt_BR.json b/src/Ryujinx/Assets/Locales/pt_BR.json index 6aeb422ed..087098959 100644 --- a/src/Ryujinx/Assets/Locales/pt_BR.json +++ b/src/Ryujinx/Assets/Locales/pt_BR.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "Ocorreu um erro ao tentar encontrar o diretório de salvamento: {0}", "FolderDialogExtractTitle": "Escolha o diretório onde os arquivos serão extraídos", "DialogNcaExtractionMessage": "Extraindo seção {0} de {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Extrator de seções NCA", + "DialogNcaExtractionTitle": "Extrator de seções NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Falha na extração. O NCA principal não foi encontrado no arquivo selecionado.", "DialogNcaExtractionCheckLogErrorMessage": "Falha na extração. Leia o arquivo de log para mais informações.", "DialogNcaExtractionSuccessMessage": "Extração concluída com êxito.", diff --git a/src/Ryujinx/Assets/Locales/ru_RU.json b/src/Ryujinx/Assets/Locales/ru_RU.json index 4980a9a5d..f1a6f64e4 100644 --- a/src/Ryujinx/Assets/Locales/ru_RU.json +++ b/src/Ryujinx/Assets/Locales/ru_RU.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "Произошла ошибка при поиске указанных данных сохранения: {0}", "FolderDialogExtractTitle": "Выберите папку для извлечения", "DialogNcaExtractionMessage": "Извлечение {0} раздела из {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Извлечение разделов NCA", + "DialogNcaExtractionTitle": "Извлечение разделов NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Ошибка извлечения. Основной NCA не присутствовал в выбранном файле.", "DialogNcaExtractionCheckLogErrorMessage": "Ошибка извлечения. Прочтите файл журнала для получения дополнительной информации.", "DialogNcaExtractionSuccessMessage": "Извлечение завершено успешно.", diff --git a/src/Ryujinx/Assets/Locales/th_TH.json b/src/Ryujinx/Assets/Locales/th_TH.json index 16c2d9455..caf8f00ff 100644 --- a/src/Ryujinx/Assets/Locales/th_TH.json +++ b/src/Ryujinx/Assets/Locales/th_TH.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "มีข้อผิดพลาดในการค้นหาข้อมูลบันทึกที่ระบุไว้: {0}", "FolderDialogExtractTitle": "เลือกโฟลเดอร์ที่จะแตกไฟล์เข้าไป", "DialogNcaExtractionMessage": "กำลังแตกไฟล์ {0} จากส่วน {1}...", - "DialogNcaExtractionTitle": "Ryujinx - เครื่องมือแตกไฟล์ของ NCA", + "DialogNcaExtractionTitle": "เครื่องมือแตกไฟล์ของ NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "เกิดความล้มเหลวในการแตกไฟล์เนื่องจากไม่พบ NCA หลักในไฟล์ที่เลือก", "DialogNcaExtractionCheckLogErrorMessage": "เกิดความล้มเหลวในการแตกไฟล์ โปรดอ่านไฟล์บันทึกประวัติเพื่อดูข้อมูลเพิ่มเติม", "DialogNcaExtractionSuccessMessage": "การแตกไฟล์เสร็จสมบูรณ์แล้ว", diff --git a/src/Ryujinx/Assets/Locales/tr_TR.json b/src/Ryujinx/Assets/Locales/tr_TR.json index d26ca18b7..83791752b 100644 --- a/src/Ryujinx/Assets/Locales/tr_TR.json +++ b/src/Ryujinx/Assets/Locales/tr_TR.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "Belirtilen kayıt verisi bulunmaya çalışırken hata: {0}", "FolderDialogExtractTitle": "İçine ayıklanacak klasörü seç", "DialogNcaExtractionMessage": "{1} den {0} kısmı ayıklanıyor...", - "DialogNcaExtractionTitle": "Ryujinx - NCA Kısmı Ayıklayıcısı", + "DialogNcaExtractionTitle": "NCA Kısmı Ayıklayıcısı", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Ayıklama hatası. Ana NCA seçilen dosyada bulunamadı.", "DialogNcaExtractionCheckLogErrorMessage": "Ayıklama hatası. Ek bilgi için kayıt dosyasını okuyun.", "DialogNcaExtractionSuccessMessage": "Ayıklama başarıyla tamamlandı.", diff --git a/src/Ryujinx/Assets/Locales/uk_UA.json b/src/Ryujinx/Assets/Locales/uk_UA.json index 0a4f251fe..807097f25 100644 --- a/src/Ryujinx/Assets/Locales/uk_UA.json +++ b/src/Ryujinx/Assets/Locales/uk_UA.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "Під час пошуку вказаних даних збереження сталася помилка: {0}", "FolderDialogExtractTitle": "Виберіть папку для видобування", "DialogNcaExtractionMessage": "Видобування розділу {0} з {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Екстрактор розділів NCA", + "DialogNcaExtractionTitle": "Екстрактор розділів NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Помилка видобування. Основний NCA не був присутній у вибраному файлі.", "DialogNcaExtractionCheckLogErrorMessage": "Помилка видобування. Прочитайте файл журналу для отримання додаткової інформації.", "DialogNcaExtractionSuccessMessage": "Видобування успішно завершено.", diff --git a/src/Ryujinx/Assets/Locales/zh_CN.json b/src/Ryujinx/Assets/Locales/zh_CN.json index d1f3c13e3..98f27b4c2 100644 --- a/src/Ryujinx/Assets/Locales/zh_CN.json +++ b/src/Ryujinx/Assets/Locales/zh_CN.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "查找指定存档时出错:{0}", "FolderDialogExtractTitle": "选择要提取到的文件夹", "DialogNcaExtractionMessage": "提取 {1} 的 {0} 分区...", - "DialogNcaExtractionTitle": "Ryujinx - NCA 分区提取", + "DialogNcaExtractionTitle": "NCA 分区提取", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失败,所选文件中没有 NCA 文件", "DialogNcaExtractionCheckLogErrorMessage": "提取失败,请查看日志文件获取详情", "DialogNcaExtractionSuccessMessage": "提取成功!", diff --git a/src/Ryujinx/Assets/Locales/zh_TW.json b/src/Ryujinx/Assets/Locales/zh_TW.json index d69e57a9c..491c07dd8 100644 --- a/src/Ryujinx/Assets/Locales/zh_TW.json +++ b/src/Ryujinx/Assets/Locales/zh_TW.json @@ -437,7 +437,7 @@ "DialogMessageFindSaveErrorMessage": "尋找指定的存檔時出現錯誤: {0}", "FolderDialogExtractTitle": "選擇要解壓到的資料夾", "DialogNcaExtractionMessage": "從 {1} 提取 {0} 分區...", - "DialogNcaExtractionTitle": "Ryujinx - NCA 分區提取器", + "DialogNcaExtractionTitle": "NCA 分區提取器", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失敗。所選檔案中不存在主 NCA 檔案。", "DialogNcaExtractionCheckLogErrorMessage": "提取失敗。請閱讀日誌檔案了解更多資訊。", "DialogNcaExtractionSuccessMessage": "提取成功。", diff --git a/src/Ryujinx/Common/ApplicationHelper.cs b/src/Ryujinx/Common/ApplicationHelper.cs index 43b69045c..db5961347 100644 --- a/src/Ryujinx/Common/ApplicationHelper.cs +++ b/src/Ryujinx/Common/ApplicationHelper.cs @@ -146,7 +146,7 @@ namespace Ryujinx.Ava.Common var cancellationToken = new CancellationTokenSource(); UpdateWaitWindow waitingDialog = new( - LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle], + App.FormatTitle(LocaleKeys.DialogNcaExtractionTitle), LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogNcaExtractionMessage, ncaSectionType, Path.GetFileName(titleFilePath)), cancellationToken); @@ -269,7 +269,7 @@ namespace Ryujinx.Ava.Common Dispatcher.UIThread.Post(waitingDialog.Close); NotificationHelper.Show( - LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle], + App.FormatTitle(LocaleKeys.DialogNcaExtractionTitle), $"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}", NotificationType.Information); } diff --git a/src/Ryujinx/Updater.cs b/src/Ryujinx/Updater.cs index a466ea832..4754f2091 100644 --- a/src/Ryujinx/Updater.cs +++ b/src/Ryujinx/Updater.cs @@ -83,7 +83,7 @@ namespace Ryujinx.Ava } catch { - Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!"); + Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {App.FullAppName} version!"); await ContentDialogHelper.CreateWarningDialog( LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage], From 69f75f2df17bb633e8bdf20590650880c6c9d3ad Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 19:58:02 -0600 Subject: [PATCH 06/20] misc: Fix small code formatting & styling issues --- .../Models/XCITrimmerFileModel.cs | 4 +- src/Ryujinx/Assets/Locales/ar_SA.json | 1 + src/Ryujinx/Assets/Locales/de_DE.json | 1 + src/Ryujinx/Assets/Locales/el_GR.json | 1 + src/Ryujinx/Assets/Locales/es_ES.json | 1 + src/Ryujinx/Assets/Locales/fr_FR.json | 1 + src/Ryujinx/Assets/Locales/he_IL.json | 1 + src/Ryujinx/Assets/Locales/it_IT.json | 1 + src/Ryujinx/Assets/Locales/ja_JP.json | 1 + src/Ryujinx/Assets/Locales/ko_KR.json | 1 + src/Ryujinx/Assets/Locales/pl_PL.json | 1 + src/Ryujinx/Assets/Locales/pt_BR.json | 1 + src/Ryujinx/Assets/Locales/ru_RU.json | 1 + src/Ryujinx/Assets/Locales/th_TH.json | 1 + src/Ryujinx/Assets/Locales/tr_TR.json | 1 + src/Ryujinx/Assets/Locales/uk_UA.json | 1 + src/Ryujinx/Assets/Locales/zh_CN.json | 1 + src/Ryujinx/Assets/Locales/zh_TW.json | 1 + src/Ryujinx/Program.cs | 1 - src/Ryujinx/UI/Helpers/ContentDialogHelper.cs | 6 +- .../Views/Input/ControllerInputView.axaml.cs | 66 ++++++++----------- src/Ryujinx/UI/Views/Input/InputView.axaml.cs | 23 ++----- src/Ryujinx/Updater.cs | 3 +- 23 files changed, 58 insertions(+), 62 deletions(-) diff --git a/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs b/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs index 05fa82920..95fb3985b 100644 --- a/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs +++ b/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs @@ -43,8 +43,8 @@ namespace Ryujinx.UI.Common.Models { if (obj == null) return false; - else - return this.Path == obj.Path; + + return this.Path == obj.Path; } public override int GetHashCode() diff --git a/src/Ryujinx/Assets/Locales/ar_SA.json b/src/Ryujinx/Assets/Locales/ar_SA.json index 129558395..495ab4b4d 100644 --- a/src/Ryujinx/Assets/Locales/ar_SA.json +++ b/src/Ryujinx/Assets/Locales/ar_SA.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "تعيين لون الخلفية", "AvatarClose": "إغلاق", "ControllerSettingsLoadProfileToolTip": "تحميل الملف الشخصي", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "إضافة ملف شخصي", "ControllerSettingsRemoveProfileToolTip": "إزالة الملف الشخصي", "ControllerSettingsSaveProfileToolTip": "حفظ الملف الشخصي", diff --git a/src/Ryujinx/Assets/Locales/de_DE.json b/src/Ryujinx/Assets/Locales/de_DE.json index 7b25336c3..e23f3b619 100644 --- a/src/Ryujinx/Assets/Locales/de_DE.json +++ b/src/Ryujinx/Assets/Locales/de_DE.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Hintergrundfarbe auswählen", "AvatarClose": "Schließen", "ControllerSettingsLoadProfileToolTip": "Lädt ein Profil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Fügt ein Profil hinzu", "ControllerSettingsRemoveProfileToolTip": "Entfernt ein Profil", "ControllerSettingsSaveProfileToolTip": "Speichert ein Profil", diff --git a/src/Ryujinx/Assets/Locales/el_GR.json b/src/Ryujinx/Assets/Locales/el_GR.json index 95c2062b0..79975b892 100644 --- a/src/Ryujinx/Assets/Locales/el_GR.json +++ b/src/Ryujinx/Assets/Locales/el_GR.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Ορισμός Χρώματος Φόντου", "AvatarClose": "Κλείσιμο", "ControllerSettingsLoadProfileToolTip": "Φόρτωση Προφίλ", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Προσθήκη Προφίλ", "ControllerSettingsRemoveProfileToolTip": "Κατάργηση Προφίλ", "ControllerSettingsSaveProfileToolTip": "Αποθήκευση Προφίλ", diff --git a/src/Ryujinx/Assets/Locales/es_ES.json b/src/Ryujinx/Assets/Locales/es_ES.json index 183addade..e6da1d113 100644 --- a/src/Ryujinx/Assets/Locales/es_ES.json +++ b/src/Ryujinx/Assets/Locales/es_ES.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Establecer color de fondo", "AvatarClose": "Cerrar", "ControllerSettingsLoadProfileToolTip": "Cargar perfil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Agregar perfil", "ControllerSettingsRemoveProfileToolTip": "Eliminar perfil", "ControllerSettingsSaveProfileToolTip": "Guardar perfil", diff --git a/src/Ryujinx/Assets/Locales/fr_FR.json b/src/Ryujinx/Assets/Locales/fr_FR.json index 34d53e16b..e52333cea 100644 --- a/src/Ryujinx/Assets/Locales/fr_FR.json +++ b/src/Ryujinx/Assets/Locales/fr_FR.json @@ -408,6 +408,7 @@ "AvatarClose": "Fermer", "ControllerSettingsLoadProfileToolTip": "Charger un profil", "ControllerSettingsAddProfileToolTip": "Ajouter un profil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsRemoveProfileToolTip": "Supprimer un profil", "ControllerSettingsSaveProfileToolTip": "Enregistrer un profil", "MenuBarFileToolsTakeScreenshot": "Prendre une capture d'écran", diff --git a/src/Ryujinx/Assets/Locales/he_IL.json b/src/Ryujinx/Assets/Locales/he_IL.json index 6bd8f0470..d2bd21124 100644 --- a/src/Ryujinx/Assets/Locales/he_IL.json +++ b/src/Ryujinx/Assets/Locales/he_IL.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "הגדר צבע רקע", "AvatarClose": "סגור", "ControllerSettingsLoadProfileToolTip": "טען פרופיל", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "הוסף פרופיל", "ControllerSettingsRemoveProfileToolTip": "הסר פרופיל", "ControllerSettingsSaveProfileToolTip": "שמור פרופיל", diff --git a/src/Ryujinx/Assets/Locales/it_IT.json b/src/Ryujinx/Assets/Locales/it_IT.json index 0d0edb42c..ee2bd3a16 100644 --- a/src/Ryujinx/Assets/Locales/it_IT.json +++ b/src/Ryujinx/Assets/Locales/it_IT.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Imposta colore di sfondo", "AvatarClose": "Chiudi", "ControllerSettingsLoadProfileToolTip": "Carica profilo", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Aggiungi profilo", "ControllerSettingsRemoveProfileToolTip": "Rimuovi profilo", "ControllerSettingsSaveProfileToolTip": "Salva profilo", diff --git a/src/Ryujinx/Assets/Locales/ja_JP.json b/src/Ryujinx/Assets/Locales/ja_JP.json index 3eade3e6e..8d15ab678 100644 --- a/src/Ryujinx/Assets/Locales/ja_JP.json +++ b/src/Ryujinx/Assets/Locales/ja_JP.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "背景色を指定", "AvatarClose": "閉じる", "ControllerSettingsLoadProfileToolTip": "プロファイルをロード", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "プロファイルを追加", "ControllerSettingsRemoveProfileToolTip": "プロファイルを削除", "ControllerSettingsSaveProfileToolTip": "プロファイルをセーブ", diff --git a/src/Ryujinx/Assets/Locales/ko_KR.json b/src/Ryujinx/Assets/Locales/ko_KR.json index bd5bed962..2a62c415d 100644 --- a/src/Ryujinx/Assets/Locales/ko_KR.json +++ b/src/Ryujinx/Assets/Locales/ko_KR.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "배경색 설정", "AvatarClose": "닫기", "ControllerSettingsLoadProfileToolTip": "프로필 불러오기", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "프로필 추가", "ControllerSettingsRemoveProfileToolTip": "프로필 제거", "ControllerSettingsSaveProfileToolTip": "프로필 저장", diff --git a/src/Ryujinx/Assets/Locales/pl_PL.json b/src/Ryujinx/Assets/Locales/pl_PL.json index 6eb1118be..d6356dc76 100644 --- a/src/Ryujinx/Assets/Locales/pl_PL.json +++ b/src/Ryujinx/Assets/Locales/pl_PL.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Ustaw kolor tła", "AvatarClose": "Zamknij", "ControllerSettingsLoadProfileToolTip": "Wczytaj profil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Dodaj profil", "ControllerSettingsRemoveProfileToolTip": "Usuń profil", "ControllerSettingsSaveProfileToolTip": "Zapisz profil", diff --git a/src/Ryujinx/Assets/Locales/pt_BR.json b/src/Ryujinx/Assets/Locales/pt_BR.json index 087098959..42a8e437b 100644 --- a/src/Ryujinx/Assets/Locales/pt_BR.json +++ b/src/Ryujinx/Assets/Locales/pt_BR.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Definir cor de fundo", "AvatarClose": "Fechar", "ControllerSettingsLoadProfileToolTip": "Carregar perfil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Adicionar perfil", "ControllerSettingsRemoveProfileToolTip": "Remover perfil", "ControllerSettingsSaveProfileToolTip": "Salvar perfil", diff --git a/src/Ryujinx/Assets/Locales/ru_RU.json b/src/Ryujinx/Assets/Locales/ru_RU.json index f1a6f64e4..4ef6ff6d9 100644 --- a/src/Ryujinx/Assets/Locales/ru_RU.json +++ b/src/Ryujinx/Assets/Locales/ru_RU.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Установить цвет фона", "AvatarClose": "Закрыть", "ControllerSettingsLoadProfileToolTip": "Загрузить профиль", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Добавить профиль", "ControllerSettingsRemoveProfileToolTip": "Удалить профиль", "ControllerSettingsSaveProfileToolTip": "Сохранить профиль", diff --git a/src/Ryujinx/Assets/Locales/th_TH.json b/src/Ryujinx/Assets/Locales/th_TH.json index caf8f00ff..91169d9e2 100644 --- a/src/Ryujinx/Assets/Locales/th_TH.json +++ b/src/Ryujinx/Assets/Locales/th_TH.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "ตั้งค่าสีพื้นหลัง", "AvatarClose": "ปิด", "ControllerSettingsLoadProfileToolTip": "โหลด โปรไฟล์", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "เพิ่ม โปรไฟล์", "ControllerSettingsRemoveProfileToolTip": "ลบ โปรไฟล์", "ControllerSettingsSaveProfileToolTip": "บันทึก โปรไฟล์", diff --git a/src/Ryujinx/Assets/Locales/tr_TR.json b/src/Ryujinx/Assets/Locales/tr_TR.json index 83791752b..b9ce3e884 100644 --- a/src/Ryujinx/Assets/Locales/tr_TR.json +++ b/src/Ryujinx/Assets/Locales/tr_TR.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Arka Plan Rengi Ayarla", "AvatarClose": "Kapat", "ControllerSettingsLoadProfileToolTip": "Profil Yükle", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Profil Ekle", "ControllerSettingsRemoveProfileToolTip": "Profili Kaldır", "ControllerSettingsSaveProfileToolTip": "Profili Kaydet", diff --git a/src/Ryujinx/Assets/Locales/uk_UA.json b/src/Ryujinx/Assets/Locales/uk_UA.json index 807097f25..581d1bca1 100644 --- a/src/Ryujinx/Assets/Locales/uk_UA.json +++ b/src/Ryujinx/Assets/Locales/uk_UA.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Встановити колір фону", "AvatarClose": "Закрити", "ControllerSettingsLoadProfileToolTip": "Завантажити профіль", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Додати профіль", "ControllerSettingsRemoveProfileToolTip": "Видалити профіль", "ControllerSettingsSaveProfileToolTip": "Зберегти профіль", diff --git a/src/Ryujinx/Assets/Locales/zh_CN.json b/src/Ryujinx/Assets/Locales/zh_CN.json index 98f27b4c2..3426b8a4a 100644 --- a/src/Ryujinx/Assets/Locales/zh_CN.json +++ b/src/Ryujinx/Assets/Locales/zh_CN.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "设置背景色", "AvatarClose": "关闭", "ControllerSettingsLoadProfileToolTip": "加载配置文件", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "新增配置文件", "ControllerSettingsRemoveProfileToolTip": "删除配置文件", "ControllerSettingsSaveProfileToolTip": "保存配置文件", diff --git a/src/Ryujinx/Assets/Locales/zh_TW.json b/src/Ryujinx/Assets/Locales/zh_TW.json index 491c07dd8..fd02254e1 100644 --- a/src/Ryujinx/Assets/Locales/zh_TW.json +++ b/src/Ryujinx/Assets/Locales/zh_TW.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "設定背景顏色", "AvatarClose": "關閉", "ControllerSettingsLoadProfileToolTip": "載入設定檔", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "新增設定檔", "ControllerSettingsRemoveProfileToolTip": "刪除設定檔", "ControllerSettingsSaveProfileToolTip": "儲存設定檔", diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index cdce023cb..76512a34a 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -30,7 +30,6 @@ namespace Ryujinx.Ava { internal partial class Program { - // public static double WindowScaleFactor { get; set; } public static double DesktopScaleFactor { get; set; } = 1.0; public static string Version { get; private set; } diff --git a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs index bd8c1e3a7..a7fe3f0ce 100644 --- a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs +++ b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs @@ -226,11 +226,11 @@ namespace Ryujinx.Ava.UI.Helpers (int)Symbol.Help, primaryButtonResult); - internal static async Task CreateConfirmationDialogExtended( + internal static async Task CreateDeniableConfirmationDialog( string primaryText, string secondaryText, string acceptButtonText, - string noacceptButtonText, + string noAcceptButtonText, string cancelButtonText, string title, UserResult primaryButtonResult = UserResult.Yes) @@ -239,7 +239,7 @@ namespace Ryujinx.Ava.UI.Helpers primaryText, secondaryText, acceptButtonText, - noacceptButtonText, + noAcceptButtonText, cancelButtonText, (int)Symbol.Help, primaryButtonResult); diff --git a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs index c900ea532..ee84fbc37 100644 --- a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs @@ -26,19 +26,17 @@ namespace Ryujinx.Ava.UI.Views.Input foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) { - if (visual is ToggleButton button and not CheckBox) + switch (visual) { - button.IsCheckedChanged += Button_IsCheckedChanged; - } - - if (visual is CheckBox check) - { - check.IsCheckedChanged += CheckBox_IsCheckedChanged; - } - - if (visual is Slider slider) - { - slider.PropertyChanged += Slider_IsCheckedChanged; + case ToggleButton button and not CheckBox: + button.IsCheckedChanged += Button_IsCheckedChanged; + break; + case CheckBox check: + check.IsCheckedChanged += CheckBox_IsCheckedChanged; + break; + case Slider slider: + slider.PropertyChanged += Slider_ValueChanged; + break; } } } @@ -47,33 +45,28 @@ namespace Ryujinx.Ava.UI.Views.Input { base.OnPointerReleased(e); - if (_currentAssigner != null && _currentAssigner.ToggledButton != null && !_currentAssigner.ToggledButton.IsPointerOver) + if (_currentAssigner is { ToggledButton.IsPointerOver: false }) { _currentAssigner.Cancel(); } } - private float _changeSlider = -1.0f; + private float _changeSlider = float.NaN; - private void Slider_IsCheckedChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void Slider_ValueChanged(object sender, AvaloniaPropertyChangedEventArgs e) { if (sender is Slider check) { - if ((bool)check.IsPointerOver && _changeSlider == -1.0f) + _changeSlider = check.IsPointerOver switch { - _changeSlider = (float)check.Value; - - } - else if (!(bool)check.IsPointerOver) - { - _changeSlider = -1.0f; - } + true when float.IsNaN(_changeSlider) => (float)check.Value, + false => float.NaN, + _ => _changeSlider + }; - if (_changeSlider != -1.0f && _changeSlider != (float)check.Value) + if (!float.IsNaN(_changeSlider) && _changeSlider != (float)check.Value) { - - var viewModel = (DataContext as ControllerInputViewModel); - viewModel.ParentModel.IsModified = true; + (DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true; _changeSlider = (float)check.Value; } } @@ -81,25 +74,20 @@ namespace Ryujinx.Ava.UI.Views.Input private void CheckBox_IsCheckedChanged(object sender, RoutedEventArgs e) { - if (sender is CheckBox check) + if (sender is CheckBox { IsPointerOver: true }) { - if ((bool)check.IsPointerOver) - { - - var viewModel = (DataContext as ControllerInputViewModel); - viewModel.ParentModel.IsModified = true; - _currentAssigner?.Cancel(); - _currentAssigner = null; - } + (DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true; + _currentAssigner?.Cancel(); + _currentAssigner = null; } } private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) { - if (sender is ToggleButton button ) + if (sender is ToggleButton button) { - if ((bool)button.IsChecked) + if (button.IsChecked is true) { if (_currentAssigner != null && button == _currentAssigner.ToggledButton) { @@ -204,7 +192,7 @@ namespace Ryujinx.Ava.UI.Views.Input } else { - if (_currentAssigner != null ) + if (_currentAssigner != null) { _currentAssigner.Cancel(); _currentAssigner = null; diff --git a/src/Ryujinx/UI/Views/Input/InputView.axaml.cs b/src/Ryujinx/UI/Views/Input/InputView.axaml.cs index 5fda7ef6a..3c9d4040f 100644 --- a/src/Ryujinx/UI/Views/Input/InputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/InputView.axaml.cs @@ -37,7 +37,7 @@ namespace Ryujinx.Ava.UI.Views.Input { _dialogOpen = true; - var result = await ContentDialogHelper.CreateConfirmationDialogExtended( + var result = await ContentDialogHelper.CreateDeniableConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmMessage], LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmSubMessage], LocaleManager.Instance[LocaleKeys.InputDialogYes], @@ -53,28 +53,19 @@ namespace Ryujinx.Ava.UI.Views.Input _dialogOpen = false; - if (result == UserResult.Cancel) - { - - return; - } - - ViewModel.IsModified = false; - - if (result != UserResult.Cancel) - { - ViewModel.PlayerId = ViewModel.PlayerIdChoose; - } - if (result == UserResult.Cancel) { if (e.AddedItems.Count > 0) { ViewModel.IsModified = true; - var player = (PlayerModel)e.AddedItems[0]; - ViewModel.PlayerId = player.Id; + ViewModel.PlayerId = ((PlayerModel)e.AddedItems[0])!.Id; } + return; } + + ViewModel.PlayerId = ViewModel.PlayerIdChoose; + + ViewModel.IsModified = false; } } diff --git a/src/Ryujinx/Updater.cs b/src/Ryujinx/Updater.cs index 4754f2091..9deff5e86 100644 --- a/src/Ryujinx/Updater.cs +++ b/src/Ryujinx/Updater.cs @@ -32,7 +32,8 @@ namespace Ryujinx.Ava internal static class Updater { private const string GitHubApiUrl = "https://api.github.com"; - private const string LatestReleaseUrl = $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest"; + private const string LatestReleaseUrl = + $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest"; private static readonly GithubReleasesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); From eb6ce7bcb3ed35b82619464739cd8dda8b990a2e Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 20:09:02 -0600 Subject: [PATCH 07/20] misc: chore: replace some new "" additions & some I missed --- src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs | 2 +- src/Ryujinx.HLE/HOS/ModLoader.cs | 2 +- src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs | 2 +- src/Ryujinx/UI/Windows/CheatWindow.axaml.cs | 2 +- src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs b/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs index 171a083f3..2e7b8ee76 100644 --- a/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs +++ b/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs @@ -2463,7 +2463,7 @@ namespace Ryujinx.HLE.HOS.Diagnostics.Demangler return ParseIntegerLiteral("unsigned short"); case 'i': _position++; - return ParseIntegerLiteral(""); + return ParseIntegerLiteral(string.Empty); case 'j': _position++; return ParseIntegerLiteral("u"); diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index ee179c929..9a81cc361 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -169,7 +169,7 @@ namespace Ryujinx.HLE.HOS foreach (var modDir in dir.EnumerateDirectories()) { types.Clear(); - Mod mod = new("", null, true); + Mod mod = new(string.Empty, null, true); if (StrEquals(RomfsDir, modDir.Name)) { diff --git a/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs b/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs index 93b2d6138..24c2c64c0 100644 --- a/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs @@ -127,7 +127,7 @@ namespace Ryujinx.UI.Common.Helper Logger.Debug?.Print(LogClass.Application, $"Adding type association {ext}"); using var openCmd = key.CreateSubKey(@"shell\open\command"); - openCmd.SetValue("", $"\"{Environment.ProcessPath}\" \"%1\""); + openCmd.SetValue(string.Empty, $"\"{Environment.ProcessPath}\" \"%1\""); Logger.Debug?.Print(LogClass.Application, $"Added type association {ext}"); } diff --git a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs index edca7949a..8c8d56b34 100644 --- a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs @@ -75,7 +75,7 @@ namespace Ryujinx.Ava.UI.Windows string parentPath = currentCheatFile.Replace(titleModsPath, string.Empty); buildId = Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper(); - currentGroup = new CheatNode("", buildId, parentPath, true); + currentGroup = new CheatNode(string.Empty, buildId, parentPath, true); LoadedCheats.Add(currentGroup); } diff --git a/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs b/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs index 580ebc9da..6df862283 100644 --- a/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs @@ -32,9 +32,9 @@ namespace Ryujinx.Ava.UI.Windows { ContentDialog contentDialog = new() { - PrimaryButtonText = "", - SecondaryButtonText = "", - CloseButtonText = "", + PrimaryButtonText = string.Empty, + SecondaryButtonText = string.Empty, + CloseButtonText = string.Empty, Content = new XCITrimmerWindow(mainWindowViewModel), Title = string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerWindowTitle]), }; From 617b81e209831c17de455c0c19bf108ab1173436 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 20:33:49 -0600 Subject: [PATCH 08/20] UI: Conditionally enable install/uninstall file types buttons based on whether they're installed already --- .../Helper/FileAssociationHelper.cs | 72 +++++++++---------- .../UI/ViewModels/MainWindowViewModel.cs | 5 ++ .../UI/Views/Main/MainMenuBarView.axaml | 6 +- 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs b/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs index 24c2c64c0..be10deca0 100644 --- a/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs @@ -4,6 +4,7 @@ using Ryujinx.Common.Logging; using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -23,6 +24,26 @@ namespace Ryujinx.UI.Common.Helper public static partial void SHChangeNotify(uint wEventId, uint uFlags, nint dwItem1, nint dwItem2); public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows()) && !ReleaseInformation.IsFlatHubBuild; + + public static bool AreMimeTypesRegistered + { + get + { + if (OperatingSystem.IsLinux()) + { + return AreMimeTypesRegisteredLinux(); + } + + if (OperatingSystem.IsWindows()) + { + return AreMimeTypesRegisteredWindows(); + } + + // TODO: Add macOS support. + + return false; + } + } [SupportedOSPlatform("linux")] private static bool AreMimeTypesRegisteredLinux() => File.Exists(Path.Combine(_mimeDbPath, "packages", "Ryujinx.xml")); @@ -72,6 +93,10 @@ namespace Ryujinx.UI.Common.Helper [SupportedOSPlatform("windows")] private static bool AreMimeTypesRegisteredWindows() { + return _fileExtensions.Aggregate(false, + (current, ext) => current | CheckRegistering(ext) + ); + static bool CheckRegistering(string ext) { RegistryKey key = Registry.CurrentUser.OpenSubKey(@$"Software\Classes\{ext}"); @@ -87,20 +112,20 @@ namespace Ryujinx.UI.Common.Helper return keyValue is not null && (keyValue.Contains("Ryujinx") || keyValue.Contains(AppDomain.CurrentDomain.FriendlyName)); } - - bool registered = false; - - foreach (string ext in _fileExtensions) - { - registered |= CheckRegistering(ext); - } - - return registered; } [SupportedOSPlatform("windows")] private static bool InstallWindowsMimeTypes(bool uninstall = false) { + bool registered = _fileExtensions.Aggregate(false, + (current, ext) => current | RegisterExtension(ext, uninstall) + ); + + // Notify Explorer the file association has been changed. + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, nint.Zero, nint.Zero); + + return registered; + static bool RegisterExtension(string ext, bool uninstall = false) { string keyString = @$"Software\Classes\{ext}"; @@ -134,35 +159,6 @@ namespace Ryujinx.UI.Common.Helper return true; } - - bool registered = false; - - foreach (string ext in _fileExtensions) - { - registered |= RegisterExtension(ext, uninstall); - } - - // Notify Explorer the file association has been changed. - SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, nint.Zero, nint.Zero); - - return registered; - } - - public static bool AreMimeTypesRegistered() - { - if (OperatingSystem.IsLinux()) - { - return AreMimeTypesRegisteredLinux(); - } - - if (OperatingSystem.IsWindows()) - { - return AreMimeTypesRegisteredWindows(); - } - - // TODO: Add macOS support. - - return false; } public static bool Install() diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 70f8b2a82..437f9861d 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -802,6 +802,11 @@ namespace Ryujinx.Ava.UI.ViewModels { get => FileAssociationHelper.IsTypeAssociationSupported; } + + public bool AreMimeTypesRegistered + { + get => FileAssociationHelper.AreMimeTypesRegistered; + } public ObservableCollectionExtended Applications { diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 0a41b3458..910741089 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -4,8 +4,10 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" + xmlns:uih="clr-namespace:Ryujinx.UI.Common.Helper" mc:Ignorable="d" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:helper="clr-namespace:Ryujinx.UI.Common.Helper;assembly=Ryujinx.UI.Common" x:DataType="viewModels:MainWindowViewModel" x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView"> @@ -265,8 +267,8 @@ - - + + From 285ee276b6a199e66d3bb5d8558fe63a686d363a Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 22:03:12 -0600 Subject: [PATCH 09/20] misc: Bake in value change logging into ReactiveObject to reduce logic duplication. --- src/Ryujinx.Common/ReactiveObject.cs | 22 ++++-- src/Ryujinx.HLE/HOS/ModLoader.cs | 17 ++--- .../Configuration/ConfigurationState.cs | 75 +++++++++---------- 3 files changed, 59 insertions(+), 55 deletions(-) diff --git a/src/Ryujinx.Common/ReactiveObject.cs b/src/Ryujinx.Common/ReactiveObject.cs index 4f27af546..8df1e20fe 100644 --- a/src/Ryujinx.Common/ReactiveObject.cs +++ b/src/Ryujinx.Common/ReactiveObject.cs @@ -1,11 +1,13 @@ +using Ryujinx.Common.Logging; using System; +using System.Globalization; using System.Threading; namespace Ryujinx.Common { public class ReactiveObject { - private readonly ReaderWriterLockSlim _readerWriterLock = new(); + private readonly ReaderWriterLockSlim _rwLock = new(); private bool _isInitialized; private T _value; @@ -15,15 +17,15 @@ namespace Ryujinx.Common { get { - _readerWriterLock.EnterReadLock(); + _rwLock.EnterReadLock(); T value = _value; - _readerWriterLock.ExitReadLock(); + _rwLock.ExitReadLock(); return value; } set { - _readerWriterLock.EnterWriteLock(); + _rwLock.EnterWriteLock(); T oldValue = _value; @@ -32,7 +34,7 @@ namespace Ryujinx.Common _isInitialized = true; _value = value; - _readerWriterLock.ExitWriteLock(); + _rwLock.ExitWriteLock(); if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value)) { @@ -40,12 +42,22 @@ namespace Ryujinx.Common } } } + + public void LogChangesToValue(string valueName, LogClass logClass = LogClass.Configuration) + => Event += (_, e) => ReactiveObjectHelper.LogValueChange(logClass, e, valueName); public static implicit operator T(ReactiveObject obj) => obj.Value; } public static class ReactiveObjectHelper { + public static void LogValueChange(LogClass logClass, ReactiveEventArgs eventArgs, string valueName) + { + string message = string.Create(CultureInfo.InvariantCulture, $"{valueName} set to: {eventArgs.NewValue}"); + + Logger.Info?.Print(logClass, message); + } + public static void Toggle(this ReactiveObject rBoolean) => rBoolean.Value = !rBoolean.Value; } diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index 9a81cc361..7cbe1afca 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -116,18 +116,13 @@ namespace Ryujinx.HLE.HOS private readonly Dictionary _appMods; // key is ApplicationId private PatchCache _patches; - private static readonly EnumerationOptions _dirEnumOptions; - - static ModLoader() + private static readonly EnumerationOptions _dirEnumOptions = new() { - _dirEnumOptions = new EnumerationOptions - { - MatchCasing = MatchCasing.CaseInsensitive, - MatchType = MatchType.Simple, - RecurseSubdirectories = false, - ReturnSpecialDirectories = false, - }; - } + MatchCasing = MatchCasing.CaseInsensitive, + MatchType = MatchType.Simple, + RecurseSubdirectories = false, + ReturnSpecialDirectories = false, + }; public ModLoader() { diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 50b3569a1..27fc0a3f1 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -13,8 +13,6 @@ using Ryujinx.UI.Common.Configuration.UI; using Ryujinx.UI.Common.Helper; using System; using System.Collections.Generic; -using System.Globalization; -using System.Text.Json.Nodes; namespace Ryujinx.UI.Common.Configuration { @@ -201,7 +199,7 @@ namespace Ryujinx.UI.Common.Configuration IsAscendingOrder = new ReactiveObject(); LanguageCode = new ReactiveObject(); ShowConsole = new ReactiveObject(); - ShowConsole.Event += static (s, e) => { ConsoleHelper.SetConsoleWindowState(e.NewValue); }; + ShowConsole.Event += static (_, e) => ConsoleHelper.SetConsoleWindowState(e.NewValue); } } @@ -268,6 +266,7 @@ namespace Ryujinx.UI.Common.Configuration public LoggerSection() { EnableDebug = new ReactiveObject(); + EnableDebug.LogChangesToValue(nameof(EnableDebug)); EnableStub = new ReactiveObject(); EnableInfo = new ReactiveObject(); EnableWarn = new ReactiveObject(); @@ -277,7 +276,7 @@ namespace Ryujinx.UI.Common.Configuration EnableFsAccessLog = new ReactiveObject(); FilteredClasses = new ReactiveObject(); EnableFileLog = new ReactiveObject(); - EnableFileLog.Event += static (sender, e) => LogValueChange(e, nameof(EnableFileLog)); + EnableFileLog.LogChangesToValue(nameof(EnableFileLog)); GraphicsDebugLevel = new ReactiveObject(); } } @@ -370,33 +369,37 @@ namespace Ryujinx.UI.Common.Configuration public SystemSection() { Language = new ReactiveObject(); + Language.LogChangesToValue(nameof(Language)); Region = new ReactiveObject(); + Region.LogChangesToValue(nameof(Region)); TimeZone = new ReactiveObject(); + TimeZone.LogChangesToValue(nameof(TimeZone)); SystemTimeOffset = new ReactiveObject(); + SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset)); EnableDockedMode = new ReactiveObject(); - EnableDockedMode.Event += static (sender, e) => LogValueChange(e, nameof(EnableDockedMode)); + EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode)); EnablePtc = new ReactiveObject(); - EnablePtc.Event += static (sender, e) => LogValueChange(e, nameof(EnablePtc)); + EnablePtc.LogChangesToValue(nameof(EnablePtc)); EnableLowPowerPtc = new ReactiveObject(); - EnableLowPowerPtc.Event += static (sender, e) => LogValueChange(e, nameof(EnableLowPowerPtc)); + EnableLowPowerPtc.LogChangesToValue(nameof(EnableLowPowerPtc)); EnableInternetAccess = new ReactiveObject(); - EnableInternetAccess.Event += static (sender, e) => LogValueChange(e, nameof(EnableInternetAccess)); + EnableInternetAccess.LogChangesToValue(nameof(EnableInternetAccess)); EnableFsIntegrityChecks = new ReactiveObject(); - EnableFsIntegrityChecks.Event += static (sender, e) => LogValueChange(e, nameof(EnableFsIntegrityChecks)); + EnableFsIntegrityChecks.LogChangesToValue(nameof(EnableFsIntegrityChecks)); FsGlobalAccessLogMode = new ReactiveObject(); - FsGlobalAccessLogMode.Event += static (sender, e) => LogValueChange(e, nameof(FsGlobalAccessLogMode)); + FsGlobalAccessLogMode.LogChangesToValue(nameof(FsGlobalAccessLogMode)); AudioBackend = new ReactiveObject(); - AudioBackend.Event += static (sender, e) => LogValueChange(e, nameof(AudioBackend)); + AudioBackend.LogChangesToValue(nameof(AudioBackend)); MemoryManagerMode = new ReactiveObject(); - MemoryManagerMode.Event += static (sender, e) => LogValueChange(e, nameof(MemoryManagerMode)); + MemoryManagerMode.LogChangesToValue(nameof(MemoryManagerMode)); DramSize = new ReactiveObject(); - DramSize.Event += static (sender, e) => LogValueChange(e, nameof(DramSize)); + DramSize.LogChangesToValue(nameof(DramSize)); IgnoreMissingServices = new ReactiveObject(); - IgnoreMissingServices.Event += static (sender, e) => LogValueChange(e, nameof(IgnoreMissingServices)); + IgnoreMissingServices.LogChangesToValue(nameof(IgnoreMissingServices)); AudioVolume = new ReactiveObject(); - AudioVolume.Event += static (sender, e) => LogValueChange(e, nameof(AudioVolume)); + AudioVolume.LogChangesToValue(nameof(AudioVolume)); UseHypervisor = new ReactiveObject(); - UseHypervisor.Event += static (sender, e) => LogValueChange(e, nameof(UseHypervisor)); + UseHypervisor.LogChangesToValue(nameof(UseHypervisor)); } } @@ -524,36 +527,36 @@ namespace Ryujinx.UI.Common.Configuration public GraphicsSection() { BackendThreading = new ReactiveObject(); - BackendThreading.Event += static (_, e) => LogValueChange(e, nameof(BackendThreading)); + BackendThreading.LogChangesToValue(nameof(BackendThreading)); ResScale = new ReactiveObject(); - ResScale.Event += static (_, e) => LogValueChange(e, nameof(ResScale)); + ResScale.LogChangesToValue(nameof(ResScale)); ResScaleCustom = new ReactiveObject(); - ResScaleCustom.Event += static (_, e) => LogValueChange(e, nameof(ResScaleCustom)); + ResScaleCustom.LogChangesToValue(nameof(ResScaleCustom)); MaxAnisotropy = new ReactiveObject(); - MaxAnisotropy.Event += static (_, e) => LogValueChange(e, nameof(MaxAnisotropy)); + MaxAnisotropy.LogChangesToValue(nameof(MaxAnisotropy)); AspectRatio = new ReactiveObject(); - AspectRatio.Event += static (_, e) => LogValueChange(e, nameof(AspectRatio)); + AspectRatio.LogChangesToValue(nameof(AspectRatio)); ShadersDumpPath = new ReactiveObject(); EnableVsync = new ReactiveObject(); - EnableVsync.Event += static (_, e) => LogValueChange(e, nameof(EnableVsync)); + EnableVsync.LogChangesToValue(nameof(EnableVsync)); EnableShaderCache = new ReactiveObject(); - EnableShaderCache.Event += static (_, e) => LogValueChange(e, nameof(EnableShaderCache)); + EnableShaderCache.LogChangesToValue(nameof(EnableShaderCache)); EnableTextureRecompression = new ReactiveObject(); - EnableTextureRecompression.Event += static (_, e) => LogValueChange(e, nameof(EnableTextureRecompression)); + EnableTextureRecompression.LogChangesToValue(nameof(EnableTextureRecompression)); GraphicsBackend = new ReactiveObject(); - GraphicsBackend.Event += static (_, e) => LogValueChange(e, nameof(GraphicsBackend)); + GraphicsBackend.LogChangesToValue(nameof(GraphicsBackend)); PreferredGpu = new ReactiveObject(); - PreferredGpu.Event += static (_, e) => LogValueChange(e, nameof(PreferredGpu)); + PreferredGpu.LogChangesToValue(nameof(PreferredGpu)); EnableMacroHLE = new ReactiveObject(); - EnableMacroHLE.Event += static (_, e) => LogValueChange(e, nameof(EnableMacroHLE)); + EnableMacroHLE.LogChangesToValue(nameof(EnableMacroHLE)); EnableColorSpacePassthrough = new ReactiveObject(); - EnableColorSpacePassthrough.Event += static (_, e) => LogValueChange(e, nameof(EnableColorSpacePassthrough)); + EnableColorSpacePassthrough.LogChangesToValue(nameof(EnableColorSpacePassthrough)); AntiAliasing = new ReactiveObject(); - AntiAliasing.Event += static (_, e) => LogValueChange(e, nameof(AntiAliasing)); + AntiAliasing.LogChangesToValue(nameof(AntiAliasing)); ScalingFilter = new ReactiveObject(); - ScalingFilter.Event += static (_, e) => LogValueChange(e, nameof(ScalingFilter)); + ScalingFilter.LogChangesToValue(nameof(ScalingFilter)); ScalingFilterLevel = new ReactiveObject(); - ScalingFilterLevel.Event += static (_, e) => LogValueChange(e, nameof(ScalingFilterLevel)); + ScalingFilterLevel.LogChangesToValue(nameof(ScalingFilterLevel)); } } @@ -576,7 +579,7 @@ namespace Ryujinx.UI.Common.Configuration { LanInterfaceId = new ReactiveObject(); Mode = new ReactiveObject(); - Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode)); + Mode.LogChangesToValue(nameof(Mode)); } } @@ -667,6 +670,7 @@ namespace Ryujinx.UI.Common.Configuration CheckUpdatesOnStart = new ReactiveObject(); ShowConfirmExit = new ReactiveObject(); IgnoreApplet = new ReactiveObject(); + IgnoreApplet.LogChangesToValue(nameof(IgnoreApplet)); RememberWindowState = new ReactiveObject(); ShowTitleBar = new ReactiveObject(); EnableHardwareAcceleration = new ReactiveObject(); @@ -1654,13 +1658,6 @@ namespace Ryujinx.UI.Common.Configuration return GraphicsBackend.OpenGl; } - private static void LogValueChange(ReactiveEventArgs eventArgs, string valueName) - { - string message = string.Create(CultureInfo.InvariantCulture, $"{valueName} set to: {eventArgs.NewValue}"); - - Ryujinx.Common.Logging.Logger.Info?.Print(LogClass.Configuration, message); - } - public static void Initialize() { if (Instance != null) From 15c20920b37ac99863caeaff4681f2e1de1a695a Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 22:04:55 -0600 Subject: [PATCH 10/20] I thought this was a typo on my part; it wasn't --- src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 27fc0a3f1..c0abb372a 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -579,7 +579,7 @@ namespace Ryujinx.UI.Common.Configuration { LanInterfaceId = new ReactiveObject(); Mode = new ReactiveObject(); - Mode.LogChangesToValue(nameof(Mode)); + Mode.LogChangesToValue(nameof(MultiplayerMode)); } } From a506d81989d492e3bc1a18b76f313e2c0cc6c698 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 22:16:45 -0600 Subject: [PATCH 11/20] Split ConfigurationState into 3 parts: Migration, Model, and everything else. --- .../ConfigurationState.Migration.cs | 715 +++++++++ .../Configuration/ConfigurationState.Model.cs | 674 ++++++++ .../Configuration/ConfigurationState.cs | 1374 +---------------- 3 files changed, 1396 insertions(+), 1367 deletions(-) create mode 100644 src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs create mode 100644 src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs new file mode 100644 index 000000000..18326a3a4 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs @@ -0,0 +1,715 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.Logging; +using Ryujinx.HLE; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Configuration.UI; +using System; +using System.Collections.Generic; + +namespace Ryujinx.UI.Common.Configuration +{ + public partial class ConfigurationState + { + public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) + { + bool configurationFileUpdated = false; + + if (configurationFileFormat.Version is < 0 or > ConfigurationFileFormat.CurrentVersion) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default."); + + LoadDefault(); + } + + if (configurationFileFormat.Version < 2) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2."); + + configurationFileFormat.SystemRegion = Region.USA; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 3) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 3."); + + configurationFileFormat.SystemTimeZone = "UTC"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 4) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 4."); + + configurationFileFormat.MaxAnisotropy = -1; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 5) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 5."); + + configurationFileFormat.SystemTimeOffset = 0; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 8) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 8."); + + configurationFileFormat.EnablePtc = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 9) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 9."); + + configurationFileFormat.ColumnSort = new ColumnSort + { + SortColumnId = 0, + SortAscending = false, + }; + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = Key.F1, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 10) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 10."); + + configurationFileFormat.AudioBackend = AudioBackend.OpenAl; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 11) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 11."); + + configurationFileFormat.ResScale = 1; + configurationFileFormat.ResScaleCustom = 1.0f; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 12) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 12."); + + configurationFileFormat.LoggingGraphicsDebugLevel = GraphicsDebugLevel.None; + + configurationFileUpdated = true; + } + + // configurationFileFormat.Version == 13 -> LDN1 + + if (configurationFileFormat.Version < 14) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 14."); + + configurationFileFormat.CheckUpdatesOnStart = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 16) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 16."); + + configurationFileFormat.EnableShaderCache = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 17) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 17."); + + configurationFileFormat.StartFullscreen = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 18) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 18."); + + configurationFileFormat.AspectRatio = AspectRatio.Fixed16x9; + + configurationFileUpdated = true; + } + + // configurationFileFormat.Version == 19 -> LDN2 + + if (configurationFileFormat.Version < 20) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 20."); + + configurationFileFormat.ShowConfirmExit = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 21) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 21."); + + // Initialize network config. + + configurationFileFormat.MultiplayerMode = MultiplayerMode.Disabled; + configurationFileFormat.MultiplayerLanInterfaceId = "0"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 22) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22."); + + configurationFileFormat.HideCursor = HideCursorMode.Never; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 24) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 24."); + + configurationFileFormat.InputConfig = new List + { + new StandardKeyboardInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.WindowKeyboard, + Id = "0", + PlayerIndex = PlayerIndex.Player1, + 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, + }, + }, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 25) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25."); + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 26) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 26."); + + configurationFileFormat.MemoryManagerMode = MemoryManagerMode.HostMappedUnsafe; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 27) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 27."); + + configurationFileFormat.EnableMouse = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 28) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 28."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = Key.F1, + Screenshot = Key.F8, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 29) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 29."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = Key.F1, + Screenshot = Key.F8, + ShowUI = Key.F4, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 30) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30."); + + foreach (InputConfig config in configurationFileFormat.InputConfig) + { + if (config is StandardControllerInputConfig controllerConfig) + { + controllerConfig.Rumble = new RumbleConfigController + { + EnableRumble = false, + StrongRumble = 1f, + WeakRumble = 1f, + }; + } + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 31) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 31."); + + configurationFileFormat.BackendThreading = BackendThreading.Auto; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 32) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 32."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = Key.F5, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 33) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 33."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = Key.F2, + }; + + configurationFileFormat.AudioVolume = 1; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 34) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 34."); + + configurationFileFormat.EnableInternetAccess = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 35) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 35."); + + foreach (InputConfig config in configurationFileFormat.InputConfig) + { + if (config is StandardControllerInputConfig controllerConfig) + { + controllerConfig.RangeLeft = 1.0f; + controllerConfig.RangeRight = 1.0f; + } + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 36) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 36."); + + configurationFileFormat.LoggingEnableTrace = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 37) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 37."); + + configurationFileFormat.ShowConsole = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 38) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 38."); + + configurationFileFormat.BaseStyle = "Dark"; + configurationFileFormat.GameListViewMode = 0; + configurationFileFormat.ShowNames = true; + configurationFileFormat.GridSize = 2; + configurationFileFormat.LanguageCode = "en_US"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 39) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 39."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = Key.Unbound, + ResScaleDown = Key.Unbound, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 40) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 40."); + + configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 41) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 41."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, + ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, + VolumeUp = Key.Unbound, + VolumeDown = Key.Unbound, + }; + } + + if (configurationFileFormat.Version < 42) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 42."); + + configurationFileFormat.EnableMacroHLE = true; + } + + if (configurationFileFormat.Version < 43) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 43."); + + configurationFileFormat.UseHypervisor = true; + } + + if (configurationFileFormat.Version < 44) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 44."); + + configurationFileFormat.AntiAliasing = AntiAliasing.None; + configurationFileFormat.ScalingFilter = ScalingFilter.Bilinear; + configurationFileFormat.ScalingFilterLevel = 80; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 45) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 45."); + + configurationFileFormat.ShownFileTypes = new ShownFileTypes + { + NSP = true, + PFS0 = true, + XCI = true, + NCA = true, + NRO = true, + NSO = true, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 46) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 46."); + + configurationFileFormat.MultiplayerLanInterfaceId = "0"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 47) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 47."); + + configurationFileFormat.WindowStartup = new WindowStartup + { + WindowPositionX = 0, + WindowPositionY = 0, + WindowSizeHeight = 760, + WindowSizeWidth = 1280, + WindowMaximized = false, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 48) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 48."); + + configurationFileFormat.EnableColorSpacePassthrough = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 49) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 49."); + + if (OperatingSystem.IsMacOS()) + { + AppDataManager.FixMacOSConfigurationFolders(); + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 50) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 50."); + + configurationFileFormat.EnableHardwareAcceleration = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 51) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 51."); + + configurationFileFormat.RememberWindowState = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 52) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52."); + + configurationFileFormat.AutoloadDirs = []; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 53) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 53."); + + configurationFileFormat.EnableLowPowerPtc = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 54) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 54."); + + configurationFileFormat.DramSize = MemoryConfiguration.MemoryConfiguration4GiB; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 55) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 55."); + + configurationFileFormat.IgnoreApplet = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 56) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 56."); + + configurationFileFormat.ShowTitleBar = !OperatingSystem.IsWindows(); + + configurationFileUpdated = true; + } + + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; + Graphics.ResScale.Value = configurationFileFormat.ResScale; + Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; + Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy; + Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio; + Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath; + Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; + Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend; + Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu; + Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing; + Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter; + Graphics.ScalingFilterLevel.Value = configurationFileFormat.ScalingFilterLevel; + Logger.EnableDebug.Value = configurationFileFormat.LoggingEnableDebug; + Logger.EnableStub.Value = configurationFileFormat.LoggingEnableStub; + Logger.EnableInfo.Value = configurationFileFormat.LoggingEnableInfo; + Logger.EnableWarn.Value = configurationFileFormat.LoggingEnableWarn; + Logger.EnableError.Value = configurationFileFormat.LoggingEnableError; + Logger.EnableTrace.Value = configurationFileFormat.LoggingEnableTrace; + Logger.EnableGuest.Value = configurationFileFormat.LoggingEnableGuest; + Logger.EnableFsAccessLog.Value = configurationFileFormat.LoggingEnableFsAccessLog; + Logger.FilteredClasses.Value = configurationFileFormat.LoggingFilteredClasses; + Logger.GraphicsDebugLevel.Value = configurationFileFormat.LoggingGraphicsDebugLevel; + System.Language.Value = configurationFileFormat.SystemLanguage; + System.Region.Value = configurationFileFormat.SystemRegion; + System.TimeZone.Value = configurationFileFormat.SystemTimeZone; + System.SystemTimeOffset.Value = configurationFileFormat.SystemTimeOffset; + System.EnableDockedMode.Value = configurationFileFormat.DockedMode; + EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration; + CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; + ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; + IgnoreApplet.Value = configurationFileFormat.IgnoreApplet; + RememberWindowState.Value = configurationFileFormat.RememberWindowState; + ShowTitleBar.Value = configurationFileFormat.ShowTitleBar; + EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration; + HideCursor.Value = configurationFileFormat.HideCursor; + Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; + Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; + Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; + Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE; + Graphics.EnableColorSpacePassthrough.Value = configurationFileFormat.EnableColorSpacePassthrough; + System.EnablePtc.Value = configurationFileFormat.EnablePtc; + System.EnableLowPowerPtc.Value = configurationFileFormat.EnableLowPowerPtc; + System.EnableInternetAccess.Value = configurationFileFormat.EnableInternetAccess; + System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks; + System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode; + System.AudioBackend.Value = configurationFileFormat.AudioBackend; + System.AudioVolume.Value = configurationFileFormat.AudioVolume; + System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode; + System.DramSize.Value = configurationFileFormat.DramSize; + System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices; + System.UseHypervisor.Value = configurationFileFormat.UseHypervisor; + UI.GuiColumns.FavColumn.Value = configurationFileFormat.GuiColumns.FavColumn; + UI.GuiColumns.IconColumn.Value = configurationFileFormat.GuiColumns.IconColumn; + UI.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn; + UI.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn; + UI.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn; + UI.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn; + UI.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn; + UI.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn; + UI.GuiColumns.FileSizeColumn.Value = configurationFileFormat.GuiColumns.FileSizeColumn; + UI.GuiColumns.PathColumn.Value = configurationFileFormat.GuiColumns.PathColumn; + UI.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId; + UI.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending; + UI.GameDirs.Value = configurationFileFormat.GameDirs; + UI.AutoloadDirs.Value = configurationFileFormat.AutoloadDirs ?? []; + UI.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP; + UI.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0; + UI.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI; + UI.ShownFileTypes.NCA.Value = configurationFileFormat.ShownFileTypes.NCA; + UI.ShownFileTypes.NRO.Value = configurationFileFormat.ShownFileTypes.NRO; + UI.ShownFileTypes.NSO.Value = configurationFileFormat.ShownFileTypes.NSO; + UI.LanguageCode.Value = configurationFileFormat.LanguageCode; + UI.BaseStyle.Value = configurationFileFormat.BaseStyle; + UI.GameListViewMode.Value = configurationFileFormat.GameListViewMode; + UI.ShowNames.Value = configurationFileFormat.ShowNames; + UI.IsAscendingOrder.Value = configurationFileFormat.IsAscendingOrder; + UI.GridSize.Value = configurationFileFormat.GridSize; + UI.ApplicationSort.Value = configurationFileFormat.ApplicationSort; + UI.StartFullscreen.Value = configurationFileFormat.StartFullscreen; + UI.ShowConsole.Value = configurationFileFormat.ShowConsole; + UI.WindowStartup.WindowSizeWidth.Value = configurationFileFormat.WindowStartup.WindowSizeWidth; + UI.WindowStartup.WindowSizeHeight.Value = configurationFileFormat.WindowStartup.WindowSizeHeight; + UI.WindowStartup.WindowPositionX.Value = configurationFileFormat.WindowStartup.WindowPositionX; + UI.WindowStartup.WindowPositionY.Value = configurationFileFormat.WindowStartup.WindowPositionY; + UI.WindowStartup.WindowMaximized.Value = configurationFileFormat.WindowStartup.WindowMaximized; + Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard; + Hid.EnableMouse.Value = configurationFileFormat.EnableMouse; + Hid.Hotkeys.Value = configurationFileFormat.Hotkeys; + Hid.InputConfig.Value = configurationFileFormat.InputConfig ?? []; + + Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId; + Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode; + + if (configurationFileUpdated) + { + ToFileFormat().SaveConfig(configurationFilePath); + + Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}"); + } + } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs new file mode 100644 index 000000000..3b9ffb5d3 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs @@ -0,0 +1,674 @@ +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.Logging; +using Ryujinx.HLE; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Helper; +using System.Collections.Generic; + +namespace Ryujinx.UI.Common.Configuration +{ + public partial class ConfigurationState + { + /// + /// UI configuration section + /// + public class UISection + { + public class Columns + { + public ReactiveObject FavColumn { get; private set; } + public ReactiveObject IconColumn { get; private set; } + public ReactiveObject AppColumn { get; private set; } + public ReactiveObject DevColumn { get; private set; } + public ReactiveObject VersionColumn { get; private set; } + public ReactiveObject TimePlayedColumn { get; private set; } + public ReactiveObject LastPlayedColumn { get; private set; } + public ReactiveObject FileExtColumn { get; private set; } + public ReactiveObject FileSizeColumn { get; private set; } + public ReactiveObject PathColumn { get; private set; } + + public Columns() + { + FavColumn = new ReactiveObject(); + IconColumn = new ReactiveObject(); + AppColumn = new ReactiveObject(); + DevColumn = new ReactiveObject(); + VersionColumn = new ReactiveObject(); + TimePlayedColumn = new ReactiveObject(); + LastPlayedColumn = new ReactiveObject(); + FileExtColumn = new ReactiveObject(); + FileSizeColumn = new ReactiveObject(); + PathColumn = new ReactiveObject(); + } + } + + public class ColumnSortSettings + { + public ReactiveObject SortColumnId { get; private set; } + public ReactiveObject SortAscending { get; private set; } + + public ColumnSortSettings() + { + SortColumnId = new ReactiveObject(); + SortAscending = new ReactiveObject(); + } + } + + /// + /// Used to toggle which file types are shown in the UI + /// + public class ShownFileTypeSettings + { + public ReactiveObject NSP { get; private set; } + public ReactiveObject PFS0 { get; private set; } + public ReactiveObject XCI { get; private set; } + public ReactiveObject NCA { get; private set; } + public ReactiveObject NRO { get; private set; } + public ReactiveObject NSO { get; private set; } + + public ShownFileTypeSettings() + { + NSP = new ReactiveObject(); + PFS0 = new ReactiveObject(); + XCI = new ReactiveObject(); + NCA = new ReactiveObject(); + NRO = new ReactiveObject(); + NSO = new ReactiveObject(); + } + } + + // + /// Determines main window start-up position, size and state + /// + public class WindowStartupSettings + { + public ReactiveObject WindowSizeWidth { get; private set; } + public ReactiveObject WindowSizeHeight { get; private set; } + public ReactiveObject WindowPositionX { get; private set; } + public ReactiveObject WindowPositionY { get; private set; } + public ReactiveObject WindowMaximized { get; private set; } + + public WindowStartupSettings() + { + WindowSizeWidth = new ReactiveObject(); + WindowSizeHeight = new ReactiveObject(); + WindowPositionX = new ReactiveObject(); + WindowPositionY = new ReactiveObject(); + WindowMaximized = new ReactiveObject(); + } + } + + /// + /// Used to toggle columns in the GUI + /// + public Columns GuiColumns { get; private set; } + + /// + /// Used to configure column sort settings in the GUI + /// + public ColumnSortSettings ColumnSort { get; private set; } + + /// + /// A list of directories containing games to be used to load games into the games list + /// + public ReactiveObject> GameDirs { get; private set; } + + /// + /// A list of directories containing DLC/updates the user wants to autoload during library refreshes + /// + public ReactiveObject> AutoloadDirs { get; private set; } + + /// + /// A list of file types to be hidden in the games List + /// + public ShownFileTypeSettings ShownFileTypes { get; private set; } + + /// + /// Determines main window start-up position, size and state + /// + public WindowStartupSettings WindowStartup { get; private set; } + + /// + /// Language Code for the UI + /// + public ReactiveObject LanguageCode { get; private set; } + + /// + /// Selects the base style + /// + public ReactiveObject BaseStyle { get; private set; } + + /// + /// Start games in fullscreen mode + /// + public ReactiveObject StartFullscreen { get; private set; } + + /// + /// Hide / Show Console Window + /// + public ReactiveObject ShowConsole { get; private set; } + + /// + /// View Mode of the Game list + /// + public ReactiveObject GameListViewMode { get; private set; } + + /// + /// Show application name in Grid Mode + /// + public ReactiveObject ShowNames { get; private set; } + + /// + /// Sets App Icon Size in Grid Mode + /// + public ReactiveObject GridSize { get; private set; } + + /// + /// Sorts Apps in Grid Mode + /// + public ReactiveObject ApplicationSort { get; private set; } + + /// + /// Sets if Grid is ordered in Ascending Order + /// + public ReactiveObject IsAscendingOrder { get; private set; } + + public UISection() + { + GuiColumns = new Columns(); + ColumnSort = new ColumnSortSettings(); + GameDirs = new ReactiveObject>(); + AutoloadDirs = new ReactiveObject>(); + ShownFileTypes = new ShownFileTypeSettings(); + WindowStartup = new WindowStartupSettings(); + BaseStyle = new ReactiveObject(); + StartFullscreen = new ReactiveObject(); + GameListViewMode = new ReactiveObject(); + ShowNames = new ReactiveObject(); + GridSize = new ReactiveObject(); + ApplicationSort = new ReactiveObject(); + IsAscendingOrder = new ReactiveObject(); + LanguageCode = new ReactiveObject(); + ShowConsole = new ReactiveObject(); + ShowConsole.Event += static (_, e) => ConsoleHelper.SetConsoleWindowState(e.NewValue); + } + } + + /// + /// Logger configuration section + /// + public class LoggerSection + { + /// + /// Enables printing debug log messages + /// + public ReactiveObject EnableDebug { get; private set; } + + /// + /// Enables printing stub log messages + /// + public ReactiveObject EnableStub { get; private set; } + + /// + /// Enables printing info log messages + /// + public ReactiveObject EnableInfo { get; private set; } + + /// + /// Enables printing warning log messages + /// + public ReactiveObject EnableWarn { get; private set; } + + /// + /// Enables printing error log messages + /// + public ReactiveObject EnableError { get; private set; } + + /// + /// Enables printing trace log messages + /// + public ReactiveObject EnableTrace { get; private set; } + + /// + /// Enables printing guest log messages + /// + public ReactiveObject EnableGuest { get; private set; } + + /// + /// Enables printing FS access log messages + /// + public ReactiveObject EnableFsAccessLog { get; private set; } + + /// + /// Controls which log messages are written to the log targets + /// + public ReactiveObject FilteredClasses { get; private set; } + + /// + /// Enables or disables logging to a file on disk + /// + public ReactiveObject EnableFileLog { get; private set; } + + /// + /// Controls which OpenGL log messages are recorded in the log + /// + public ReactiveObject GraphicsDebugLevel { get; private set; } + + public LoggerSection() + { + EnableDebug = new ReactiveObject(); + EnableDebug.LogChangesToValue(nameof(EnableDebug)); + EnableStub = new ReactiveObject(); + EnableInfo = new ReactiveObject(); + EnableWarn = new ReactiveObject(); + EnableError = new ReactiveObject(); + EnableTrace = new ReactiveObject(); + EnableGuest = new ReactiveObject(); + EnableFsAccessLog = new ReactiveObject(); + FilteredClasses = new ReactiveObject(); + EnableFileLog = new ReactiveObject(); + EnableFileLog.LogChangesToValue(nameof(EnableFileLog)); + GraphicsDebugLevel = new ReactiveObject(); + } + } + + /// + /// System configuration section + /// + public class SystemSection + { + /// + /// Change System Language + /// + public ReactiveObject Language { get; private set; } + + /// + /// Change System Region + /// + public ReactiveObject Region { get; private set; } + + /// + /// Change System TimeZone + /// + public ReactiveObject TimeZone { get; private set; } + + /// + /// System Time Offset in Seconds + /// + public ReactiveObject SystemTimeOffset { get; private set; } + + /// + /// Enables or disables Docked Mode + /// + public ReactiveObject EnableDockedMode { get; private set; } + + /// + /// Enables or disables persistent profiled translation cache + /// + public ReactiveObject EnablePtc { get; private set; } + + /// + /// Enables or disables low-power persistent profiled translation cache loading + /// + public ReactiveObject EnableLowPowerPtc { get; private set; } + + /// + /// Enables or disables guest Internet access + /// + public ReactiveObject EnableInternetAccess { get; private set; } + + /// + /// Enables integrity checks on Game content files + /// + public ReactiveObject EnableFsIntegrityChecks { get; private set; } + + /// + /// Enables FS access log output to the console. Possible modes are 0-3 + /// + public ReactiveObject FsGlobalAccessLogMode { get; private set; } + + /// + /// The selected audio backend + /// + public ReactiveObject AudioBackend { get; private set; } + + /// + /// The audio backend volume + /// + public ReactiveObject AudioVolume { get; private set; } + + /// + /// The selected memory manager mode + /// + public ReactiveObject MemoryManagerMode { get; private set; } + + /// + /// Defines the amount of RAM available on the emulated system, and how it is distributed + /// + public ReactiveObject DramSize { get; private set; } + + /// + /// Enable or disable ignoring missing services + /// + public ReactiveObject IgnoreMissingServices { get; private set; } + + /// + /// Uses Hypervisor over JIT if available + /// + public ReactiveObject UseHypervisor { get; private set; } + + public SystemSection() + { + Language = new ReactiveObject(); + Language.LogChangesToValue(nameof(Language)); + Region = new ReactiveObject(); + Region.LogChangesToValue(nameof(Region)); + TimeZone = new ReactiveObject(); + TimeZone.LogChangesToValue(nameof(TimeZone)); + SystemTimeOffset = new ReactiveObject(); + SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset)); + EnableDockedMode = new ReactiveObject(); + EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode)); + EnablePtc = new ReactiveObject(); + EnablePtc.LogChangesToValue(nameof(EnablePtc)); + EnableLowPowerPtc = new ReactiveObject(); + EnableLowPowerPtc.LogChangesToValue(nameof(EnableLowPowerPtc)); + EnableInternetAccess = new ReactiveObject(); + EnableInternetAccess.LogChangesToValue(nameof(EnableInternetAccess)); + EnableFsIntegrityChecks = new ReactiveObject(); + EnableFsIntegrityChecks.LogChangesToValue(nameof(EnableFsIntegrityChecks)); + FsGlobalAccessLogMode = new ReactiveObject(); + FsGlobalAccessLogMode.LogChangesToValue(nameof(FsGlobalAccessLogMode)); + AudioBackend = new ReactiveObject(); + AudioBackend.LogChangesToValue(nameof(AudioBackend)); + MemoryManagerMode = new ReactiveObject(); + MemoryManagerMode.LogChangesToValue(nameof(MemoryManagerMode)); + DramSize = new ReactiveObject(); + DramSize.LogChangesToValue(nameof(DramSize)); + IgnoreMissingServices = new ReactiveObject(); + IgnoreMissingServices.LogChangesToValue(nameof(IgnoreMissingServices)); + AudioVolume = new ReactiveObject(); + AudioVolume.LogChangesToValue(nameof(AudioVolume)); + UseHypervisor = new ReactiveObject(); + UseHypervisor.LogChangesToValue(nameof(UseHypervisor)); + } + } + + /// + /// Hid configuration section + /// + public class HidSection + { + /// + /// Enable or disable keyboard support (Independent from controllers binding) + /// + public ReactiveObject EnableKeyboard { get; private set; } + + /// + /// Enable or disable mouse support (Independent from controllers binding) + /// + public ReactiveObject EnableMouse { get; private set; } + + /// + /// Hotkey Keyboard Bindings + /// + public ReactiveObject Hotkeys { get; private set; } + + /// + /// Input device configuration. + /// NOTE: This ReactiveObject won't issue an event when the List has elements added or removed. + /// TODO: Implement a ReactiveList class. + /// + public ReactiveObject> InputConfig { get; private set; } + + public HidSection() + { + EnableKeyboard = new ReactiveObject(); + EnableMouse = new ReactiveObject(); + Hotkeys = new ReactiveObject(); + InputConfig = new ReactiveObject>(); + } + } + + /// + /// Graphics configuration section + /// + public class GraphicsSection + { + /// + /// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime. + /// + public ReactiveObject BackendThreading { get; private set; } + + /// + /// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide. + /// + public ReactiveObject MaxAnisotropy { get; private set; } + + /// + /// Aspect Ratio applied to the renderer window. + /// + public ReactiveObject AspectRatio { get; private set; } + + /// + /// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead. + /// + public ReactiveObject ResScale { get; private set; } + + /// + /// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1. + /// + public ReactiveObject ResScaleCustom { get; private set; } + + /// + /// Dumps shaders in this local directory + /// + public ReactiveObject ShadersDumpPath { get; private set; } + + /// + /// Enables or disables Vertical Sync + /// + public ReactiveObject EnableVsync { get; private set; } + + /// + /// Enables or disables Shader cache + /// + public ReactiveObject EnableShaderCache { get; private set; } + + /// + /// Enables or disables texture recompression + /// + public ReactiveObject EnableTextureRecompression { get; private set; } + + /// + /// Enables or disables Macro high-level emulation + /// + public ReactiveObject EnableMacroHLE { get; private set; } + + /// + /// Enables or disables color space passthrough, if available. + /// + public ReactiveObject EnableColorSpacePassthrough { get; private set; } + + /// + /// Graphics backend + /// + public ReactiveObject GraphicsBackend { get; private set; } + + /// + /// Applies anti-aliasing to the renderer. + /// + public ReactiveObject AntiAliasing { get; private set; } + + /// + /// Sets the framebuffer upscaling type. + /// + public ReactiveObject ScalingFilter { get; private set; } + + /// + /// Sets the framebuffer upscaling level. + /// + public ReactiveObject ScalingFilterLevel { get; private set; } + + /// + /// Preferred GPU + /// + public ReactiveObject PreferredGpu { get; private set; } + + public GraphicsSection() + { + BackendThreading = new ReactiveObject(); + BackendThreading.LogChangesToValue(nameof(BackendThreading)); + ResScale = new ReactiveObject(); + ResScale.LogChangesToValue(nameof(ResScale)); + ResScaleCustom = new ReactiveObject(); + ResScaleCustom.LogChangesToValue(nameof(ResScaleCustom)); + MaxAnisotropy = new ReactiveObject(); + MaxAnisotropy.LogChangesToValue(nameof(MaxAnisotropy)); + AspectRatio = new ReactiveObject(); + AspectRatio.LogChangesToValue(nameof(AspectRatio)); + ShadersDumpPath = new ReactiveObject(); + EnableVsync = new ReactiveObject(); + EnableVsync.LogChangesToValue(nameof(EnableVsync)); + EnableShaderCache = new ReactiveObject(); + EnableShaderCache.LogChangesToValue(nameof(EnableShaderCache)); + EnableTextureRecompression = new ReactiveObject(); + EnableTextureRecompression.LogChangesToValue(nameof(EnableTextureRecompression)); + GraphicsBackend = new ReactiveObject(); + GraphicsBackend.LogChangesToValue(nameof(GraphicsBackend)); + PreferredGpu = new ReactiveObject(); + PreferredGpu.LogChangesToValue(nameof(PreferredGpu)); + EnableMacroHLE = new ReactiveObject(); + EnableMacroHLE.LogChangesToValue(nameof(EnableMacroHLE)); + EnableColorSpacePassthrough = new ReactiveObject(); + EnableColorSpacePassthrough.LogChangesToValue(nameof(EnableColorSpacePassthrough)); + AntiAliasing = new ReactiveObject(); + AntiAliasing.LogChangesToValue(nameof(AntiAliasing)); + ScalingFilter = new ReactiveObject(); + ScalingFilter.LogChangesToValue(nameof(ScalingFilter)); + ScalingFilterLevel = new ReactiveObject(); + ScalingFilterLevel.LogChangesToValue(nameof(ScalingFilterLevel)); + } + } + + /// + /// Multiplayer configuration section + /// + public class MultiplayerSection + { + /// + /// GUID for the network interface used by LAN (or 0 for default) + /// + public ReactiveObject LanInterfaceId { get; private set; } + + /// + /// Multiplayer Mode + /// + public ReactiveObject Mode { get; private set; } + + public MultiplayerSection() + { + LanInterfaceId = new ReactiveObject(); + Mode = new ReactiveObject(); + Mode.LogChangesToValue(nameof(MultiplayerMode)); + } + } + + /// + /// The default configuration instance + /// + public static ConfigurationState Instance { get; private set; } + + /// + /// The UI section + /// + public UISection UI { get; private set; } + + /// + /// The Logger section + /// + public LoggerSection Logger { get; private set; } + + /// + /// The System section + /// + public SystemSection System { get; private set; } + + /// + /// The Graphics section + /// + public GraphicsSection Graphics { get; private set; } + + /// + /// The Hid section + /// + public HidSection Hid { get; private set; } + + /// + /// The Multiplayer section + /// + public MultiplayerSection Multiplayer { get; private set; } + + /// + /// Enables or disables Discord Rich Presence + /// + public ReactiveObject EnableDiscordIntegration { get; private set; } + + /// + /// Checks for updates when Ryujinx starts when enabled + /// + public ReactiveObject CheckUpdatesOnStart { get; private set; } + + /// + /// Show "Confirm Exit" Dialog + /// + public ReactiveObject ShowConfirmExit { get; private set; } + + /// + /// Ignore Applet + /// + public ReactiveObject IgnoreApplet { get; private set; } + + /// + /// Enables or disables save window size, position and state on close. + /// + public ReactiveObject RememberWindowState { get; private set; } + + /// + /// Enables or disables the redesigned title bar + /// + public ReactiveObject ShowTitleBar { get; private set; } + + /// + /// Enables hardware-accelerated rendering for Avalonia + /// + public ReactiveObject EnableHardwareAcceleration { get; private set; } + + /// + /// Hide Cursor on Idle + /// + public ReactiveObject HideCursor { get; private set; } + + private ConfigurationState() + { + UI = new UISection(); + Logger = new LoggerSection(); + System = new SystemSection(); + Graphics = new GraphicsSection(); + Hid = new HidSection(); + Multiplayer = new MultiplayerSection(); + EnableDiscordIntegration = new ReactiveObject(); + CheckUpdatesOnStart = new ReactiveObject(); + ShowConfirmExit = new ReactiveObject(); + IgnoreApplet = new ReactiveObject(); + IgnoreApplet.LogChangesToValue(nameof(IgnoreApplet)); + RememberWindowState = new ReactiveObject(); + ShowTitleBar = new ReactiveObject(); + EnableHardwareAcceleration = new ReactiveObject(); + HideCursor = new ReactiveObject(); + } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index c0abb372a..5023e48c0 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -1,5 +1,4 @@ using ARMeilleure; -using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; @@ -10,673 +9,25 @@ using Ryujinx.Graphics.Vulkan; using Ryujinx.HLE; using Ryujinx.UI.Common.Configuration.System; using Ryujinx.UI.Common.Configuration.UI; -using Ryujinx.UI.Common.Helper; using System; using System.Collections.Generic; namespace Ryujinx.UI.Common.Configuration { - public class ConfigurationState + public partial class ConfigurationState { - /// - /// UI configuration section - /// - public class UISection + public static void Initialize() { - public class Columns + if (Instance != null) { - public ReactiveObject FavColumn { get; private set; } - public ReactiveObject IconColumn { get; private set; } - public ReactiveObject AppColumn { get; private set; } - public ReactiveObject DevColumn { get; private set; } - public ReactiveObject VersionColumn { get; private set; } - public ReactiveObject TimePlayedColumn { get; private set; } - public ReactiveObject LastPlayedColumn { get; private set; } - public ReactiveObject FileExtColumn { get; private set; } - public ReactiveObject FileSizeColumn { get; private set; } - public ReactiveObject PathColumn { get; private set; } - - public Columns() - { - FavColumn = new ReactiveObject(); - IconColumn = new ReactiveObject(); - AppColumn = new ReactiveObject(); - DevColumn = new ReactiveObject(); - VersionColumn = new ReactiveObject(); - TimePlayedColumn = new ReactiveObject(); - LastPlayedColumn = new ReactiveObject(); - FileExtColumn = new ReactiveObject(); - FileSizeColumn = new ReactiveObject(); - PathColumn = new ReactiveObject(); - } + throw new InvalidOperationException("Configuration is already initialized"); } - public class ColumnSortSettings - { - public ReactiveObject SortColumnId { get; private set; } - public ReactiveObject SortAscending { get; private set; } + Instance = new ConfigurationState(); - public ColumnSortSettings() - { - SortColumnId = new ReactiveObject(); - SortAscending = new ReactiveObject(); - } - } - - /// - /// Used to toggle which file types are shown in the UI - /// - public class ShownFileTypeSettings - { - public ReactiveObject NSP { get; private set; } - public ReactiveObject PFS0 { get; private set; } - public ReactiveObject XCI { get; private set; } - public ReactiveObject NCA { get; private set; } - public ReactiveObject NRO { get; private set; } - public ReactiveObject NSO { get; private set; } - - public ShownFileTypeSettings() - { - NSP = new ReactiveObject(); - PFS0 = new ReactiveObject(); - XCI = new ReactiveObject(); - NCA = new ReactiveObject(); - NRO = new ReactiveObject(); - NSO = new ReactiveObject(); - } - } - - // - /// Determines main window start-up position, size and state - /// - public class WindowStartupSettings - { - public ReactiveObject WindowSizeWidth { get; private set; } - public ReactiveObject WindowSizeHeight { get; private set; } - public ReactiveObject WindowPositionX { get; private set; } - public ReactiveObject WindowPositionY { get; private set; } - public ReactiveObject WindowMaximized { get; private set; } - - public WindowStartupSettings() - { - WindowSizeWidth = new ReactiveObject(); - WindowSizeHeight = new ReactiveObject(); - WindowPositionX = new ReactiveObject(); - WindowPositionY = new ReactiveObject(); - WindowMaximized = new ReactiveObject(); - } - } - - /// - /// Used to toggle columns in the GUI - /// - public Columns GuiColumns { get; private set; } - - /// - /// Used to configure column sort settings in the GUI - /// - public ColumnSortSettings ColumnSort { get; private set; } - - /// - /// A list of directories containing games to be used to load games into the games list - /// - public ReactiveObject> GameDirs { get; private set; } - - /// - /// A list of directories containing DLC/updates the user wants to autoload during library refreshes - /// - public ReactiveObject> AutoloadDirs { get; private set; } - - /// - /// A list of file types to be hidden in the games List - /// - public ShownFileTypeSettings ShownFileTypes { get; private set; } - - /// - /// Determines main window start-up position, size and state - /// - public WindowStartupSettings WindowStartup { get; private set; } - - /// - /// Language Code for the UI - /// - public ReactiveObject LanguageCode { get; private set; } - - /// - /// Selects the base style - /// - public ReactiveObject BaseStyle { get; private set; } - - /// - /// Start games in fullscreen mode - /// - public ReactiveObject StartFullscreen { get; private set; } - - /// - /// Hide / Show Console Window - /// - public ReactiveObject ShowConsole { get; private set; } - - /// - /// View Mode of the Game list - /// - public ReactiveObject GameListViewMode { get; private set; } - - /// - /// Show application name in Grid Mode - /// - public ReactiveObject ShowNames { get; private set; } - - /// - /// Sets App Icon Size in Grid Mode - /// - public ReactiveObject GridSize { get; private set; } - - /// - /// Sorts Apps in Grid Mode - /// - public ReactiveObject ApplicationSort { get; private set; } - - /// - /// Sets if Grid is ordered in Ascending Order - /// - public ReactiveObject IsAscendingOrder { get; private set; } - - public UISection() - { - GuiColumns = new Columns(); - ColumnSort = new ColumnSortSettings(); - GameDirs = new ReactiveObject>(); - AutoloadDirs = new ReactiveObject>(); - ShownFileTypes = new ShownFileTypeSettings(); - WindowStartup = new WindowStartupSettings(); - BaseStyle = new ReactiveObject(); - StartFullscreen = new ReactiveObject(); - GameListViewMode = new ReactiveObject(); - ShowNames = new ReactiveObject(); - GridSize = new ReactiveObject(); - ApplicationSort = new ReactiveObject(); - IsAscendingOrder = new ReactiveObject(); - LanguageCode = new ReactiveObject(); - ShowConsole = new ReactiveObject(); - ShowConsole.Event += static (_, e) => ConsoleHelper.SetConsoleWindowState(e.NewValue); - } + Instance.System.EnableLowPowerPtc.Event += (_, evnt) => Optimizations.LowPower = evnt.NewValue; } - - /// - /// Logger configuration section - /// - public class LoggerSection - { - /// - /// Enables printing debug log messages - /// - public ReactiveObject EnableDebug { get; private set; } - - /// - /// Enables printing stub log messages - /// - public ReactiveObject EnableStub { get; private set; } - - /// - /// Enables printing info log messages - /// - public ReactiveObject EnableInfo { get; private set; } - - /// - /// Enables printing warning log messages - /// - public ReactiveObject EnableWarn { get; private set; } - - /// - /// Enables printing error log messages - /// - public ReactiveObject EnableError { get; private set; } - - /// - /// Enables printing trace log messages - /// - public ReactiveObject EnableTrace { get; private set; } - - /// - /// Enables printing guest log messages - /// - public ReactiveObject EnableGuest { get; private set; } - - /// - /// Enables printing FS access log messages - /// - public ReactiveObject EnableFsAccessLog { get; private set; } - - /// - /// Controls which log messages are written to the log targets - /// - public ReactiveObject FilteredClasses { get; private set; } - - /// - /// Enables or disables logging to a file on disk - /// - public ReactiveObject EnableFileLog { get; private set; } - - /// - /// Controls which OpenGL log messages are recorded in the log - /// - public ReactiveObject GraphicsDebugLevel { get; private set; } - - public LoggerSection() - { - EnableDebug = new ReactiveObject(); - EnableDebug.LogChangesToValue(nameof(EnableDebug)); - EnableStub = new ReactiveObject(); - EnableInfo = new ReactiveObject(); - EnableWarn = new ReactiveObject(); - EnableError = new ReactiveObject(); - EnableTrace = new ReactiveObject(); - EnableGuest = new ReactiveObject(); - EnableFsAccessLog = new ReactiveObject(); - FilteredClasses = new ReactiveObject(); - EnableFileLog = new ReactiveObject(); - EnableFileLog.LogChangesToValue(nameof(EnableFileLog)); - GraphicsDebugLevel = new ReactiveObject(); - } - } - - /// - /// System configuration section - /// - public class SystemSection - { - /// - /// Change System Language - /// - public ReactiveObject Language { get; private set; } - - /// - /// Change System Region - /// - public ReactiveObject Region { get; private set; } - - /// - /// Change System TimeZone - /// - public ReactiveObject TimeZone { get; private set; } - - /// - /// System Time Offset in Seconds - /// - public ReactiveObject SystemTimeOffset { get; private set; } - - /// - /// Enables or disables Docked Mode - /// - public ReactiveObject EnableDockedMode { get; private set; } - - /// - /// Enables or disables persistent profiled translation cache - /// - public ReactiveObject EnablePtc { get; private set; } - - /// - /// Enables or disables low-power persistent profiled translation cache loading - /// - public ReactiveObject EnableLowPowerPtc { get; private set; } - - /// - /// Enables or disables guest Internet access - /// - public ReactiveObject EnableInternetAccess { get; private set; } - - /// - /// Enables integrity checks on Game content files - /// - public ReactiveObject EnableFsIntegrityChecks { get; private set; } - - /// - /// Enables FS access log output to the console. Possible modes are 0-3 - /// - public ReactiveObject FsGlobalAccessLogMode { get; private set; } - - /// - /// The selected audio backend - /// - public ReactiveObject AudioBackend { get; private set; } - - /// - /// The audio backend volume - /// - public ReactiveObject AudioVolume { get; private set; } - - /// - /// The selected memory manager mode - /// - public ReactiveObject MemoryManagerMode { get; private set; } - - /// - /// Defines the amount of RAM available on the emulated system, and how it is distributed - /// - public ReactiveObject DramSize { get; private set; } - - /// - /// Enable or disable ignoring missing services - /// - public ReactiveObject IgnoreMissingServices { get; private set; } - - /// - /// Uses Hypervisor over JIT if available - /// - public ReactiveObject UseHypervisor { get; private set; } - - public SystemSection() - { - Language = new ReactiveObject(); - Language.LogChangesToValue(nameof(Language)); - Region = new ReactiveObject(); - Region.LogChangesToValue(nameof(Region)); - TimeZone = new ReactiveObject(); - TimeZone.LogChangesToValue(nameof(TimeZone)); - SystemTimeOffset = new ReactiveObject(); - SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset)); - EnableDockedMode = new ReactiveObject(); - EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode)); - EnablePtc = new ReactiveObject(); - EnablePtc.LogChangesToValue(nameof(EnablePtc)); - EnableLowPowerPtc = new ReactiveObject(); - EnableLowPowerPtc.LogChangesToValue(nameof(EnableLowPowerPtc)); - EnableInternetAccess = new ReactiveObject(); - EnableInternetAccess.LogChangesToValue(nameof(EnableInternetAccess)); - EnableFsIntegrityChecks = new ReactiveObject(); - EnableFsIntegrityChecks.LogChangesToValue(nameof(EnableFsIntegrityChecks)); - FsGlobalAccessLogMode = new ReactiveObject(); - FsGlobalAccessLogMode.LogChangesToValue(nameof(FsGlobalAccessLogMode)); - AudioBackend = new ReactiveObject(); - AudioBackend.LogChangesToValue(nameof(AudioBackend)); - MemoryManagerMode = new ReactiveObject(); - MemoryManagerMode.LogChangesToValue(nameof(MemoryManagerMode)); - DramSize = new ReactiveObject(); - DramSize.LogChangesToValue(nameof(DramSize)); - IgnoreMissingServices = new ReactiveObject(); - IgnoreMissingServices.LogChangesToValue(nameof(IgnoreMissingServices)); - AudioVolume = new ReactiveObject(); - AudioVolume.LogChangesToValue(nameof(AudioVolume)); - UseHypervisor = new ReactiveObject(); - UseHypervisor.LogChangesToValue(nameof(UseHypervisor)); - } - } - - /// - /// Hid configuration section - /// - public class HidSection - { - /// - /// Enable or disable keyboard support (Independent from controllers binding) - /// - public ReactiveObject EnableKeyboard { get; private set; } - - /// - /// Enable or disable mouse support (Independent from controllers binding) - /// - public ReactiveObject EnableMouse { get; private set; } - - /// - /// Hotkey Keyboard Bindings - /// - public ReactiveObject Hotkeys { get; private set; } - - /// - /// Input device configuration. - /// NOTE: This ReactiveObject won't issue an event when the List has elements added or removed. - /// TODO: Implement a ReactiveList class. - /// - public ReactiveObject> InputConfig { get; private set; } - - public HidSection() - { - EnableKeyboard = new ReactiveObject(); - EnableMouse = new ReactiveObject(); - Hotkeys = new ReactiveObject(); - InputConfig = new ReactiveObject>(); - } - } - - /// - /// Graphics configuration section - /// - public class GraphicsSection - { - /// - /// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime. - /// - public ReactiveObject BackendThreading { get; private set; } - - /// - /// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide. - /// - public ReactiveObject MaxAnisotropy { get; private set; } - - /// - /// Aspect Ratio applied to the renderer window. - /// - public ReactiveObject AspectRatio { get; private set; } - - /// - /// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead. - /// - public ReactiveObject ResScale { get; private set; } - - /// - /// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1. - /// - public ReactiveObject ResScaleCustom { get; private set; } - - /// - /// Dumps shaders in this local directory - /// - public ReactiveObject ShadersDumpPath { get; private set; } - - /// - /// Enables or disables Vertical Sync - /// - public ReactiveObject EnableVsync { get; private set; } - - /// - /// Enables or disables Shader cache - /// - public ReactiveObject EnableShaderCache { get; private set; } - - /// - /// Enables or disables texture recompression - /// - public ReactiveObject EnableTextureRecompression { get; private set; } - - /// - /// Enables or disables Macro high-level emulation - /// - public ReactiveObject EnableMacroHLE { get; private set; } - - /// - /// Enables or disables color space passthrough, if available. - /// - public ReactiveObject EnableColorSpacePassthrough { get; private set; } - - /// - /// Graphics backend - /// - public ReactiveObject GraphicsBackend { get; private set; } - - /// - /// Applies anti-aliasing to the renderer. - /// - public ReactiveObject AntiAliasing { get; private set; } - - /// - /// Sets the framebuffer upscaling type. - /// - public ReactiveObject ScalingFilter { get; private set; } - - /// - /// Sets the framebuffer upscaling level. - /// - public ReactiveObject ScalingFilterLevel { get; private set; } - - /// - /// Preferred GPU - /// - public ReactiveObject PreferredGpu { get; private set; } - - public GraphicsSection() - { - BackendThreading = new ReactiveObject(); - BackendThreading.LogChangesToValue(nameof(BackendThreading)); - ResScale = new ReactiveObject(); - ResScale.LogChangesToValue(nameof(ResScale)); - ResScaleCustom = new ReactiveObject(); - ResScaleCustom.LogChangesToValue(nameof(ResScaleCustom)); - MaxAnisotropy = new ReactiveObject(); - MaxAnisotropy.LogChangesToValue(nameof(MaxAnisotropy)); - AspectRatio = new ReactiveObject(); - AspectRatio.LogChangesToValue(nameof(AspectRatio)); - ShadersDumpPath = new ReactiveObject(); - EnableVsync = new ReactiveObject(); - EnableVsync.LogChangesToValue(nameof(EnableVsync)); - EnableShaderCache = new ReactiveObject(); - EnableShaderCache.LogChangesToValue(nameof(EnableShaderCache)); - EnableTextureRecompression = new ReactiveObject(); - EnableTextureRecompression.LogChangesToValue(nameof(EnableTextureRecompression)); - GraphicsBackend = new ReactiveObject(); - GraphicsBackend.LogChangesToValue(nameof(GraphicsBackend)); - PreferredGpu = new ReactiveObject(); - PreferredGpu.LogChangesToValue(nameof(PreferredGpu)); - EnableMacroHLE = new ReactiveObject(); - EnableMacroHLE.LogChangesToValue(nameof(EnableMacroHLE)); - EnableColorSpacePassthrough = new ReactiveObject(); - EnableColorSpacePassthrough.LogChangesToValue(nameof(EnableColorSpacePassthrough)); - AntiAliasing = new ReactiveObject(); - AntiAliasing.LogChangesToValue(nameof(AntiAliasing)); - ScalingFilter = new ReactiveObject(); - ScalingFilter.LogChangesToValue(nameof(ScalingFilter)); - ScalingFilterLevel = new ReactiveObject(); - ScalingFilterLevel.LogChangesToValue(nameof(ScalingFilterLevel)); - } - } - - /// - /// Multiplayer configuration section - /// - public class MultiplayerSection - { - /// - /// GUID for the network interface used by LAN (or 0 for default) - /// - public ReactiveObject LanInterfaceId { get; private set; } - - /// - /// Multiplayer Mode - /// - public ReactiveObject Mode { get; private set; } - - public MultiplayerSection() - { - LanInterfaceId = new ReactiveObject(); - Mode = new ReactiveObject(); - Mode.LogChangesToValue(nameof(MultiplayerMode)); - } - } - - /// - /// The default configuration instance - /// - public static ConfigurationState Instance { get; private set; } - - /// - /// The UI section - /// - public UISection UI { get; private set; } - - /// - /// The Logger section - /// - public LoggerSection Logger { get; private set; } - - /// - /// The System section - /// - public SystemSection System { get; private set; } - - /// - /// The Graphics section - /// - public GraphicsSection Graphics { get; private set; } - - /// - /// The Hid section - /// - public HidSection Hid { get; private set; } - - /// - /// The Multiplayer section - /// - public MultiplayerSection Multiplayer { get; private set; } - - /// - /// Enables or disables Discord Rich Presence - /// - public ReactiveObject EnableDiscordIntegration { get; private set; } - - /// - /// Checks for updates when Ryujinx starts when enabled - /// - public ReactiveObject CheckUpdatesOnStart { get; private set; } - - /// - /// Show "Confirm Exit" Dialog - /// - public ReactiveObject ShowConfirmExit { get; private set; } - - /// - /// Ignore Applet - /// - public ReactiveObject IgnoreApplet { get; private set; } - - /// - /// Enables or disables save window size, position and state on close. - /// - public ReactiveObject RememberWindowState { get; private set; } - - /// - /// Enables or disables the redesigned title bar - /// - public ReactiveObject ShowTitleBar { get; private set; } - - /// - /// Enables hardware-accelerated rendering for Avalonia - /// - public ReactiveObject EnableHardwareAcceleration { get; private set; } - - /// - /// Hide Cursor on Idle - /// - public ReactiveObject HideCursor { get; private set; } - - private ConfigurationState() - { - UI = new UISection(); - Logger = new LoggerSection(); - System = new SystemSection(); - Graphics = new GraphicsSection(); - Hid = new HidSection(); - Multiplayer = new MultiplayerSection(); - EnableDiscordIntegration = new ReactiveObject(); - CheckUpdatesOnStart = new ReactiveObject(); - ShowConfirmExit = new ReactiveObject(); - IgnoreApplet = new ReactiveObject(); - IgnoreApplet.LogChangesToValue(nameof(IgnoreApplet)); - RememberWindowState = new ReactiveObject(); - ShowTitleBar = new ReactiveObject(); - EnableHardwareAcceleration = new ReactiveObject(); - HideCursor = new ReactiveObject(); - } - + public ConfigurationFileFormat ToFileFormat() { ConfigurationFileFormat configurationFile = new() @@ -944,708 +295,9 @@ namespace Ryujinx.UI.Common.Configuration StickButton = Key.H, }, } - ]; } - public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) - { - bool configurationFileUpdated = false; - - if (configurationFileFormat.Version is < 0 or > ConfigurationFileFormat.CurrentVersion) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default."); - - LoadDefault(); - } - - if (configurationFileFormat.Version < 2) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2."); - - configurationFileFormat.SystemRegion = Region.USA; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 3) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 3."); - - configurationFileFormat.SystemTimeZone = "UTC"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 4) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 4."); - - configurationFileFormat.MaxAnisotropy = -1; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 5) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 5."); - - configurationFileFormat.SystemTimeOffset = 0; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 8) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 8."); - - configurationFileFormat.EnablePtc = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 9) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 9."); - - configurationFileFormat.ColumnSort = new ColumnSort - { - SortColumnId = 0, - SortAscending = false, - }; - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = Key.F1, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 10) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 10."); - - configurationFileFormat.AudioBackend = AudioBackend.OpenAl; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 11) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 11."); - - configurationFileFormat.ResScale = 1; - configurationFileFormat.ResScaleCustom = 1.0f; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 12) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 12."); - - configurationFileFormat.LoggingGraphicsDebugLevel = GraphicsDebugLevel.None; - - configurationFileUpdated = true; - } - - // configurationFileFormat.Version == 13 -> LDN1 - - if (configurationFileFormat.Version < 14) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 14."); - - configurationFileFormat.CheckUpdatesOnStart = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 16) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 16."); - - configurationFileFormat.EnableShaderCache = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 17) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 17."); - - configurationFileFormat.StartFullscreen = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 18) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 18."); - - configurationFileFormat.AspectRatio = AspectRatio.Fixed16x9; - - configurationFileUpdated = true; - } - - // configurationFileFormat.Version == 19 -> LDN2 - - if (configurationFileFormat.Version < 20) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 20."); - - configurationFileFormat.ShowConfirmExit = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 21) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 21."); - - // Initialize network config. - - configurationFileFormat.MultiplayerMode = MultiplayerMode.Disabled; - configurationFileFormat.MultiplayerLanInterfaceId = "0"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 22) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22."); - - configurationFileFormat.HideCursor = HideCursorMode.Never; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 24) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 24."); - - configurationFileFormat.InputConfig = new List - { - new StandardKeyboardInputConfig - { - Version = InputConfig.CurrentVersion, - Backend = InputBackendType.WindowKeyboard, - Id = "0", - PlayerIndex = PlayerIndex.Player1, - 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, - }, - }, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 25) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25."); - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 26) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 26."); - - configurationFileFormat.MemoryManagerMode = MemoryManagerMode.HostMappedUnsafe; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 27) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 27."); - - configurationFileFormat.EnableMouse = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 28) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 28."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = Key.F1, - Screenshot = Key.F8, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 29) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 29."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = Key.F1, - Screenshot = Key.F8, - ShowUI = Key.F4, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 30) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30."); - - foreach (InputConfig config in configurationFileFormat.InputConfig) - { - if (config is StandardControllerInputConfig controllerConfig) - { - controllerConfig.Rumble = new RumbleConfigController - { - EnableRumble = false, - StrongRumble = 1f, - WeakRumble = 1f, - }; - } - } - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 31) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 31."); - - configurationFileFormat.BackendThreading = BackendThreading.Auto; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 32) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 32."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUI = configurationFileFormat.Hotkeys.ShowUI, - Pause = Key.F5, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 33) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 33."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUI = configurationFileFormat.Hotkeys.ShowUI, - Pause = configurationFileFormat.Hotkeys.Pause, - ToggleMute = Key.F2, - }; - - configurationFileFormat.AudioVolume = 1; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 34) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 34."); - - configurationFileFormat.EnableInternetAccess = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 35) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 35."); - - foreach (InputConfig config in configurationFileFormat.InputConfig) - { - if (config is StandardControllerInputConfig controllerConfig) - { - controllerConfig.RangeLeft = 1.0f; - controllerConfig.RangeRight = 1.0f; - } - } - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 36) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 36."); - - configurationFileFormat.LoggingEnableTrace = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 37) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 37."); - - configurationFileFormat.ShowConsole = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 38) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 38."); - - configurationFileFormat.BaseStyle = "Dark"; - configurationFileFormat.GameListViewMode = 0; - configurationFileFormat.ShowNames = true; - configurationFileFormat.GridSize = 2; - configurationFileFormat.LanguageCode = "en_US"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 39) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 39."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUI = configurationFileFormat.Hotkeys.ShowUI, - Pause = configurationFileFormat.Hotkeys.Pause, - ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, - ResScaleUp = Key.Unbound, - ResScaleDown = Key.Unbound, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 40) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 40."); - - configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 41) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 41."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUI = configurationFileFormat.Hotkeys.ShowUI, - Pause = configurationFileFormat.Hotkeys.Pause, - ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, - ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, - ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, - VolumeUp = Key.Unbound, - VolumeDown = Key.Unbound, - }; - } - - if (configurationFileFormat.Version < 42) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 42."); - - configurationFileFormat.EnableMacroHLE = true; - } - - if (configurationFileFormat.Version < 43) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 43."); - - configurationFileFormat.UseHypervisor = true; - } - - if (configurationFileFormat.Version < 44) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 44."); - - configurationFileFormat.AntiAliasing = AntiAliasing.None; - configurationFileFormat.ScalingFilter = ScalingFilter.Bilinear; - configurationFileFormat.ScalingFilterLevel = 80; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 45) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 45."); - - configurationFileFormat.ShownFileTypes = new ShownFileTypes - { - NSP = true, - PFS0 = true, - XCI = true, - NCA = true, - NRO = true, - NSO = true, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 46) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 46."); - - configurationFileFormat.MultiplayerLanInterfaceId = "0"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 47) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 47."); - - configurationFileFormat.WindowStartup = new WindowStartup - { - WindowPositionX = 0, - WindowPositionY = 0, - WindowSizeHeight = 760, - WindowSizeWidth = 1280, - WindowMaximized = false, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 48) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 48."); - - configurationFileFormat.EnableColorSpacePassthrough = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 49) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 49."); - - if (OperatingSystem.IsMacOS()) - { - AppDataManager.FixMacOSConfigurationFolders(); - } - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 50) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 50."); - - configurationFileFormat.EnableHardwareAcceleration = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 51) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 51."); - - configurationFileFormat.RememberWindowState = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 52) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52."); - - configurationFileFormat.AutoloadDirs = []; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 53) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 53."); - - configurationFileFormat.EnableLowPowerPtc = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 54) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 54."); - - configurationFileFormat.DramSize = MemoryConfiguration.MemoryConfiguration4GiB; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 55) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 55."); - - configurationFileFormat.IgnoreApplet = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 56) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 56."); - - configurationFileFormat.ShowTitleBar = !OperatingSystem.IsWindows(); - - configurationFileUpdated = true; - } - - Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; - Graphics.ResScale.Value = configurationFileFormat.ResScale; - Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; - Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy; - Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio; - Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath; - Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; - Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend; - Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu; - Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing; - Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter; - Graphics.ScalingFilterLevel.Value = configurationFileFormat.ScalingFilterLevel; - Logger.EnableDebug.Value = configurationFileFormat.LoggingEnableDebug; - Logger.EnableStub.Value = configurationFileFormat.LoggingEnableStub; - Logger.EnableInfo.Value = configurationFileFormat.LoggingEnableInfo; - Logger.EnableWarn.Value = configurationFileFormat.LoggingEnableWarn; - Logger.EnableError.Value = configurationFileFormat.LoggingEnableError; - Logger.EnableTrace.Value = configurationFileFormat.LoggingEnableTrace; - Logger.EnableGuest.Value = configurationFileFormat.LoggingEnableGuest; - Logger.EnableFsAccessLog.Value = configurationFileFormat.LoggingEnableFsAccessLog; - Logger.FilteredClasses.Value = configurationFileFormat.LoggingFilteredClasses; - Logger.GraphicsDebugLevel.Value = configurationFileFormat.LoggingGraphicsDebugLevel; - System.Language.Value = configurationFileFormat.SystemLanguage; - System.Region.Value = configurationFileFormat.SystemRegion; - System.TimeZone.Value = configurationFileFormat.SystemTimeZone; - System.SystemTimeOffset.Value = configurationFileFormat.SystemTimeOffset; - System.EnableDockedMode.Value = configurationFileFormat.DockedMode; - EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration; - CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; - ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; - IgnoreApplet.Value = configurationFileFormat.IgnoreApplet; - RememberWindowState.Value = configurationFileFormat.RememberWindowState; - ShowTitleBar.Value = configurationFileFormat.ShowTitleBar; - EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration; - HideCursor.Value = configurationFileFormat.HideCursor; - Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; - Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; - Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; - Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE; - Graphics.EnableColorSpacePassthrough.Value = configurationFileFormat.EnableColorSpacePassthrough; - System.EnablePtc.Value = configurationFileFormat.EnablePtc; - System.EnableLowPowerPtc.Value = configurationFileFormat.EnableLowPowerPtc; - System.EnableInternetAccess.Value = configurationFileFormat.EnableInternetAccess; - System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks; - System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode; - System.AudioBackend.Value = configurationFileFormat.AudioBackend; - System.AudioVolume.Value = configurationFileFormat.AudioVolume; - System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode; - System.DramSize.Value = configurationFileFormat.DramSize; - System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices; - System.UseHypervisor.Value = configurationFileFormat.UseHypervisor; - UI.GuiColumns.FavColumn.Value = configurationFileFormat.GuiColumns.FavColumn; - UI.GuiColumns.IconColumn.Value = configurationFileFormat.GuiColumns.IconColumn; - UI.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn; - UI.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn; - UI.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn; - UI.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn; - UI.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn; - UI.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn; - UI.GuiColumns.FileSizeColumn.Value = configurationFileFormat.GuiColumns.FileSizeColumn; - UI.GuiColumns.PathColumn.Value = configurationFileFormat.GuiColumns.PathColumn; - UI.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId; - UI.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending; - UI.GameDirs.Value = configurationFileFormat.GameDirs; - UI.AutoloadDirs.Value = configurationFileFormat.AutoloadDirs ?? []; - UI.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP; - UI.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0; - UI.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI; - UI.ShownFileTypes.NCA.Value = configurationFileFormat.ShownFileTypes.NCA; - UI.ShownFileTypes.NRO.Value = configurationFileFormat.ShownFileTypes.NRO; - UI.ShownFileTypes.NSO.Value = configurationFileFormat.ShownFileTypes.NSO; - UI.LanguageCode.Value = configurationFileFormat.LanguageCode; - UI.BaseStyle.Value = configurationFileFormat.BaseStyle; - UI.GameListViewMode.Value = configurationFileFormat.GameListViewMode; - UI.ShowNames.Value = configurationFileFormat.ShowNames; - UI.IsAscendingOrder.Value = configurationFileFormat.IsAscendingOrder; - UI.GridSize.Value = configurationFileFormat.GridSize; - UI.ApplicationSort.Value = configurationFileFormat.ApplicationSort; - UI.StartFullscreen.Value = configurationFileFormat.StartFullscreen; - UI.ShowConsole.Value = configurationFileFormat.ShowConsole; - UI.WindowStartup.WindowSizeWidth.Value = configurationFileFormat.WindowStartup.WindowSizeWidth; - UI.WindowStartup.WindowSizeHeight.Value = configurationFileFormat.WindowStartup.WindowSizeHeight; - UI.WindowStartup.WindowPositionX.Value = configurationFileFormat.WindowStartup.WindowPositionX; - UI.WindowStartup.WindowPositionY.Value = configurationFileFormat.WindowStartup.WindowPositionY; - UI.WindowStartup.WindowMaximized.Value = configurationFileFormat.WindowStartup.WindowMaximized; - Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard; - Hid.EnableMouse.Value = configurationFileFormat.EnableMouse; - Hid.Hotkeys.Value = configurationFileFormat.Hotkeys; - Hid.InputConfig.Value = configurationFileFormat.InputConfig ?? []; - - Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId; - Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode; - - if (configurationFileUpdated) - { - ToFileFormat().SaveConfig(configurationFilePath); - - Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}"); - } - } - private static GraphicsBackend DefaultGraphicsBackend() { // Any system running macOS or returning any amount of valid Vulkan devices should default to Vulkan. @@ -1657,17 +309,5 @@ namespace Ryujinx.UI.Common.Configuration return GraphicsBackend.OpenGl; } - - public static void Initialize() - { - if (Instance != null) - { - throw new InvalidOperationException("Configuration is already initialized"); - } - - Instance = new ConfigurationState(); - - Instance.System.EnableLowPowerPtc.Event += (_, evnt) => Optimizations.LowPower = evnt.NewValue; - } } } From 736907945995e2b802b431e05ab08706d723c929 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 22:23:03 -0600 Subject: [PATCH 12/20] misc: chore: cleanup 2 accidental xml namespaces --- src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 910741089..883bf8971 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -4,10 +4,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" - xmlns:uih="clr-namespace:Ryujinx.UI.Common.Helper" mc:Ignorable="d" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" - xmlns:helper="clr-namespace:Ryujinx.UI.Common.Helper;assembly=Ryujinx.UI.Common" x:DataType="viewModels:MainWindowViewModel" x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView"> From 826ffd4a04f53e2c209706160c1f7f10639d35d5 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 22:31:26 -0600 Subject: [PATCH 13/20] misc: Move the LowPowerPtc event handler that changes Optimizations.LowPower into the ConfigurationState ctor --- .../Configuration/ConfigurationState.Model.cs | 7 +++++-- src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs | 2 -- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs index 3b9ffb5d3..2b43b2032 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs @@ -1,4 +1,5 @@ -using Ryujinx.Common; +using ARMeilleure; +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Multiplayer; @@ -12,7 +13,7 @@ namespace Ryujinx.UI.Common.Configuration { public partial class ConfigurationState { - /// + /// /// UI configuration section /// public class UISection @@ -376,6 +377,8 @@ namespace Ryujinx.UI.Common.Configuration EnablePtc.LogChangesToValue(nameof(EnablePtc)); EnableLowPowerPtc = new ReactiveObject(); EnableLowPowerPtc.LogChangesToValue(nameof(EnableLowPowerPtc)); + EnableLowPowerPtc.Event += (_, evnt) + => Optimizations.LowPower = evnt.NewValue; EnableInternetAccess = new ReactiveObject(); EnableInternetAccess.LogChangesToValue(nameof(EnableInternetAccess)); EnableFsIntegrityChecks = new ReactiveObject(); diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 5023e48c0..6fd5cfb93 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -24,8 +24,6 @@ namespace Ryujinx.UI.Common.Configuration } Instance = new ConfigurationState(); - - Instance.System.EnableLowPowerPtc.Event += (_, evnt) => Optimizations.LowPower = evnt.NewValue; } public ConfigurationFileFormat ToFileFormat() From 79ba9d12580675c1d1f6a34168f1f1ba5212315d Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 23:26:15 -0600 Subject: [PATCH 14/20] UI: RPC: Fix asset image hover string version pointing to the Canary repo in Canary --- .github/workflows/canary.yml | 5 +- .../Logging/Targets/FileLogTarget.cs | 2 +- .../Configuration/LoggerModule.cs | 145 +++++++----------- .../DiscordIntegrationModule.cs | 10 +- 4 files changed, 66 insertions(+), 96 deletions(-) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index b64fc1d92..0fde764d5 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -22,6 +22,7 @@ env: RYUJINX_BASE_VERSION: "1.2" RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "canary" RYUJINX_TARGET_RELEASE_CHANNEL_OWNER: "GreemDev" + RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO: "Ryujinx" RYUJINX_TARGET_RELEASE_CHANNEL_REPO: "Ryujinx-Canary" RELEASE: 1 @@ -92,7 +93,7 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs - sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash @@ -227,7 +228,7 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs - sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash diff --git a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs index 8d4ede96c..631df3056 100644 --- a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs +++ b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs @@ -47,7 +47,7 @@ namespace Ryujinx.Common.Logging.Targets } // Clean up old logs, should only keep 3 - FileInfo[] files = logDir.GetFiles("*.log").OrderBy((info => info.CreationTime)).ToArray(); + FileInfo[] files = logDir.GetFiles("*.log").OrderBy(info => info.CreationTime).ToArray(); for (int i = 0; i < files.Length - 2; i++) { try diff --git a/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs b/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs index 9cb283593..7ac505349 100644 --- a/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs +++ b/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs @@ -1,4 +1,3 @@ -using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Logging.Targets; @@ -11,103 +10,69 @@ namespace Ryujinx.UI.Common.Configuration { public static void Initialize() { - ConfigurationState.Instance.Logger.EnableDebug.Event += ReloadEnableDebug; - ConfigurationState.Instance.Logger.EnableStub.Event += ReloadEnableStub; - ConfigurationState.Instance.Logger.EnableInfo.Event += ReloadEnableInfo; - ConfigurationState.Instance.Logger.EnableWarn.Event += ReloadEnableWarning; - ConfigurationState.Instance.Logger.EnableError.Event += ReloadEnableError; - ConfigurationState.Instance.Logger.EnableTrace.Event += ReloadEnableTrace; - ConfigurationState.Instance.Logger.EnableGuest.Event += ReloadEnableGuest; - ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += ReloadEnableFsAccessLog; - ConfigurationState.Instance.Logger.FilteredClasses.Event += ReloadFilteredClasses; - ConfigurationState.Instance.Logger.EnableFileLog.Event += ReloadFileLogger; - } - - private static void ReloadEnableDebug(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Debug, e.NewValue); - } - - private static void ReloadEnableStub(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Stub, e.NewValue); - } - - private static void ReloadEnableInfo(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Info, e.NewValue); - } - - private static void ReloadEnableWarning(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Warning, e.NewValue); - } - - private static void ReloadEnableError(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Error, e.NewValue); - } - - private static void ReloadEnableTrace(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Trace, e.NewValue); - } - - private static void ReloadEnableGuest(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Guest, e.NewValue); - } - - private static void ReloadEnableFsAccessLog(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.AccessLog, e.NewValue); - } - - private static void ReloadFilteredClasses(object sender, ReactiveEventArgs e) - { - bool noFilter = e.NewValue.Length == 0; - - foreach (var logClass in Enum.GetValues()) + ConfigurationState.Instance.Logger.EnableDebug.Event += + (_, e) => Logger.SetEnable(LogLevel.Debug, e.NewValue); + ConfigurationState.Instance.Logger.EnableStub.Event += + (_, e) => Logger.SetEnable(LogLevel.Stub, e.NewValue); + ConfigurationState.Instance.Logger.EnableInfo.Event += + (_, e) => Logger.SetEnable(LogLevel.Info, e.NewValue); + ConfigurationState.Instance.Logger.EnableWarn.Event += + (_, e) => Logger.SetEnable(LogLevel.Warning, e.NewValue); + ConfigurationState.Instance.Logger.EnableError.Event += + (_, e) => Logger.SetEnable(LogLevel.Error, e.NewValue); + ConfigurationState.Instance.Logger.EnableTrace.Event += + (_, e) => Logger.SetEnable(LogLevel.Error, e.NewValue); + ConfigurationState.Instance.Logger.EnableGuest.Event += + (_, e) => Logger.SetEnable(LogLevel.Guest, e.NewValue); + ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += + (_, e) => Logger.SetEnable(LogLevel.AccessLog, e.NewValue); + + ConfigurationState.Instance.Logger.FilteredClasses.Event += (_, e) => { - Logger.SetEnable(logClass, noFilter); - } + bool noFilter = e.NewValue.Length == 0; - foreach (var logClass in e.NewValue) - { - Logger.SetEnable(logClass, true); - } - } - - private static void ReloadFileLogger(object sender, ReactiveEventArgs e) - { - if (e.NewValue) - { - string logDir = AppDataManager.LogsDirPath; - FileStream logFile = null; - - if (!string.IsNullOrEmpty(logDir)) + foreach (var logClass in Enum.GetValues()) { - logFile = FileLogTarget.PrepareLogFile(logDir); + Logger.SetEnable(logClass, noFilter); } - if (logFile == null) + foreach (var logClass in e.NewValue) + { + Logger.SetEnable(logClass, true); + } + }; + + ConfigurationState.Instance.Logger.EnableFileLog.Event += (_, e) => + { + if (e.NewValue) + { + string logDir = AppDataManager.LogsDirPath; + FileStream logFile = null; + + if (!string.IsNullOrEmpty(logDir)) + { + logFile = FileLogTarget.PrepareLogFile(logDir); + } + + if (logFile == null) + { + Logger.Error?.Print(LogClass.Application, + "No writable log directory available. Make sure either the Logs directory, Application Data, or the Ryujinx directory is writable."); + Logger.RemoveTarget("file"); + + return; + } + + Logger.AddTarget(new AsyncLogTargetWrapper( + new FileLogTarget("file", logFile), + 1000 + )); + } + else { - Logger.Error?.Print(LogClass.Application, "No writable log directory available. Make sure either the Logs directory, Application Data, or the Ryujinx directory is writable."); Logger.RemoveTarget("file"); - - return; } - - Logger.AddTarget(new AsyncLogTargetWrapper( - new FileLogTarget("file", logFile), - 1000, - AsyncLogTargetOverflowAction.Block - )); - } - else - { - Logger.RemoveTarget("file"); - } + }; } } } diff --git a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs index c21a46e29..780d94249 100644 --- a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs +++ b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs @@ -15,9 +15,13 @@ namespace Ryujinx.UI.Common { public static Timestamps StartedAt { get; set; } - private static readonly string _description = ReleaseInformation.IsValid - ? $"v{ReleaseInformation.Version} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}@{ReleaseInformation.BuildGitHash}" - : "dev build"; + private static string VersionString + => (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}"; + + private static readonly string _description = + ReleaseInformation.IsValid + ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}@{ReleaseInformation.BuildGitHash}" + : "dev build"; private const string ApplicationId = "1293250299716173864"; From 42cbe24bb10baa21f847a550ed1fec3fb5194d11 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 23:32:37 -0600 Subject: [PATCH 15/20] Actually fix Canary showing the wrong repo --- .github/workflows/canary.yml | 4 +++- .github/workflows/release.yml | 2 ++ src/Ryujinx.Common/ReleaseInformation.cs | 2 ++ src/Ryujinx.UI.Common/DiscordIntegrationModule.cs | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 0fde764d5..f3593dff3 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -94,6 +94,7 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash @@ -228,7 +229,8 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs - sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a78718be..183d4c618 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,6 +93,7 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash @@ -224,6 +225,7 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash diff --git a/src/Ryujinx.Common/ReleaseInformation.cs b/src/Ryujinx.Common/ReleaseInformation.cs index 523479d82..f4c62155a 100644 --- a/src/Ryujinx.Common/ReleaseInformation.cs +++ b/src/Ryujinx.Common/ReleaseInformation.cs @@ -15,6 +15,7 @@ namespace Ryujinx.Common private const string ConfigFileName = "%%RYUJINX_CONFIG_FILE_NAME%%"; public const string ReleaseChannelOwner = "%%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER%%"; + public const string ReleaseChannelSourceRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO%%"; public const string ReleaseChannelRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_REPO%%"; public static string ConfigName => !ConfigFileName.StartsWith("%%") ? ConfigFileName : "Config.json"; @@ -23,6 +24,7 @@ namespace Ryujinx.Common !BuildGitHash.StartsWith("%%") && !ReleaseChannelName.StartsWith("%%") && !ReleaseChannelOwner.StartsWith("%%") && + !ReleaseChannelSourceRepo.StartsWith("%%") && !ReleaseChannelRepo.StartsWith("%%") && !ConfigFileName.StartsWith("%%"); diff --git a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs index 780d94249..03dd9a41e 100644 --- a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs +++ b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs @@ -20,7 +20,7 @@ namespace Ryujinx.UI.Common private static readonly string _description = ReleaseInformation.IsValid - ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}@{ReleaseInformation.BuildGitHash}" + ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}" : "dev build"; private const string ApplicationId = "1293250299716173864"; From d404a8b05b8b28261d1c596af1bf7f942a74e000 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 10 Nov 2024 23:34:30 -0600 Subject: [PATCH 16/20] i may be stupid --- .github/workflows/canary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index f3593dff3..d102beaa3 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -93,7 +93,7 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs - sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash From abfcfcaf0f006d508922ab31ce14051409010e23 Mon Sep 17 00:00:00 2001 From: Jonas Henriksson Date: Mon, 11 Nov 2024 21:14:29 +0100 Subject: [PATCH 17/20] Fix issue with Logger.Trace (#228) --- src/Ryujinx.UI.Common/Configuration/LoggerModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs b/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs index 7ac505349..a7913f142 100644 --- a/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs +++ b/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs @@ -21,7 +21,7 @@ namespace Ryujinx.UI.Common.Configuration ConfigurationState.Instance.Logger.EnableError.Event += (_, e) => Logger.SetEnable(LogLevel.Error, e.NewValue); ConfigurationState.Instance.Logger.EnableTrace.Event += - (_, e) => Logger.SetEnable(LogLevel.Error, e.NewValue); + (_, e) => Logger.SetEnable(LogLevel.Trace, e.NewValue); ConfigurationState.Instance.Logger.EnableGuest.Event += (_, e) => Logger.SetEnable(LogLevel.Guest, e.NewValue); ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += From 6d8738c04853fa02947b56157a949549ad55fb3e Mon Sep 17 00:00:00 2001 From: Vudjun Date: Mon, 11 Nov 2024 22:06:50 +0000 Subject: [PATCH 18/20] TESTERS WANTED: RyuLDN implementation (#65) These changes allow players to matchmake for local wireless using a LDN server. The network implementation originates from Berry's public TCP RyuLDN fork. Logo and unrelated changes have been removed. Additionally displays LDN game status in the game selection window when RyuLDN is enabled. Functionality is only enabled while network mode is set to "RyuLDN" in the settings. --- Directory.Packages.props | 1 + .../Multiplayer/MultiplayerMode.cs | 1 + .../Memory/StructArrayHelpers.cs | 24 +- .../Utilities/NetworkHelpers.cs | 6 + src/Ryujinx.Graphics.GAL/IRenderer.cs | 2 +- src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs | 2 +- src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 2 +- src/Ryujinx.HLE/HLEConfiguration.cs | 23 +- .../HOS/Services/Ldn/Types/NetworkConfig.cs | 2 +- .../HOS/Services/Ldn/Types/ScanFilter.cs | 2 +- .../HOS/Services/Ldn/Types/SecurityConfig.cs | 2 +- .../Services/Ldn/Types/SecurityParameter.cs | 2 +- .../HOS/Services/Ldn/Types/UserConfig.cs | 2 +- .../Ldn/UserServiceCreator/AccessPoint.cs | 9 +- .../Ldn/UserServiceCreator/INetworkClient.cs | 1 + .../IUserLocalCommunicationService.cs | 62 +- .../UserServiceCreator/LdnDisabledClient.cs | 7 + .../LdnMitm/LdnMitmClient.cs | 1 + .../UserServiceCreator/LdnRyu/IProxyClient.cs | 7 + .../LdnRyu/LdnMasterProxyClient.cs | 645 ++++++++++++++ .../LdnRyu/NetworkTimeout.cs | 83 ++ .../LdnRyu/Proxy/EphemeralPortPool.cs | 53 ++ .../LdnRyu/Proxy/LdnProxy.cs | 254 ++++++ .../LdnRyu/Proxy/LdnProxySocket.cs | 797 ++++++++++++++++++ .../LdnRyu/Proxy/P2pProxyClient.cs | 93 ++ .../LdnRyu/Proxy/P2pProxyServer.cs | 388 +++++++++ .../LdnRyu/Proxy/P2pProxySession.cs | 90 ++ .../LdnRyu/Proxy/ProxyHelpers.cs | 24 + .../LdnRyu/RyuLdnProtocol.cs | 380 +++++++++ .../LdnRyu/Types/DisconnectMessage.cs | 10 + .../LdnRyu/Types/ExternalProxyConfig.cs | 19 + .../Types/ExternalProxyConnectionState.cs | 18 + .../LdnRyu/Types/ExternalProxyToken.cs | 20 + .../LdnRyu/Types/InitializeMessage.cs | 20 + .../LdnRyu/Types/LdnHeader.cs | 13 + .../LdnRyu/Types/PacketId.cs | 36 + .../LdnRyu/Types/PassphraseMessage.cs | 11 + .../LdnRyu/Types/PingMessage.cs | 11 + .../LdnRyu/Types/ProxyConnectRequest.cs | 10 + .../LdnRyu/Types/ProxyConnectResponse.cs | 10 + .../LdnRyu/Types/ProxyDataHeader.cs | 14 + .../LdnRyu/Types/ProxyDataPacket.cs | 8 + .../LdnRyu/Types/ProxyDisconnectMessage.cs | 11 + .../LdnRyu/Types/ProxyInfo.cs | 20 + .../LdnRyu/Types/RejectRequest.cs | 18 + .../LdnRyu/Types/RyuNetworkConfig.cs | 23 + .../LdnRyu/Types/SetAcceptPolicyRequest.cs | 11 + .../Ldn/UserServiceCreator/Station.cs | 9 +- .../Types/CreateAccessPointPrivateRequest.cs | 3 + .../Types/CreateAccessPointRequest.cs | 5 +- .../UserServiceCreator/Types/ProxyConfig.cs | 11 + .../HOS/Services/Sockets/Bsd/IClient.cs | 2 +- .../Sockets/Bsd/Impl/ManagedSocket.cs | 33 +- .../Bsd/Impl/ManagedSocketPollManager.cs | 159 ++-- .../Sockets/Bsd/Proxy/DefaultSocket.cs | 178 ++++ .../HOS/Services/Sockets/Bsd/Proxy/ISocket.cs | 47 ++ .../Sockets/Bsd/Proxy/SocketHelpers.cs | 74 ++ .../Services/Sockets/Sfdnsres/IResolver.cs | 2 +- .../SslService/SslManagedSocketConnection.cs | 3 +- .../Loaders/Processes/ProcessResult.cs | 4 +- src/Ryujinx.HLE/Ryujinx.HLE.csproj | 1 + src/Ryujinx.Headless.SDL2/Program.cs | 5 +- src/Ryujinx.UI.Common/App/ApplicationData.cs | 2 + .../App/ApplicationLibrary.cs | 49 +- src/Ryujinx.UI.Common/App/LdnGameData.cs | 16 + .../App/LdnGameDataReceivedEventArgs.cs | 10 + .../App/LdnGameDataSerializerContext.cs | 11 + .../Configuration/ConfigurationFileFormat.cs | 15 + .../ConfigurationState.Migration.cs | 5 +- .../Configuration/ConfigurationState.Model.cs | 25 +- .../Configuration/ConfigurationState.cs | 17 +- .../Configuration/UI/GuiColumns.cs | 1 + src/Ryujinx/AppHost.cs | 29 +- src/Ryujinx/Assets/Locales/en_US.json | 14 +- src/Ryujinx/Common/LocaleManager.cs | 2 +- src/Ryujinx/Program.cs | 14 +- src/Ryujinx/UI/Applet/AvaHostUIHandler.cs | 2 +- .../Applet/AvaloniaDynamicTextInputHandler.cs | 4 +- .../UI/Applet/ControllerAppletDialog.axaml.cs | 4 +- .../UI/Controls/ApplicationListView.axaml | 6 + .../UI/Controls/NavigationDialogHost.axaml.cs | 12 +- src/Ryujinx/UI/Helpers/GlyphValueConverter.cs | 4 +- .../UI/Helpers/MultiplayerInfoConverter.cs | 44 + src/Ryujinx/UI/Helpers/TimeZoneConverter.cs | 8 +- .../UI/Models/StatusUpdatedEventArgs.cs | 2 +- .../UI/ViewModels/MainWindowViewModel.cs | 20 +- .../UI/ViewModels/SettingsViewModel.cs | 45 +- .../UI/Views/Main/MainMenuBarView.axaml.cs | 4 +- .../Views/Settings/SettingsNetworkView.axaml | 48 +- .../Settings/SettingsNetworkView.axaml.cs | 17 + src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 72 +- .../UI/Windows/SettingsWindow.axaml.cs | 1 + src/Ryujinx/UI/Windows/StyleableWindow.cs | 2 +- 93 files changed, 4100 insertions(+), 189 deletions(-) create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs create mode 100644 src/Ryujinx.UI.Common/App/LdnGameData.cs create mode 100644 src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs create mode 100644 src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs create mode 100644 src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index fff045062..c0ace079d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,6 +33,7 @@ + diff --git a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs index 69f7d876d..be0e1518c 100644 --- a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs +++ b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs @@ -3,6 +3,7 @@ namespace Ryujinx.Common.Configuration.Multiplayer public enum MultiplayerMode { Disabled, + LdnRyu, LdnMitm, } } diff --git a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs index 762c73889..fcb2229a7 100644 --- a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs +++ b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs @@ -803,18 +803,6 @@ namespace Ryujinx.Common.Memory public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); } - public struct Array256 : IArray where T : unmanaged - { - T _e0; - Array128 _other; - Array127 _other2; - public readonly int Length => 256; - public ref T this[int index] => ref AsSpan()[index]; - - [Pure] - public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); - } - public struct Array140 : IArray where T : unmanaged { T _e0; @@ -828,6 +816,18 @@ namespace Ryujinx.Common.Memory public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); } + public struct Array256 : IArray where T : unmanaged + { + T _e0; + Array128 _other; + Array127 _other2; + public readonly int Length => 256; + public ref T this[int index] => ref AsSpan()[index]; + + [Pure] + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); + } + public struct Array384 : IArray where T : unmanaged { T _e0; diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs index 71e02184e..53d1e4f33 100644 --- a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs +++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs @@ -1,6 +1,7 @@ using System.Buffers.Binary; using System.Net; using System.Net.NetworkInformation; +using System.Runtime.InteropServices; namespace Ryujinx.Common.Utilities { @@ -65,6 +66,11 @@ namespace Ryujinx.Common.Utilities return (targetProperties, targetAddressInfo); } + public static bool SupportsDynamicDns() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + public static uint ConvertIpv4Address(IPAddress ipAddress) { return BinaryPrimitives.ReadUInt32BigEndian(ipAddress.GetAddressBytes()); diff --git a/src/Ryujinx.Graphics.GAL/IRenderer.cs b/src/Ryujinx.Graphics.GAL/IRenderer.cs index 9b5e2cc42..c2fdcbe4b 100644 --- a/src/Ryujinx.Graphics.GAL/IRenderer.cs +++ b/src/Ryujinx.Graphics.GAL/IRenderer.cs @@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.GAL IPipeline Pipeline { get; } IWindow Window { get; } - + uint ProgramCount { get; } void BackgroundContextAction(Action action, bool alwaysBackground = false); diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs index 2deee045c..6ead314fd 100644 --- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs +++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs @@ -97,7 +97,7 @@ namespace Ryujinx.Graphics.OpenGL public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info) { ProgramCount++; - + return new Program(shaders, info.FragmentOutputMap); } diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index 8d324957a..cc2bc36c2 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -549,7 +549,7 @@ namespace Ryujinx.Graphics.Vulkan public IProgram CreateProgram(ShaderSource[] sources, ShaderInfo info) { ProgramCount++; - + bool isCompute = sources.Length == 1 && sources[0].Stage == ShaderStage.Compute; if (info.State.HasValue || isCompute) diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index 955fee4b5..70fcf278d 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -164,6 +164,21 @@ namespace Ryujinx.HLE /// public MultiplayerMode MultiplayerMode { internal get; set; } + /// + /// Disable P2P mode + /// + public bool MultiplayerDisableP2p { internal get; set; } + + /// + /// Multiplayer Passphrase + /// + public string MultiplayerLdnPassphrase { internal get; set; } + + /// + /// LDN Server + /// + public string MultiplayerLdnServer { internal get; set; } + /// /// An action called when HLE force a refresh of output after docked mode changed. /// @@ -194,7 +209,10 @@ namespace Ryujinx.HLE float audioVolume, bool useHypervisor, string multiplayerLanInterfaceId, - MultiplayerMode multiplayerMode) + MultiplayerMode multiplayerMode, + bool multiplayerDisableP2p, + string multiplayerLdnPassphrase, + string multiplayerLdnServer) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -222,6 +240,9 @@ namespace Ryujinx.HLE UseHypervisor = useHypervisor; MultiplayerLanInterfaceId = multiplayerLanInterfaceId; MultiplayerMode = multiplayerMode; + MultiplayerDisableP2p = multiplayerDisableP2p; + MultiplayerLdnPassphrase = multiplayerLdnPassphrase; + MultiplayerLdnServer = multiplayerLdnServer; } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs index 4da5fe42b..c6d6ac944 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x20)] + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 8)] struct NetworkConfig { public IntentId IntentId; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs index 449c923cc..f3ab1edd5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x60)] + [StructLayout(LayoutKind.Sequential, Size = 0x60, Pack = 8)] struct ScanFilter { public NetworkId NetworkId; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs index 5939a1394..f3968aab4 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x44)] + [StructLayout(LayoutKind.Sequential, Size = 0x44, Pack = 2)] struct SecurityConfig { public SecurityMode SecurityMode; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs index dbcaa9eeb..e564a2ec9 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x20)] + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 1)] struct SecurityParameter { public Array16 Data; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs index 3820f936e..7246f6f80 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x30)] + [StructLayout(LayoutKind.Sequential, Size = 0x30, Pack = 1)] struct UserConfig { public Array33 UserName; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs index 78ebcac82..bd00a3139 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs @@ -15,6 +15,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public Array8 LatestUpdates = new(); public bool Connected { get; private set; } + public ProxyConfig Config => _parent.NetworkClient.Config; + public AccessPoint(IUserLocalCommunicationService parent) { _parent = parent; @@ -24,9 +26,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public void Dispose() { - _parent.NetworkClient.DisconnectNetwork(); + if (_parent?.NetworkClient != null) + { + _parent.NetworkClient.DisconnectNetwork(); - _parent.NetworkClient.NetworkChange -= NetworkChanged; + _parent.NetworkClient.NetworkChange -= NetworkChanged; + } } private void NetworkChanged(object sender, NetworkChangeEventArgs e) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs index 7ad6de51d..028ab6cfc 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs @@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { interface INetworkClient : IDisposable { + ProxyConfig Config { get; } bool NeedsRealId { get; } event EventHandler NetworkChange; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 1d4b5485e..9f65aed4b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -9,6 +9,8 @@ using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; @@ -21,6 +23,9 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { class IUserLocalCommunicationService : IpcService, IDisposable { + public static string DefaultLanPlayHost = "ryuldn.vudjun.com"; + public static short LanPlayPort = 30456; + public INetworkClient NetworkClient { get; private set; } private const int NifmRequestID = 90; @@ -175,19 +180,37 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_state == NetworkState.AccessPointCreated || _state == NetworkState.StationConnected) { - (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId); - - if (unicastAddress == null) + ProxyConfig config = _state switch { - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress)); - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask)); + NetworkState.AccessPointCreated => _accessPoint.Config, + NetworkState.StationConnected => _station.Config, + + _ => default + }; + + if (config.ProxyIp == 0) + { + (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId); + + if (unicastAddress == null) + { + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress)); + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask)); + } + else + { + Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); + + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address)); + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask)); + } } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); + Logger.Info?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP."); - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address)); - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask)); + context.ResponseData.Write(config.ProxyIp); + context.ResponseData.Write(config.ProxySubnetMask); } } else @@ -1066,6 +1089,27 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator switch (mode) { + case MultiplayerMode.LdnRyu: + try + { + string ldnServer = context.Device.Configuration.MultiplayerLdnServer; + if (string.IsNullOrEmpty(ldnServer)) + { + ldnServer = DefaultLanPlayHost; + } + if (!IPAddress.TryParse(ldnServer, out IPAddress ipAddress)) + { + ipAddress = Dns.GetHostEntry(ldnServer).AddressList[0]; + } + NetworkClient = new LdnMasterProxyClient(ipAddress.ToString(), LanPlayPort, context.Device.Configuration); + } + catch (Exception ex) + { + Logger.Error?.Print(LogClass.ServiceLdn, "Could not locate LdnRyu server. Defaulting to stubbed wireless."); + Logger.Error?.Print(LogClass.ServiceLdn, ex.Message); + NetworkClient = new LdnDisabledClient(); + } + break; case MultiplayerMode.LdnMitm: NetworkClient = new LdnMitmClient(context.Device.Configuration); break; @@ -1103,7 +1147,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _accessPoint?.Dispose(); _accessPoint = null; - NetworkClient?.Dispose(); + NetworkClient?.DisconnectAndStop(); NetworkClient = null; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs index e3385a1ed..2e8bb8d83 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; @@ -6,12 +7,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { class LdnDisabledClient : INetworkClient { + public ProxyConfig Config { get; } public bool NeedsRealId => true; public event EventHandler NetworkChange; public NetworkError Connect(ConnectRequest request) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return NetworkError.None; @@ -19,6 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public NetworkError ConnectPrivate(ConnectPrivateRequest request) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return NetworkError.None; @@ -26,6 +30,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return true; @@ -33,6 +38,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return true; @@ -49,6 +55,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to scan for networks, but Multiplayer is disabled!"); return Array.Empty(); } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs index 273acdd5e..40697d122 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs @@ -12,6 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm /// internal class LdnMitmClient : INetworkClient { + public ProxyConfig Config { get; } public bool NeedsRealId => false; public event EventHandler NetworkChange; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs new file mode 100644 index 000000000..a7c435506 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs @@ -0,0 +1,7 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + interface IProxyClient + { + bool SendAsync(byte[] buffer); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs new file mode 100644 index 000000000..4c7814b8e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs @@ -0,0 +1,645 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; +using Ryujinx.HLE.Utilities; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using TcpClient = NetCoreServer.TcpClient; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + class LdnMasterProxyClient : TcpClient, INetworkClient, IProxyClient + { + public bool NeedsRealId => true; + + private static InitializeMessage InitializeMemory = new InitializeMessage(); + + private const int InactiveTimeout = 6000; + private const int FailureTimeout = 4000; + private const int ScanTimeout = 1000; + + private bool _useP2pProxy; + private NetworkError _lastError; + + private readonly ManualResetEvent _connected = new ManualResetEvent(false); + private readonly ManualResetEvent _error = new ManualResetEvent(false); + private readonly ManualResetEvent _scan = new ManualResetEvent(false); + private readonly ManualResetEvent _reject = new ManualResetEvent(false); + private readonly AutoResetEvent _apConnected = new AutoResetEvent(false); + + private readonly RyuLdnProtocol _protocol; + private readonly NetworkTimeout _timeout; + + private readonly List _availableGames = new List(); + private DisconnectReason _disconnectReason; + + private P2pProxyServer _hostedProxy; + private P2pProxyClient _connectedProxy; + + private bool _networkConnected; + + private string _passphrase; + private byte[] _gameVersion = new byte[0x10]; + + private readonly HLEConfiguration _config; + + public event EventHandler NetworkChange; + + public ProxyConfig Config { get; private set; } + + public LdnMasterProxyClient(string address, int port, HLEConfiguration config) : base(address, port) + { + if (ProxyHelpers.SupportsNoDelay()) + { + OptionNoDelay = true; + } + + _protocol = new RyuLdnProtocol(); + _timeout = new NetworkTimeout(InactiveTimeout, TimeoutConnection); + + _protocol.Initialize += HandleInitialize; + _protocol.Connected += HandleConnected; + _protocol.Reject += HandleReject; + _protocol.RejectReply += HandleRejectReply; + _protocol.SyncNetwork += HandleSyncNetwork; + _protocol.ProxyConfig += HandleProxyConfig; + _protocol.Disconnected += HandleDisconnected; + + _protocol.ScanReply += HandleScanReply; + _protocol.ScanReplyEnd += HandleScanReplyEnd; + _protocol.ExternalProxy += HandleExternalProxy; + + _protocol.Ping += HandlePing; + _protocol.NetworkError += HandleNetworkError; + + _config = config; + _useP2pProxy = !config.MultiplayerDisableP2p; + } + + private void TimeoutConnection() + { + _connected.Reset(); + + DisconnectAsync(); + + while (IsConnected) + { + Thread.Yield(); + } + } + + private bool EnsureConnected() + { + if (IsConnected) + { + return true; + } + + _error.Reset(); + + ConnectAsync(); + + int index = WaitHandle.WaitAny(new WaitHandle[] { _connected, _error }, FailureTimeout); + + if (IsConnected) + { + SendAsync(_protocol.Encode(PacketId.Initialize, InitializeMemory)); + } + + return index == 0 && IsConnected; + } + + private void UpdatePassphraseIfNeeded() + { + string passphrase = _config.MultiplayerLdnPassphrase ?? ""; + if (passphrase != _passphrase) + { + _passphrase = passphrase; + + SendAsync(_protocol.Encode(PacketId.Passphrase, StringUtils.GetFixedLengthBytes(passphrase, 0x80, Encoding.UTF8))); + } + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}"); + + UpdatePassphraseIfNeeded(); + + _connected.Set(); + } + + protected override void OnDisconnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}"); + + _passphrase = null; + + _connected.Reset(); + + if (_networkConnected) + { + DisconnectInternal(); + } + } + + public void DisconnectAndStop() + { + _timeout.Dispose(); + + DisconnectAsync(); + + while (IsConnected) + { + Thread.Yield(); + } + + Dispose(); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(buffer, (int)offset, (int)size); + } + + protected override void OnError(SocketError error) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}"); + + _error.Set(); + } + + + + private void HandleInitialize(LdnHeader header, InitializeMessage initialize) + { + InitializeMemory = initialize; + } + + private void HandleExternalProxy(LdnHeader header, ExternalProxyConfig config) + { + int length = config.AddressFamily switch + { + AddressFamily.InterNetwork => 4, + AddressFamily.InterNetworkV6 => 16, + _ => 0 + }; + + if (length == 0) + { + return; // Invalid external proxy. + } + + IPAddress address = new(config.ProxyIp.AsSpan()[..length].ToArray()); + P2pProxyClient proxy = new(address.ToString(), config.ProxyPort); + + _connectedProxy = proxy; + + bool success = proxy.PerformAuth(config); + + if (!success) + { + DisconnectInternal(); + } + } + + private void HandlePing(LdnHeader header, PingMessage ping) + { + if (ping.Requester == 0) // Server requested. + { + // Send the ping message back. + + SendAsync(_protocol.Encode(PacketId.Ping, ping)); + } + } + + private void HandleNetworkError(LdnHeader header, NetworkErrorMessage error) + { + if (error.Error == NetworkError.PortUnreachable) + { + _useP2pProxy = false; + } + else + { + _lastError = error.Error; + } + } + + private NetworkError ConsumeNetworkError() + { + NetworkError result = _lastError; + + _lastError = NetworkError.None; + + return result; + } + + private void HandleSyncNetwork(LdnHeader header, NetworkInfo info) + { + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true)); + } + + private void HandleConnected(LdnHeader header, NetworkInfo info) + { + _networkConnected = true; + _disconnectReason = DisconnectReason.None; + + _apConnected.Set(); + + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true)); + } + + private void HandleDisconnected(LdnHeader header, DisconnectMessage message) + { + DisconnectInternal(); + } + + private void HandleReject(LdnHeader header, RejectRequest reject) + { + // When the client receives a Reject request, we have been rejected and will be disconnected shortly. + _disconnectReason = reject.DisconnectReason; + } + + private void HandleRejectReply(LdnHeader header) + { + _reject.Set(); + } + + private void HandleScanReply(LdnHeader header, NetworkInfo info) + { + _availableGames.Add(info); + } + + private void HandleScanReplyEnd(LdnHeader obj) + { + _scan.Set(); + } + + private void DisconnectInternal() + { + if (_networkConnected) + { + _networkConnected = false; + + _hostedProxy?.Dispose(); + _hostedProxy = null; + + _connectedProxy?.Dispose(); + _connectedProxy = null; + + _apConnected.Reset(); + + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false, _disconnectReason)); + + if (IsConnected) + { + _timeout.RefreshTimeout(); + } + } + } + + public void DisconnectNetwork() + { + if (_networkConnected) + { + SendAsync(_protocol.Encode(PacketId.Disconnect, new DisconnectMessage())); + + DisconnectInternal(); + } + } + + public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId) + { + if (_networkConnected) + { + _reject.Reset(); + + SendAsync(_protocol.Encode(PacketId.Reject, new RejectRequest(disconnectReason, nodeId))); + + int index = WaitHandle.WaitAny(new WaitHandle[] { _reject, _error }, InactiveTimeout); + + if (index == 0) + { + return (ConsumeNetworkError() != NetworkError.None) ? ResultCode.InvalidState : ResultCode.Success; + } + } + + return ResultCode.InvalidState; + } + + public void SetAdvertiseData(byte[] data) + { + // TODO: validate we're the owner (the server will do this anyways tho) + if (_networkConnected) + { + SendAsync(_protocol.Encode(PacketId.SetAdvertiseData, data)); + } + } + + public void SetGameVersion(byte[] versionString) + { + _gameVersion = versionString; + + if (_gameVersion.Length < 0x10) + { + Array.Resize(ref _gameVersion, 0x10); + } + } + + public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy) + { + // TODO: validate we're the owner (the server will do this anyways tho) + if (_networkConnected) + { + SendAsync(_protocol.Encode(PacketId.SetAcceptPolicy, new SetAcceptPolicyRequest + { + StationAcceptPolicy = acceptPolicy + })); + } + } + + private void DisposeProxy() + { + _hostedProxy?.Dispose(); + _hostedProxy = null; + } + + private void ConfigureAccessPoint(ref RyuNetworkConfig request) + { + _gameVersion.AsSpan().CopyTo(request.GameVersion.AsSpan()); + + if (_useP2pProxy) + { + // Before sending the request, attempt to set up a proxy server. + // This can be on a range of private ports, which can be exposed on a range of public + // ports via UPnP. If any of this fails, we just fall back to using the master server. + + int i = 0; + for (; i < P2pProxyServer.PrivatePortRange; i++) + { + _hostedProxy = new P2pProxyServer(this, (ushort)(P2pProxyServer.PrivatePortBase + i), _protocol); + + try + { + _hostedProxy.Start(); + + break; + } + catch (SocketException e) + { + _hostedProxy.Dispose(); + _hostedProxy = null; + + if (e.SocketErrorCode != SocketError.AddressAlreadyInUse) + { + i = P2pProxyServer.PrivatePortRange; // Immediately fail. + } + } + } + + bool openSuccess = i < P2pProxyServer.PrivatePortRange; + + if (openSuccess) + { + Task natPunchResult = _hostedProxy.NatPunch(); + + try + { + if (natPunchResult.Result != 0) + { + // Tell the server that we are hosting the proxy. + request.ExternalProxyPort = natPunchResult.Result; + } + } + catch (Exception) { } + + if (request.ExternalProxyPort == 0) + { + Logger.Warning?.Print(LogClass.ServiceLdn, "Failed to open a port with UPnP for P2P connection. Proxying through the master server instead. Expect higher latency."); + _hostedProxy.Dispose(); + } + else + { + Logger.Info?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}."); + _hostedProxy.Start(); + + (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(); + + unicastAddress.Address.GetAddressBytes().AsSpan().CopyTo(request.PrivateIp.AsSpan()); + request.InternalProxyPort = _hostedProxy.PrivatePort; + request.AddressFamily = unicastAddress.Address.AddressFamily; + } + } + else + { + Logger.Warning?.Print(LogClass.ServiceLdn, "Cannot create a P2P server. Proxying through the master server instead. Expect higher latency."); + } + } + } + + private bool CreateNetworkCommon() + { + bool signalled = _apConnected.WaitOne(FailureTimeout); + + if (!_useP2pProxy && _hostedProxy != null) + { + Logger.Warning?.Print(LogClass.ServiceLdn, "Locally hosted proxy server was not externally reachable. Proxying through the master server instead. Expect higher latency."); + + DisposeProxy(); + } + + if (signalled && _connectedProxy != null) + { + _connectedProxy.EnsureProxyReady(); + + Config = _connectedProxy.ProxyConfig; + } + else + { + DisposeProxy(); + } + + return signalled; + } + + public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData) + { + _timeout.DisableTimeout(); + + ConfigureAccessPoint(ref request.RyuNetworkConfig); + + if (!EnsureConnected()) + { + DisposeProxy(); + + return false; + } + + UpdatePassphraseIfNeeded(); + + SendAsync(_protocol.Encode(PacketId.CreateAccessPoint, request, advertiseData)); + + // Send a network change event with dummy data immediately. Necessary to avoid crashes in some games + var networkChangeEvent = new NetworkChangeEventArgs(new NetworkInfo() + { + Common = new CommonNetworkInfo() + { + MacAddress = InitializeMemory.MacAddress, + Channel = request.NetworkConfig.Channel, + LinkLevel = 3, + NetworkType = 2, + Ssid = new Ssid() + { + Length = 32 + } + }, + Ldn = new LdnNetworkInfo() + { + AdvertiseDataSize = (ushort)advertiseData.Length, + AuthenticationId = 0, + NodeCount = 1, + NodeCountMax = request.NetworkConfig.NodeCountMax, + SecurityMode = (ushort)request.SecurityConfig.SecurityMode + } + }, true); + networkChangeEvent.Info.Ldn.Nodes[0] = new NodeInfo() + { + Ipv4Address = 175243265, + IsConnected = 1, + LocalCommunicationVersion = request.NetworkConfig.LocalCommunicationVersion, + MacAddress = InitializeMemory.MacAddress, + NodeId = 0, + UserName = request.UserConfig.UserName + }; + "12345678123456781234567812345678"u8.ToArray().CopyTo(networkChangeEvent.Info.Common.Ssid.Name.AsSpan()); + NetworkChange?.Invoke(this, networkChangeEvent); + + return CreateNetworkCommon(); + } + + public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData) + { + _timeout.DisableTimeout(); + + ConfigureAccessPoint(ref request.RyuNetworkConfig); + + if (!EnsureConnected()) + { + DisposeProxy(); + + return false; + } + + UpdatePassphraseIfNeeded(); + + SendAsync(_protocol.Encode(PacketId.CreateAccessPointPrivate, request, advertiseData)); + + return CreateNetworkCommon(); + } + + public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter) + { + if (!_networkConnected) + { + _timeout.RefreshTimeout(); + } + + _availableGames.Clear(); + + int index = -1; + + if (EnsureConnected()) + { + UpdatePassphraseIfNeeded(); + + _scan.Reset(); + + SendAsync(_protocol.Encode(PacketId.Scan, scanFilter)); + + index = WaitHandle.WaitAny(new WaitHandle[] { _scan, _error }, ScanTimeout); + } + + if (index != 0) + { + // An error occurred or timeout. Write 0 games. + return Array.Empty(); + } + + return _availableGames.ToArray(); + } + + private NetworkError ConnectCommon() + { + bool signalled = _apConnected.WaitOne(FailureTimeout); + + NetworkError error = ConsumeNetworkError(); + + if (error != NetworkError.None) + { + return error; + } + + if (signalled && _connectedProxy != null) + { + _connectedProxy.EnsureProxyReady(); + + Config = _connectedProxy.ProxyConfig; + } + + return signalled ? NetworkError.None : NetworkError.ConnectTimeout; + } + + public NetworkError Connect(ConnectRequest request) + { + _timeout.DisableTimeout(); + + if (!EnsureConnected()) + { + return NetworkError.Unknown; + } + + SendAsync(_protocol.Encode(PacketId.Connect, request)); + + var networkChangeEvent = new NetworkChangeEventArgs(new NetworkInfo() + { + Common = request.NetworkInfo.Common, + Ldn = request.NetworkInfo.Ldn + }, true); + + NetworkChange?.Invoke(this, networkChangeEvent); + + return ConnectCommon(); + } + + public NetworkError ConnectPrivate(ConnectPrivateRequest request) + { + _timeout.DisableTimeout(); + + if (!EnsureConnected()) + { + return NetworkError.Unknown; + } + + SendAsync(_protocol.Encode(PacketId.ConnectPrivate, request)); + + return ConnectCommon(); + } + + private void HandleProxyConfig(LdnHeader header, ProxyConfig config) + { + Config = config; + + SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol)); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs new file mode 100644 index 000000000..5012d5d81 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + class NetworkTimeout : IDisposable + { + private readonly int _idleTimeout; + private readonly Action _timeoutCallback; + private CancellationTokenSource _cancel; + + private readonly object _lock = new object(); + + public NetworkTimeout(int idleTimeout, Action timeoutCallback) + { + _idleTimeout = idleTimeout; + _timeoutCallback = timeoutCallback; + } + + private async Task TimeoutTask() + { + CancellationTokenSource cts; + + lock (_lock) + { + cts = _cancel; + } + + if (cts == null) + { + return; + } + + try + { + await Task.Delay(_idleTimeout, cts.Token); + } + catch (TaskCanceledException) + { + return; // Timeout cancelled. + } + + lock (_lock) + { + // Run the timeout callback. If the cancel token source has been replaced, we have _just_ been cancelled. + if (cts == _cancel) + { + _timeoutCallback(); + } + } + } + + public bool RefreshTimeout() + { + lock (_lock) + { + _cancel?.Cancel(); + + _cancel = new CancellationTokenSource(); + + Task.Run(TimeoutTask); + } + + return true; + } + + public void DisableTimeout() + { + lock (_lock) + { + _cancel?.Cancel(); + + _cancel = new CancellationTokenSource(); + } + } + + public void Dispose() + { + DisableTimeout(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs new file mode 100644 index 000000000..bc3a5edf2 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + public class EphemeralPortPool + { + private const ushort EphemeralBase = 49152; + + private readonly List _ephemeralPorts = new List(); + + private readonly object _lock = new object(); + + public ushort Get() + { + ushort port = EphemeralBase; + lock (_lock) + { + // Starting at the ephemeral port base, return an ephemeral port that is not in use. + // Returns 0 if the range is exhausted. + + for (int i = 0; i < _ephemeralPorts.Count; i++) + { + ushort existingPort = _ephemeralPorts[i]; + + if (existingPort > port) + { + // The port was free - take it. + _ephemeralPorts.Insert(i, port); + + return port; + } + + port++; + } + + if (port != 0) + { + _ephemeralPorts.Add(port); + } + + return port; + } + } + + public void Return(ushort port) + { + lock (_lock) + { + _ephemeralPorts.Remove(port); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs new file mode 100644 index 000000000..bb390d49a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs @@ -0,0 +1,254 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class LdnProxy : IDisposable + { + public EndPoint LocalEndpoint { get; } + public IPAddress LocalAddress { get; } + + private readonly List _sockets = new List(); + private readonly Dictionary _ephemeralPorts = new Dictionary(); + + private readonly IProxyClient _parent; + private RyuLdnProtocol _protocol; + private readonly uint _subnetMask; + private readonly uint _localIp; + private readonly uint _broadcast; + + public LdnProxy(ProxyConfig config, IProxyClient client, RyuLdnProtocol protocol) + { + _parent = client; + _protocol = protocol; + + _ephemeralPorts[ProtocolType.Udp] = new EphemeralPortPool(); + _ephemeralPorts[ProtocolType.Tcp] = new EphemeralPortPool(); + + byte[] address = BitConverter.GetBytes(config.ProxyIp); + Array.Reverse(address); + LocalAddress = new IPAddress(address); + + _subnetMask = config.ProxySubnetMask; + _localIp = config.ProxyIp; + _broadcast = _localIp | (~_subnetMask); + + RegisterHandlers(protocol); + } + + public bool Supported(AddressFamily domain, SocketType type, ProtocolType protocol) + { + if (protocol == ProtocolType.Tcp) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Tcp proxy networking is untested. Please report this game so that it can be tested."); + } + return domain == AddressFamily.InterNetwork && (protocol == ProtocolType.Tcp || protocol == ProtocolType.Udp); + } + + private void RegisterHandlers(RyuLdnProtocol protocol) + { + protocol.ProxyConnect += HandleConnectionRequest; + protocol.ProxyConnectReply += HandleConnectionResponse; + protocol.ProxyData += HandleData; + protocol.ProxyDisconnect += HandleDisconnect; + + _protocol = protocol; + } + + public void UnregisterHandlers(RyuLdnProtocol protocol) + { + protocol.ProxyConnect -= HandleConnectionRequest; + protocol.ProxyConnectReply -= HandleConnectionResponse; + protocol.ProxyData -= HandleData; + protocol.ProxyDisconnect -= HandleDisconnect; + } + + public ushort GetEphemeralPort(ProtocolType type) + { + return _ephemeralPorts[type].Get(); + } + + public void ReturnEphemeralPort(ProtocolType type, ushort port) + { + _ephemeralPorts[type].Return(port); + } + + public void RegisterSocket(LdnProxySocket socket) + { + lock (_sockets) + { + _sockets.Add(socket); + } + } + + public void UnregisterSocket(LdnProxySocket socket) + { + lock (_sockets) + { + _sockets.Remove(socket); + } + } + + private void ForRoutedSockets(ProxyInfo info, Action action) + { + lock (_sockets) + { + foreach (LdnProxySocket socket in _sockets) + { + // Must match protocol and destination port. + if (socket.ProtocolType != info.Protocol || socket.LocalEndPoint is not IPEndPoint endpoint || endpoint.Port != info.DestPort) + { + continue; + } + + // We can assume packets routed to us have been sent to our destination. + // They will either be sent to us, or broadcast packets. + + action(socket); + } + } + } + + public void HandleConnectionRequest(LdnHeader header, ProxyConnectRequest request) + { + ForRoutedSockets(request.Info, (socket) => + { + socket.HandleConnectRequest(request); + }); + } + + public void HandleConnectionResponse(LdnHeader header, ProxyConnectResponse response) + { + ForRoutedSockets(response.Info, (socket) => + { + socket.HandleConnectResponse(response); + }); + } + + public void HandleData(LdnHeader header, ProxyDataHeader proxyHeader, byte[] data) + { + ProxyDataPacket packet = new ProxyDataPacket() { Header = proxyHeader, Data = data }; + + ForRoutedSockets(proxyHeader.Info, (socket) => + { + socket.IncomingData(packet); + }); + } + + public void HandleDisconnect(LdnHeader header, ProxyDisconnectMessage disconnect) + { + ForRoutedSockets(disconnect.Info, (socket) => + { + socket.HandleDisconnect(disconnect); + }); + } + + private uint GetIpV4(IPEndPoint endpoint) + { + if (endpoint.AddressFamily != AddressFamily.InterNetwork) + { + throw new NotSupportedException(); + } + + byte[] address = endpoint.Address.GetAddressBytes(); + Array.Reverse(address); + + return BitConverter.ToUInt32(address); + } + + private ProxyInfo MakeInfo(IPEndPoint localEp, IPEndPoint remoteEP, ProtocolType type) + { + return new ProxyInfo + { + SourceIpV4 = GetIpV4(localEp), + SourcePort = (ushort)localEp.Port, + + DestIpV4 = GetIpV4(remoteEP), + DestPort = (ushort)remoteEP.Port, + + Protocol = type + }; + } + + public void RequestConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We must ask the other side to initialize a connection, so they can accept a socket for us. + + ProxyConnectRequest request = new ProxyConnectRequest + { + Info = MakeInfo(localEp, remoteEp, type) + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyConnect, request)); + } + + public void SignalConnected(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We must tell the other side that we have accepted their request for connection. + + ProxyConnectResponse request = new ProxyConnectResponse + { + Info = MakeInfo(localEp, remoteEp, type) + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyConnectReply, request)); + } + + public void EndConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We must tell the other side that our connection is dropped. + + ProxyDisconnectMessage request = new ProxyDisconnectMessage + { + Info = MakeInfo(localEp, remoteEp, type), + DisconnectReason = 0 // TODO + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyDisconnect, request)); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We send exactly as much as the user wants us to, currently instantly. + // TODO: handle over "virtual mtu" (we have a max packet size to worry about anyways). fragment if tcp? throw if udp? + + ProxyDataHeader request = new ProxyDataHeader + { + Info = MakeInfo(localEp, remoteEp, type), + DataLength = (uint)buffer.Length + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyData, request, buffer.ToArray())); + + return buffer.Length; + } + + public bool IsBroadcast(uint ip) + { + return ip == _broadcast; + } + + public bool IsMyself(uint ip) + { + return ip == _localIp; + } + + public void Dispose() + { + UnregisterHandlers(_protocol); + + lock (_sockets) + { + foreach (LdnProxySocket socket in _sockets) + { + socket.ProxyDestroyed(); + } + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs new file mode 100644 index 000000000..ed7a9c751 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs @@ -0,0 +1,797 @@ +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + /// + /// This socket is forwarded through a TCP stream that goes through the Ldn server. + /// The Ldn server will then route the packets we send (or need to receive) within the virtual adhoc network. + /// + class LdnProxySocket : ISocketImpl + { + private readonly LdnProxy _proxy; + + private bool _isListening; + private readonly List _listenSockets = new List(); + + private readonly Queue _connectRequests = new Queue(); + + private readonly AutoResetEvent _acceptEvent = new AutoResetEvent(false); + private readonly int _acceptTimeout = -1; + + private readonly Queue _errors = new Queue(); + + private readonly AutoResetEvent _connectEvent = new AutoResetEvent(false); + private ProxyConnectResponse _connectResponse; + + private int _receiveTimeout = -1; + private readonly AutoResetEvent _receiveEvent = new AutoResetEvent(false); + private readonly Queue _receiveQueue = new Queue(); + + // private int _sendTimeout = -1; // Sends are techically instant right now, so not _really_ used. + + private bool _connecting; + private bool _broadcast; + private bool _readShutdown; + // private bool _writeShutdown; + private bool _closed; + + private readonly Dictionary _socketOptions = new Dictionary() + { + { SocketOptionName.Broadcast, 0 }, //TODO: honor this value + { SocketOptionName.DontLinger, 0 }, + { SocketOptionName.Debug, 0 }, + { SocketOptionName.Error, 0 }, + { SocketOptionName.KeepAlive, 0 }, + { SocketOptionName.OutOfBandInline, 0 }, + { SocketOptionName.ReceiveBuffer, 131072 }, + { SocketOptionName.ReceiveTimeout, -1 }, + { SocketOptionName.SendBuffer, 131072 }, + { SocketOptionName.SendTimeout, -1 }, + { SocketOptionName.Type, 0 }, + { SocketOptionName.ReuseAddress, 0 } //TODO: honor this value + }; + + public EndPoint RemoteEndPoint { get; private set; } + + public EndPoint LocalEndPoint { get; private set; } + + public bool Connected { get; private set; } + + public bool IsBound { get; private set; } + + public AddressFamily AddressFamily { get; } + + public SocketType SocketType { get; } + + public ProtocolType ProtocolType { get; } + + public bool Blocking { get; set; } + + public int Available + { + get + { + int result = 0; + + lock (_receiveQueue) + { + foreach (ProxyDataPacket data in _receiveQueue) + { + result += data.Data.Length; + } + } + + return result; + } + } + + public bool Readable + { + get + { + if (_isListening) + { + lock (_connectRequests) + { + return _connectRequests.Count > 0; + } + } + else + { + if (_readShutdown) + { + return true; + } + + lock (_receiveQueue) + { + return _receiveQueue.Count > 0; + } + } + + } + } + public bool Writable => Connected || ProtocolType == ProtocolType.Udp; + public bool Error => false; + + public LdnProxySocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, LdnProxy proxy) + { + AddressFamily = addressFamily; + SocketType = socketType; + ProtocolType = protocolType; + + _proxy = proxy; + _socketOptions[SocketOptionName.Type] = (int)socketType; + + proxy.RegisterSocket(this); + } + + private IPEndPoint EnsureLocalEndpoint(bool replace) + { + if (LocalEndPoint != null) + { + if (replace) + { + _proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port); + } + else + { + return (IPEndPoint)LocalEndPoint; + } + } + + IPEndPoint localEp = new IPEndPoint(_proxy.LocalAddress, _proxy.GetEphemeralPort(ProtocolType)); + LocalEndPoint = localEp; + + return localEp; + } + + public LdnProxySocket AsAccepted(IPEndPoint remoteEp) + { + Connected = true; + RemoteEndPoint = remoteEp; + + IPEndPoint localEp = EnsureLocalEndpoint(true); + + _proxy.SignalConnected(localEp, remoteEp, ProtocolType); + + return this; + } + + private void SignalError(WsaError error) + { + lock (_errors) + { + _errors.Enqueue((int)error); + } + } + + private IPEndPoint GetEndpoint(uint ipv4, ushort port) + { + byte[] address = BitConverter.GetBytes(ipv4); + Array.Reverse(address); + + return new IPEndPoint(new IPAddress(address), port); + } + + public void IncomingData(ProxyDataPacket packet) + { + bool isBroadcast = _proxy.IsBroadcast(packet.Header.Info.DestIpV4); + + if (!_closed && (_broadcast || !isBroadcast)) + { + lock (_receiveQueue) + { + _receiveQueue.Enqueue(packet); + } + } + } + + public ISocketImpl Accept() + { + if (!_isListening) + { + throw new InvalidOperationException(); + } + + // Accept a pending request to this socket. + + lock (_connectRequests) + { + if (!Blocking && _connectRequests.Count == 0) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + } + + while (true) + { + _acceptEvent.WaitOne(_acceptTimeout); + + lock (_connectRequests) + { + while (_connectRequests.Count > 0) + { + ProxyConnectRequest request = _connectRequests.Dequeue(); + + if (_connectRequests.Count > 0) + { + _acceptEvent.Set(); // Still more accepts to do. + } + + // Is this request made for us? + IPEndPoint endpoint = GetEndpoint(request.Info.DestIpV4, request.Info.DestPort); + + if (Equals(endpoint, LocalEndPoint)) + { + // Yes - let's accept. + IPEndPoint remoteEndpoint = GetEndpoint(request.Info.SourceIpV4, request.Info.SourcePort); + + LdnProxySocket socket = new LdnProxySocket(AddressFamily, SocketType, ProtocolType, _proxy).AsAccepted(remoteEndpoint); + + lock (_listenSockets) + { + _listenSockets.Add(socket); + } + + return socket; + } + } + } + } + } + + public void Bind(EndPoint localEP) + { + ArgumentNullException.ThrowIfNull(localEP); + + if (LocalEndPoint != null) + { + _proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port); + } + var asIPEndpoint = (IPEndPoint)localEP; + if (asIPEndpoint.Port == 0) + { + asIPEndpoint.Port = (ushort)_proxy.GetEphemeralPort(ProtocolType); + } + + LocalEndPoint = (IPEndPoint)localEP; + + IsBound = true; + } + + public void Close() + { + _closed = true; + + _proxy.UnregisterSocket(this); + + if (Connected) + { + Disconnect(false); + } + + lock (_listenSockets) + { + foreach (LdnProxySocket socket in _listenSockets) + { + socket.Close(); + } + } + + _isListening = false; + } + + public void Connect(EndPoint remoteEP) + { + if (_isListening || !IsBound) + { + throw new InvalidOperationException(); + } + + if (remoteEP is not IPEndPoint) + { + throw new NotSupportedException(); + } + + IPEndPoint localEp = EnsureLocalEndpoint(true); + + _connecting = true; + + _proxy.RequestConnection(localEp, (IPEndPoint)remoteEP, ProtocolType); + + if (!Blocking && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + + _connectEvent.WaitOne(); //timeout? + + if (_connectResponse.Info.SourceIpV4 == 0) + { + throw new SocketException((int)WsaError.WSAECONNREFUSED); + } + + _connectResponse = default; + } + + public void HandleConnectResponse(ProxyConnectResponse obj) + { + if (!_connecting) + { + return; + } + + _connecting = false; + + if (_connectResponse.Info.SourceIpV4 != 0) + { + IPEndPoint remoteEp = GetEndpoint(obj.Info.SourceIpV4, obj.Info.SourcePort); + RemoteEndPoint = remoteEp; + + Connected = true; + } + else + { + // Connection failed + + SignalError(WsaError.WSAECONNREFUSED); + } + } + + public void Disconnect(bool reuseSocket) + { + if (Connected) + { + ConnectionEnded(); + + // The other side needs to be notified that connection ended. + _proxy.EndConnection(LocalEndPoint as IPEndPoint, RemoteEndPoint as IPEndPoint, ProtocolType); + } + } + + private void ConnectionEnded() + { + if (Connected) + { + RemoteEndPoint = null; + Connected = false; + } + } + + public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue) + { + if (optionLevel != SocketOptionLevel.Socket) + { + throw new NotImplementedException(); + } + + if (_socketOptions.TryGetValue(optionName, out int result)) + { + byte[] data = BitConverter.GetBytes(result); + Array.Copy(data, 0, optionValue, 0, Math.Min(data.Length, optionValue.Length)); + } + else + { + throw new NotImplementedException(); + } + } + + public void Listen(int backlog) + { + if (!IsBound) + { + throw new SocketException(); + } + + _isListening = true; + } + + public void HandleConnectRequest(ProxyConnectRequest obj) + { + lock (_connectRequests) + { + _connectRequests.Enqueue(obj); + } + + _connectEvent.Set(); + } + + public void HandleDisconnect(ProxyDisconnectMessage message) + { + Disconnect(false); + } + + public int Receive(Span buffer) + { + EndPoint dummy = new IPEndPoint(IPAddress.Any, 0); + + return ReceiveFrom(buffer, SocketFlags.None, ref dummy); + } + + public int Receive(Span buffer, SocketFlags flags) + { + EndPoint dummy = new IPEndPoint(IPAddress.Any, 0); + + return ReceiveFrom(buffer, flags, ref dummy); + } + + public int Receive(Span buffer, SocketFlags flags, out SocketError socketError) + { + EndPoint dummy = new IPEndPoint(IPAddress.Any, 0); + + return ReceiveFrom(buffer, flags, out socketError, ref dummy); + } + + public int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEp) + { + // We just receive all packets meant for us anyways regardless of EP in the actual implementation. + // The point is mostly to return the endpoint that we got the data from. + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, ref remoteEp); + } + else if (_readShutdown) + { + return 0; + } + else if (!Blocking) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + } + + int timeout = _receiveTimeout; + + _receiveEvent.WaitOne(timeout == 0 ? -1 : timeout); + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, ref remoteEp); + } + else if (_readShutdown) + { + return 0; + } + else + { + throw new SocketException((int)WsaError.WSAETIMEDOUT); + } + } + } + + public int ReceiveFrom(Span buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp) + { + // We just receive all packets meant for us anyways regardless of EP in the actual implementation. + // The point is mostly to return the endpoint that we got the data from. + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + socketError = SocketError.ConnectionReset; + return -1; + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp); + } + else if (_readShutdown) + { + socketError = SocketError.Success; + return 0; + } + else if (!Blocking) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + } + + int timeout = _receiveTimeout; + + _receiveEvent.WaitOne(timeout == 0 ? -1 : timeout); + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp); + } + else if (_readShutdown) + { + socketError = SocketError.Success; + return 0; + } + else + { + socketError = SocketError.TimedOut; + return -1; + } + } + } + + private int ReceiveFromQueue(Span buffer, SocketFlags flags, ref EndPoint remoteEp) + { + int size = buffer.Length; + + // Assumes we have the receive queue lock, and at least one item in the queue. + ProxyDataPacket packet = _receiveQueue.Peek(); + + remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort); + + bool peek = (flags & SocketFlags.Peek) != 0; + + int read; + + if (packet.Data.Length > size) + { + read = size; + + // Cannot fit in the output buffer. Copy up to what we've got. + packet.Data.AsSpan(0, size).CopyTo(buffer); + + if (ProtocolType == ProtocolType.Udp) + { + // Udp overflows, loses the data, then throws an exception. + + if (!peek) + { + _receiveQueue.Dequeue(); + } + + throw new SocketException((int)WsaError.WSAEMSGSIZE); + } + else if (ProtocolType == ProtocolType.Tcp) + { + // Split the data at the buffer boundary. It will stay on the recieve queue. + + byte[] newData = new byte[packet.Data.Length - size]; + Array.Copy(packet.Data, size, newData, 0, newData.Length); + + packet.Data = newData; + } + } + else + { + read = packet.Data.Length; + + packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer); + + if (!peek) + { + _receiveQueue.Dequeue(); + } + } + + return read; + } + + private int ReceiveFromQueue(Span buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp) + { + int size = buffer.Length; + + // Assumes we have the receive queue lock, and at least one item in the queue. + ProxyDataPacket packet = _receiveQueue.Peek(); + + remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort); + + bool peek = (flags & SocketFlags.Peek) != 0; + + int read; + + if (packet.Data.Length > size) + { + read = size; + + // Cannot fit in the output buffer. Copy up to what we've got. + packet.Data.AsSpan(0, size).CopyTo(buffer); + + if (ProtocolType == ProtocolType.Udp) + { + // Udp overflows, loses the data, then throws an exception. + + if (!peek) + { + _receiveQueue.Dequeue(); + } + + socketError = SocketError.MessageSize; + return -1; + } + else if (ProtocolType == ProtocolType.Tcp) + { + // Split the data at the buffer boundary. It will stay on the recieve queue. + + byte[] newData = new byte[packet.Data.Length - size]; + Array.Copy(packet.Data, size, newData, 0, newData.Length); + + packet.Data = newData; + } + } + else + { + read = packet.Data.Length; + + packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer); + + if (!peek) + { + _receiveQueue.Dequeue(); + } + } + + socketError = SocketError.Success; + + return read; + } + + public int Send(ReadOnlySpan buffer) + { + // Send to the remote host chosen when we "connect" or "accept". + if (!Connected) + { + throw new SocketException(); + } + + return SendTo(buffer, SocketFlags.None, RemoteEndPoint); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags) + { + // Send to the remote host chosen when we "connect" or "accept". + if (!Connected) + { + throw new SocketException(); + } + + return SendTo(buffer, flags, RemoteEndPoint); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError) + { + // Send to the remote host chosen when we "connect" or "accept". + if (!Connected) + { + throw new SocketException(); + } + + return SendTo(buffer, flags, out socketError, RemoteEndPoint); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP) + { + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + IPEndPoint localEp = EnsureLocalEndpoint(false); + + if (remoteEP is not IPEndPoint) + { + throw new NotSupportedException(); + } + + return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError, EndPoint remoteEP) + { + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + socketError = SocketError.ConnectionReset; + return -1; + } + + IPEndPoint localEp = EnsureLocalEndpoint(false); + + if (remoteEP is not IPEndPoint) + { + // throw new NotSupportedException(); + socketError = SocketError.OperationNotSupported; + return -1; + } + + socketError = SocketError.Success; + + return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType); + } + + public bool Poll(int microSeconds, SelectMode mode) + { + return mode switch + { + SelectMode.SelectRead => Readable, + SelectMode.SelectWrite => Writable, + SelectMode.SelectError => Error, + _ => false + }; + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue) + { + if (optionLevel != SocketOptionLevel.Socket) + { + throw new NotImplementedException(); + } + + switch (optionName) + { + case SocketOptionName.SendTimeout: + //_sendTimeout = optionValue; + break; + case SocketOptionName.ReceiveTimeout: + _receiveTimeout = optionValue; + break; + case SocketOptionName.Broadcast: + _broadcast = optionValue != 0; + break; + } + + lock (_socketOptions) + { + _socketOptions[optionName] = optionValue; + } + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue) + { + // Just linger uses this for now in BSD, which we ignore. + } + + public void Shutdown(SocketShutdown how) + { + switch (how) + { + case SocketShutdown.Both: + _readShutdown = true; + // _writeShutdown = true; + break; + case SocketShutdown.Receive: + _readShutdown = true; + break; + case SocketShutdown.Send: + // _writeShutdown = true; + break; + } + } + + public void ProxyDestroyed() + { + // Do nothing, for now. Will likely be more useful with TCP. + } + + public void Dispose() + { + + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs new file mode 100644 index 000000000..7da1aa998 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs @@ -0,0 +1,93 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; +using System.Net.Sockets; +using System.Threading; +using TcpClient = NetCoreServer.TcpClient; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class P2pProxyClient : TcpClient, IProxyClient + { + private const int FailureTimeout = 4000; + + public ProxyConfig ProxyConfig { get; private set; } + + private readonly RyuLdnProtocol _protocol; + + private readonly ManualResetEvent _connected = new ManualResetEvent(false); + private readonly ManualResetEvent _ready = new ManualResetEvent(false); + private readonly AutoResetEvent _error = new AutoResetEvent(false); + + public P2pProxyClient(string address, int port) : base(address, port) + { + if (ProxyHelpers.SupportsNoDelay()) + { + OptionNoDelay = true; + } + + _protocol = new RyuLdnProtocol(); + + _protocol.ProxyConfig += HandleProxyConfig; + + ConnectAsync(); + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client connected a new session with Id {Id}"); + + _connected.Set(); + } + + protected override void OnDisconnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client disconnected a session with Id {Id}"); + + SocketHelpers.UnregisterProxy(); + + _connected.Reset(); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(buffer, (int)offset, (int)size); + } + + protected override void OnError(SocketError error) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client caught an error with code {error}"); + + _error.Set(); + } + + private void HandleProxyConfig(LdnHeader header, ProxyConfig config) + { + ProxyConfig = config; + + SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol)); + + _ready.Set(); + } + + public bool EnsureProxyReady() + { + return _ready.WaitOne(FailureTimeout); + } + + public bool PerformAuth(ExternalProxyConfig config) + { + bool signalled = _connected.WaitOne(FailureTimeout); + + if (!signalled) + { + return false; + } + + SendAsync(_protocol.Encode(PacketId.ExternalProxy, config)); + + return true; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs new file mode 100644 index 000000000..598fb654f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs @@ -0,0 +1,388 @@ +using NetCoreServer; +using Open.Nat; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class P2pProxyServer : TcpServer, IDisposable + { + public const ushort PrivatePortBase = 39990; + public const int PrivatePortRange = 10; + + private const ushort PublicPortBase = 39990; + private const int PublicPortRange = 10; + + private const ushort PortLeaseLength = 60; + private const ushort PortLeaseRenew = 50; + + private const ushort AuthWaitSeconds = 1; + + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + public ushort PrivatePort { get; } + + private ushort _publicPort; + + private bool _disposed; + private readonly CancellationTokenSource _disposedCancellation = new CancellationTokenSource(); + + private NatDevice _natDevice; + private Mapping _portMapping; + + private readonly List _players = new List(); + + private readonly List _waitingTokens = new List(); + private readonly AutoResetEvent _tokenEvent = new AutoResetEvent(false); + + private uint _broadcastAddress; + + private readonly LdnMasterProxyClient _master; + private readonly RyuLdnProtocol _masterProtocol; + private readonly RyuLdnProtocol _protocol; + + public P2pProxyServer(LdnMasterProxyClient master, ushort port, RyuLdnProtocol masterProtocol) : base(IPAddress.Any, port) + { + if (ProxyHelpers.SupportsNoDelay()) + { + OptionNoDelay = true; + } + + PrivatePort = port; + + _master = master; + _masterProtocol = masterProtocol; + + _masterProtocol.ExternalProxyState += HandleStateChange; + _masterProtocol.ExternalProxyToken += HandleToken; + + _protocol = new RyuLdnProtocol(); + } + + private void HandleToken(LdnHeader header, ExternalProxyToken token) + { + _lock.EnterWriteLock(); + + _waitingTokens.Add(token); + + _lock.ExitWriteLock(); + + _tokenEvent.Set(); + } + + private void HandleStateChange(LdnHeader header, ExternalProxyConnectionState state) + { + if (!state.Connected) + { + _lock.EnterWriteLock(); + + _waitingTokens.RemoveAll(token => token.VirtualIp == state.IpAddress); + + _players.RemoveAll(player => + { + if (player.VirtualIpAddress == state.IpAddress) + { + player.DisconnectAndStop(); + + return true; + } + + return false; + }); + + _lock.ExitWriteLock(); + } + } + + public void Configure(ProxyConfig config) + { + _broadcastAddress = config.ProxyIp | (~config.ProxySubnetMask); + } + + public async Task NatPunch() + { + NatDiscoverer discoverer = new NatDiscoverer(); + CancellationTokenSource cts = new CancellationTokenSource(1000); + + NatDevice device; + + try + { + device = await discoverer.DiscoverDeviceAsync(PortMapper.Upnp, cts); + } + catch (NatDeviceNotFoundException) + { + return 0; + } + + _publicPort = PublicPortBase; + + for (int i = 0; i < PublicPortRange; i++) + { + try + { + _portMapping = new Mapping(Protocol.Tcp, PrivatePort, _publicPort, PortLeaseLength, "Ryujinx Local Multiplayer"); + + await device.CreatePortMapAsync(_portMapping); + + break; + } + catch (MappingException) + { + _publicPort++; + } + catch (Exception) + { + return 0; + } + + if (i == PublicPortRange - 1) + { + _publicPort = 0; + } + } + + if (_publicPort != 0) + { + _ = Task.Delay(PortLeaseRenew * 1000, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease)); + } + + _natDevice = device; + + return _publicPort; + } + + // Proxy handlers + + private void RouteMessage(P2pProxySession sender, ref ProxyInfo info, Action action) + { + if (info.SourceIpV4 == 0) + { + // If they sent from a connection bound on 0.0.0.0, make others see it as them. + info.SourceIpV4 = sender.VirtualIpAddress; + } + else if (info.SourceIpV4 != sender.VirtualIpAddress) + { + // Can't pretend to be somebody else. + return; + } + + uint destIp = info.DestIpV4; + + if (destIp == 0xc0a800ff) + { + destIp = _broadcastAddress; + } + + bool isBroadcast = destIp == _broadcastAddress; + + _lock.EnterReadLock(); + + if (isBroadcast) + { + _players.ForEach(player => + { + action(player); + }); + } + else + { + P2pProxySession target = _players.FirstOrDefault(player => player.VirtualIpAddress == destIp); + + if (target != null) + { + action(target); + } + } + + _lock.ExitReadLock(); + } + + public void HandleProxyDisconnect(P2pProxySession sender, LdnHeader header, ProxyDisconnectMessage message) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyDisconnect, message)); + }); + } + + public void HandleProxyData(P2pProxySession sender, LdnHeader header, ProxyDataHeader message, byte[] data) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyData, message, data)); + }); + } + + public void HandleProxyConnectReply(P2pProxySession sender, LdnHeader header, ProxyConnectResponse message) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnectReply, message)); + }); + } + + public void HandleProxyConnect(P2pProxySession sender, LdnHeader header, ProxyConnectRequest message) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnect, message)); + }); + } + + // End proxy handlers + + private async Task RefreshLease() + { + if (_disposed || _natDevice == null) + { + return; + } + + try + { + await _natDevice.CreatePortMapAsync(_portMapping); + } + catch (Exception) + { + + } + + _ = Task.Delay(PortLeaseRenew, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease)); + } + + public bool TryRegisterUser(P2pProxySession session, ExternalProxyConfig config) + { + _lock.EnterWriteLock(); + + // Attempt to find matching configuration. If we don't find one, wait for a bit and try again. + // Woken by new tokens coming in from the master server. + + IPAddress address = (session.Socket.RemoteEndPoint as IPEndPoint).Address; + byte[] addressBytes = ProxyHelpers.AddressTo16Byte(address); + + long time; + long endTime = Stopwatch.GetTimestamp() + Stopwatch.Frequency * AuthWaitSeconds; + + do + { + for (int i = 0; i < _waitingTokens.Count; i++) + { + ExternalProxyToken waitToken = _waitingTokens[i]; + + // Allow any client that has a private IP to connect. (indicated by the server as all 0 in the token) + + bool isPrivate = waitToken.PhysicalIp.AsSpan().SequenceEqual(new byte[16]); + bool ipEqual = isPrivate || waitToken.AddressFamily == address.AddressFamily && waitToken.PhysicalIp.AsSpan().SequenceEqual(addressBytes); + + if (ipEqual && waitToken.Token.AsSpan().SequenceEqual(config.Token.AsSpan())) + { + // This is a match. + + _waitingTokens.RemoveAt(i); + + session.SetIpv4(waitToken.VirtualIp); + + ProxyConfig pconfig = new ProxyConfig + { + ProxyIp = session.VirtualIpAddress, + ProxySubnetMask = 0xFFFF0000 // TODO: Use from server. + }; + + if (_players.Count == 0) + { + Configure(pconfig); + } + + _players.Add(session); + + session.SendAsync(_protocol.Encode(PacketId.ProxyConfig, pconfig)); + + _lock.ExitWriteLock(); + + return true; + } + } + + // Couldn't find the token. + // It may not have arrived yet, so wait for one to arrive. + + _lock.ExitWriteLock(); + + time = Stopwatch.GetTimestamp(); + int remainingMs = (int)((endTime - time) / (Stopwatch.Frequency / 1000)); + + if (remainingMs < 0) + { + remainingMs = 0; + } + + _tokenEvent.WaitOne(remainingMs); + + _lock.EnterWriteLock(); + + } while (time < endTime); + + _lock.ExitWriteLock(); + + return false; + } + + public void DisconnectProxyClient(P2pProxySession session) + { + _lock.EnterWriteLock(); + + bool removed = _players.Remove(session); + + if (removed) + { + _master.SendAsync(_masterProtocol.Encode(PacketId.ExternalProxyState, new ExternalProxyConnectionState + { + IpAddress = session.VirtualIpAddress, + Connected = false + })); + } + + _lock.ExitWriteLock(); + } + + public new void Dispose() + { + base.Dispose(); + + _disposed = true; + _disposedCancellation.Cancel(); + + try + { + Task delete = _natDevice?.DeletePortMapAsync(new Mapping(Protocol.Tcp, PrivatePort, _publicPort, 60, "Ryujinx Local Multiplayer")); + + // Just absorb any exceptions. + delete?.ContinueWith((task) => { }); + } + catch (Exception) + { + // Fail silently. + } + } + + protected override TcpSession CreateSession() + { + return new P2pProxySession(this); + } + + protected override void OnError(SocketError error) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP server caught an error with code {error}"); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs new file mode 100644 index 000000000..515feeac5 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs @@ -0,0 +1,90 @@ +using NetCoreServer; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using System; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class P2pProxySession : TcpSession + { + public uint VirtualIpAddress { get; private set; } + public RyuLdnProtocol Protocol { get; } + + private readonly P2pProxyServer _parent; + + private bool _masterClosed; + + public P2pProxySession(P2pProxyServer server) : base(server) + { + _parent = server; + + Protocol = new RyuLdnProtocol(); + + Protocol.ProxyDisconnect += HandleProxyDisconnect; + Protocol.ProxyData += HandleProxyData; + Protocol.ProxyConnectReply += HandleProxyConnectReply; + Protocol.ProxyConnect += HandleProxyConnect; + + Protocol.ExternalProxy += HandleAuthentication; + } + + private void HandleAuthentication(LdnHeader header, ExternalProxyConfig token) + { + if (!_parent.TryRegisterUser(this, token)) + { + Disconnect(); + } + } + + public void SetIpv4(uint ip) + { + VirtualIpAddress = ip; + } + + public void DisconnectAndStop() + { + _masterClosed = true; + + Disconnect(); + } + + protected override void OnDisconnected() + { + if (!_masterClosed) + { + _parent.DisconnectProxyClient(this); + } + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + try + { + Protocol.Read(buffer, (int)offset, (int)size); + } + catch (Exception) + { + Disconnect(); + } + } + + private void HandleProxyDisconnect(LdnHeader header, ProxyDisconnectMessage message) + { + _parent.HandleProxyDisconnect(this, header, message); + } + + private void HandleProxyData(LdnHeader header, ProxyDataHeader message, byte[] data) + { + _parent.HandleProxyData(this, header, message, data); + } + + private void HandleProxyConnectReply(LdnHeader header, ProxyConnectResponse data) + { + _parent.HandleProxyConnectReply(this, header, data); + } + + private void HandleProxyConnect(LdnHeader header, ProxyConnectRequest message) + { + _parent.HandleProxyConnect(this, header, message); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs new file mode 100644 index 000000000..42b1ab6a2 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs @@ -0,0 +1,24 @@ +using System; +using System.Net; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + static class ProxyHelpers + { + public static byte[] AddressTo16Byte(IPAddress address) + { + byte[] ipBytes = new byte[16]; + byte[] srcBytes = address.GetAddressBytes(); + + Array.Copy(srcBytes, 0, ipBytes, 0, srcBytes.Length); + + return ipBytes; + } + + public static bool SupportsNoDelay() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs new file mode 100644 index 000000000..d0eeaf125 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs @@ -0,0 +1,380 @@ +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + class RyuLdnProtocol + { + private const byte CurrentProtocolVersion = 1; + private const int Magic = ('R' << 0) | ('L' << 8) | ('D' << 16) | ('N' << 24); + private const int MaxPacketSize = 131072; + + private readonly int _headerSize = Marshal.SizeOf(); + + private readonly byte[] _buffer = new byte[MaxPacketSize]; + private int _bufferEnd = 0; + + // Client Packets. + public event Action Initialize; + public event Action Passphrase; + public event Action Connected; + public event Action SyncNetwork; + public event Action ScanReply; + public event Action ScanReplyEnd; + public event Action Disconnected; + + // External Proxy Packets. + public event Action ExternalProxy; + public event Action ExternalProxyState; + public event Action ExternalProxyToken; + + // Server Packets. + public event Action CreateAccessPoint; + public event Action CreateAccessPointPrivate; + public event Action Reject; + public event Action RejectReply; + public event Action SetAcceptPolicy; + public event Action SetAdvertiseData; + public event Action Connect; + public event Action ConnectPrivate; + public event Action Scan; + + // Proxy Packets. + public event Action ProxyConfig; + public event Action ProxyConnect; + public event Action ProxyConnectReply; + public event Action ProxyData; + public event Action ProxyDisconnect; + + // Lifecycle Packets. + public event Action NetworkError; + public event Action Ping; + + public RyuLdnProtocol() { } + + public void Reset() + { + _bufferEnd = 0; + } + + public void Read(byte[] data, int offset, int size) + { + int index = 0; + + while (index < size) + { + if (_bufferEnd < _headerSize) + { + // Assemble the header first. + + int copyable = Math.Min(size - index, Math.Min(size, _headerSize - _bufferEnd)); + + Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable); + + index += copyable; + _bufferEnd += copyable; + } + + if (_bufferEnd >= _headerSize) + { + // The header is available. Make sure we received all the data (size specified in the header) + + LdnHeader ldnHeader = MemoryMarshal.Cast(_buffer)[0]; + + if (ldnHeader.Magic != Magic) + { + throw new InvalidOperationException("Invalid magic number in received packet."); + } + + if (ldnHeader.Version != CurrentProtocolVersion) + { + throw new InvalidOperationException($"Protocol version mismatch. Expected ${CurrentProtocolVersion}, was ${ldnHeader.Version}."); + } + + int finalSize = _headerSize + ldnHeader.DataSize; + + if (finalSize >= MaxPacketSize) + { + throw new InvalidOperationException($"Max packet size {MaxPacketSize} exceeded."); + } + + int copyable = Math.Min(size - index, Math.Min(size, finalSize - _bufferEnd)); + + Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable); + + index += copyable; + _bufferEnd += copyable; + + if (finalSize == _bufferEnd) + { + // The full packet has been retrieved. Send it to be decoded. + + byte[] ldnData = new byte[ldnHeader.DataSize]; + + Array.Copy(_buffer, _headerSize, ldnData, 0, ldnData.Length); + + DecodeAndHandle(ldnHeader, ldnData); + + Reset(); + } + } + } + } + + private (T, byte[]) ParseWithData(byte[] data) where T : struct + { + T str = default; + int size = Marshal.SizeOf(str); + + byte[] remainder = new byte[data.Length - size]; + + if (remainder.Length > 0) + { + Array.Copy(data, size, remainder, 0, remainder.Length); + } + + return (MemoryMarshal.Read(data), remainder); + } + + private void DecodeAndHandle(LdnHeader header, byte[] data) + { + switch ((PacketId)header.Type) + { + // Client Packets. + case PacketId.Initialize: + { + Initialize?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.Passphrase: + { + Passphrase?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.Connected: + { + Connected?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.SyncNetwork: + { + SyncNetwork?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ScanReply: + { + ScanReply?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + case PacketId.ScanReplyEnd: + { + ScanReplyEnd?.Invoke(header); + + break; + } + case PacketId.Disconnect: + { + Disconnected?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // External Proxy Packets. + case PacketId.ExternalProxy: + { + ExternalProxy?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ExternalProxyState: + { + ExternalProxyState?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ExternalProxyToken: + { + ExternalProxyToken?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // Server Packets. + case PacketId.CreateAccessPoint: + { + (CreateAccessPointRequest packet, byte[] extraData) = ParseWithData(data); + CreateAccessPoint?.Invoke(header, packet, extraData); + break; + } + case PacketId.CreateAccessPointPrivate: + { + (CreateAccessPointPrivateRequest packet, byte[] extraData) = ParseWithData(data); + CreateAccessPointPrivate?.Invoke(header, packet, extraData); + break; + } + case PacketId.Reject: + { + Reject?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.RejectReply: + { + RejectReply?.Invoke(header); + + break; + } + case PacketId.SetAcceptPolicy: + { + SetAcceptPolicy?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.SetAdvertiseData: + { + SetAdvertiseData?.Invoke(header, data); + + break; + } + case PacketId.Connect: + { + Connect?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ConnectPrivate: + { + ConnectPrivate?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.Scan: + { + Scan?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // Proxy Packets + case PacketId.ProxyConfig: + { + ProxyConfig?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ProxyConnect: + { + ProxyConnect?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ProxyConnectReply: + { + ProxyConnectReply?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ProxyData: + { + (ProxyDataHeader packet, byte[] extraData) = ParseWithData(data); + + ProxyData?.Invoke(header, packet, extraData); + + break; + } + case PacketId.ProxyDisconnect: + { + ProxyDisconnect?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // Lifecycle Packets. + case PacketId.Ping: + { + Ping?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.NetworkError: + { + NetworkError?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + default: + break; + } + } + + private static LdnHeader GetHeader(PacketId type, int dataSize) + { + return new LdnHeader() + { + Magic = Magic, + Version = CurrentProtocolVersion, + Type = (byte)type, + DataSize = dataSize + }; + } + + public byte[] Encode(PacketId type) + { + LdnHeader header = GetHeader(type, 0); + + return SpanHelpers.AsSpan(ref header).ToArray(); + } + + public byte[] Encode(PacketId type, byte[] data) + { + LdnHeader header = GetHeader(type, data.Length); + + byte[] result = SpanHelpers.AsSpan(ref header).ToArray(); + + Array.Resize(ref result, result.Length + data.Length); + Array.Copy(data, 0, result, Marshal.SizeOf(), data.Length); + + return result; + } + + public byte[] Encode(PacketId type, T packet) where T : unmanaged + { + byte[] packetData = SpanHelpers.AsSpan(ref packet).ToArray(); + + LdnHeader header = GetHeader(type, packetData.Length); + + byte[] result = SpanHelpers.AsSpan(ref header).ToArray(); + + Array.Resize(ref result, result.Length + packetData.Length); + Array.Copy(packetData, 0, result, Marshal.SizeOf(), packetData.Length); + + return result; + } + + public byte[] Encode(PacketId type, T packet, byte[] data) where T : unmanaged + { + byte[] packetData = SpanHelpers.AsSpan(ref packet).ToArray(); + + LdnHeader header = GetHeader(type, packetData.Length + data.Length); + + byte[] result = SpanHelpers.AsSpan(ref header).ToArray(); + + Array.Resize(ref result, result.Length + packetData.Length + data.Length); + Array.Copy(packetData, 0, result, Marshal.SizeOf(), packetData.Length); + Array.Copy(data, 0, result, Marshal.SizeOf() + packetData.Length, data.Length); + + return result; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs new file mode 100644 index 000000000..448d33f29 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x4)] + struct DisconnectMessage + { + public uint DisconnectIP; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs new file mode 100644 index 000000000..9cbb80242 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs @@ -0,0 +1,19 @@ +using Ryujinx.Common.Memory; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Sent by the server to point a client towards an external server being used as a proxy. + /// The client then forwards this to the external proxy after connecting, to verify the connection worked. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x26, Pack = 1)] + struct ExternalProxyConfig + { + public Array16 ProxyIp; + public AddressFamily AddressFamily; + public ushort ProxyPort; + public Array16 Token; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs new file mode 100644 index 000000000..ecf4e14f7 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Indicates a change in connection state for the given client. + /// Is sent to notify the master server when connection is first established. + /// Can be sent by the external proxy to the master server to notify it of a proxy disconnect. + /// Can be sent by the master server to notify the external proxy of a user leaving a room. + /// Both will result in a force kick. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 4)] + struct ExternalProxyConnectionState + { + public uint IpAddress; + public bool Connected; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs new file mode 100644 index 000000000..0a8980c37 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs @@ -0,0 +1,20 @@ +using Ryujinx.Common.Memory; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Sent by the master server to an external proxy to tell them someone is going to connect. + /// This drives authentication, and lets the proxy know what virtual IP to give to each joiner, + /// as these are managed by the master server. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x28)] + struct ExternalProxyToken + { + public uint VirtualIp; + public Array16 Token; + public Array16 PhysicalIp; + public AddressFamily AddressFamily; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs new file mode 100644 index 000000000..36ddc65fe --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs @@ -0,0 +1,20 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// This message is first sent by the client to identify themselves. + /// If the server has a token+mac combo that matches the submission, then they are returned their new ID and mac address. (the mac is also reassigned to the new id) + /// Otherwise, they are returned a random mac address. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x16)] + struct InitializeMessage + { + // All 0 if we don't have an ID yet. + public Array16 Id; + + // All 0 if we don't have a mac yet. + public Array6 MacAddress; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs new file mode 100644 index 000000000..f41f15ab4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0xA)] + struct LdnHeader + { + public uint Magic; + public byte Type; + public byte Version; + public int DataSize; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs new file mode 100644 index 000000000..b8ef5fbc1 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs @@ -0,0 +1,36 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + enum PacketId + { + Initialize, + Passphrase, + + CreateAccessPoint, + CreateAccessPointPrivate, + ExternalProxy, + ExternalProxyToken, + ExternalProxyState, + SyncNetwork, + Reject, + RejectReply, + Scan, + ScanReply, + ScanReplyEnd, + Connect, + ConnectPrivate, + Connected, + Disconnect, + + ProxyConfig, + ProxyConnect, + ProxyConnectReply, + ProxyData, + ProxyDisconnect, + + SetAcceptPolicy, + SetAdvertiseData, + + Ping = 254, + NetworkError = 255 + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs new file mode 100644 index 000000000..0deba0b07 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs @@ -0,0 +1,11 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x80)] + struct PassphraseMessage + { + public Array128 Passphrase; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs new file mode 100644 index 000000000..135e39caa --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x2)] + struct PingMessage + { + public byte Requester; + public byte Id; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs new file mode 100644 index 000000000..ffce77791 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + struct ProxyConnectRequest + { + public ProxyInfo Info; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs new file mode 100644 index 000000000..de2e430fb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + struct ProxyConnectResponse + { + public ProxyInfo Info; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs new file mode 100644 index 000000000..e46a40692 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Represents data sent over a transport layer. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x14)] + struct ProxyDataHeader + { + public ProxyInfo Info; + public uint DataLength; // Followed by the data with the specified byte length. + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs new file mode 100644 index 000000000..eb3648413 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + class ProxyDataPacket + { + public ProxyDataHeader Header; + public byte[] Data; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs new file mode 100644 index 000000000..2154ae109 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x14)] + struct ProxyDisconnectMessage + { + public ProxyInfo Info; + public int DisconnectReason; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs new file mode 100644 index 000000000..d9338f244 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs @@ -0,0 +1,20 @@ +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Information included in all proxied communication. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 1)] + struct ProxyInfo + { + public uint SourceIpV4; + public ushort SourcePort; + + public uint DestIpV4; + public ushort DestPort; + + public ProtocolType Protocol; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs new file mode 100644 index 000000000..1c2ce1f8b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs @@ -0,0 +1,18 @@ +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8)] + struct RejectRequest + { + public uint NodeId; + public DisconnectReason DisconnectReason; + + public RejectRequest(DisconnectReason disconnectReason, uint nodeId) + { + DisconnectReason = disconnectReason; + NodeId = nodeId; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs new file mode 100644 index 000000000..f3bd72023 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs @@ -0,0 +1,23 @@ +using Ryujinx.Common.Memory; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x28, Pack = 1)] + struct RyuNetworkConfig + { + public Array16 GameVersion; + + // PrivateIp is included for external proxies for the case where a client attempts to join from + // their own LAN. UPnP forwarding can fail when connecting devices on the same network over the public IP, + // so if their public IP is identical, the internal address should be sent instead. + + // The fields below are 0 if not hosting a p2p proxy. + + public Array16 PrivateIp; + public AddressFamily AddressFamily; + public ushort ExternalProxyPort; + public ushort InternalProxyPort; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs new file mode 100644 index 000000000..c4a969901 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs @@ -0,0 +1,11 @@ +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x1, Pack = 1)] + struct SetAcceptPolicyRequest + { + public AcceptPolicy StationAcceptPolicy; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs index e39c01978..fa43f789e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs @@ -14,6 +14,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public bool Connected { get; private set; } + public ProxyConfig Config => _parent.NetworkClient.Config; + public Station(IUserLocalCommunicationService parent) { _parent = parent; @@ -48,9 +50,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public void Dispose() { - _parent.NetworkClient.DisconnectNetwork(); + if (_parent.NetworkClient != null) + { + _parent.NetworkClient.DisconnectNetwork(); - _parent.NetworkClient.NetworkChange -= NetworkChanged; + _parent.NetworkClient.NetworkChange -= NetworkChanged; + } } private ResultCode NetworkErrorToResult(NetworkError error) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs index ac0ff7d94..0972c21c0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types @@ -14,5 +15,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types public UserConfig UserConfig; public NetworkConfig NetworkConfig; public AddressList AddressList; + + public RyuNetworkConfig RyuNetworkConfig; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs index f67f0aac9..d2dc5b698 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types @@ -6,11 +7,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types /// /// Advertise data is appended separately (remaining data in the buffer). /// - [StructLayout(LayoutKind.Sequential, Size = 0x94, CharSet = CharSet.Ansi)] + [StructLayout(LayoutKind.Sequential, Size = 0xBC, Pack = 1)] struct CreateAccessPointRequest { public SecurityConfig SecurityConfig; public UserConfig UserConfig; public NetworkConfig NetworkConfig; + + public RyuNetworkConfig RyuNetworkConfig; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs new file mode 100644 index 000000000..c89c08bbe --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8)] + struct ProxyConfig + { + public uint ProxyIp; + public uint ProxySubnetMask; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs index 21d48288e..3a40a4ac5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs @@ -95,7 +95,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd } } - ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol) + ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol, context.Device.Configuration.MultiplayerLanInterfaceId) { Blocking = !creationFlags.HasFlag(BsdSocketCreationFlags.NonBlocking), }; diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs index c9b811cf5..981fe0a8f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs @@ -1,4 +1,5 @@ using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types; using System; using System.Collections.Generic; @@ -21,21 +22,21 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; } - public nint Handle => Socket.Handle; + public nint Handle => IntPtr.Zero; public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint; public IPEndPoint LocalEndPoint => Socket.LocalEndPoint as IPEndPoint; - public Socket Socket { get; } + public ISocketImpl Socket { get; } - public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType) + public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, string lanInterfaceId) { - Socket = new Socket(addressFamily, socketType, protocolType); + Socket = SocketHelpers.CreateSocket(addressFamily, socketType, protocolType, lanInterfaceId); Refcount = 1; } - private ManagedSocket(Socket socket) + private ManagedSocket(ISocketImpl socket) { Socket = socket; Refcount = 1; @@ -185,6 +186,8 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl } } + bool hasEmittedBlockingWarning = false; + public LinuxError Receive(out int receiveSize, Span buffer, BsdSocketFlags flags) { LinuxError result; @@ -199,6 +202,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl shouldBlockAfterOperation = true; } + if (Blocking && !hasEmittedBlockingWarning) + { + Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors."); + hasEmittedBlockingWarning = true; + } + receiveSize = Socket.Receive(buffer, ConvertBsdSocketFlags(flags)); result = LinuxError.SUCCESS; @@ -236,6 +245,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl shouldBlockAfterOperation = true; } + if (Blocking && !hasEmittedBlockingWarning) + { + Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors."); + hasEmittedBlockingWarning = true; + } + if (!Socket.IsBound) { receiveSize = -1; @@ -313,7 +328,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported GetSockOpt Option: {option} Level: {level}"); optionValue.Clear(); - return LinuxError.SUCCESS; + return LinuxError.EOPNOTSUPP; } byte[] tempOptionValue = new byte[optionValue.Length]; @@ -347,7 +362,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl { Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported SetSockOpt Option: {option} Level: {level}"); - return LinuxError.SUCCESS; + return LinuxError.EOPNOTSUPP; } int value = optionValue.Length >= 4 ? MemoryMarshal.Read(optionValue) : MemoryMarshal.Read(optionValue); @@ -493,7 +508,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl try { - int receiveSize = Socket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); + int receiveSize = (Socket as DefaultSocket).BaseSocket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); if (receiveSize > 0) { @@ -531,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl try { - int sendSize = Socket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); + int sendSize = (Socket as DefaultSocket).BaseSocket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); if (sendSize > 0) { diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs index d0db44086..e870e8aea 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs @@ -1,4 +1,5 @@ using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types; using System.Collections.Generic; using System.Net.Sockets; @@ -26,45 +27,46 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public LinuxError Poll(List events, int timeoutMilliseconds, out int updatedCount) { - List readEvents = new(); - List writeEvents = new(); - List errorEvents = new(); + List readEvents = new(); + List writeEvents = new(); + List errorEvents = new(); updatedCount = 0; foreach (PollEvent evnt in events) { - ManagedSocket socket = (ManagedSocket)evnt.FileDescriptor; - - bool isValidEvent = evnt.Data.InputEvents == 0; - - errorEvents.Add(socket.Socket); - - if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + if (evnt.FileDescriptor is ManagedSocket ms) { - readEvents.Add(socket.Socket); + bool isValidEvent = evnt.Data.InputEvents == 0; - isValidEvent = true; - } + errorEvents.Add(ms.Socket); - if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0) - { - readEvents.Add(socket.Socket); + if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + { + readEvents.Add(ms.Socket); - isValidEvent = true; - } + isValidEvent = true; + } - if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0) - { - writeEvents.Add(socket.Socket); + if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0) + { + readEvents.Add(ms.Socket); - isValidEvent = true; - } + isValidEvent = true; + } - if (!isValidEvent) - { - Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}"); - return LinuxError.EINVAL; + if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0) + { + writeEvents.Add(ms.Socket); + + isValidEvent = true; + } + + if (!isValidEvent) + { + Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}"); + return LinuxError.EINVAL; + } } } @@ -72,7 +74,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl { int actualTimeoutMicroseconds = timeoutMilliseconds == -1 ? -1 : timeoutMilliseconds * 1000; - Socket.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds); + SocketHelpers.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds); } catch (SocketException exception) { @@ -81,34 +83,37 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl foreach (PollEvent evnt in events) { - Socket socket = ((ManagedSocket)evnt.FileDescriptor).Socket; - - PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents; - - if (errorEvents.Contains(socket)) + if (evnt.FileDescriptor is ManagedSocket ms) { - outputEvents |= PollEventTypeMask.Error; + ISocketImpl socket = ms.Socket; - if (!socket.Connected || !socket.IsBound) + PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents; + + if (errorEvents.Contains(ms.Socket)) { - outputEvents |= PollEventTypeMask.Disconnected; - } - } + outputEvents |= PollEventTypeMask.Error; - if (readEvents.Contains(socket)) - { - if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + if (!socket.Connected || !socket.IsBound) + { + outputEvents |= PollEventTypeMask.Disconnected; + } + } + + if (readEvents.Contains(ms.Socket)) { - outputEvents |= PollEventTypeMask.Input; + if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + { + outputEvents |= PollEventTypeMask.Input; + } } - } - if (writeEvents.Contains(socket)) - { - outputEvents |= PollEventTypeMask.Output; - } + if (writeEvents.Contains(ms.Socket)) + { + outputEvents |= PollEventTypeMask.Output; + } - evnt.Data.OutputEvents = outputEvents; + evnt.Data.OutputEvents = outputEvents; + } } updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count; @@ -118,53 +123,55 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public LinuxError Select(List events, int timeout, out int updatedCount) { - List readEvents = new(); - List writeEvents = new(); - List errorEvents = new(); + List readEvents = new(); + List writeEvents = new(); + List errorEvents = new(); updatedCount = 0; foreach (PollEvent pollEvent in events) { - ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor; - - if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input)) + if (pollEvent.FileDescriptor is ManagedSocket ms) { - readEvents.Add(socket.Socket); - } + if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input)) + { + readEvents.Add(ms.Socket); + } - if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output)) - { - writeEvents.Add(socket.Socket); - } + if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output)) + { + writeEvents.Add(ms.Socket); + } - if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error)) - { - errorEvents.Add(socket.Socket); + if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error)) + { + errorEvents.Add(ms.Socket); + } } } - Socket.Select(readEvents, writeEvents, errorEvents, timeout); + SocketHelpers.Select(readEvents, writeEvents, errorEvents, timeout); updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count; foreach (PollEvent pollEvent in events) { - ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor; - - if (readEvents.Contains(socket.Socket)) + if (pollEvent.FileDescriptor is ManagedSocket ms) { - pollEvent.Data.OutputEvents |= PollEventTypeMask.Input; - } + if (readEvents.Contains(ms.Socket)) + { + pollEvent.Data.OutputEvents |= PollEventTypeMask.Input; + } - if (writeEvents.Contains(socket.Socket)) - { - pollEvent.Data.OutputEvents |= PollEventTypeMask.Output; - } + if (writeEvents.Contains(ms.Socket)) + { + pollEvent.Data.OutputEvents |= PollEventTypeMask.Output; + } - if (errorEvents.Contains(socket.Socket)) - { - pollEvent.Data.OutputEvents |= PollEventTypeMask.Error; + if (errorEvents.Contains(ms.Socket)) + { + pollEvent.Data.OutputEvents |= PollEventTypeMask.Error; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs new file mode 100644 index 000000000..f1040e799 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs @@ -0,0 +1,178 @@ +using Ryujinx.Common.Utilities; +using System; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy +{ + class DefaultSocket : ISocketImpl + { + public Socket BaseSocket { get; } + + public EndPoint RemoteEndPoint => BaseSocket.RemoteEndPoint; + + public EndPoint LocalEndPoint => BaseSocket.LocalEndPoint; + + public bool Connected => BaseSocket.Connected; + + public bool IsBound => BaseSocket.IsBound; + + public AddressFamily AddressFamily => BaseSocket.AddressFamily; + + public SocketType SocketType => BaseSocket.SocketType; + + public ProtocolType ProtocolType => BaseSocket.ProtocolType; + + public bool Blocking { get => BaseSocket.Blocking; set => BaseSocket.Blocking = value; } + + public int Available => BaseSocket.Available; + + private readonly string _lanInterfaceId; + + public DefaultSocket(Socket baseSocket, string lanInterfaceId) + { + _lanInterfaceId = lanInterfaceId; + + BaseSocket = baseSocket; + } + + public DefaultSocket(AddressFamily domain, SocketType type, ProtocolType protocol, string lanInterfaceId) + { + _lanInterfaceId = lanInterfaceId; + + BaseSocket = new Socket(domain, type, protocol); + } + + private void EnsureNetworkInterfaceBound() + { + if (_lanInterfaceId != "0" && !BaseSocket.IsBound) + { + (_, UnicastIPAddressInformation ipInfo) = NetworkHelpers.GetLocalInterface(_lanInterfaceId); + + BaseSocket.Bind(new IPEndPoint(ipInfo.Address, 0)); + } + } + + public ISocketImpl Accept() + { + return new DefaultSocket(BaseSocket.Accept(), _lanInterfaceId); + } + + public void Bind(EndPoint localEP) + { + // NOTE: The guest is able to receive on 0.0.0.0 without it being limited to the chosen network interface. + // This is because it must get loopback traffic as well. This could allow other network traffic to leak in. + + BaseSocket.Bind(localEP); + } + + public void Close() + { + BaseSocket.Close(); + } + + public void Connect(EndPoint remoteEP) + { + EnsureNetworkInterfaceBound(); + + BaseSocket.Connect(remoteEP); + } + + public void Disconnect(bool reuseSocket) + { + BaseSocket.Disconnect(reuseSocket); + } + + public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue) + { + BaseSocket.GetSocketOption(optionLevel, optionName, optionValue); + } + + public void Listen(int backlog) + { + BaseSocket.Listen(backlog); + } + + public int Receive(Span buffer) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Receive(buffer); + } + + public int Receive(Span buffer, SocketFlags flags) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Receive(buffer, flags); + } + + public int Receive(Span buffer, SocketFlags flags, out SocketError socketError) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Receive(buffer, flags, out socketError); + } + + public int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEP) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.ReceiveFrom(buffer, flags, ref remoteEP); + } + + public int Send(ReadOnlySpan buffer) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Send(buffer); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Send(buffer, flags); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Send(buffer, flags, out socketError); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.SendTo(buffer, flags, remoteEP); + } + + public bool Poll(int microSeconds, SelectMode mode) + { + return BaseSocket.Poll(microSeconds, mode); + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue) + { + BaseSocket.SetSocketOption(optionLevel, optionName, optionValue); + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue) + { + BaseSocket.SetSocketOption(optionLevel, optionName, optionValue); + } + + public void Shutdown(SocketShutdown how) + { + BaseSocket.Shutdown(how); + } + + public void Dispose() + { + BaseSocket.Dispose(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs new file mode 100644 index 000000000..b7055f08b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs @@ -0,0 +1,47 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy +{ + interface ISocketImpl : IDisposable + { + EndPoint RemoteEndPoint { get; } + EndPoint LocalEndPoint { get; } + bool Connected { get; } + bool IsBound { get; } + + AddressFamily AddressFamily { get; } + SocketType SocketType { get; } + ProtocolType ProtocolType { get; } + + bool Blocking { get; set; } + int Available { get; } + + int Receive(Span buffer); + int Receive(Span buffer, SocketFlags flags); + int Receive(Span buffer, SocketFlags flags, out SocketError socketError); + int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEP); + + int Send(ReadOnlySpan buffer); + int Send(ReadOnlySpan buffer, SocketFlags flags); + int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError); + int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP); + + bool Poll(int microSeconds, SelectMode mode); + + ISocketImpl Accept(); + + void Bind(EndPoint localEP); + void Connect(EndPoint remoteEP); + void Listen(int backlog); + + void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue); + void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue); + void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue); + + void Shutdown(SocketShutdown how); + void Disconnect(bool reuseSocket); + void Close(); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs new file mode 100644 index 000000000..485a7f86b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs @@ -0,0 +1,74 @@ +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy +{ + static class SocketHelpers + { + private static LdnProxy _proxy; + + public static void Select(List readEvents, List writeEvents, List errorEvents, int timeout) + { + var readDefault = readEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList(); + var writeDefault = writeEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList(); + var errorDefault = errorEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList(); + + if (readDefault.Count != 0 || writeDefault.Count != 0 || errorDefault.Count != 0) + { + Socket.Select(readDefault, writeDefault, errorDefault, timeout); + } + + void FilterSockets(List removeFrom, List selectedSockets, Func ldnCheck) + { + removeFrom.RemoveAll(socket => + { + switch (socket) + { + case DefaultSocket dsocket: + return !selectedSockets.Contains(dsocket.BaseSocket); + case LdnProxySocket psocket: + return !ldnCheck(psocket); + default: + throw new NotImplementedException(); + } + }); + }; + + FilterSockets(readEvents, readDefault, (socket) => socket.Readable); + FilterSockets(writeEvents, writeDefault, (socket) => socket.Writable); + FilterSockets(errorEvents, errorDefault, (socket) => socket.Error); + } + + public static void RegisterProxy(LdnProxy proxy) + { + if (_proxy != null) + { + UnregisterProxy(); + } + + _proxy = proxy; + } + + public static void UnregisterProxy() + { + _proxy?.Dispose(); + _proxy = null; + } + + public static ISocketImpl CreateSocket(AddressFamily domain, SocketType type, ProtocolType protocol, string lanInterfaceId) + { + if (_proxy != null) + { + if (_proxy.Supported(domain, type, protocol)) + { + return new LdnProxySocket(domain, type, protocol, _proxy); + } + } + + return new DefaultSocket(domain, type, protocol, lanInterfaceId); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs index 39af90383..5b2de13f0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs @@ -292,7 +292,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres { string host = MemoryHelper.ReadAsciiString(context.Memory, inputBufferPosition, (int)inputBufferSize); - if (!context.Device.Configuration.EnableInternetAccess) + if (host != "localhost" && !context.Device.Configuration.EnableInternetAccess) { Logger.Info?.Print(LogClass.ServiceSfdnsres, $"Guest network access disabled, DNS Blocked: {host}"); diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs index 8cc761baf..dc33dd6a5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs @@ -1,5 +1,6 @@ using Ryujinx.HLE.HOS.Services.Sockets.Bsd; using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; using Ryujinx.HLE.HOS.Services.Ssl.Types; using System; using System.IO; @@ -116,7 +117,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService public ResultCode Handshake(string hostName) { StartSslOperation(); - _stream = new SslStream(new NetworkStream(((ManagedSocket)Socket).Socket, false), false, null, null); + _stream = new SslStream(new NetworkStream(((DefaultSocket)((ManagedSocket)Socket).Socket).BaseSocket, false), false, null, null); hostName = RetrieveHostName(hostName); _stream.AuthenticateAsClient(hostName, null, TranslateSslVersion(_sslVersion), false); EndSslOperation(); diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index 10561a5a1..e187b2360 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -85,8 +85,8 @@ namespace Ryujinx.HLE.Loaders.Processes } // TODO: LibHac npdm currently doesn't support version field. - string version = ProgramId > 0x0100000000007FFF - ? DisplayVersion + string version = ProgramId > 0x0100000000007FFF + ? DisplayVersion : device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?"; Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {Name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]"); diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index a7bb3cd7f..5f7f6db69 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index 78cdb7718..e3bbd1e51 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -577,7 +577,10 @@ namespace Ryujinx.Headless.SDL2 options.AudioVolume, options.UseHypervisor ?? true, options.MultiplayerLanInterfaceId, - Common.Configuration.Multiplayer.MultiplayerMode.Disabled); + Common.Configuration.Multiplayer.MultiplayerMode.Disabled, + false, + "", + ""); return new Switch(configuration); } diff --git a/src/Ryujinx.UI.Common/App/ApplicationData.cs b/src/Ryujinx.UI.Common/App/ApplicationData.cs index 4c1c1a043..7aa0dccaa 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationData.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationData.cs @@ -27,6 +27,8 @@ namespace Ryujinx.UI.App.Common public ulong Id { get; set; } public string Developer { get; set; } = "Unknown"; public string Version { get; set; } = "0"; + public int PlayerCount { get; set; } + public int GameCount { get; set; } public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } public string FileExtension { get; set; } diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 044eccbea..174db51ad 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -12,6 +12,7 @@ using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; @@ -27,10 +28,12 @@ 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; +using System.Threading.Tasks; using ContentType = LibHac.Ncm.ContentType; using MissingKeyException = LibHac.Common.Keys.MissingKeyException; using Path = System.IO.Path; @@ -41,8 +44,10 @@ namespace Ryujinx.UI.App.Common { public class ApplicationLibrary { + public static string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com"; public Language DesiredLanguage { get; set; } public event EventHandler ApplicationCountUpdated; + public event EventHandler LdnGameDataReceived; public readonly IObservableCache Applications; public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates; @@ -62,6 +67,7 @@ namespace Ryujinx.UI.App.Common private readonly SourceCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> _downloadableContents = new(it => it.Dlc); private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly LdnGameDataSerializerContext _ldnDataSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel) { @@ -687,7 +693,7 @@ namespace Ryujinx.UI.App.Common (Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0) || (Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI) || (Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA) || - (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO) || + (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO) || (Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO) ); @@ -719,6 +725,7 @@ namespace Ryujinx.UI.App.Common } } + // Loops through applications list, creating a struct and then firing an event containing the struct for each application foreach (string applicationPath in applicationPaths) { @@ -775,6 +782,46 @@ namespace Ryujinx.UI.App.Common } } + public async Task RefreshLdn() + { + + if (ConfigurationState.Instance.Multiplayer.Mode == MultiplayerMode.LdnRyu) + { + try + { + string ldnWebHost = ConfigurationState.Instance.Multiplayer.LdnServer; + if (string.IsNullOrEmpty(ldnWebHost)) + { + ldnWebHost = DefaultLanPlayWebHost; + } + IEnumerable ldnGameDataArray = Array.Empty(); + using HttpClient httpClient = new HttpClient(); + string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games"); + ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData); + var evt = new LdnGameDataReceivedEventArgs + { + LdnData = ldnGameDataArray + }; + LdnGameDataReceived?.Invoke(null, evt); + } + catch (Exception ex) + { + Logger.Warning?.Print(LogClass.Application, $"Failed to fetch the public games JSON from the API. Player and game count in the game list will be unavailable.\n{ex.Message}"); + LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs() + { + LdnData = Array.Empty() + }); + } + } + else + { + LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs() + { + LdnData = Array.Empty() + }); + } + } + // Replace the currently stored DLC state for the game with the provided DLC state. public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs) { diff --git a/src/Ryujinx.UI.Common/App/LdnGameData.cs b/src/Ryujinx.UI.Common/App/LdnGameData.cs new file mode 100644 index 000000000..6c784c991 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/LdnGameData.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Ryujinx.UI.App.Common +{ + public struct LdnGameData + { + public string Id { get; set; } + public int PlayerCount { get; set; } + public int MaxPlayerCount { get; set; } + public string GameName { get; set; } + public string TitleId { get; set; } + public string Mode { get; set; } + public string Status { get; set; } + public IEnumerable Players { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs b/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs new file mode 100644 index 000000000..7c7454411 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace Ryujinx.UI.App.Common +{ + public class LdnGameDataReceivedEventArgs : EventArgs + { + public IEnumerable LdnData { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs b/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs new file mode 100644 index 000000000..ce8edcdb6 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.App.Common +{ + [JsonSerializable(typeof(IEnumerable))] + internal partial class LdnGameDataSerializerContext : JsonSerializerContext + { + + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index 77c6346f2..80ba1b186 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -392,6 +392,21 @@ namespace Ryujinx.UI.Common.Configuration /// public string MultiplayerLanInterfaceId { get; set; } + /// + /// Disable P2p Toggle + /// + public bool MultiplayerDisableP2p { get; set; } + + /// + /// Local network passphrase, for private networks. + /// + public string MultiplayerLdnPassphrase { get; set; } + + /// + /// Custom LDN Server + /// + public string LdnServer { get; set; } + /// /// Uses Hypervisor over JIT if available /// diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs index 18326a3a4..65dd88106 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Keyboard; @@ -703,6 +703,9 @@ namespace Ryujinx.UI.Common.Configuration Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId; Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode; + Multiplayer.DisableP2p.Value = configurationFileFormat.MultiplayerDisableP2p; + Multiplayer.LdnPassphrase.Value = configurationFileFormat.MultiplayerLdnPassphrase; + Multiplayer.LdnServer.Value = configurationFileFormat.LdnServer; if (configurationFileUpdated) { diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs index 2b43b2032..9be8f4df7 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs @@ -13,7 +13,7 @@ namespace Ryujinx.UI.Common.Configuration { public partial class ConfigurationState { - /// + /// /// UI configuration section /// public class UISection @@ -25,6 +25,7 @@ namespace Ryujinx.UI.Common.Configuration public ReactiveObject AppColumn { get; private set; } public ReactiveObject DevColumn { get; private set; } public ReactiveObject VersionColumn { get; private set; } + public ReactiveObject LdnInfoColumn { get; private set; } public ReactiveObject TimePlayedColumn { get; private set; } public ReactiveObject LastPlayedColumn { get; private set; } public ReactiveObject FileExtColumn { get; private set; } @@ -38,6 +39,7 @@ namespace Ryujinx.UI.Common.Configuration AppColumn = new ReactiveObject(); DevColumn = new ReactiveObject(); VersionColumn = new ReactiveObject(); + LdnInfoColumn = new ReactiveObject(); TimePlayedColumn = new ReactiveObject(); LastPlayedColumn = new ReactiveObject(); FileExtColumn = new ReactiveObject(); @@ -572,11 +574,32 @@ namespace Ryujinx.UI.Common.Configuration /// public ReactiveObject Mode { get; private set; } + /// + /// Disable P2P + /// + public ReactiveObject DisableP2p { get; private set; } + + /// + /// LDN PassPhrase + /// + public ReactiveObject LdnPassphrase { get; private set; } + + /// + /// LDN Server + /// + public ReactiveObject LdnServer { get; private set; } + public MultiplayerSection() { LanInterfaceId = new ReactiveObject(); Mode = new ReactiveObject(); Mode.LogChangesToValue(nameof(MultiplayerMode)); + DisableP2p = new ReactiveObject(); + DisableP2p.LogChangesToValue(nameof(DisableP2p)); + LdnPassphrase = new ReactiveObject(); + LdnPassphrase.LogChangesToValue(nameof(LdnPassphrase)); + LdnServer = new ReactiveObject(); + LdnServer.LogChangesToValue(nameof(LdnServer)); } } diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 6fd5cfb93..b3012568e 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -21,11 +21,11 @@ namespace Ryujinx.UI.Common.Configuration if (Instance != null) { throw new InvalidOperationException("Configuration is already initialized"); - } + } Instance = new ConfigurationState(); - } - + } + public ConfigurationFileFormat ToFileFormat() { ConfigurationFileFormat configurationFile = new() @@ -87,6 +87,7 @@ namespace Ryujinx.UI.Common.Configuration AppColumn = UI.GuiColumns.AppColumn, DevColumn = UI.GuiColumns.DevColumn, VersionColumn = UI.GuiColumns.VersionColumn, + LdnInfoColumn = UI.GuiColumns.LdnInfoColumn, TimePlayedColumn = UI.GuiColumns.TimePlayedColumn, LastPlayedColumn = UI.GuiColumns.LastPlayedColumn, FileExtColumn = UI.GuiColumns.FileExtColumn, @@ -136,6 +137,9 @@ namespace Ryujinx.UI.Common.Configuration PreferredGpu = Graphics.PreferredGpu, MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId, MultiplayerMode = Multiplayer.Mode, + MultiplayerDisableP2p = Multiplayer.DisableP2p, + MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase, + LdnServer = Multiplayer.LdnServer, }; return configurationFile; @@ -195,6 +199,9 @@ namespace Ryujinx.UI.Common.Configuration System.UseHypervisor.Value = true; Multiplayer.LanInterfaceId.Value = "0"; Multiplayer.Mode.Value = MultiplayerMode.Disabled; + Multiplayer.DisableP2p.Value = false; + Multiplayer.LdnPassphrase.Value = ""; + Multiplayer.LdnServer.Value = ""; UI.GuiColumns.FavColumn.Value = true; UI.GuiColumns.IconColumn.Value = true; UI.GuiColumns.AppColumn.Value = true; @@ -307,5 +314,5 @@ namespace Ryujinx.UI.Common.Configuration return GraphicsBackend.OpenGl; } - } -} + } + } diff --git a/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs b/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs index c778ef1f1..c486492e0 100644 --- a/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs +++ b/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs @@ -7,6 +7,7 @@ namespace Ryujinx.UI.Common.Configuration.UI public bool AppColumn { get; set; } public bool DevColumn { get; set; } public bool VersionColumn { get; set; } + public bool LdnInfoColumn { get; set; } public bool TimePlayedColumn { get; set; } public bool LastPlayedColumn { get; set; } public bool FileExtColumn { get; set; } diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index dc4f4ff36..7246be4b9 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -207,6 +207,9 @@ namespace Ryujinx.Ava ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState; + ConfigurationState.Instance.Multiplayer.LdnPassphrase.Event += UpdateLdnPassphraseState; + ConfigurationState.Instance.Multiplayer.LdnServer.Event += UpdateLdnServerState; + ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateDisableP2pState; _gpuCancellationTokenSource = new CancellationTokenSource(); _gpuDoneEvent = new ManualResetEvent(false); @@ -491,6 +494,21 @@ namespace Ryujinx.Ava Device.Configuration.MultiplayerMode = e.NewValue; } + private void UpdateLdnPassphraseState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerLdnPassphrase = e.NewValue; + } + + private void UpdateLdnServerState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerLdnServer = e.NewValue; + } + + private void UpdateDisableP2pState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerDisableP2p = e.NewValue; + } + public void ToggleVSync() { Device.EnableDeviceVsync = !Device.EnableDeviceVsync; @@ -863,10 +881,11 @@ namespace Ryujinx.Ava ConfigurationState.Instance.Graphics.AspectRatio, ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, - ConfigurationState.Instance.Multiplayer.LanInterfaceId, - ConfigurationState.Instance.Multiplayer.Mode - ) - ); + ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Multiplayer.DisableP2p, + ConfigurationState.Instance.Multiplayer.LdnPassphrase, + ConfigurationState.Instance.Multiplayer.LdnServer)); } private static IHardwareDeviceDriver InitializeAudio() @@ -1050,7 +1069,7 @@ namespace Ryujinx.Ava string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld]; UpdateShaderCount(); - + if (GraphicsConfig.ResScale != 1) { dockedMode += $" ({GraphicsConfig.ResScale}x)"; diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 88eb43ce4..fdd2d4df2 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -848,5 +848,17 @@ "MultiplayerMode": "Mode:", "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", "MultiplayerModeDisabled": "Disabled", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Common/LocaleManager.cs b/src/Ryujinx/Common/LocaleManager.cs index 3247a55f8..b57caa468 100644 --- a/src/Ryujinx/Common/LocaleManager.cs +++ b/src/Ryujinx/Common/LocaleManager.cs @@ -99,7 +99,7 @@ namespace Ryujinx.Ava.Common.Locale _ => false }; - public static string FormatDynamicValue(LocaleKeys key, params object[] values) + public static string FormatDynamicValue(LocaleKeys key, params object[] values) => Instance.UpdateAndGetDynamicValue(key, values); public string UpdateAndGetDynamicValue(LocaleKeys key, params object[] values) diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 76512a34a..f78b8e972 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -103,7 +103,7 @@ namespace Ryujinx.Ava Console.Title = $"{App.FullAppName} Console {Version}"; // Hook unhandled exception and process exit events. - AppDomain.CurrentDomain.UnhandledException += (sender, e) + AppDomain.CurrentDomain.UnhandledException += (sender, e) => ProcessUnhandledException(sender, e.ExceptionObject as Exception, e.IsTerminating); AppDomain.CurrentDomain.ProcessExit += (_, _) => Exit(); @@ -229,11 +229,9 @@ namespace Ryujinx.Ava var enabledLogLevels = Logger.GetEnabledLevels().ToArray(); - Logger.Notice.Print(LogClass.Application, $"Logs Enabled: { - (enabledLogLevels.Length is 0 + Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(enabledLogLevels.Length is 0 ? "" - : enabledLogLevels.JoinToString(", ")) - }"); + : enabledLogLevels.JoinToString(", "))}"); Logger.Notice.Print(LogClass.Application, AppDataManager.Mode == AppDataManager.LaunchMode.Custom @@ -245,13 +243,13 @@ namespace Ryujinx.Ava { Logger.Log log = Logger.Error ?? Logger.Notice; string message = $"Unhandled exception caught: {ex}"; - + // ReSharper disable once ConstantConditionalAccessQualifier - if (sender?.GetType()?.AsPrettyString() is {} senderName) + if (sender?.GetType()?.AsPrettyString() is { } senderName) log.Print(LogClass.Application, message, senderName); else log.PrintMsg(LogClass.Application, message); - + if (isTerminating) Exit(); } diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs index 1dbf37255..2ebba7ac0 100644 --- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs +++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs @@ -31,7 +31,7 @@ namespace Ryujinx.Ava.UI.Applet public bool DisplayMessageDialog(ControllerAppletUIArgs args) { ManualResetEvent dialogCloseEvent = new(false); - + bool okPressed = false; if (ConfigurationState.Instance.IgnoreApplet) diff --git a/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs b/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs index 5ec7737ed..0cd3f18e5 100644 --- a/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs +++ b/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs @@ -24,7 +24,7 @@ namespace Ryujinx.Ava.UI.Applet public AvaloniaDynamicTextInputHandler(MainWindow parent) { _parent = parent; - + if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver) { avaloniaKeyboardDriver.KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed; @@ -121,7 +121,7 @@ namespace Ryujinx.Ava.UI.Applet avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease; avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput; } - + _textChangedSubscription?.Dispose(); _selectionStartChangedSubscription?.Dispose(); _selectionEndtextChangedSubscription?.Dispose(); diff --git a/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs b/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs index 3a8350893..ee0e884d2 100644 --- a/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs +++ b/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs @@ -37,8 +37,8 @@ namespace Ryujinx.Ava.UI.Applet public ControllerAppletDialog(MainWindow mainWindow, ControllerAppletUIArgs args) { - PlayerCount = args.PlayerCountMin == args.PlayerCountMax - ? args.PlayerCountMin.ToString() + PlayerCount = args.PlayerCountMin == args.PlayerCountMax + ? args.PlayerCountMin.ToString() : $"{args.PlayerCountMin} - {args.PlayerCountMax}"; SupportsProController = (args.SupportedStyles & ControllerType.ProController) != 0; diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml index ea748a3bf..0daa77ac4 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -7,6 +7,7 @@ xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:converters="clr-namespace:Avalonia.Data.Converters;assembly=Avalonia.Base" d:DesignHeight="450" d:DesignWidth="800" Focusable="True" @@ -110,6 +111,11 @@ Text="{Binding FileExtension}" TextAlignment="Start" TextWrapping="Wrap" /> + UserFirmwareAvatarSelectorViewModel.PreloadAvatars(contentManager, virtualFileSystem)); - + InitializeComponent(); } @@ -60,13 +60,13 @@ namespace Ryujinx.Ava.UI.Controls LoadProfiles(); } - public void Navigate(Type sourcePageType, object parameter) + public void Navigate(Type sourcePageType, object parameter) => ContentFrame.Navigate(sourcePageType, parameter); public static async Task Show( - AccountManager ownerAccountManager, + AccountManager ownerAccountManager, ContentManager ownerContentManager, - VirtualFileSystem ownerVirtualFileSystem, + VirtualFileSystem ownerVirtualFileSystem, HorizonClient ownerHorizonClient) { var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient); @@ -158,9 +158,9 @@ namespace Ryujinx.Ava.UI.Controls _ = Dispatcher.UIThread.InvokeAsync(async () => await ContentDialogHelper.CreateErrorDialog( LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage])); - + return; - } + } AccountManager.OpenUser(profile.UserId); } diff --git a/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs b/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs index 94c3ab35d..6196421c8 100644 --- a/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs +++ b/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs @@ -22,9 +22,9 @@ namespace Ryujinx.Ava.UI.Helpers _key = key; } - public string this[string key] => + public string this[string key] => _glyphs.TryGetValue(Enum.Parse(key), out var val) - ? val + ? val : string.Empty; public override object ProvideValue(IServiceProvider serviceProvider) => this[_key]; diff --git a/src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs new file mode 100644 index 000000000..8bd8b5f0d --- /dev/null +++ b/src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs @@ -0,0 +1,44 @@ +using Avalonia.Data.Converters; +using Avalonia.Markup.Xaml; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Helper; +using System; +using System.Globalization; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class MultiplayerInfoConverter : MarkupExtension, IValueConverter + { + private static readonly MultiplayerInfoConverter _instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is ApplicationData applicationData) + { + if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0) + { + return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}"; + } + else + { + return ""; + } + } + else + { + return ""; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + return _instance; + } + } +} diff --git a/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs b/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs index 5edc6482e..0e5525c7b 100644 --- a/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs +++ b/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs @@ -9,12 +9,12 @@ namespace Ryujinx.Ava.UI.Helpers { public static TimeZoneConverter Instance = new(); - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - => value is TimeZone timeZone - ? $"{timeZone.UtcDifference} {timeZone.Location} {timeZone.Abbreviation}" + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is TimeZone timeZone + ? $"{timeZone.UtcDifference} {timeZone.Location} {timeZone.Abbreviation}" : null; - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); } } diff --git a/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs b/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs index f12cf0aa6..40f783c44 100644 --- a/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs +++ b/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs @@ -10,7 +10,7 @@ namespace Ryujinx.Ava.UI.Models public string DockedMode { get; } public string FifoStatus { get; } public string GameStatus { get; } - + public uint ShaderCount { get; } public StatusUpdatedEventArgs(bool vSyncEnabled, string volumeStatus, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, uint shaderCount) diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 437f9861d..53263847b 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -117,6 +117,8 @@ namespace Ryujinx.Ava.UI.ViewModels public ApplicationData ListSelectedApplication; public ApplicationData GridSelectedApplication; + public IEnumerable LastLdnGameData; + public static readonly Bitmap IconBitmap = new(Assembly.GetAssembly(typeof(ConfigurationState))!.GetManifestResourceStream("Ryujinx.UI.Common.Resources.Logo_Ryujinx.png")!); @@ -173,7 +175,7 @@ namespace Ryujinx.Ava.UI.ViewModels SwitchToGameControl = switchToGameControl; SetMainContent = setMainContent; TopLevel = topLevel; - + #if DEBUG topLevel.AttachDevTools(new KeyGesture(Avalonia.Input.Key.F12, KeyModifiers.Control)); #endif @@ -268,7 +270,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool ShowFirmwareStatus => !ShowLoadProgress; - public bool ShowRightmostSeparator + public bool ShowRightmostSeparator { get => _showRightmostSeparator; set @@ -553,7 +555,7 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(); } } - + public string ShaderCountText { get => _shaderCountText; @@ -1021,7 +1023,7 @@ namespace Ryujinx.Ava.UI.ViewModels ? SortExpressionComparer.Ascending(selector) : SortExpressionComparer.Descending(selector); - private IComparer GetComparer() + private IComparer GetComparer() => SortMode switch { #pragma warning disable IDE0055 // Disable formatting @@ -1251,7 +1253,7 @@ namespace Ryujinx.Ava.UI.ViewModels private void InitializeGame() { RendererHostControl.WindowCreated += RendererHost_Created; - + AppHost.StatusUpdatedEvent += Update_StatusBar; AppHost.AppExit += AppHost_AppExit; @@ -1300,9 +1302,9 @@ namespace Ryujinx.Ava.UI.ViewModels GameStatusText = args.GameStatus; VolumeStatusText = args.VolumeStatus; FifoStatusText = args.FifoStatus; - - ShaderCountText = (ShowRightmostSeparator = args.ShaderCount > 0) - ? $"{LocaleManager.Instance[LocaleKeys.CompilingShaders]}: {args.ShaderCount}" + + ShaderCountText = (ShowRightmostSeparator = args.ShaderCount > 0) + ? $"{LocaleManager.Instance[LocaleKeys.CompilingShaders]}: {args.ShaderCount}" : string.Empty; ShowStatusSeparator = true; @@ -1707,7 +1709,7 @@ namespace Ryujinx.Ava.UI.ViewModels RendererHostControl.Focus(); }); - public static void UpdateGameMetadata(string titleId) + public static void UpdateGameMetadata(string titleId) => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame()); public void RefreshFirmwareStatus() diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index f069896f8..2da252d00 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -25,12 +25,13 @@ using System.Collections.ObjectModel; using System.Linq; using System.Net.NetworkInformation; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using System.Threading.Tasks; using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; namespace Ryujinx.Ava.UI.ViewModels { - public class SettingsViewModel : BaseModel + public partial class SettingsViewModel : BaseModel { private readonly VirtualFileSystem _virtualFileSystem; private readonly ContentManager _contentManager; @@ -56,6 +57,8 @@ namespace Ryujinx.Ava.UI.ViewModels public event Action SaveSettingsEvent; private int _networkInterfaceIndex; private int _multiplayerModeIndex; + private string _ldnPassphrase; + private string _LdnServer; public int ResolutionScale { @@ -180,10 +183,24 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsVulkanSelected => GraphicsBackendIndex == 0; public bool UseHypervisor { get; set; } + public bool DisableP2P { get; set; } public string TimeZone { get; set; } public string ShaderDumpPath { get; set; } + public string LdnPassphrase + { + get => _ldnPassphrase; + set + { + _ldnPassphrase = value; + IsInvalidLdnPassphraseVisible = !ValidateLdnPassphrase(value); + + OnPropertyChanged(); + OnPropertyChanged(nameof(IsInvalidLdnPassphraseVisible)); + } + } + public int Language { get; set; } public int Region { get; set; } public int FsGlobalAccessLogMode { get; set; } @@ -276,6 +293,21 @@ namespace Ryujinx.Ava.UI.ViewModels } } + [GeneratedRegex("Ryujinx-[0-9a-f]{8}")] + private static partial Regex LdnPassphraseRegex(); + + public bool IsInvalidLdnPassphraseVisible { get; set; } + + public string LdnServer + { + get => _LdnServer; + set + { + _LdnServer = value; + OnPropertyChanged(); + } + } + public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() { _virtualFileSystem = virtualFileSystem; @@ -393,6 +425,11 @@ namespace Ryujinx.Ava.UI.ViewModels Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(NetworkInterfaceIndex))); } + private bool ValidateLdnPassphrase(string passphrase) + { + return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && LdnPassphraseRegex().IsMatch(passphrase)); + } + public void ValidateAndSetTimeZone(string location) { if (_validTzRegions.Contains(location)) @@ -497,6 +534,9 @@ namespace Ryujinx.Ava.UI.ViewModels OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value; + DisableP2P = config.Multiplayer.DisableP2p.Value; + LdnPassphrase = config.Multiplayer.LdnPassphrase.Value; + LdnServer = config.Multiplayer.LdnServer.Value; } public void SaveSettings() @@ -613,6 +653,9 @@ namespace Ryujinx.Ava.UI.ViewModels config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]]; config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex; + config.Multiplayer.DisableP2p.Value = DisableP2P; + config.Multiplayer.LdnPassphrase.Value = LdnPassphrase; + config.Multiplayer.LdnServer.Value = LdnServer; config.ToFileFormat().SaveConfig(Program.ConfigurationPath); diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index 1acee3af5..ce4d9fd59 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -40,8 +40,8 @@ namespace Ryujinx.Ava.UI.Views.Main private CheckBox[] GenerateToggleFileTypeItems() => Enum.GetValues() .Select(it => (FileName: Enum.GetName(it)!, FileType: it)) - .Select(it => - new CheckBox + .Select(it => + new CheckBox { Content = $".{it.FileName}", IsChecked = it.FileType.GetConfigValue(ConfigurationState.Instance.UI.ShownFileTypes), diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml index c7736bf8d..2fc59f04d 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs index b771933eb..c69307522 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs @@ -1,12 +1,29 @@ using Avalonia.Controls; +using Avalonia.Interactivity; +using Ryujinx.Ava.UI.ViewModels; +using System; namespace Ryujinx.Ava.UI.Views.Settings { public partial class SettingsNetworkView : UserControl { + public SettingsViewModel ViewModel; + public SettingsNetworkView() { InitializeComponent(); } + + private void GenLdnPassButton_OnClick(object sender, RoutedEventArgs e) + { + byte[] code = new byte[4]; + new Random().NextBytes(code); + ViewModel.LdnPassphrase = $"Ryujinx-{BitConverter.ToUInt32(code):x8}"; + } + + private void ClearLdnPassButton_OnClick(object sender, RoutedEventArgs e) + { + ViewModel.LdnPassphrase = ""; + } } } diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 24a1b62a2..a43c29518 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -154,6 +154,36 @@ namespace Ryujinx.Ava.UI.Windows }); } + private void ApplicationLibrary_LdnGameDataReceived(object sender, LdnGameDataReceivedEventArgs e) + { + Dispatcher.UIThread.Post(() => + { + var ldnGameDataArray = e.LdnData; + ViewModel.LastLdnGameData = ldnGameDataArray; + foreach (var application in ViewModel.Applications) + { + UpdateApplicationWithLdnData(application); + } + ViewModel.RefreshView(); + }); + } + + private void UpdateApplicationWithLdnData(ApplicationData application) + { + if (application.ControlHolder.ByteSpan.Length > 0 && ViewModel.LastLdnGameData != null) + { + IEnumerable ldnGameData = ViewModel.LastLdnGameData.Where(game => application.ControlHolder.Value.LocalCommunicationId.Items.Contains(Convert.ToUInt64(game.TitleId, 16))); + + application.PlayerCount = ldnGameData.Sum(game => game.PlayerCount); + application.GameCount = ldnGameData.Count(); + } + else + { + application.PlayerCount = 0; + application.GameCount = 0; + } + } + public void Application_Opened(object sender, ApplicationOpenedEventArgs args) { if (args.Application != null) @@ -450,7 +480,20 @@ namespace Ryujinx.Ava.UI.Windows .Connect() .ObserveOn(SynchronizationContext.Current!) .Bind(ViewModel.Applications) + .OnItemAdded(UpdateApplicationWithLdnData) .Subscribe(); + ApplicationLibrary.LdnGameDataReceived += ApplicationLibrary_LdnGameDataReceived; + + ConfigurationState.Instance.Multiplayer.Mode.Event += (sender, evt) => + { + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); + }; + + ConfigurationState.Instance.Multiplayer.LdnServer.Event += (sender, evt) => + { + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); + }; + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); ViewModel.RefreshFirmwareStatus(); @@ -459,7 +502,7 @@ namespace Ryujinx.Ava.UI.Windows { LoadApplications(); } - + _ = CheckLaunchState(); } @@ -588,13 +631,26 @@ namespace Ryujinx.Ava.UI.Windows { switch (fileType) { - case "NSP": ConfigurationState.Instance.UI.ShownFileTypes.NSP.Toggle(); break; - case "PFS0": ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Toggle(); break; - case "XCI": ConfigurationState.Instance.UI.ShownFileTypes.XCI.Toggle(); break; - case "NCA": ConfigurationState.Instance.UI.ShownFileTypes.NCA.Toggle(); break; - case "NRO": ConfigurationState.Instance.UI.ShownFileTypes.NRO.Toggle(); break; - case "NSO": ConfigurationState.Instance.UI.ShownFileTypes.NSO.Toggle(); break; - default: throw new ArgumentOutOfRangeException(fileType); + case "NSP": + ConfigurationState.Instance.UI.ShownFileTypes.NSP.Toggle(); + break; + case "PFS0": + ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Toggle(); + break; + case "XCI": + ConfigurationState.Instance.UI.ShownFileTypes.XCI.Toggle(); + break; + case "NCA": + ConfigurationState.Instance.UI.ShownFileTypes.NCA.Toggle(); + break; + case "NRO": + ConfigurationState.Instance.UI.ShownFileTypes.NRO.Toggle(); + break; + case "NSO": + ConfigurationState.Instance.UI.ShownFileTypes.NSO.Toggle(); + break; + default: + throw new ArgumentOutOfRangeException(fileType); } ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs index 1a177d182..d8c88bed8 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs @@ -80,6 +80,7 @@ namespace Ryujinx.Ava.UI.Windows NavPanel.Content = AudioPage; break; case "NetworkPage": + NetworkPage.ViewModel = ViewModel; NavPanel.Content = NetworkPage; break; case "LoggingPage": diff --git a/src/Ryujinx/UI/Windows/StyleableWindow.cs b/src/Ryujinx/UI/Windows/StyleableWindow.cs index 493214ee2..9e4eed2e4 100644 --- a/src/Ryujinx/UI/Windows/StyleableWindow.cs +++ b/src/Ryujinx/UI/Windows/StyleableWindow.cs @@ -17,7 +17,7 @@ namespace Ryujinx.Ava.UI.Windows LocaleManager.Instance.LocaleChanged += LocaleChanged; LocaleChanged(); - + Icon = MainWindowViewModel.IconBitmap; } From e1dfb48e23cea779d3f6280bcd3f5916aeee3e55 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Mon, 11 Nov 2024 18:21:52 -0600 Subject: [PATCH 19/20] misc: Specify Normal or Canary in Version log line --- src/Ryujinx/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index f78b8e972..05fd66b90 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -224,7 +224,7 @@ namespace Ryujinx.Ava private static void PrintSystemInfo() { - Logger.Notice.Print(LogClass.Application, $"Ryujinx Version: {Version}"); + Logger.Notice.Print(LogClass.Application, $"{App.FullAppName} Version: {Version}"); SystemInfo.Gather().Print(); var enabledLogLevels = Logger.GetEnabledLevels().ToArray(); From 4cb5946be425477825f14080e9f37088c0b80d99 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Mon, 11 Nov 2024 18:22:14 -0600 Subject: [PATCH 20/20] UI: RPC: Only show hours at maximum for play time --- src/Ryujinx.UI.Common/DiscordIntegrationModule.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs index 03dd9a41e..d4b2a4187 100644 --- a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs +++ b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs @@ -1,11 +1,10 @@ using DiscordRPC; using Humanizer; -using LibHac.Bcat; +using Humanizer.Localisation; using Ryujinx.Common; using Ryujinx.HLE.Loaders.Processes; using Ryujinx.UI.App.Common; using Ryujinx.UI.Common.Configuration; -using System.Collections.Generic; using System.Linq; using System.Text; @@ -78,13 +77,13 @@ namespace Ryujinx.UI.Common Assets = new Assets { LargeImageKey = _discordGameAssetKeys.Contains(procRes.ProgramIdText) ? procRes.ProgramIdText : "game", - LargeImageText = TruncateToByteLength($"{appMeta.Title} | {procRes.DisplayVersion}"), + LargeImageText = TruncateToByteLength($"{appMeta.Title} (v{procRes.DisplayVersion})"), SmallImageKey = "ryujinx", SmallImageText = TruncateToByteLength(_description) }, Details = TruncateToByteLength($"Playing {appMeta.Title}"), State = appMeta.LastPlayed.HasValue && appMeta.TimePlayed.TotalSeconds > 5 - ? $"Total play time: {appMeta.TimePlayed.Humanize(2, false)}" + ? $"Total play time: {appMeta.TimePlayed.Humanize(2, false, maxUnit: TimeUnit.Hour)}" : "Never played", Timestamps = Timestamps.Now });