mirror of
https://github.com/azahar-emu/azahar.git
synced 2026-06-05 18:23:39 -04:00
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:
parent
d19e6086fa
commit
f0ba10239a
10 changed files with 757 additions and 24 deletions
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue