ryujinx/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
mageven 189c0c9c72
Implement modding support (#1249)
* Implement Modding Support

* Executables: Rewrite to use contiguous mem and Spans

* Reorder ExeFs, Npdm, ControlData and SaveData calls

After discussion with gdkchan, it was decided it's best to call
LoadExeFs after all other loads are done as it starts the guest process.

* Build RomFs manually instead of Layering FS

Layered FS approach has considerable latency when building the final
romfs. So, we manually replace files in a single romfs instance.

* Add RomFs modding via storage file

* Fix and cleanup MemPatch

* Add dynamically loaded NRO patching

* Support exefs file replacement

* Rewrite ModLoader to use mods-search architecture

* Disable PPTC when exefs patches are detected

Disable PPTC on exefs replacements too

* Rewrite ModLoader, again

* Increased maintainability and matches Atmosphere closely
* Creates base mods structure if it doesn't exist
* Add Exefs partition replacement
* IPSwitch: Fix nsobid parsing

* Move mod logs to new LogClass

* Allow custom suffixes to title dirs again

* Address nits

* Add a per-App "Open Mods Directory" context menu item

Creates the path if not present.

* Normalize tooltips verbiage

* Use LocalStorage and remove unused namespaces
2020-07-09 14:31:15 +10:00

323 lines
10 KiB
C#

using LibHac;
using LibHac.Common;
using LibHac.Fs;
using LibHac.FsService;
using LibHac.FsSystem;
using LibHac.Spl;
using Ryujinx.HLE.FileSystem.Content;
using Ryujinx.HLE.HOS;
using System;
using System.IO;
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 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; }
public FileSystemServer FsServer { get; private set; }
public FileSystemClient FsClient { get; private set; }
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; }
public void LoadRomFs(string fileName)
{
RomFs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
}
public void SetRomFs(Stream romfsStream)
{
RomFs?.Close();
RomFs = romfsStream;
}
public string GetFullPath(string basePath, string fileName)
{
if (fileName.StartsWith("//"))
{
fileName = fileName.Substring(2);
}
else if (fileName.StartsWith('/'))
{
fileName = fileName.Substring(1);
}
else
{
return null;
}
string fullPath = Path.GetFullPath(Path.Combine(basePath, fileName));
if (!fullPath.StartsWith(GetBasePath()))
{
return null;
}
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);
public string GetSystemPath() => MakeFullPath(SystemPath);
internal string GetSavePath(ServiceCtx context, SaveInfo saveInfo, bool isDirectory = true)
{
string saveUserPath = "";
string baseSavePath = NandPath;
ulong currentTitleId = saveInfo.TitleId;
switch (saveInfo.SaveSpaceId)
{
case SaveSpaceId.NandUser: baseSavePath = UserNandPath; break;
case SaveSpaceId.NandSystem: baseSavePath = SystemNandPath; break;
case SaveSpaceId.SdCard: baseSavePath = Path.Combine(SdCardPath, "Nintendo"); break;
}
baseSavePath = Path.Combine(baseSavePath, "save");
if (saveInfo.TitleId == 0 && saveInfo.SaveDataType == SaveDataType.SaveData)
{
currentTitleId = context.Process.TitleId;
}
if (saveInfo.SaveSpaceId == SaveSpaceId.NandUser)
{
saveUserPath = saveInfo.UserId.IsNull ? "savecommon" : saveInfo.UserId.ToString();
}
string savePath = Path.Combine(baseSavePath,
saveInfo.SaveId.ToString("x16"),
saveUserPath,
saveInfo.SaveDataType == SaveDataType.SaveData ? currentTitleId.ToString("x16") : string.Empty);
return MakeFullPath(savePath, isDirectory);
}
public string GetFullPartitionPath(string partitionPath)
{
return MakeFullPath(partitionPath);
}
public string SwitchPathToSystemPath(string switchPath)
{
string[] parts = switchPath.Split(":");
if (parts.Length != 2)
{
return null;
}
return GetFullPath(MakeFullPath(parts[0]), parts[1]);
}
public string SystemPathToSwitchPath(string systemPath)
{
string baseSystemPath = GetBasePath() + Path.DirectorySeparatorChar;
if (systemPath.StartsWith(baseSystemPath))
{
string rawPath = systemPath.Replace(baseSystemPath, "");
int firstSeparatorOffset = rawPath.IndexOf(Path.DirectorySeparatorChar);
if (firstSeparatorOffset == -1)
{
return $"{rawPath}:/";
}
string basePath = rawPath.Substring(0, firstSeparatorOffset);
string fileName = rawPath.Substring(firstSeparatorOffset + 1);
return $"{basePath}:/{fileName}";
}
return null;
}
private string MakeFullPath(string path, bool isDirectory = true)
{
// Handles Common Switch Content Paths
switch (path)
{
case ContentPath.SdCard:
case "@Sdcard":
path = SdCardPath;
break;
case ContentPath.User:
path = UserNandPath;
break;
case ContentPath.System:
path = SystemNandPath;
break;
case ContentPath.SdCardContent:
path = Path.Combine(SdCardPath, "Nintendo", "Contents");
break;
case ContentPath.UserContent:
path = Path.Combine(UserNandPath, "Contents");
break;
case ContentPath.SystemContent:
path = Path.Combine(SystemNandPath, "Contents");
break;
}
string fullPath = Path.Combine(GetBasePath(), path);
if (isDirectory)
{
if (!Directory.Exists(fullPath))
{
Directory.CreateDirectory(fullPath);
}
}
return fullPath;
}
public DriveInfo GetDrive()
{
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();
LocalFileSystem serverBaseFs = new LocalFileSystem(GetBasePath());
DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet);
GameCard = fsServerObjects.GameCard;
SdCard = fsServerObjects.SdCard;
SdCard.SetSdCardInsertionStatus(true);
FileSystemServerConfig fsServerConfig = new FileSystemServerConfig
{
FsCreators = fsServerObjects.FsCreators,
DeviceOperator = fsServerObjects.DeviceOperator,
ExternalKeySet = KeySet.ExternalKeySet
};
FsServer = new FileSystemServer(fsServerConfig);
FsClient = FsServer.CreateFileSystemClient();
}
private void ReloadKeySet()
{
string keyFile = null;
string titleKeyFile = null;
string consoleKeyFile = null;
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
LoadSetAtPath(Path.Combine(home, ".switch"));
LoadSetAtPath(GetSystemPath());
void LoadSetAtPath(string basePath)
{
string localKeyFile = Path.Combine(basePath, "prod.keys");
string localTitleKeyFile = Path.Combine(basePath, "title.keys");
string localConsoleKeyFile = Path.Combine(basePath, "console.keys");
if (File.Exists(localKeyFile))
{
keyFile = localKeyFile;
}
if (File.Exists(localTitleKeyFile))
{
titleKeyFile = localTitleKeyFile;
}
if (File.Exists(localConsoleKeyFile))
{
consoleKeyFile = localConsoleKeyFile;
}
}
KeySet = ExternalKeyReader.ReadKeyFile(keyFile, titleKeyFile, consoleKeyFile);
}
public void ImportTickets(IFileSystem fs)
{
foreach (DirectoryEntryEx ticketEntry in fs.EnumerateEntries("/", "*.tik"))
{
Result result = fs.OpenFile(out IFile ticketFile, ticketEntry.FullPath.ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
Ticket ticket = new Ticket(ticketFile.AsStream());
KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(KeySet)));
}
}
}
public void Unload()
{
RomFs?.Dispose();
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Unload();
}
}
public static VirtualFileSystem CreateInstance()
{
if (_isInitialized)
{
throw new InvalidOperationException($"VirtualFileSystem can only be instantiated once!");
}
_isInitialized = true;
return new VirtualFileSystem();
}
}
}