// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" #include "common/fs/file.h" #include "common/fs/fs.h" #include "common/fs/path_util.h" #include "common/logging/log.h" #include "common/string_util.h" #include "core/hle/service/mii/mii_database_manager.h" #include "core/hle/service/mii/mii_result.h" #include "core/hle/service/mii/mii_util.h" #include "core/hle/service/mii/types/char_info.h" #include "core/hle/service/mii/types/store_data.h" namespace Service::Mii { constexpr std::string DbFileName = "MiiDatabase.dat"; DatabaseManager::DatabaseManager() {} Result DatabaseManager::MountSaveData() { if (!is_save_data_mounted) { system_save_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / "system/save/8000000000000030"; if (is_test_db) { system_save_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / "system/save/8000000000000031"; } // mount point should be "mii:" if (!Common::FS::CreateDirs(system_save_dir)) { return ResultUnknown; } } is_save_data_mounted = true; return ResultSuccess; } Result DatabaseManager::Initialize(DatabaseSessionMetadata& metadata, bool& is_database_broken) { is_database_broken = false; if (!is_save_data_mounted) { return ResultInvalidArgument; } database.CleanDatabase(); update_counter++; metadata.update_counter = update_counter; const Common::FS::IOFile db_file{system_save_dir / DbFileName, Common::FS::FileAccessMode::Read, Common::FS::FileType::BinaryFile}; if (!db_file.IsOpen()) { return SaveDatabase(); } if (Common::FS::GetSize(system_save_dir / DbFileName) != sizeof(NintendoFigurineDatabase)) { is_database_broken = true; } if (db_file.Read(database) != 1) { is_database_broken = true; } if (is_database_broken) { // Dragons happen here for simplicity just clean the database LOG_ERROR(Service_Mii, "Mii database is corrupted"); database.CleanDatabase(); return ResultUnknown; } const auto result = database.CheckIntegrity(); if (result.IsError()) { LOG_ERROR(Service_Mii, "Mii database is corrupted 0x{:0x}", result.raw); database.CleanDatabase(); return ResultSuccess; } LOG_INFO(Service_Mii, "Successfully loaded mii database. size={}", database.GetDatabaseLength()); return ResultSuccess; } bool DatabaseManager::IsFullDatabase() const { return database.GetDatabaseLength() == MaxDatabaseLength; } bool DatabaseManager::IsModified() const { return is_moddified; } u64 DatabaseManager::GetUpdateCounter() const { return update_counter; } u32 DatabaseManager::GetCount(const DatabaseSessionMetadata& metadata) const { const u32 database_size = database.GetDatabaseLength(); if (metadata.magic == MiiMagic) { return database_size; } // Special mii can't be used. Skip those. u32 mii_count{}; for (std::size_t index = 0; index < database_size; ++index) { const auto& store_data = database.Get(index); if (store_data.IsSpecial()) { continue; } mii_count++; } return mii_count; } void DatabaseManager::Get(StoreData& out_store_data, std::size_t index, const DatabaseSessionMetadata& metadata) const { if (metadata.magic == MiiMagic) { out_store_data = database.Get(index); return; } // The index refeers to the mii index without special mii. // Search on the database until we find it u32 virtual_index = 0; const u32 database_size = database.GetDatabaseLength(); for (std::size_t i = 0; i < database_size; ++i) { const auto& store_data = database.Get(i); if (store_data.IsSpecial()) { continue; } if (virtual_index == index) { out_store_data = store_data; return; } virtual_index++; } // This function doesn't fail. It returns the first mii instead out_store_data = database.Get(0); } Result DatabaseManager::FindIndex(s32& out_index, const Common::UUID& create_id, bool is_special) const { u32 index{}; const bool is_found = database.GetIndexByCreatorId(index, create_id); if (!is_found) { return ResultNotFound; } if (is_special) { out_index = index; return ResultSuccess; } if (database.Get(index).IsSpecial()) { return ResultNotFound; } out_index = 0; if (index < 1) { return ResultSuccess; } for (std::size_t i = 0; i <= index; ++i) { if (database.Get(i).IsSpecial()) { continue; } out_index++; } return ResultSuccess; } Result DatabaseManager::FindIndex(const DatabaseSessionMetadata& metadata, u32& out_index, const Common::UUID& create_id) const { u32 index{}; const bool is_found = database.GetIndexByCreatorId(index, create_id); if (!is_found) { return ResultNotFound; } if (metadata.magic == MiiMagic) { out_index = index; return ResultSuccess; } if (database.Get(index).IsSpecial()) { return ResultNotFound; } out_index = 0; if (index < 1) { return ResultSuccess; } // The index refeers to the mii index without special mii. // Search on the database until we find it for (std::size_t i = 0; i <= index; ++i) { const auto& store_data = database.Get(i); if (store_data.IsSpecial()) { continue; } out_index++; } return ResultSuccess; } Result DatabaseManager::FindMoveIndex(u32& out_index, u32 new_index, const Common::UUID& create_id) const { const auto database_size = database.GetDatabaseLength(); if (database_size >= 1) { u32 virtual_index{}; for (std::size_t i = 0; i < database_size; ++i) { const StoreData& store_data = database.Get(i); if (store_data.IsSpecial()) { continue; } if (virtual_index == new_index) { const bool is_found = database.GetIndexByCreatorId(out_index, create_id); if (!is_found) { return ResultNotFound; } if (store_data.IsSpecial()) { return ResultInvalidOperation; } return ResultSuccess; } virtual_index++; } } const bool is_found = database.GetIndexByCreatorId(out_index, create_id); if (!is_found) { return ResultNotFound; } const StoreData& store_data = database.Get(out_index); if (store_data.IsSpecial()) { return ResultInvalidOperation; } return ResultSuccess; } Result DatabaseManager::Move(DatabaseSessionMetadata& metadata, u32 new_index, const Common::UUID& create_id) { u32 current_index{}; if (metadata.magic == MiiMagic) { const bool is_found = database.GetIndexByCreatorId(current_index, create_id); if (!is_found) { return ResultNotFound; } } else { const auto result = FindMoveIndex(current_index, new_index, create_id); if (result.IsError()) { return result; } } const auto result = database.Move(current_index, new_index); if (result.IsFailure()) { return result; } is_moddified = true; update_counter++; metadata.update_counter = update_counter; return ResultSuccess; } Result DatabaseManager::AddOrReplace(DatabaseSessionMetadata& metadata, const StoreData& store_data) { if (store_data.IsValid() != ValidationResult::NoErrors) { return ResultInvalidStoreData; } if (metadata.magic != MiiMagic && store_data.IsSpecial()) { return ResultInvalidOperation; } u32 index{}; const bool is_found = database.GetIndexByCreatorId(index, store_data.GetCreateId()); if (is_found) { const StoreData& old_store_data = database.Get(index); if (store_data.IsSpecial() != old_store_data.IsSpecial()) { return ResultInvalidOperation; } database.Replace(index, store_data); } else { if (database.IsFull()) { return ResultDatabaseFull; } database.Add(store_data); } is_moddified = true; update_counter++; metadata.update_counter = update_counter; return ResultSuccess; } Result DatabaseManager::Delete(DatabaseSessionMetadata& metadata, const Common::UUID& create_id) { u32 index{}; const bool is_found = database.GetIndexByCreatorId(index, create_id); if (!is_found) { return ResultNotFound; } if (metadata.magic != MiiMagic) { const auto& store_data = database.Get(index); if (store_data.IsSpecial()) { return ResultInvalidOperation; } } database.Delete(index); is_moddified = true; update_counter++; metadata.update_counter = update_counter; return ResultSuccess; } Result DatabaseManager::Append(DatabaseSessionMetadata& metadata, const CharInfo& char_info) { if (char_info.Verify() != ValidationResult::NoErrors) { return ResultInvalidCharInfo2; } if (char_info.GetType() == 1) { return ResultInvalidCharInfoType; } u32 index{}; StoreData store_data{}; // Loop until the mii we created is not on the database do { store_data.BuildWithCharInfo(char_info); } while (database.GetIndexByCreatorId(index, store_data.GetCreateId())); const Result result = store_data.Restore(); if (result.IsSuccess() || result == ResultNotUpdated) { return AddOrReplace(metadata, store_data); } return result; } Result DatabaseManager::DestroyFile(DatabaseSessionMetadata& metadata) { database.CorruptCrc(); is_moddified = true; update_counter++; metadata.update_counter = update_counter; const auto result = SaveDatabase(); database.CleanDatabase(); return result; } Result DatabaseManager::DeleteFile() { const bool result = Common::FS::RemoveFile(system_save_dir / DbFileName); // Return proper FS error here return result ? ResultSuccess : ResultUnknown; } void DatabaseManager::Format(DatabaseSessionMetadata& metadata) { database.CleanDatabase(); is_moddified = true; update_counter++; metadata.update_counter = update_counter; } Result DatabaseManager::SaveDatabase() { // TODO: Replace unknown error codes with proper FS error codes when available if (!Common::FS::Exists(system_save_dir / DbFileName)) { if (!Common::FS::NewFile(system_save_dir / DbFileName)) { LOG_ERROR(Service_Mii, "Failed to create mii database"); return ResultUnknown; } } const auto file_size = Common::FS::GetSize(system_save_dir / DbFileName); if (file_size != 0 && file_size != sizeof(NintendoFigurineDatabase)) { if (!Common::FS::RemoveFile(system_save_dir / DbFileName)) { LOG_ERROR(Service_Mii, "Failed to delete mii database"); return ResultUnknown; } if (!Common::FS::NewFile(system_save_dir / DbFileName)) { LOG_ERROR(Service_Mii, "Failed to create mii database"); return ResultUnknown; } } const Common::FS::IOFile db_file{system_save_dir / DbFileName, Common::FS::FileAccessMode::ReadWrite, Common::FS::FileType::BinaryFile}; if (db_file.Write(database) != 1) { LOG_ERROR(Service_Mii, "Failed to save mii database"); return ResultUnknown; } is_moddified = false; return ResultSuccess; } } // namespace Service::Mii