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>
This commit is contained in:
parent
e0e12b1672
commit
63b24b4af2
22 changed files with 877 additions and 384 deletions
|
@ -48,11 +48,11 @@ namespace Ryujinx
|
|||
|
||||
Application.Init();
|
||||
|
||||
string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "system", "prod.keys");
|
||||
string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Ryujinx", "system", "prod.keys");
|
||||
string userProfilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".switch", "prod.keys");
|
||||
if (!File.Exists(appDataPath) && !File.Exists(userProfilePath))
|
||||
{
|
||||
GtkDialog.CreateErrorDialog($"Key file was not found. Please refer to `KEYS.md` for more info");
|
||||
GtkDialog.CreateErrorDialog("Key file was not found. Please refer to `KEYS.md` for more info");
|
||||
}
|
||||
|
||||
MainWindow mainWindow = new MainWindow();
|
||||
|
|
|
@ -40,21 +40,8 @@ namespace Ryujinx.Ui
|
|||
_discordLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.DiscordLogo.png", 30 , 30 );
|
||||
_twitterLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.TwitterLogo.png", 30 , 30 );
|
||||
|
||||
try
|
||||
{
|
||||
IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase });
|
||||
|
||||
using (Stream stream = File.OpenRead(System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFS", "Installer", "Config", "Config.json")))
|
||||
{
|
||||
AboutInformation = JsonSerializer.Deserialize<AboutInfo>(stream, resolver);
|
||||
}
|
||||
|
||||
_versionText.Text = $"Version {AboutInformation.InstallVersion} - {AboutInformation.InstallBranch} ({AboutInformation.InstallCommit})";
|
||||
}
|
||||
catch
|
||||
{
|
||||
_versionText.Text = "Unknown Version";
|
||||
}
|
||||
// todo: Get version string
|
||||
_versionText.Text = "Unknown Version";
|
||||
}
|
||||
|
||||
private static void OpenUrl(string url)
|
||||
|
|
|
@ -13,5 +13,6 @@
|
|||
public string FileExtension { get; set; }
|
||||
public string FileSize { get; set; }
|
||||
public string Path { get; set; }
|
||||
public string SaveDataPath { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
using JsonPrettyPrinterPlus;
|
||||
using LibHac;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Shim;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.FsSystem.NcaUtils;
|
||||
using LibHac.Ncm;
|
||||
using LibHac.Spl;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.Loaders.Npdm;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
@ -16,6 +19,7 @@ using System.Text;
|
|||
using Utf8Json;
|
||||
using Utf8Json.Resolvers;
|
||||
|
||||
using RightsId = LibHac.Fs.RightsId;
|
||||
using TitleLanguage = Ryujinx.HLE.HOS.SystemState.TitleLanguage;
|
||||
|
||||
namespace Ryujinx.Ui
|
||||
|
@ -34,7 +38,7 @@ namespace Ryujinx.Ui
|
|||
private static TitleLanguage _desiredTitleLanguage;
|
||||
private static ApplicationMetadata _appMetadata;
|
||||
|
||||
public static void LoadApplications(List<string> appDirs, Keyset keySet, TitleLanguage desiredTitleLanguage)
|
||||
public static void LoadApplications(List<string> appDirs, Keyset keySet, TitleLanguage desiredTitleLanguage, FileSystemClient fsClient = null, VirtualFileSystem vfs = null)
|
||||
{
|
||||
int numApplicationsFound = 0;
|
||||
int numApplicationsLoaded = 0;
|
||||
|
@ -127,6 +131,7 @@ namespace Ryujinx.Ui
|
|||
string titleId = "0000000000000000";
|
||||
string developer = "Unknown";
|
||||
string version = "0";
|
||||
string saveDataPath = null;
|
||||
byte[] applicationIcon = null;
|
||||
|
||||
using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read))
|
||||
|
@ -336,6 +341,20 @@ namespace Ryujinx.Ui
|
|||
|
||||
(bool favorite, string timePlayed, string lastPlayed) = GetMetadata(titleId);
|
||||
|
||||
if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNum))
|
||||
{
|
||||
SaveDataFilter filter = new SaveDataFilter();
|
||||
filter.SetUserId(new UserId(1, 0));
|
||||
filter.SetTitleId(new TitleId(titleIdNum));
|
||||
|
||||
Result result = fsClient.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, ref filter);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
saveDataPath = Path.Combine(vfs.GetNandPath(), $"user/save/{saveDataInfo.SaveDataId:x16}");
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationData data = new ApplicationData()
|
||||
{
|
||||
Favorite = favorite,
|
||||
|
@ -349,6 +368,7 @@ namespace Ryujinx.Ui
|
|||
FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1),
|
||||
FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + "MB" : fileSize.ToString("0.##") + "GB",
|
||||
Path = applicationPath,
|
||||
SaveDataPath = saveDataPath
|
||||
};
|
||||
|
||||
numApplicationsLoaded++;
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
using Gtk;
|
||||
using LibHac;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Shim;
|
||||
using LibHac.Ncm;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
|
@ -13,6 +18,7 @@ namespace Ryujinx.Ui
|
|||
{
|
||||
private static ListStore _gameTableStore;
|
||||
private static TreeIter _rowIter;
|
||||
private FileSystemClient _fsClient;
|
||||
|
||||
#pragma warning disable CS0649
|
||||
#pragma warning disable IDE0044
|
||||
|
@ -20,9 +26,10 @@ namespace Ryujinx.Ui
|
|||
#pragma warning restore CS0649
|
||||
#pragma warning restore IDE0044
|
||||
|
||||
public GameTableContextMenu(ListStore gameTableStore, TreeIter rowIter) : this(new Builder("Ryujinx.Ui.GameTableContextMenu.glade"), gameTableStore, rowIter) { }
|
||||
public GameTableContextMenu(ListStore gameTableStore, TreeIter rowIter, FileSystemClient fsClient)
|
||||
: this(new Builder("Ryujinx.Ui.GameTableContextMenu.glade"), gameTableStore, rowIter, fsClient) { }
|
||||
|
||||
private GameTableContextMenu(Builder builder, ListStore gameTableStore, TreeIter rowIter) : base(builder.GetObject("_contextMenu").Handle)
|
||||
private GameTableContextMenu(Builder builder, ListStore gameTableStore, TreeIter rowIter, FileSystemClient fsClient) : base(builder.GetObject("_contextMenu").Handle)
|
||||
{
|
||||
builder.Autoconnect(this);
|
||||
|
||||
|
@ -30,6 +37,7 @@ namespace Ryujinx.Ui
|
|||
|
||||
_gameTableStore = gameTableStore;
|
||||
_rowIter = rowIter;
|
||||
_fsClient = fsClient;
|
||||
}
|
||||
|
||||
//Events
|
||||
|
@ -37,33 +45,14 @@ namespace Ryujinx.Ui
|
|||
{
|
||||
string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0];
|
||||
string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower();
|
||||
string saveDir = System.IO.Path.Combine(new VirtualFileSystem().GetNandPath(), "user", "save", "0000000000000000", "00000000000000000000000000000001", titleId, "0");
|
||||
|
||||
if (!Directory.Exists(saveDir))
|
||||
if (!TryFindSaveData(titleName, titleId, out ulong saveDataId))
|
||||
{
|
||||
MessageDialog messageDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, null)
|
||||
{
|
||||
Title = "Ryujinx",
|
||||
Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"),
|
||||
Text = $"Could not find save directory for {titleName} [{titleId}]",
|
||||
SecondaryText = "Would you like to create the directory?",
|
||||
WindowPosition = WindowPosition.Center
|
||||
};
|
||||
|
||||
if (messageDialog.Run() == (int)ResponseType.Yes)
|
||||
{
|
||||
Directory.CreateDirectory(saveDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
messageDialog.Dispose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
messageDialog.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
string saveDir = GetSaveDataDirectory(saveDataId);
|
||||
|
||||
Process.Start(new ProcessStartInfo()
|
||||
{
|
||||
FileName = saveDir,
|
||||
|
@ -71,5 +60,93 @@ namespace Ryujinx.Ui
|
|||
Verb = "open"
|
||||
});
|
||||
}
|
||||
|
||||
private bool TryFindSaveData(string titleName, string titleIdText, out ulong saveDataId)
|
||||
{
|
||||
saveDataId = default;
|
||||
|
||||
if (!ulong.TryParse(titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleId))
|
||||
{
|
||||
GtkDialog.CreateErrorDialog("UI error: The selected game did not have a valid title ID");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
SaveDataFilter filter = new SaveDataFilter();
|
||||
filter.SetUserId(new UserId(1, 0));
|
||||
filter.SetTitleId(new TitleId(titleId));
|
||||
|
||||
Result result = _fsClient.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, ref filter);
|
||||
|
||||
if (result == ResultFs.TargetNotFound)
|
||||
{
|
||||
// Savedata was not found. Ask the user if they want to create it
|
||||
using MessageDialog messageDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, null)
|
||||
{
|
||||
Title = "Ryujinx",
|
||||
Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"),
|
||||
Text = $"There is no savedata for {titleName} [{titleId:x16}]",
|
||||
SecondaryText = "Would you like to create savedata for this game?",
|
||||
WindowPosition = WindowPosition.Center
|
||||
};
|
||||
|
||||
if (messageDialog.Run() != (int)ResponseType.Yes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = _fsClient.CreateSaveData(new TitleId(titleId), new UserId(1, 0), new TitleId(titleId), 0, 0, 0);
|
||||
|
||||
if (result.IsFailure())
|
||||
{
|
||||
GtkDialog.CreateErrorDialog($"There was an error creating the specified savedata: {result.ToStringWithName()}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to find the savedata again after creating it
|
||||
result = _fsClient.FindSaveDataWithFilter(out saveDataInfo, SaveDataSpaceId.User, ref filter);
|
||||
}
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
saveDataId = saveDataInfo.SaveDataId;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
GtkDialog.CreateErrorDialog($"There was an error finding the specified savedata: {result.ToStringWithName()}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetSaveDataDirectory(ulong saveDataId)
|
||||
{
|
||||
string saveRootPath = System.IO.Path.Combine(new VirtualFileSystem().GetNandPath(), $"user/save/{saveDataId:x16}");
|
||||
|
||||
if (!Directory.Exists(saveRootPath))
|
||||
{
|
||||
// Inconsistent state. Create the directory
|
||||
Directory.CreateDirectory(saveRootPath);
|
||||
}
|
||||
|
||||
string committedPath = System.IO.Path.Combine(saveRootPath, "0");
|
||||
string workingPath = System.IO.Path.Combine(saveRootPath, "1");
|
||||
|
||||
// If the committed directory exists, that path will be loaded the next time the savedata is mounted
|
||||
if (Directory.Exists(committedPath))
|
||||
{
|
||||
return committedPath;
|
||||
}
|
||||
|
||||
// If the working directory exists and the committed directory doesn't,
|
||||
// the working directory will be loaded the next time the savedata is mounted
|
||||
if (!Directory.Exists(workingPath))
|
||||
{
|
||||
Directory.CreateDirectory(workingPath);
|
||||
}
|
||||
|
||||
return workingPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
using Gtk;
|
||||
using JsonPrettyPrinterPlus;
|
||||
using Ryujinx.Audio;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Configuration;
|
||||
using Ryujinx.Graphics.Gal;
|
||||
using Ryujinx.Graphics.Gal.OpenGL;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Profiler;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Ryujinx.Configuration;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Utf8Json;
|
||||
using JsonPrettyPrinterPlus;
|
||||
using Utf8Json.Resolvers;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
|
||||
|
||||
using GUI = Gtk.Builder.ObjectAttribute;
|
||||
|
||||
|
@ -74,6 +73,12 @@ namespace Ryujinx.Ui
|
|||
|
||||
_gameTable.ButtonReleaseEvent += Row_Clicked;
|
||||
|
||||
bool continueWithStartup = Migration.PromptIfMigrationNeededForStartup(this, out bool migrationNeeded);
|
||||
if (!continueWithStartup)
|
||||
{
|
||||
End();
|
||||
}
|
||||
|
||||
_renderer = new OglRenderer();
|
||||
|
||||
_audioOut = InitializeAudioEngine();
|
||||
|
@ -81,6 +86,16 @@ namespace Ryujinx.Ui
|
|||
// TODO: Initialization and dispose of HLE.Switch when starting/stoping emulation.
|
||||
_device = InitializeSwitchInstance();
|
||||
|
||||
if (migrationNeeded)
|
||||
{
|
||||
bool migrationSuccessful = Migration.DoMigrationForStartup(this, _device);
|
||||
|
||||
if (!migrationSuccessful)
|
||||
{
|
||||
End();
|
||||
}
|
||||
}
|
||||
|
||||
_treeView = _gameTable;
|
||||
|
||||
ApplyTheme();
|
||||
|
@ -198,7 +213,9 @@ namespace Ryujinx.Ui
|
|||
|
||||
_tableStore.Clear();
|
||||
|
||||
await Task.Run(() => ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs, _device.System.KeySet, _device.System.State.DesiredTitleLanguage));
|
||||
await Task.Run(() => ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs,
|
||||
_device.System.KeySet, _device.System.State.DesiredTitleLanguage, _device.System.FsClient,
|
||||
_device.FileSystem));
|
||||
|
||||
_updatingGameTable = false;
|
||||
}
|
||||
|
@ -377,8 +394,8 @@ namespace Ryujinx.Ui
|
|||
}
|
||||
|
||||
Profile.FinishProfiling();
|
||||
_device.Dispose();
|
||||
_audioOut.Dispose();
|
||||
_device?.Dispose();
|
||||
_audioOut?.Dispose();
|
||||
Logger.Shutdown();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
@ -474,7 +491,7 @@ namespace Ryujinx.Ui
|
|||
|
||||
if (treeIter.UserData == IntPtr.Zero) return;
|
||||
|
||||
GameTableContextMenu contextMenu = new GameTableContextMenu(_tableStore, treeIter);
|
||||
GameTableContextMenu contextMenu = new GameTableContextMenu(_tableStore, treeIter, _device.System.FsClient);
|
||||
contextMenu.ShowAll();
|
||||
contextMenu.PopupAtPointer(null);
|
||||
}
|
||||
|
|
184
Ryujinx/Ui/Migration.cs
Normal file
184
Ryujinx/Ui/Migration.cs
Normal file
|
@ -0,0 +1,184 @@
|
|||
using Gtk;
|
||||
using LibHac;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
using Switch = Ryujinx.HLE.Switch;
|
||||
|
||||
namespace Ryujinx.Ui
|
||||
{
|
||||
internal class Migration
|
||||
{
|
||||
private Switch Device { get; }
|
||||
|
||||
public Migration(Switch device)
|
||||
{
|
||||
Device = device;
|
||||
}
|
||||
|
||||
public static bool PromptIfMigrationNeededForStartup(Window parentWindow, out bool isMigrationNeeded)
|
||||
{
|
||||
if (!IsMigrationNeeded())
|
||||
{
|
||||
isMigrationNeeded = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
isMigrationNeeded = true;
|
||||
|
||||
int dialogResponse;
|
||||
|
||||
using (MessageDialog dialog = new MessageDialog(parentWindow, DialogFlags.Modal, MessageType.Question,
|
||||
ButtonsType.YesNo, "What's this?"))
|
||||
{
|
||||
dialog.Title = "Data Migration Needed";
|
||||
dialog.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png");
|
||||
dialog.Text =
|
||||
"The folder structure of Ryujinx's RyuFs folder has been updated and renamed to \"Ryujinx\". " +
|
||||
"Your RyuFs folder must be copied and migrated to the new \"Ryujinx\" structure. Would you like to do the migration now?\n\n" +
|
||||
"Select \"Yes\" to automatically perform the migration. Your old RyuFs folder will remain as it is.\n\n" +
|
||||
"Selecting \"No\" will exit Ryujinx without changing anything.";
|
||||
|
||||
dialogResponse = dialog.Run();
|
||||
}
|
||||
|
||||
return dialogResponse == (int)ResponseType.Yes;
|
||||
}
|
||||
|
||||
public static bool DoMigrationForStartup(Window parentWindow, Switch device)
|
||||
{
|
||||
try
|
||||
{
|
||||
Migration migration = new Migration(device);
|
||||
int saveCount = migration.Migrate();
|
||||
|
||||
using MessageDialog dialogSuccess = new MessageDialog(parentWindow, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, null)
|
||||
{
|
||||
Title = "Migration Success",
|
||||
Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"),
|
||||
Text = $"Data migration was successful. {saveCount} saves were migrated.",
|
||||
};
|
||||
|
||||
dialogSuccess.Run();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (HorizonResultException ex)
|
||||
{
|
||||
GtkDialog.CreateErrorDialog(ex.Message);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the number of saves migrated
|
||||
public int Migrate()
|
||||
{
|
||||
string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
|
||||
string oldBasePath = Path.Combine(appDataPath, "RyuFs");
|
||||
string newBasePath = Path.Combine(appDataPath, "Ryujinx");
|
||||
|
||||
string oldSaveDir = Path.Combine(oldBasePath, "nand/user/save");
|
||||
|
||||
CopyRyuFs(oldBasePath, newBasePath);
|
||||
|
||||
SaveImporter importer = new SaveImporter(oldSaveDir, Device.System.FsClient);
|
||||
|
||||
return importer.Import();
|
||||
}
|
||||
|
||||
private static void CopyRyuFs(string oldPath, string newPath)
|
||||
{
|
||||
Directory.CreateDirectory(newPath);
|
||||
|
||||
CopyExcept(oldPath, newPath, "nand", "bis", "sdmc", "sdcard");
|
||||
|
||||
string oldNandPath = Path.Combine(oldPath, "nand");
|
||||
string newNandPath = Path.Combine(newPath, "bis");
|
||||
|
||||
CopyExcept(oldNandPath, newNandPath, "system", "user");
|
||||
|
||||
string oldSdPath = Path.Combine(oldPath, "sdmc");
|
||||
string newSdPath = Path.Combine(newPath, "sdcard");
|
||||
|
||||
CopyDirectory(oldSdPath, newSdPath);
|
||||
|
||||
string oldSystemPath = Path.Combine(oldNandPath, "system");
|
||||
string newSystemPath = Path.Combine(newNandPath, "system");
|
||||
|
||||
CopyExcept(oldSystemPath, newSystemPath, "save");
|
||||
|
||||
string oldUserPath = Path.Combine(oldNandPath, "user");
|
||||
string newUserPath = Path.Combine(newNandPath, "user");
|
||||
|
||||
CopyExcept(oldUserPath, newUserPath, "save");
|
||||
}
|
||||
|
||||
private static void CopyExcept(string srcPath, string dstPath, params string[] exclude)
|
||||
{
|
||||
exclude = exclude.Select(x => x.ToLowerInvariant()).ToArray();
|
||||
|
||||
DirectoryInfo srcDir = new DirectoryInfo(srcPath);
|
||||
|
||||
if (!srcDir.Exists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(dstPath);
|
||||
|
||||
foreach (DirectoryInfo subDir in srcDir.EnumerateDirectories())
|
||||
{
|
||||
if (exclude.Contains(subDir.Name.ToLowerInvariant()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CopyDirectory(subDir.FullName, Path.Combine(dstPath, subDir.Name));
|
||||
}
|
||||
|
||||
foreach (FileInfo file in srcDir.EnumerateFiles())
|
||||
{
|
||||
file.CopyTo(Path.Combine(dstPath, file.Name));
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyDirectory(string srcPath, string dstPath)
|
||||
{
|
||||
Directory.CreateDirectory(dstPath);
|
||||
|
||||
DirectoryInfo srcDir = new DirectoryInfo(srcPath);
|
||||
|
||||
if (!srcDir.Exists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(dstPath);
|
||||
|
||||
foreach (DirectoryInfo subDir in srcDir.EnumerateDirectories())
|
||||
{
|
||||
CopyDirectory(subDir.FullName, Path.Combine(dstPath, subDir.Name));
|
||||
}
|
||||
|
||||
foreach (FileInfo file in srcDir.EnumerateFiles())
|
||||
{
|
||||
file.CopyTo(Path.Combine(dstPath, file.Name));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsMigrationNeeded()
|
||||
{
|
||||
string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
|
||||
string oldBasePath = Path.Combine(appDataPath, "RyuFs");
|
||||
string newBasePath = Path.Combine(appDataPath, "Ryujinx");
|
||||
|
||||
return Directory.Exists(oldBasePath) && !Directory.Exists(newBasePath);
|
||||
}
|
||||
}
|
||||
}
|
218
Ryujinx/Ui/SaveImporter.cs
Normal file
218
Ryujinx/Ui/SaveImporter.cs
Normal file
|
@ -0,0 +1,218 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
using Gtk;
|
||||
using Ryujinx.Configuration;
|
||||
using Ryujinx.Configuration.Hid;
|
||||
using Ryujinx.Configuration.System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Ryujinx.Configuration;
|
||||
using Ryujinx.Configuration.System;
|
||||
using Ryujinx.Configuration.Hid;
|
||||
|
||||
using GUI = Gtk.Builder.ObjectAttribute;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue