From cc1a933a2f4adf05a45c7adcf02669c4f423bedb Mon Sep 17 00:00:00 2001
From: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Date: Wed, 3 May 2023 02:07:16 +0200
Subject: [PATCH] ModLoader: Fix case sensitivy issues (#4720)

* Fix case sensitivity for mod subdirectories

* Small refactoring of ModLoader
---
 .../UI/ViewModels/MainWindowViewModel.cs      |   8 +-
 .../UI/Windows/CheatWindow.axaml.cs           |   4 +-
 src/Ryujinx.HLE/HOS/ModLoader.cs              | 269 +++++++++---------
 .../Extensions/LocalFileSystemExtensions.cs   |   7 +-
 .../Processes/Extensions/NcaExtensions.cs     |   5 +-
 .../Ui/Widgets/GameTableContextMenu.cs        |   8 +-
 src/Ryujinx/Ui/Windows/CheatWindow.cs         |   4 +-
 7 files changed, 158 insertions(+), 147 deletions(-)

diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
index f4556bc3..4ce47999 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
@@ -1576,8 +1576,8 @@ namespace Ryujinx.Ava.UI.ViewModels
         {
             if (SelectedApplication != null)
             {
-                string modsBasePath  = VirtualFileSystem.ModLoader.GetModsBasePath();
-                string titleModsPath = VirtualFileSystem.ModLoader.GetTitleDir(modsBasePath, SelectedApplication.TitleId);
+                string modsBasePath  = ModLoader.GetModsBasePath();
+                string titleModsPath = ModLoader.GetTitleDir(modsBasePath, SelectedApplication.TitleId);
 
                 OpenHelper.OpenFolder(titleModsPath);
             }
@@ -1587,8 +1587,8 @@ namespace Ryujinx.Ava.UI.ViewModels
         {
             if (SelectedApplication != null)
             {
-                string sdModsBasePath = VirtualFileSystem.ModLoader.GetSdModsBasePath();
-                string titleModsPath  = VirtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, SelectedApplication.TitleId);
+                string sdModsBasePath = ModLoader.GetSdModsBasePath();
+                string titleModsPath  = ModLoader.GetTitleDir(sdModsBasePath, SelectedApplication.TitleId);
 
                 OpenHelper.OpenFolder(titleModsPath);
             }
diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs
index cb939763..241a6c34 100644
--- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs
@@ -35,8 +35,8 @@ namespace Ryujinx.Ava.UI.Windows
 
             InitializeComponent();
 
-            string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath();
-            string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId);
+            string modsBasePath = ModLoader.GetModsBasePath();
+            string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId);
             ulong titleIdValue = ulong.Parse(titleId, System.Globalization.NumberStyles.HexNumber);
 
             _enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt");
diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs
index 16512541..a173ac41 100644
--- a/src/Ryujinx.HLE/HOS/ModLoader.cs
+++ b/src/Ryujinx.HLE/HOS/ModLoader.cs
@@ -89,7 +89,7 @@ namespace Ryujinx.HLE.HOS
         }
 
         // Title independent mods
-        public class PatchCache
+        private class PatchCache
         {
             public List<Mod<DirectoryInfo>> NsoPatches { get; }
             public List<Mod<DirectoryInfo>> NroPatches { get; }
@@ -107,14 +107,14 @@ namespace Ryujinx.HLE.HOS
             }
         }
 
-        public Dictionary<ulong, ModCache> AppMods; // key is TitleId
-        public PatchCache Patches;
+        private readonly Dictionary<ulong, ModCache> _appMods; // key is TitleId
+        private PatchCache _patches;
 
-        private static readonly EnumerationOptions _dirEnumOptions;
+        private static readonly EnumerationOptions DirEnumOptions;
 
         static ModLoader()
         {
-            _dirEnumOptions = new EnumerationOptions
+            DirEnumOptions = new EnumerationOptions
             {
                 MatchCasing = MatchCasing.CaseInsensitive,
                 MatchType = MatchType.Simple,
@@ -125,37 +125,73 @@ namespace Ryujinx.HLE.HOS
 
         public ModLoader()
         {
-            AppMods = new Dictionary<ulong, ModCache>();
-            Patches = new PatchCache();
+            _appMods = new Dictionary<ulong, ModCache>();
+            _patches = new PatchCache();
         }
 
-        public void Clear()
+        private void Clear()
         {
-            AppMods.Clear();
-            Patches = new PatchCache();
+            _appMods.Clear();
+            _patches = new PatchCache();
         }
 
         private static bool StrEquals(string s1, string s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase);
 
-        public string GetModsBasePath()   => EnsureBaseDirStructure(AppDataManager.GetModsPath());
-        public string GetSdModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetSdModsPath());
+        public static string GetModsBasePath()   => EnsureBaseDirStructure(AppDataManager.GetModsPath());
+        public static string GetSdModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetSdModsPath());
 
-        private string EnsureBaseDirStructure(string modsBasePath)
+        private static string EnsureBaseDirStructure(string modsBasePath)
         {
             var modsDir = new DirectoryInfo(modsBasePath);
 
             modsDir.CreateSubdirectory(AmsContentsDir);
             modsDir.CreateSubdirectory(AmsNsoPatchDir);
             modsDir.CreateSubdirectory(AmsNroPatchDir);
-            // modsDir.CreateSubdirectory(AmsKipPatchDir); // uncomment when KIPs are supported
+            // TODO: uncomment when KIPs are supported
+            // modsDir.CreateSubdirectory(AmsKipPatchDir);
 
             return modsDir.FullName;
         }
 
         private static DirectoryInfo FindTitleDir(DirectoryInfo contentsDir, string titleId)
-            => contentsDir.EnumerateDirectories($"{titleId}*", _dirEnumOptions).FirstOrDefault();
+            => contentsDir.EnumerateDirectories($"{titleId}*", DirEnumOptions).FirstOrDefault();
 
-        public string GetTitleDir(string modsBasePath, string titleId)
+        private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, string titleId)
+        {
+            System.Text.StringBuilder types = new();
+
+            foreach (var modDir in dir.EnumerateDirectories())
+            {
+                types.Clear();
+                Mod<DirectoryInfo> mod = new("", null);
+
+                if (StrEquals(RomfsDir, modDir.Name))
+                {
+                    mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleId} RomFs>", modDir));
+                    types.Append('R');
+                }
+                else if (StrEquals(ExefsDir, modDir.Name))
+                {
+                    mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleId} ExeFs>", modDir));
+                    types.Append('E');
+                }
+                else if (StrEquals(CheatDir, modDir.Name))
+                {
+                    types.Append('C', QueryCheatsDir(mods, modDir));
+                }
+                else
+                {
+                    AddModsFromDirectory(mods, modDir, titleId);
+                }
+
+                if (types.Length > 0)
+                {
+                    Logger.Info?.Print(LogClass.ModLoader, $"Found mod '{mod.Name}' [{types}]");
+                }
+            }
+        }
+
+        public static string GetTitleDir(string modsBasePath, string titleId)
         {
             var contentsDir = new DirectoryInfo(Path.Combine(modsBasePath, AmsContentsDir));
             var titleModsPath = FindTitleDir(contentsDir, titleId);
@@ -170,17 +206,32 @@ namespace Ryujinx.HLE.HOS
         }
 
         // Static Query Methods
-        public static void QueryPatchDirs(PatchCache cache, DirectoryInfo patchDir)
+        private static void QueryPatchDirs(PatchCache cache, DirectoryInfo patchDir)
         {
-            if (cache.Initialized || !patchDir.Exists) return;
+            if (cache.Initialized || !patchDir.Exists)
+            {
+                return;
+            }
 
-            var patches = cache.KipPatches;
-            string type = null;
+            List<Mod<DirectoryInfo>> patches;
+            string type;
 
-            if (StrEquals(AmsNsoPatchDir, patchDir.Name)) { patches = cache.NsoPatches; type = "NSO"; }
-            else if (StrEquals(AmsNroPatchDir, patchDir.Name)) { patches = cache.NroPatches; type = "NRO"; }
-            else if (StrEquals(AmsKipPatchDir, patchDir.Name)) { patches = cache.KipPatches; type = "KIP"; }
-            else return;
+            if (StrEquals(AmsNsoPatchDir, patchDir.Name))
+            {
+                patches = cache.NsoPatches; type = "NSO";
+            }
+            else if (StrEquals(AmsNroPatchDir, patchDir.Name))
+            {
+                patches = cache.NroPatches; type = "NRO";
+            }
+            else if (StrEquals(AmsKipPatchDir, patchDir.Name))
+            {
+                patches = cache.KipPatches; type = "KIP";
+            }
+            else
+            {
+                return;
+            }
 
             foreach (var modDir in patchDir.EnumerateDirectories())
             {
@@ -189,9 +240,12 @@ namespace Ryujinx.HLE.HOS
             }
         }
 
-        public static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir)
+        private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir)
         {
-            if (!titleDir.Exists) return;
+            if (!titleDir.Exists)
+            {
+                return;
+            }
 
             var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer));
             if (fsFile.Exists)
@@ -205,64 +259,15 @@ namespace Ryujinx.HLE.HOS
                 mods.ExefsContainers.Add(new Mod<FileInfo>($"<{titleDir.Name} ExeFs>", fsFile));
             }
 
-            System.Text.StringBuilder types = new System.Text.StringBuilder(5);
-
-            foreach (var modDir in titleDir.EnumerateDirectories())
-            {
-                types.Clear();
-                Mod<DirectoryInfo> mod = new Mod<DirectoryInfo>("", null);
-
-                if (StrEquals(RomfsDir, modDir.Name))
-                {
-                    mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleDir.Name} RomFs>", modDir));
-                    types.Append('R');
-                }
-                else if (StrEquals(ExefsDir, modDir.Name))
-                {
-                    mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>($"<{titleDir.Name} ExeFs>", modDir));
-                    types.Append('E');
-                }
-                else if (StrEquals(CheatDir, modDir.Name))
-                {
-                    for (int i = 0; i < QueryCheatsDir(mods, modDir); i++)
-                    {
-                        types.Append('C');
-                    }
-                }
-                else
-                {
-                    var romfs = new DirectoryInfo(Path.Combine(modDir.FullName, RomfsDir));
-                    var exefs = new DirectoryInfo(Path.Combine(modDir.FullName, ExefsDir));
-                    var cheat = new DirectoryInfo(Path.Combine(modDir.FullName, CheatDir));
-
-                    if (romfs.Exists)
-                    {
-                        mods.RomfsDirs.Add(mod = new Mod<DirectoryInfo>(modDir.Name, romfs));
-                        types.Append('R');
-                    }
-
-                    if (exefs.Exists)
-                    {
-                        mods.ExefsDirs.Add(mod = new Mod<DirectoryInfo>(modDir.Name, exefs));
-                        types.Append('E');
-                    }
-
-                    if (cheat.Exists)
-                    {
-                        for (int i = 0; i < QueryCheatsDir(mods, cheat); i++)
-                        {
-                            types.Append('C');
-                        }
-                    }
-                }
-
-                if (types.Length > 0) Logger.Info?.Print(LogClass.ModLoader, $"Found mod '{mod.Name}' [{types}]");
-            }
+            AddModsFromDirectory(mods, titleDir, titleDir.Name);
         }
 
         public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ulong titleId)
         {
-            if (!contentsDir.Exists) return;
+            if (!contentsDir.Exists)
+            {
+                return;
+            }
 
             Logger.Info?.Print(LogClass.ModLoader, $"Searching mods for {((titleId & 0x1000) != 0 ? "DLC" : "Title")} {titleId:X16}");
 
@@ -302,9 +307,16 @@ namespace Ryujinx.HLE.HOS
                     continue;
                 }
 
+                int oldCheatsCount = mods.Cheats.Count;
+
                 // A cheat file can contain several cheats for the same executable, so the file must be parsed in
                 // order to properly enumerate them.
                 mods.Cheats.AddRange(GetCheatsInFile(file));
+
+                if (mods.Cheats.Count - oldCheatsCount > 0)
+                {
+                    numMods++;
+                }
             }
 
             return numMods;
@@ -313,57 +325,54 @@ namespace Ryujinx.HLE.HOS
         private static IEnumerable<Cheat> GetCheatsInFile(FileInfo cheatFile)
         {
             string cheatName = DefaultCheatName;
-            List<string> instructions = new List<string>();
-            List<Cheat> cheats = new List<Cheat>();
+            List<string> instructions = new();
+            List<Cheat> cheats = new();
 
-            using (StreamReader cheatData = cheatFile.OpenText())
+            using StreamReader cheatData = cheatFile.OpenText();
+            while (cheatData.ReadLine() is { } line)
             {
-                string line;
-                while ((line = cheatData.ReadLine()) != null)
+                line = line.Trim();
+
+                if (line.StartsWith('['))
                 {
-                    line = line.Trim();
-
-                    if (line.StartsWith('['))
+                    // This line starts a new cheat section.
+                    if (!line.EndsWith(']') || line.Length < 3)
                     {
-                        // This line starts a new cheat section.
-                        if (!line.EndsWith(']') || line.Length < 3)
-                        {
-                            // Skip the entire file if there's any error while parsing the cheat file.
+                        // Skip the entire file if there's any error while parsing the cheat file.
 
-                            Logger.Warning?.Print(LogClass.ModLoader, $"Ignoring cheat '{cheatFile.FullName}' because it is malformed");
+                        Logger.Warning?.Print(LogClass.ModLoader, $"Ignoring cheat '{cheatFile.FullName}' because it is malformed");
 
-                            return new List<Cheat>();
-                        }
-
-                        // Add the previous section to the list.
-                        if (instructions.Count != 0)
-                        {
-                            cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions));
-                        }
-
-                        // Start a new cheat section.
-                        cheatName = line.Substring(1, line.Length - 2);
-                        instructions = new List<string>();
+                        return Array.Empty<Cheat>();
                     }
-                    else if (line.Length > 0)
+
+                    // Add the previous section to the list.
+                    if (instructions.Count > 0)
                     {
-                        // The line contains an instruction.
-                        instructions.Add(line);
+                        cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions));
                     }
+
+                    // Start a new cheat section.
+                    cheatName = line.Substring(1, line.Length - 2);
+                    instructions.Clear();
                 }
-
-                // Add the last section being processed.
-                if (instructions.Count != 0)
+                else if (line.Length > 0)
                 {
-                    cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions));
+                    // The line contains an instruction.
+                    instructions.Add(line);
                 }
             }
 
+            // Add the last section being processed.
+            if (instructions.Count > 0)
+            {
+                cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions));
+            }
+
             return cheats;
         }
 
         // Assumes searchDirPaths don't overlap
-        public static void CollectMods(Dictionary<ulong, ModCache> modCaches, PatchCache patches, params string[] searchDirPaths)
+        private static void CollectMods(Dictionary<ulong, ModCache> modCaches, PatchCache patches, params string[] searchDirPaths)
         {
             static bool IsPatchesDir(string name) => StrEquals(AmsNsoPatchDir, name) ||
                                                      StrEquals(AmsNroPatchDir, name) ||
@@ -375,7 +384,7 @@ namespace Ryujinx.HLE.HOS
             {
                 if (IsContentsDir(searchDir.Name))
                 {
-                    foreach (var (titleId, cache) in modCaches)
+                    foreach ((ulong titleId, ModCache cache) in modCaches)
                     {
                         QueryContentsDir(cache, searchDir, titleId);
                     }
@@ -419,15 +428,15 @@ namespace Ryujinx.HLE.HOS
 
             foreach (ulong titleId in titles)
             {
-                AppMods[titleId] = new ModCache();
+                _appMods[titleId] = new ModCache();
             }
 
-            CollectMods(AppMods, Patches, searchDirPaths);
+            CollectMods(_appMods, _patches, searchDirPaths);
         }
 
         internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage)
         {
-            if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.RomfsDirs.Count + mods.RomfsContainers.Count == 0)
+            if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.RomfsDirs.Count + mods.RomfsContainers.Count == 0)
             {
                 return baseStorage;
             }
@@ -487,7 +496,7 @@ namespace Ryujinx.HLE.HOS
             return newStorage;
         }
 
-        private static void AddFiles(IFileSystem fs, string modName, HashSet<string> fileSet, RomFsBuilder builder)
+        private static void AddFiles(IFileSystem fs, string modName, ISet<string> fileSet, RomFsBuilder builder)
         {
             foreach (var entry in fs.EnumerateEntries()
                                     .Where(f => f.Type == DirectoryEntryType.File)
@@ -509,7 +518,7 @@ namespace Ryujinx.HLE.HOS
 
         internal bool ReplaceExefsPartition(ulong titleId, ref IFileSystem exefs)
         {
-            if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsContainers.Count == 0)
+            if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsContainers.Count == 0)
             {
                 return false;
             }
@@ -537,13 +546,13 @@ namespace Ryujinx.HLE.HOS
 
         internal ModLoadResult ApplyExefsMods(ulong titleId, NsoExecutable[] nsos)
         {
-            ModLoadResult modLoadResult = new ModLoadResult
+            ModLoadResult modLoadResult = new()
             {
                 Stubs = new BitVector32(),
                 Replaces = new BitVector32()
             };
 
-            if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsDirs.Count == 0)
+            if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsDirs.Count == 0)
             {
                 return modLoadResult;
             }
@@ -561,7 +570,7 @@ namespace Ryujinx.HLE.HOS
                 {
                     var nsoName = ProcessConst.ExeFsPrefixes[i];
 
-                    FileInfo nsoFile = new FileInfo(Path.Combine(mod.Path.FullName, nsoName));
+                    FileInfo nsoFile = new(Path.Combine(mod.Path.FullName, nsoName));
                     if (nsoFile.Exists)
                     {
                         if (modLoadResult.Replaces[1 << i])
@@ -580,7 +589,7 @@ namespace Ryujinx.HLE.HOS
                     modLoadResult.Stubs[1 << i] |= File.Exists(Path.Combine(mod.Path.FullName, nsoName + StubExtension));
                 }
 
-                FileInfo npdmFile = new FileInfo(Path.Combine(mod.Path.FullName, "main.npdm"));
+                FileInfo npdmFile = new(Path.Combine(mod.Path.FullName, "main.npdm"));
                 if (npdmFile.Exists)
                 {
                     if (modLoadResult.Npdm != null)
@@ -611,7 +620,7 @@ namespace Ryujinx.HLE.HOS
 
         internal void ApplyNroPatches(NroExecutable nro)
         {
-            var nroPatches = Patches.NroPatches;
+            var nroPatches = _patches.NroPatches;
 
             if (nroPatches.Count == 0) return;
 
@@ -622,9 +631,9 @@ namespace Ryujinx.HLE.HOS
 
         internal bool ApplyNsoPatches(ulong titleId, params IExecutable[] programs)
         {
-            IEnumerable<Mod<DirectoryInfo>> nsoMods = Patches.NsoPatches;
+            IEnumerable<Mod<DirectoryInfo>> nsoMods = _patches.NsoPatches;
 
-            if (AppMods.TryGetValue(titleId, out ModCache mods))
+            if (_appMods.TryGetValue(titleId, out ModCache mods))
             {
                 nsoMods = nsoMods.Concat(mods.ExefsDirs);
             }
@@ -636,7 +645,7 @@ namespace Ryujinx.HLE.HOS
 
         internal void LoadCheats(ulong titleId, ProcessTamperInfo tamperInfo, TamperMachine tamperMachine)
         {
-            if (tamperInfo == null || tamperInfo.BuildIds == null || tamperInfo.CodeAddresses == null)
+            if (tamperInfo?.BuildIds == null || tamperInfo.CodeAddresses == null)
             {
                 Logger.Error?.Print(LogClass.ModLoader, "Unable to install cheat because the associated process is invalid");
 
@@ -645,14 +654,14 @@ namespace Ryujinx.HLE.HOS
 
             Logger.Info?.Print(LogClass.ModLoader, $"Build ids found for title {titleId:X16}:\n    {String.Join("\n    ", tamperInfo.BuildIds)}");
 
-            if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.Cheats.Count == 0)
+            if (!_appMods.TryGetValue(titleId, out ModCache mods) || mods.Cheats.Count == 0)
             {
                 return;
             }
 
             var cheats = mods.Cheats;
             var processExes = tamperInfo.BuildIds.Zip(tamperInfo.CodeAddresses, (k, v) => new { k, v })
-                .ToDictionary(x => x.k.Substring(0, Math.Min(Cheat.CheatIdSize, x.k.Length)), x => x.v);
+                .ToDictionary(x => x.k[..Math.Min(Cheat.CheatIdSize, x.k.Length)], x => x.v);
 
             foreach (var cheat in cheats)
             {
@@ -758,4 +767,4 @@ namespace Ryujinx.HLE.HOS
             return count > 0;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs
index 28d90785..fb85329d 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs
@@ -2,6 +2,7 @@
 using LibHac.FsSystem;
 using LibHac.Loader;
 using LibHac.Ns;
+using Ryujinx.HLE.HOS;
 using Ryujinx.HLE.Loaders.Processes.Extensions;
 using ApplicationId = LibHac.Ncm.ApplicationId;
 
@@ -17,8 +18,8 @@ namespace Ryujinx.HLE.Loaders.Processes
 
             device.Configuration.VirtualFileSystem.ModLoader.CollectMods(
                 new[] { programId },
-                device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath(),
-                device.Configuration.VirtualFileSystem.ModLoader.GetSdModsBasePath());
+                ModLoader.GetModsBasePath(),
+                ModLoader.GetSdModsBasePath());
 
             if (programId != 0)
             {
@@ -36,4 +37,4 @@ namespace Ryujinx.HLE.Loaders.Processes
             return processResult;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs
index 473f374d..e11b81d7 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs
@@ -8,6 +8,7 @@ using LibHac.Ns;
 using LibHac.Tools.FsSystem;
 using LibHac.Tools.FsSystem.NcaUtils;
 using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS;
 using System.IO;
 using System.Linq;
 using ApplicationId = LibHac.Ncm.ApplicationId;
@@ -35,8 +36,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
             // Collecting mods related to AocTitleIds and ProgramId.
             device.Configuration.VirtualFileSystem.ModLoader.CollectMods(
                 device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId()),
-                device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath(),
-                device.Configuration.VirtualFileSystem.ModLoader.GetSdModsBasePath());
+                ModLoader.GetModsBasePath(),
+                ModLoader.GetSdModsBasePath());
 
             // Load Nacp file.
             var nacpData = new BlitStruct<ApplicationControlProperty>(1);
diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
index 558288aa..6d833165 100644
--- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
+++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
@@ -460,16 +460,16 @@ namespace Ryujinx.Ui.Widgets
 
         private void OpenTitleModDir_Clicked(object sender, EventArgs args)
         {
-            string modsBasePath  = _virtualFileSystem.ModLoader.GetModsBasePath();
-            string titleModsPath = _virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, _titleIdText);
+            string modsBasePath  = ModLoader.GetModsBasePath();
+            string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText);
 
             OpenHelper.OpenFolder(titleModsPath);
         }
 
         private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
         {
-            string sdModsBasePath  = _virtualFileSystem.ModLoader.GetSdModsBasePath();
-            string titleModsPath   = _virtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, _titleIdText);
+            string sdModsBasePath  = ModLoader.GetSdModsBasePath();
+            string titleModsPath   = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText);
 
             OpenHelper.OpenFolder(titleModsPath);
         }
diff --git a/src/Ryujinx/Ui/Windows/CheatWindow.cs b/src/Ryujinx/Ui/Windows/CheatWindow.cs
index 917603b2..7dbea012 100644
--- a/src/Ryujinx/Ui/Windows/CheatWindow.cs
+++ b/src/Ryujinx/Ui/Windows/CheatWindow.cs
@@ -28,8 +28,8 @@ namespace Ryujinx.Ui.Windows
             builder.Autoconnect(this);
             _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]";
 
-            string modsBasePath  = virtualFileSystem.ModLoader.GetModsBasePath();
-            string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16"));
+            string modsBasePath  = ModLoader.GetModsBasePath();
+            string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16"));
 
             _enabledCheatsPath = System.IO.Path.Combine(titleModsPath, "cheats", "enabled.txt");