From b9398f1f3a13612a0bc3e64205cdb7eca3f48d4d Mon Sep 17 00:00:00 2001
From: mageven <62494521+mageven@users.noreply.github.com>
Date: Sun, 30 Aug 2020 22:21:53 +0530
Subject: [PATCH] Allow launching with custom data directories (#1505)

* Allow launching with custom data directories

Don't load alternate keys when using custom directory

* Address gdkchan's comments

* Misc fixes to log levels

Added more enabled log levels by default

Moved successful config updation to Notice as
1. It's not a warning
2. Warnings could've been disabled by the config load and hence message
   would be lost
---
 ARMeilleure/Translation/PTC/Ptc.cs            | 11 +--
 .../Configuration/AppDataManager.cs           | 69 +++++++++++++++++++
 .../Configuration/ConfigurationState.cs       |  2 +-
 Ryujinx.Common/Logging/Logger.cs              |  5 ++
 Ryujinx.HLE/FileSystem/VirtualFileSystem.cs   | 37 +++-------
 Ryujinx.HLE/HOS/ApplicationLoader.cs          |  8 +--
 Ryujinx.HLE/HOS/ModLoader.cs                  |  8 ++-
 Ryujinx/Program.cs                            | 60 +++++++++++-----
 Ryujinx/Ui/ApplicationLibrary.cs              |  4 +-
 Ryujinx/Ui/ControllerWindow.cs                |  3 +-
 Ryujinx/Ui/DlcWindow.cs                       |  2 +-
 Ryujinx/Ui/GameTableContextMenu.cs            | 10 +--
 Ryujinx/Ui/MainWindow.cs                      |  5 +-
 Ryujinx/Ui/Migration.cs                       |  3 +
 Ryujinx/Ui/TitleUpdateWindow.cs               |  2 +-
 15 files changed, 159 insertions(+), 70 deletions(-)
 create mode 100644 Ryujinx.Common/Configuration/AppDataManager.cs

diff --git a/ARMeilleure/Translation/PTC/Ptc.cs b/ARMeilleure/Translation/PTC/Ptc.cs
index ccb3f7054..c6069d887 100644
--- a/ARMeilleure/Translation/PTC/Ptc.cs
+++ b/ARMeilleure/Translation/PTC/Ptc.cs
@@ -1,6 +1,7 @@
 using ARMeilleure.CodeGen;
 using ARMeilleure.CodeGen.Unwinding;
 using ARMeilleure.Memory;
+using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Logging;
 using System;
 using System.Buffers.Binary;
@@ -22,8 +23,6 @@ namespace ARMeilleure.Translation.PTC
 
         private const int InternalVersion = 1471; //! To be incremented manually for each change to the ARMeilleure project.
 
-        private const string BaseDir = "Ryujinx";
-
         private const string ActualDir = "0";
         private const string BackupDir = "1";
 
@@ -49,8 +48,6 @@ namespace ARMeilleure.Translation.PTC
 
         private static readonly AutoResetEvent _loggerEvent;
 
-        private static readonly string _basePath;
-
         private static readonly object _lock;
 
         private static bool _disposed;
@@ -83,8 +80,6 @@ namespace ARMeilleure.Translation.PTC
 
             _loggerEvent = new AutoResetEvent(false);
 
-            _basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), BaseDir);
-
             _lock = new object();
 
             _disposed = false;
@@ -130,8 +125,8 @@ namespace ARMeilleure.Translation.PTC
 
             if (enabled)
             {
-                string workPathActual = Path.Combine(_basePath, "games", TitleIdText, "cache", "cpu", ActualDir);
-                string workPathBackup = Path.Combine(_basePath, "games", TitleIdText, "cache", "cpu", BackupDir);
+                string workPathActual = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", ActualDir);
+                string workPathBackup = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", BackupDir);
 
                 if (!Directory.Exists(workPathActual))
                 {
diff --git a/Ryujinx.Common/Configuration/AppDataManager.cs b/Ryujinx.Common/Configuration/AppDataManager.cs
new file mode 100644
index 000000000..6630026f0
--- /dev/null
+++ b/Ryujinx.Common/Configuration/AppDataManager.cs
@@ -0,0 +1,69 @@
+using Ryujinx.Common.Logging;
+using System;
+using System.IO;
+
+namespace Ryujinx.Common.Configuration
+{
+    public static class AppDataManager
+    {
+        private static readonly string _defaultBaseDirPath;
+
+        private const string DefaultBaseDir = "Ryujinx";
+
+        // The following 3 are always part of Base Directory
+        private const string GamesDir = "games";
+        private const string ProfilesDir = "profiles";
+        private const string KeysDir = "system";
+
+        public static bool IsCustomBasePath { get; private set; }
+        public static string BaseDirPath { get; private set; }
+        public static string GamesDirPath { get; private set; }
+        public static string ProfilesDirPath { get; private set; }
+        public static string KeysDirPath { get; private set; }
+        public static string KeysDirPathAlt { get; }
+
+        public const string DefaultNandDir = "bis";
+        public const string DefaultSdcardDir = "sdcard";
+        private const string DefaultModsDir = "mods";
+
+        public static string CustomModsPath { get; set; }
+        public static string CustomNandPath { get; set; } // TODO: Actually implement this into VFS
+        public static string CustomSdCardPath { get; set; } // TODO: Actually implement this into VFS
+
+        static AppDataManager()
+        {
+            _defaultBaseDirPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), DefaultBaseDir);
+            KeysDirPathAlt = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".switch");
+        }
+
+        public static void Initialize(string baseDirPath)
+        {
+            BaseDirPath = _defaultBaseDirPath;
+
+            if (baseDirPath != null && baseDirPath != _defaultBaseDirPath)
+            {
+                if (!Directory.Exists(baseDirPath))
+                {
+                    Logger.Error?.Print(LogClass.Application, $"Custom Data Directory '{baseDirPath}' does not exist. Using defaults...");
+                }
+                else
+                {
+                    BaseDirPath = baseDirPath;
+                    IsCustomBasePath = true;
+                }
+            }
+
+            SetupBasePaths();
+        }
+
+        private static void SetupBasePaths()
+        {
+            Directory.CreateDirectory(BaseDirPath);
+            Directory.CreateDirectory(GamesDirPath = Path.Combine(BaseDirPath, GamesDir));
+            Directory.CreateDirectory(ProfilesDirPath = Path.Combine(BaseDirPath, ProfilesDir));
+            Directory.CreateDirectory(KeysDirPath = Path.Combine(BaseDirPath, KeysDir));
+        }
+
+        public static string GetModsPath() => CustomModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultModsDir)).FullName;
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Common/Configuration/ConfigurationState.cs b/Ryujinx.Common/Configuration/ConfigurationState.cs
index 489382cde..7f79dd6e1 100644
--- a/Ryujinx.Common/Configuration/ConfigurationState.cs
+++ b/Ryujinx.Common/Configuration/ConfigurationState.cs
@@ -750,7 +750,7 @@ namespace Ryujinx.Configuration
             {
                 ToFileFormat().SaveConfig(configurationFilePath);
 
-                Common.Logging.Logger.Warning?.Print(LogClass.Application, "Configuration file has been updated!");
+                Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}");
             }
         }
 
diff --git a/Ryujinx.Common/Logging/Logger.cs b/Ryujinx.Common/Logging/Logger.cs
index 9246368c4..e41eaf535 100644
--- a/Ryujinx.Common/Logging/Logger.cs
+++ b/Ryujinx.Common/Logging/Logger.cs
@@ -112,6 +112,11 @@ namespace Ryujinx.Common.Logging
                 AsyncLogTargetOverflowAction.Discard));
 
             Notice = new Log(LogLevel.Notice);
+            
+            // Enable important log levels before configuration is loaded
+            Error = new Log(LogLevel.Error);
+            Warning = new Log(LogLevel.Warning);
+            Info = new Log(LogLevel.Info);
         }
 
         public static void RestartTime()
diff --git a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
index 588909953..a95c9c0f5 100644
--- a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
+++ b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
@@ -4,6 +4,7 @@ using LibHac.Fs;
 using LibHac.FsService;
 using LibHac.FsSystem;
 using LibHac.Spl;
+using Ryujinx.Common.Configuration;
 using Ryujinx.HLE.FileSystem.Content;
 using Ryujinx.HLE.HOS;
 using System;
@@ -13,11 +14,8 @@ namespace Ryujinx.HLE.FileSystem
 {
     public class VirtualFileSystem : IDisposable
     {
-        public const string BasePath   = "Ryujinx";
-        public const string NandPath   = "bis";
-        public const string SdCardPath = "sdcard";
-        public const string SystemPath = "system";
-        public const string ModsPath   = "mods";
+        public const string NandPath   = AppDataManager.DefaultNandDir;
+        public const string SdCardPath = AppDataManager.DefaultSdcardDir;
 
         public static string SafeNandPath   = Path.Combine(NandPath, "safe");
         public static string SystemNandPath = Path.Combine(NandPath, "system");
@@ -77,20 +75,10 @@ namespace Ryujinx.HLE.FileSystem
             return fullPath;
         }
 
-        public string GetBaseModsPath()
-        {
-            var baseModsDir = Path.Combine(GetBasePath(), "mods");
-            ModLoader.EnsureBaseDirStructure(baseModsDir);
-
-            return baseModsDir;
-        }
-
-        public string GetSdCardPath() => MakeFullPath(SdCardPath);
-
+        internal string GetBasePath() => AppDataManager.BaseDirPath;
+        internal string GetSdCardPath() => MakeFullPath(SdCardPath);
         public string GetNandPath() => MakeFullPath(NandPath);
 
-        public string GetSystemPath() => MakeFullPath(SystemPath);
-
         internal string GetSavePath(ServiceCtx context, SaveInfo saveInfo, bool isDirectory = true)
         {
             string saveUserPath   = "";
@@ -207,13 +195,6 @@ namespace Ryujinx.HLE.FileSystem
             return new DriveInfo(Path.GetPathRoot(GetBasePath()));
         }
 
-        public string GetBasePath()
-        {
-            string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
-
-            return Path.Combine(appDataPath, BasePath);
-        }
-
         public void Reload()
         {
             ReloadKeySet();
@@ -245,10 +226,12 @@ namespace Ryujinx.HLE.FileSystem
             string titleKeyFile   = null;
             string consoleKeyFile = null;
 
-            string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+            if (!AppDataManager.IsCustomBasePath)
+            {
+                LoadSetAtPath(AppDataManager.KeysDirPathAlt);
+            }
 
-            LoadSetAtPath(Path.Combine(home, ".switch"));
-            LoadSetAtPath(GetSystemPath());
+            LoadSetAtPath(AppDataManager.KeysDirPath);
 
             void LoadSetAtPath(string basePath)
             {
diff --git a/Ryujinx.HLE/HOS/ApplicationLoader.cs b/Ryujinx.HLE/HOS/ApplicationLoader.cs
index b6e17b7b5..b52678de5 100644
--- a/Ryujinx.HLE/HOS/ApplicationLoader.cs
+++ b/Ryujinx.HLE/HOS/ApplicationLoader.cs
@@ -69,7 +69,7 @@ namespace Ryujinx.HLE.HOS
 
             Npdm metaData = ReadNpdm(codeFs);
 
-            _fileSystem.ModLoader.CollectMods(TitleId, _fileSystem.GetBaseModsPath());
+            _fileSystem.ModLoader.CollectMods(TitleId, _fileSystem.ModLoader.GetModsBasePath());
 
             if (TitleId != 0)
             {
@@ -224,7 +224,7 @@ namespace Ryujinx.HLE.HOS
             IFileSystem codeFs = null;
 
             // Load Update
-            string titleUpdateMetadataPath = Path.Combine(_fileSystem.GetBasePath(), "games", mainNca.Header.TitleId.ToString("x16"), "updates.json");
+            string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json");
 
             if (File.Exists(titleUpdateMetadataPath))
             {
@@ -261,7 +261,7 @@ namespace Ryujinx.HLE.HOS
             }
 
             // Load Aoc
-            string titleAocMetadataPath = Path.Combine(_fileSystem.GetBasePath(), "games", mainNca.Header.TitleId.ToString("x16"), "dlc.json");
+            string titleAocMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "dlc.json");
 
             if (File.Exists(titleAocMetadataPath))
             {
@@ -310,7 +310,7 @@ namespace Ryujinx.HLE.HOS
 
             Npdm metaData = ReadNpdm(codeFs);
 
-            _fileSystem.ModLoader.CollectMods(TitleId, _fileSystem.GetBaseModsPath());
+            _fileSystem.ModLoader.CollectMods(TitleId, _fileSystem.ModLoader.GetModsBasePath());
 
             if (controlNca != null)
             {
diff --git a/Ryujinx.HLE/HOS/ModLoader.cs b/Ryujinx.HLE/HOS/ModLoader.cs
index 867d120f4..786484aea 100644
--- a/Ryujinx.HLE/HOS/ModLoader.cs
+++ b/Ryujinx.HLE/HOS/ModLoader.cs
@@ -2,6 +2,7 @@ using LibHac.Common;
 using LibHac.Fs;
 using LibHac.FsSystem;
 using LibHac.FsSystem.RomFs;
+using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Logging;
 using Ryujinx.HLE.Loaders.Mods;
 using Ryujinx.HLE.Loaders.Executables;
@@ -105,15 +106,18 @@ namespace Ryujinx.HLE.HOS
 
         private static bool StrEquals(string s1, string s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase);
 
-        public void EnsureBaseDirStructure(string modsBasePath)
+        public string GetModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetModsPath());
+
+        private string EnsureBaseDirStructure(string modsBasePath)
         {
             var modsDir = new DirectoryInfo(modsBasePath);
-            modsDir.Create();
 
             modsDir.CreateSubdirectory(AmsContentsDir);
             modsDir.CreateSubdirectory(AmsNsoPatchDir);
             modsDir.CreateSubdirectory(AmsNroPatchDir);
             // modsDir.CreateSubdirectory(AmsKipPatchDir); // uncomment when KIPs are supported
+
+            return modsDir.FullName;
         }
 
         private static DirectoryInfo FindTitleDir(DirectoryInfo contentsDir, string titleId)
diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs
index 0b55d6f8e..5320a1eb6 100644
--- a/Ryujinx/Program.cs
+++ b/Ryujinx/Program.cs
@@ -1,5 +1,6 @@
 using ARMeilleure.Translation.PTC;
 using Gtk;
+using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.SystemInfo;
 using Ryujinx.Configuration;
@@ -20,6 +21,29 @@ namespace Ryujinx
 
         static void Main(string[] args)
         {
+            // Parse Arguments
+            string launchPath = null;
+            string baseDirPath = null;
+            for (int i = 0; i < args.Length; ++i)
+            {
+                string arg = args[i];
+
+                if (arg == "-r" || arg == "--root-data-dir")
+                {
+                    if (i + 1 >= args.Length)
+                    {
+                        Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
+                        continue;
+                    }
+
+                    baseDirPath = args[++i];
+                }
+                else if (launchPath == null)
+                {
+                    launchPath = arg;
+                }
+            }
+
             Toolkit.Init(new ToolkitOptions
             {
                 Backend = PlatformBackend.PreferNative,
@@ -38,6 +62,9 @@ namespace Ryujinx
             AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating);
             AppDomain.CurrentDomain.ProcessExit += (object sender, EventArgs e) => ProgramExit();
 
+            // Setup base data directory
+            AppDataManager.Initialize(baseDirPath);
+
             // Initialize the configuration
             ConfigurationState.Initialize();
 
@@ -47,9 +74,8 @@ namespace Ryujinx
             // Initialize Discord integration
             DiscordIntegrationModule.Initialize();
 
-            string localConfigurationPath  = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json");
-            string globalBasePath          = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Ryujinx");
-            string globalConfigurationPath = Path.Combine(globalBasePath, "Config.json");
+            string localConfigurationPath   = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json");
+            string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, "Config.json");
 
             // Now load the configuration as the other subsystems are now registered
             if (File.Exists(localConfigurationPath))
@@ -60,24 +86,21 @@ namespace Ryujinx
 
                 ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
             }
-            else if (File.Exists(globalConfigurationPath))
+            else if (File.Exists(appDataConfigurationPath))
             {
-                ConfigurationPath = globalConfigurationPath;
+                ConfigurationPath = appDataConfigurationPath;
 
-                ConfigurationFileFormat configurationFileFormat = ConfigurationFileFormat.Load(globalConfigurationPath);
+                ConfigurationFileFormat configurationFileFormat = ConfigurationFileFormat.Load(appDataConfigurationPath);
 
                 ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
             }
             else
             {
                 // No configuration, we load the default values and save it on disk
-                ConfigurationPath = globalConfigurationPath;
-
-                // Make sure to create the Ryujinx directory if needed.
-                Directory.CreateDirectory(globalBasePath);
+                ConfigurationPath = appDataConfigurationPath;
 
                 ConfigurationState.Instance.LoadDefault();
-                ConfigurationState.Instance.ToFileFormat().SaveConfig(globalConfigurationPath);
+                ConfigurationState.Instance.ToFileFormat().SaveConfig(appDataConfigurationPath);
             }
 
             PrintSystemInfo();
@@ -86,9 +109,9 @@ namespace Ryujinx
 
             Application.Init();
 
-            string globalProdKeysPath = Path.Combine(globalBasePath, "system", "prod.keys");
-            string userProfilePath    = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".switch", "prod.keys");
-            if (!File.Exists(globalProdKeysPath) && !File.Exists(userProfilePath) && !Migration.IsMigrationNeeded())
+            bool hasGlobalProdKeys = File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys"));
+            bool hasAltProdKeys    = !AppDataManager.IsCustomBasePath && File.Exists(Path.Combine(AppDataManager.KeysDirPathAlt, "prod.keys"));
+            if (!hasGlobalProdKeys && !hasAltProdKeys && !Migration.IsMigrationNeeded())
             {
                 GtkDialog.CreateWarningDialog("Key file was not found", "Please refer to `KEYS.md` for more info");
             }
@@ -96,9 +119,9 @@ namespace Ryujinx
             MainWindow mainWindow = new MainWindow();
             mainWindow.Show();
 
-            if (args.Length == 1)
+            if (launchPath != null)
             {
-                mainWindow.LoadApplication(args[0]);
+                mainWindow.LoadApplication(launchPath);
             }
 
             Application.Run();
@@ -114,6 +137,11 @@ namespace Ryujinx
 
             var enabledLogs = Logger.GetEnabledLevels();
             Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(enabledLogs.Count == 0 ? "<None>" : string.Join(", ", enabledLogs))}");
+
+            if (AppDataManager.IsCustomBasePath)
+            {
+                Logger.Notice.Print(LogClass.Application, $"Custom Data Directory: {AppDataManager.BaseDirPath}");
+            }
         }
 
         private static void ProcessUnhandledException(Exception e, bool isTerminating)
diff --git a/Ryujinx/Ui/ApplicationLibrary.cs b/Ryujinx/Ui/ApplicationLibrary.cs
index e20a42920..742dcc073 100644
--- a/Ryujinx/Ui/ApplicationLibrary.cs
+++ b/Ryujinx/Ui/ApplicationLibrary.cs
@@ -480,7 +480,7 @@ namespace Ryujinx.Ui
 
         internal static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action<ApplicationMetadata> modifyFunction = null)
         {
-            string metadataFolder = Path.Combine(_virtualFileSystem.GetBasePath(), "games", titleId, "gui");
+            string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui");
             string metadataFile   = Path.Combine(metadataFolder, "metadata.json");
 
             ApplicationMetadata appMetadata;
@@ -618,7 +618,7 @@ namespace Ryujinx.Ui
 
         private static bool IsUpdateApplied(string titleId, out string version)
         {
-            string jsonPath = Path.Combine(_virtualFileSystem.GetBasePath(), "games", titleId, "updates.json");
+            string jsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
 
             if (File.Exists(jsonPath))
             {
diff --git a/Ryujinx/Ui/ControllerWindow.cs b/Ryujinx/Ui/ControllerWindow.cs
index 9518ba98d..1d879eb59 100644
--- a/Ryujinx/Ui/ControllerWindow.cs
+++ b/Ryujinx/Ui/ControllerWindow.cs
@@ -1,5 +1,6 @@
 using Gtk;
 using OpenTK.Input;
+using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Configuration.Hid;
 using Ryujinx.Common.Utilities;
 using Ryujinx.Configuration;
@@ -602,7 +603,7 @@ namespace Ryujinx.Ui
 
         private string GetProfileBasePath()
         {
-            string path = System.IO.Path.Combine(_virtualFileSystem.GetBasePath(), "profiles");
+            string path = AppDataManager.ProfilesDirPath;
 
             if (_inputDevice.ActiveId.StartsWith("keyboard"))
             {
diff --git a/Ryujinx/Ui/DlcWindow.cs b/Ryujinx/Ui/DlcWindow.cs
index 3e57c5790..09ff9afcb 100644
--- a/Ryujinx/Ui/DlcWindow.cs
+++ b/Ryujinx/Ui/DlcWindow.cs
@@ -38,7 +38,7 @@ namespace Ryujinx.Ui
 
             _titleId                 = titleId;
             _virtualFileSystem       = virtualFileSystem;
-            _dlcJsonPath             = System.IO.Path.Combine(virtualFileSystem.GetBasePath(), "games", _titleId, "dlc.json");
+            _dlcJsonPath             = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json");
             _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]";
 
             try
diff --git a/Ryujinx/Ui/GameTableContextMenu.cs b/Ryujinx/Ui/GameTableContextMenu.cs
index afa2182d0..93306b576 100644
--- a/Ryujinx/Ui/GameTableContextMenu.cs
+++ b/Ryujinx/Ui/GameTableContextMenu.cs
@@ -339,7 +339,7 @@ namespace Ryujinx.Ui
                             return;
                         }
 
-                        string titleUpdateMetadataPath = System.IO.Path.Combine(_virtualFileSystem.GetBasePath(), "games", mainNca.Header.TitleId.ToString("x16"), "updates.json");
+                        string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json");
 
                         if (File.Exists(titleUpdateMetadataPath))
                         {
@@ -614,7 +614,7 @@ namespace Ryujinx.Ui
         {
             string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower();
 
-            var modsBasePath = _virtualFileSystem.GetBaseModsPath();
+            var modsBasePath = _virtualFileSystem.ModLoader.GetModsBasePath();
             var titleModsPath = _virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId);
 
             Process.Start(new ProcessStartInfo
@@ -643,7 +643,7 @@ namespace Ryujinx.Ui
         private void OpenPtcDir_Clicked(object sender, EventArgs args)
         {
             string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower();
-            string ptcDir  = System.IO.Path.Combine(_virtualFileSystem.GetBasePath(), "games", titleId, "cache", "cpu");
+            string ptcDir  = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "cpu");
             
             string mainPath   = System.IO.Path.Combine(ptcDir, "0");
             string backupPath = System.IO.Path.Combine(ptcDir, "1");
@@ -668,8 +668,8 @@ namespace Ryujinx.Ui
             string[] tableEntry = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n");
             string titleId = tableEntry[1].ToLower();
             
-            DirectoryInfo mainDir   = new DirectoryInfo(System.IO.Path.Combine(_virtualFileSystem.GetBasePath(), "games", titleId, "cache", "cpu", "0"));
-            DirectoryInfo backupDir = new DirectoryInfo(System.IO.Path.Combine(_virtualFileSystem.GetBasePath(), "games", titleId, "cache", "cpu", "1"));
+            DirectoryInfo mainDir   = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "cpu", "0"));
+            DirectoryInfo backupDir = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "cpu", "1"));
             
             MessageDialog warningDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Warning, ButtonsType.YesNo, null)
             {
diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs
index d2b303e42..0501b96e5 100644
--- a/Ryujinx/Ui/MainWindow.cs
+++ b/Ryujinx/Ui/MainWindow.cs
@@ -3,6 +3,7 @@ using Gtk;
 using LibHac.Common;
 using LibHac.Ns;
 using Ryujinx.Audio;
+using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.System;
 using Ryujinx.Configuration;
@@ -873,7 +874,7 @@ namespace Ryujinx.Ui
         {
             Process.Start(new ProcessStartInfo()
             {
-                FileName        = _virtualFileSystem.GetBasePath(),
+                FileName        = AppDataManager.BaseDirPath,
                 UseShellExecute = true,
                 Verb            = "open"
             });
@@ -1113,7 +1114,7 @@ namespace Ryujinx.Ui
 
         private void Update_Pressed(object sender, EventArgs args)
         {
-            string ryuUpdater = System.IO.Path.Combine(_virtualFileSystem.GetBasePath(), "RyuUpdater.exe");
+            string ryuUpdater = System.IO.Path.Combine(AppDataManager.BaseDirPath, "RyuUpdater.exe");
 
             try
             {
diff --git a/Ryujinx/Ui/Migration.cs b/Ryujinx/Ui/Migration.cs
index 20d810647..a7a49105a 100644
--- a/Ryujinx/Ui/Migration.cs
+++ b/Ryujinx/Ui/Migration.cs
@@ -1,5 +1,6 @@
 using Gtk;
 using LibHac;
+using Ryujinx.Common.Configuration;
 using Ryujinx.HLE.FileSystem;
 using System;
 using System.IO;
@@ -175,6 +176,8 @@ namespace Ryujinx.Ui
 
         public static bool IsMigrationNeeded()
         {
+            if (AppDataManager.IsCustomBasePath) return false;
+
             string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
 
             string oldBasePath = Path.Combine(appDataPath, "RyuFs");
diff --git a/Ryujinx/Ui/TitleUpdateWindow.cs b/Ryujinx/Ui/TitleUpdateWindow.cs
index 538f1d2cf..35fdf8728 100644
--- a/Ryujinx/Ui/TitleUpdateWindow.cs
+++ b/Ryujinx/Ui/TitleUpdateWindow.cs
@@ -41,7 +41,7 @@ namespace Ryujinx.Ui
 
             _titleId                     = titleId;
             _virtualFileSystem           = virtualFileSystem;
-            _updateJsonPath              = System.IO.Path.Combine(_virtualFileSystem.GetBasePath(), "games", _titleId, "updates.json");
+            _updateJsonPath              = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json");
             _radioButtonToPathDictionary = new Dictionary<RadioButton, string>();
 
             try