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.
This commit is contained in:
Masamune3210 2026-05-15 10:02:12 -05:00
parent d19e6086fa
commit f0ba10239a
10 changed files with 757 additions and 24 deletions

View file

@ -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<u32>(title_id >> 32) == 0x00040000;
}
static bool IsPtmRecordableTitle(u64 title_id) {
const u32 title_id_high = static_cast<u32>(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<Kernel::Object> object,
const std::vector<u8>& 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<Kernel::Object> object,
const std::vector<u8>& 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<Kernel::Object> 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<Kern
const std::vector<u8>& 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_ptr<Kern
// If a system applet is not already registered, it is started by APT.
const auto slot_id =
applet_id == AppletId::HomeMenu ? AppletSlot::HomeMenu : AppletSlot::SystemApplet;
if (!GetAppletSlot(slot_id)->registered) {
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_ptr<Kern
}
active_slot = slot_id;
if (applet_was_registered) {
RecordSlotPtmPlayEvent(slot_id, play_event_application_resume, play_event_applet_resume,
"StartSystemApplet");
} else {
auto cfg = Service::CFG::GetModule(system);
RecordPtmPlayEvent(system, GetTitleIdForApplet(applet_id, cfg->GetRegionValue(false)),
play_event_applet_launch, "StartSystemApplet");
}
SendApplicationParameterAfterRegistration({
.sender_id = source_applet_id,
@ -902,8 +1009,14 @@ Result AppletManager::CloseSystemApplet(std::shared_ptr<Kernel::Object> 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<Kernel::Object> 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<Kernel::Object> object,
const std::vector<u8>& 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<Kernel::Object> 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<Kernel::Object> 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<u8>& 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");
}
}

View file

@ -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.

View file

@ -2,6 +2,7 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <cryptopp/aes.h>
#include <cryptopp/cmac.h>
#include <cryptopp/modes.h>
@ -1492,6 +1493,34 @@ void FS_USER::GetNumSeeds(Kernel::HLERequestContext& ctx) {
rb.Push<u32>(FileSys::GetSeedCount());
}
void FS_USER::ListSeeds(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
const u32 max_count = rp.Pop<u32>();
auto& output_buffer = rp.PopMappedBuffer();
FileSys::SeedDB db;
std::vector<u64_le> seed_title_ids;
if (db.Load()) {
const u32 count = std::min(max_count, static_cast<u32>(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<u32>(static_cast<u32>(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<u64>()};
@ -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<u32>(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<u32>();
auto& output_buffer = rp.PopMappedBuffer();
IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
rb.Push(ResultSuccess);
rb.Push<u32>(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<u64>();
@ -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
};

View file

@ -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:

View file

@ -2,6 +2,10 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <array>
#include <cstring>
#include <vector>
#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<PlayHistoryEntry> 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<u32>((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<u32>(value & PLAY_HISTORY_TIMESTAMP_MASK);
}
static u32 GetPlayHistoryEntryTimestamp(const PlayHistoryEntry& entry) {
return static_cast<u32>(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<FileSys::ArchiveBackend> 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<FileSys::FileBackend> 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<const u8*>(&header));
play_history->Write(PLAY_HISTORY_ENTRIES_OFFSET, data.entries.size() * sizeof(PlayHistoryEntry),
true, false, reinterpret_cast<const u8*>(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<u8*>(&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<u8*>(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<u32>(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<u32>(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<u32, 5>& params) {
for (std::size_t i = 0; i + 1 < params.size(); ++i) {
const u64 candidate = static_cast<u64>(params[i]) | (static_cast<u64>(params[i + 1]) << 32);
if (candidate == PLAY_HISTORY_SPECIAL_TITLE_ID || IsLikelyTitleId(candidate)) {
return candidate;
}
}
return GetCurrentProcessTitleId(system);
}
static u32 PickNotifyPlayEventType(const std::array<u32, 5>& 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<u32>();
const u32 start_time = rp.Pop<u32>();
auto& steps_buffer = rp.PopMappedBuffer();
auto& timestamps_buffer = rp.PopMappedBuffer();
if (steps_buffer.GetSize() != 0) {
std::vector<u8> steps(steps_buffer.GetSize());
steps_buffer.Write(steps.data(), 0, steps.size());
}
if (timestamps_buffer.GetSize() != 0) {
std::vector<u8> 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<u32>();
const u32 total_entries = rp.Pop<u32>();
auto& buffer = rp.PopMappedBuffer();
const auto entries = ptm->GetPlayHistoryEntries(entry_offset, total_entries);
if (!entries.empty()) {
std::vector<PlayHistoryIpcEntry> 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<std::size_t>(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<u32>(static_cast<u32>(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<u64>();
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<u32, 5> params{
rp.Pop<u32>(), rp.Pop<u32>(), rp.Pop<u32>(), rp.Pop<u32>(), rp.Pop<u32>()};
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 <class Archive>
@ -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<PlayHistoryEntry> Module::GetPlayHistoryEntries(u32 offset, u32 count) const {
const PlayHistoryData data = ReadPlayHistoryData(true);
std::vector<PlayHistoryEntry> 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<u32>(title_id >> 32),
.title_id_low = static_cast<u32>(title_id),
.info_timestamp =
static_cast<u32>((GetPlayHistoryTimestamp(system) << 4) | (event_type & 0xF)),
};
WritePlayHistoryData(data);
}
Module::Interface::Interface(std::shared_ptr<Module> ptm, const char* name, u32 max_session)
: ServiceFramework(name, max_session), ptm(std::move(ptm)) {}
std::shared_ptr<Module> Module::Interface::GetModule() const {
return ptm;
}
std::shared_ptr<Module> GetModule(Core::System& system) {
auto ptm = system.ServiceManager().GetService<Module::Interface>("ptm:play");
if (!ptm) {
return nullptr;
}
return ptm->GetModule();
}
void InstallInterfaces(Core::System& system) {
auto& service_manager = system.ServiceManager();
auto ptm = std::make_shared<Module>(system);

View file

@ -4,7 +4,9 @@
#pragma once
#include <array>
#include <memory>
#include <vector>
#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<Interface> {
public:
Interface(std::shared_ptr<Module> ptm, const char* name, u32 max_session);
std::shared_ptr<Module> 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<Module> ptm;
};
@ -164,6 +199,13 @@ private:
bool battery_is_charging = true;
bool pedometer_is_counting = false;
std::vector<PlayHistoryEntry> 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 <class Archive>
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<Module> GetModule(Core::System& system);
} // namespace Service::PTM
BOOST_CLASS_EXPORT_KEY(Service::PTM::Module)

View file

@ -14,7 +14,7 @@ PTM_Gets::PTM_Gets(std::shared_ptr<Module> 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<Module> 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

View file

@ -14,7 +14,7 @@ PTM_Play::PTM_Play(std::shared_ptr<Module> 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<Module> 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);

View file

@ -15,7 +15,7 @@ PTM_S_Common::PTM_S_Common(std::shared_ptr<Module> 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<Module> 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<Module> 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"},

View file

@ -12,7 +12,7 @@ namespace Service::PTM {
PTM_U::PTM_U(std::shared_ptr<Module> 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<Module> 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);