Update to LibHac 0.13.1 (#2328)
Update the LibHac dependency to version 0.13.1. This brings a ton of improvements and changes such as: - Refactor `FsSrv` to match the official refactoring done in FS. - Change how the `Horizon` and `HorizonClient` classes are handled. Each client created represents a different process with its own process ID and client state. - Add FS access control to handle permissions for FS service method calls. - Add FS program registry to keep track of the program ID, location and permissions of each process. - Add FS program index map info manager to track the program IDs and indexes of multi-application programs. - Add all FS IPC interfaces. - Rewrite `Fs.Fsa` code to be more accurate. - Rewrite a lot of `FsSrv` code to be more accurate. - Extend directory save data to store `SaveDataExtraData` - Extend directory save data to lock the save directory to allow only one accessor at a time. - Improve waiting and retrying when encountering access issues in `LocalFileSystem` and `DirectorySaveDataFileSystem`. - More `IFileSystemProxy` methods should work now. - Probably a bunch more stuff. On the Ryujinx side: - Forward most `IFileSystemProxy` methods to LibHac. - Register programs and program index map info when launching an application. - Remove hacks and workarounds for missing LibHac functionality. - Recreate missing save data extra data found on emulator startup. - Create system save data that wasn't indexed correctly on an older LibHac version. `FsSrv` now enforces access control for each process. When a process tries to open a save data file system, FS reads the save's extra data to determine who the save owner is and if the caller has permission to open the save data. Previously-created save data did not have extra data created when the save was created. With access control checks in place, this means that processes with no permissions (most games) wouldn't be able to access their own save data. The extra data can be partially created from data in the save data indexer, which should be enough for access control purposes.
This commit is contained in:
parent
04dce402ac
commit
19afb3209c
31 changed files with 1795 additions and 574 deletions
|
@ -653,11 +653,11 @@ namespace Ryujinx.HLE.FileSystem.Content
|
|||
|
||||
public SystemVersion VerifyFirmwarePackage(string firmwarePackage)
|
||||
{
|
||||
_virtualFileSystem.Reload();
|
||||
_virtualFileSystem.ReloadKeySet();
|
||||
|
||||
// LibHac.NcaHeader's DecryptHeader doesn't check if HeaderKey is empty and throws InvalidDataException instead
|
||||
// So, we check it early for a better user experience.
|
||||
if (_virtualFileSystem.KeySet.HeaderKey.IsEmpty())
|
||||
if (_virtualFileSystem.KeySet.HeaderKey.IsZeros())
|
||||
{
|
||||
throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers.");
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.HLE.FileSystem
|
||||
{
|
||||
static class SaveHelper
|
||||
{
|
||||
public static IFileSystem OpenSystemSaveData(ServiceCtx context, ulong saveId)
|
||||
{
|
||||
SaveInfo saveInfo = new SaveInfo(0, (long)saveId, SaveDataType.SystemSaveData, SaveSpaceId.NandSystem);
|
||||
string savePath = context.Device.FileSystem.GetSavePath(context, saveInfo, false);
|
||||
|
||||
if (File.Exists(savePath))
|
||||
{
|
||||
string tempDirectoryPath = $"{savePath}_temp";
|
||||
|
||||
Directory.CreateDirectory(tempDirectoryPath);
|
||||
|
||||
IFileSystem outputFolder = new LocalFileSystem(tempDirectoryPath);
|
||||
|
||||
using (LocalStorage systemSaveData = new LocalStorage(savePath, FileAccess.Read, FileMode.Open))
|
||||
{
|
||||
IFileSystem saveFs = new LibHac.FsSystem.Save.SaveDataFileSystem(context.Device.System.KeySet, systemSaveData, IntegrityCheckLevel.None, false);
|
||||
|
||||
saveFs.CopyDirectory(outputFolder, "/", "/");
|
||||
}
|
||||
|
||||
File.Delete(savePath);
|
||||
|
||||
Directory.Move(tempDirectoryPath, savePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Directory.Exists(savePath))
|
||||
{
|
||||
Directory.CreateDirectory(savePath);
|
||||
}
|
||||
}
|
||||
|
||||
return new LocalFileSystem(savePath);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,23 @@
|
|||
using LibHac;
|
||||
using LibHac.Common;
|
||||
using LibHac.Common.Keys;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.Fs.Shim;
|
||||
using LibHac.FsSrv;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Ncm;
|
||||
using LibHac.Spl;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem.Content;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using System;
|
||||
using System.Buffers.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using RightsId = LibHac.Fs.RightsId;
|
||||
|
||||
namespace Ryujinx.HLE.FileSystem
|
||||
{
|
||||
|
@ -24,17 +32,15 @@ namespace Ryujinx.HLE.FileSystem
|
|||
|
||||
private static bool _isInitialized = false;
|
||||
|
||||
public Keyset KeySet { get; private set; }
|
||||
public FileSystemServer FsServer { get; private set; }
|
||||
public FileSystemClient FsClient { get; private set; }
|
||||
public KeySet KeySet { get; private set; }
|
||||
public EmulatedGameCard GameCard { get; private set; }
|
||||
public EmulatedSdCard SdCard { get; private set; }
|
||||
|
||||
public ModLoader ModLoader {get; private set;}
|
||||
public ModLoader ModLoader { get; private set; }
|
||||
|
||||
private VirtualFileSystem()
|
||||
{
|
||||
Reload();
|
||||
ReloadKeySet();
|
||||
ModLoader = new ModLoader(); // Should only be created once
|
||||
}
|
||||
|
||||
|
@ -80,39 +86,6 @@ namespace Ryujinx.HLE.FileSystem
|
|||
internal string GetSdCardPath() => MakeFullPath(SdCardPath);
|
||||
public string GetNandPath() => MakeFullPath(NandPath);
|
||||
|
||||
internal string GetSavePath(ServiceCtx context, SaveInfo saveInfo, bool isDirectory = true)
|
||||
{
|
||||
string saveUserPath = "";
|
||||
string baseSavePath = NandPath;
|
||||
ulong currentTitleId = saveInfo.TitleId;
|
||||
|
||||
switch (saveInfo.SaveSpaceId)
|
||||
{
|
||||
case SaveSpaceId.NandUser: baseSavePath = UserNandPath; break;
|
||||
case SaveSpaceId.NandSystem: baseSavePath = SystemNandPath; break;
|
||||
case SaveSpaceId.SdCard: baseSavePath = Path.Combine(SdCardPath, "Nintendo"); break;
|
||||
}
|
||||
|
||||
baseSavePath = Path.Combine(baseSavePath, "save");
|
||||
|
||||
if (saveInfo.TitleId == 0 && saveInfo.SaveDataType == SaveDataType.SaveData)
|
||||
{
|
||||
currentTitleId = context.Process.TitleId;
|
||||
}
|
||||
|
||||
if (saveInfo.SaveSpaceId == SaveSpaceId.NandUser)
|
||||
{
|
||||
saveUserPath = saveInfo.UserId.IsNull ? "savecommon" : saveInfo.UserId.ToString();
|
||||
}
|
||||
|
||||
string savePath = Path.Combine(baseSavePath,
|
||||
saveInfo.SaveId.ToString("x16"),
|
||||
saveUserPath,
|
||||
saveInfo.SaveDataType == SaveDataType.SaveData ? currentTitleId.ToString("x16") : string.Empty);
|
||||
|
||||
return MakeFullPath(savePath, isDirectory);
|
||||
}
|
||||
|
||||
public string GetFullPartitionPath(string partitionPath)
|
||||
{
|
||||
return MakeFullPath(partitionPath);
|
||||
|
@ -136,8 +109,8 @@ namespace Ryujinx.HLE.FileSystem
|
|||
|
||||
if (systemPath.StartsWith(baseSystemPath))
|
||||
{
|
||||
string rawPath = systemPath.Replace(baseSystemPath, "");
|
||||
int firstSeparatorOffset = rawPath.IndexOf(Path.DirectorySeparatorChar);
|
||||
string rawPath = systemPath.Replace(baseSystemPath, "");
|
||||
int firstSeparatorOffset = rawPath.IndexOf(Path.DirectorySeparatorChar);
|
||||
|
||||
if (firstSeparatorOffset == -1)
|
||||
{
|
||||
|
@ -196,33 +169,34 @@ namespace Ryujinx.HLE.FileSystem
|
|||
return new DriveInfo(Path.GetPathRoot(GetBasePath()));
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
public void InitializeFsServer(LibHac.Horizon horizon, out HorizonClient fsServerClient)
|
||||
{
|
||||
ReloadKeySet();
|
||||
|
||||
LocalFileSystem serverBaseFs = new LocalFileSystem(GetBasePath());
|
||||
|
||||
DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet);
|
||||
fsServerClient = horizon.CreatePrivilegedHorizonClient();
|
||||
var fsServer = new FileSystemServer(fsServerClient);
|
||||
|
||||
DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet, fsServer);
|
||||
|
||||
GameCard = fsServerObjects.GameCard;
|
||||
SdCard = fsServerObjects.SdCard;
|
||||
SdCard = fsServerObjects.SdCard;
|
||||
|
||||
SdCard.SetSdCardInsertionStatus(true);
|
||||
|
||||
FileSystemServerConfig fsServerConfig = new FileSystemServerConfig
|
||||
var fsServerConfig = new FileSystemServerConfig
|
||||
{
|
||||
FsCreators = fsServerObjects.FsCreators,
|
||||
DeviceOperator = fsServerObjects.DeviceOperator,
|
||||
ExternalKeySet = KeySet.ExternalKeySet
|
||||
ExternalKeySet = KeySet.ExternalKeySet,
|
||||
FsCreators = fsServerObjects.FsCreators
|
||||
};
|
||||
|
||||
FsServer = new FileSystemServer(fsServerConfig);
|
||||
FsClient = FsServer.CreateFileSystemClient();
|
||||
FileSystemServerInitializer.InitializeWithConfig(fsServerClient, fsServer, fsServerConfig);
|
||||
}
|
||||
|
||||
|
||||
private void ReloadKeySet()
|
||||
public void ReloadKeySet()
|
||||
{
|
||||
KeySet ??= KeySet.CreateDefaultKeySet();
|
||||
|
||||
string keyFile = null;
|
||||
string titleKeyFile = null;
|
||||
string consoleKeyFile = null;
|
||||
|
@ -256,7 +230,7 @@ namespace Ryujinx.HLE.FileSystem
|
|||
}
|
||||
}
|
||||
|
||||
KeySet = ExternalKeyReader.ReadKeyFile(keyFile, titleKeyFile, consoleKeyFile);
|
||||
ExternalKeyReader.ReadKeyFile(KeySet, keyFile, titleKeyFile, consoleKeyFile, null);
|
||||
}
|
||||
|
||||
public void ImportTickets(IFileSystem fs)
|
||||
|
@ -277,6 +251,269 @@ namespace Ryujinx.HLE.FileSystem
|
|||
}
|
||||
}
|
||||
|
||||
// Save data created before we supported extra data in directory save data will not work properly if
|
||||
// given empty extra data. Luckily some of that extra data can be created using the data from the
|
||||
// save data indexer, which should be enough to check access permissions for user saves.
|
||||
// Every single save data's extra data will be checked and fixed if needed each time the emulator is opened.
|
||||
// Consider removing this at some point in the future when we don't need to worry about old saves.
|
||||
public static Result FixExtraData(HorizonClient hos)
|
||||
{
|
||||
Result rc = GetSystemSaveList(hos, out List<ulong> systemSaveIds);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = FixUnindexedSystemSaves(hos, systemSaveIds);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = FixExtraDataInSpaceId(hos, SaveDataSpaceId.System);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = FixExtraDataInSpaceId(hos, SaveDataSpaceId.User);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = FixExtraDataInSpaceId(hos, SaveDataSpaceId.SdCache);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private static Result FixExtraDataInSpaceId(HorizonClient hos, SaveDataSpaceId spaceId)
|
||||
{
|
||||
Span<SaveDataInfo> info = stackalloc SaveDataInfo[8];
|
||||
|
||||
Result rc = hos.Fs.OpenSaveDataIterator(out var iterator, spaceId);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
while (true)
|
||||
{
|
||||
rc = iterator.ReadSaveDataInfo(out long count, info);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (count == 0)
|
||||
return Result.Success;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
rc = FixExtraData(out bool wasFixNeeded, hos, in info[i]);
|
||||
|
||||
if (rc.IsFailure())
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Error {rc.ToStringWithName()} when fixing extra data for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space");
|
||||
}
|
||||
else if (wasFixNeeded)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, $"Tried to rebuild extra data for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gets a list of all the save data files or directories in the system partition.
|
||||
private static Result GetSystemSaveList(HorizonClient hos, out List<ulong> list)
|
||||
{
|
||||
list = null;
|
||||
|
||||
var mountName = "system".ToU8Span();
|
||||
DirectoryHandle handle = default;
|
||||
List<ulong> localList = new List<ulong>();
|
||||
|
||||
try
|
||||
{
|
||||
Result rc = hos.Fs.MountBis(mountName, BisPartitionId.System);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = hos.Fs.OpenDirectory(out handle, "system:/save".ToU8Span(), OpenDirectoryMode.All);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
DirectoryEntry entry = new DirectoryEntry();
|
||||
|
||||
while (true)
|
||||
{
|
||||
rc = hos.Fs.ReadDirectory(out long readCount, SpanHelpers.AsSpan(ref entry), handle);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (readCount == 0)
|
||||
break;
|
||||
|
||||
if (Utf8Parser.TryParse(entry.Name, out ulong saveDataId, out int bytesRead, 'x') &&
|
||||
bytesRead == 16 && (long)saveDataId < 0)
|
||||
{
|
||||
localList.Add(saveDataId);
|
||||
}
|
||||
}
|
||||
|
||||
list = localList;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (handle.IsValid)
|
||||
{
|
||||
hos.Fs.CloseDirectory(handle);
|
||||
}
|
||||
|
||||
if (hos.Fs.IsMounted(mountName))
|
||||
{
|
||||
hos.Fs.Unmount(mountName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adds system save data that isn't in the save data indexer to the indexer and creates extra data for it.
|
||||
// Only save data IDs added to SystemExtraDataFixInfo will be fixed.
|
||||
private static Result FixUnindexedSystemSaves(HorizonClient hos, List<ulong> existingSaveIds)
|
||||
{
|
||||
foreach (var fixInfo in SystemExtraDataFixInfo)
|
||||
{
|
||||
if (!existingSaveIds.Contains(fixInfo.StaticSaveDataId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Result rc = FixSystemExtraData(out bool wasFixNeeded, hos, in fixInfo);
|
||||
|
||||
if (rc.IsFailure())
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application,
|
||||
$"Error {rc.ToStringWithName()} when fixing extra data for system save data 0x{fixInfo.StaticSaveDataId:x}");
|
||||
}
|
||||
else if (wasFixNeeded)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application,
|
||||
$"Tried to rebuild extra data for system save data 0x{fixInfo.StaticSaveDataId:x}");
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private static Result FixSystemExtraData(out bool wasFixNeeded, HorizonClient hos, in ExtraDataFixInfo info)
|
||||
{
|
||||
wasFixNeeded = true;
|
||||
|
||||
Result rc = hos.Fs.Impl.ReadSaveDataFileSystemExtraData(out SaveDataExtraData extraData, info.StaticSaveDataId);
|
||||
if (!rc.IsSuccess())
|
||||
{
|
||||
if (!ResultFs.TargetNotFound.Includes(rc))
|
||||
return rc;
|
||||
|
||||
// We'll reach this point only if the save data directory exists but it's not in the save data indexer.
|
||||
// Creating the save will add it to the indexer while leaving its existing contents intact.
|
||||
return hos.Fs.CreateSystemSaveData(info.StaticSaveDataId, UserId.InvalidId, info.OwnerId, info.DataSize,
|
||||
info.JournalSize, info.Flags);
|
||||
}
|
||||
|
||||
if (extraData.Attribute.StaticSaveDataId != 0 && extraData.OwnerId != 0)
|
||||
{
|
||||
wasFixNeeded = false;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
extraData = new SaveDataExtraData
|
||||
{
|
||||
Attribute = { StaticSaveDataId = info.StaticSaveDataId },
|
||||
OwnerId = info.OwnerId,
|
||||
Flags = info.Flags,
|
||||
DataSize = info.DataSize,
|
||||
JournalSize = info.JournalSize
|
||||
};
|
||||
|
||||
// Make a mask for writing the entire extra data
|
||||
Unsafe.SkipInit(out SaveDataExtraData extraDataMask);
|
||||
SpanHelpers.AsByteSpan(ref extraDataMask).Fill(0xFF);
|
||||
|
||||
return hos.Fs.Impl.WriteSaveDataFileSystemExtraData(SaveDataSpaceId.System, info.StaticSaveDataId,
|
||||
in extraData, in extraDataMask);
|
||||
}
|
||||
|
||||
private static Result FixExtraData(out bool wasFixNeeded, HorizonClient hos, in SaveDataInfo info)
|
||||
{
|
||||
wasFixNeeded = true;
|
||||
|
||||
Result rc = hos.Fs.Impl.ReadSaveDataFileSystemExtraData(out SaveDataExtraData extraData, info.SpaceId,
|
||||
info.SaveDataId);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
// The extra data should have program ID or static save data ID set if it's valid.
|
||||
// We only try to fix the extra data if the info from the save data indexer has a program ID or static save data ID.
|
||||
bool canFixByProgramId = extraData.Attribute.ProgramId == ProgramId.InvalidId &&
|
||||
info.ProgramId != ProgramId.InvalidId;
|
||||
|
||||
bool canFixBySaveDataId = extraData.Attribute.StaticSaveDataId == 0 && info.StaticSaveDataId != 0;
|
||||
|
||||
if (!canFixByProgramId && !canFixBySaveDataId)
|
||||
{
|
||||
wasFixNeeded = false;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
// The save data attribute struct can be completely created from the save data info.
|
||||
extraData.Attribute.ProgramId = info.ProgramId;
|
||||
extraData.Attribute.UserId = info.UserId;
|
||||
extraData.Attribute.StaticSaveDataId = info.StaticSaveDataId;
|
||||
extraData.Attribute.Type = info.Type;
|
||||
extraData.Attribute.Rank = info.Rank;
|
||||
extraData.Attribute.Index = info.Index;
|
||||
|
||||
// The rest of the extra data can't be created from the save data info.
|
||||
// On user saves the owner ID will almost certainly be the same as the program ID.
|
||||
if (info.Type != LibHac.Fs.SaveDataType.System)
|
||||
{
|
||||
extraData.OwnerId = info.ProgramId.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try to match the system save with one of the known saves
|
||||
foreach (ExtraDataFixInfo fixInfo in SystemExtraDataFixInfo)
|
||||
{
|
||||
if (extraData.Attribute.StaticSaveDataId == fixInfo.StaticSaveDataId)
|
||||
{
|
||||
extraData.OwnerId = fixInfo.OwnerId;
|
||||
extraData.Flags = fixInfo.Flags;
|
||||
extraData.DataSize = fixInfo.DataSize;
|
||||
extraData.JournalSize = fixInfo.JournalSize;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make a mask for writing the entire extra data
|
||||
Unsafe.SkipInit(out SaveDataExtraData extraDataMask);
|
||||
SpanHelpers.AsByteSpan(ref extraDataMask).Fill(0xFF);
|
||||
|
||||
return hos.Fs.Impl.WriteSaveDataFileSystemExtraData(info.SpaceId, info.SaveDataId, in extraData, in extraDataMask);
|
||||
}
|
||||
|
||||
struct ExtraDataFixInfo
|
||||
{
|
||||
public ulong StaticSaveDataId;
|
||||
public ulong OwnerId;
|
||||
public SaveDataFlags Flags;
|
||||
public long DataSize;
|
||||
public long JournalSize;
|
||||
}
|
||||
|
||||
private static readonly ExtraDataFixInfo[] SystemExtraDataFixInfo =
|
||||
{
|
||||
new ExtraDataFixInfo()
|
||||
{
|
||||
StaticSaveDataId = 0x8000000000000030,
|
||||
OwnerId = 0x010000000000001F,
|
||||
Flags = SaveDataFlags.KeepAfterResettingSystemSaveDataWithoutUserSaveData,
|
||||
DataSize = 0x10000,
|
||||
JournalSize = 0x10000
|
||||
},
|
||||
new ExtraDataFixInfo()
|
||||
{
|
||||
StaticSaveDataId = 0x8000000000001040,
|
||||
OwnerId = 0x0100000000001009,
|
||||
Flags = SaveDataFlags.None,
|
||||
DataSize = 0xC000,
|
||||
JournalSize = 0xC000
|
||||
}
|
||||
};
|
||||
|
||||
public void Unload()
|
||||
{
|
||||
RomFs?.Dispose();
|
||||
|
@ -299,7 +536,7 @@ namespace Ryujinx.HLE.FileSystem
|
|||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
throw new InvalidOperationException($"VirtualFileSystem can only be instantiated once!");
|
||||
throw new InvalidOperationException("VirtualFileSystem can only be instantiated once!");
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
using LibHac.FsSystem;
|
||||
using Ryujinx.Audio.Integration;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.FileSystem.Content;
|
||||
|
@ -10,7 +8,6 @@ using Ryujinx.HLE.HOS;
|
|||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.HLE.HOS.SystemState;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.HLE
|
||||
{
|
||||
|
@ -25,6 +22,12 @@ namespace Ryujinx.HLE
|
|||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly VirtualFileSystem VirtualFileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// The manager for handling a LibHac Horizon instance.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly LibHacHorizonManager LibHacHorizonManager;
|
||||
|
||||
/// <summary>
|
||||
/// The account manager used by the account service.
|
||||
/// </summary>
|
||||
|
@ -38,7 +41,7 @@ namespace Ryujinx.HLE
|
|||
internal readonly ContentManager ContentManager;
|
||||
|
||||
/// <summary>
|
||||
/// The persistant information between run for multi-application capabilities.
|
||||
/// The persistent information between run for multi-application capabilities.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
public readonly UserChannelPersistence UserChannelPersistence;
|
||||
|
@ -124,7 +127,7 @@ namespace Ryujinx.HLE
|
|||
public MemoryManagerMode MemoryManagerMode { internal get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Control the inital state of the ignore missing services setting.
|
||||
/// Control the initial state of the ignore missing services setting.
|
||||
/// If this is set to true, when a missing service is encountered, it will try to automatically handle it instead of throwing an exception.
|
||||
/// </summary>
|
||||
/// TODO: Update this again.
|
||||
|
@ -141,6 +144,7 @@ namespace Ryujinx.HLE
|
|||
public Action RefreshInputConfig { internal get; set; }
|
||||
|
||||
public HLEConfiguration(VirtualFileSystem virtualFileSystem,
|
||||
LibHacHorizonManager libHacHorizonManager,
|
||||
ContentManager contentManager,
|
||||
AccountManager accountManager,
|
||||
UserChannelPersistence userChannelPersistence,
|
||||
|
@ -162,6 +166,7 @@ namespace Ryujinx.HLE
|
|||
AspectRatio aspectRatio)
|
||||
{
|
||||
VirtualFileSystem = virtualFileSystem;
|
||||
LibHacHorizonManager = libHacHorizonManager;
|
||||
AccountManager = accountManager;
|
||||
ContentManager = contentManager;
|
||||
UserChannelPersistence = userChannelPersistence;
|
||||
|
|
|
@ -4,15 +4,17 @@ using LibHac.Account;
|
|||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.Fs.Shim;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.FsSystem.NcaUtils;
|
||||
using LibHac.Loader;
|
||||
using LibHac.Ncm;
|
||||
using LibHac.Ns;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS.Kernel.Process;
|
||||
using Ryujinx.HLE.Loaders.Executables;
|
||||
using Ryujinx.HLE.Loaders.Npdm;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
@ -57,14 +59,14 @@ namespace Ryujinx.HLE.HOS
|
|||
public string TitleName => _titleName;
|
||||
public string DisplayVersion => _displayVersion;
|
||||
|
||||
public ulong TitleId { get; private set; }
|
||||
public bool TitleIs64Bit { get; private set; }
|
||||
public ulong TitleId { get; private set; }
|
||||
public bool TitleIs64Bit { get; private set; }
|
||||
|
||||
public string TitleIdText => TitleId.ToString("x16");
|
||||
|
||||
public ApplicationLoader(Switch device)
|
||||
{
|
||||
_device = device;
|
||||
_device = device;
|
||||
_controlData = new BlitStruct<ApplicationControlProperty>(1);
|
||||
}
|
||||
|
||||
|
@ -77,7 +79,7 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
LocalFileSystem codeFs = new LocalFileSystem(exeFsDir);
|
||||
|
||||
Npdm metaData = ReadNpdm(codeFs);
|
||||
MetaLoader metaData = ReadNpdm(codeFs);
|
||||
|
||||
_device.Configuration.VirtualFileSystem.ModLoader.CollectMods(new[] { TitleId }, _device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath());
|
||||
|
||||
|
@ -91,8 +93,8 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex)
|
||||
{
|
||||
Nca mainNca = null;
|
||||
Nca patchNca = null;
|
||||
Nca mainNca = null;
|
||||
Nca patchNca = null;
|
||||
Nca controlNca = null;
|
||||
|
||||
fileSystem.ImportTickets(pfs);
|
||||
|
@ -202,7 +204,7 @@ namespace Ryujinx.HLE.HOS
|
|||
public void LoadXci(string xciFile)
|
||||
{
|
||||
FileStream file = new FileStream(xciFile, FileMode.Open, FileAccess.Read);
|
||||
Xci xci = new Xci(_device.Configuration.VirtualFileSystem.KeySet, file.AsStorage());
|
||||
Xci xci = new Xci(_device.Configuration.VirtualFileSystem.KeySet, file.AsStorage());
|
||||
|
||||
if (!xci.HasPartition(XciPartitionType.Secure))
|
||||
{
|
||||
|
@ -220,6 +222,8 @@ namespace Ryujinx.HLE.HOS
|
|||
try
|
||||
{
|
||||
(mainNca, patchNca, controlNca) = GetGameData(_device.Configuration.VirtualFileSystem, securePartition, _device.Configuration.UserChannelPersistence.Index);
|
||||
|
||||
RegisterProgramMapInfo(securePartition).ThrowIfFailure();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -244,8 +248,8 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
public void LoadNsp(string nspFile)
|
||||
{
|
||||
FileStream file = new FileStream(nspFile, FileMode.Open, FileAccess.Read);
|
||||
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
|
||||
FileStream file = new FileStream(nspFile, FileMode.Open, FileAccess.Read);
|
||||
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
|
||||
|
||||
Nca mainNca;
|
||||
Nca patchNca;
|
||||
|
@ -254,6 +258,8 @@ namespace Ryujinx.HLE.HOS
|
|||
try
|
||||
{
|
||||
(mainNca, patchNca, controlNca) = GetGameData(_device.Configuration.VirtualFileSystem, nsp, _device.Configuration.UserChannelPersistence.Index);
|
||||
|
||||
RegisterProgramMapInfo(nsp).ThrowIfFailure();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -286,7 +292,7 @@ namespace Ryujinx.HLE.HOS
|
|||
public void LoadNca(string ncaFile)
|
||||
{
|
||||
FileStream file = new FileStream(ncaFile, FileMode.Open, FileAccess.Read);
|
||||
Nca nca = new Nca(_device.Configuration.VirtualFileSystem.KeySet, file.AsStorage(false));
|
||||
Nca nca = new Nca(_device.Configuration.VirtualFileSystem.KeySet, file.AsStorage(false));
|
||||
|
||||
LoadNca(nca, null, null);
|
||||
}
|
||||
|
@ -300,8 +306,8 @@ namespace Ryujinx.HLE.HOS
|
|||
return;
|
||||
}
|
||||
|
||||
IStorage dataStorage = null;
|
||||
IFileSystem codeFs = null;
|
||||
IStorage dataStorage = null;
|
||||
IFileSystem codeFs = null;
|
||||
|
||||
(Nca updatePatchNca, Nca updateControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), _device.Configuration.UserChannelPersistence.Index, out _);
|
||||
|
||||
|
@ -366,7 +372,7 @@ namespace Ryujinx.HLE.HOS
|
|||
return;
|
||||
}
|
||||
|
||||
Npdm metaData = ReadNpdm(codeFs);
|
||||
MetaLoader metaData = ReadNpdm(codeFs);
|
||||
|
||||
_device.Configuration.VirtualFileSystem.ModLoader.CollectMods(_device.Configuration.ContentManager.GetAocTitleIds().Prepend(TitleId), _device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath());
|
||||
|
||||
|
@ -400,9 +406,12 @@ namespace Ryujinx.HLE.HOS
|
|||
_device.Configuration.VirtualFileSystem.SetRomFs(newStorage.AsStream(FileAccess.Read));
|
||||
}
|
||||
|
||||
if (TitleId != 0)
|
||||
// Don't create save data for system programs.
|
||||
if (TitleId != 0 && (TitleId < SystemProgramId.Start.Value || TitleId > SystemAppletId.End.Value))
|
||||
{
|
||||
EnsureSaveData(new ApplicationId(TitleId));
|
||||
// Multi-program applications can technically use any program ID for the main program, but in practice they always use 0 in the low nibble.
|
||||
// We'll know if this changes in the future because stuff will get errors when trying to mount the correct save.
|
||||
EnsureSaveData(new ApplicationId(TitleId & ~0xFul));
|
||||
}
|
||||
|
||||
LoadExeFs(codeFs, metaData);
|
||||
|
@ -411,11 +420,11 @@ namespace Ryujinx.HLE.HOS
|
|||
}
|
||||
|
||||
// Sets TitleId, so be sure to call before using it
|
||||
private Npdm ReadNpdm(IFileSystem fs)
|
||||
private MetaLoader ReadNpdm(IFileSystem fs)
|
||||
{
|
||||
Result result = fs.OpenFile(out IFile npdmFile, "/main.npdm".ToU8Span(), OpenMode.Read);
|
||||
|
||||
Npdm metaData;
|
||||
MetaLoader metaData;
|
||||
|
||||
if (ResultFs.PathNotFound.Includes(result))
|
||||
{
|
||||
|
@ -425,11 +434,20 @@ namespace Ryujinx.HLE.HOS
|
|||
}
|
||||
else
|
||||
{
|
||||
metaData = new Npdm(npdmFile.AsStream());
|
||||
npdmFile.GetSize(out long fileSize).ThrowIfFailure();
|
||||
|
||||
var npdmBuffer = new byte[fileSize];
|
||||
npdmFile.Read(out _, 0, npdmBuffer).ThrowIfFailure();
|
||||
|
||||
metaData = new MetaLoader();
|
||||
metaData.Load(npdmBuffer).ThrowIfFailure();
|
||||
}
|
||||
|
||||
TitleId = metaData.Aci0.TitleId;
|
||||
TitleIs64Bit = metaData.Is64Bit;
|
||||
metaData.GetNpdm(out var npdm).ThrowIfFailure();
|
||||
|
||||
TitleId = npdm.Aci.Value.ProgramId.Value;
|
||||
TitleIs64Bit = (npdm.Meta.Value.Flags & 1) != 0;
|
||||
_device.System.LibHacHorizonManager.ArpIReader.ApplicationId = new LibHac.ApplicationId(TitleId);
|
||||
|
||||
return metaData;
|
||||
}
|
||||
|
@ -437,7 +455,7 @@ namespace Ryujinx.HLE.HOS
|
|||
private static void ReadControlData(Switch device, Nca controlNca, ref BlitStruct<ApplicationControlProperty> controlData, ref string titleName, ref string displayVersion)
|
||||
{
|
||||
IFileSystem controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, device.System.FsIntegrityCheckLevel);
|
||||
Result result = controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read);
|
||||
Result result = controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
|
@ -461,7 +479,7 @@ namespace Ryujinx.HLE.HOS
|
|||
}
|
||||
}
|
||||
|
||||
private void LoadExeFs(IFileSystem codeFs, Npdm metaData = null)
|
||||
private void LoadExeFs(IFileSystem codeFs, MetaLoader metaData = null)
|
||||
{
|
||||
if (_device.Configuration.VirtualFileSystem.ModLoader.ReplaceExefsPartition(TitleId, ref codeFs))
|
||||
{
|
||||
|
@ -519,22 +537,26 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
Ptc.Initialize(TitleIdText, DisplayVersion, usePtc, _device.Configuration.MemoryManagerMode);
|
||||
|
||||
ProgramLoader.LoadNsos(_device.System.KernelContext, out ProcessTamperInfo tamperInfo, metaData, executables: programs);
|
||||
metaData.GetNpdm(out Npdm npdm).ThrowIfFailure();
|
||||
ProgramLoader.LoadNsos(_device.System.KernelContext, out ProcessTamperInfo tamperInfo, metaData, new ProgramInfo(in npdm), executables: programs);
|
||||
|
||||
_device.Configuration.VirtualFileSystem.ModLoader.LoadCheats(TitleId, tamperInfo, _device.TamperMachine);
|
||||
}
|
||||
|
||||
public void LoadProgram(string filePath)
|
||||
{
|
||||
Npdm metaData = GetDefaultNpdm();
|
||||
bool isNro = Path.GetExtension(filePath).ToLower() == ".nro";
|
||||
MetaLoader metaData = GetDefaultNpdm();
|
||||
metaData.GetNpdm(out Npdm npdm).ThrowIfFailure();
|
||||
ProgramInfo programInfo = new ProgramInfo(in npdm);
|
||||
|
||||
bool isNro = Path.GetExtension(filePath).ToLower() == ".nro";
|
||||
|
||||
IExecutable executable;
|
||||
|
||||
if (isNro)
|
||||
{
|
||||
FileStream input = new FileStream(filePath, FileMode.Open);
|
||||
NroExecutable obj = new NroExecutable(input.AsStorage());
|
||||
FileStream input = new FileStream(filePath, FileMode.Open);
|
||||
NroExecutable obj = new NroExecutable(input.AsStorage());
|
||||
|
||||
executable = obj;
|
||||
|
||||
|
@ -552,13 +574,13 @@ namespace Ryujinx.HLE.HOS
|
|||
if (asetVersion == 0)
|
||||
{
|
||||
ulong iconOffset = reader.ReadUInt64();
|
||||
ulong iconSize = reader.ReadUInt64();
|
||||
ulong iconSize = reader.ReadUInt64();
|
||||
|
||||
ulong nacpOffset = reader.ReadUInt64();
|
||||
ulong nacpSize = reader.ReadUInt64();
|
||||
ulong nacpSize = reader.ReadUInt64();
|
||||
|
||||
ulong romfsOffset = reader.ReadUInt64();
|
||||
ulong romfsSize = reader.ReadUInt64();
|
||||
ulong romfsSize = reader.ReadUInt64();
|
||||
|
||||
if (romfsSize != 0)
|
||||
{
|
||||
|
@ -573,28 +595,28 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
ref ApplicationControlProperty nacp = ref ControlData.Value;
|
||||
|
||||
metaData.TitleName = nacp.Titles[(int)_device.System.State.DesiredTitleLanguage].Name.ToString();
|
||||
programInfo.Name = nacp.Titles[(int)_device.System.State.DesiredTitleLanguage].Name.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(metaData.TitleName))
|
||||
if (string.IsNullOrWhiteSpace(programInfo.Name))
|
||||
{
|
||||
metaData.TitleName = nacp.Titles.ToArray().FirstOrDefault(x => x.Name[0] != 0).Name.ToString();
|
||||
programInfo.Name = nacp.Titles.ToArray().FirstOrDefault(x => x.Name[0] != 0).Name.ToString();
|
||||
}
|
||||
|
||||
if (nacp.PresenceGroupId != 0)
|
||||
{
|
||||
metaData.Aci0.TitleId = nacp.PresenceGroupId;
|
||||
programInfo.ProgramId = nacp.PresenceGroupId;
|
||||
}
|
||||
else if (nacp.SaveDataOwnerId.Value != 0)
|
||||
{
|
||||
metaData.Aci0.TitleId = nacp.SaveDataOwnerId.Value;
|
||||
programInfo.ProgramId = nacp.SaveDataOwnerId.Value;
|
||||
}
|
||||
else if (nacp.AddOnContentBaseId != 0)
|
||||
{
|
||||
metaData.Aci0.TitleId = nacp.AddOnContentBaseId - 0x1000;
|
||||
programInfo.ProgramId = nacp.AddOnContentBaseId - 0x1000;
|
||||
}
|
||||
else
|
||||
{
|
||||
metaData.Aci0.TitleId = 0000000000000000;
|
||||
programInfo.ProgramId = 0000000000000000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -612,29 +634,109 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
_device.Configuration.ContentManager.LoadEntries(_device);
|
||||
|
||||
_titleName = metaData.TitleName;
|
||||
TitleId = metaData.Aci0.TitleId;
|
||||
TitleIs64Bit = metaData.Is64Bit;
|
||||
_titleName = programInfo.Name;
|
||||
TitleId = programInfo.ProgramId;
|
||||
TitleIs64Bit = (npdm.Meta.Value.Flags & 1) != 0;
|
||||
_device.System.LibHacHorizonManager.ArpIReader.ApplicationId = new LibHac.ApplicationId(TitleId);
|
||||
|
||||
// Explicitly null titleid to disable the shader cache
|
||||
Graphics.Gpu.GraphicsConfig.TitleId = null;
|
||||
_device.Gpu.HostInitalized.Set();
|
||||
|
||||
ProgramLoader.LoadNsos(_device.System.KernelContext, out ProcessTamperInfo tamperInfo, metaData, executables: executable);
|
||||
ProgramLoader.LoadNsos(_device.System.KernelContext, out ProcessTamperInfo tamperInfo, metaData, programInfo, executables: executable);
|
||||
|
||||
_device.Configuration.VirtualFileSystem.ModLoader.LoadCheats(TitleId, tamperInfo, _device.TamperMachine);
|
||||
}
|
||||
|
||||
private Npdm GetDefaultNpdm()
|
||||
private MetaLoader GetDefaultNpdm()
|
||||
{
|
||||
Assembly asm = Assembly.GetCallingAssembly();
|
||||
|
||||
using (Stream npdmStream = asm.GetManifestResourceStream("Ryujinx.HLE.Homebrew.npdm"))
|
||||
{
|
||||
return new Npdm(npdmStream);
|
||||
var npdmBuffer = new byte[npdmStream.Length];
|
||||
npdmStream.Read(npdmBuffer);
|
||||
|
||||
var metaLoader = new MetaLoader();
|
||||
metaLoader.Load(npdmBuffer).ThrowIfFailure();
|
||||
|
||||
return metaLoader;
|
||||
}
|
||||
}
|
||||
|
||||
private static (ulong applicationId, int programCount) GetMultiProgramInfo(VirtualFileSystem fileSystem, PartitionFileSystem pfs)
|
||||
{
|
||||
ulong mainProgramId = 0;
|
||||
Span<bool> hasIndex = stackalloc bool[0x10];
|
||||
|
||||
fileSystem.ImportTickets(pfs);
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = new Nca(fileSystem.KeySet, ncaFile.AsStorage());
|
||||
|
||||
if (nca.Header.ContentType != NcaContentType.Program)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
||||
|
||||
if (nca.Header.GetFsHeader(dataIndex).IsPatchSection())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ulong currentProgramId = nca.Header.TitleId;
|
||||
ulong currentMainProgramId = currentProgramId & ~0xFFFul;
|
||||
|
||||
if (mainProgramId == 0 && currentMainProgramId != 0)
|
||||
{
|
||||
mainProgramId = currentMainProgramId;
|
||||
}
|
||||
|
||||
if (mainProgramId != currentMainProgramId)
|
||||
{
|
||||
// As far as I know there aren't any multi-application game cards containing multi-program applications,
|
||||
// so because multi-application game cards are the only way we should run into multiple applications
|
||||
// we'll just return that there's a single program.
|
||||
return (mainProgramId, 1);
|
||||
}
|
||||
|
||||
hasIndex[(int)(currentProgramId & 0xF)] = true;
|
||||
}
|
||||
|
||||
int programCount = 0;
|
||||
|
||||
for (int i = 0; i < hasIndex.Length && hasIndex[i]; i++)
|
||||
{
|
||||
programCount++;
|
||||
}
|
||||
|
||||
return (mainProgramId, programCount);
|
||||
}
|
||||
|
||||
private Result RegisterProgramMapInfo(PartitionFileSystem pfs)
|
||||
{
|
||||
(ulong applicationId, int programCount) = GetMultiProgramInfo(_device.Configuration.VirtualFileSystem, pfs);
|
||||
|
||||
if (programCount <= 0)
|
||||
return Result.Success;
|
||||
|
||||
Span<ProgramIndexMapInfo> mapInfo = stackalloc ProgramIndexMapInfo[0x10];
|
||||
|
||||
for (int i = 0; i < programCount; i++)
|
||||
{
|
||||
mapInfo[i].ProgramId = new ProgramId(applicationId + (uint)i);
|
||||
mapInfo[i].MainProgramId = new ProgramId(applicationId);
|
||||
mapInfo[i].ProgramIndex = (byte)i;
|
||||
}
|
||||
|
||||
return _device.System.LibHacHorizonManager.NsClient.Fs.RegisterProgramIndexMapInfo(mapInfo.Slice(0, programCount));
|
||||
}
|
||||
|
||||
private Result EnsureSaveData(ApplicationId applicationId)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Ensuring required savedata exists.");
|
||||
|
@ -643,7 +745,7 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
ref ApplicationControlProperty control = ref ControlData.Value;
|
||||
|
||||
if (LibHac.Utilities.IsEmpty(ControlData.ByteSpan))
|
||||
if (LibHac.Utilities.IsZeros(ControlData.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.
|
||||
|
@ -657,8 +759,8 @@ namespace Ryujinx.HLE.HOS
|
|||
"No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
|
||||
}
|
||||
|
||||
FileSystemClient fileSystem = _device.Configuration.VirtualFileSystem.FsClient;
|
||||
Result resultCode = fileSystem.EnsureApplicationCacheStorage(out _, applicationId, ref control);
|
||||
HorizonClient hos = _device.System.LibHacHorizonManager.RyujinxClient;
|
||||
Result resultCode = hos.Fs.EnsureApplicationCacheStorage(out _, out _, applicationId, ref control);
|
||||
|
||||
if (resultCode.IsFailure())
|
||||
{
|
||||
|
@ -667,7 +769,7 @@ namespace Ryujinx.HLE.HOS
|
|||
return resultCode;
|
||||
}
|
||||
|
||||
resultCode = EnsureApplicationSaveData(fileSystem, out _, applicationId, ref control, ref user);
|
||||
resultCode = EnsureApplicationSaveData(hos.Fs, out _, applicationId, ref control, ref user);
|
||||
|
||||
if (resultCode.IsFailure())
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using LibHac;
|
||||
using LibHac.Bcat;
|
||||
using LibHac.Common.Keys;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Shim;
|
||||
using LibHac.FsSystem;
|
||||
using Ryujinx.Audio;
|
||||
using Ryujinx.Audio.Input;
|
||||
|
@ -18,7 +18,6 @@ using Ryujinx.HLE.HOS.Services;
|
|||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemAppletProxy;
|
||||
using Ryujinx.HLE.HOS.Services.Apm;
|
||||
using Ryujinx.HLE.HOS.Services.Arp;
|
||||
using Ryujinx.HLE.HOS.Services.Audio.AudioRenderer;
|
||||
using Ryujinx.HLE.HOS.Services.Caps;
|
||||
using Ryujinx.HLE.HOS.Services.Mii;
|
||||
|
@ -38,6 +37,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using TimeSpanType = Ryujinx.HLE.HOS.Services.Time.Clock.TimeSpanType;
|
||||
|
||||
namespace Ryujinx.HLE.HOS
|
||||
{
|
||||
|
@ -97,7 +97,7 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
internal KEvent DisplayResolutionChangeEvent { get; private set; }
|
||||
|
||||
public Keyset KeySet => Device.FileSystem.KeySet;
|
||||
public KeySet KeySet => Device.FileSystem.KeySet;
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
|
@ -111,8 +111,7 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
internal NvHostSyncpt HostSyncpoint { get; private set; }
|
||||
|
||||
internal LibHac.Horizon LibHacHorizonServer { get; private set; }
|
||||
internal HorizonClient LibHacHorizonClient { get; private set; }
|
||||
internal LibHacHorizonManager LibHacHorizonManager { get; private set; }
|
||||
|
||||
public Horizon(Switch device)
|
||||
{
|
||||
|
@ -184,6 +183,8 @@ namespace Ryujinx.HLE.HOS
|
|||
ContentManager = device.Configuration.ContentManager;
|
||||
CaptureManager = new CaptureManager(device);
|
||||
|
||||
LibHacHorizonManager = device.Configuration.LibHacHorizonManager;
|
||||
|
||||
// TODO: use set:sys (and get external clock source id from settings)
|
||||
// TODO: use "time!standard_steady_clock_rtc_update_interval_minutes" and implement a worker thread to be accurate.
|
||||
UInt128 clockSourceId = new UInt128(Guid.NewGuid().ToByteArray());
|
||||
|
@ -223,17 +224,16 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
TimeServiceManager.Instance.SetupStandardUserSystemClock(null, false, SteadyClockTimePoint.GetRandom());
|
||||
|
||||
// FIXME: TimeZone shoud be init here but it's actually done in ContentManager
|
||||
// FIXME: TimeZone should be init here but it's actually done in ContentManager
|
||||
|
||||
TimeServiceManager.Instance.SetupEphemeralNetworkSystemClock();
|
||||
|
||||
DatabaseImpl.Instance.InitializeDatabase(device);
|
||||
DatabaseImpl.Instance.InitializeDatabase(LibHacHorizonManager.SdbClient);
|
||||
|
||||
HostSyncpoint = new NvHostSyncpt(device);
|
||||
|
||||
SurfaceFlinger = new SurfaceFlinger(device);
|
||||
|
||||
InitLibHacHorizon();
|
||||
InitializeAudioRenderer();
|
||||
}
|
||||
|
||||
|
@ -309,20 +309,6 @@ namespace Ryujinx.HLE.HOS
|
|||
ProgramLoader.LoadKip(KernelContext, new KipExecutable(kipFile));
|
||||
}
|
||||
|
||||
private void InitLibHacHorizon()
|
||||
{
|
||||
LibHac.Horizon horizon = new LibHac.Horizon(null, Device.FileSystem.FsServer);
|
||||
|
||||
horizon.CreateHorizonClient(out HorizonClient ryujinxClient).ThrowIfFailure();
|
||||
horizon.CreateHorizonClient(out HorizonClient bcatClient).ThrowIfFailure();
|
||||
|
||||
ryujinxClient.Sm.RegisterService(new LibHacIReader(this), "arp:r").ThrowIfFailure();
|
||||
new BcatServer(bcatClient);
|
||||
|
||||
LibHacHorizonServer = horizon;
|
||||
LibHacHorizonClient = ryujinxClient;
|
||||
}
|
||||
|
||||
public void ChangeDockedModeState(bool newState)
|
||||
{
|
||||
if (newState != State.DockedMode)
|
||||
|
@ -355,8 +341,8 @@ namespace Ryujinx.HLE.HOS
|
|||
{
|
||||
if (NfpDevices[nfpDeviceId].State == NfpDeviceState.SearchingForTag)
|
||||
{
|
||||
NfpDevices[nfpDeviceId].State = NfpDeviceState.TagFound;
|
||||
NfpDevices[nfpDeviceId].AmiiboId = amiiboId;
|
||||
NfpDevices[nfpDeviceId].State = NfpDeviceState.TagFound;
|
||||
NfpDevices[nfpDeviceId].AmiiboId = amiiboId;
|
||||
NfpDevices[nfpDeviceId].UseRandomUuid = useRandomUuid;
|
||||
}
|
||||
}
|
||||
|
@ -453,6 +439,8 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
AudioRendererManager.Dispose();
|
||||
|
||||
LibHacHorizonManager.AmClient.Fs.UnregisterProgram(LibHacHorizonManager.ApplicationClient.Os.GetCurrentProcessId().Value);
|
||||
|
||||
KernelContext.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
124
Ryujinx.HLE/HOS/LibHacHorizonManager.cs
Normal file
124
Ryujinx.HLE/HOS/LibHacHorizonManager.cs
Normal file
|
@ -0,0 +1,124 @@
|
|||
using LibHac;
|
||||
using LibHac.Bcat;
|
||||
using LibHac.FsSrv.Impl;
|
||||
using LibHac.Loader;
|
||||
using LibHac.Ncm;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS.Services.Arp;
|
||||
using System;
|
||||
using StorageId = LibHac.Ncm.StorageId;
|
||||
|
||||
namespace Ryujinx.HLE.HOS
|
||||
{
|
||||
public class LibHacHorizonManager
|
||||
{
|
||||
private LibHac.Horizon Server { get; set; }
|
||||
public HorizonClient RyujinxClient { get; private set; }
|
||||
|
||||
public HorizonClient ApplicationClient { get; private set; }
|
||||
|
||||
public HorizonClient AccountClient { get; private set; }
|
||||
public HorizonClient AmClient { get; private set; }
|
||||
public HorizonClient BcatClient { get; private set; }
|
||||
public HorizonClient FsClient { get; private set; }
|
||||
public HorizonClient NsClient { get; private set; }
|
||||
public HorizonClient SdbClient { get; private set; }
|
||||
|
||||
internal LibHacIReader ArpIReader { get; private set; }
|
||||
|
||||
public LibHacHorizonManager()
|
||||
{
|
||||
InitializeServer();
|
||||
}
|
||||
|
||||
private void InitializeServer()
|
||||
{
|
||||
Server = new LibHac.Horizon(new HorizonConfiguration());
|
||||
|
||||
RyujinxClient = Server.CreatePrivilegedHorizonClient();
|
||||
}
|
||||
|
||||
public void InitializeArpServer()
|
||||
{
|
||||
ArpIReader = new LibHacIReader();
|
||||
RyujinxClient.Sm.RegisterService(new LibHacArpServiceObject(ArpIReader), "arp:r").ThrowIfFailure();
|
||||
}
|
||||
|
||||
public void InitializeBcatServer()
|
||||
{
|
||||
BcatClient = Server.CreateHorizonClient(new ProgramLocation(SystemProgramId.Bcat, StorageId.BuiltInSystem),
|
||||
BcatFsPermissions);
|
||||
|
||||
_ = new BcatServer(BcatClient);
|
||||
}
|
||||
|
||||
public void InitializeFsServer(VirtualFileSystem virtualFileSystem)
|
||||
{
|
||||
virtualFileSystem.InitializeFsServer(Server, out var fsClient);
|
||||
|
||||
FsClient = fsClient;
|
||||
}
|
||||
|
||||
public void InitializeSystemClients()
|
||||
{
|
||||
AccountClient = Server.CreateHorizonClient(new ProgramLocation(SystemProgramId.Account, StorageId.BuiltInSystem),
|
||||
AccountFsPermissions);
|
||||
|
||||
AmClient = Server.CreateHorizonClient(new ProgramLocation(SystemProgramId.Am, StorageId.BuiltInSystem),
|
||||
AmFsPermissions);
|
||||
|
||||
NsClient = Server.CreateHorizonClient(new ProgramLocation(SystemProgramId.Ns, StorageId.BuiltInSystem),
|
||||
NsFsPermissions);
|
||||
|
||||
SdbClient = Server.CreateHorizonClient(new ProgramLocation(SystemProgramId.Sdb, StorageId.BuiltInSystem),
|
||||
SdbFacData, SdbFacDescriptor);
|
||||
}
|
||||
|
||||
public void InitializeApplicationClient(ProgramId programId, in Npdm npdm)
|
||||
{
|
||||
ApplicationClient = Server.CreateHorizonClient(new ProgramLocation(programId, StorageId.BuiltInUser),
|
||||
npdm.FsAccessControlData, npdm.FsAccessControlDescriptor);
|
||||
}
|
||||
|
||||
private static AccessControlBits.Bits AccountFsPermissions => AccessControlBits.Bits.SystemSaveData |
|
||||
AccessControlBits.Bits.GameCard |
|
||||
AccessControlBits.Bits.SaveDataMeta |
|
||||
AccessControlBits.Bits.GetRightsId;
|
||||
|
||||
private static AccessControlBits.Bits AmFsPermissions => AccessControlBits.Bits.SaveDataManagement |
|
||||
AccessControlBits.Bits.CreateSaveData |
|
||||
AccessControlBits.Bits.SystemData;
|
||||
private static AccessControlBits.Bits BcatFsPermissions => AccessControlBits.Bits.SystemSaveData;
|
||||
|
||||
private static AccessControlBits.Bits NsFsPermissions => AccessControlBits.Bits.ApplicationInfo |
|
||||
AccessControlBits.Bits.SystemSaveData |
|
||||
AccessControlBits.Bits.GameCard |
|
||||
AccessControlBits.Bits.SaveDataManagement |
|
||||
AccessControlBits.Bits.ContentManager |
|
||||
AccessControlBits.Bits.ImageManager |
|
||||
AccessControlBits.Bits.SystemSaveDataManagement |
|
||||
AccessControlBits.Bits.SystemUpdate |
|
||||
AccessControlBits.Bits.SdCard |
|
||||
AccessControlBits.Bits.FormatSdCard |
|
||||
AccessControlBits.Bits.GetRightsId |
|
||||
AccessControlBits.Bits.RegisterProgramIndexMapInfo |
|
||||
AccessControlBits.Bits.MoveCacheStorage;
|
||||
|
||||
// Sdb has save data access control info so we can't store just its access control bits
|
||||
private static ReadOnlySpan<byte> SdbFacData => new byte[]
|
||||
{
|
||||
0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
||||
0x03, 0x03, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01
|
||||
};
|
||||
|
||||
private static ReadOnlySpan<byte> SdbFacDescriptor => new byte[]
|
||||
{
|
||||
0x01, 0x00, 0x02, 0x00, 0x08, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01
|
||||
};
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ using LibHac.Fs;
|
|||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.FsSystem.RomFs;
|
||||
using LibHac.Loader;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.Loaders.Mods;
|
||||
|
@ -12,7 +13,6 @@ using System.Collections.Generic;
|
|||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using Ryujinx.HLE.Loaders.Npdm;
|
||||
using Ryujinx.HLE.HOS.Kernel.Process;
|
||||
using System.Globalization;
|
||||
|
||||
|
@ -522,7 +522,7 @@ namespace Ryujinx.HLE.HOS
|
|||
{
|
||||
public BitVector32 Stubs;
|
||||
public BitVector32 Replaces;
|
||||
public Npdm Npdm;
|
||||
public MetaLoader Npdm;
|
||||
|
||||
public bool Modified => (Stubs.Data | Replaces.Data) != 0;
|
||||
}
|
||||
|
@ -582,9 +582,10 @@ namespace Ryujinx.HLE.HOS
|
|||
continue;
|
||||
}
|
||||
|
||||
modLoadResult.Npdm = new Npdm(npdmFile.OpenRead());
|
||||
modLoadResult.Npdm = new MetaLoader();
|
||||
modLoadResult.Npdm.Load(File.ReadAllBytes(npdmFile.FullName));
|
||||
|
||||
Logger.Info?.Print(LogClass.ModLoader, $"main.npdm replaced");
|
||||
Logger.Info?.Print(LogClass.ModLoader, "main.npdm replaced");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
using ARMeilleure.Translation.PTC;
|
||||
using LibHac.Loader;
|
||||
using LibHac.Ncm;
|
||||
using LibHac.Util;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Kernel;
|
||||
|
@ -6,12 +9,25 @@ using Ryujinx.HLE.HOS.Kernel.Common;
|
|||
using Ryujinx.HLE.HOS.Kernel.Memory;
|
||||
using Ryujinx.HLE.HOS.Kernel.Process;
|
||||
using Ryujinx.HLE.Loaders.Executables;
|
||||
using Ryujinx.HLE.Loaders.Npdm;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Npdm = LibHac.Loader.Npdm;
|
||||
|
||||
namespace Ryujinx.HLE.HOS
|
||||
{
|
||||
struct ProgramInfo
|
||||
{
|
||||
public string Name;
|
||||
public ulong ProgramId;
|
||||
|
||||
public ProgramInfo(in Npdm npdm)
|
||||
{
|
||||
Name = StringUtils.Utf8ZToString(npdm.Meta.Value.ProgramName);
|
||||
ProgramId = npdm.Aci.Value.ProgramId.Value;
|
||||
}
|
||||
}
|
||||
|
||||
static class ProgramLoader
|
||||
{
|
||||
private const bool AslrEnabled = true;
|
||||
|
@ -125,11 +141,21 @@ namespace Ryujinx.HLE.HOS
|
|||
return true;
|
||||
}
|
||||
|
||||
public static bool LoadNsos(KernelContext context, out ProcessTamperInfo tamperInfo, Npdm metaData, byte[] arguments = null, params IExecutable[] executables)
|
||||
public static bool LoadNsos(KernelContext context, out ProcessTamperInfo tamperInfo, MetaLoader metaData, ProgramInfo programInfo, byte[] arguments = null, params IExecutable[] executables)
|
||||
{
|
||||
LibHac.Result rc = metaData.GetNpdm(out var npdm);
|
||||
|
||||
if (rc.IsFailure())
|
||||
{
|
||||
tamperInfo = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
ref readonly var meta = ref npdm.Meta.Value;
|
||||
|
||||
ulong argsStart = 0;
|
||||
uint argsSize = 0;
|
||||
ulong codeStart = metaData.Is64Bit ? 0x8000000UL : 0x200000UL;
|
||||
ulong codeStart = (meta.Flags & 1) != 0 ? 0x8000000UL : 0x200000UL;
|
||||
uint codeSize = 0;
|
||||
|
||||
var buildIds = executables.Select(e => (e switch
|
||||
|
@ -182,18 +208,20 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
int codePagesCount = (int)(codeSize / KPageTableBase.PageSize);
|
||||
|
||||
int personalMmHeapPagesCount = metaData.PersonalMmHeapSize / KPageTableBase.PageSize;
|
||||
int personalMmHeapPagesCount = (int)(meta.SystemResourceSize / KPageTableBase.PageSize);
|
||||
|
||||
ProcessCreationInfo creationInfo = new ProcessCreationInfo(
|
||||
metaData.TitleName,
|
||||
metaData.Version,
|
||||
metaData.Aci0.TitleId,
|
||||
programInfo.Name,
|
||||
(int)meta.Version,
|
||||
programInfo.ProgramId,
|
||||
codeStart,
|
||||
codePagesCount,
|
||||
(ProcessCreationFlags)metaData.ProcessFlags | ProcessCreationFlags.IsApplication,
|
||||
(ProcessCreationFlags)meta.Flags | ProcessCreationFlags.IsApplication,
|
||||
0,
|
||||
personalMmHeapPagesCount);
|
||||
|
||||
context.Device.System.LibHacHorizonManager.InitializeApplicationClient(new ProgramId(programInfo.ProgramId), in npdm);
|
||||
|
||||
KernelResult result;
|
||||
|
||||
KResourceLimit resourceLimit = new KResourceLimit(context);
|
||||
|
@ -217,7 +245,7 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
KProcess process = new KProcess(context);
|
||||
|
||||
MemoryRegion memoryRegion = (MemoryRegion)((metaData.Acid.Flags >> 2) & 0xf);
|
||||
MemoryRegion memoryRegion = (MemoryRegion)((npdm.Acid.Value.Flags >> 2) & 0xf);
|
||||
|
||||
if (memoryRegion > MemoryRegion.NvServices)
|
||||
{
|
||||
|
@ -232,7 +260,7 @@ namespace Ryujinx.HLE.HOS
|
|||
|
||||
result = process.Initialize(
|
||||
creationInfo,
|
||||
metaData.Aci0.KernelAccessControl.Capabilities,
|
||||
MemoryMarshal.Cast<byte, int>(npdm.KernelCapabilityData).ToArray(),
|
||||
resourceLimit,
|
||||
memoryRegion,
|
||||
processContextFactory);
|
||||
|
@ -262,9 +290,9 @@ namespace Ryujinx.HLE.HOS
|
|||
}
|
||||
}
|
||||
|
||||
process.DefaultCpuCore = metaData.DefaultCpuId;
|
||||
process.DefaultCpuCore = meta.DefaultCpuId;
|
||||
|
||||
result = process.Start(metaData.MainThreadPriority, (ulong)metaData.MainThreadStackSize);
|
||||
result = process.Start(meta.MainThreadPriority, meta.MainThreadStackSize);
|
||||
|
||||
if (result != KernelResult.Success)
|
||||
{
|
||||
|
|
|
@ -2,12 +2,9 @@
|
|||
using LibHac.Fs;
|
||||
using LibHac.Fs.Shim;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.FileSystem.Content;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Account.Acc
|
||||
|
@ -16,16 +13,20 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
|
|||
{
|
||||
public static readonly UserId DefaultUserId = new UserId("00000000000000010000000000000000");
|
||||
|
||||
private readonly VirtualFileSystem _virtualFileSystem;
|
||||
private readonly AccountSaveDataManager _accountSaveDataManager;
|
||||
|
||||
// Todo: The account service doesn't have the permissions to delete save data. Qlaunch takes care of deleting
|
||||
// save data, so we're currently passing a client with full permissions. Consider moving save data deletion
|
||||
// outside of the AccountManager.
|
||||
private readonly HorizonClient _horizonClient;
|
||||
|
||||
private ConcurrentDictionary<string, UserProfile> _profiles;
|
||||
|
||||
public UserProfile LastOpenedUser { get; private set; }
|
||||
|
||||
public AccountManager(VirtualFileSystem virtualFileSystem)
|
||||
public AccountManager(HorizonClient horizonClient)
|
||||
{
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_horizonClient = horizonClient;
|
||||
|
||||
_profiles = new ConcurrentDictionary<string, UserProfile>();
|
||||
|
||||
|
@ -169,31 +170,22 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
|
|||
SaveDataFilter saveDataFilter = new SaveDataFilter();
|
||||
saveDataFilter.SetUserId(new LibHac.Fs.UserId((ulong)userId.High, (ulong)userId.Low));
|
||||
|
||||
Result result = _virtualFileSystem.FsClient.OpenSaveDataIterator(out SaveDataIterator saveDataIterator, SaveDataSpaceId.User, ref saveDataFilter);
|
||||
if (result.IsSuccess())
|
||||
_horizonClient.Fs.OpenSaveDataIterator(out SaveDataIterator saveDataIterator, SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
|
||||
|
||||
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
|
||||
|
||||
while (true)
|
||||
{
|
||||
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
|
||||
saveDataIterator.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure();
|
||||
|
||||
while (true)
|
||||
if (readCount == 0)
|
||||
{
|
||||
saveDataIterator.ReadSaveDataInfo(out long readCount, saveDataInfo);
|
||||
break;
|
||||
}
|
||||
|
||||
if (readCount == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < readCount; i++)
|
||||
{
|
||||
// TODO: We use Directory.Delete workaround because DeleteSaveData softlock without, due to a bug in LibHac 0.12.0.
|
||||
string savePath = Path.Combine(_virtualFileSystem.GetNandPath(), $"user/save/{saveDataInfo[i].SaveDataId:x16}");
|
||||
string saveMetaPath = Path.Combine(_virtualFileSystem.GetNandPath(), $"user/saveMeta/{saveDataInfo[i].SaveDataId:x16}");
|
||||
|
||||
Directory.Delete(savePath, true);
|
||||
Directory.Delete(saveMetaPath, true);
|
||||
|
||||
_virtualFileSystem.FsClient.DeleteSaveData(SaveDataSpaceId.User, saveDataInfo[i].SaveDataId);
|
||||
}
|
||||
for (int i = 0; i < readCount; i++)
|
||||
{
|
||||
_horizonClient.Fs.DeleteSaveData(SaveDataSpaceId.User, saveDataInfo[i].SaveDataId).ThrowIfFailure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
|||
private int _notificationStorageChannelEventHandle;
|
||||
private int _healthWarningDisappearedSystemEventHandle;
|
||||
|
||||
private HorizonClient _horizon;
|
||||
|
||||
public IApplicationFunctions(Horizon system)
|
||||
{
|
||||
// TODO: Find where they are signaled.
|
||||
|
@ -44,6 +46,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
|||
_friendInvitationStorageChannelEvent = new KEvent(system.KernelContext);
|
||||
_notificationStorageChannelEvent = new KEvent(system.KernelContext);
|
||||
_healthWarningDisappearedSystemEvent = new KEvent(system.KernelContext);
|
||||
|
||||
_horizon = system.LibHacHorizonManager.AmClient;
|
||||
}
|
||||
|
||||
[CommandHipc(1)]
|
||||
|
@ -103,14 +107,16 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
|||
// EnsureSaveData(nn::account::Uid) -> u64
|
||||
public ResultCode EnsureSaveData(ServiceCtx context)
|
||||
{
|
||||
Uid userId = context.RequestData.ReadStruct<AccountUid>().ToLibHacUid();
|
||||
ApplicationId applicationId = new ApplicationId(context.Process.TitleId);
|
||||
Uid userId = context.RequestData.ReadStruct<AccountUid>().ToLibHacUid();
|
||||
|
||||
// Mask out the low nibble of the program ID to get the application ID
|
||||
ApplicationId applicationId = new ApplicationId(context.Device.Application.TitleId & ~0xFul);
|
||||
|
||||
BlitStruct<ApplicationControlProperty> controlHolder = context.Device.Application.ControlData;
|
||||
|
||||
ref ApplicationControlProperty control = ref controlHolder.Value;
|
||||
|
||||
if (LibHac.Utilities.IsEmpty(controlHolder.ByteSpan))
|
||||
if (LibHac.Utilities.IsZeros(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.
|
||||
|
@ -124,7 +130,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
|||
"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.FileSystem.FsClient, out long requiredSize, applicationId, ref control, ref userId);
|
||||
HorizonClient hos = context.Device.System.LibHacHorizonManager.AmClient;
|
||||
Result result = EnsureApplicationSaveData(hos.Fs, out long requiredSize, applicationId, ref control, ref userId);
|
||||
|
||||
context.ResponseData.Write(requiredSize);
|
||||
|
||||
|
@ -195,7 +202,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
|||
public ResultCode ExtendSaveData(ServiceCtx context)
|
||||
{
|
||||
SaveDataType saveDataType = (SaveDataType)context.RequestData.ReadUInt64();
|
||||
Uid userId = context.RequestData.ReadStruct<AccountUid>().ToLibHacUid();
|
||||
Uid userId = context.RequestData.ReadStruct<Uid>();
|
||||
ulong saveDataSize = context.RequestData.ReadUInt64();
|
||||
ulong journalSize = context.RequestData.ReadUInt64();
|
||||
|
||||
|
@ -217,7 +224,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
|||
public ResultCode GetSaveDataSize(ServiceCtx context)
|
||||
{
|
||||
SaveDataType saveDataType = (SaveDataType)context.RequestData.ReadUInt64();
|
||||
Uid userId = context.RequestData.ReadStruct<AccountUid>().ToLibHacUid();
|
||||
Uid userId = context.RequestData.ReadStruct<Uid>();
|
||||
|
||||
// NOTE: Service calls nn::fs::FindSaveDataWithFilter with SaveDataType = 1 hardcoded.
|
||||
// Then it calls nn::fs::GetSaveDataAvailableSize and nn::fs::GetSaveDataJournalSize to get the sizes.
|
||||
|
@ -231,6 +238,31 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
|||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandHipc(27)] // 5.0.0+
|
||||
// CreateCacheStorage(u16 index, s64 save_size, s64 journal_size) -> (u32 storageTarget, u64 requiredSize)
|
||||
public ResultCode CreateCacheStorage(ServiceCtx context)
|
||||
{
|
||||
ushort index = (ushort)context.RequestData.ReadUInt64();
|
||||
long saveSize = context.RequestData.ReadInt64();
|
||||
long journalSize = context.RequestData.ReadInt64();
|
||||
|
||||
// Mask out the low nibble of the program ID to get the application ID
|
||||
ApplicationId applicationId = new ApplicationId(context.Device.Application.TitleId & ~0xFul);
|
||||
|
||||
BlitStruct<ApplicationControlProperty> controlHolder = context.Device.Application.ControlData;
|
||||
|
||||
Result result = _horizon.Fs.CreateApplicationCacheStorage(out long requiredSize,
|
||||
out CacheStorageTargetMedia storageTarget, applicationId, ref controlHolder.Value, index, saveSize,
|
||||
journalSize);
|
||||
|
||||
if (result.IsFailure()) return (ResultCode)result.Value;
|
||||
|
||||
context.ResponseData.Write((ulong)storageTarget);
|
||||
context.ResponseData.Write(requiredSize);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandHipc(30)]
|
||||
// BeginBlockingHomeButtonShortAndLongPressed()
|
||||
public ResultCode BeginBlockingHomeButtonShortAndLongPressed(ServiceCtx context)
|
||||
|
@ -517,7 +549,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
|||
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_gpuErrorDetectedSystemEventHandle);
|
||||
|
||||
// NOTE: This is used by "sdk" NSO during applet-application initialization.
|
||||
// A seperate thread is setup where event-waiting is handled.
|
||||
// A separate thread is setup where event-waiting is handled.
|
||||
// When the Event is signaled, official sw will assert.
|
||||
|
||||
return ResultCode.Success;
|
||||
|
|
|
@ -9,19 +9,14 @@ namespace Ryujinx.HLE.HOS.Services.Arp
|
|||
{
|
||||
class LibHacIReader : LibHac.Arp.Impl.IReader
|
||||
{
|
||||
private Horizon System { get; }
|
||||
|
||||
public LibHacIReader(Horizon system)
|
||||
{
|
||||
System = system;
|
||||
}
|
||||
public ApplicationId ApplicationId { get; set; }
|
||||
|
||||
public Result GetApplicationLaunchProperty(out LibHac.Arp.ApplicationLaunchProperty launchProperty, ulong processId)
|
||||
{
|
||||
launchProperty = new LibHac.Arp.ApplicationLaunchProperty
|
||||
{
|
||||
BaseStorageId = StorageId.BuiltInUser,
|
||||
ApplicationId = new ApplicationId(System.Device.Application.TitleId)
|
||||
ApplicationId = ApplicationId
|
||||
};
|
||||
|
||||
return Result.Success;
|
||||
|
@ -47,5 +42,27 @@ namespace Ryujinx.HLE.HOS.Services.Arp
|
|||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Result GetServiceObject(out object serviceObject)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal class LibHacArpServiceObject : LibHac.Sm.IServiceObject
|
||||
{
|
||||
private LibHacIReader _serviceObject;
|
||||
|
||||
public LibHacArpServiceObject(LibHacIReader serviceObject)
|
||||
{
|
||||
_serviceObject = serviceObject;
|
||||
}
|
||||
|
||||
public Result GetServiceObject(out object serviceObject)
|
||||
{
|
||||
serviceObject = _serviceObject;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,11 +11,12 @@ namespace Ryujinx.HLE.HOS.Services.Bcat
|
|||
[Service("bcat:s", "bcat:s")]
|
||||
class IServiceCreator : IpcService
|
||||
{
|
||||
private LibHac.Bcat.Detail.Ipc.IServiceCreator _base;
|
||||
private LibHac.Bcat.Impl.Ipc.IServiceCreator _base;
|
||||
|
||||
public IServiceCreator(ServiceCtx context, string serviceName)
|
||||
{
|
||||
context.Device.System.LibHacHorizonClient.Sm.GetService(out _base, serviceName).ThrowIfFailure();
|
||||
var applicationClient = context.Device.System.LibHacHorizonManager.ApplicationClient;
|
||||
applicationClient.Sm.GetService(out _base, serviceName).ThrowIfFailure();
|
||||
}
|
||||
|
||||
[CommandHipc(0)]
|
||||
|
@ -42,7 +43,7 @@ namespace Ryujinx.HLE.HOS.Services.Bcat
|
|||
{
|
||||
ulong pid = context.RequestData.ReadUInt64();
|
||||
|
||||
Result rc = _base.CreateDeliveryCacheStorageService(out LibHac.Bcat.Detail.Ipc.IDeliveryCacheStorageService serv, pid);
|
||||
Result rc = _base.CreateDeliveryCacheStorageService(out LibHac.Bcat.Impl.Ipc.IDeliveryCacheStorageService serv, pid);
|
||||
|
||||
if (rc.IsSuccess())
|
||||
{
|
||||
|
@ -58,7 +59,7 @@ namespace Ryujinx.HLE.HOS.Services.Bcat
|
|||
{
|
||||
ApplicationId applicationId = context.RequestData.ReadStruct<ApplicationId>();
|
||||
|
||||
Result rc = _base.CreateDeliveryCacheStorageServiceWithApplicationId(out LibHac.Bcat.Detail.Ipc.IDeliveryCacheStorageService serv,
|
||||
Result rc = _base.CreateDeliveryCacheStorageServiceWithApplicationId(out LibHac.Bcat.Impl.Ipc.IDeliveryCacheStorageService serv,
|
||||
applicationId);
|
||||
|
||||
if (rc.IsSuccess())
|
||||
|
|
|
@ -7,9 +7,9 @@ namespace Ryujinx.HLE.HOS.Services.Bcat.ServiceCreator
|
|||
{
|
||||
class IDeliveryCacheDirectoryService : DisposableIpcService
|
||||
{
|
||||
private LibHac.Bcat.Detail.Ipc.IDeliveryCacheDirectoryService _base;
|
||||
private LibHac.Bcat.Impl.Ipc.IDeliveryCacheDirectoryService _base;
|
||||
|
||||
public IDeliveryCacheDirectoryService(LibHac.Bcat.Detail.Ipc.IDeliveryCacheDirectoryService baseService)
|
||||
public IDeliveryCacheDirectoryService(LibHac.Bcat.Impl.Ipc.IDeliveryCacheDirectoryService baseService)
|
||||
{
|
||||
_base = baseService;
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ namespace Ryujinx.HLE.HOS.Services.Bcat.ServiceCreator
|
|||
{
|
||||
class IDeliveryCacheFileService : DisposableIpcService
|
||||
{
|
||||
private LibHac.Bcat.Detail.Ipc.IDeliveryCacheFileService _base;
|
||||
private LibHac.Bcat.Impl.Ipc.IDeliveryCacheFileService _base;
|
||||
|
||||
public IDeliveryCacheFileService(LibHac.Bcat.Detail.Ipc.IDeliveryCacheFileService baseService)
|
||||
public IDeliveryCacheFileService(LibHac.Bcat.Impl.Ipc.IDeliveryCacheFileService baseService)
|
||||
{
|
||||
_base = baseService;
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ namespace Ryujinx.HLE.HOS.Services.Bcat.ServiceCreator
|
|||
{
|
||||
class IDeliveryCacheStorageService : DisposableIpcService
|
||||
{
|
||||
private LibHac.Bcat.Detail.Ipc.IDeliveryCacheStorageService _base;
|
||||
private LibHac.Bcat.Impl.Ipc.IDeliveryCacheStorageService _base;
|
||||
|
||||
public IDeliveryCacheStorageService(ServiceCtx context, LibHac.Bcat.Detail.Ipc.IDeliveryCacheStorageService baseService)
|
||||
public IDeliveryCacheStorageService(ServiceCtx context, LibHac.Bcat.Impl.Ipc.IDeliveryCacheStorageService baseService)
|
||||
{
|
||||
_base = baseService;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ namespace Ryujinx.HLE.HOS.Services.Bcat.ServiceCreator
|
|||
// CreateFileService() -> object<nn::bcat::detail::ipc::IDeliveryCacheFileService>
|
||||
public ResultCode CreateFileService(ServiceCtx context)
|
||||
{
|
||||
Result result = _base.CreateFileService(out LibHac.Bcat.Detail.Ipc.IDeliveryCacheFileService service);
|
||||
Result result = _base.CreateFileService(out LibHac.Bcat.Impl.Ipc.IDeliveryCacheFileService service);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
|
@ -31,7 +31,7 @@ namespace Ryujinx.HLE.HOS.Services.Bcat.ServiceCreator
|
|||
// CreateDirectoryService() -> object<nn::bcat::detail::ipc::IDeliveryCacheDirectoryService>
|
||||
public ResultCode CreateDirectoryService(ServiceCtx context)
|
||||
{
|
||||
Result result = _base.CreateDirectoryService(out LibHac.Bcat.Detail.Ipc.IDeliveryCacheDirectoryService service);
|
||||
Result result = _base.CreateDirectoryService(out LibHac.Bcat.Impl.Ipc.IDeliveryCacheDirectoryService service);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
using LibHac;
|
||||
using LibHac.Common;
|
||||
using LibHac.Common.Keys;
|
||||
using LibHac.Fs;
|
||||
using LibHac.FsSrv.Impl;
|
||||
using LibHac.FsSrv.Sf;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.FsSystem.NcaUtils;
|
||||
using LibHac.Spl;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
||||
{
|
||||
|
@ -16,12 +22,12 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
|
||||
try
|
||||
{
|
||||
LocalStorage storage = new LocalStorage(pfsPath, FileAccess.Read, FileMode.Open);
|
||||
PartitionFileSystem nsp = new PartitionFileSystem(storage);
|
||||
LocalStorage storage = new LocalStorage(pfsPath, FileAccess.Read, FileMode.Open);
|
||||
ReferenceCountedDisposable<LibHac.Fs.Fsa.IFileSystem> nsp = new(new PartitionFileSystem(storage));
|
||||
|
||||
ImportTitleKeysFromNsp(nsp, context.Device.System.KeySet);
|
||||
ImportTitleKeysFromNsp(nsp.Target, context.Device.System.KeySet);
|
||||
|
||||
openedFileSystem = new IFileSystem(nsp);
|
||||
openedFileSystem = new IFileSystem(FileSystemInterfaceAdapter.CreateShared(ref nsp));
|
||||
}
|
||||
catch (HorizonResultException ex)
|
||||
{
|
||||
|
@ -45,8 +51,9 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
}
|
||||
|
||||
LibHac.Fs.Fsa.IFileSystem fileSystem = nca.OpenFileSystem(NcaSectionType.Data, context.Device.System.FsIntegrityCheckLevel);
|
||||
var sharedFs = new ReferenceCountedDisposable<LibHac.Fs.Fsa.IFileSystem>(fileSystem);
|
||||
|
||||
openedFileSystem = new IFileSystem(fileSystem);
|
||||
openedFileSystem = new IFileSystem(FileSystemInterfaceAdapter.CreateShared(ref sharedFs));
|
||||
}
|
||||
catch (HorizonResultException ex)
|
||||
{
|
||||
|
@ -99,7 +106,7 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
return ResultCode.PathDoesNotExist;
|
||||
}
|
||||
|
||||
public static void ImportTitleKeysFromNsp(LibHac.Fs.Fsa.IFileSystem nsp, Keyset keySet)
|
||||
public static void ImportTitleKeysFromNsp(LibHac.Fs.Fsa.IFileSystem nsp, KeySet keySet)
|
||||
{
|
||||
foreach (DirectoryEntryEx ticketEntry in nsp.EnumerateEntries("/", "*.tik"))
|
||||
{
|
||||
|
@ -125,5 +132,27 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
|
||||
return FsPath.FromSpan(out path, pathBytes);
|
||||
}
|
||||
|
||||
public static ref readonly FspPath GetFspPath(ServiceCtx context, int index = 0)
|
||||
{
|
||||
ulong position = (ulong)context.Request.PtrBuff[index].Position;
|
||||
ulong size = (ulong)context.Request.PtrBuff[index].Size;
|
||||
|
||||
ReadOnlySpan<byte> buffer = context.Memory.GetSpan(position, (int)size);
|
||||
ReadOnlySpan<FspPath> fspBuffer = MemoryMarshal.Cast<byte, FspPath>(buffer);
|
||||
|
||||
return ref fspBuffer[0];
|
||||
}
|
||||
|
||||
public static ref readonly LibHac.FsSrv.Sf.Path GetSfPath(ServiceCtx context, int index = 0)
|
||||
{
|
||||
ulong position = (ulong)context.Request.PtrBuff[index].Position;
|
||||
ulong size = (ulong)context.Request.PtrBuff[index].Size;
|
||||
|
||||
ReadOnlySpan<byte> buffer = context.Memory.GetSpan(position, (int)size);
|
||||
ReadOnlySpan<LibHac.FsSrv.Sf.Path> pathBuffer = MemoryMarshal.Cast<byte, LibHac.FsSrv.Sf.Path>(buffer);
|
||||
|
||||
return ref pathBuffer[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
using LibHac;
|
||||
using LibHac.Fs;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using LibHac.Sf;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
||||
{
|
||||
class IDirectory : IpcService
|
||||
class IDirectory : DisposableIpcService
|
||||
{
|
||||
private LibHac.Fs.Fsa.IDirectory _baseDirectory;
|
||||
private ReferenceCountedDisposable<LibHac.FsSrv.Sf.IDirectory> _baseDirectory;
|
||||
|
||||
public IDirectory(LibHac.Fs.Fsa.IDirectory directory)
|
||||
public IDirectory(ReferenceCountedDisposable<LibHac.FsSrv.Sf.IDirectory> directory)
|
||||
{
|
||||
_baseDirectory = directory;
|
||||
}
|
||||
|
@ -19,14 +17,13 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
public ResultCode Read(ServiceCtx context)
|
||||
{
|
||||
ulong bufferPosition = context.Request.ReceiveBuff[0].Position;
|
||||
ulong bufferLen = context.Request.ReceiveBuff[0].Size;
|
||||
ulong bufferLen = context.Request.ReceiveBuff[0].Size;
|
||||
|
||||
byte[] entriesBytes = new byte[bufferLen];
|
||||
Span<DirectoryEntry> entries = MemoryMarshal.Cast<byte, DirectoryEntry>(entriesBytes);
|
||||
byte[] entryBuffer = new byte[bufferLen];
|
||||
|
||||
Result result = _baseDirectory.Read(out long entriesRead, entries);
|
||||
Result result = _baseDirectory.Target.Read(out long entriesRead, new OutBuffer(entryBuffer));
|
||||
|
||||
context.Memory.Write(bufferPosition, entriesBytes);
|
||||
context.Memory.Write(bufferPosition, entryBuffer);
|
||||
context.ResponseData.Write(entriesRead);
|
||||
|
||||
return (ResultCode)result.Value;
|
||||
|
@ -36,11 +33,19 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
// GetEntryCount() -> u64
|
||||
public ResultCode GetEntryCount(ServiceCtx context)
|
||||
{
|
||||
Result result = _baseDirectory.GetEntryCount(out long entryCount);
|
||||
Result result = _baseDirectory.Target.GetEntryCount(out long entryCount);
|
||||
|
||||
context.ResponseData.Write(entryCount);
|
||||
|
||||
return (ResultCode)result.Value;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (isDisposing)
|
||||
{
|
||||
_baseDirectory?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
using LibHac;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Sf;
|
||||
using Ryujinx.Common;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
||||
{
|
||||
class IFile : DisposableIpcService
|
||||
{
|
||||
private LibHac.Fs.Fsa.IFile _baseFile;
|
||||
private ReferenceCountedDisposable<LibHac.FsSrv.Sf.IFile> _baseFile;
|
||||
|
||||
public IFile(LibHac.Fs.Fsa.IFile baseFile)
|
||||
public IFile(ReferenceCountedDisposable<LibHac.FsSrv.Sf.IFile> baseFile)
|
||||
{
|
||||
_baseFile = baseFile;
|
||||
}
|
||||
|
@ -18,15 +20,15 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
{
|
||||
ulong position = context.Request.ReceiveBuff[0].Position;
|
||||
|
||||
ReadOption readOption = new ReadOption(context.RequestData.ReadInt32());
|
||||
ReadOption readOption = context.RequestData.ReadStruct<ReadOption>();
|
||||
context.RequestData.BaseStream.Position += 4;
|
||||
|
||||
long offset = context.RequestData.ReadInt64();
|
||||
long size = context.RequestData.ReadInt64();
|
||||
|
||||
byte[] data = new byte[size];
|
||||
byte[] data = new byte[context.Request.ReceiveBuff[0].Size];
|
||||
|
||||
Result result = _baseFile.Read(out long bytesRead, offset, data, readOption);
|
||||
Result result = _baseFile.Target.Read(out long bytesRead, offset, new OutBuffer(data), size, readOption);
|
||||
|
||||
context.Memory.Write(position, data);
|
||||
|
||||
|
@ -41,24 +43,24 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
{
|
||||
ulong position = context.Request.SendBuff[0].Position;
|
||||
|
||||
WriteOption writeOption = new WriteOption(context.RequestData.ReadInt32());
|
||||
WriteOption writeOption = context.RequestData.ReadStruct<WriteOption>();
|
||||
context.RequestData.BaseStream.Position += 4;
|
||||
|
||||
long offset = context.RequestData.ReadInt64();
|
||||
long size = context.RequestData.ReadInt64();
|
||||
|
||||
byte[] data = new byte[size];
|
||||
byte[] data = new byte[context.Request.SendBuff[0].Size];
|
||||
|
||||
context.Memory.Read(position, data);
|
||||
|
||||
return (ResultCode)_baseFile.Write(offset, data, writeOption).Value;
|
||||
return (ResultCode)_baseFile.Target.Write(offset, new InBuffer(data), size, writeOption).Value;
|
||||
}
|
||||
|
||||
[CommandHipc(2)]
|
||||
// Flush()
|
||||
public ResultCode Flush(ServiceCtx context)
|
||||
{
|
||||
return (ResultCode)_baseFile.Flush().Value;
|
||||
return (ResultCode)_baseFile.Target.Flush().Value;
|
||||
}
|
||||
|
||||
[CommandHipc(3)]
|
||||
|
@ -67,14 +69,14 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
{
|
||||
long size = context.RequestData.ReadInt64();
|
||||
|
||||
return (ResultCode)_baseFile.SetSize(size).Value;
|
||||
return (ResultCode)_baseFile.Target.SetSize(size).Value;
|
||||
}
|
||||
|
||||
[CommandHipc(4)]
|
||||
// GetSize() -> u64 fileSize
|
||||
public ResultCode GetSize(ServiceCtx context)
|
||||
{
|
||||
Result result = _baseFile.GetSize(out long size);
|
||||
Result result = _baseFile.Target.GetSize(out long size);
|
||||
|
||||
context.ResponseData.Write(size);
|
||||
|
||||
|
|
|
@ -1,21 +1,19 @@
|
|||
using LibHac;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using static Ryujinx.HLE.Utilities.StringUtils;
|
||||
using LibHac.FsSrv.Sf;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
||||
{
|
||||
class IFileSystem : IpcService
|
||||
class IFileSystem : DisposableIpcService
|
||||
{
|
||||
private LibHac.Fs.Fsa.IFileSystem _fileSystem;
|
||||
private ReferenceCountedDisposable<LibHac.FsSrv.Sf.IFileSystem> _fileSystem;
|
||||
|
||||
public IFileSystem(LibHac.Fs.Fsa.IFileSystem provider)
|
||||
public IFileSystem(ReferenceCountedDisposable<LibHac.FsSrv.Sf.IFileSystem> provider)
|
||||
{
|
||||
_fileSystem = provider;
|
||||
}
|
||||
|
||||
public LibHac.Fs.Fsa.IFileSystem GetBaseFileSystem()
|
||||
public ReferenceCountedDisposable<LibHac.FsSrv.Sf.IFileSystem> GetBaseFileSystem()
|
||||
{
|
||||
return _fileSystem;
|
||||
}
|
||||
|
@ -24,79 +22,79 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
// CreateFile(u32 createOption, u64 size, buffer<bytes<0x301>, 0x19, 0x301> path)
|
||||
public ResultCode CreateFile(ServiceCtx context)
|
||||
{
|
||||
U8Span name = ReadUtf8Span(context);
|
||||
ref readonly Path name = ref FileSystemProxyHelper.GetSfPath(context);
|
||||
|
||||
CreateFileOptions createOption = (CreateFileOptions)context.RequestData.ReadInt32();
|
||||
int createOption = context.RequestData.ReadInt32();
|
||||
context.RequestData.BaseStream.Position += 4;
|
||||
|
||||
long size = context.RequestData.ReadInt64();
|
||||
|
||||
return (ResultCode)_fileSystem.CreateFile(name, size, createOption).Value;
|
||||
return (ResultCode)_fileSystem.Target.CreateFile(in name, size, createOption).Value;
|
||||
}
|
||||
|
||||
[CommandHipc(1)]
|
||||
// DeleteFile(buffer<bytes<0x301>, 0x19, 0x301> path)
|
||||
public ResultCode DeleteFile(ServiceCtx context)
|
||||
{
|
||||
U8Span name = ReadUtf8Span(context);
|
||||
ref readonly Path name = ref FileSystemProxyHelper.GetSfPath(context);
|
||||
|
||||
return (ResultCode)_fileSystem.DeleteFile(name).Value;
|
||||
return (ResultCode)_fileSystem.Target.DeleteFile(in name).Value;
|
||||
}
|
||||
|
||||
[CommandHipc(2)]
|
||||
// CreateDirectory(buffer<bytes<0x301>, 0x19, 0x301> path)
|
||||
public ResultCode CreateDirectory(ServiceCtx context)
|
||||
{
|
||||
U8Span name = ReadUtf8Span(context);
|
||||
ref readonly Path name = ref FileSystemProxyHelper.GetSfPath(context);
|
||||
|
||||
return (ResultCode)_fileSystem.CreateDirectory(name).Value;
|
||||
return (ResultCode)_fileSystem.Target.CreateDirectory(in name).Value;
|
||||
}
|
||||
|
||||
[CommandHipc(3)]
|
||||
// DeleteDirectory(buffer<bytes<0x301>, 0x19, 0x301> path)
|
||||
public ResultCode DeleteDirectory(ServiceCtx context)
|
||||
{
|
||||
U8Span name = ReadUtf8Span(context);
|
||||
ref readonly Path name = ref FileSystemProxyHelper.GetSfPath(context);
|
||||
|
||||
return (ResultCode)_fileSystem.DeleteDirectory(name).Value;
|
||||
return (ResultCode)_fileSystem.Target.DeleteDirectory(in name).Value;
|
||||
}
|
||||
|
||||
[CommandHipc(4)]
|
||||
// DeleteDirectoryRecursively(buffer<bytes<0x301>, 0x19, 0x301> path)
|
||||
public ResultCode DeleteDirectoryRecursively(ServiceCtx context)
|
||||
{
|
||||
U8Span name = ReadUtf8Span(context);
|
||||
ref readonly Path name = ref FileSystemProxyHelper.GetSfPath(context);
|
||||
|
||||
return (ResultCode)_fileSystem.DeleteDirectoryRecursively(name).Value;
|
||||
return (ResultCode)_fileSystem.Target.DeleteDirectoryRecursively(in name).Value;
|
||||
}
|
||||
|
||||
[CommandHipc(5)]
|
||||
// RenameFile(buffer<bytes<0x301>, 0x19, 0x301> oldPath, buffer<bytes<0x301>, 0x19, 0x301> newPath)
|
||||
public ResultCode RenameFile(ServiceCtx context)
|
||||
{
|
||||
U8Span oldName = ReadUtf8Span(context, 0);
|
||||
U8Span newName = ReadUtf8Span(context, 1);
|
||||
ref readonly Path currentName = ref FileSystemProxyHelper.GetSfPath(context, index: 0);
|
||||
ref readonly Path newName = ref FileSystemProxyHelper.GetSfPath(context, index: 1);
|
||||
|
||||
return (ResultCode)_fileSystem.RenameFile(oldName, newName).Value;
|
||||
return (ResultCode)_fileSystem.Target.RenameFile(in currentName, in newName).Value;
|
||||
}
|
||||
|
||||
[CommandHipc(6)]
|
||||
// RenameDirectory(buffer<bytes<0x301>, 0x19, 0x301> oldPath, buffer<bytes<0x301>, 0x19, 0x301> newPath)
|
||||
public ResultCode RenameDirectory(ServiceCtx context)
|
||||
{
|
||||
U8Span oldName = ReadUtf8Span(context, 0);
|
||||
U8Span newName = ReadUtf8Span(context, 1);
|
||||
ref readonly Path currentName = ref FileSystemProxyHelper.GetSfPath(context, index: 0);
|
||||
ref readonly Path newName = ref FileSystemProxyHelper.GetSfPath(context, index: 1);
|
||||
|
||||
return (ResultCode)_fileSystem.RenameDirectory(oldName, newName).Value;
|
||||
return (ResultCode)_fileSystem.Target.RenameDirectory(in currentName, in newName).Value;
|
||||
}
|
||||
|
||||
[CommandHipc(7)]
|
||||
// GetEntryType(buffer<bytes<0x301>, 0x19, 0x301> path) -> nn::fssrv::sf::DirectoryEntryType
|
||||
public ResultCode GetEntryType(ServiceCtx context)
|
||||
{
|
||||
U8Span name = ReadUtf8Span(context);
|
||||
ref readonly Path name = ref FileSystemProxyHelper.GetSfPath(context);
|
||||
|
||||
Result result = _fileSystem.GetEntryType(out DirectoryEntryType entryType, name);
|
||||
Result result = _fileSystem.Target.GetEntryType(out uint entryType, in name);
|
||||
|
||||
context.ResponseData.Write((int)entryType);
|
||||
|
||||
|
@ -107,11 +105,11 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
// OpenFile(u32 mode, buffer<bytes<0x301>, 0x19, 0x301> path) -> object<nn::fssrv::sf::IFile> file
|
||||
public ResultCode OpenFile(ServiceCtx context)
|
||||
{
|
||||
OpenMode mode = (OpenMode)context.RequestData.ReadInt32();
|
||||
uint mode = context.RequestData.ReadUInt32();
|
||||
|
||||
U8Span name = ReadUtf8Span(context);
|
||||
ref readonly Path name = ref FileSystemProxyHelper.GetSfPath(context);
|
||||
|
||||
Result result = _fileSystem.OpenFile(out LibHac.Fs.Fsa.IFile file, name, mode);
|
||||
Result result = _fileSystem.Target.OpenFile(out ReferenceCountedDisposable<LibHac.FsSrv.Sf.IFile> file, in name, mode);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
|
@ -127,11 +125,11 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
// OpenDirectory(u32 filter_flags, buffer<bytes<0x301>, 0x19, 0x301> path) -> object<nn::fssrv::sf::IDirectory> directory
|
||||
public ResultCode OpenDirectory(ServiceCtx context)
|
||||
{
|
||||
OpenDirectoryMode mode = (OpenDirectoryMode)context.RequestData.ReadInt32();
|
||||
uint mode = context.RequestData.ReadUInt32();
|
||||
|
||||
U8Span name = ReadUtf8Span(context);
|
||||
ref readonly Path name = ref FileSystemProxyHelper.GetSfPath(context);
|
||||
|
||||
Result result = _fileSystem.OpenDirectory(out LibHac.Fs.Fsa.IDirectory dir, name, mode);
|
||||
Result result = _fileSystem.Target.OpenDirectory(out ReferenceCountedDisposable<LibHac.FsSrv.Sf.IDirectory> dir, name, mode);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
|
@ -147,16 +145,16 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
// Commit()
|
||||
public ResultCode Commit(ServiceCtx context)
|
||||
{
|
||||
return (ResultCode)_fileSystem.Commit().Value;
|
||||
return (ResultCode)_fileSystem.Target.Commit().Value;
|
||||
}
|
||||
|
||||
[CommandHipc(11)]
|
||||
// GetFreeSpaceSize(buffer<bytes<0x301>, 0x19, 0x301> path) -> u64 totalFreeSpace
|
||||
public ResultCode GetFreeSpaceSize(ServiceCtx context)
|
||||
{
|
||||
U8Span name = ReadUtf8Span(context);
|
||||
ref readonly Path name = ref FileSystemProxyHelper.GetSfPath(context);
|
||||
|
||||
Result result = _fileSystem.GetFreeSpaceSize(out long size, name);
|
||||
Result result = _fileSystem.Target.GetFreeSpaceSize(out long size, in name);
|
||||
|
||||
context.ResponseData.Write(size);
|
||||
|
||||
|
@ -167,9 +165,9 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
// GetTotalSpaceSize(buffer<bytes<0x301>, 0x19, 0x301> path) -> u64 totalSize
|
||||
public ResultCode GetTotalSpaceSize(ServiceCtx context)
|
||||
{
|
||||
U8Span name = ReadUtf8Span(context);
|
||||
ref readonly Path name = ref FileSystemProxyHelper.GetSfPath(context);
|
||||
|
||||
Result result = _fileSystem.GetTotalSpaceSize(out long size, name);
|
||||
Result result = _fileSystem.Target.GetTotalSpaceSize(out long size, in name);
|
||||
|
||||
context.ResponseData.Write(size);
|
||||
|
||||
|
@ -180,18 +178,18 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
// CleanDirectoryRecursively(buffer<bytes<0x301>, 0x19, 0x301> path)
|
||||
public ResultCode CleanDirectoryRecursively(ServiceCtx context)
|
||||
{
|
||||
U8Span name = ReadUtf8Span(context);
|
||||
ref readonly Path name = ref FileSystemProxyHelper.GetSfPath(context);
|
||||
|
||||
return (ResultCode)_fileSystem.CleanDirectoryRecursively(name).Value;
|
||||
return (ResultCode)_fileSystem.Target.CleanDirectoryRecursively(in name).Value;
|
||||
}
|
||||
|
||||
[CommandHipc(14)]
|
||||
// GetFileTimeStampRaw(buffer<bytes<0x301>, 0x19, 0x301> path) -> bytes<0x20> timestamp
|
||||
public ResultCode GetFileTimeStampRaw(ServiceCtx context)
|
||||
{
|
||||
U8Span name = ReadUtf8Span(context);
|
||||
ref readonly Path name = ref FileSystemProxyHelper.GetSfPath(context);
|
||||
|
||||
Result result = _fileSystem.GetFileTimeStampRaw(out FileTimeStampRaw timestamp, name);
|
||||
Result result = _fileSystem.Target.GetFileTimeStampRaw(out FileTimeStampRaw timestamp, in name);
|
||||
|
||||
context.ResponseData.Write(timestamp.Created);
|
||||
context.ResponseData.Write(timestamp.Modified);
|
||||
|
@ -206,5 +204,13 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
|
||||
return (ResultCode)result.Value;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (isDisposing)
|
||||
{
|
||||
_fileSystem?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
using LibHac;
|
||||
using LibHac.Sf;
|
||||
using Ryujinx.HLE.HOS.Ipc;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
||||
{
|
||||
class IStorage : DisposableIpcService
|
||||
{
|
||||
private LibHac.Fs.IStorage _baseStorage;
|
||||
private ReferenceCountedDisposable<LibHac.FsSrv.Sf.IStorage> _baseStorage;
|
||||
|
||||
public IStorage(LibHac.Fs.IStorage baseStorage)
|
||||
public IStorage(ReferenceCountedDisposable<LibHac.FsSrv.Sf.IStorage> baseStorage)
|
||||
{
|
||||
_baseStorage = baseStorage;
|
||||
}
|
||||
|
@ -31,7 +32,7 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
|
||||
byte[] data = new byte[size];
|
||||
|
||||
Result result = _baseStorage.Read((long)offset, data);
|
||||
Result result = _baseStorage.Target.Read((long)offset, new OutBuffer(data), (long)size);
|
||||
|
||||
context.Memory.Write(buffDesc.Position, data);
|
||||
|
||||
|
@ -45,7 +46,7 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||
// GetSize() -> u64 size
|
||||
public ResultCode GetSize(ServiceCtx context)
|
||||
{
|
||||
Result result = _baseStorage.GetSize(out long size);
|
||||
Result result = _baseStorage.Target.GetSize(out long size);
|
||||
|
||||
context.ResponseData.Write(size);
|
||||
|
||||
|
|
|
@ -3,11 +3,11 @@ using LibHac.FsSrv;
|
|||
|
||||
namespace Ryujinx.HLE.HOS.Services.Fs
|
||||
{
|
||||
class IDeviceOperator : IpcService
|
||||
class IDeviceOperator : DisposableIpcService
|
||||
{
|
||||
private LibHac.FsSrv.IDeviceOperator _baseOperator;
|
||||
private ReferenceCountedDisposable<LibHac.FsSrv.Sf.IDeviceOperator> _baseOperator;
|
||||
|
||||
public IDeviceOperator(LibHac.FsSrv.IDeviceOperator baseOperator)
|
||||
public IDeviceOperator(ReferenceCountedDisposable<LibHac.FsSrv.Sf.IDeviceOperator> baseOperator)
|
||||
{
|
||||
_baseOperator = baseOperator;
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ namespace Ryujinx.HLE.HOS.Services.Fs
|
|||
// IsSdCardInserted() -> b8 is_inserted
|
||||
public ResultCode IsSdCardInserted(ServiceCtx context)
|
||||
{
|
||||
Result result = _baseOperator.IsSdCardInserted(out bool isInserted);
|
||||
Result result = _baseOperator.Target.IsSdCardInserted(out bool isInserted);
|
||||
|
||||
context.ResponseData.Write(isInserted);
|
||||
|
||||
|
@ -27,7 +27,7 @@ namespace Ryujinx.HLE.HOS.Services.Fs
|
|||
// IsGameCardInserted() -> b8 is_inserted
|
||||
public ResultCode IsGameCardInserted(ServiceCtx context)
|
||||
{
|
||||
Result result = _baseOperator.IsGameCardInserted(out bool isInserted);
|
||||
Result result = _baseOperator.Target.IsGameCardInserted(out bool isInserted);
|
||||
|
||||
context.ResponseData.Write(isInserted);
|
||||
|
||||
|
@ -38,11 +38,19 @@ namespace Ryujinx.HLE.HOS.Services.Fs
|
|||
// GetGameCardHandle() -> u32 gamecard_handle
|
||||
public ResultCode GetGameCardHandle(ServiceCtx context)
|
||||
{
|
||||
Result result = _baseOperator.GetGameCardHandle(out GameCardHandle handle);
|
||||
Result result = _baseOperator.Target.GetGameCardHandle(out GameCardHandle handle);
|
||||
|
||||
context.ResponseData.Write(handle.Value);
|
||||
|
||||
return (ResultCode)result.Value;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (isDisposing)
|
||||
{
|
||||
_baseOperator?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,11 +3,11 @@ using Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy;
|
|||
|
||||
namespace Ryujinx.HLE.HOS.Services.Fs
|
||||
{
|
||||
class IMultiCommitManager : IpcService // 6.0.0+
|
||||
class IMultiCommitManager : DisposableIpcService // 6.0.0+
|
||||
{
|
||||
private LibHac.FsSrv.IMultiCommitManager _baseCommitManager;
|
||||
private ReferenceCountedDisposable<LibHac.FsSrv.Sf.IMultiCommitManager> _baseCommitManager;
|
||||
|
||||
public IMultiCommitManager(LibHac.FsSrv.IMultiCommitManager baseCommitManager)
|
||||
public IMultiCommitManager(ReferenceCountedDisposable<LibHac.FsSrv.Sf.IMultiCommitManager> baseCommitManager)
|
||||
{
|
||||
_baseCommitManager = baseCommitManager;
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ namespace Ryujinx.HLE.HOS.Services.Fs
|
|||
{
|
||||
IFileSystem fileSystem = GetObject<IFileSystem>(context, 0);
|
||||
|
||||
Result result = _baseCommitManager.Add(fileSystem.GetBaseFileSystem());
|
||||
Result result = _baseCommitManager.Target.Add(fileSystem.GetBaseFileSystem());
|
||||
|
||||
return (ResultCode)result.Value;
|
||||
}
|
||||
|
@ -27,9 +27,17 @@ namespace Ryujinx.HLE.HOS.Services.Fs
|
|||
// Commit()
|
||||
public ResultCode Commit(ServiceCtx context)
|
||||
{
|
||||
Result result = _baseCommitManager.Commit();
|
||||
Result result = _baseCommitManager.Target.Commit();
|
||||
|
||||
return (ResultCode)result.Value;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (isDisposing)
|
||||
{
|
||||
_baseCommitManager?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
using System;
|
||||
using LibHac;
|
||||
using LibHac;
|
||||
using LibHac.Sf;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Fs
|
||||
{
|
||||
class ISaveDataInfoReader : DisposableIpcService
|
||||
{
|
||||
private ReferenceCountedDisposable<LibHac.FsSrv.ISaveDataInfoReader> _baseReader;
|
||||
private ReferenceCountedDisposable<LibHac.FsSrv.Sf.ISaveDataInfoReader> _baseReader;
|
||||
|
||||
public ISaveDataInfoReader(ReferenceCountedDisposable<LibHac.FsSrv.ISaveDataInfoReader> baseReader)
|
||||
public ISaveDataInfoReader(ReferenceCountedDisposable<LibHac.FsSrv.Sf.ISaveDataInfoReader> baseReader)
|
||||
{
|
||||
_baseReader = baseReader;
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Fs
|
|||
|
||||
byte[] infoBuffer = new byte[bufferLen];
|
||||
|
||||
Result result = _baseReader.Target.Read(out long readCount, infoBuffer);
|
||||
Result result = _baseReader.Target.Read(out long readCount, new OutBuffer(infoBuffer));
|
||||
|
||||
context.Memory.Write(bufferPosition, infoBuffer);
|
||||
context.ResponseData.Write(readCount);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using Ryujinx.HLE.HOS.Services.Mii.Types;
|
||||
using LibHac;
|
||||
using Ryujinx.HLE.HOS.Services.Mii.Types;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Mii
|
||||
|
@ -147,9 +148,9 @@ namespace Ryujinx.HLE.HOS.Services.Mii
|
|||
return GetDefault(flag, ref count, elements);
|
||||
}
|
||||
|
||||
public ResultCode InitializeDatabase(Switch device)
|
||||
public ResultCode InitializeDatabase(HorizonClient horizonClient)
|
||||
{
|
||||
_miiDatabase.InitializeDatabase(device);
|
||||
_miiDatabase.InitializeDatabase(horizonClient);
|
||||
_miiDatabase.LoadFromFile(out _isBroken);
|
||||
|
||||
// Nintendo ignore any error code from before
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
using LibHac;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.Fs.Shim;
|
||||
using LibHac.Ncm;
|
||||
using Ryujinx.HLE.HOS.Services.Mii.Types;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
|
@ -14,8 +16,6 @@ namespace Ryujinx.HLE.HOS.Services.Mii
|
|||
|
||||
private const ulong DatabaseTestSaveDataId = 0x8000000000000031;
|
||||
private const ulong DatabaseSaveDataId = 0x8000000000000030;
|
||||
private const ulong NsTitleId = 0x010000000000001F;
|
||||
private const ulong SdbTitleId = 0x0100000000000039;
|
||||
|
||||
private static U8String DatabasePath = new U8String("mii:/MiiDatabase.dat");
|
||||
private static U8String MountName = new U8String("mii");
|
||||
|
@ -23,7 +23,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii
|
|||
private NintendoFigurineDatabase _database;
|
||||
private bool _isDirty;
|
||||
|
||||
private FileSystemClient _filesystemClient;
|
||||
private HorizonClient _horizonClient;
|
||||
|
||||
protected ulong UpdateCounter { get; private set; }
|
||||
|
||||
|
@ -94,74 +94,62 @@ namespace Ryujinx.HLE.HOS.Services.Mii
|
|||
return virtualIndex;
|
||||
}
|
||||
|
||||
public void InitializeDatabase(Switch device)
|
||||
public void InitializeDatabase(HorizonClient horizonClient)
|
||||
{
|
||||
_filesystemClient = device.FileSystem.FsClient;
|
||||
_horizonClient = horizonClient;
|
||||
|
||||
// Ensure we have valid data in the database
|
||||
_database.Format();
|
||||
|
||||
// TODO: Unmount is currently not implemented properly at dispose, implement that and decrement MountCounter.
|
||||
MountCounter = 0;
|
||||
|
||||
MountSave();
|
||||
}
|
||||
|
||||
private Result MountSave()
|
||||
{
|
||||
Result result = Result.Success;
|
||||
|
||||
if (MountCounter == 0)
|
||||
if (MountCounter != 0)
|
||||
{
|
||||
ulong targetSaveDataId;
|
||||
ulong targetTitleId;
|
||||
MountCounter++;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
ulong saveDataId = IsTestModeEnabled ? DatabaseTestSaveDataId : DatabaseSaveDataId;
|
||||
|
||||
Result result = _horizonClient.Fs.MountSystemSaveData(MountName, SaveDataSpaceId.System, saveDataId);
|
||||
|
||||
if (result.IsFailure())
|
||||
{
|
||||
if (!ResultFs.TargetNotFound.Includes(result))
|
||||
return result;
|
||||
|
||||
if (IsTestModeEnabled)
|
||||
{
|
||||
targetSaveDataId = DatabaseTestSaveDataId;
|
||||
targetTitleId = SdbTitleId;
|
||||
result = _horizonClient.Fs.CreateSystemSaveData(saveDataId, 0x10000, 0x10000,
|
||||
SaveDataFlags.KeepAfterResettingSystemSaveDataWithoutUserSaveData);
|
||||
if (result.IsFailure()) return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetSaveDataId = DatabaseSaveDataId;
|
||||
|
||||
// Nintendo use NS TitleID when creating the production save even on sdb, let's follow that behaviour.
|
||||
targetTitleId = NsTitleId;
|
||||
result = _horizonClient.Fs.CreateSystemSaveData(saveDataId, SystemProgramId.Ns.Value, 0x10000,
|
||||
0x10000, SaveDataFlags.KeepAfterResettingSystemSaveDataWithoutUserSaveData);
|
||||
if (result.IsFailure()) return result;
|
||||
}
|
||||
|
||||
U8Span mountName = new U8Span(MountName);
|
||||
|
||||
result = _filesystemClient.MountSystemSaveData(mountName, SaveDataSpaceId.System, targetSaveDataId);
|
||||
|
||||
if (result.IsFailure())
|
||||
{
|
||||
if (ResultFs.TargetNotFound.Includes(result))
|
||||
{
|
||||
// TODO: We're currently always specifying the owner ID because FS doesn't have a way of
|
||||
// knowing which process called it
|
||||
result = _filesystemClient.CreateSystemSaveData(targetSaveDataId, targetTitleId, 0x10000,
|
||||
0x10000, SaveDataFlags.KeepAfterResettingSystemSaveDataWithoutUserSaveData);
|
||||
if (result.IsFailure()) return result;
|
||||
|
||||
result = _filesystemClient.MountSystemSaveData(mountName, SaveDataSpaceId.System, targetSaveDataId);
|
||||
if (result.IsFailure()) return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (result == Result.Success)
|
||||
{
|
||||
MountCounter++;
|
||||
}
|
||||
result = _horizonClient.Fs.MountSystemSaveData(MountName, SaveDataSpaceId.System, saveDataId);
|
||||
if (result.IsFailure()) return result;
|
||||
}
|
||||
|
||||
if (result == Result.Success)
|
||||
{
|
||||
MountCounter++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public ResultCode DeleteFile()
|
||||
{
|
||||
ResultCode result = (ResultCode)_filesystemClient.DeleteFile(DatabasePath).Value;
|
||||
ResultCode result = (ResultCode)_horizonClient.Fs.DeleteFile(DatabasePath).Value;
|
||||
|
||||
_filesystemClient.Commit(MountName);
|
||||
_horizonClient.Fs.Commit(MountName);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -179,17 +167,17 @@ namespace Ryujinx.HLE.HOS.Services.Mii
|
|||
|
||||
ResetDatabase();
|
||||
|
||||
Result result = _filesystemClient.OpenFile(out FileHandle handle, DatabasePath, OpenMode.Read);
|
||||
Result result = _horizonClient.Fs.OpenFile(out FileHandle handle, DatabasePath, OpenMode.Read);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
result = _filesystemClient.GetFileSize(out long fileSize, handle);
|
||||
result = _horizonClient.Fs.GetFileSize(out long fileSize, handle);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
if (fileSize == Unsafe.SizeOf<NintendoFigurineDatabase>())
|
||||
{
|
||||
result = _filesystemClient.ReadFile(handle, 0, _database.AsSpan());
|
||||
result = _horizonClient.Fs.ReadFile(handle, 0, _database.AsSpan());
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
|
@ -211,7 +199,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii
|
|||
}
|
||||
}
|
||||
|
||||
_filesystemClient.CloseFile(handle);
|
||||
_horizonClient.Fs.CloseFile(handle);
|
||||
|
||||
return (ResultCode)result.Value;
|
||||
}
|
||||
|
@ -225,32 +213,32 @@ namespace Ryujinx.HLE.HOS.Services.Mii
|
|||
|
||||
private Result ForceSaveDatabase()
|
||||
{
|
||||
Result result = _filesystemClient.CreateFile(DatabasePath, Unsafe.SizeOf<NintendoFigurineDatabase>());
|
||||
Result result = _horizonClient.Fs.CreateFile(DatabasePath, Unsafe.SizeOf<NintendoFigurineDatabase>());
|
||||
|
||||
if (result.IsSuccess() || ResultFs.PathAlreadyExists.Includes(result))
|
||||
{
|
||||
result = _filesystemClient.OpenFile(out FileHandle handle, DatabasePath, OpenMode.Write);
|
||||
result = _horizonClient.Fs.OpenFile(out FileHandle handle, DatabasePath, OpenMode.Write);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
result = _filesystemClient.GetFileSize(out long fileSize, handle);
|
||||
result = _horizonClient.Fs.GetFileSize(out long fileSize, handle);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
// If the size doesn't match, recreate the file
|
||||
if (fileSize != Unsafe.SizeOf<NintendoFigurineDatabase>())
|
||||
{
|
||||
_filesystemClient.CloseFile(handle);
|
||||
_horizonClient.Fs.CloseFile(handle);
|
||||
|
||||
result = _filesystemClient.DeleteFile(DatabasePath);
|
||||
result = _horizonClient.Fs.DeleteFile(DatabasePath);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
result = _filesystemClient.CreateFile(DatabasePath, Unsafe.SizeOf<NintendoFigurineDatabase>());
|
||||
result = _horizonClient.Fs.CreateFile(DatabasePath, Unsafe.SizeOf<NintendoFigurineDatabase>());
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
result = _filesystemClient.OpenFile(out handle, DatabasePath, OpenMode.Write);
|
||||
result = _horizonClient.Fs.OpenFile(out handle, DatabasePath, OpenMode.Write);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,10 +248,10 @@ namespace Ryujinx.HLE.HOS.Services.Mii
|
|||
}
|
||||
}
|
||||
|
||||
result = _filesystemClient.WriteFile(handle, 0, _database.AsReadOnlySpan(), WriteOption.Flush);
|
||||
result = _horizonClient.Fs.WriteFile(handle, 0, _database.AsReadOnlySpan(), WriteOption.Flush);
|
||||
}
|
||||
|
||||
_filesystemClient.CloseFile(handle);
|
||||
_horizonClient.Fs.CloseFile(handle);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,7 +259,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii
|
|||
{
|
||||
_isDirty = false;
|
||||
|
||||
result = _filesystemClient.Commit(MountName);
|
||||
result = _horizonClient.Fs.Commit(MountName);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Concentus" Version="1.1.7" />
|
||||
<PackageReference Include="LibHac" Version="0.12.0" />
|
||||
<PackageReference Include="LibHac" Version="0.13.1" />
|
||||
<PackageReference Include="MsgPack.Cli" Version="1.0.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -52,9 +52,10 @@ namespace Ryujinx.Ui
|
|||
{
|
||||
public class MainWindow : Window
|
||||
{
|
||||
private readonly VirtualFileSystem _virtualFileSystem;
|
||||
private readonly ContentManager _contentManager;
|
||||
private readonly AccountManager _accountManager;
|
||||
private readonly VirtualFileSystem _virtualFileSystem;
|
||||
private readonly ContentManager _contentManager;
|
||||
private readonly AccountManager _accountManager;
|
||||
private readonly LibHacHorizonManager _libHacHorizonManager;
|
||||
|
||||
private UserChannelPersistence _userChannelPersistence;
|
||||
|
||||
|
@ -156,13 +157,27 @@ namespace Ryujinx.Ui
|
|||
// Hide emulation context status bar.
|
||||
_statusBar.Hide();
|
||||
|
||||
// Instanciate HLE objects.
|
||||
_virtualFileSystem = VirtualFileSystem.CreateInstance();
|
||||
// Instantiate HLE objects.
|
||||
_virtualFileSystem = VirtualFileSystem.CreateInstance();
|
||||
_libHacHorizonManager = new LibHacHorizonManager();
|
||||
|
||||
_libHacHorizonManager.InitializeFsServer(_virtualFileSystem);
|
||||
_libHacHorizonManager.InitializeArpServer();
|
||||
_libHacHorizonManager.InitializeBcatServer();
|
||||
_libHacHorizonManager.InitializeSystemClients();
|
||||
|
||||
// Save data created before we supported extra data in directory save data will not work properly if
|
||||
// given empty extra data. Luckily some of that extra data can be created using the data from the
|
||||
// save data indexer, which should be enough to check access permissions for user saves.
|
||||
// Every single save data's extra data will be checked and fixed if needed each time the emulator is opened.
|
||||
// Consider removing this at some point in the future when we don't need to worry about old saves.
|
||||
VirtualFileSystem.FixExtraData(_libHacHorizonManager.RyujinxClient);
|
||||
|
||||
_contentManager = new ContentManager(_virtualFileSystem);
|
||||
_accountManager = new AccountManager(_virtualFileSystem);
|
||||
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient);
|
||||
_userChannelPersistence = new UserChannelPersistence();
|
||||
|
||||
// Instanciate GUI objects.
|
||||
// Instantiate GUI objects.
|
||||
_applicationLibrary = new ApplicationLibrary(_virtualFileSystem);
|
||||
_uiHandler = new GtkHostUiHandler(this);
|
||||
_deviceExitStatus = new AutoResetEvent(false);
|
||||
|
@ -370,7 +385,7 @@ namespace Ryujinx.Ui
|
|||
|
||||
private void InitializeSwitchInstance()
|
||||
{
|
||||
_virtualFileSystem.Reload();
|
||||
_virtualFileSystem.ReloadKeySet();
|
||||
|
||||
IRenderer renderer;
|
||||
|
||||
|
@ -440,6 +455,7 @@ namespace Ryujinx.Ui
|
|||
IntegrityCheckLevel fsIntegrityCheckLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None;
|
||||
|
||||
HLE.HLEConfiguration configuration = new HLE.HLEConfiguration(_virtualFileSystem,
|
||||
_libHacHorizonManager,
|
||||
_contentManager,
|
||||
_accountManager,
|
||||
_userChannelPersistence,
|
||||
|
@ -1092,7 +1108,7 @@ namespace Ryujinx.Ui
|
|||
|
||||
BlitStruct<ApplicationControlProperty> controlData = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10);
|
||||
|
||||
_ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, titleFilePath, titleName, titleId, controlData);
|
||||
_ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData);
|
||||
}
|
||||
|
||||
private void Load_Application_File(object sender, EventArgs args)
|
||||
|
@ -1208,15 +1224,15 @@ namespace Ryujinx.Ui
|
|||
|
||||
SystemVersion firmwareVersion = _contentManager.VerifyFirmwarePackage(filename);
|
||||
|
||||
string dialogTitle = $"Install Firmware {firmwareVersion.VersionString}";
|
||||
|
||||
if (firmwareVersion == null)
|
||||
if (firmwareVersion is null)
|
||||
{
|
||||
GtkDialog.CreateErrorDialog($"A valid system firmware was not found in {filename}.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
string dialogTitle = $"Install Firmware {firmwareVersion.VersionString}";
|
||||
|
||||
SystemVersion currentVersion = _contentManager.GetCurrentFirmwareVersion();
|
||||
|
||||
string dialogMessage = $"System version {firmwareVersion.VersionString} will be installed.";
|
||||
|
|
|
@ -33,6 +33,7 @@ namespace Ryujinx.Ui.Widgets
|
|||
private readonly MainWindow _parent;
|
||||
private readonly VirtualFileSystem _virtualFileSystem;
|
||||
private readonly AccountManager _accountManager;
|
||||
private readonly HorizonClient _horizonClient;
|
||||
private readonly BlitStruct<ApplicationControlProperty> _controlData;
|
||||
|
||||
private readonly string _titleFilePath;
|
||||
|
@ -43,7 +44,7 @@ namespace Ryujinx.Ui.Widgets
|
|||
private MessageDialog _dialog;
|
||||
private bool _cancel;
|
||||
|
||||
public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, string titleFilePath, string titleName, string titleId, BlitStruct<ApplicationControlProperty> controlData)
|
||||
public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct<ApplicationControlProperty> controlData)
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
|
@ -51,6 +52,7 @@ namespace Ryujinx.Ui.Widgets
|
|||
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_accountManager = accountManager;
|
||||
_horizonClient = horizonClient;
|
||||
_titleFilePath = titleFilePath;
|
||||
_titleName = titleName;
|
||||
_titleIdText = titleId;
|
||||
|
@ -63,9 +65,9 @@ namespace Ryujinx.Ui.Widgets
|
|||
return;
|
||||
}
|
||||
|
||||
_openSaveUserDirMenuItem.Sensitive = !Utilities.IsEmpty(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0;
|
||||
_openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsEmpty(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0;
|
||||
_openSaveBcatDirMenuItem.Sensitive = !Utilities.IsEmpty(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0;
|
||||
_openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0;
|
||||
_openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0;
|
||||
_openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0;
|
||||
|
||||
string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower();
|
||||
bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci";
|
||||
|
@ -81,7 +83,7 @@ namespace Ryujinx.Ui.Widgets
|
|||
{
|
||||
saveDataId = default;
|
||||
|
||||
Result result = _virtualFileSystem.FsClient.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, ref filter);
|
||||
Result result = _horizonClient.Fs.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, in filter);
|
||||
|
||||
if (ResultFs.TargetNotFound.Includes(result))
|
||||
{
|
||||
|
@ -102,7 +104,7 @@ namespace Ryujinx.Ui.Widgets
|
|||
|
||||
ref ApplicationControlProperty control = ref controlHolder.Value;
|
||||
|
||||
if (Utilities.IsEmpty(controlHolder.ByteSpan))
|
||||
if (Utilities.IsZeros(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.
|
||||
|
@ -117,7 +119,7 @@ namespace Ryujinx.Ui.Widgets
|
|||
|
||||
Uid user = new Uid((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
|
||||
|
||||
result = EnsureApplicationSaveData(_virtualFileSystem.FsClient, out _, new LibHac.Ncm.ApplicationId(titleId), ref control, ref user);
|
||||
result = EnsureApplicationSaveData(_horizonClient.Fs, out _, new LibHac.Ncm.ApplicationId(titleId), ref control, ref user);
|
||||
|
||||
if (result.IsFailure())
|
||||
{
|
||||
|
@ -127,7 +129,7 @@ namespace Ryujinx.Ui.Widgets
|
|||
}
|
||||
|
||||
// Try to find the savedata again after creating it
|
||||
result = _virtualFileSystem.FsClient.FindSaveDataWithFilter(out saveDataInfo, SaveDataSpaceId.User, ref filter);
|
||||
result = _horizonClient.Fs.FindSaveDataWithFilter(out saveDataInfo, SaveDataSpaceId.User, in filter);
|
||||
}
|
||||
|
||||
if (result.IsSuccess())
|
||||
|
@ -284,7 +286,7 @@ namespace Ryujinx.Ui.Widgets
|
|||
IFileSystem ncaFileSystem = patchNca != null ? mainNca.OpenFileSystemWithPatch(patchNca, index, IntegrityCheckLevel.ErrorOnInvalid)
|
||||
: mainNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid);
|
||||
|
||||
FileSystemClient fsClient = _virtualFileSystem.FsClient;
|
||||
FileSystemClient fsClient = _horizonClient.Fs;
|
||||
|
||||
string source = DateTime.Now.ToFileTime().ToString()[10..];
|
||||
string output = DateTime.Now.ToFileTime().ToString()[10..];
|
||||
|
@ -409,7 +411,7 @@ namespace Ryujinx.Ui.Widgets
|
|||
rc = fs.ReadFile(out long _, sourceHandle, offset, buf);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = fs.WriteFile(destHandle, offset, buf);
|
||||
rc = fs.WriteFile(destHandle, offset, buf, WriteOption.None);
|
||||
if (rc.IsFailure()) return rc;
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue