using LibHac; using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Shim; using Ryujinx.HLE.HOS.Services.Mii.Types; using System.Runtime.CompilerServices; namespace Ryujinx.HLE.HOS.Services.Mii { class MiiDatabaseManager { private static bool IsTestModeEnabled = false; private static uint MountCounter = 0; 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"); private NintendoFigurineDatabase _database; private bool _isDirty; private FileSystemClient _filesystemClient; protected ulong UpdateCounter { get; private set; } public MiiDatabaseManager() { _database = new NintendoFigurineDatabase(); _isDirty = false; UpdateCounter = 0; } private void ResetDatabase() { _database = new NintendoFigurineDatabase(); _database.Format(); } private void MarkDirty(DatabaseSessionMetadata metadata) { _isDirty = true; UpdateCounter++; metadata.UpdateCounter = UpdateCounter; } private bool GetAtVirtualIndex(int index, out int realIndex, out StoreData storeData) { realIndex = -1; storeData = new StoreData(); int virtualIndex = 0; for (int i = 0; i < _database.Length; i++) { StoreData tmp = _database.Get(i); if (!tmp.IsSpecial()) { if (index == virtualIndex) { realIndex = i; storeData = tmp; return true; } virtualIndex++; } } return false; } private int ConvertRealIndexToVirtualIndex(int realIndex) { int virtualIndex = 0; for (int i = 0; i < realIndex; i++) { StoreData tmp = _database.Get(i); if (!tmp.IsSpecial()) { virtualIndex++; } } return virtualIndex; } public void InitializeDatabase(Switch device) { _filesystemClient = device.FileSystem.FsClient; // Ensure we have valid data in the database _database.Format(); MountSave(); } private Result MountSave() { Result result = Result.Success; if (MountCounter == 0) { ulong targetSaveDataId; ulong targetTitleId; if (IsTestModeEnabled) { targetSaveDataId = DatabaseTestSaveDataId; targetTitleId = SdbTitleId; } else { targetSaveDataId = DatabaseSaveDataId; // Nintendo use NS TitleID when creating the production save even on sdb, let's follow that behaviour. targetTitleId = NsTitleId; } 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++; } } return result; } public ResultCode DeleteFile() { ResultCode result = (ResultCode)_filesystemClient.DeleteFile(DatabasePath).Value; _filesystemClient.Commit(MountName); return result; } public ResultCode LoadFromFile(out bool isBroken) { isBroken = false; if (MountCounter == 0) { return ResultCode.InvalidArgument; } UpdateCounter++; ResetDatabase(); Result result = _filesystemClient.OpenFile(out FileHandle handle, DatabasePath, OpenMode.Read); if (result.IsSuccess()) { result = _filesystemClient.GetFileSize(out long fileSize, handle); if (result.IsSuccess()) { if (fileSize == Unsafe.SizeOf()) { result = _filesystemClient.ReadFile(handle, 0, _database.AsSpan()); if (result.IsSuccess()) { if (_database.Verify() != ResultCode.Success) { ResetDatabase(); isBroken = true; } else { isBroken = _database.FixDatabase(); } } } else { isBroken = true; } } _filesystemClient.CloseFile(handle); return (ResultCode)result.Value; } else if (ResultFs.PathNotFound.Includes(result)) { return (ResultCode)ForceSaveDatabase().Value; } return ResultCode.Success; } private Result ForceSaveDatabase() { Result result = _filesystemClient.CreateFile(DatabasePath, Unsafe.SizeOf()); if (result.IsSuccess() || ResultFs.PathAlreadyExists.Includes(result)) { result = _filesystemClient.OpenFile(out FileHandle handle, DatabasePath, OpenMode.Write); if (result.IsSuccess()) { result = _filesystemClient.GetFileSize(out long fileSize, handle); if (result.IsSuccess()) { // If the size doesn't match, recreate the file if (fileSize != Unsafe.SizeOf()) { _filesystemClient.CloseFile(handle); result = _filesystemClient.DeleteFile(DatabasePath); if (result.IsSuccess()) { result = _filesystemClient.CreateFile(DatabasePath, Unsafe.SizeOf()); if (result.IsSuccess()) { result = _filesystemClient.OpenFile(out handle, DatabasePath, OpenMode.Write); } } if (result.IsFailure()) { return result; } } result = _filesystemClient.WriteFile(handle, 0, _database.AsReadOnlySpan(), WriteOption.Flush); } _filesystemClient.CloseFile(handle); } } if (result.IsSuccess()) { _isDirty = false; result = _filesystemClient.Commit(MountName); } return result; } public DatabaseSessionMetadata CreateSessionMetadata(SpecialMiiKeyCode miiKeyCode) { return new DatabaseSessionMetadata(UpdateCounter, miiKeyCode); } public void SetInterfaceVersion(DatabaseSessionMetadata metadata, uint interfaceVersion) { metadata.InterfaceVersion = interfaceVersion; } public bool IsUpdated(DatabaseSessionMetadata metadata) { bool result = metadata.UpdateCounter != UpdateCounter; metadata.UpdateCounter = UpdateCounter; return result; } public int GetCount(DatabaseSessionMetadata metadata) { if (!metadata.MiiKeyCode.IsEnabledSpecialMii()) { int count = 0; for (int i = 0; i < _database.Length; i++) { StoreData tmp = _database.Get(i); if (!tmp.IsSpecial()) { count++; } } return count; } else { return _database.Length; } } public void Get(DatabaseSessionMetadata metadata, int index, out StoreData storeData) { if (!metadata.MiiKeyCode.IsEnabledSpecialMii()) { if (GetAtVirtualIndex(index, out int realIndex, out _)) { index = realIndex; } else { index = 0; } } storeData = _database.Get(index); } public ResultCode FindIndex(DatabaseSessionMetadata metadata, out int index, CreateId createId) { return FindIndex(out index, createId, metadata.MiiKeyCode.IsEnabledSpecialMii()); } public ResultCode FindIndex(out int index, CreateId createId, bool isSpecial) { if (_database.GetIndexByCreatorId(out int realIndex, createId)) { if (isSpecial) { index = realIndex; return ResultCode.Success; } StoreData storeData = _database.Get(realIndex); if (!storeData.IsSpecial()) { if (realIndex < 1) { index = 0; } else { index = ConvertRealIndexToVirtualIndex(realIndex); } return ResultCode.Success; } } index = -1; return ResultCode.NotFound; } public ResultCode Move(DatabaseSessionMetadata metadata, int newIndex, CreateId createId) { if (!metadata.MiiKeyCode.IsEnabledSpecialMii()) { if (GetAtVirtualIndex(newIndex, out int realIndex, out _)) { newIndex = realIndex; } else { newIndex = 0; } } if (_database.GetIndexByCreatorId(out int oldIndex, createId)) { StoreData realStoreData = _database.Get(oldIndex); if (!metadata.MiiKeyCode.IsEnabledSpecialMii() && realStoreData.IsSpecial()) { return ResultCode.InvalidOperationOnSpecialMii; } ResultCode result = _database.Move(newIndex, oldIndex); if (result == ResultCode.Success) { MarkDirty(metadata); } return result; } return ResultCode.NotFound; } public ResultCode AddOrReplace(DatabaseSessionMetadata metadata, StoreData storeData) { if (!storeData.IsValid()) { return ResultCode.InvalidStoreData; } if (!metadata.MiiKeyCode.IsEnabledSpecialMii() && !storeData.IsSpecial()) { if (_database.GetIndexByCreatorId(out int index, storeData.CreateId)) { StoreData oldStoreData = _database.Get(index); if (oldStoreData.IsSpecial()) { return ResultCode.InvalidOperationOnSpecialMii; } _database.Replace(index, storeData); } else { if (_database.IsFull()) { return ResultCode.DatabaseFull; } _database.Add(storeData); } MarkDirty(metadata); return ResultCode.Success; } return ResultCode.InvalidOperationOnSpecialMii; } public ResultCode Delete(DatabaseSessionMetadata metadata, CreateId createId) { if (!_database.GetIndexByCreatorId(out int index, createId)) { return ResultCode.NotFound; } if (!metadata.MiiKeyCode.IsEnabledSpecialMii()) { StoreData storeData = _database.Get(index); if (storeData.IsSpecial()) { return ResultCode.InvalidOperationOnSpecialMii; } } _database.Delete(index); MarkDirty(metadata); return ResultCode.Success; } public ResultCode DestroyFile(DatabaseSessionMetadata metadata) { _database.CorruptDatabase(); MarkDirty(metadata); ResultCode result = SaveDatabase(); ResetDatabase(); return result; } public ResultCode SaveDatabase() { if (_isDirty) { return (ResultCode)ForceSaveDatabase().Value; } else { return ResultCode.NotUpdated; } } public void FormatDatabase(DatabaseSessionMetadata metadata) { _database.Format(); MarkDirty(metadata); } public bool IsFullDatabase() { return _database.IsFull(); } } }