diff --git a/Ryujinx.Common/Logging/LogClass.cs b/Ryujinx.Common/Logging/LogClass.cs index 2120c9cb04..aad0489136 100644 --- a/Ryujinx.Common/Logging/LogClass.cs +++ b/Ryujinx.Common/Logging/LogClass.cs @@ -14,6 +14,7 @@ namespace Ryujinx.Common.Logging KernelScheduler, KernelSvc, Loader, + ModLoader, Ptc, Service, ServiceAcc, diff --git a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs index 347b4fa4dd..5889099533 100644 --- a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs +++ b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs @@ -17,11 +17,12 @@ namespace Ryujinx.HLE.FileSystem public const string NandPath = "bis"; public const string SdCardPath = "sdcard"; public const string SystemPath = "system"; + public const string ModsPath = "mods"; public static string SafeNandPath = Path.Combine(NandPath, "safe"); public static string SystemNandPath = Path.Combine(NandPath, "system"); public static string UserNandPath = Path.Combine(NandPath, "user"); - + private static bool _isInitialized = false; public Keyset KeySet { get; private set; } @@ -30,9 +31,12 @@ namespace Ryujinx.HLE.FileSystem public EmulatedGameCard GameCard { get; private set; } public EmulatedSdCard SdCard { get; private set; } + public ModLoader ModLoader {get; private set;} + private VirtualFileSystem() { Reload(); + ModLoader = new ModLoader(); // Should only be created once } public Stream RomFs { get; private set; } @@ -73,6 +77,14 @@ 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); public string GetNandPath() => MakeFullPath(NandPath); diff --git a/Ryujinx.HLE/HOS/ApplicationLoader.cs b/Ryujinx.HLE/HOS/ApplicationLoader.cs index bc7016bda2..994d0f2595 100644 --- a/Ryujinx.HLE/HOS/ApplicationLoader.cs +++ b/Ryujinx.HLE/HOS/ApplicationLoader.cs @@ -43,7 +43,8 @@ namespace Ryujinx.HLE.HOS public bool EnablePtc => _device.System.EnablePtc; - public IntegrityCheckLevel FsIntegrityCheckLevel => _device.System.FsIntegrityCheckLevel; + // Binaries from exefs are loaded into mem in this order. Do not change. + private static readonly string[] ExeFsPrefixes = { "rtld", "main", "subsdk*", "sdk" }; public ApplicationLoader(Switch device, VirtualFileSystem fileSystem, ContentManager contentManager) { @@ -52,6 +53,9 @@ namespace Ryujinx.HLE.HOS _fileSystem = fileSystem; ControlData = new BlitStruct(1); + + // Clear Mods cache + _fileSystem.ModLoader.Clear(); } public void LoadCart(string exeFsDir, string romFsFile = null) @@ -63,12 +67,14 @@ namespace Ryujinx.HLE.HOS LocalFileSystem codeFs = new LocalFileSystem(exeFsDir); - LoadExeFs(codeFs, out _); + Npdm metaData = ReadNpdm(codeFs); if (TitleId != 0) { EnsureSaveData(new TitleId(TitleId)); } + + LoadExeFs(codeFs, metaData); } private (Nca main, Nca patch, Nca control) GetGameData(PartitionFileSystem pfs) @@ -191,7 +197,7 @@ namespace Ryujinx.HLE.HOS } // This is not a normal NSP, it's actually a ExeFS as a NSP - LoadExeFs(nsp, out _); + LoadExeFs(nsp); } public void LoadNca(string ncaFile) @@ -272,24 +278,24 @@ namespace Ryujinx.HLE.HOS { if (mainNca.CanOpenSection(NcaSectionType.Data)) { - dataStorage = mainNca.OpenStorage(NcaSectionType.Data, FsIntegrityCheckLevel); + dataStorage = mainNca.OpenStorage(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); } if (mainNca.CanOpenSection(NcaSectionType.Code)) { - codeFs = mainNca.OpenFileSystem(NcaSectionType.Code, FsIntegrityCheckLevel); + codeFs = mainNca.OpenFileSystem(NcaSectionType.Code, _device.System.FsIntegrityCheckLevel); } } else { if (patchNca.CanOpenSection(NcaSectionType.Data)) { - dataStorage = mainNca.OpenStorageWithPatch(patchNca, NcaSectionType.Data, FsIntegrityCheckLevel); + dataStorage = mainNca.OpenStorageWithPatch(patchNca, NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); } if (patchNca.CanOpenSection(NcaSectionType.Code)) { - codeFs = mainNca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, FsIntegrityCheckLevel); + codeFs = mainNca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, _device.System.FsIntegrityCheckLevel); } } @@ -300,14 +306,9 @@ namespace Ryujinx.HLE.HOS return; } - if (dataStorage == null) - { - Logger.PrintWarning(LogClass.Loader, "No RomFS found in NCA"); - } - else - { - _fileSystem.SetRomFs(dataStorage.AsStream(FileAccess.Read)); - } + Npdm metaData = ReadNpdm(codeFs); + + _fileSystem.ModLoader.CollectMods(TitleId, _fileSystem.GetBaseModsPath()); if (controlNca != null) { @@ -318,19 +319,52 @@ namespace Ryujinx.HLE.HOS ControlData.ByteSpan.Clear(); } - LoadExeFs(codeFs, out _); + if (dataStorage == null) + { + Logger.PrintWarning(LogClass.Loader, "No RomFS found in NCA"); + } + else + { + IStorage newStorage = _fileSystem.ModLoader.ApplyRomFsMods(TitleId, dataStorage); + _fileSystem.SetRomFs(newStorage.AsStream(FileAccess.Read)); + } if (TitleId != 0) { EnsureSaveData(new TitleId(TitleId)); } + LoadExeFs(codeFs, metaData); + Logger.PrintInfo(LogClass.Loader, $"Application Loaded: {TitleName} v{DisplayVersion} [{TitleIdText}] [{(TitleIs64Bit ? "64-bit" : "32-bit")}]"); } - public void ReadControlData(Nca controlNca) + // Sets TitleId, so be sure to call before using it + private Npdm ReadNpdm(IFileSystem fs) { - IFileSystem controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, FsIntegrityCheckLevel); + Result result = fs.OpenFile(out IFile npdmFile, "/main.npdm".ToU8Span(), OpenMode.Read); + Npdm metaData; + + if (ResultFs.PathNotFound.Includes(result)) + { + Logger.PrintWarning(LogClass.Loader, "NPDM file not found, using default values!"); + + metaData = GetDefaultNpdm(); + } + else + { + metaData = new Npdm(npdmFile.AsStream()); + } + + TitleId = metaData.Aci0.TitleId; + TitleIs64Bit = metaData.Is64Bit; + + return metaData; + } + + private void ReadControlData(Nca controlNca) + { + IFileSystem controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); Result result = controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read); @@ -358,26 +392,20 @@ namespace Ryujinx.HLE.HOS } } - private void LoadExeFs(IFileSystem codeFs, out Npdm metaData) + private void LoadExeFs(IFileSystem codeFs, Npdm metaData = null) { - Result result = codeFs.OpenFile(out IFile npdmFile, "/main.npdm".ToU8Span(), OpenMode.Read); - - if (ResultFs.PathNotFound.Includes(result)) + if (_fileSystem.ModLoader.ReplaceExefsPartition(TitleId, ref codeFs)) { - Logger.PrintWarning(LogClass.Loader, "NPDM file not found, using default values!"); - - metaData = GetDefaultNpdm(); - } - else - { - metaData = new Npdm(npdmFile.AsStream()); + metaData = null; //TODO: Check if we should retain old npdm } - List nsos = new List(); + metaData ??= ReadNpdm(codeFs); - void LoadNso(string filename) + List nsos = new List(); + + foreach (string exePrefix in ExeFsPrefixes) // Load binaries with standard prefixes { - foreach (DirectoryEntryEx file in codeFs.EnumerateEntries("/", $"{filename}*")) + foreach (DirectoryEntryEx file in codeFs.EnumerateEntries("/", exePrefix)) { if (Path.GetExtension(file.Name) != string.Empty) { @@ -388,25 +416,29 @@ namespace Ryujinx.HLE.HOS codeFs.OpenFile(out IFile nsoFile, file.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - NsoExecutable nso = new NsoExecutable(nsoFile.AsStorage()); + NsoExecutable nso = new NsoExecutable(nsoFile.AsStorage(), file.Name); nsos.Add(nso); } } - TitleId = metaData.Aci0.TitleId; - TitleIs64Bit = metaData.Is64Bit; + // ExeFs file replacements + bool modified = _fileSystem.ModLoader.ApplyExefsMods(TitleId, nsos); - LoadNso("rtld"); - LoadNso("main"); - LoadNso("subsdk"); - LoadNso("sdk"); + var programs = nsos.ToArray(); + + modified |= _fileSystem.ModLoader.ApplyNsoPatches(TitleId, programs); _contentManager.LoadEntries(_device); - Ptc.Initialize(TitleIdText, DisplayVersion, EnablePtc); + if (EnablePtc && modified) + { + Logger.PrintWarning(LogClass.Ptc, $"Detected exefs modifications. PPTC disabled."); + } - ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: nsos.ToArray()); + Ptc.Initialize(TitleIdText, DisplayVersion, EnablePtc && !modified); + + ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, executables: programs); } public void LoadProgram(string filePath) @@ -420,7 +452,7 @@ namespace Ryujinx.HLE.HOS if (isNro) { FileStream input = new FileStream(filePath, FileMode.Open); - NroExecutable obj = new NroExecutable(input); + NroExecutable obj = new NroExecutable(input.AsStorage()); executable = obj; // homebrew NRO can actually have some data after the actual NRO @@ -493,7 +525,7 @@ namespace Ryujinx.HLE.HOS } else { - executable = new NsoExecutable(new LocalStorage(filePath, FileAccess.Read)); + executable = new NsoExecutable(new LocalStorage(filePath, FileAccess.Read), Path.GetFileNameWithoutExtension(filePath)); } _contentManager.LoadEntries(_device); diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index c1baae3078..b3af32903e 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -184,11 +184,11 @@ namespace Ryujinx.HLE.HOS InitLibHacHorizon(); } - public void LoadKip(string kipFile) + public void LoadKip(string kipPath) { - using IStorage fs = new LocalStorage(kipFile, FileAccess.Read); + using IStorage kipFile = new LocalStorage(kipPath, FileAccess.Read); - ProgramLoader.LoadKip(KernelContext, new KipExecutable(fs)); + ProgramLoader.LoadKip(KernelContext, new KipExecutable(kipFile)); } private void InitLibHacHorizon() diff --git a/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs index 81f8bb6a6c..c67e5c5c2b 100644 --- a/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs +++ b/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs @@ -66,7 +66,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process private ulong _imageSize; private ulong _mainThreadStackSize; private ulong _memoryUsageCapacity; - private int _category; + private int _version; public KHandleTable HandleTable { get; private set; } @@ -377,7 +377,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process _creationTimestamp = PerformanceCounter.ElapsedMilliseconds; MmuFlags = creationInfo.MmuFlags; - _category = creationInfo.Category; + _version = creationInfo.Version; TitleId = creationInfo.TitleId; _entrypoint = creationInfo.CodeAddress; _imageSize = (ulong)creationInfo.CodePagesCount * KMemoryManager.PageSize; diff --git a/Ryujinx.HLE/HOS/Kernel/Process/ProcessCreationInfo.cs b/Ryujinx.HLE/HOS/Kernel/Process/ProcessCreationInfo.cs index 7431d7dd25..a582086538 100644 --- a/Ryujinx.HLE/HOS/Kernel/Process/ProcessCreationInfo.cs +++ b/Ryujinx.HLE/HOS/Kernel/Process/ProcessCreationInfo.cs @@ -4,7 +4,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process { public string Name { get; private set; } - public int Category { get; private set; } + public int Version { get; private set; } public ulong TitleId { get; private set; } public ulong CodeAddress { get; private set; } @@ -25,7 +25,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process int personalMmHeapPagesCount) { Name = name; - Category = category; + Version = category; TitleId = titleId; CodeAddress = codeAddress; CodePagesCount = codePagesCount; diff --git a/Ryujinx.HLE/HOS/ModLoader.cs b/Ryujinx.HLE/HOS/ModLoader.cs new file mode 100644 index 0000000000..654d0cbe3e --- /dev/null +++ b/Ryujinx.HLE/HOS/ModLoader.cs @@ -0,0 +1,536 @@ +using LibHac.Common; +using LibHac.Fs; +using LibHac.FsSystem; +using LibHac.FsSystem.RomFs; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.Loaders.Mods; +using Ryujinx.HLE.Loaders.Executables; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.IO; + +namespace Ryujinx.HLE.HOS +{ + public class ModLoader + { + private const string RomfsDir = "romfs"; + private const string ExefsDir = "exefs"; + private const string RomfsContainer = "romfs.bin"; + private const string ExefsContainer = "exefs.nsp"; + private const string StubExtension = ".stub"; + + private const string AmsContentsDir = "contents"; + private const string AmsNsoPatchDir = "exefs_patches"; + private const string AmsNroPatchDir = "nro_patches"; + private const string AmsKipPatchDir = "kip_patches"; + + public struct Mod where T : FileSystemInfo + { + public readonly string Name; + public readonly T Path; + + public Mod(string name, T path) + { + Name = name; + Path = path; + } + } + + // Title dependent mods + public class ModCache + { + public List> RomfsContainers { get; } + public List> ExefsContainers { get; } + + public List> RomfsDirs { get; } + public List> ExefsDirs { get; } + + public ModCache() + { + RomfsContainers = new List>(); + ExefsContainers = new List>(); + RomfsDirs = new List>(); + ExefsDirs = new List>(); + } + } + + // Title independent mods + public class PatchCache + { + public List> NsoPatches { get; } + public List> NroPatches { get; } + public List> KipPatches { get; } + + public HashSet SearchedDirs { get; } + + public PatchCache() + { + NsoPatches = new List>(); + NroPatches = new List>(); + KipPatches = new List>(); + + SearchedDirs = new HashSet(); + } + } + + public Dictionary AppMods; // key is TitleId + public PatchCache Patches; + + private static readonly EnumerationOptions _dirEnumOptions; + + static ModLoader() + { + _dirEnumOptions = new EnumerationOptions + { + MatchCasing = MatchCasing.CaseInsensitive, + MatchType = MatchType.Simple, + RecurseSubdirectories = false, + ReturnSpecialDirectories = false + }; + } + + public ModLoader() + { + AppMods = new Dictionary(); + Patches = new PatchCache(); + } + + public void Clear() + { + AppMods.Clear(); + Patches = new PatchCache(); + } + + private static bool StrEquals(string s1, string s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); + + public void 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 + } + + private static DirectoryInfo FindTitleDir(DirectoryInfo contentsDir, string titleId) + => contentsDir.EnumerateDirectories($"{titleId}*", _dirEnumOptions).FirstOrDefault(); + + public string GetTitleDir(string modsBasePath, string titleId) + { + var contentsDir = new DirectoryInfo(Path.Combine(modsBasePath, AmsContentsDir)); + var titleModsPath = FindTitleDir(contentsDir, titleId); + + if (titleModsPath == null) + { + Logger.PrintInfo(LogClass.ModLoader, $"Creating mods dir for Title {titleId.ToUpper()}"); + titleModsPath = contentsDir.CreateSubdirectory(titleId); + } + + return titleModsPath.FullName; + } + + // Static Query Methods + public static void QueryPatchDirs(PatchCache cache, DirectoryInfo patchDir, DirectoryInfo searchDir) + { + if (!patchDir.Exists || cache.SearchedDirs.Contains(searchDir.FullName)) return; + + var patches = cache.KipPatches; + string type = null; + + 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()) + { + patches.Add(new Mod(modDir.Name, modDir)); + Logger.PrintInfo(LogClass.ModLoader, $"Found {type} patch '{modDir.Name}'"); + } + } + + public static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir) + { + if (!titleDir.Exists) return; + + var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer)); + if (fsFile.Exists) + { + mods.RomfsContainers.Add(new Mod($"<{titleDir.Name} RomFs>", fsFile)); + } + + fsFile = new FileInfo(Path.Combine(titleDir.FullName, ExefsContainer)); + if (fsFile.Exists) + { + mods.ExefsContainers.Add(new Mod($"<{titleDir.Name} ExeFs>", fsFile)); + } + + System.Text.StringBuilder types = new System.Text.StringBuilder(5); + + foreach (var modDir in titleDir.EnumerateDirectories()) + { + types.Clear(); + Mod mod = new Mod("", null); + + if (StrEquals(RomfsDir, modDir.Name)) + { + mods.RomfsDirs.Add(mod = new Mod($"<{titleDir.Name} RomFs>", modDir)); + types.Append('R'); + } + else if (StrEquals(ExefsDir, modDir.Name)) + { + mods.ExefsDirs.Add(mod = new Mod($"<{titleDir.Name} ExeFs>", modDir)); + types.Append('E'); + } + else + { + var romfs = new DirectoryInfo(Path.Combine(modDir.FullName, RomfsDir)); + var exefs = new DirectoryInfo(Path.Combine(modDir.FullName, ExefsDir)); + if (romfs.Exists) + { + mods.RomfsDirs.Add(mod = new Mod(modDir.Name, romfs)); + types.Append('R'); + } + if (exefs.Exists) + { + mods.ExefsDirs.Add(mod = new Mod(modDir.Name, exefs)); + types.Append('E'); + } + } + + if (types.Length > 0) Logger.PrintInfo(LogClass.ModLoader, $"Found mod '{mod.Name}' [{types}]"); + } + } + + public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ulong titleId) + { + if (!contentsDir.Exists) return; + + Logger.PrintInfo(LogClass.ModLoader, $"Searching mods for Title {titleId:X16}"); + + var titleDir = FindTitleDir(contentsDir, $"{titleId:x16}"); + + if (titleDir != null) + { + QueryTitleDir(mods, titleDir); + } + } + + public static void CollectMods(ModCache mods, PatchCache patches, ulong? titleId, params string[] searchDirPaths) + { + static bool IsPatchesDir(string name) => StrEquals(AmsNsoPatchDir, name) || + StrEquals(AmsNroPatchDir, name) || + StrEquals(AmsKipPatchDir, name); + + static bool TryQuery(ModCache mods, PatchCache patches, ulong? titleId, DirectoryInfo dir, DirectoryInfo searchDir) + { + if (StrEquals(AmsContentsDir, dir.Name)) + { + if (titleId.HasValue) + { + QueryContentsDir(mods, dir, (ulong)titleId); + + return true; + } + } + else if (IsPatchesDir(dir.Name)) + { + QueryPatchDirs(patches, dir, searchDir); + + return true; + } + + return false; + } + + foreach (var path in searchDirPaths) + { + var dir = new DirectoryInfo(path); + if (!dir.Exists) + { + Logger.PrintWarning(LogClass.ModLoader, $"Mod Search Dir '{dir.FullName}' doesn't exist"); + continue; + } + + if (!TryQuery(mods, patches, titleId, dir, dir)) + { + foreach (var subdir in dir.EnumerateDirectories()) + { + TryQuery(mods, patches, titleId, subdir, dir); + } + } + + patches.SearchedDirs.Add(dir.FullName); + } + } + + public void CollectMods(ulong titleId, params string[] searchDirPaths) + { + if (!AppMods.TryGetValue(titleId, out ModCache mods)) + { + mods = new ModCache(); + AppMods[titleId] = mods; + } + + CollectMods(mods, Patches, titleId, searchDirPaths); + } + + internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage) + { + if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.RomfsDirs.Count + mods.RomfsContainers.Count == 0) + { + return baseStorage; + } + + var fileSet = new HashSet(); + var builder = new RomFsBuilder(); + int count = 0; + + Logger.PrintInfo(LogClass.ModLoader, $"Applying RomFS mods for Title {titleId:X16}"); + + // Prioritize loose files first + foreach (var mod in mods.RomfsDirs) + { + using (IFileSystem fs = new LocalFileSystem(mod.Path.FullName)) + { + AddFiles(fs, mod.Name, fileSet, builder); + } + count++; + } + + // Then files inside images + foreach (var mod in mods.RomfsContainers) + { + Logger.PrintInfo(LogClass.ModLoader, $"Found 'romfs.bin' for Title {titleId:X16}"); + using (IFileSystem fs = new RomFsFileSystem(mod.Path.OpenRead().AsStorage())) + { + AddFiles(fs, mod.Name, fileSet, builder); + } + count++; + } + + if (fileSet.Count == 0) + { + Logger.PrintInfo(LogClass.ModLoader, "No files found. Using base RomFS"); + + return baseStorage; + } + + Logger.PrintInfo(LogClass.ModLoader, $"Replaced {fileSet.Count} file(s) over {count} mod(s). Processing base storage..."); + + // And finally, the base romfs + var baseRom = new RomFsFileSystem(baseStorage); + foreach (var entry in baseRom.EnumerateEntries() + .Where(f => f.Type == DirectoryEntryType.File && !fileSet.Contains(f.FullPath)) + .OrderBy(f => f.FullPath, StringComparer.Ordinal)) + { + baseRom.OpenFile(out IFile file, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + builder.AddFile(entry.FullPath, file); + } + + Logger.PrintInfo(LogClass.ModLoader, "Building new RomFS..."); + IStorage newStorage = builder.Build(); + Logger.PrintInfo(LogClass.ModLoader, "Using modded RomFS"); + + return newStorage; + } + + private static void AddFiles(IFileSystem fs, string modName, HashSet fileSet, RomFsBuilder builder) + { + foreach (var entry in fs.EnumerateEntries() + .Where(f => f.Type == DirectoryEntryType.File) + .OrderBy(f => f.FullPath, StringComparer.Ordinal)) + { + fs.OpenFile(out IFile file, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + if (fileSet.Add(entry.FullPath)) + { + builder.AddFile(entry.FullPath, file); + } + else + { + Logger.PrintWarning(LogClass.ModLoader, $" Skipped duplicate file '{entry.FullPath}' from '{modName}'", "ApplyRomFsMods"); + } + } + } + + internal bool ReplaceExefsPartition(ulong titleId, ref IFileSystem exefs) + { + if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsContainers.Count == 0) + { + return false; + } + + if (mods.ExefsContainers.Count > 1) + { + Logger.PrintWarning(LogClass.ModLoader, "Multiple ExeFS partition replacements detected"); + } + + Logger.PrintInfo(LogClass.ModLoader, $"Using replacement ExeFS partition"); + + exefs = new PartitionFileSystem(mods.ExefsContainers[0].Path.OpenRead().AsStorage()); + + return true; + } + + internal bool ApplyExefsMods(ulong titleId, List nsos) + { + if (!AppMods.TryGetValue(titleId, out ModCache mods) || mods.ExefsDirs.Count == 0) + { + return false; + } + + bool replaced = false; + + if (nsos.Count > 32) + { + throw new ArgumentOutOfRangeException("NSO Count is more than 32"); + } + + var exeMods = mods.ExefsDirs; + + BitVector32 stubs = new BitVector32(); + BitVector32 repls = new BitVector32(); + + foreach (var mod in exeMods) + { + for (int i = 0; i < nsos.Count; ++i) + { + var nso = nsos[i]; + var nsoName = nso.Name; + + FileInfo nsoFile = new FileInfo(Path.Combine(mod.Path.FullName, nsoName)); + if (nsoFile.Exists) + { + if (repls[1 << i]) + { + Logger.PrintWarning(LogClass.ModLoader, $"Multiple replacements to '{nsoName}'"); + continue; + } + + repls[1 << i] = true; + + nsos[i] = new NsoExecutable(nsoFile.OpenRead().AsStorage(), nsoName); + Logger.PrintInfo(LogClass.ModLoader, $"NSO '{nsoName}' replaced"); + + replaced = true; + + continue; + } + + stubs[1 << i] |= File.Exists(Path.Combine(mod.Path.FullName, nsoName + StubExtension)); + } + } + + for (int i = nsos.Count - 1; i >= 0; --i) + { + if (stubs[1 << i] && !repls[1 << i]) // Prioritizes replacements over stubs + { + Logger.PrintInfo(LogClass.ModLoader, $" NSO '{nsos[i].Name}' stubbed"); + nsos.RemoveAt(i); + replaced = true; + } + } + + return replaced; + } + + internal void ApplyNroPatches(NroExecutable nro) + { + var nroPatches = Patches.NroPatches; + + if (nroPatches.Count == 0) return; + + // NRO patches aren't offset relative to header unlike NSO + // according to Atmosphere's ro patcher module + ApplyProgramPatches(nroPatches, 0, nro); + } + + internal bool ApplyNsoPatches(ulong titleId, params IExecutable[] programs) + { + AppMods.TryGetValue(titleId, out ModCache mods); + var nsoMods = Patches.NsoPatches.Concat(mods.ExefsDirs); + + // NSO patches are created with offset 0 according to Atmosphere's patcher module + // But `Program` doesn't contain the header which is 0x100 bytes. So, we adjust for that here + return ApplyProgramPatches(nsoMods, 0x100, programs); + } + + private static bool ApplyProgramPatches(IEnumerable> mods, int protectedOffset, params IExecutable[] programs) + { + int count = 0; + + MemPatch[] patches = new MemPatch[programs.Length]; + + for (int i = 0; i < patches.Length; ++i) + { + patches[i] = new MemPatch(); + } + + var buildIds = programs.Select(p => p switch + { + NsoExecutable nso => BitConverter.ToString(nso.BuildId.Bytes.ToArray()).Replace("-", "").TrimEnd('0'), + NroExecutable nro => BitConverter.ToString(nro.Header.BuildId).Replace("-", "").TrimEnd('0'), + _ => string.Empty + }).ToList(); + + int GetIndex(string buildId) => buildIds.FindIndex(id => id == buildId); // O(n) but list is small + + // Collect patches + foreach (var mod in mods) + { + var patchDir = mod.Path; + foreach (var patchFile in patchDir.EnumerateFiles()) + { + if (StrEquals(".ips", patchFile.Extension)) // IPS|IPS32 + { + string filename = Path.GetFileNameWithoutExtension(patchFile.FullName).Split('.')[0]; + string buildId = filename.TrimEnd('0'); + + int index = GetIndex(buildId); + if (index == -1) + { + continue; + } + + Logger.PrintInfo(LogClass.ModLoader, $"Matching IPS patch '{patchFile.Name}' in '{mod.Name}' bid={buildId}"); + + using var fs = patchFile.OpenRead(); + using var reader = new BinaryReader(fs); + + var patcher = new IpsPatcher(reader); + patcher.AddPatches(patches[index]); + } + else if (StrEquals(".pchtxt", patchFile.Extension)) // IPSwitch + { + using var fs = patchFile.OpenRead(); + using var reader = new StreamReader(fs); + + var patcher = new IPSwitchPatcher(reader); + + int index = GetIndex(patcher.BuildId); + if (index == -1) + { + continue; + } + + Logger.PrintInfo(LogClass.ModLoader, $"Matching IPSwitch patch '{patchFile.Name}' in '{mod.Name}' bid={patcher.BuildId}"); + + patcher.AddPatches(patches[index]); + } + } + } + + // Apply patches + for (int i = 0; i < programs.Length; ++i) + { + count += patches[i].Patch(programs[i].Program, protectedOffset); + } + + return count > 0; + } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/ProgramLoader.cs b/Ryujinx.HLE/HOS/ProgramLoader.cs index cf5b15bac4..231830a366 100644 --- a/Ryujinx.HLE/HOS/ProgramLoader.cs +++ b/Ryujinx.HLE/HOS/ProgramLoader.cs @@ -179,7 +179,7 @@ namespace Ryujinx.HLE.HOS ProcessCreationInfo creationInfo = new ProcessCreationInfo( metaData.TitleName, - metaData.ProcessCategory, + metaData.Version, metaData.Aci0.TitleId, codeStart, codePagesCount, diff --git a/Ryujinx.HLE/HOS/Services/Ro/IRoInterface.cs b/Ryujinx.HLE/HOS/Services/Ro/IRoInterface.cs index 3ff622b656..46374acc45 100644 --- a/Ryujinx.HLE/HOS/Services/Ro/IRoInterface.cs +++ b/Ryujinx.HLE/HOS/Services/Ro/IRoInterface.cs @@ -1,4 +1,5 @@ -using Ryujinx.Common; +using LibHac.FsSystem; +using Ryujinx.Common; using Ryujinx.Cpu; using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Memory; @@ -163,33 +164,36 @@ namespace Ryujinx.HLE.HOS.Services.Ro stream.Position = 0; - NroExecutable executable = new NroExecutable(stream, nroAddress, bssAddress); + NroExecutable nro = new NroExecutable(stream.AsStorage(), nroAddress, bssAddress); - // check if everything is page align. - if ((executable.Text.Length & 0xFFF) != 0 || (executable.Ro.Length & 0xFFF) != 0 || - (executable.Data.Length & 0xFFF) != 0 || (executable.BssSize & 0xFFF) != 0) + // Check if everything is page align. + if ((nro.Text.Length & 0xFFF) != 0 || (nro.Ro.Length & 0xFFF) != 0 || + (nro.Data.Length & 0xFFF) != 0 || (nro.BssSize & 0xFFF) != 0) { return ResultCode.InvalidNro; } - // check if everything is contiguous. - if (executable.RoOffset != executable.TextOffset + executable.Text.Length || - executable.DataOffset != executable.RoOffset + executable.Ro.Length || - nroFileSize != executable.DataOffset + executable.Data.Length) + // Check if everything is contiguous. + if (nro.RoOffset != nro.TextOffset + nro.Text.Length || + nro.DataOffset != nro.RoOffset + nro.Ro.Length || + nroFileSize != nro.DataOffset + nro.Data.Length) { return ResultCode.InvalidNro; } - // finally check the bss size match. - if ((ulong)executable.BssSize != bssSize) + // Check the bss size match. + if ((ulong)nro.BssSize != bssSize) { return ResultCode.InvalidNro; } - int totalSize = executable.Text.Length + executable.Ro.Length + executable.Data.Length + executable.BssSize; + int totalSize = nro.Text.Length + nro.Ro.Length + nro.Data.Length + nro.BssSize; + + // Apply patches + context.Device.FileSystem.ModLoader.ApplyNroPatches(nro); res = new NroInfo( - executable, + nro, nroHash, nroAddress, nroSize, diff --git a/Ryujinx.HLE/Loaders/Executables/IExecutable.cs b/Ryujinx.HLE/Loaders/Executables/IExecutable.cs index 440e8f5fae..76a550df71 100644 --- a/Ryujinx.HLE/Loaders/Executables/IExecutable.cs +++ b/Ryujinx.HLE/Loaders/Executables/IExecutable.cs @@ -1,10 +1,13 @@ +using System; + namespace Ryujinx.HLE.Loaders.Executables { interface IExecutable { - byte[] Text { get; } - byte[] Ro { get; } - byte[] Data { get; } + byte[] Program { get; } + Span Text { get; } + Span Ro { get; } + Span Data { get; } int TextOffset { get; } int RoOffset { get; } diff --git a/Ryujinx.HLE/Loaders/Executables/KipExecutable.cs b/Ryujinx.HLE/Loaders/Executables/KipExecutable.cs index 0f1309c028..a44b7c486e 100644 --- a/Ryujinx.HLE/Loaders/Executables/KipExecutable.cs +++ b/Ryujinx.HLE/Loaders/Executables/KipExecutable.cs @@ -1,18 +1,24 @@ using LibHac.Fs; using LibHac.Loader; +using System; namespace Ryujinx.HLE.Loaders.Executables { class KipExecutable : IExecutable { - public byte[] Text { get; } - public byte[] Ro { get; } - public byte[] Data { get; } + public byte[] Program { get; } + public Span Text => Program.AsSpan().Slice(TextOffset, TextSize); + public Span Ro => Program.AsSpan().Slice(RoOffset, RoSize); + public Span Data => Program.AsSpan().Slice(DataOffset, DataSize); public int TextOffset { get; } public int RoOffset { get; } public int DataOffset { get; } public int BssOffset { get; } + + public int TextSize { get; } + public int RoSize { get; } + public int DataSize { get; } public int BssSize { get; } public int[] Capabilities { get; } @@ -25,7 +31,6 @@ namespace Ryujinx.HLE.Loaders.Executables public byte IdealCoreId { get; } public int Version { get; } public string Name { get; } - public KipExecutable(IStorage inStorage) { KipReader reader = new KipReader(); @@ -57,20 +62,23 @@ namespace Ryujinx.HLE.Loaders.Executables Capabilities[index] = (int)reader.Capabilities[index]; } - Text = DecompressSection(reader, KipReader.SegmentType.Text); - Ro = DecompressSection(reader, KipReader.SegmentType.Ro); - Data = DecompressSection(reader, KipReader.SegmentType.Data); + reader.GetSegmentSize(KipReader.SegmentType.Data, out int uncompressedSize).ThrowIfFailure(); + Program = new byte[DataOffset + uncompressedSize]; + + TextSize = DecompressSection(reader, KipReader.SegmentType.Text, TextOffset, Program); + RoSize = DecompressSection(reader, KipReader.SegmentType.Ro, RoOffset, Program); + DataSize = DecompressSection(reader, KipReader.SegmentType.Data, DataOffset, Program); } - private static byte[] DecompressSection(KipReader reader, KipReader.SegmentType segmentType) + private static int DecompressSection(KipReader reader, KipReader.SegmentType segmentType, int offset, byte[] Program) { reader.GetSegmentSize(segmentType, out int uncompressedSize).ThrowIfFailure(); - byte[] result = new byte[uncompressedSize]; + var span = Program.AsSpan().Slice(offset, uncompressedSize); - reader.ReadSegment(segmentType, result).ThrowIfFailure(); + reader.ReadSegment(segmentType, span).ThrowIfFailure(); - return result; + return uncompressedSize; } } } \ No newline at end of file diff --git a/Ryujinx.HLE/Loaders/Executables/NroExecutable.cs b/Ryujinx.HLE/Loaders/Executables/NroExecutable.cs index 4a7f21167a..b7a887b7cb 100644 --- a/Ryujinx.HLE/Loaders/Executables/NroExecutable.cs +++ b/Ryujinx.HLE/Loaders/Executables/NroExecutable.cs @@ -1,67 +1,38 @@ -using System.IO; +using LibHac; +using LibHac.Fs; +using System; namespace Ryujinx.HLE.Loaders.Executables { - class NroExecutable : IExecutable + class NroExecutable : Nro, IExecutable { - public byte[] Text { get; private set; } - public byte[] Ro { get; private set; } - public byte[] Data { get; private set; } + public byte[] Program { get; } + public Span Text => Program.AsSpan().Slice(TextOffset, (int)Header.NroSegments[0].Size); + public Span Ro => Program.AsSpan().Slice(RoOffset, (int)Header.NroSegments[1].Size); + public Span Data => Program.AsSpan().Slice(DataOffset, (int)Header.NroSegments[2].Size); - public int Mod0Offset { get; private set; } - public int TextOffset { get; private set; } - public int RoOffset { get; private set; } - public int DataOffset { get; private set; } - public int BssSize { get; private set; } - public int FileSize { get; private set; } + public int TextOffset => (int)Header.NroSegments[0].FileOffset; + public int RoOffset => (int)Header.NroSegments[1].FileOffset; + public int DataOffset => (int)Header.NroSegments[2].FileOffset; + public int BssOffset => DataOffset + Data.Length; + public int BssSize => (int)Header.BssSize; - public int BssOffset => DataOffset + Data.Length; + public int Mod0Offset => Start.Mod0Offset; + public int FileSize => (int)Header.Size; public ulong SourceAddress { get; private set; } public ulong BssAddress { get; private set; } - public NroExecutable(Stream input, ulong sourceAddress = 0, ulong bssAddress = 0) + public NroExecutable(IStorage inStorage, ulong sourceAddress = 0, ulong bssAddress = 0) : base(inStorage) { + Program = new byte[FileSize]; + SourceAddress = sourceAddress; BssAddress = bssAddress; - BinaryReader reader = new BinaryReader(input); - - input.Seek(4, SeekOrigin.Begin); - - int mod0Offset = reader.ReadInt32(); - int padding8 = reader.ReadInt32(); - int paddingC = reader.ReadInt32(); - int nroMagic = reader.ReadInt32(); - int unknown14 = reader.ReadInt32(); - int fileSize = reader.ReadInt32(); - int unknown1C = reader.ReadInt32(); - int textOffset = reader.ReadInt32(); - int textSize = reader.ReadInt32(); - int roOffset = reader.ReadInt32(); - int roSize = reader.ReadInt32(); - int dataOffset = reader.ReadInt32(); - int dataSize = reader.ReadInt32(); - int bssSize = reader.ReadInt32(); - - Mod0Offset = mod0Offset; - TextOffset = textOffset; - RoOffset = roOffset; - DataOffset = dataOffset; - BssSize = bssSize; - - byte[] Read(long position, int size) - { - input.Seek(position, SeekOrigin.Begin); - - return reader.ReadBytes(size); - } - - Text = Read(textOffset, textSize); - Ro = Read(roOffset, roSize); - Data = Read(dataOffset, dataSize); - - FileSize = fileSize; + OpenNroSegment(NroSegmentType.Text, false).Read(0, Text); + OpenNroSegment(NroSegmentType.Ro , false).Read(0, Ro); + OpenNroSegment(NroSegmentType.Data, false).Read(0, Data); } } } \ No newline at end of file diff --git a/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs b/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs index bbe2c87fe2..96f1df4e86 100644 --- a/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs +++ b/Ryujinx.HLE/Loaders/Executables/NsoExecutable.cs @@ -1,23 +1,32 @@ +using LibHac.Common; using LibHac.Fs; using LibHac.FsSystem; using LibHac.Loader; +using System; namespace Ryujinx.HLE.Loaders.Executables { class NsoExecutable : IExecutable { - public byte[] Text { get; } - public byte[] Ro { get; } - public byte[] Data { get; } + public byte[] Program { get; } + public Span Text => Program.AsSpan().Slice(TextOffset, TextSize); + public Span Ro => Program.AsSpan().Slice(RoOffset, RoSize); + public Span Data => Program.AsSpan().Slice(DataOffset, DataSize); public int TextOffset { get; } public int RoOffset { get; } public int DataOffset { get; } public int BssOffset => DataOffset + Data.Length; + public int TextSize { get; } + public int RoSize { get; } + public int DataSize { get; } public int BssSize { get; } - public NsoExecutable(IStorage inStorage) + public string Name; + public Buffer32 BuildId; + + public NsoExecutable(IStorage inStorage, string name = null) { NsoReader reader = new NsoReader(); @@ -28,20 +37,26 @@ namespace Ryujinx.HLE.Loaders.Executables DataOffset = (int)reader.Header.Segments[2].MemoryOffset; BssSize = (int)reader.Header.BssSize; - Text = DecompressSection(reader, NsoReader.SegmentType.Text); - Ro = DecompressSection(reader, NsoReader.SegmentType.Ro); - Data = DecompressSection(reader, NsoReader.SegmentType.Data); + reader.GetSegmentSize(NsoReader.SegmentType.Data, out uint uncompressedSize).ThrowIfFailure(); + Program = new byte[DataOffset + uncompressedSize]; + + TextSize = DecompressSection(reader, NsoReader.SegmentType.Text, TextOffset, Program); + RoSize = DecompressSection(reader, NsoReader.SegmentType.Ro, RoOffset, Program); + DataSize = DecompressSection(reader, NsoReader.SegmentType.Data, DataOffset, Program); + + Name = name; + BuildId = reader.Header.ModuleId; } - private static byte[] DecompressSection(NsoReader reader, NsoReader.SegmentType segmentType) + private static int DecompressSection(NsoReader reader, NsoReader.SegmentType segmentType, int offset, byte[] Program) { reader.GetSegmentSize(segmentType, out uint uncompressedSize).ThrowIfFailure(); - byte[] result = new byte[uncompressedSize]; + var span = Program.AsSpan().Slice(offset, (int)uncompressedSize); - reader.ReadSegment(segmentType, result).ThrowIfFailure(); + reader.ReadSegment(segmentType, span).ThrowIfFailure(); - return result; + return (int)uncompressedSize; } } } \ No newline at end of file diff --git a/Ryujinx.HLE/Loaders/Mods/IPSPatcher.cs b/Ryujinx.HLE/Loaders/Mods/IPSPatcher.cs new file mode 100644 index 0000000000..d104d25aa5 --- /dev/null +++ b/Ryujinx.HLE/Loaders/Mods/IPSPatcher.cs @@ -0,0 +1,117 @@ +using Ryujinx.Common.Logging; +using System; +using System.IO; +using System.Text; + +namespace Ryujinx.HLE.Loaders.Mods +{ + class IpsPatcher + { + MemPatch _patches; + + public IpsPatcher(BinaryReader reader) + { + _patches = ParseIps(reader); + if (_patches != null) + { + Logger.PrintInfo(LogClass.ModLoader, "IPS patch loaded successfully"); + } + } + + private static MemPatch ParseIps(BinaryReader reader) + { + Span IpsHeaderMagic = Encoding.ASCII.GetBytes("PATCH").AsSpan(); + Span IpsTailMagic = Encoding.ASCII.GetBytes("EOF").AsSpan(); + Span Ips32HeaderMagic = Encoding.ASCII.GetBytes("IPS32").AsSpan(); + Span Ips32TailMagic = Encoding.ASCII.GetBytes("EEOF").AsSpan(); + + MemPatch patches = new MemPatch(); + var header = reader.ReadBytes(IpsHeaderMagic.Length).AsSpan(); + + if (header.Length != IpsHeaderMagic.Length) + { + return null; + } + + bool is32; + Span tailSpan; + + if (header.SequenceEqual(IpsHeaderMagic)) + { + is32 = false; + tailSpan = IpsTailMagic; + } + else if (header.SequenceEqual(Ips32HeaderMagic)) + { + is32 = true; + tailSpan = Ips32TailMagic; + } + else + { + return null; + } + + byte[] buf = new byte[tailSpan.Length]; + + bool ReadNext(int size) => reader.Read(buf, 0, size) != size; + + while (true) + { + if (ReadNext(buf.Length)) + { + return null; + } + + if (buf.AsSpan().SequenceEqual(tailSpan)) + { + break; + } + + int patchOffset = is32 ? buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3] + : buf[0] << 16 | buf[1] << 8 | buf[2]; + + if (ReadNext(2)) + { + return null; + } + + int patchSize = buf[0] << 8 | buf[1]; + + if (patchSize == 0) // RLE/Fill mode + { + if (ReadNext(2)) + { + return null; + } + + int fillLength = buf[0] << 8 | buf[1]; + + if (ReadNext(1)) + { + return null; + } + + patches.AddFill((uint)patchOffset, fillLength, buf[0]); + } + else // Copy mode + { + var patch = reader.ReadBytes(patchSize); + + if (patch.Length != patchSize) + { + return null; + } + + patches.Add((uint)patchOffset, patch); + } + } + + return patches; + } + + public void AddPatches(MemPatch patches) + { + patches.AddFrom(_patches); + } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/Loaders/Mods/IPSwitchPatcher.cs b/Ryujinx.HLE/Loaders/Mods/IPSwitchPatcher.cs new file mode 100644 index 0000000000..d5b0b1be83 --- /dev/null +++ b/Ryujinx.HLE/Loaders/Mods/IPSwitchPatcher.cs @@ -0,0 +1,262 @@ +using Ryujinx.Common.Logging; +using System; +using System.IO; +using System.Text; + +namespace Ryujinx.HLE.Loaders.Mods +{ + class IPSwitchPatcher + { + readonly StreamReader _reader; + public string BuildId { get; } + + const string BidHeader = "@nsobid-"; + + public IPSwitchPatcher(StreamReader reader) + { + string header = reader.ReadLine(); + if (header == null || !header.StartsWith(BidHeader)) + { + Logger.PrintError(LogClass.ModLoader, "IPSwitch: Malformed PCHTXT file. Skipping..."); + return; + } + + _reader = reader; + BuildId = header.Substring(BidHeader.Length).TrimEnd().TrimEnd('0'); + } + + private enum Token + { + Normal, + String, + EscapeChar, + Comment + } + + // Uncomments line and unescapes C style strings within + private static string PreprocessLine(string line) + { + StringBuilder str = new StringBuilder(); + Token state = Token.Normal; + + for (int i = 0; i < line.Length; ++i) + { + char c = line[i]; + char la = i + 1 != line.Length ? line[i + 1] : '\0'; + + switch (state) + { + case Token.Normal: + state = c == '"' ? Token.String : + c == '/' && la == '/' ? Token.Comment : + c == '/' && la != '/' ? Token.Comment : // Ignore error and stop parsing + Token.Normal; + break; + case Token.String: + state = c switch + { + '"' => Token.Normal, + '\\' => Token.EscapeChar, + _ => Token.String + }; + break; + case Token.EscapeChar: + state = Token.String; + c = c switch + { + 'a' => '\a', + 'b' => '\b', + 'f' => '\f', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + 'v' => '\v', + '\\' => '\\', + _ => '?' + }; + break; + } + + if (state == Token.Comment) break; + + if (state < Token.EscapeChar) + { + str.Append(c); + } + } + + return str.ToString().Trim(); + } + + static int ParseHexByte(byte c) + { + if (c >= '0' && c <= '9') + { + return c - '0'; + } + else if (c >= 'A' && c <= 'F') + { + return c - 'A' + 10; + } + else if (c >= 'a' && c <= 'f') + { + return c - 'a' + 10; + } + else + { + return 0; + } + } + + // Big Endian + static byte[] Hex2ByteArrayBE(string hexstr) + { + if ((hexstr.Length & 1) == 1) return null; + + byte[] bytes = new byte[hexstr.Length >> 1]; + + for (int i = 0; i < hexstr.Length; i += 2) + { + int high = ParseHexByte((byte)hexstr[i]); + int low = ParseHexByte((byte)hexstr[i + 1]); + bytes[i >> 1] = (byte)((high << 4) | low); + } + + return bytes; + } + + // Auto base discovery + private static bool ParseInt(string str, out int value) + { + if (str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) + { + return Int32.TryParse(str.Substring(2), System.Globalization.NumberStyles.HexNumber, null, out value); + } + else + { + return Int32.TryParse(str, System.Globalization.NumberStyles.Integer, null, out value); + } + } + + private MemPatch Parse() + { + if (_reader == null) + { + return null; + } + + MemPatch patches = new MemPatch(); + + bool enabled = true; + bool printValues = false; + int offset_shift = 0; + + string line; + int lineNum = 0; + + static void Print(string s) => Logger.PrintInfo(LogClass.ModLoader, $"IPSwitch: {s}"); + + void ParseWarn() => Logger.PrintWarning(LogClass.ModLoader, $"IPSwitch: Parse error at line {lineNum} for bid={BuildId}"); + + while ((line = _reader.ReadLine()) != null) + { + line = PreprocessLine(line); + lineNum += 1; + + if (line.Length == 0) + { + continue; + } + else if (line.StartsWith('#')) + { + Print(line); + } + else if (line.StartsWith("@stop")) + { + break; + } + else if (line.StartsWith("@enabled")) + { + enabled = true; + } + else if (line.StartsWith("@disabled")) + { + enabled = false; + } + else if (line.StartsWith("@flag")) + { + var tokens = line.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries); + + if (tokens.Length < 2) + { + ParseWarn(); + continue; + } + + if (tokens[1] == "offset_shift") + { + if (tokens.Length != 3 || !ParseInt(tokens[2], out offset_shift)) + { + ParseWarn(); + continue; + } + } + else if (tokens[1] == "print_values") + { + printValues = true; + } + } + else if (line.StartsWith('@')) + { + // Ignore + } + else + { + if (!enabled) + { + continue; + } + + var tokens = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + + if (tokens.Length < 2) + { + ParseWarn(); + continue; + } + + if (!Int32.TryParse(tokens[0], System.Globalization.NumberStyles.HexNumber, null, out int offset)) + { + ParseWarn(); + continue; + } + + offset += offset_shift; + + if (printValues) + { + Print($"print_values 0x{offset:x} <= {tokens[1]}"); + } + + if (tokens[1][0] == '"') + { + var patch = Encoding.ASCII.GetBytes(tokens[1].Trim('"')); + patches.Add((uint)offset, patch); + } + else + { + var patch = Hex2ByteArrayBE(tokens[1]); + patches.Add((uint)offset, patch); + } + } + } + + return patches; + } + + public void AddPatches(MemPatch patches) + { + patches.AddFrom(Parse()); + } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/Loaders/Mods/MemPatch.cs b/Ryujinx.HLE/Loaders/Mods/MemPatch.cs new file mode 100644 index 0000000000..0fc1915947 --- /dev/null +++ b/Ryujinx.HLE/Loaders/Mods/MemPatch.cs @@ -0,0 +1,98 @@ +using Ryujinx.Cpu; +using System; +using System.Collections.Generic; +using System.Linq; + +using Ryujinx.Common.Logging; + +namespace Ryujinx.HLE.Loaders.Mods +{ + public class MemPatch + { + readonly Dictionary _patches = new Dictionary(); + + /// + /// Adds a patch to specified offset. Overwrites if already present. + /// + /// Memory offset + /// The patch to add + public void Add(uint offset, byte[] patch) + { + _patches[offset] = patch; + } + + /// + /// Adds a patch in the form of an RLE (Fill mode). + /// + /// Memory offset + /// + /// The byte to fill + public void AddFill(uint offset, int length, byte filler) + { + // TODO: Can be made space efficient by changing `_patches` + // Should suffice for now + byte[] patch = new byte[length]; + patch.AsSpan().Fill(filler); + + _patches[offset] = patch; + } + + /// + /// Adds all patches from an existing MemPatch + /// + /// The patches to add + public void AddFrom(MemPatch patches) + { + if (patches == null) + { + return; + } + + foreach (var (patchOffset, patch) in patches._patches) + { + _patches[patchOffset] = patch; + } + } + + /// + /// Applies all the patches added to this instance. + /// + /// + /// Patches are applied in ascending order of offsets to guarantee + /// overlapping patches always apply the same way. + /// + /// The span of bytes to patch + /// The maximum size of the slice of patchable memory + /// A secondary offset used in special cases (NSO header) + /// Successful patches count + public int Patch(Span memory, int protectedOffset = 0) + { + int count = 0; + foreach (var (offset, patch) in _patches.OrderBy(item => item.Key)) + { + int patchOffset = (int)offset; + int patchSize = patch.Length; + + if (patchOffset < protectedOffset || patchOffset > memory.Length) + { + continue; // Add warning? + } + + patchOffset -= protectedOffset; + + if (patchOffset + patchSize > memory.Length) + { + patchSize = memory.Length - (int)patchOffset; // Add warning? + } + + Logger.PrintInfo(LogClass.ModLoader, $"Patching address offset {patchOffset:x} <= {BitConverter.ToString(patch).Replace('-', ' ')} len={patchSize}"); + + patch.AsSpan().Slice(0, patchSize).CopyTo(memory.Slice(patchOffset, patchSize)); + + count++; + } + + return count; + } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/Loaders/Npdm/Npdm.cs b/Ryujinx.HLE/Loaders/Npdm/Npdm.cs index 345721c7ac..7f1e2935e3 100644 --- a/Ryujinx.HLE/Loaders/Npdm/Npdm.cs +++ b/Ryujinx.HLE/Loaders/Npdm/Npdm.cs @@ -16,7 +16,7 @@ namespace Ryujinx.HLE.Loaders.Npdm public byte MainThreadPriority { get; private set; } public byte DefaultCpuId { get; private set; } public int PersonalMmHeapSize { get; private set; } - public int ProcessCategory { get; private set; } + public int Version { get; private set; } public int MainThreadStackSize { get; private set; } public string TitleName { get; set; } public byte[] ProductCode { get; private set; } @@ -48,7 +48,7 @@ namespace Ryujinx.HLE.Loaders.Npdm PersonalMmHeapSize = reader.ReadInt32(); - ProcessCategory = reader.ReadInt32(); + Version = reader.ReadInt32(); MainThreadStackSize = reader.ReadInt32(); diff --git a/Ryujinx/Ui/GameTableContextMenu.cs b/Ryujinx/Ui/GameTableContextMenu.cs index d2d0030542..2efe1d3e85 100644 --- a/Ryujinx/Ui/GameTableContextMenu.cs +++ b/Ryujinx/Ui/GameTableContextMenu.cs @@ -45,24 +45,24 @@ namespace Ryujinx.Ui MenuItem openSaveUserDir = new MenuItem("Open User Save Directory") { Sensitive = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0, - TooltipText = "Open the folder where the User save for the application is loaded" + TooltipText = "Open the directory which contains Application's User Saves." }; MenuItem openSaveDeviceDir = new MenuItem("Open Device Save Directory") { Sensitive = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0, - TooltipText = "Open the folder where the Device save for the application is loaded" + TooltipText = "Open the directory which contains Application's Device Saves." }; MenuItem openSaveBcatDir = new MenuItem("Open BCAT Save Directory") { Sensitive = !Util.IsEmpty(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0, - TooltipText = "Open the folder where the BCAT save for the application is loaded" + TooltipText = "Open the directory which contains Application's BCAT Saves." }; MenuItem manageTitleUpdates = new MenuItem("Manage Title Updates") { - TooltipText = "Open the title update management window" + TooltipText = "Open the Title Update management window" }; MenuItem manageDlc = new MenuItem("Manage DLC") @@ -70,6 +70,11 @@ namespace Ryujinx.Ui TooltipText = "Open the DLC management window" }; + MenuItem openTitleModDir = new MenuItem("Open Mods Directory") + { + TooltipText = "Open the directory which contains Application's Mods." + }; + string ext = System.IO.Path.GetExtension(_gameTableStore.GetValue(_rowIter, 9).ToString()).ToLower(); bool hasNca = ext == ".nca" || ext == ".nsp" || ext == ".pfs0" || ext == ".xci"; @@ -78,19 +83,19 @@ namespace Ryujinx.Ui MenuItem extractRomFs = new MenuItem("RomFS") { Sensitive = hasNca, - TooltipText = "Extract the RomFs section present in the main NCA" + TooltipText = "Extract the RomFS section from Application's current config (including updates)." }; MenuItem extractExeFs = new MenuItem("ExeFS") { Sensitive = hasNca, - TooltipText = "Extract the ExeFs section present in the main NCA" + TooltipText = "Extract the ExeFS section from Application's current config (including updates)." }; MenuItem extractLogo = new MenuItem("Logo") { Sensitive = hasNca, - TooltipText = "Extract the Logo section present in the main NCA" + TooltipText = "Extract the Logo section from Application's current config (including updates)." }; Menu extractSubMenu = new Menu(); @@ -103,14 +108,14 @@ namespace Ryujinx.Ui MenuItem managePtcMenu = new MenuItem("Cache Management"); - MenuItem purgePtcCache = new MenuItem("Purge the PPTC cache") + MenuItem purgePtcCache = new MenuItem("Purge PPTC cache") { - TooltipText = "Delete the PPTC cache of the game" + TooltipText = "Delete the Application's PPTC cache." }; - MenuItem openPtcDir = new MenuItem("Open the PPTC directory") + MenuItem openPtcDir = new MenuItem("Open PPTC directory") { - TooltipText = "Open the PPTC directory in the file explorer" + TooltipText = "Open the directory which contains Application's PPTC cache." }; Menu managePtcSubMenu = new Menu(); @@ -125,6 +130,7 @@ namespace Ryujinx.Ui openSaveBcatDir.Activated += OpenSaveBcatDir_Clicked; manageTitleUpdates.Activated += ManageTitleUpdates_Clicked; manageDlc.Activated += ManageDlc_Clicked; + openTitleModDir.Activated += OpenTitleModDir_Clicked; extractRomFs.Activated += ExtractRomFs_Clicked; extractExeFs.Activated += ExtractExeFs_Clicked; extractLogo.Activated += ExtractLogo_Clicked; @@ -137,6 +143,7 @@ namespace Ryujinx.Ui this.Add(new SeparatorMenuItem()); this.Add(manageTitleUpdates); this.Add(manageDlc); + this.Add(openTitleModDir); this.Add(new SeparatorMenuItem()); this.Add(managePtcMenu); this.Add(extractMenu); @@ -602,6 +609,21 @@ namespace Ryujinx.Ui dlcWindow.Show(); } + private void OpenTitleModDir_Clicked(object sender, EventArgs args) + { + string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); + + var modsBasePath = _virtualFileSystem.GetBaseModsPath(); + var titleModsPath = _virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId); + + Process.Start(new ProcessStartInfo + { + FileName = titleModsPath, + UseShellExecute = true, + Verb = "open" + }); + } + private void ExtractRomFs_Clicked(object sender, EventArgs args) { ExtractSection(NcaSectionType.Data);