From f0ba10239ac4a8fddacd00a4b0193b0388591244 Mon Sep 17 00:00:00 2001 From: Masamune3210 <1053504+Masamune3210@users.noreply.github.com> Date: Fri, 15 May 2026 10:02:12 -0500 Subject: [PATCH] ptm: implement Activity Log play history Add PTM PlayHistory.dat support backed by system savedata 00010022, including read/write helpers, play-history IPC handlers, NotifyPlayEvent, ClearPlayHistory, CalcPlayHistoryStart, and zero-filled empty-state repair. Record play events from APT application, HOME Menu, library applet, and system applet lifecycle transitions so Activity Log can aggregate valid sessions from regenerated saves. Add fs:USER handlers for ListSeeds, GetNumTitleTags, and ListTitleTags used by Activity Log library scans, and wire remaining PTM common handlers needed by the app. Validation: git diff --check; user confirmed regenerated PTM and Activity Log saves display correctly. --- src/core/hle/service/apt/applet_manager.cpp | 147 +++++- src/core/hle/service/apt/applet_manager.h | 3 + src/core/hle/service/fs/fs_user.cpp | 54 +++ src/core/hle/service/fs/fs_user.h | 6 + src/core/hle/service/ptm/ptm.cpp | 489 ++++++++++++++++++++ src/core/hle/service/ptm/ptm.h | 44 ++ src/core/hle/service/ptm/ptm_gets.cpp | 4 +- src/core/hle/service/ptm/ptm_play.cpp | 12 +- src/core/hle/service/ptm/ptm_sysm.cpp | 18 +- src/core/hle/service/ptm/ptm_u.cpp | 4 +- 10 files changed, 757 insertions(+), 24 deletions(-) diff --git a/src/core/hle/service/apt/applet_manager.cpp b/src/core/hle/service/apt/applet_manager.cpp index 8ee778f89..fb3a4fc94 100644 --- a/src/core/hle/service/apt/applet_manager.cpp +++ b/src/core/hle/service/apt/applet_manager.cpp @@ -18,6 +18,7 @@ #include "core/hle/service/apt/ns.h" #include "core/hle/service/cfg/cfg.h" #include "core/hle/service/gsp/gsp_gpu.h" +#include "core/hle/service/ptm/ptm.h" #include "video_core/utils.h" SERVICE_CONSTRUCT_IMPL(Service::APT::AppletManager) @@ -28,6 +29,48 @@ namespace Service::APT { static constexpr u64 button_update_interval_us = 16666; /// The interval at which the HLE Applet update callback will be called, 16.6ms. static constexpr u64 hle_applet_update_interval_us = 16666; +static constexpr u32 play_event_application_launch = 0b0000; +static constexpr u32 play_event_application_close = 0b0001; +static constexpr u32 play_event_applet_launch = 0b0010; +static constexpr u32 play_event_applet_close = 0b0011; +static constexpr u32 play_event_application_resume = 0b0100; +static constexpr u32 play_event_application_suspend = 0b0101; +static constexpr u32 play_event_applet_resume = 0b0110; +static constexpr u32 play_event_applet_suspend = 0b0111; + +static u64 GetTitleIdForApplet(AppletId id, u32 region_value); + +static bool IsPtmRecordableApplicationTitle(u64 title_id) { + return static_cast(title_id >> 32) == 0x00040000; +} + +static bool IsPtmRecordableTitle(u64 title_id) { + const u32 title_id_high = static_cast(title_id >> 32); + return title_id_high == 0x00040000 || title_id_high == 0x00040010 || + title_id_high == 0x00040030 || title_id_high == 0x00048004 || + title_id_high == 0x00048005 || title_id == ~u64{0}; +} + +static void RecordPtmPlayEvent(Core::System& system, u64 title_id, u32 event_type, + [[maybe_unused]] const char* source) { + if (!IsPtmRecordableTitle(title_id)) { + return; + } + + auto ptm = PTM::GetModule(system); + if (!ptm) { + return; + } + + ptm->RecordPlayEvent(title_id, event_type); +} + +static void RecordHomeMenuPtmPlayEvent(Core::System& system, u32 event_type, const char* source) { + auto cfg = Service::CFG::GetModule(system); + const u64 home_menu_title_id = + GetTitleIdForApplet(AppletId::HomeMenu, cfg->GetRegionValue(false)); + RecordPtmPlayEvent(system, home_menu_title_id, event_type, source); +} struct AppletTitleData { // There are two possible applet ids for each applet. @@ -246,6 +289,45 @@ AppletManager::AppletSlot AppletManager::GetAppletSlotFromPos(AppletPos pos) { return GetAppletSlotFromId(applet_id); } +u64 AppletManager::GetAppletSlotTitleId(AppletSlot slot) { + if (slot == AppletSlot::Error) { + return 0; + } + + auto slot_data = GetAppletSlot(slot); + if (slot_data->title_id != 0) { + return slot_data->title_id; + } + + if (slot_data->applet_id == AppletId::None) { + return 0; + } + + auto cfg = Service::CFG::GetModule(system); + return GetTitleIdForApplet(slot_data->applet_id, cfg->GetRegionValue(false)); +} + +void AppletManager::RecordSlotPtmPlayEvent(AppletSlot slot, u32 application_event, + u32 applet_event, const char* source) { + if (slot == AppletSlot::Error) { + return; + } + + auto slot_data = GetAppletSlot(slot); + if (!slot_data->registered && slot_data->applet_id == AppletId::None) { + return; + } + + const u64 title_id = GetAppletSlotTitleId(slot); + if (title_id == 0) { + return; + } + + const u32 event_type = + slot == AppletSlot::Application ? application_event : applet_event; + RecordPtmPlayEvent(system, title_id, event_type, source); +} + void AppletManager::CancelAndSendParameter(const MessageParameter& parameter) { // If the applet is being HLEd, send directly to the applet. const auto applet = hle_applets[parameter.destination_id]; @@ -676,7 +758,12 @@ Result AppletManager::FinishPreloadingLibraryApplet(AppletId applet_id) { Result AppletManager::StartLibraryApplet(AppletId applet_id, std::shared_ptr object, const std::vector& buffer) { + RecordSlotPtmPlayEvent(last_library_launcher_slot, play_event_application_suspend, + play_event_applet_suspend, "StartLibraryApplet"); active_slot = AppletSlot::LibraryApplet; + auto cfg = Service::CFG::GetModule(system); + RecordPtmPlayEvent(system, GetTitleIdForApplet(applet_id, cfg->GetRegionValue(false)), + play_event_applet_launch, "StartLibraryApplet"); auto send_res = SendParameter({ .sender_id = GetAppletSlotId(last_library_launcher_slot), @@ -715,6 +802,15 @@ Result AppletManager::CloseLibraryApplet(std::shared_ptr object, const std::vector& buffer) { auto slot = GetAppletSlot(AppletSlot::LibraryApplet); auto destination_id = GetAppletSlotId(last_library_launcher_slot); + const bool paused = library_applet_closing_command == SignalType::WakeupByPause; + + RecordSlotPtmPlayEvent(AppletSlot::LibraryApplet, + paused ? play_event_application_suspend + : play_event_application_close, + paused ? play_event_applet_suspend : play_event_applet_close, + "CloseLibraryApplet"); + RecordSlotPtmPlayEvent(last_library_launcher_slot, play_event_application_resume, + play_event_applet_resume, "CloseLibraryApplet"); active_slot = last_library_launcher_slot; @@ -726,7 +822,7 @@ Result AppletManager::CloseLibraryApplet(std::shared_ptr object, .buffer = buffer, }; - if (library_applet_closing_command != SignalType::WakeupByPause) { + if (!paused) { CancelAndSendParameter(param); // TODO: Terminate the running applet title slot->Reset(); @@ -839,6 +935,8 @@ Result AppletManager::StartSystemApplet(AppletId applet_id, std::shared_ptr& buffer) { auto source_applet_id = AppletId::Application; if (last_system_launcher_slot != AppletSlot::Error) { + RecordSlotPtmPlayEvent(last_system_launcher_slot, play_event_application_suspend, + play_event_applet_suspend, "StartSystemApplet"); const auto launcher_slot_data = GetAppletSlot(last_system_launcher_slot); source_applet_id = launcher_slot_data->applet_id; @@ -860,7 +958,8 @@ Result AppletManager::StartSystemApplet(AppletId applet_id, std::shared_ptrregistered) { + const bool applet_was_registered = GetAppletSlot(slot_id)->registered; + if (!applet_was_registered) { bool is_setup = system.GetAppLoader().DoingInitialSetup(); auto cfg = Service::CFG::GetModule(system); auto process = @@ -874,6 +973,14 @@ Result AppletManager::StartSystemApplet(AppletId applet_id, std::shared_ptrGetRegionValue(false)), + play_event_applet_launch, "StartSystemApplet"); + } SendApplicationParameterAfterRegistration({ .sender_id = source_applet_id, @@ -902,8 +1009,14 @@ Result AppletManager::CloseSystemApplet(std::shared_ptr object, auto slot = GetAppletSlot(active_slot); auto closed_applet_id = slot->applet_id; + const auto closed_slot = active_slot; + + RecordSlotPtmPlayEvent(closed_slot, play_event_application_close, play_event_applet_close, + "CloseSystemApplet"); active_slot = last_system_launcher_slot; + RecordSlotPtmPlayEvent(active_slot, play_event_application_resume, play_event_applet_resume, + "CloseSystemApplet"); slot->Reset(); if (ordered_to_close_sys_applet) { @@ -988,6 +1101,9 @@ Result AppletManager::JumpToHomeMenu(std::shared_ptr object, switch (slot_data->attributes.applet_pos) { case AppletPos::Application: + RecordPtmPlayEvent(system, slot_data->title_id, play_event_application_suspend, + "JumpToHomeMenu"); + RecordHomeMenuPtmPlayEvent(system, play_event_applet_resume, "JumpToHomeMenu"); active_slot = AppletSlot::HomeMenu; param.destination_id = AppletId::HomeMenu; @@ -1044,6 +1160,9 @@ Result AppletManager::PrepareToLeaveHomeMenu() { Result AppletManager::LeaveHomeMenu(std::shared_ptr object, const std::vector& buffer) { active_slot = AppletSlot::Application; + RecordHomeMenuPtmPlayEvent(system, play_event_applet_suspend, "LeaveHomeMenu"); + RecordPtmPlayEvent(system, GetAppletSlot(AppletSlot::Application)->title_id, + play_event_application_resume, "LeaveHomeMenu"); // If no event was provided (HOME Menu passed handle=0), supply a pre-signaled event so the // application can safely call svcSignalEvent on the received handle without crashing. @@ -1095,6 +1214,9 @@ Result AppletManager::OrderToCloseApplication() { } ordered_to_close_application = true; + RecordHomeMenuPtmPlayEvent(system, play_event_applet_suspend, "OrderToCloseApplication"); + RecordPtmPlayEvent(system, GetAppletSlot(AppletSlot::Application)->title_id, + play_event_application_resume, "OrderToCloseApplication"); active_slot = AppletSlot::Application; SendParameter({ @@ -1162,7 +1284,10 @@ Result AppletManager::CloseApplication(std::shared_ptr object, ordered_to_close_application = false; application_cancelled = false; - GetAppletSlot(AppletSlot::Application)->Reset(); + auto application_slot = GetAppletSlot(AppletSlot::Application); + RecordPtmPlayEvent(system, application_slot->title_id, play_event_application_close, + "CloseApplication"); + application_slot->Reset(); if (application_close_target != AppletSlot::Error) { // If exiting to the home menu and it is not loaded, exit to game list. @@ -1171,6 +1296,8 @@ Result AppletManager::CloseApplication(std::shared_ptr object, system.RequestShutdown(); } else { active_slot = application_close_target; + RecordSlotPtmPlayEvent(application_close_target, play_event_application_resume, + play_event_applet_resume, "CloseApplication"); CancelAndSendParameter({ .sender_id = AppletId::Application, @@ -1495,14 +1622,22 @@ Result AppletManager::StartApplication(const std::vector& parameter, // PM::LaunchTitle. We should research more about that. ASSERT_MSG(app_start_parameters, "Trying to start an application without preparing it first."); + const u64 next_title_id = app_start_parameters->next_title_id; + if (IsPtmRecordableApplicationTitle(next_title_id) && + (active_slot == AppletSlot::HomeMenu || GetAppletSlot(AppletSlot::HomeMenu)->registered)) { + RecordHomeMenuPtmPlayEvent(system, + play_event_applet_suspend, "StartApplication"); + } active_slot = AppletSlot::Application; // Launch the title directly. - auto process = NS::LaunchTitle(system, app_start_parameters->next_media_type, - app_start_parameters->next_title_id); + auto process = NS::LaunchTitle(system, app_start_parameters->next_media_type, next_title_id); if (!process) { LOG_CRITICAL(Service_APT, "Failed to launch title during application start, exiting."); system.RequestShutdown(); + } else { + RecordPtmPlayEvent(system, next_title_id, play_event_application_launch, + "StartApplication"); } app_start_parameters.reset(); @@ -1617,6 +1752,8 @@ void AppletManager::EnsureHomeMenuLoaded() { if (!process) { LOG_WARNING(Service_APT, "The Home Menu failed to launch, application jumping will not work."); + } else { + RecordPtmPlayEvent(system, menu_title_id, play_event_applet_launch, "EnsureHomeMenuLoaded"); } } diff --git a/src/core/hle/service/apt/applet_manager.h b/src/core/hle/service/apt/applet_manager.h index 0def102d3..f2f65473f 100644 --- a/src/core/hle/service/apt/applet_manager.h +++ b/src/core/hle/service/apt/applet_manager.h @@ -540,6 +540,9 @@ private: AppletSlot GetAppletSlotFromId(AppletId id); AppletSlot GetAppletSlotFromAttributes(AppletAttributes attributes); AppletSlot GetAppletSlotFromPos(AppletPos pos); + u64 GetAppletSlotTitleId(AppletSlot slot); + void RecordSlotPtmPlayEvent(AppletSlot slot, u32 application_event, u32 applet_event, + const char* source); /// Checks if the Application slot has already been registered and sends the parameter to it, /// otherwise it queues for sending when the application registers itself with APT::Enable. diff --git a/src/core/hle/service/fs/fs_user.cpp b/src/core/hle/service/fs/fs_user.cpp index ba08cbadc..1e2225a24 100644 --- a/src/core/hle/service/fs/fs_user.cpp +++ b/src/core/hle/service/fs/fs_user.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include #include @@ -1492,6 +1493,34 @@ void FS_USER::GetNumSeeds(Kernel::HLERequestContext& ctx) { rb.Push(FileSys::GetSeedCount()); } +void FS_USER::ListSeeds(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 max_count = rp.Pop(); + auto& output_buffer = rp.PopMappedBuffer(); + + FileSys::SeedDB db; + std::vector seed_title_ids; + if (db.Load()) { + const u32 count = std::min(max_count, static_cast(db.seeds.size())); + seed_title_ids.reserve(count); + for (u32 i = 0; i < count; ++i) { + seed_title_ids.push_back(db.seeds[i].title_id); + } + if (!seed_title_ids.empty()) { + output_buffer.Write(seed_title_ids.data(), 0, + seed_title_ids.size() * sizeof(seed_title_ids[0])); + } + } + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); + rb.Push(ResultSuccess); + rb.Push(static_cast(seed_title_ids.size())); + rb.PushMappedBuffer(output_buffer); + + LOG_DEBUG(Service_FS, "called, max_count={} returned={}", max_count, + seed_title_ids.size()); +} + void FS_USER::AddSeed(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); u64 title_id{rp.Pop()}; @@ -1558,6 +1587,28 @@ void FS_USER::GetUnknown0x80Data(Kernel::HLERequestContext& ctx) { LOG_WARNING(Service_FS, "(STUBBED) title_id={:016X}", title_id); } +void FS_USER::GetNumTitleTags(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(0); + + LOG_DEBUG(Service_FS, "(STUBBED) returning no title tags"); +} + +void FS_USER::ListTitleTags(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 max_count = rp.Pop(); + auto& output_buffer = rp.PopMappedBuffer(); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); + rb.Push(ResultSuccess); + rb.Push(0); + rb.PushMappedBuffer(output_buffer); + + LOG_DEBUG(Service_FS, "(STUBBED) max_count={} returned=0", max_count); +} + void FS_USER::ObsoletedSetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const u64 value = rp.Pop(); @@ -2066,8 +2117,11 @@ FS_USER::FS_USER(Core::System& system) {0x087B, &FS_USER::GetSeed, "GetSeed"}, {0x087C, &FS_USER::DeleteSeed, "GetSeed"}, {0x087D, &FS_USER::GetNumSeeds, "GetNumSeeds"}, + {0x087E, &FS_USER::ListSeeds, "ListSeeds"}, {0x0880, &FS_USER::SetUnknown0x80Data, "SetUnknown0x80Data"}, {0x0881, &FS_USER::GetUnknown0x80Data, "GetUnknown0x80Data"}, + {0x0883, &FS_USER::GetNumTitleTags, "GetNumTitleTags"}, + {0x0884, &FS_USER::ListTitleTags, "ListTitleTags"}, {0x0886, nullptr, "CheckUpdatedDat"}, // clang-format on }; diff --git a/src/core/hle/service/fs/fs_user.h b/src/core/hle/service/fs/fs_user.h index 1d4c5b377..3ecbe6768 100644 --- a/src/core/hle/service/fs/fs_user.h +++ b/src/core/hle/service/fs/fs_user.h @@ -729,10 +729,16 @@ private: */ void GetNumSeeds(Kernel::HLERequestContext& ctx); + void ListSeeds(Kernel::HLERequestContext& ctx); + void SetUnknown0x80Data(Kernel::HLERequestContext& ctx); void GetUnknown0x80Data(Kernel::HLERequestContext& ctx); + void GetNumTitleTags(Kernel::HLERequestContext& ctx); + + void ListTitleTags(Kernel::HLERequestContext& ctx); + /** * FS_User::SetSaveDataSecureValue service function. * Inputs: diff --git a/src/core/hle/service/ptm/ptm.cpp b/src/core/hle/service/ptm/ptm.cpp index f0e5769e1..55916a6c9 100644 --- a/src/core/hle/service/ptm/ptm.cpp +++ b/src/core/hle/service/ptm/ptm.cpp @@ -2,6 +2,10 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include +#include +#include +#include #include "common/archives.h" #include "common/common_paths.h" #include "common/file_util.h" @@ -9,8 +13,10 @@ #include "common/settings.h" #include "core/core.h" #include "core/file_sys/archive_extsavedata.h" +#include "core/file_sys/archive_systemsavedata.h" #include "core/file_sys/errors.h" #include "core/file_sys/file_backend.h" +#include "core/hle/kernel/process.h" #include "core/hle/kernel/shared_page.h" #include "core/hle/service/mcu/mcu_rtc.h" #include "core/hle/service/ptm/ptm.h" @@ -28,6 +34,256 @@ namespace Service::PTM { /// Values for the default gamecoin.dat file static const GameCoin default_game_coin = {0x4F00, 42, 0, 0, 0, 2014, 12, 29}; +constexpr u32 PTM_SYSTEM_SAVE_DATA_HIGH = 0; +constexpr u32 PTM_SYSTEM_SAVE_DATA_LOW = 0x00010022; +constexpr u32 PLAY_HISTORY_MAX_ENTRIES = 0x11D28; +constexpr u32 PLAY_HISTORY_FILE_SIZE = 0xD5DE8; +constexpr u32 PLAY_HISTORY_ENTRIES_OFFSET = 0x8; +constexpr u64 PLAY_HISTORY_SPECIAL_TITLE_ID = 0xFFFFFFFFFFFFFFFF; +constexpr u32 PLAY_HISTORY_EVENT_MASK = 0xF; +constexpr u32 PLAY_HISTORY_TIMESTAMP_MASK = 0x0FFFFFFF; +constexpr u64 MILLISECONDS_BETWEEN_1900_AND_2000 = 3155673600000ULL; + +struct PlayHistoryHeader { + u32_le start_index; + u32_le total_entries; +}; +static_assert(sizeof(PlayHistoryHeader) == PLAY_HISTORY_ENTRIES_OFFSET, + "PlayHistoryHeader size is wrong"); + +struct PlayHistoryData { + u32 start_index = 0; + u32 total_entries = 0; + std::vector entries; +}; + +struct PlayHistoryIpcEntry { + u32_le title_id_high; + u32_le title_id_low; + u32_le info_timestamp; +}; +static_assert(sizeof(PlayHistoryIpcEntry) == sizeof(PlayHistoryEntry), + "PlayHistoryIpcEntry size is wrong"); + +static u32 GetPlayHistoryTimestamp(Core::System& system) { + const u64 system_time_ms = system.Kernel().GetSharedPageHandler().GetSystemTimeSince2000(); + return static_cast((system_time_ms / 60000) & PLAY_HISTORY_TIMESTAMP_MASK); +} + +static u32 ConvertToPlayHistoryTimestamp(u64 value) { + if (value > MILLISECONDS_BETWEEN_1900_AND_2000) { + value -= MILLISECONDS_BETWEEN_1900_AND_2000; + } + + if (value > PLAY_HISTORY_TIMESTAMP_MASK) { + value /= 60000; + } + + return static_cast(value & PLAY_HISTORY_TIMESTAMP_MASK); +} + +static u32 GetPlayHistoryEntryTimestamp(const PlayHistoryEntry& entry) { + return static_cast(entry.info_timestamp) >> 4; +} + +static bool IsKnownTitleIdHigh(u32 title_id_high) { + return title_id_high >= 0x00040000 && title_id_high <= 0x00048FFF; +} + +static bool IsValidPlayHistoryData(const PlayHistoryData& data) { + return data.start_index < PLAY_HISTORY_MAX_ENTRIES && + data.total_entries <= PLAY_HISTORY_MAX_ENTRIES && + data.entries.size() == PLAY_HISTORY_MAX_ENTRIES; +} + +static FileSys::Path GetPtmSystemSaveDataPath() { + return FileSys::ConstructSystemSaveDataBinaryPath(PTM_SYSTEM_SAVE_DATA_HIGH, + PTM_SYSTEM_SAVE_DATA_LOW); +} + +static std::unique_ptr OpenPtmSystemSaveDataArchive(bool create) { + const std::string& nand_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); + FileSys::ArchiveFactory_SystemSaveData systemsavedata_archive_factory(nand_directory); + const FileSys::Path archive_path = GetPtmSystemSaveDataPath(); + + auto initial_archive_result = systemsavedata_archive_factory.Open(archive_path, 0); + if (initial_archive_result.Succeeded()) { + return std::move(initial_archive_result).Unwrap(); + } + + if (create) { + const FileSys::ArchiveFormatInfo format_info{ + .total_size = PLAY_HISTORY_FILE_SIZE, + .number_directories = 0, + .number_files = 2, + .duplicate_data = 0, + }; + systemsavedata_archive_factory.Format(archive_path, format_info, 0, 1, 2); + + auto created_archive_result = systemsavedata_archive_factory.Open(archive_path, 0); + if (created_archive_result.Succeeded()) { + return std::move(created_archive_result).Unwrap(); + } + } + + return nullptr; +} + +static std::unique_ptr OpenPlayHistoryFile(FileSys::ArchiveBackend& archive, + bool create) { + FileSys::Path play_history_path("/PlayHistory.dat"); + FileSys::Mode open_mode = {}; + open_mode.read_flag.Assign(1); + open_mode.write_flag.Assign(1); + + auto initial_play_history_result = archive.OpenFile(play_history_path, open_mode); + if (initial_play_history_result.Succeeded()) { + auto play_history = std::move(initial_play_history_result).Unwrap(); + if (play_history->GetSize() != PLAY_HISTORY_FILE_SIZE) { + play_history->SetSize(PLAY_HISTORY_FILE_SIZE); + } + return play_history; + } + + if (create) { + archive.CreateFile(play_history_path, PLAY_HISTORY_FILE_SIZE); + auto created_play_history_result = archive.OpenFile(play_history_path, open_mode); + if (created_play_history_result.Succeeded()) { + auto play_history = std::move(created_play_history_result).Unwrap(); + if (play_history->GetSize() != PLAY_HISTORY_FILE_SIZE) { + play_history->SetSize(PLAY_HISTORY_FILE_SIZE); + } + return play_history; + } + } + + return nullptr; +} + +static void WritePlayHistoryData(const PlayHistoryData& data) { + auto archive = OpenPtmSystemSaveDataArchive(true); + if (!archive) { + LOG_ERROR(Service_PTM, "Could not open PTM SystemSaveData archive!"); + return; + } + + auto play_history = OpenPlayHistoryFile(*archive, true); + if (!play_history) { + LOG_ERROR(Service_PTM, "Could not open PlayHistory.dat!"); + return; + } + + PlayHistoryHeader header{ + .start_index = data.start_index, + .total_entries = data.total_entries, + }; + play_history->Write(0, sizeof(header), true, false, reinterpret_cast(&header)); + play_history->Write(PLAY_HISTORY_ENTRIES_OFFSET, data.entries.size() * sizeof(PlayHistoryEntry), + true, false, reinterpret_cast(data.entries.data())); + play_history->Close(); +} + +static PlayHistoryData MakeEmptyPlayHistoryData() { + PlayHistoryData data; + data.entries.resize(PLAY_HISTORY_MAX_ENTRIES); + for (auto& entry : data.entries) { + entry = PlayHistoryEntry{ + .title_id_high = 0xFFFFFFFF, + .title_id_low = 0xFFFFFFFF, + .info_timestamp = 0xFFFFFFFF, + }; + } + return data; +} + +static PlayHistoryData ReadPlayHistoryData(bool create) { + auto archive = OpenPtmSystemSaveDataArchive(create); + if (!archive) { + return MakeEmptyPlayHistoryData(); + } + + auto play_history = OpenPlayHistoryFile(*archive, create); + if (!play_history) { + return MakeEmptyPlayHistoryData(); + } + + PlayHistoryData data; + data.entries.resize(PLAY_HISTORY_MAX_ENTRIES); + + PlayHistoryHeader header{}; + play_history->Read(0, sizeof(header), reinterpret_cast(&header)); + data.start_index = header.start_index; + data.total_entries = header.total_entries; + play_history->Read(PLAY_HISTORY_ENTRIES_OFFSET, data.entries.size() * sizeof(PlayHistoryEntry), + reinterpret_cast(data.entries.data())); + play_history->Close(); + + bool migrated_swapped_title_words = false; + const u32 entries_to_check = std::min(data.total_entries, PLAY_HISTORY_MAX_ENTRIES); + for (u32 i = 0; i < entries_to_check; ++i) { + auto& entry = data.entries[i]; + if (!IsKnownTitleIdHigh(entry.title_id_high) && + IsKnownTitleIdHigh(static_cast(entry.title_id_low))) { + std::swap(entry.title_id_high, entry.title_id_low); + migrated_swapped_title_words = true; + } + } + + const bool zero_filled_empty_file = + data.total_entries == 0 && !data.entries.empty() && data.entries.front().title_id_high == 0 && + data.entries.front().title_id_low == 0 && data.entries.front().info_timestamp == 0; + + if (!IsValidPlayHistoryData(data) || zero_filled_empty_file) { + data = MakeEmptyPlayHistoryData(); + if (create) { + WritePlayHistoryData(data); + } + } else if (migrated_swapped_title_words && create) { + WritePlayHistoryData(data); + } + + return data; +} + +static bool IsLikelyTitleId(u64 title_id) { + const u32 high = static_cast(title_id >> 32); + return IsKnownTitleIdHigh(high); +} + +static u64 GetCurrentProcessTitleId(Core::System& system) { + const auto process = system.Kernel().GetCurrentProcess(); + if (process && process->codeset) { + return process->codeset->program_id; + } + return 0; +} + +static u64 PickNotifyPlayEventTitleId(Core::System& system, const std::array& params) { + for (std::size_t i = 0; i + 1 < params.size(); ++i) { + const u64 candidate = static_cast(params[i]) | (static_cast(params[i + 1]) << 32); + if (candidate == PLAY_HISTORY_SPECIAL_TITLE_ID || IsLikelyTitleId(candidate)) { + return candidate; + } + } + return GetCurrentProcessTitleId(system); +} + +static u32 PickNotifyPlayEventType(const std::array& params) { + for (u32 param : params) { + if (param <= PLAY_HISTORY_EVENT_MASK) { + return param; + } + } + return 0; +} + +void Module::Interface::RegisterAlarmClient(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + void Module::Interface::GetAdapterState(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); @@ -98,6 +354,34 @@ void Module::Interface::GetStepHistory(Kernel::HLERequestContext& ctx) { hours); } +void Module::Interface::GetStepHistoryAll(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + const u32 hours = rp.Pop(); + const u32 start_time = rp.Pop(); + auto& steps_buffer = rp.PopMappedBuffer(); + auto& timestamps_buffer = rp.PopMappedBuffer(); + + if (steps_buffer.GetSize() != 0) { + std::vector steps(steps_buffer.GetSize()); + steps_buffer.Write(steps.data(), 0, steps.size()); + } + if (timestamps_buffer.GetSize() != 0) { + std::vector timestamps(timestamps_buffer.GetSize()); + timestamps_buffer.Write(timestamps.data(), 0, timestamps.size()); + } + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 4); + rb.Push(ResultSuccess); + rb.PushMappedBuffer(steps_buffer); + rb.PushMappedBuffer(timestamps_buffer); + + LOG_DEBUG(Service_PTM, + "(STUBBED) called, from time(raw): 0x{:x}, hours={}, steps_size={}, " + "timestamps_size={}", + start_time, hours, steps_buffer.GetSize(), timestamps_buffer.GetSize()); +} + void Module::Interface::GetTotalStepCount(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); @@ -190,6 +474,119 @@ void Module::Interface::GetSystemTime(Kernel::HLERequestContext& ctx) { rb.Push(console_time); } +void Module::Interface::GetPlayHistory(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 entry_offset = rp.Pop(); + const u32 total_entries = rp.Pop(); + auto& buffer = rp.PopMappedBuffer(); + + const auto entries = ptm->GetPlayHistoryEntries(entry_offset, total_entries); + if (!entries.empty()) { + std::vector ipc_entries; + ipc_entries.reserve(entries.size()); + for (const auto& entry : entries) { + ipc_entries.push_back({ + .title_id_high = entry.title_id_high, + .title_id_low = entry.title_id_low, + .info_timestamp = entry.info_timestamp, + }); + } + + const std::size_t bytes_to_write = + std::min(ipc_entries.size() * sizeof(PlayHistoryIpcEntry), + buffer.GetSize()); + buffer.Write(ipc_entries.data(), 0, bytes_to_write); + } + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); + rb.Push(ResultSuccess); + rb.Push(static_cast(entries.size())); + rb.PushMappedBuffer(buffer); + + LOG_DEBUG(Service_PTM, "GetPlayHistory offset={} requested={} returned={} buffer_size={}", + entry_offset, total_entries, entries.size(), buffer.GetSize()); +} + +void Module::Interface::GetPlayHistoryStart(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 start = ptm->GetPlayHistoryStart(); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(start); + + LOG_DEBUG(Service_PTM, "GetPlayHistoryStart returned={}", start); +} + +void Module::Interface::GetPlayHistoryLength(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 length = ptm->GetPlayHistoryLength(); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(length); + + LOG_DEBUG(Service_PTM, "GetPlayHistoryLength returned={}", length); +} + +void Module::Interface::ClearPlayHistory(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + ptm->ClearPlayHistory(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + + LOG_DEBUG(Service_PTM, "ClearPlayHistory called"); +} + +void Module::Interface::CalcPlayHistoryStart(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u64 system_time = rp.Pop(); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + const u32 start = ptm->CalcPlayHistoryStart(system_time); + rb.Push(start); + + LOG_DEBUG(Service_PTM, + "CalcPlayHistoryStart raw_input={} normalized_timestamp_minutes={} returned={}", + system_time, ConvertToPlayHistoryTimestamp(system_time), start); +} + +void Module::Interface::NotifyPlayEvent(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const std::array params{ + rp.Pop(), rp.Pop(), rp.Pop(), rp.Pop(), rp.Pop()}; + const u64 title_id = PickNotifyPlayEventTitleId(ptm->system, params); + const u32 event_type = PickNotifyPlayEventType(params); + + if (title_id != 0) { + ptm->NotifyPlayEvent(title_id, event_type); + } else { + LOG_DEBUG(Service_PTM, + "NotifyPlayEvent ignored because no title id could be decoded, raw={:08X} " + "{:08X} {:08X} {:08X} {:08X}", + params[0], params[1], params[2], params[3], params[4]); + } + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + + LOG_DEBUG(Service_PTM, + "NotifyPlayEvent title_id={:016X} event_type={} raw={:08X} {:08X} {:08X} " + "{:08X} {:08X}", + title_id, event_type, params[0], params[1], params[2], params[3], params[4]); +} + +void Module::Interface::ClearSoftwareClosedFlag(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + + LOG_DEBUG(Service_PTM, "(STUBBED) ClearSoftwareClosedFlag called"); +} + static void WriteGameCoinData(GameCoin gamecoin_data) { const std::string& nand_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); FileSys::ArchiveFactory_ExtSaveData extdata_archive_factory(nand_directory, @@ -267,6 +664,8 @@ Module::Module(Core::System& system_) : system(system_) { if (archive_result.Code() == FileSys::ResultNotFormatted) { WriteGameCoinData(default_game_coin); } + + ReadPlayHistoryData(true); } template @@ -290,9 +689,99 @@ void Module::SetPlayCoins(u16 play_coins) { WriteGameCoinData(game_coin); } +void Module::RecordPlayEvent(u64 title_id, u32 event_type) { + NotifyPlayEvent(title_id, event_type); +} + +std::vector Module::GetPlayHistoryEntries(u32 offset, u32 count) const { + const PlayHistoryData data = ReadPlayHistoryData(true); + std::vector entries; + entries.reserve(count); + + u32 visible_index = 0; + for (u32 i = 0; i < data.total_entries && entries.size() < count; ++i) { + const u32 physical_index = (data.start_index + i) % PLAY_HISTORY_MAX_ENTRIES; + const auto& entry = data.entries[physical_index]; + + if (visible_index++ < offset) { + continue; + } + + entries.push_back(entry); + } + return entries; +} + +u32 Module::GetPlayHistoryStart() const { + return ReadPlayHistoryData(true).start_index; +} + +u32 Module::GetPlayHistoryLength() const { + const PlayHistoryData data = ReadPlayHistoryData(true); + return data.total_entries; +} + +u32 Module::CalcPlayHistoryStart(u64 system_time) const { + const u32 timestamp = ConvertToPlayHistoryTimestamp(system_time); + const PlayHistoryData data = ReadPlayHistoryData(true); + + u32 visible_index = 0; + for (u32 i = 0; i < data.total_entries; ++i) { + const u32 physical_index = (data.start_index + i) % PLAY_HISTORY_MAX_ENTRIES; + const auto& entry = data.entries[physical_index]; + const u32 entry_timestamp = GetPlayHistoryEntryTimestamp(entry); + if (entry_timestamp >= timestamp) { + return visible_index; + } + ++visible_index; + } + + return visible_index; +} + +void Module::ClearPlayHistory() { + WritePlayHistoryData(MakeEmptyPlayHistoryData()); +} + +void Module::NotifyPlayEvent(u64 title_id, u32 event_type) { + PlayHistoryData data = ReadPlayHistoryData(true); + if (!IsValidPlayHistoryData(data)) { + data = MakeEmptyPlayHistoryData(); + } + + const u32 write_index = data.total_entries < PLAY_HISTORY_MAX_ENTRIES + ? data.total_entries + : data.start_index; + if (data.total_entries < PLAY_HISTORY_MAX_ENTRIES) { + ++data.total_entries; + } else { + data.start_index = (data.start_index + 1) % PLAY_HISTORY_MAX_ENTRIES; + } + + data.entries[write_index] = PlayHistoryEntry{ + .title_id_high = static_cast(title_id >> 32), + .title_id_low = static_cast(title_id), + .info_timestamp = + static_cast((GetPlayHistoryTimestamp(system) << 4) | (event_type & 0xF)), + }; + WritePlayHistoryData(data); +} + Module::Interface::Interface(std::shared_ptr ptm, const char* name, u32 max_session) : ServiceFramework(name, max_session), ptm(std::move(ptm)) {} +std::shared_ptr Module::Interface::GetModule() const { + return ptm; +} + +std::shared_ptr GetModule(Core::System& system) { + auto ptm = system.ServiceManager().GetService("ptm:play"); + if (!ptm) { + return nullptr; + } + return ptm->GetModule(); +} + void InstallInterfaces(Core::System& system) { auto& service_manager = system.ServiceManager(); auto ptm = std::make_shared(system); diff --git a/src/core/hle/service/ptm/ptm.h b/src/core/hle/service/ptm/ptm.h index 95863350a..4331ec51d 100644 --- a/src/core/hle/service/ptm/ptm.h +++ b/src/core/hle/service/ptm/ptm.h @@ -4,7 +4,9 @@ #pragma once +#include #include +#include #include "common/common_types.h" #include "core/hle/ipc_helpers.h" #include "core/hle/service/service.h" @@ -43,6 +45,18 @@ struct GameCoin { u8 day; }; +/** + * Represents one 0xC-byte entry in PTM SystemSaveData /PlayHistory.dat. + * The low nibble of info_timestamp is the event type; the high 28 bits are minutes since + * 2000-01-01. + */ +struct PlayHistoryEntry { + u32_le title_id_high; + u32_le title_id_low; + u32_le info_timestamp; +}; +static_assert(sizeof(PlayHistoryEntry) == 0xC, "PlayHistoryEntry size is wrong"); + void CheckNew3DS(IPC::RequestBuilder& rb); class Module final { @@ -52,11 +66,14 @@ public: static u16 GetPlayCoins(); static void SetPlayCoins(u16 play_coins); + void RecordPlayEvent(u64 title_id, u32 event_type); class Interface : public ServiceFramework { public: Interface(std::shared_ptr ptm, const char* name, u32 max_session); + std::shared_ptr GetModule() const; + protected: /** * It is unknown if GetAdapterState is the same as GetBatteryChargeState, @@ -102,6 +119,8 @@ public: */ void GetPedometerState(Kernel::HLERequestContext& ctx); + void RegisterAlarmClient(Kernel::HLERequestContext& ctx); + /** * PTM::GetStepHistory service function * Inputs: @@ -114,6 +133,8 @@ public: */ void GetStepHistory(Kernel::HLERequestContext& ctx); + void GetStepHistoryAll(Kernel::HLERequestContext& ctx); + /** * PTM::GetTotalStepCount service function * Outputs: @@ -153,6 +174,20 @@ public: */ void GetSystemTime(Kernel::HLERequestContext& ctx); + void GetPlayHistory(Kernel::HLERequestContext& ctx); + + void GetPlayHistoryStart(Kernel::HLERequestContext& ctx); + + void GetPlayHistoryLength(Kernel::HLERequestContext& ctx); + + void ClearPlayHistory(Kernel::HLERequestContext& ctx); + + void CalcPlayHistoryStart(Kernel::HLERequestContext& ctx); + + void NotifyPlayEvent(Kernel::HLERequestContext& ctx); + + void ClearSoftwareClosedFlag(Kernel::HLERequestContext& ctx); + protected: std::shared_ptr ptm; }; @@ -164,6 +199,13 @@ private: bool battery_is_charging = true; bool pedometer_is_counting = false; + std::vector GetPlayHistoryEntries(u32 offset, u32 count) const; + u32 GetPlayHistoryStart() const; + u32 GetPlayHistoryLength() const; + u32 CalcPlayHistoryStart(u64 system_time) const; + void ClearPlayHistory(); + void NotifyPlayEvent(u64 title_id, u32 event_type); + template void serialize(Archive& ar, const unsigned int); friend class boost::serialization::access; @@ -171,6 +213,8 @@ private: void InstallInterfaces(Core::System& system); +std::shared_ptr GetModule(Core::System& system); + } // namespace Service::PTM BOOST_CLASS_EXPORT_KEY(Service::PTM::Module) diff --git a/src/core/hle/service/ptm/ptm_gets.cpp b/src/core/hle/service/ptm/ptm_gets.cpp index a1a5dc774..7b0ccc519 100644 --- a/src/core/hle/service/ptm/ptm_gets.cpp +++ b/src/core/hle/service/ptm/ptm_gets.cpp @@ -14,7 +14,7 @@ PTM_Gets::PTM_Gets(std::shared_ptr ptm) static const FunctionInfo functions[] = { // ptm:u common commands // clang-format off - {0x0001, nullptr, "RegisterAlarmClient"}, + {0x0001, &PTM_Gets::RegisterAlarmClient, "RegisterAlarmClient"}, {0x0002, nullptr, "SetRtcAlarm"}, {0x0003, nullptr, "GetRtcAlarm"}, {0x0004, nullptr, "CancelRtcAlarm"}, @@ -28,7 +28,7 @@ PTM_Gets::PTM_Gets(std::shared_ptr ptm) {0x000C, &PTM_Gets::GetTotalStepCount, "GetTotalStepCount"}, {0x000D, nullptr, "SetPedometerRecordingMode"}, {0x000E, nullptr, "GetPedometerRecordingMode"}, - {0x000F, nullptr, "GetStepHistoryAll"}, + {0x000F, &PTM_Gets::GetStepHistoryAll, "GetStepHistoryAll"}, // ptm:gets {0x0401, &PTM_Gets::GetSystemTime, "GetSystemTime"}, // clang-format on diff --git a/src/core/hle/service/ptm/ptm_play.cpp b/src/core/hle/service/ptm/ptm_play.cpp index 823b45b80..d6799c88a 100644 --- a/src/core/hle/service/ptm/ptm_play.cpp +++ b/src/core/hle/service/ptm/ptm_play.cpp @@ -14,7 +14,7 @@ PTM_Play::PTM_Play(std::shared_ptr ptm) static const FunctionInfo functions[] = { // ptm:u common commands // clang-format off - {0x0001, nullptr, "RegisterAlarmClient"}, + {0x0001, &PTM_Play::RegisterAlarmClient, "RegisterAlarmClient"}, {0x0002, nullptr, "SetRtcAlarm"}, {0x0003, nullptr, "GetRtcAlarm"}, {0x0004, nullptr, "CancelRtcAlarm"}, @@ -28,12 +28,12 @@ PTM_Play::PTM_Play(std::shared_ptr ptm) {0x000C, &PTM_Play::GetTotalStepCount, "GetTotalStepCount"}, {0x000D, nullptr, "SetPedometerRecordingMode"}, {0x000E, nullptr, "GetPedometerRecordingMode"}, - {0x000F, nullptr, "GetStepHistoryAll"}, + {0x000F, &PTM_Play::GetStepHistoryAll, "GetStepHistoryAll"}, // ptm:play - {0x0807, nullptr, "GetPlayHistory"}, - {0x0808, nullptr, "GetPlayHistoryStart"}, - {0x0809, nullptr, "GetPlayHistoryLength"}, - {0x080B, nullptr, "CalcPlayHistoryStart"}, + {0x0807, &PTM_Play::GetPlayHistory, "GetPlayHistory"}, + {0x0808, &PTM_Play::GetPlayHistoryStart, "GetPlayHistoryStart"}, + {0x0809, &PTM_Play::GetPlayHistoryLength, "GetPlayHistoryLength"}, + {0x080B, &PTM_Play::CalcPlayHistoryStart, "CalcPlayHistoryStart"}, // clang-format on }; RegisterHandlers(functions); diff --git a/src/core/hle/service/ptm/ptm_sysm.cpp b/src/core/hle/service/ptm/ptm_sysm.cpp index aced50518..674b355e8 100644 --- a/src/core/hle/service/ptm/ptm_sysm.cpp +++ b/src/core/hle/service/ptm/ptm_sysm.cpp @@ -15,7 +15,7 @@ PTM_S_Common::PTM_S_Common(std::shared_ptr ptm, const char* name) static const FunctionInfo functions[] = { // ptm:u common commands // clang-format off - {0x0001, nullptr, "RegisterAlarmClient"}, + {0x0001, &PTM_S_Common::RegisterAlarmClient, "RegisterAlarmClient"}, {0x0002, nullptr, "SetRtcAlarm"}, {0x0003, nullptr, "GetRtcAlarm"}, {0x0004, nullptr, "CancelRtcAlarm"}, @@ -29,7 +29,7 @@ PTM_S_Common::PTM_S_Common(std::shared_ptr ptm, const char* name) {0x000C, &PTM_S_Common::GetTotalStepCount, "GetTotalStepCount"}, {0x000D, nullptr, "SetPedometerRecordingMode"}, {0x000E, nullptr, "GetPedometerRecordingMode"}, - {0x000F, nullptr, "GetStepHistoryAll"}, + {0x000F, &PTM_S_Common::GetStepHistoryAll, "GetStepHistoryAll"}, // ptm:sysm & ptm:s {0x0401, nullptr, "SetRtcAlarmEx"}, {0x0402, nullptr, "ReplySleepQuery"}, @@ -47,16 +47,16 @@ PTM_S_Common::PTM_S_Common(std::shared_ptr ptm, const char* name) {0x0804, nullptr, "SetBatteryEmptyLEDPattern"}, {0x0805, nullptr, "ClearStepHistory"}, {0x0806, nullptr, "SetStepHistory"}, - {0x0807, nullptr, "GetPlayHistory"}, - {0x0808, nullptr, "GetPlayHistoryStart"}, - {0x0809, nullptr, "GetPlayHistoryLength"}, - {0x080A, nullptr, "ClearPlayHistory"}, - {0x080B, nullptr, "CalcPlayHistoryStart"}, + {0x0807, &PTM_S_Common::GetPlayHistory, "GetPlayHistory"}, + {0x0808, &PTM_S_Common::GetPlayHistoryStart, "GetPlayHistoryStart"}, + {0x0809, &PTM_S_Common::GetPlayHistoryLength, "GetPlayHistoryLength"}, + {0x080A, &PTM_S_Common::ClearPlayHistory, "ClearPlayHistory"}, + {0x080B, &PTM_S_Common::CalcPlayHistoryStart, "CalcPlayHistoryStart"}, {0x080C, nullptr, "SetUserTime"}, {0x080D, nullptr, "InvalidateSystemTime"}, - {0x080E, nullptr, "NotifyPlayEvent"}, + {0x080E, &PTM_S_Common::NotifyPlayEvent, "NotifyPlayEvent"}, {0x080F, &PTM_S_Common::GetSoftwareClosedFlag, "GetSoftwareClosedFlag"}, - {0x0810, nullptr, "ClearSoftwareClosedFlag"}, + {0x0810, &PTM_S_Common::ClearSoftwareClosedFlag, "ClearSoftwareClosedFlag"}, {0x0811, &PTM_S_Common::GetShellState, "GetShellState"}, {0x0812, nullptr, "IsShutdownByBatteryEmpty"}, {0x0813, nullptr, "FormatSavedata"}, diff --git a/src/core/hle/service/ptm/ptm_u.cpp b/src/core/hle/service/ptm/ptm_u.cpp index 7234a0c0f..aa2610a65 100644 --- a/src/core/hle/service/ptm/ptm_u.cpp +++ b/src/core/hle/service/ptm/ptm_u.cpp @@ -12,7 +12,7 @@ namespace Service::PTM { PTM_U::PTM_U(std::shared_ptr ptm) : Module::Interface(std::move(ptm), "ptm:u", 26) { static const FunctionInfo functions[] = { // clang-format off - {0x0001, nullptr, "RegisterAlarmClient"}, + {0x0001, &PTM_U::RegisterAlarmClient, "RegisterAlarmClient"}, {0x0002, nullptr, "SetRtcAlarm"}, {0x0003, nullptr, "GetRtcAlarm"}, {0x0004, nullptr, "CancelRtcAlarm"}, @@ -26,7 +26,7 @@ PTM_U::PTM_U(std::shared_ptr ptm) : Module::Interface(std::move(ptm), "p {0x000C, &PTM_U::GetTotalStepCount, "GetTotalStepCount"}, {0x000D, nullptr, "SetPedometerRecordingMode"}, {0x000E, nullptr, "GetPedometerRecordingMode"}, - {0x000F, nullptr, "GetStepHistoryAll"}, + {0x000F, &PTM_U::GetStepHistoryAll, "GetStepHistoryAll"}, // clang-format on }; RegisterHandlers(functions);