From 255bfd7be6ce3da4cfa72d752dbebea631ddaf35 Mon Sep 17 00:00:00 2001 From: Masamune3210 <1053504+Masamune3210@users.noreply.github.com> Date: Sat, 16 May 2026 16:28:18 -0500 Subject: [PATCH] hle: implement remaining 3DSident-facing service commands Implement several previously-stubbed service commands and refine the system version reporting from the prior hardware-info work: - ptm:sysm ConfigureNew3DSCPU (0x0818): accept the clock/L2 config bitfield. Azahar always runs as New 3DS, so no switching is needed. - fs:USER GetSdmcCid (0x0819) / GetNandCid (0x081A): return all-zero CIDs, consistent with the all-zero ID0/ID1 used in the SDMC path. - cfg:i ClearParentalControls (0x040F): clear the parental restriction email block (0x000C0002) and persist. The PIN and secret answer in 0x00100001 are intentionally preserved, matching hardware behaviour. Synthesize the virtual TWL /sys/log/product.log dynamically from the installed CVer/NVer titles' RomFS version.bin instead of a hardcoded string, so the reported system version reflects the actual installed firmware. If those titles are absent, the file is reported as not found rather than returning misleading data. Drop the dummy all-zero TwlParentalRestrictions config block default, which caused a fake parental PIN to be reported when the block was never set up. Co-Authored-By: Claude Opus 4.7 --- src/core/file_sys/archive_nand.cpp | 185 ++++++++++++++++++++-- src/core/hle/service/cfg/cfg.cpp | 15 ++ src/core/hle/service/cfg/cfg.h | 1 + src/core/hle/service/cfg/cfg_defaults.cpp | 4 - src/core/hle/service/cfg/cfg_i.cpp | 1 + src/core/hle/service/fs/fs_user.cpp | 36 ++++- src/core/hle/service/fs/fs_user.h | 22 +++ src/core/hle/service/ptm/ptm.cpp | 12 ++ src/core/hle/service/ptm/ptm.h | 11 ++ src/core/hle/service/ptm/ptm_sysm.cpp | 2 +- 10 files changed, 268 insertions(+), 21 deletions(-) diff --git a/src/core/file_sys/archive_nand.cpp b/src/core/file_sys/archive_nand.cpp index 8d203bd19..b7b0fbf01 100644 --- a/src/core/file_sys/archive_nand.cpp +++ b/src/core/file_sys/archive_nand.cpp @@ -3,17 +3,28 @@ // Refer to the license.txt file included. #include +#include #include +#include +#include +#include +#include #include "common/archives.h" #include "common/common_paths.h" #include "common/error.h" #include "common/file_util.h" #include "common/logging/log.h" #include "common/settings.h" +#include "common/string_util.h" #include "core/file_sys/archive_nand.h" #include "core/file_sys/disk_archive.h" #include "core/file_sys/errors.h" +#include "core/file_sys/layered_fs.h" +#include "core/file_sys/ncch_container.h" #include "core/file_sys/path_parser.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/fs/archive.h" +#include "core/loader/loader.h" SERIALIZE_EXPORT_IMPL(FileSys::VirtualFile) SERIALIZE_EXPORT_IMPL(FileSys::NANDArchive) @@ -27,19 +38,165 @@ namespace FileSys { namespace { -// Virtual TWL NAND files served from memory; keyed by ASCII path as stored on the TWL partition. -// These files reside on actual TWL NAND hardware and are never written by normal operation, so -// Azahar serves fixed content without creating any host-filesystem files. -struct TwlVirtualEntry { - const char* path; - const char* content; +constexpr u64 SYSTEM_DATA_ARCHIVE_TITLE_ID_HIGH = 0x000400DBULL; +constexpr std::array NVER_OLD_3DS_TITLE_ID_LOWS = { + 0x00016202, 0x00016302, 0x00016102, 0x00016102, 0x00016402, 0x00016502, 0x00016602}; +constexpr std::array NVER_NEW_3DS_TITLE_ID_LOWS = { + 0x20016202, 0x20016302, 0x20016102, 0x20016102, 0, 0x20016502, 0}; +constexpr std::array CVER_TITLE_ID_LOWS = { + 0x00017202, 0x00017302, 0x00017102, 0x00017102, 0x00017402, 0x00017502, 0x00017602}; + +constexpr std::size_t VERSION_BIN_SIZE = 8; +constexpr u32 INVALID_ROMFS_OFFSET = 0xFFFFFFFF; + +struct RomFSDirectoryMetadata { + u32_le parent_directory_offset; + u32_le next_sibling_offset; + u32_le first_child_directory_offset; + u32_le first_file_offset; + u32_le hash_bucket_next; + u32_le name_length; }; -constexpr TwlVirtualEntry TWL_VIRTUAL_FILES[] = { - // 3DSident reads this via Kernel_GetInitalVersion(): extracts between "cup:" and " preInstall:" - // then appends "-" and the value between "nup:" and " cup:". Display format is "cup-nup". - // Azahar has no distinct initial vs current firmware, so both report the current system version. - {"/sys/log/product.log", "nup:11.17.0-50U cup:11.17.0-50U preInstall:\n"}, +static_assert(sizeof(RomFSDirectoryMetadata) == 0x18, + "Size of RomFSDirectoryMetadata is not correct"); + +struct RomFSFileMetadata { + u32_le parent_directory_offset; + u32_le next_sibling_offset; + u64_le file_data_offset; + u64_le file_data_length; + u32_le hash_bucket_next; + u32_le name_length; }; +static_assert(sizeof(RomFSFileMetadata) == 0x20, "Size of RomFSFileMetadata is not correct"); + +template +bool ReadRomFSStruct(RomFSReader& romfs, std::size_t offset, T& out) { + return romfs.ReadFile(offset, sizeof(T), reinterpret_cast(&out)) == sizeof(T); +} + +std::string ReadRomFSName(RomFSReader& romfs, std::size_t offset, u32 name_length) { + std::vector buffer(name_length / sizeof(u16_le)); + if (romfs.ReadFile(offset, name_length, reinterpret_cast(buffer.data())) != name_length) { + return {}; + } + + std::u16string name(buffer.size(), 0); + std::transform(buffer.begin(), buffer.end(), name.begin(), [](u16_le character) { + return static_cast(static_cast(character)); + }); + return Common::UTF16ToUTF8(name); +} + +std::optional> ReadRootRomFSFile(RomFSReader& romfs, + std::string_view target_name) { + RomFSHeader header{}; + if (!ReadRomFSStruct(romfs, 0, header)) { + return std::nullopt; + } + + RomFSDirectoryMetadata root_directory{}; + if (!ReadRomFSStruct(romfs, header.directory_metadata_table.offset, root_directory)) { + return std::nullopt; + } + + u32 file_offset = root_directory.first_file_offset; + while (file_offset != INVALID_ROMFS_OFFSET) { + RomFSFileMetadata file{}; + const auto metadata_offset = header.file_metadata_table.offset + file_offset; + if (!ReadRomFSStruct(romfs, metadata_offset, file)) { + return std::nullopt; + } + + const std::string name = + ReadRomFSName(romfs, metadata_offset + sizeof(file), file.name_length); + if (name == target_name && file.file_data_length == VERSION_BIN_SIZE) { + std::array data{}; + const std::size_t data_offset = header.file_data_offset + file.file_data_offset; + if (romfs.ReadFile(data_offset, data.size(), data.data()) != data.size()) { + return std::nullopt; + } + return data; + } + + file_offset = file.next_sibling_offset; + } + + return std::nullopt; +} + +std::vector GetRegionSearchOrder() { + std::vector order; + const auto configured_region = Settings::values.region_value.GetValue(); + if (configured_region >= 0 && configured_region < static_cast(CVER_TITLE_ID_LOWS.size())) { + order.push_back(static_cast(configured_region)); + } + + for (std::size_t region = 0; region < CVER_TITLE_ID_LOWS.size(); ++region) { + if (std::find(order.begin(), order.end(), region) == order.end()) { + order.push_back(region); + } + } + return order; +} + +std::optional> ReadVersionBin(u64 title_id) { + const std::string content_path = + Service::AM::GetTitleContentPath(Service::FS::MediaType::NAND, title_id); + if (!FileUtil::Exists(content_path)) { + return std::nullopt; + } + + NCCHContainer container(content_path); + std::shared_ptr romfs; + if (container.ReadRomFS(romfs, false) != Loader::ResultStatus::Success || !romfs) { + return std::nullopt; + } + + return ReadRootRomFSFile(*romfs, "version.bin"); +} + +std::optional> FindInstalledVersionBin( + const std::array& title_id_lows) { + for (const std::size_t region : GetRegionSearchOrder()) { + const u32 title_id_low = title_id_lows[region]; + if (title_id_low == 0) { + continue; + } + + const u64 title_id = (SYSTEM_DATA_ARCHIVE_TITLE_ID_HIGH << 32) | title_id_low; + if (auto version = ReadVersionBin(title_id)) { + return version; + } + } + + return std::nullopt; +} + +std::optional> FindInstalledNVer() { + const bool is_new_3ds = Settings::values.is_new_3ds.GetValue(); + if (auto version = + FindInstalledVersionBin(is_new_3ds ? NVER_NEW_3DS_TITLE_ID_LOWS + : NVER_OLD_3DS_TITLE_ID_LOWS)) { + return version; + } + + return FindInstalledVersionBin(is_new_3ds ? NVER_OLD_3DS_TITLE_ID_LOWS + : NVER_NEW_3DS_TITLE_ID_LOWS); +} + +std::optional BuildProductLog() { + const auto cver = FindInstalledVersionBin(CVER_TITLE_ID_LOWS); + const auto nver = FindInstalledNVer(); + if (!cver || !nver) { + return std::nullopt; + } + + const std::string cup = std::to_string((*cver)[2]) + "." + std::to_string((*cver)[1]) + "." + + std::to_string((*cver)[0]); + const std::string nup = std::to_string((*nver)[2]) + static_cast((*nver)[4]); + return "nup:" + nup + " cup:" + cup + " preInstall:\n"; +} } // namespace @@ -53,9 +210,9 @@ ResultVal> NANDArchive::OpenFile(const Path& path, return ResultInvalidOpenFlags; } const std::string path_str = path.AsString(); - for (const auto& entry : TWL_VIRTUAL_FILES) { - if (path_str == entry.path) { - return std::make_unique(entry.content); + if (path_str == "/sys/log/product.log") { + if (auto product_log = BuildProductLog()) { + return std::make_unique(*product_log); } } return ResultNotFound; diff --git a/src/core/hle/service/cfg/cfg.cpp b/src/core/hle/service/cfg/cfg.cpp index 78867c8f4..60d77531a 100644 --- a/src/core/hle/service/cfg/cfg.cpp +++ b/src/core/hle/service/cfg/cfg.cpp @@ -651,6 +651,21 @@ void Module::Interface::UpdateConfigNANDSavegame(Kernel::HLERequestContext& ctx) rb.Push(cfg->UpdateConfigNANDSavegame()); } +void Module::Interface::ClearParentalControls(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + // Clear the parental restriction email (0x000C0002); PIN and secret answer in + // 0x00100001 are intentionally preserved per hardware behaviour. + static const std::array empty_email{}; + Result res = cfg->SetConfigBlock(static_cast(ParentalRestrictionEmailBlockID), + static_cast(empty_email.size()), + AccessFlag::SystemWrite, empty_email.data()); + if (res.IsSuccess()) { + res = cfg->UpdateConfigNANDSavegame(); + } + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(res); +} + void Module::Interface::GetLocalFriendCodeSeedData(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); [[maybe_unused]] u32 out_size = rp.Pop(); diff --git a/src/core/hle/service/cfg/cfg.h b/src/core/hle/service/cfg/cfg.h index 8b02d6f8e..5c6caf837 100644 --- a/src/core/hle/service/cfg/cfg.h +++ b/src/core/hle/service/cfg/cfg.h @@ -365,6 +365,7 @@ public: * 1 : Result of function, 0 on success, otherwise error code */ void UpdateConfigNANDSavegame(Kernel::HLERequestContext& ctx); + void ClearParentalControls(Kernel::HLERequestContext& ctx); /** * CFG::GetLocalFriendCodeSeedData service function diff --git a/src/core/hle/service/cfg/cfg_defaults.cpp b/src/core/hle/service/cfg/cfg_defaults.cpp index dc36bf033..4440e7670 100644 --- a/src/core/hle/service/cfg/cfg_defaults.cpp +++ b/src/core/hle/service/cfg/cfg_defaults.cpp @@ -61,7 +61,6 @@ constexpr u32_le DEFAULT_CLOCK_SEQUENCE = 0; constexpr const char DEFAULT_SERVER_TYPE[4] = {'L', '1', '\0', '\0'}; constexpr u32_le DEFAULT_0x00160000_DATA = 0; constexpr u32_le DEFAULT_MIIVERSE_ACCESS_KEY = 0; -constexpr std::array DEFAULT_TWL_PARENTAL_RESTRICTIONS = {}; static const std::unordered_map DEFAULT_CONFIG_BLOCKS = { {UserTimeOffsetBlockID, @@ -73,9 +72,6 @@ static const std::unordered_map DEFAULT_CONF {BacklightControlNew3dsBlockID, {AccessFlag::System, &DEFAULT_NEW_3DS_BACKLIGHT_CONTROLS, sizeof(DEFAULT_NEW_3DS_BACKLIGHT_CONTROLS)}}, - {TwlParentalRestrictionsBlockID, - {AccessFlag::System, &DEFAULT_TWL_PARENTAL_RESTRICTIONS, - sizeof(DEFAULT_TWL_PARENTAL_RESTRICTIONS)}}, {SoundOutputModeBlockID, {AccessFlag::Global, &DEFAULT_SOUND_OUTPUT_MODE, sizeof(DEFAULT_SOUND_OUTPUT_MODE)}}, {ConsoleUniqueID1BlockID, diff --git a/src/core/hle/service/cfg/cfg_i.cpp b/src/core/hle/service/cfg/cfg_i.cpp index aa7c17c3b..d3ba03e6b 100644 --- a/src/core/hle/service/cfg/cfg_i.cpp +++ b/src/core/hle/service/cfg/cfg_i.cpp @@ -34,6 +34,7 @@ CFG_I::CFG_I(std::shared_ptr cfg) : Module::Interface(std::move(cfg), "c {0x0407, &CFG_I::SecureInfoGetByte101, "SecureInfoGetByte101"}, {0x0408, &CFG_I::SecureInfoGetSerialNo, "SecureInfoGetSerialNo"}, {0x0409, nullptr, "UpdateConfigBlk00040003"}, + {0x040F, &CFG_I::ClearParentalControls, "ClearParentalControls"}, // cfg:i {0x0801, &CFG_I::GetSystemConfig, "GetSystemConfig"}, {0x0802, &CFG_I::SetSystemConfig, "SetSystemConfig"}, diff --git a/src/core/hle/service/fs/fs_user.cpp b/src/core/hle/service/fs/fs_user.cpp index 1e2225a24..e5f60dc7b 100644 --- a/src/core/hle/service/fs/fs_user.cpp +++ b/src/core/hle/service/fs/fs_user.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include #include #include #include @@ -970,6 +971,37 @@ void FS_USER::GetNandArchiveResource(Kernel::HLERequestContext& ctx) { rb.PushRaw(*resource); } +void FS_USER::GetSdmcCid(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 size = rp.Pop(); + auto& output_buffer = rp.PopMappedBuffer(); + + // All-zeroes dummy SD CID. Azahar uses all-zeroes for ID0/ID1 in the SDMC path + // (Nintendo 3DS/000…/000…), so returning a non-zero CID would produce a SHA-256- + // derived ID0 that disagrees with those paths. All-zeroes is the honest answer + // for an emulated device with no real SD card. + static constexpr std::array sdmc_cid = {}; + output_buffer.Write(sdmc_cid.data(), 0, std::min(size, sdmc_cid.size())); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushMappedBuffer(output_buffer); +} + +void FS_USER::GetNandCid(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 size = rp.Pop(); + auto& output_buffer = rp.PopMappedBuffer(); + + // All-zeroes dummy NAND CID, consistent with Azahar's all-zeroes ID0/ID1. + static constexpr std::array nand_cid = {}; + output_buffer.Write(nand_cid.data(), 0, std::min(size, nand_cid.size())); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushMappedBuffer(output_buffer); +} + void FS_USER::CreateExtSaveData(Kernel::HLERequestContext& ctx) { // TODO(Subv): Figure out the other parameters. IPC::RequestParser rp(ctx); @@ -2026,8 +2058,8 @@ FS_USER::FS_USER(Core::System& system) {0x0816, nullptr, "GetSdmcFatfsError"}, {0x0817, &FS_USER::IsSdmcDetected, "IsSdmcDetected"}, {0x0818, &FS_USER::IsSdmcWriteable, "IsSdmcWritable"}, - {0x0819, nullptr, "GetSdmcCid"}, - {0x081A, nullptr, "GetNandCid"}, + {0x0819, &FS_USER::GetSdmcCid, "GetSdmcCid"}, + {0x081A, &FS_USER::GetNandCid, "GetNandCid"}, {0x081B, nullptr, "GetSdmcSpeedInfo"}, {0x081C, nullptr, "GetNandSpeedInfo"}, {0x081D, nullptr, "GetSdmcLog"}, diff --git a/src/core/hle/service/fs/fs_user.h b/src/core/hle/service/fs/fs_user.h index 3ecbe6768..dee13d0e3 100644 --- a/src/core/hle/service/fs/fs_user.h +++ b/src/core/hle/service/fs/fs_user.h @@ -396,6 +396,28 @@ private: */ void GetNandArchiveResource(Kernel::HLERequestContext& ctx); + /** + * FS_User::GetSdmcCid service function + * Inputs: + * 1 : Output buffer size (must be 0x10) + * 2 : (size << 4) | 0xC (mapped write buffer descriptor) + * 3 : Output buffer pointer + * Outputs: + * 1 : Result code + */ + void GetSdmcCid(Kernel::HLERequestContext& ctx); + + /** + * FS_User::GetNandCid service function + * Inputs: + * 1 : Output buffer size (must be 0x10) + * 2 : (size << 4) | 0xC (mapped write buffer descriptor) + * 3 : Output buffer pointer + * Outputs: + * 1 : Result code + */ + void GetNandCid(Kernel::HLERequestContext& ctx); + /** * FS_User::CreateExtSaveData service function * Inputs: diff --git a/src/core/hle/service/ptm/ptm.cpp b/src/core/hle/service/ptm/ptm.cpp index 7a3baf26a..7f611058b 100644 --- a/src/core/hle/service/ptm/ptm.cpp +++ b/src/core/hle/service/ptm/ptm.cpp @@ -456,6 +456,18 @@ void Module::Interface::IsShutdownByBatteryEmpty(Kernel::HLERequestContext& ctx) rb.Push(false); } +void Module::Interface::ConfigureNew3DSCPU(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + // Bit 0: 804 MHz clock (0 = 268 MHz, 1 = 804 MHz) + // Bit 1: New 3DS L2 cache (0 = disabled, 1 = enabled) + // Azahar always runs as New 3DS; no actual clock/cache switching is needed. + const u32 config = rp.Pop(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + LOG_DEBUG(Service_PTM, "called, config={:02X}", config); +} + void Module::Interface::GetSystemTime(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); diff --git a/src/core/hle/service/ptm/ptm.h b/src/core/hle/service/ptm/ptm.h index cad29697e..913dc050f 100644 --- a/src/core/hle/service/ptm/ptm.h +++ b/src/core/hle/service/ptm/ptm.h @@ -168,6 +168,17 @@ public: void IsShutdownByBatteryEmpty(Kernel::HLERequestContext& ctx); + /** + * PTM::ConfigureNew3DSCPU service function + * Inputs: + * 1 : u32 config bitfield + * bit 0 = clock rate (0 = 268 MHz, 1 = 804 MHz) + * bit 1 = L2 cache (0 = disabled, 1 = enabled) + * Outputs: + * 1 : Result code + */ + void ConfigureNew3DSCPU(Kernel::HLERequestContext& ctx); + /** * PTM::GetSystemTime service function * Outputs: diff --git a/src/core/hle/service/ptm/ptm_sysm.cpp b/src/core/hle/service/ptm/ptm_sysm.cpp index af53345da..10f0823b9 100644 --- a/src/core/hle/service/ptm/ptm_sysm.cpp +++ b/src/core/hle/service/ptm/ptm_sysm.cpp @@ -61,7 +61,7 @@ PTM_S_Common::PTM_S_Common(std::shared_ptr ptm, const char* name) {0x0812, &PTM_S_Common::IsShutdownByBatteryEmpty, "IsShutdownByBatteryEmpty"}, {0x0813, nullptr, "FormatSavedata"}, {0x0814, nullptr, "GetLegacyJumpProhibitedFlag"}, - {0x0818, nullptr, "ConfigureNew3DSCPU"}, + {0x0818, &PTM_S_Common::ConfigureNew3DSCPU, "ConfigureNew3DSCPU"}, // clang-format on }; RegisterHandlers(functions);