R/Ryujinx/Ui/SaveImporter.cs
Alex Barney 63b24b4af2 Rename "RyuFs" directory to "Ryujinx" and use the same savedata system the Switch uses (#801)
* Use savedata FS commands from LibHac

* Add EnsureSaveData. Use ApplicationControlProperty struct

* Add a function to migrate to the new directory layout

* LibHac update

* Change backup structure

* Don't create UI files in the save path

* Update RyuFs paths

* Add GetProgramIndexForAccessLog

Ryujinx only runs one program at a time, so always return values reflecting that

* Load control NCA when loading from an NSP

* Skip over UI stats when exiting

* Set TitleName and TitleId in more cases. Fix TitleID naming style

* Completely comment out GUI play stats code

* rebase

* Update LibHac

* Update LibHac

* Revert UI changes

* Do migration automatically at startup

* Rename RyuFs directory to Ryujinx

* Update RyuFs text

* Store savedata paths in the GUI

* Make "Open Save Directory" work

* Use a dummy NACP in EnsureSaveData if one is not loaded

* Remove manual migration button

* Respond to feedback

* Don't read the installer config to get a version string

* Delete nuget.config

* Exclude 'sdcard' and 'bis' during migration

Co-authored-by: Thog <thog@protonmail.com>
2020-01-05 12:49:44 +01:00

218 lines
6.4 KiB
C#

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.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Ryujinx.Ui
{
internal class SaveImporter
{
private FileSystemClient FsClient { get; }
private string ImportPath { get; }
public SaveImporter(string importPath, FileSystemClient destFsClient)
{
ImportPath = importPath;
FsClient = destFsClient;
}
// Returns the number of saves imported
public int Import()
{
return ImportSaves(FsClient, ImportPath);
}
private static int ImportSaves(FileSystemClient fsClient, string rootSaveDir)
{
if (!Directory.Exists(rootSaveDir))
{
return 0;
}
SaveFinder finder = new SaveFinder();
finder.FindSaves(rootSaveDir);
foreach (SaveToImport save in finder.Saves)
{
Result importResult = ImportSave(fsClient, save);
if (importResult.IsFailure())
{
throw new HorizonResultException(importResult, $"Error importing save {save.Path}");
}
}
return finder.Saves.Count;
}
private static Result ImportSave(FileSystemClient fs, SaveToImport 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<SaveToImport> Saves { get; } = new List<SaveToImport>();
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)
};
SaveToImport save = new SaveToImport(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<UInt128, UserId>(ref id);
return true;
}
catch { }
}
userId = default;
return false;
}
}
private class SaveToImport
{
public string Path { get; }
public SaveDataAttribute Attribute { get; }
public SaveToImport(string path, SaveDataAttribute attribute)
{
Path = path;
Attribute = attribute;
}
}
}
}