diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index c665a2ddb1..1dfb55e201 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -120,7 +120,7 @@ namespace Ryujinx.HLE.HOS internal long HidBaseAddress { get; private set; } internal FileSystemServer FsServer { get; private set; } - internal FileSystemClient FsClient { get; private set; } + public FileSystemClient FsClient { get; private set; } internal EmulatedGameCard GameCard { get; private set; } diff --git a/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs b/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs index 01126e72e2..1dd5fb86e7 100644 --- a/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs +++ b/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs @@ -3,54 +3,12 @@ using LibHac.Fs; using LibHac.FsSystem; using LibHac.FsSystem.NcaUtils; using LibHac.Spl; -using Ryujinx.Common; -using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Utilities; using System.IO; namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy { static class FileSystemProxyHelper { - public static ResultCode LoadSaveDataFileSystem(ServiceCtx context, bool readOnly, out IFileSystem loadedFileSystem) - { - loadedFileSystem = null; - - SaveSpaceId saveSpaceId = (SaveSpaceId)context.RequestData.ReadInt64(); - ulong titleId = context.RequestData.ReadUInt64(); - UInt128 userId = context.RequestData.ReadStruct(); - long saveId = context.RequestData.ReadInt64(); - SaveDataType saveDataType = (SaveDataType)context.RequestData.ReadByte(); - SaveInfo saveInfo = new SaveInfo(titleId, saveId, saveDataType, saveSpaceId, userId); - string savePath = context.Device.FileSystem.GetSavePath(context, saveInfo); - - try - { - LocalFileSystem fileSystem = new LocalFileSystem(savePath); - - Result result = DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem dirFileSystem, fileSystem); - if (result.IsFailure()) - { - return (ResultCode)result.Value; - } - - LibHac.Fs.IFileSystem saveFileSystem = dirFileSystem; - - if (readOnly) - { - saveFileSystem = new ReadOnlyFileSystem(saveFileSystem); - } - - loadedFileSystem = new IFileSystem(saveFileSystem); - } - catch (HorizonResultException ex) - { - return (ResultCode)ex.ResultValue.Value; - } - - return ResultCode.Success; - } - public static ResultCode OpenNsp(ServiceCtx context, string pfsPath, out IFileSystem openedFileSystem) { openedFileSystem = null; diff --git a/Ryujinx.HLE/Switch.cs b/Ryujinx.HLE/Switch.cs index 4a15f616e7..fe3841e362 100644 --- a/Ryujinx.HLE/Switch.cs +++ b/Ryujinx.HLE/Switch.cs @@ -17,7 +17,7 @@ namespace Ryujinx.HLE internal NvGpu Gpu { get; private set; } - internal VirtualFileSystem FileSystem { get; private set; } + public VirtualFileSystem FileSystem { get; private set; } public Horizon System { get; private set; } diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 28c6ac5863..6332b046fc 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -6,6 +6,7 @@ using Ryujinx.Common.Logging; using Ryujinx.Graphics.Gal; using Ryujinx.Graphics.Gal.OpenGL; using Ryujinx.Profiler; +using Ryujinx.Ui; using System; using System.Diagnostics; using System.IO; @@ -524,6 +525,13 @@ namespace Ryujinx.UI } } + private void Migration_Pressed(object o, EventArgs args) + { + Migration migration = new Migration(_device); + + migration.Migrate(); + } + private void About_Pressed(object o, EventArgs args) { AboutWindow AboutWin = new AboutWindow(); diff --git a/Ryujinx/Ui/MainWindow.glade b/Ryujinx/Ui/MainWindow.glade index e12a7b1bb2..6cddef6f36 100644 --- a/Ryujinx/Ui/MainWindow.glade +++ b/Ryujinx/Ui/MainWindow.glade @@ -253,6 +253,22 @@ False Tools True + + + True + False + + + True + False + Migrate files from an old layout + Migrate Files + True + + + + + diff --git a/Ryujinx/Ui/Migration.cs b/Ryujinx/Ui/Migration.cs new file mode 100644 index 0000000000..606abf8b6b --- /dev/null +++ b/Ryujinx/Ui/Migration.cs @@ -0,0 +1,275 @@ +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; +using LibHac.FsSystem; +using LibHac.FsSystem.Save; +using LibHac.Ncm; +using Ryujinx.HLE; +using Ryujinx.HLE.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Ui +{ + internal class Migration + { + private Switch Device { get; } + + public Migration(Switch device) + { + Device = device; + } + + public void Migrate() + { + string basePath = Device.FileSystem.GetBasePath(); + string backupPath = Path.Combine(basePath, "Migration backup (Can delete if successful)"); + string backupUserSavePath = Path.Combine(backupPath, "userSaves"); + + if (!IsMigrationNeeded(basePath)) + return; + + MigrateDirectories(basePath); + + BackupSaves(basePath, backupPath); + + MigrateSaves(Device.System.FsClient, backupUserSavePath); + } + + private static bool IsMigrationNeeded(string basePath) + { + return !Directory.Exists(Path.Combine(basePath, "bis")) && + !Directory.Exists(Path.Combine(basePath, "sdcard")); + } + + private static void MigrateDirectories(string basePath) + { + RenameDirectory(Path.Combine(basePath, "nand"), Path.Combine(basePath, "bis")); + RenameDirectory(Path.Combine(basePath, "sdmc"), Path.Combine(basePath, "sdcard")); + } + + private static bool RenameDirectory(string oldDir, string newDir) + { + if (Directory.Exists(newDir)) + return false; + + if (!Directory.Exists(oldDir)) + { + Directory.CreateDirectory(newDir); + + return true; + } + + Directory.Move(oldDir, newDir); + + return true; + } + + private static void BackupSaves(string basePath, string backupPath) + { + Directory.CreateDirectory(backupPath); + + string userSaveDir = Path.Combine(basePath, "bis/user/save"); + string backupUserSaveDir = Path.Combine(backupPath, "userSaves"); + + if (Directory.Exists(userSaveDir)) + { + RenameDirectory(userSaveDir, backupUserSaveDir); + } + + string systemSaveDir = Path.Combine(basePath, "bis/system/save"); + string backupSystemSaveDir = Path.Combine(backupPath, "systemSaves"); + + if (Directory.Exists(systemSaveDir)) + { + RenameDirectory(systemSaveDir, backupSystemSaveDir); + } + } + + private static void MigrateSaves(FileSystemClient fsClient, string rootSaveDir) + { + if (!Directory.Exists(rootSaveDir)) + { + return; + } + + SaveFinder finder = new SaveFinder(); + finder.FindSaves(rootSaveDir); + + foreach (SaveToMigrate save in finder.Saves) + { + Result migrateResult = MigrateSave(fsClient, save); + + if (migrateResult.IsFailure()) + { + throw new HorizonResultException(migrateResult, $"Error migrating save {save.Path}"); + } + } + } + + private static Result MigrateSave(FileSystemClient fs, SaveToMigrate save) + { + SaveDataAttribute key = save.Attribute; + + Result result = fs.CreateSaveData(key.TitleId, key.UserId, key.TitleId, 0, 0, 0); + if (result.IsFailure()) return result; + + bool isOldMounted = false; + bool isNewMounted = false; + + try + { + result = fs.Register("OldSave".ToU8Span(), new LocalFileSystem(save.Path)); + if (result.IsFailure()) return result; + + isOldMounted = true; + + result = fs.MountSaveData("NewSave".ToU8Span(), key.TitleId, key.UserId); + if (result.IsFailure()) return result; + + isNewMounted = true; + + result = fs.CopyDirectory("OldSave:/", "NewSave:/"); + if (result.IsFailure()) return result; + + result = fs.Commit("NewSave"); + } + finally + { + if (isOldMounted) + { + fs.Unmount("OldSave"); + } + + if (isNewMounted) + { + fs.Unmount("NewSave"); + } + } + + return result; + } + + private class SaveFinder + { + public List Saves { get; } = new List(); + + public void FindSaves(string rootPath) + { + foreach (string subDir in Directory.EnumerateDirectories(rootPath)) + { + if (TryGetUInt64(subDir, out ulong saveDataId)) + { + SearchSaveId(subDir, saveDataId); + } + } + } + + private void SearchSaveId(string path, ulong saveDataId) + { + foreach (string subDir in Directory.EnumerateDirectories(path)) + { + if (TryGetUserId(subDir, out UserId userId)) + { + SearchUser(subDir, saveDataId, userId); + } + } + } + + private void SearchUser(string path, ulong saveDataId, UserId userId) + { + foreach (string subDir in Directory.EnumerateDirectories(path)) + { + if (TryGetUInt64(subDir, out ulong titleId) && TryGetDataPath(subDir, out string dataPath)) + { + SaveDataAttribute attribute = new SaveDataAttribute + { + Type = SaveDataType.SaveData, + UserId = userId, + TitleId = new TitleId(titleId) + }; + + SaveToMigrate save = new SaveToMigrate(dataPath, attribute); + + Saves.Add(save); + } + } + } + + private static bool TryGetDataPath(string path, out string dataPath) + { + string committedPath = Path.Combine(path, "0"); + string workingPath = Path.Combine(path, "1"); + + if (Directory.Exists(committedPath) && Directory.EnumerateFileSystemEntries(committedPath).Any()) + { + dataPath = committedPath; + return true; + } + + if (Directory.Exists(workingPath) && Directory.EnumerateFileSystemEntries(workingPath).Any()) + { + dataPath = workingPath; + return true; + } + + dataPath = default; + return false; + } + + private static bool TryGetUInt64(string path, out ulong converted) + { + string name = Path.GetFileName(path); + + if (name.Length == 16) + { + try + { + converted = Convert.ToUInt64(name, 16); + return true; + } + catch { } + } + + converted = default; + return false; + } + + private static bool TryGetUserId(string path, out UserId userId) + { + string name = Path.GetFileName(path); + + if (name.Length == 32) + { + try + { + UInt128 id = new UInt128(name); + + userId = Unsafe.As(ref id); + return true; + } + catch { } + } + + userId = default; + return false; + } + } + + private class SaveToMigrate + { + public string Path { get; } + public SaveDataAttribute Attribute { get; } + + public SaveToMigrate(string path, SaveDataAttribute attribute) + { + Path = path; + Attribute = attribute; + } + } + } +}