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
4
KEYS.md
4
KEYS.md
|
@ -6,7 +6,7 @@ Keys are required for decrypting most of the file formats used by the Nintendo S
|
||||||
* `prod.keys` - Contains common keys used by all Nintendo Switch devices.
|
* `prod.keys` - Contains common keys used by all Nintendo Switch devices.
|
||||||
* `title.keys` - Contains game-specific keys.
|
* `title.keys` - Contains game-specific keys.
|
||||||
|
|
||||||
Ryujinx will first look for keys in `RyuFS/system`, and if it doesn't find any there it will look in `$HOME/.switch`.
|
Ryujinx will first look for keys in `Ryujinx/system`, and if it doesn't find any there it will look in `$HOME/.switch`.
|
||||||
To dump your `prod.keys` and `title.keys` please follow these following steps.
|
To dump your `prod.keys` and `title.keys` please follow these following steps.
|
||||||
1. First off learn how to boot into RCM mode and inject payloads if you haven't already. This can be done [here](https://nh-server.github.io/switch-guide/).
|
1. First off learn how to boot into RCM mode and inject payloads if you haven't already. This can be done [here](https://nh-server.github.io/switch-guide/).
|
||||||
2. Make sure you have an SD card with the latest release of [Atmosphere](https://github.com/Atmosphere-NX/Atmosphere/releases) inserted into your Nintendo Switch.
|
2. Make sure you have an SD card with the latest release of [Atmosphere](https://github.com/Atmosphere-NX/Atmosphere/releases) inserted into your Nintendo Switch.
|
||||||
|
@ -18,7 +18,7 @@ To dump your `prod.keys` and `title.keys` please follow these following steps.
|
||||||
8. After its completion press any button to return to the main menu of Lockpick_RCM.
|
8. After its completion press any button to return to the main menu of Lockpick_RCM.
|
||||||
9. Navigate to and select `Power off` if you have an SD card reader. Or you could Navigate and select `Reboot (RCM)` if you want to mount your SD card using `TegraRCMGUI > Tools > Memloader V3 > MMC - SD Card`.
|
9. Navigate to and select `Power off` if you have an SD card reader. Or you could Navigate and select `Reboot (RCM)` if you want to mount your SD card using `TegraRCMGUI > Tools > Memloader V3 > MMC - SD Card`.
|
||||||
10. You can find your keys in `sd:/switch/prod.keys` and `sd:/switch/title.keys` respectively.
|
10. You can find your keys in `sd:/switch/prod.keys` and `sd:/switch/title.keys` respectively.
|
||||||
11. Copy these files and paste them in `RyuFS/system`.
|
11. Copy these files and paste them in `Ryujinx/system`.
|
||||||
And you're done!
|
And you're done!
|
||||||
|
|
||||||
## Title keys
|
## Title keys
|
||||||
|
|
|
@ -29,7 +29,7 @@ If you build it yourself you will need to:
|
||||||
Run `dotnet run -c Release -- path\to\homebrew.nro` inside the Ryujinx project folder to run homebrew apps.
|
Run `dotnet run -c Release -- path\to\homebrew.nro` inside the Ryujinx project folder to run homebrew apps.
|
||||||
Run `dotnet run -c Release -- path\to\game.nsp/xci` to run official games.
|
Run `dotnet run -c Release -- path\to\game.nsp/xci` to run official games.
|
||||||
|
|
||||||
Every file related to Ryujinx is stored in the `RyuFs` folder. Located in `C:\Users\USERNAME\AppData\Roaming\` for Windows, `/home/USERNAME/.config` for Linux or `/Users/USERNAME/Library/Application Support/` for macOS. It can also be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.
|
Every file related to Ryujinx is stored in the `Ryujinx` folder. Located in `C:\Users\USERNAME\AppData\Roaming\` for Windows, `/home/USERNAME/.config` for Linux or `/Users/USERNAME/Library/Application Support/` for macOS. It can also be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.
|
||||||
|
|
||||||
## Latest build
|
## Latest build
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ The latest automatic build for Windows, macOS, and Linux can be found on the [Of
|
||||||
|
|
||||||
- **System Titles**
|
- **System Titles**
|
||||||
|
|
||||||
Some of our System Module implementations, like `time`, require [System Data Archives](https://switchbrew.org/wiki/Title_list#System_Data_Archives). You can install them by mounting your nand partition using [HacDiskMount](https://switchtools.sshnuke.net/) and copying the content to `RyuFs/nand/system`.
|
Some of our System Module implementations, like `time`, require [System Data Archives](https://switchbrew.org/wiki/Title_list#System_Data_Archives). You can install them by mounting your nand partition using [HacDiskMount](https://switchtools.sshnuke.net/) and copying the content to `Ryujinx/nand/system`.
|
||||||
|
|
||||||
- **Executables**
|
- **Executables**
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ namespace Ryujinx.HLE.FileSystem
|
||||||
{
|
{
|
||||||
public class VirtualFileSystem : IDisposable
|
public class VirtualFileSystem : IDisposable
|
||||||
{
|
{
|
||||||
public const string BasePath = "RyuFs";
|
public const string BasePath = "Ryujinx";
|
||||||
public const string NandPath = "nand";
|
public const string NandPath = "bis";
|
||||||
public const string SdCardPath = "sdmc";
|
public const string SdCardPath = "sdcard";
|
||||||
public const string SystemPath = "system";
|
public const string SystemPath = "system";
|
||||||
|
|
||||||
public static string SafeNandPath = Path.Combine(NandPath, "safe");
|
public static string SafeNandPath = Path.Combine(NandPath, "safe");
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
using LibHac;
|
using LibHac;
|
||||||
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.FsService;
|
using LibHac.FsService;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using LibHac.FsSystem.NcaUtils;
|
using LibHac.FsSystem.NcaUtils;
|
||||||
|
using LibHac.Ns;
|
||||||
using LibHac.Spl;
|
using LibHac.Spl;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem.Content;
|
using Ryujinx.HLE.FileSystem.Content;
|
||||||
|
@ -103,7 +105,7 @@ namespace Ryujinx.HLE.HOS
|
||||||
|
|
||||||
private bool _hasStarted;
|
private bool _hasStarted;
|
||||||
|
|
||||||
public Nacp ControlData { get; set; }
|
public BlitStruct<ApplicationControlProperty> ControlData { get; set; }
|
||||||
|
|
||||||
public string TitleName { get; private set; }
|
public string TitleName { get; private set; }
|
||||||
|
|
||||||
|
@ -116,11 +118,13 @@ namespace Ryujinx.HLE.HOS
|
||||||
internal long HidBaseAddress { get; private set; }
|
internal long HidBaseAddress { get; private set; }
|
||||||
|
|
||||||
internal FileSystemServer FsServer { get; private set; }
|
internal FileSystemServer FsServer { get; private set; }
|
||||||
|
public FileSystemClient FsClient { get; private set; }
|
||||||
|
|
||||||
internal EmulatedGameCard GameCard { get; private set; }
|
internal EmulatedGameCard GameCard { get; private set; }
|
||||||
|
|
||||||
public Horizon(Switch device)
|
public Horizon(Switch device)
|
||||||
{
|
{
|
||||||
ControlData = new Nacp();
|
ControlData = new BlitStruct<ApplicationControlProperty>(1);
|
||||||
|
|
||||||
Device = device;
|
Device = device;
|
||||||
|
|
||||||
|
@ -245,6 +249,7 @@ namespace Ryujinx.HLE.HOS
|
||||||
};
|
};
|
||||||
|
|
||||||
FsServer = new FileSystemServer(fsServerConfig);
|
FsServer = new FileSystemServer(fsServerConfig);
|
||||||
|
FsClient = FsServer.CreateFileSystemClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadCart(string exeFsDir, string romFsFile = null)
|
public void LoadCart(string exeFsDir, string romFsFile = null)
|
||||||
|
@ -350,6 +355,10 @@ namespace Ryujinx.HLE.HOS
|
||||||
{
|
{
|
||||||
ReadControlData(controlNca);
|
ReadControlData(controlNca);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ControlData.ByteSpan.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
return (mainNca, patchNca, controlNca);
|
return (mainNca, patchNca, controlNca);
|
||||||
}
|
}
|
||||||
|
@ -362,9 +371,23 @@ namespace Ryujinx.HLE.HOS
|
||||||
|
|
||||||
if (result.IsSuccess())
|
if (result.IsSuccess())
|
||||||
{
|
{
|
||||||
ControlData = new Nacp(controlFile.AsStream());
|
result = controlFile.Read(out long bytesRead, 0, ControlData.ByteSpan, ReadOption.None);
|
||||||
|
|
||||||
TitleName = ControlData.Descriptions[(int)State.DesiredTitleLanguage].Title;
|
if (result.IsSuccess() && bytesRead == ControlData.ByteSpan.Length)
|
||||||
|
{
|
||||||
|
TitleName = ControlData.Value
|
||||||
|
.Titles[(int) State.DesiredTitleLanguage].Name.ToString();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(TitleName))
|
||||||
|
{
|
||||||
|
TitleName = ControlData.Value.Titles.ToArray()
|
||||||
|
.FirstOrDefault(x => x.Name[0] != 0).Name.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ControlData.ByteSpan.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -489,33 +512,16 @@ namespace Ryujinx.HLE.HOS
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadExeFs(codeFs, out Npdm metaData);
|
LoadExeFs(codeFs, out Npdm metaData);
|
||||||
|
|
||||||
Nacp ReadControlData()
|
TitleId = metaData.Aci0.TitleId.ToString("x16");
|
||||||
{
|
|
||||||
IFileSystem controlRomfs = controlNca.OpenFileSystem(NcaSectionType.Data, FsIntegrityCheckLevel);
|
|
||||||
|
|
||||||
controlRomfs.OpenFile(out IFile controlFile, "/control.nacp", OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
Nacp controlData = new Nacp(controlFile.AsStream());
|
|
||||||
|
|
||||||
TitleName = controlData.Descriptions[(int)State.DesiredTitleLanguage].Title;
|
|
||||||
TitleId = metaData.Aci0.TitleId.ToString("x16");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(TitleName))
|
|
||||||
{
|
|
||||||
TitleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title;
|
|
||||||
}
|
|
||||||
|
|
||||||
return controlData;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controlNca != null)
|
if (controlNca != null)
|
||||||
{
|
{
|
||||||
ReadControlData();
|
ReadControlData(controlNca);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
TitleId = metaData.Aci0.TitleId.ToString("x16");
|
ControlData.ByteSpan.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -613,28 +619,28 @@ namespace Ryujinx.HLE.HOS
|
||||||
if (nacpSize != 0)
|
if (nacpSize != 0)
|
||||||
{
|
{
|
||||||
input.Seek(obj.FileSize + (long)nacpOffset, SeekOrigin.Begin);
|
input.Seek(obj.FileSize + (long)nacpOffset, SeekOrigin.Begin);
|
||||||
using (MemoryStream stream = new MemoryStream(reader.ReadBytes((int)nacpSize)))
|
|
||||||
{
|
|
||||||
ControlData = new Nacp(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
metaData.TitleName = ControlData.Descriptions[(int)State.DesiredTitleLanguage].Title;
|
reader.Read(ControlData.ByteSpan);
|
||||||
|
|
||||||
|
ref ApplicationControlProperty nacp = ref ControlData.Value;
|
||||||
|
|
||||||
|
metaData.TitleName = nacp.Titles[(int)State.DesiredTitleLanguage].Name.ToString();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(metaData.TitleName))
|
if (string.IsNullOrWhiteSpace(metaData.TitleName))
|
||||||
{
|
{
|
||||||
metaData.TitleName = ControlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title;
|
metaData.TitleName = nacp.Titles.ToArray().FirstOrDefault(x => x.Name[0] != 0).Name.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
metaData.Aci0.TitleId = ControlData.PresenceGroupId;
|
metaData.Aci0.TitleId = nacp.PresenceGroupId;
|
||||||
|
|
||||||
if (metaData.Aci0.TitleId == 0)
|
if (metaData.Aci0.TitleId == 0)
|
||||||
{
|
{
|
||||||
metaData.Aci0.TitleId = ControlData.SaveDataOwnerId;
|
metaData.Aci0.TitleId = nacp.SaveDataOwnerId.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metaData.Aci0.TitleId == 0)
|
if (metaData.Aci0.TitleId == 0)
|
||||||
{
|
{
|
||||||
metaData.Aci0.TitleId = ControlData.AddOnContentBaseId - 0x1000;
|
metaData.Aci0.TitleId = nacp.AddOnContentBaseId - 0x1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metaData.Aci0.TitleId.ToString("x16") == "fffffffffffff000")
|
if (metaData.Aci0.TitleId.ToString("x16") == "fffffffffffff000")
|
||||||
|
|
|
@ -287,7 +287,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
|
||||||
// Account actually calls nn::arp::detail::IReader::GetApplicationControlProperty() with the current PID and store the result (NACP File) internally.
|
// Account actually calls nn::arp::detail::IReader::GetApplicationControlProperty() with the current PID and store the result (NACP File) internally.
|
||||||
// But since we use LibHac and we load one Application at a time, it's not necessary.
|
// But since we use LibHac and we load one Application at a time, it's not necessary.
|
||||||
|
|
||||||
context.ResponseData.Write(context.Device.System.ControlData.UserAccountSwitchLock);
|
context.ResponseData.Write(context.Device.System.ControlData.Value.UserAccountSwitchLock);
|
||||||
|
|
||||||
Logger.PrintStub(LogClass.ServiceAcc);
|
Logger.PrintStub(LogClass.ServiceAcc);
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
|
using LibHac;
|
||||||
|
using LibHac.Account;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Ncm;
|
||||||
|
using LibHac.Ns;
|
||||||
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.HOS.Ipc;
|
using Ryujinx.HLE.HOS.Ipc;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Common;
|
using Ryujinx.HLE.HOS.Kernel.Common;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
|
|
||||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage;
|
using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage;
|
||||||
using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService;
|
using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService;
|
||||||
using Ryujinx.HLE.Utilities;
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
|
using static LibHac.Fs.ApplicationSaveDataManagement;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy
|
namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy
|
||||||
{
|
{
|
||||||
class IApplicationFunctions : IpcService
|
class IApplicationFunctions : IpcService
|
||||||
|
@ -24,7 +30,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||||
public ResultCode PopLaunchParameter(ServiceCtx context)
|
public ResultCode PopLaunchParameter(ServiceCtx context)
|
||||||
{
|
{
|
||||||
// Only the first 0x18 bytes of the Data seems to be actually used.
|
// Only the first 0x18 bytes of the Data seems to be actually used.
|
||||||
MakeObject(context, new IStorage(StorageHelper.MakeLaunchParams()));
|
MakeObject(context, new AppletAE.IStorage(StorageHelper.MakeLaunchParams()));
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
@ -33,13 +39,33 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||||
// EnsureSaveData(nn::account::Uid) -> u64
|
// EnsureSaveData(nn::account::Uid) -> u64
|
||||||
public ResultCode EnsureSaveData(ServiceCtx context)
|
public ResultCode EnsureSaveData(ServiceCtx context)
|
||||||
{
|
{
|
||||||
UInt128 userId = new UInt128(context.RequestData.ReadBytes(0x10));
|
Uid userId = context.RequestData.ReadStruct<Uid>();
|
||||||
|
TitleId titleId = new TitleId(context.Process.TitleId);
|
||||||
|
|
||||||
context.ResponseData.Write(0L);
|
BlitStruct<ApplicationControlProperty> controlHolder = context.Device.System.ControlData;
|
||||||
|
|
||||||
Logger.PrintStub(LogClass.ServiceAm, new { userId });
|
ref ApplicationControlProperty control = ref controlHolder.Value;
|
||||||
|
|
||||||
return ResultCode.Success;
|
if (Util.IsEmpty(controlHolder.ByteSpan))
|
||||||
|
{
|
||||||
|
// If the current application doesn't have a loaded control property, create a dummy one
|
||||||
|
// and set the savedata sizes so a user savedata will be created.
|
||||||
|
control = ref new BlitStruct<ApplicationControlProperty>(1).Value;
|
||||||
|
|
||||||
|
// The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
|
||||||
|
control.UserAccountSaveDataSize = 0x4000;
|
||||||
|
control.UserAccountSaveDataJournalSize = 0x4000;
|
||||||
|
|
||||||
|
Logger.PrintWarning(LogClass.ServiceAm,
|
||||||
|
"No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Result result = EnsureApplicationSaveData(context.Device.System.FsClient, out long requiredSize, titleId,
|
||||||
|
ref context.Device.System.ControlData.Value, ref userId);
|
||||||
|
|
||||||
|
context.ResponseData.Write(requiredSize);
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command(21)]
|
[Command(21)]
|
||||||
|
|
|
@ -3,54 +3,12 @@ using LibHac.Fs;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using LibHac.FsSystem.NcaUtils;
|
using LibHac.FsSystem.NcaUtils;
|
||||||
using LibHac.Spl;
|
using LibHac.Spl;
|
||||||
using Ryujinx.Common;
|
|
||||||
using Ryujinx.HLE.FileSystem;
|
|
||||||
using Ryujinx.HLE.Utilities;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
||||||
{
|
{
|
||||||
static class FileSystemProxyHelper
|
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<UInt128>();
|
|
||||||
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)
|
public static ResultCode OpenNsp(ServiceCtx context, string pfsPath, out IFileSystem openedFileSystem)
|
||||||
{
|
{
|
||||||
openedFileSystem = null;
|
openedFileSystem = null;
|
||||||
|
@ -154,5 +112,15 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Result ReadFsPath(out FsPath path, ServiceCtx context, int index = 0)
|
||||||
|
{
|
||||||
|
long position = context.Request.SendBuff[index].Position;
|
||||||
|
long size = context.Request.SendBuff[index].Size;
|
||||||
|
|
||||||
|
byte[] pathBytes = context.Memory.ReadBytes(position, size);
|
||||||
|
|
||||||
|
return FsPath.FromSpan(out path, pathBytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,14 @@ using LibHac.Fs;
|
||||||
using LibHac.FsService;
|
using LibHac.FsService;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using LibHac.FsSystem.NcaUtils;
|
using LibHac.FsSystem.NcaUtils;
|
||||||
|
using LibHac.Ncm;
|
||||||
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
|
||||||
using Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy;
|
using Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
using static Ryujinx.HLE.FileSystem.VirtualFileSystem;
|
|
||||||
using static Ryujinx.HLE.Utilities.StringUtils;
|
using static Ryujinx.HLE.Utilities.StringUtils;
|
||||||
|
using StorageId = Ryujinx.HLE.FileSystem.StorageId;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Fs
|
namespace Ryujinx.HLE.HOS.Services.Fs
|
||||||
{
|
{
|
||||||
|
@ -90,29 +91,13 @@ namespace Ryujinx.HLE.HOS.Services.Fs
|
||||||
// OpenBisFileSystem(nn::fssrv::sf::Partition partitionID, buffer<bytes<0x301>, 0x19, 0x301>) -> object<nn::fssrv::sf::IFileSystem> Bis
|
// OpenBisFileSystem(nn::fssrv::sf::Partition partitionID, buffer<bytes<0x301>, 0x19, 0x301>) -> object<nn::fssrv::sf::IFileSystem> Bis
|
||||||
public ResultCode OpenBisFileSystem(ServiceCtx context)
|
public ResultCode OpenBisFileSystem(ServiceCtx context)
|
||||||
{
|
{
|
||||||
int bisPartitionId = context.RequestData.ReadInt32();
|
BisPartitionId bisPartitionId = (BisPartitionId)context.RequestData.ReadInt32();
|
||||||
string partitionString = ReadUtf8String(context);
|
|
||||||
string bisPartitionPath = string.Empty;
|
|
||||||
|
|
||||||
switch (bisPartitionId)
|
Result rc = FileSystemProxyHelper.ReadFsPath(out FsPath path, context);
|
||||||
{
|
if (rc.IsFailure()) return (ResultCode)rc.Value;
|
||||||
case 29:
|
|
||||||
bisPartitionPath = SafeNandPath;
|
|
||||||
break;
|
|
||||||
case 30:
|
|
||||||
case 31:
|
|
||||||
bisPartitionPath = SystemNandPath;
|
|
||||||
break;
|
|
||||||
case 32:
|
|
||||||
bisPartitionPath = UserNandPath;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return ResultCode.InvalidInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
string fullPath = context.Device.FileSystem.GetFullPartitionPath(bisPartitionPath);
|
rc = _baseFileSystemProxy.OpenBisFileSystem(out LibHac.Fs.IFileSystem fileSystem, ref path, bisPartitionId);
|
||||||
|
if (rc.IsFailure()) return (ResultCode)rc.Value;
|
||||||
LocalFileSystem fileSystem = new LocalFileSystem(fullPath);
|
|
||||||
|
|
||||||
MakeObject(context, new FileSystemProxy.IFileSystem(fileSystem));
|
MakeObject(context, new FileSystemProxy.IFileSystem(fileSystem));
|
||||||
|
|
||||||
|
@ -123,15 +108,69 @@ namespace Ryujinx.HLE.HOS.Services.Fs
|
||||||
// OpenSdCardFileSystem() -> object<nn::fssrv::sf::IFileSystem>
|
// OpenSdCardFileSystem() -> object<nn::fssrv::sf::IFileSystem>
|
||||||
public ResultCode OpenSdCardFileSystem(ServiceCtx context)
|
public ResultCode OpenSdCardFileSystem(ServiceCtx context)
|
||||||
{
|
{
|
||||||
string sdCardPath = context.Device.FileSystem.GetSdCardPath();
|
Result rc = _baseFileSystemProxy.OpenSdCardFileSystem(out LibHac.Fs.IFileSystem fileSystem);
|
||||||
|
if (rc.IsFailure()) return (ResultCode)rc.Value;
|
||||||
LocalFileSystem fileSystem = new LocalFileSystem(sdCardPath);
|
|
||||||
|
|
||||||
MakeObject(context, new FileSystemProxy.IFileSystem(fileSystem));
|
MakeObject(context, new FileSystemProxy.IFileSystem(fileSystem));
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Command(21)]
|
||||||
|
public ResultCode DeleteSaveDataFileSystem(ServiceCtx context)
|
||||||
|
{
|
||||||
|
ulong saveDataId = context.RequestData.ReadUInt64();
|
||||||
|
|
||||||
|
Result result = _baseFileSystemProxy.DeleteSaveDataFileSystem(saveDataId);
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(22)]
|
||||||
|
public ResultCode CreateSaveDataFileSystem(ServiceCtx context)
|
||||||
|
{
|
||||||
|
SaveDataAttribute attribute = context.RequestData.ReadStruct<SaveDataAttribute>();
|
||||||
|
SaveDataCreateInfo createInfo = context.RequestData.ReadStruct<SaveDataCreateInfo>();
|
||||||
|
SaveMetaCreateInfo metaCreateInfo = context.RequestData.ReadStruct<SaveMetaCreateInfo>();
|
||||||
|
|
||||||
|
Result result = _baseFileSystemProxy.CreateSaveDataFileSystem(ref attribute, ref createInfo, ref metaCreateInfo);
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(23)]
|
||||||
|
public ResultCode CreateSaveDataFileSystemBySystemSaveDataId(ServiceCtx context)
|
||||||
|
{
|
||||||
|
SaveDataAttribute attribute = context.RequestData.ReadStruct<SaveDataAttribute>();
|
||||||
|
SaveDataCreateInfo createInfo = context.RequestData.ReadStruct<SaveDataCreateInfo>();
|
||||||
|
|
||||||
|
Result result = _baseFileSystemProxy.CreateSaveDataFileSystemBySystemSaveDataId(ref attribute, ref createInfo);
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(25)]
|
||||||
|
public ResultCode DeleteSaveDataFileSystemBySaveDataSpaceId(ServiceCtx context)
|
||||||
|
{
|
||||||
|
SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64();
|
||||||
|
ulong saveDataId = context.RequestData.ReadUInt64();
|
||||||
|
|
||||||
|
Result result = _baseFileSystemProxy.DeleteSaveDataFileSystemBySaveDataSpaceId(spaceId, saveDataId);
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(28)]
|
||||||
|
public ResultCode DeleteSaveDataFileSystemBySaveDataAttribute(ServiceCtx context)
|
||||||
|
{
|
||||||
|
SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64();
|
||||||
|
SaveDataAttribute attribute = context.RequestData.ReadStruct<SaveDataAttribute>();
|
||||||
|
|
||||||
|
Result result = _baseFileSystemProxy.DeleteSaveDataFileSystemBySaveDataAttribute(spaceId, ref attribute);
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
[Command(30)]
|
[Command(30)]
|
||||||
// OpenGameCardStorage(u32, u32) -> object<nn::fssrv::sf::IStorage>
|
// OpenGameCardStorage(u32, u32) -> object<nn::fssrv::sf::IStorage>
|
||||||
public ResultCode OpenGameCardStorage(ServiceCtx context)
|
public ResultCode OpenGameCardStorage(ServiceCtx context)
|
||||||
|
@ -149,46 +188,141 @@ namespace Ryujinx.HLE.HOS.Services.Fs
|
||||||
return (ResultCode)result.Value;
|
return (ResultCode)result.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Command(35)]
|
||||||
|
public ResultCode CreateSaveDataFileSystemWithHashSalt(ServiceCtx context)
|
||||||
|
{
|
||||||
|
SaveDataAttribute attribute = context.RequestData.ReadStruct<SaveDataAttribute>();
|
||||||
|
SaveDataCreateInfo createInfo = context.RequestData.ReadStruct<SaveDataCreateInfo>();
|
||||||
|
SaveMetaCreateInfo metaCreateInfo = context.RequestData.ReadStruct<SaveMetaCreateInfo>();
|
||||||
|
HashSalt hashSalt = context.RequestData.ReadStruct<HashSalt>();
|
||||||
|
|
||||||
|
Result result = _baseFileSystemProxy.CreateSaveDataFileSystemWithHashSalt(ref attribute, ref createInfo, ref metaCreateInfo, ref hashSalt);
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
[Command(51)]
|
[Command(51)]
|
||||||
// OpenSaveDataFileSystem(u8 save_data_space_id, nn::fssrv::sf::SaveStruct saveStruct) -> object<nn::fssrv::sf::IFileSystem> saveDataFs
|
// OpenSaveDataFileSystem(u8 save_data_space_id, nn::fssrv::sf::SaveStruct saveStruct) -> object<nn::fssrv::sf::IFileSystem> saveDataFs
|
||||||
public ResultCode OpenSaveDataFileSystem(ServiceCtx context)
|
public ResultCode OpenSaveDataFileSystem(ServiceCtx context)
|
||||||
{
|
{
|
||||||
ResultCode result = FileSystemProxyHelper.LoadSaveDataFileSystem(context, false, out FileSystemProxy.IFileSystem fileSystem);
|
SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64();
|
||||||
|
SaveDataAttribute attribute = context.RequestData.ReadStruct<SaveDataAttribute>();
|
||||||
|
|
||||||
if (result == ResultCode.Success)
|
if (attribute.TitleId == TitleId.Zero)
|
||||||
{
|
{
|
||||||
MakeObject(context, fileSystem);
|
attribute.TitleId = new TitleId(context.Process.TitleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
Result result = _baseFileSystemProxy.OpenSaveDataFileSystem(out LibHac.Fs.IFileSystem fileSystem, spaceId, ref attribute);
|
||||||
|
|
||||||
|
if (result.IsSuccess())
|
||||||
|
{
|
||||||
|
MakeObject(context, new FileSystemProxy.IFileSystem(fileSystem));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command(52)]
|
[Command(52)]
|
||||||
// OpenSaveDataFileSystemBySystemSaveDataId(u8 save_data_space_id, nn::fssrv::sf::SaveStruct saveStruct) -> object<nn::fssrv::sf::IFileSystem> systemSaveDataFs
|
// OpenSaveDataFileSystemBySystemSaveDataId(u8 save_data_space_id, nn::fssrv::sf::SaveStruct saveStruct) -> object<nn::fssrv::sf::IFileSystem> systemSaveDataFs
|
||||||
public ResultCode OpenSaveDataFileSystemBySystemSaveDataId(ServiceCtx context)
|
public ResultCode OpenSaveDataFileSystemBySystemSaveDataId(ServiceCtx context)
|
||||||
{
|
{
|
||||||
ResultCode result = FileSystemProxyHelper.LoadSaveDataFileSystem(context, false, out FileSystemProxy.IFileSystem fileSystem);
|
SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64();
|
||||||
|
SaveDataAttribute attribute = context.RequestData.ReadStruct<SaveDataAttribute>();
|
||||||
|
|
||||||
if (result == ResultCode.Success)
|
Result result = _baseFileSystemProxy.OpenSaveDataFileSystemBySystemSaveDataId(out LibHac.Fs.IFileSystem fileSystem, spaceId, ref attribute);
|
||||||
|
|
||||||
|
if (result.IsSuccess())
|
||||||
{
|
{
|
||||||
MakeObject(context, fileSystem);
|
MakeObject(context, new FileSystemProxy.IFileSystem(fileSystem));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return (ResultCode)result.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command(53)]
|
[Command(53)]
|
||||||
// OpenReadOnlySaveDataFileSystem(u8 save_data_space_id, nn::fssrv::sf::SaveStruct save_struct) -> object<nn::fssrv::sf::IFileSystem>
|
// OpenReadOnlySaveDataFileSystem(u8 save_data_space_id, nn::fssrv::sf::SaveStruct save_struct) -> object<nn::fssrv::sf::IFileSystem>
|
||||||
public ResultCode OpenReadOnlySaveDataFileSystem(ServiceCtx context)
|
public ResultCode OpenReadOnlySaveDataFileSystem(ServiceCtx context)
|
||||||
{
|
{
|
||||||
ResultCode result = FileSystemProxyHelper.LoadSaveDataFileSystem(context, true, out FileSystemProxy.IFileSystem fileSystem);
|
SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64();
|
||||||
|
SaveDataAttribute attribute = context.RequestData.ReadStruct<SaveDataAttribute>();
|
||||||
|
|
||||||
if (result == ResultCode.Success)
|
if (attribute.TitleId == TitleId.Zero)
|
||||||
{
|
{
|
||||||
MakeObject(context, fileSystem);
|
attribute.TitleId = new TitleId(context.Process.TitleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
Result result = _baseFileSystemProxy.OpenReadOnlySaveDataFileSystem(out LibHac.Fs.IFileSystem fileSystem, spaceId, ref attribute);
|
||||||
|
|
||||||
|
if (result.IsSuccess())
|
||||||
|
{
|
||||||
|
MakeObject(context, new FileSystemProxy.IFileSystem(fileSystem));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(60)]
|
||||||
|
public ResultCode OpenSaveDataInfoReader(ServiceCtx context)
|
||||||
|
{
|
||||||
|
Result result = _baseFileSystemProxy.OpenSaveDataInfoReader(out LibHac.FsService.ISaveDataInfoReader infoReader);
|
||||||
|
|
||||||
|
if (result.IsSuccess())
|
||||||
|
{
|
||||||
|
MakeObject(context, new ISaveDataInfoReader(infoReader));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(61)]
|
||||||
|
public ResultCode OpenSaveDataInfoReaderBySaveDataSpaceId(ServiceCtx context)
|
||||||
|
{
|
||||||
|
SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadByte();
|
||||||
|
|
||||||
|
Result result = _baseFileSystemProxy.OpenSaveDataInfoReaderBySaveDataSpaceId(out LibHac.FsService.ISaveDataInfoReader infoReader, spaceId);
|
||||||
|
|
||||||
|
if (result.IsSuccess())
|
||||||
|
{
|
||||||
|
MakeObject(context, new ISaveDataInfoReader(infoReader));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(67)]
|
||||||
|
public ResultCode FindSaveDataWithFilter(ServiceCtx context)
|
||||||
|
{
|
||||||
|
SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64();
|
||||||
|
SaveDataFilter filter = context.RequestData.ReadStruct<SaveDataFilter>();
|
||||||
|
|
||||||
|
long bufferPosition = context.Request.ReceiveBuff[0].Position;
|
||||||
|
long bufferLen = context.Request.ReceiveBuff[0].Size;
|
||||||
|
|
||||||
|
byte[] infoBuffer = new byte[bufferLen];
|
||||||
|
|
||||||
|
Result result = _baseFileSystemProxy.FindSaveDataWithFilter(out long count, infoBuffer, spaceId, ref filter);
|
||||||
|
|
||||||
|
context.Memory.WriteBytes(bufferPosition, infoBuffer);
|
||||||
|
context.ResponseData.Write(count);
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(68)]
|
||||||
|
public ResultCode OpenSaveDataInfoReaderWithFilter(ServiceCtx context)
|
||||||
|
{
|
||||||
|
SaveDataSpaceId spaceId = (SaveDataSpaceId)context.RequestData.ReadInt64();
|
||||||
|
SaveDataFilter filter = context.RequestData.ReadStruct<SaveDataFilter>();
|
||||||
|
|
||||||
|
Result result = _baseFileSystemProxy.OpenSaveDataInfoReaderWithFilter(out LibHac.FsService.ISaveDataInfoReader infoReader, spaceId, ref filter);
|
||||||
|
|
||||||
|
if (result.IsSuccess())
|
||||||
|
{
|
||||||
|
MakeObject(context, new ISaveDataInfoReader(infoReader));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command(200)]
|
[Command(200)]
|
||||||
|
@ -306,5 +440,17 @@ namespace Ryujinx.HLE.HOS.Services.Fs
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Command(1011)]
|
||||||
|
public ResultCode GetProgramIndexForAccessLog(ServiceCtx context)
|
||||||
|
{
|
||||||
|
int programIndex = 0;
|
||||||
|
int programCount = 1;
|
||||||
|
|
||||||
|
context.ResponseData.Write(programIndex);
|
||||||
|
context.ResponseData.Write(programCount);
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
31
Ryujinx.HLE/HOS/Services/Fs/ISaveDataInfoReader.cs
Normal file
31
Ryujinx.HLE/HOS/Services/Fs/ISaveDataInfoReader.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
using LibHac;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Fs
|
||||||
|
{
|
||||||
|
class ISaveDataInfoReader : IpcService
|
||||||
|
{
|
||||||
|
private LibHac.FsService.ISaveDataInfoReader _baseReader;
|
||||||
|
|
||||||
|
public ISaveDataInfoReader(LibHac.FsService.ISaveDataInfoReader baseReader)
|
||||||
|
{
|
||||||
|
_baseReader = baseReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(0)]
|
||||||
|
// ReadSaveDataInfo() -> (u64, buffer<unknown, 6>)
|
||||||
|
public ResultCode ReadSaveDataInfo(ServiceCtx context)
|
||||||
|
{
|
||||||
|
long bufferPosition = context.Request.ReceiveBuff[0].Position;
|
||||||
|
long bufferLen = context.Request.ReceiveBuff[0].Size;
|
||||||
|
|
||||||
|
byte[] infoBuffer = new byte[bufferLen];
|
||||||
|
|
||||||
|
Result result = _baseReader.ReadSaveDataInfo(out long readCount, infoBuffer);
|
||||||
|
|
||||||
|
context.Memory.WriteBytes(bufferPosition, infoBuffer);
|
||||||
|
context.ResponseData.Write(readCount);
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,4 @@
|
||||||
using LibHac;
|
namespace Ryujinx.HLE.HOS.Services.Ns
|
||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ns
|
|
||||||
{
|
{
|
||||||
[Service("ns:am")]
|
[Service("ns:am")]
|
||||||
class IApplicationManagerInterface : IpcService
|
class IApplicationManagerInterface : IpcService
|
||||||
|
@ -10,201 +6,17 @@ namespace Ryujinx.HLE.HOS.Services.Ns
|
||||||
public IApplicationManagerInterface(ServiceCtx context) { }
|
public IApplicationManagerInterface(ServiceCtx context) { }
|
||||||
|
|
||||||
[Command(400)]
|
[Command(400)]
|
||||||
// GetApplicationControlData(unknown<0x10>) -> (unknown<4>, buffer<unknown, 6>)
|
// GetApplicationControlData(u8, u64) -> (unknown<4>, buffer<unknown, 6>)
|
||||||
public ResultCode GetApplicationControlData(ServiceCtx context)
|
public ResultCode GetApplicationControlData(ServiceCtx context)
|
||||||
{
|
{
|
||||||
|
byte source = (byte)context.RequestData.ReadInt64();
|
||||||
|
ulong titleId = (byte)context.RequestData.ReadUInt64();
|
||||||
|
|
||||||
long position = context.Request.ReceiveBuff[0].Position;
|
long position = context.Request.ReceiveBuff[0].Position;
|
||||||
|
|
||||||
Nacp nacp = context.Device.System.ControlData;
|
byte[] nacpData = context.Device.System.ControlData.ByteSpan.ToArray();
|
||||||
|
|
||||||
for (int i = 0; i < 0x10; i++)
|
context.Memory.WriteBytes(position, nacpData);
|
||||||
{
|
|
||||||
NacpDescription description = nacp.Descriptions[i];
|
|
||||||
|
|
||||||
byte[] titleData = new byte[0x200];
|
|
||||||
byte[] developerData = new byte[0x100];
|
|
||||||
|
|
||||||
if (description !=null && description.Title != null)
|
|
||||||
{
|
|
||||||
byte[] titleDescriptionData = Encoding.ASCII.GetBytes(description.Title);
|
|
||||||
Buffer.BlockCopy(titleDescriptionData, 0, titleData, 0, titleDescriptionData.Length);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (description != null && description.Developer != null)
|
|
||||||
{
|
|
||||||
byte[] developerDescriptionData = Encoding.ASCII.GetBytes(description.Developer);
|
|
||||||
Buffer.BlockCopy(developerDescriptionData, 0, developerData, 0, developerDescriptionData.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Memory.WriteBytes(position, titleData);
|
|
||||||
context.Memory.WriteBytes(position + 0x200, developerData);
|
|
||||||
|
|
||||||
position += i * 0x300;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] isbn = new byte[0x25];
|
|
||||||
|
|
||||||
if (nacp.Isbn != null)
|
|
||||||
{
|
|
||||||
byte[] isbnData = Encoding.ASCII.GetBytes(nacp.Isbn);
|
|
||||||
Buffer.BlockCopy(isbnData, 0, isbn, 0, isbnData.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Memory.WriteBytes(position, isbn);
|
|
||||||
position += isbn.Length;
|
|
||||||
|
|
||||||
context.Memory.WriteByte(position++, nacp.StartupUserAccount);
|
|
||||||
context.Memory.WriteByte(position++, nacp.UserAccountSwitchLock);
|
|
||||||
context.Memory.WriteByte(position++, nacp.AocRegistrationType);
|
|
||||||
|
|
||||||
context.Memory.WriteInt32(position, nacp.AttributeFlag);
|
|
||||||
position += 4;
|
|
||||||
|
|
||||||
context.Memory.WriteUInt32(position, nacp.SupportedLanguageFlag);
|
|
||||||
position += 4;
|
|
||||||
|
|
||||||
context.Memory.WriteUInt32(position, nacp.ParentalControlFlag);
|
|
||||||
position += 4;
|
|
||||||
|
|
||||||
context.Memory.WriteByte(position++, nacp.Screenshot);
|
|
||||||
context.Memory.WriteByte(position++, nacp.VideoCapture);
|
|
||||||
context.Memory.WriteByte(position++, nacp.DataLossConfirmation);
|
|
||||||
context.Memory.WriteByte(position++, nacp.PlayLogPolicy);
|
|
||||||
|
|
||||||
context.Memory.WriteUInt64(position, nacp.PresenceGroupId);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
for (int i = 0; i < nacp.RatingAge.Length; i++)
|
|
||||||
{
|
|
||||||
context.Memory.WriteSByte(position++, nacp.RatingAge[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] displayVersion = new byte[0x10];
|
|
||||||
|
|
||||||
if (nacp.DisplayVersion != null)
|
|
||||||
{
|
|
||||||
byte[] displayVersionData = Encoding.ASCII.GetBytes(nacp.DisplayVersion);
|
|
||||||
Buffer.BlockCopy(displayVersionData, 0, displayVersion, 0, displayVersionData.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Memory.WriteBytes(position, displayVersion);
|
|
||||||
position += displayVersion.Length;
|
|
||||||
|
|
||||||
context.Memory.WriteUInt64(position, nacp.AddOnContentBaseId);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteUInt64(position, nacp.SaveDataOwnerId);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.UserAccountSaveDataSize);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.UserAccountSaveDataJournalSize);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.DeviceSaveDataSize);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.DeviceSaveDataJournalSize);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.BcatDeliveryCacheStorageSize);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
byte[] applicationErrorCodeCategory = new byte[0x8];
|
|
||||||
|
|
||||||
if (nacp.ApplicationErrorCodeCategory != null)
|
|
||||||
{
|
|
||||||
byte[] applicationErrorCodeCategoryData = Encoding.ASCII.GetBytes(nacp.ApplicationErrorCodeCategory);
|
|
||||||
Buffer.BlockCopy(applicationErrorCodeCategoryData, 0, applicationErrorCodeCategoryData, 0, applicationErrorCodeCategoryData.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Memory.WriteBytes(position, applicationErrorCodeCategory);
|
|
||||||
position += applicationErrorCodeCategory.Length;
|
|
||||||
|
|
||||||
for (int i = 0; i < nacp.LocalCommunicationId.Length; i++)
|
|
||||||
{
|
|
||||||
context.Memory.WriteUInt64(position, nacp.LocalCommunicationId[i]);
|
|
||||||
position += 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Memory.WriteByte(position++, nacp.LogoType);
|
|
||||||
context.Memory.WriteByte(position++, nacp.LogoHandling);
|
|
||||||
context.Memory.WriteByte(position++, nacp.RuntimeAddOnContentInstall);
|
|
||||||
|
|
||||||
byte[] reserved000 = new byte[0x3];
|
|
||||||
context.Memory.WriteBytes(position, reserved000);
|
|
||||||
position += reserved000.Length;
|
|
||||||
|
|
||||||
context.Memory.WriteByte(position++, nacp.CrashReport);
|
|
||||||
context.Memory.WriteByte(position++, nacp.Hdcp);
|
|
||||||
context.Memory.WriteUInt64(position, nacp.SeedForPseudoDeviceId);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
byte[] bcatPassphrase = new byte[65];
|
|
||||||
if (nacp.BcatPassphrase != null)
|
|
||||||
{
|
|
||||||
byte[] bcatPassphraseData = Encoding.ASCII.GetBytes(nacp.BcatPassphrase);
|
|
||||||
Buffer.BlockCopy(bcatPassphraseData, 0, bcatPassphrase, 0, bcatPassphraseData.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Memory.WriteBytes(position, bcatPassphrase);
|
|
||||||
position += bcatPassphrase.Length;
|
|
||||||
|
|
||||||
context.Memory.WriteByte(position++, nacp.Reserved01);
|
|
||||||
|
|
||||||
byte[] reserved02 = new byte[0x6];
|
|
||||||
context.Memory.WriteBytes(position, reserved02);
|
|
||||||
position += reserved02.Length;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.UserAccountSaveDataSizeMax);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.UserAccountSaveDataJournalSizeMax);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.DeviceSaveDataSizeMax);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.DeviceSaveDataJournalSizeMax);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.TemporaryStorageSize);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.CacheStorageSize);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.CacheStorageJournalSize);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt64(position, nacp.CacheStorageDataAndJournalSizeMax);
|
|
||||||
position += 8;
|
|
||||||
|
|
||||||
context.Memory.WriteInt16(position, nacp.CacheStorageIndex);
|
|
||||||
position += 2;
|
|
||||||
|
|
||||||
byte[] reserved03 = new byte[0x6];
|
|
||||||
context.Memory.WriteBytes(position, reserved03);
|
|
||||||
position += reserved03.Length;
|
|
||||||
|
|
||||||
for (int i = 0; i < 16; i++)
|
|
||||||
{
|
|
||||||
ulong value = 0;
|
|
||||||
|
|
||||||
if (nacp.PlayLogQueryableApplicationId.Count > i)
|
|
||||||
{
|
|
||||||
value = nacp.PlayLogQueryableApplicationId[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Memory.WriteUInt64(position, value);
|
|
||||||
position += 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Memory.WriteByte(position++, nacp.PlayLogQueryCapability);
|
|
||||||
context.Memory.WriteByte(position++, nacp.RepairFlag);
|
|
||||||
context.Memory.WriteByte(position++, nacp.ProgramIndex);
|
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)context.Device.System.ControlData.PlayLogQueryCapability;
|
PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)context.Device.System.ControlData.Value.PlayLogQueryCapability;
|
||||||
|
|
||||||
List<ulong> titleIds = new List<ulong>();
|
List<ulong> titleIds = new List<ulong>();
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
|
||||||
// Check if input title ids are in the whitelist.
|
// Check if input title ids are in the whitelist.
|
||||||
foreach (ulong titleId in titleIds)
|
foreach (ulong titleId in titleIds)
|
||||||
{
|
{
|
||||||
if (!context.Device.System.ControlData.PlayLogQueryableApplicationId.Contains(titleId))
|
if (!context.Device.System.ControlData.Value.PlayLogQueryableApplicationId.Contains(titleId))
|
||||||
{
|
{
|
||||||
return (ResultCode)Am.ResultCode.ObjectInvalid;
|
return (ResultCode)Am.ResultCode.ObjectInvalid;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Concentus" Version="1.1.7" />
|
<PackageReference Include="Concentus" Version="1.1.7" />
|
||||||
<PackageReference Include="LibHac" Version="0.6.0" />
|
<PackageReference Include="LibHac" Version="0.7.0" />
|
||||||
<PackageReference Include="TimeZoneConverter.Posix" Version="2.1.0" />
|
<PackageReference Include="TimeZoneConverter.Posix" Version="2.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ namespace Ryujinx.HLE
|
||||||
|
|
||||||
internal NvGpu Gpu { get; private set; }
|
internal NvGpu Gpu { get; private set; }
|
||||||
|
|
||||||
internal VirtualFileSystem FileSystem { get; private set; }
|
public VirtualFileSystem FileSystem { get; private set; }
|
||||||
|
|
||||||
public Horizon System { get; private set; }
|
public Horizon System { get; private set; }
|
||||||
|
|
||||||
|
|
|
@ -48,11 +48,11 @@ namespace Ryujinx
|
||||||
|
|
||||||
Application.Init();
|
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");
|
string userProfilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".switch", "prod.keys");
|
||||||
if (!File.Exists(appDataPath) && !File.Exists(userProfilePath))
|
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();
|
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 );
|
_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 );
|
_twitterLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.TwitterLogo.png", 30 , 30 );
|
||||||
|
|
||||||
try
|
// todo: Get version string
|
||||||
{
|
_versionText.Text = "Unknown Version";
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OpenUrl(string url)
|
private static void OpenUrl(string url)
|
||||||
|
|
|
@ -13,5 +13,6 @@
|
||||||
public string FileExtension { get; set; }
|
public string FileExtension { get; set; }
|
||||||
public string FileSize { get; set; }
|
public string FileSize { get; set; }
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
|
public string SaveDataPath { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
using JsonPrettyPrinterPlus;
|
using JsonPrettyPrinterPlus;
|
||||||
using LibHac;
|
using LibHac;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Shim;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using LibHac.FsSystem.NcaUtils;
|
using LibHac.FsSystem.NcaUtils;
|
||||||
|
using LibHac.Ncm;
|
||||||
using LibHac.Spl;
|
using LibHac.Spl;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.Loaders.Npdm;
|
using Ryujinx.HLE.Loaders.Npdm;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
@ -16,6 +19,7 @@ using System.Text;
|
||||||
using Utf8Json;
|
using Utf8Json;
|
||||||
using Utf8Json.Resolvers;
|
using Utf8Json.Resolvers;
|
||||||
|
|
||||||
|
using RightsId = LibHac.Fs.RightsId;
|
||||||
using TitleLanguage = Ryujinx.HLE.HOS.SystemState.TitleLanguage;
|
using TitleLanguage = Ryujinx.HLE.HOS.SystemState.TitleLanguage;
|
||||||
|
|
||||||
namespace Ryujinx.Ui
|
namespace Ryujinx.Ui
|
||||||
|
@ -34,7 +38,7 @@ namespace Ryujinx.Ui
|
||||||
private static TitleLanguage _desiredTitleLanguage;
|
private static TitleLanguage _desiredTitleLanguage;
|
||||||
private static ApplicationMetadata _appMetadata;
|
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 numApplicationsFound = 0;
|
||||||
int numApplicationsLoaded = 0;
|
int numApplicationsLoaded = 0;
|
||||||
|
@ -127,6 +131,7 @@ namespace Ryujinx.Ui
|
||||||
string titleId = "0000000000000000";
|
string titleId = "0000000000000000";
|
||||||
string developer = "Unknown";
|
string developer = "Unknown";
|
||||||
string version = "0";
|
string version = "0";
|
||||||
|
string saveDataPath = null;
|
||||||
byte[] applicationIcon = null;
|
byte[] applicationIcon = null;
|
||||||
|
|
||||||
using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read))
|
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);
|
(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()
|
ApplicationData data = new ApplicationData()
|
||||||
{
|
{
|
||||||
Favorite = favorite,
|
Favorite = favorite,
|
||||||
|
@ -349,6 +368,7 @@ namespace Ryujinx.Ui
|
||||||
FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1),
|
FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1),
|
||||||
FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + "MB" : fileSize.ToString("0.##") + "GB",
|
FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + "MB" : fileSize.ToString("0.##") + "GB",
|
||||||
Path = applicationPath,
|
Path = applicationPath,
|
||||||
|
SaveDataPath = saveDataPath
|
||||||
};
|
};
|
||||||
|
|
||||||
numApplicationsLoaded++;
|
numApplicationsLoaded++;
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
using Gtk;
|
using Gtk;
|
||||||
|
using LibHac;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Shim;
|
||||||
|
using LibHac.Ncm;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
|
@ -13,6 +18,7 @@ namespace Ryujinx.Ui
|
||||||
{
|
{
|
||||||
private static ListStore _gameTableStore;
|
private static ListStore _gameTableStore;
|
||||||
private static TreeIter _rowIter;
|
private static TreeIter _rowIter;
|
||||||
|
private FileSystemClient _fsClient;
|
||||||
|
|
||||||
#pragma warning disable CS0649
|
#pragma warning disable CS0649
|
||||||
#pragma warning disable IDE0044
|
#pragma warning disable IDE0044
|
||||||
|
@ -20,9 +26,10 @@ namespace Ryujinx.Ui
|
||||||
#pragma warning restore CS0649
|
#pragma warning restore CS0649
|
||||||
#pragma warning restore IDE0044
|
#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);
|
builder.Autoconnect(this);
|
||||||
|
|
||||||
|
@ -30,6 +37,7 @@ namespace Ryujinx.Ui
|
||||||
|
|
||||||
_gameTableStore = gameTableStore;
|
_gameTableStore = gameTableStore;
|
||||||
_rowIter = rowIter;
|
_rowIter = rowIter;
|
||||||
|
_fsClient = fsClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Events
|
//Events
|
||||||
|
@ -37,33 +45,14 @@ namespace Ryujinx.Ui
|
||||||
{
|
{
|
||||||
string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0];
|
string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0];
|
||||||
string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower();
|
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)
|
return;
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string saveDir = GetSaveDataDirectory(saveDataId);
|
||||||
|
|
||||||
Process.Start(new ProcessStartInfo()
|
Process.Start(new ProcessStartInfo()
|
||||||
{
|
{
|
||||||
FileName = saveDir,
|
FileName = saveDir,
|
||||||
|
@ -71,5 +60,93 @@ namespace Ryujinx.Ui
|
||||||
Verb = "open"
|
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 Gtk;
|
||||||
|
using JsonPrettyPrinterPlus;
|
||||||
using Ryujinx.Audio;
|
using Ryujinx.Audio;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Configuration;
|
||||||
using Ryujinx.Graphics.Gal;
|
using Ryujinx.Graphics.Gal;
|
||||||
using Ryujinx.Graphics.Gal.OpenGL;
|
using Ryujinx.Graphics.Gal.OpenGL;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.Profiler;
|
using Ryujinx.Profiler;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Ryujinx.Configuration;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Utf8Json;
|
using Utf8Json;
|
||||||
using JsonPrettyPrinterPlus;
|
|
||||||
using Utf8Json.Resolvers;
|
using Utf8Json.Resolvers;
|
||||||
using Ryujinx.HLE.FileSystem;
|
|
||||||
|
|
||||||
|
|
||||||
using GUI = Gtk.Builder.ObjectAttribute;
|
using GUI = Gtk.Builder.ObjectAttribute;
|
||||||
|
|
||||||
|
@ -74,6 +73,12 @@ namespace Ryujinx.Ui
|
||||||
|
|
||||||
_gameTable.ButtonReleaseEvent += Row_Clicked;
|
_gameTable.ButtonReleaseEvent += Row_Clicked;
|
||||||
|
|
||||||
|
bool continueWithStartup = Migration.PromptIfMigrationNeededForStartup(this, out bool migrationNeeded);
|
||||||
|
if (!continueWithStartup)
|
||||||
|
{
|
||||||
|
End();
|
||||||
|
}
|
||||||
|
|
||||||
_renderer = new OglRenderer();
|
_renderer = new OglRenderer();
|
||||||
|
|
||||||
_audioOut = InitializeAudioEngine();
|
_audioOut = InitializeAudioEngine();
|
||||||
|
@ -81,6 +86,16 @@ namespace Ryujinx.Ui
|
||||||
// TODO: Initialization and dispose of HLE.Switch when starting/stoping emulation.
|
// TODO: Initialization and dispose of HLE.Switch when starting/stoping emulation.
|
||||||
_device = InitializeSwitchInstance();
|
_device = InitializeSwitchInstance();
|
||||||
|
|
||||||
|
if (migrationNeeded)
|
||||||
|
{
|
||||||
|
bool migrationSuccessful = Migration.DoMigrationForStartup(this, _device);
|
||||||
|
|
||||||
|
if (!migrationSuccessful)
|
||||||
|
{
|
||||||
|
End();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_treeView = _gameTable;
|
_treeView = _gameTable;
|
||||||
|
|
||||||
ApplyTheme();
|
ApplyTheme();
|
||||||
|
@ -198,7 +213,9 @@ namespace Ryujinx.Ui
|
||||||
|
|
||||||
_tableStore.Clear();
|
_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;
|
_updatingGameTable = false;
|
||||||
}
|
}
|
||||||
|
@ -377,8 +394,8 @@ namespace Ryujinx.Ui
|
||||||
}
|
}
|
||||||
|
|
||||||
Profile.FinishProfiling();
|
Profile.FinishProfiling();
|
||||||
_device.Dispose();
|
_device?.Dispose();
|
||||||
_audioOut.Dispose();
|
_audioOut?.Dispose();
|
||||||
Logger.Shutdown();
|
Logger.Shutdown();
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
|
@ -474,7 +491,7 @@ namespace Ryujinx.Ui
|
||||||
|
|
||||||
if (treeIter.UserData == IntPtr.Zero) return;
|
if (treeIter.UserData == IntPtr.Zero) return;
|
||||||
|
|
||||||
GameTableContextMenu contextMenu = new GameTableContextMenu(_tableStore, treeIter);
|
GameTableContextMenu contextMenu = new GameTableContextMenu(_tableStore, treeIter, _device.System.FsClient);
|
||||||
contextMenu.ShowAll();
|
contextMenu.ShowAll();
|
||||||
contextMenu.PopupAtPointer(null);
|
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 Gtk;
|
||||||
|
using Ryujinx.Configuration;
|
||||||
|
using Ryujinx.Configuration.Hid;
|
||||||
|
using Ryujinx.Configuration.System;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Ryujinx.Configuration;
|
|
||||||
using Ryujinx.Configuration.System;
|
|
||||||
using Ryujinx.Configuration.Hid;
|
|
||||||
|
|
||||||
using GUI = Gtk.Builder.ObjectAttribute;
|
using GUI = Gtk.Builder.ObjectAttribute;
|
||||||
|
|
||||||
|
|
Reference in a new issue