diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 81f98f1e5d..ec7ce42a1b 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1131,6 +1131,8 @@ add_library(core STATIC internal_network/sockets.h internal_network/wifi_scanner.cpp internal_network/wifi_scanner.h + launch_timestamp_cache.cpp + launch_timestamp_cache.h loader/deconstructed_rom_directory.cpp loader/deconstructed_rom_directory.h loader/kip.cpp diff --git a/src/core/core.cpp b/src/core/core.cpp index bf97184f8f..c811aefbae 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -15,6 +15,8 @@ #include "common/string_util.h" #include "core/arm/exclusive_monitor.h" #include "core/core.h" + +#include "launch_timestamp_cache.h" #include "core/core_timing.h" #include "core/cpu_manager.h" #include "core/debugger/debugger.h" @@ -331,6 +333,9 @@ struct System::Impl { LOG_INFO(Core, "Loading {} ({:016X}) ...", name, params.program_id); + // Track launch time for frontend launches + LaunchTimestampCache::SaveLaunchTimestamp(params.program_id); + // Make the process created be the application kernel.MakeApplicationProcess(process->GetHandle()); diff --git a/src/core/hle/service/am/service/application_creator.cpp b/src/core/hle/service/am/service/application_creator.cpp index 2fc33a303c..d16fd7dd84 100644 --- a/src/core/hle/service/am/service/application_creator.cpp +++ b/src/core/hle/service/am/service/application_creator.cpp @@ -15,6 +15,7 @@ #include "core/hle/service/am/window_system.h" #include "core/hle/service/cmif_serialization.h" #include "core/loader/loader.h" +#include "core/launch_timestamp_cache.h" namespace Service::AM { @@ -72,6 +73,7 @@ IApplicationCreator::~IApplicationCreator() = default; Result IApplicationCreator::CreateApplication( Out> out_application_accessor, u64 application_id) { LOG_INFO(Service_NS, "called, application_id={:016X}", application_id); + Core::LaunchTimestampCache::SaveLaunchTimestamp(application_id); R_RETURN( CreateGuestApplication(out_application_accessor, system, m_window_system, application_id)); } @@ -103,6 +105,7 @@ Result IApplicationCreator::CreateSystemApplication( *out_application_accessor = std::make_shared(system, applet, m_window_system); + Core::LaunchTimestampCache::SaveLaunchTimestamp(application_id); R_SUCCEED(); } diff --git a/src/core/hle/service/ns/application_manager_interface.cpp b/src/core/hle/service/ns/application_manager_interface.cpp index 77481a0d98..f989bdc0a8 100644 --- a/src/core/hle/service/ns/application_manager_interface.cpp +++ b/src/core/hle/service/ns/application_manager_interface.cpp @@ -16,6 +16,10 @@ #include "core/hle/service/ns/read_only_application_control_data_interface.h" #include "core/file_sys/patch_manager.h" #include "frontend_common/firmware_manager.h" +#include "core/launch_timestamp_cache.h" + +#include +#include namespace Service::NS { @@ -560,33 +564,42 @@ Result IApplicationManagerInterface::ListApplicationRecord( s32 offset) { const auto limit = out_records.size(); - LOG_WARNING(Service_NS, "(STUBBED) called"); + LOG_DEBUG(Service_NS, "called"); const auto& cache = system.GetContentProviderUnion(); const auto installed_games = cache.ListEntriesFilterOrigin( std::nullopt, FileSys::TitleType::Application, FileSys::ContentRecordType::Program); - size_t i = 0; - u8 ii = 24; + std::vector records; + records.reserve(installed_games.size()); for (const auto& [slot, game] : installed_games) { - if (i >= limit) { - break; - } - if (game.title_id == 0 || game.title_id < 0x0100000000001FFFull) { - continue; - } - if (offset > 0) { - offset--; - continue; - } + if (game.title_id == 0 || game.title_id < 0x0100000000001FFFull) { + continue; + } + if ((game.title_id & 0xFFF) != 0) { + continue; // skip sub-programs (e.g., 001) + } + + ApplicationRecord record{}; + record.application_id = game.title_id; + record.last_event = ApplicationEvent::Installed; + record.attributes = 0; + record.last_updated = Core::LaunchTimestampCache::GetLaunchTimestamp(game.title_id); + + records.push_back(record); + } + + std::sort(records.begin(), records.end(), [](const ApplicationRecord& lhs, const ApplicationRecord& rhs) { + if (lhs.last_updated == rhs.last_updated) { + return lhs.application_id < rhs.application_id; + } + return lhs.last_updated > rhs.last_updated; + }); - ApplicationRecord record{}; - record.application_id = game.title_id; - record.type = ApplicationRecordType::Installed; - record.unknown = 0; // 2 = needs update - record.unknown2 = ii++; - - out_records[i++] = record; + size_t i = 0; + const size_t start = static_cast(std::max(0, offset)); + for (size_t idx = start; idx < records.size() && i < limit; ++idx) { + out_records[i++] = records[idx]; } *out_count = static_cast(i); diff --git a/src/core/hle/service/ns/ns_types.h b/src/core/hle/service/ns/ns_types.h index 247f7063af..c581e8d6c3 100644 --- a/src/core/hle/service/ns/ns_types.h +++ b/src/core/hle/service/ns/ns_types.h @@ -12,7 +12,7 @@ namespace Service::NS { -enum class ApplicationRecordType : u8 { +enum class ApplicationEvent : u8 { Installing = 2, Installed = 3, GameCardNotInserted = 5, @@ -34,11 +34,10 @@ enum class BackgroundNetworkUpdateState : u8 { struct ApplicationRecord { u64 application_id; - ApplicationRecordType type; - u8 unknown; + ApplicationEvent last_event; + u8 attributes; INSERT_PADDING_BYTES_NOINIT(0x6); - u8 unknown2; - INSERT_PADDING_BYTES_NOINIT(0x7); + s64 last_updated; }; static_assert(sizeof(ApplicationRecord) == 0x18, "ApplicationRecord has incorrect size."); diff --git a/src/core/launch_timestamp_cache.cpp b/src/core/launch_timestamp_cache.cpp new file mode 100644 index 0000000000..5a154a5c2f --- /dev/null +++ b/src/core/launch_timestamp_cache.cpp @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/launch_timestamp_cache.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "common/fs/fs.h" +#include "common/fs/file.h" +#include "common/fs/path_util.h" +#include "common/logging/log.h" + +namespace Core::LaunchTimestampCache { +namespace { + +using CacheMap = std::unordered_map; + +std::mutex mutex; +CacheMap cache; +bool loaded = false; + +std::filesystem::path GetCachePath() { + return Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "launched.json"; +} + +std::optional ReadFileToString(const std::filesystem::path& path) { + const std::ifstream file{path, std::ios::in | std::ios::binary}; + if (!file) { + return std::nullopt; + } + std::ostringstream ss; + ss << file.rdbuf(); + return ss.str(); +} + +bool WriteStringToFile(const std::filesystem::path& path, const std::string& data) { + if (!Common::FS::CreateParentDirs(path)) { + return false; + } + std::ofstream file{path, std::ios::out | std::ios::binary | std::ios::trunc}; + if (!file) { + return false; + } + file.write(data.data(), static_cast(data.size())); + return static_cast(file); +} + +void Load() { + if (loaded) { + return; + } + + loaded = true; + + const auto path = GetCachePath(); + if (!std::filesystem::exists(path)) { + return; + } + + const auto data = ReadFileToString(path); + if (!data) { + LOG_WARNING(Core, "Failed to read launch timestamp cache: {}", + Common::FS::PathToUTF8String(path)); + return; + } + + try { + const auto json = nlohmann::json::parse(data->data(), data->data() + data->size()); + if (!json.is_object()) { + return; + } + for (auto it = json.begin(); it != json.end(); ++it) { + const auto key_str = it.key(); + const auto value = it.value(); + u64 key{}; + try { + key = std::stoull(key_str, nullptr, 16); + } catch (...) { + continue; + } + if (value.is_number_integer()) { + cache[key] = value.get(); + } + } + } catch (const std::exception& e) { + LOG_WARNING(Core, "Failed to parse launch timestamp cache"); + } +} + +void Save() { + nlohmann::json json = nlohmann::json::object(); + for (const auto& [key, value] : cache) { + json[fmt::format("{:016X}", key)] = value; + } + + const auto path = GetCachePath(); + if (!WriteStringToFile(path, json.dump(4))) { + LOG_WARNING(Core, "Failed to write launch timestamp cache: {}", + Common::FS::PathToUTF8String(path)); + } +} + +s64 NowSeconds() { + return std::time(nullptr); +} + +} // namespace + +void SaveLaunchTimestamp(u64 title_id) { + std::scoped_lock lk{mutex}; + Load(); + cache[title_id] = NowSeconds(); + Save(); +} + +s64 GetLaunchTimestamp(u64 title_id) { + std::scoped_lock lk{mutex}; + Load(); + const auto it = cache.find(title_id); + if (it != cache.end()) { + return it->second; + } + // we need a timestamp, i decided on 01/01/2026 00:00 + return 1767225600; +} + +} // namespace Core::LaunchTimestampCache diff --git a/src/core/launch_timestamp_cache.h b/src/core/launch_timestamp_cache.h new file mode 100644 index 0000000000..b3722033cb --- /dev/null +++ b/src/core/launch_timestamp_cache.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/common_types.h" + +namespace Core::LaunchTimestampCache { + +void SaveLaunchTimestamp(u64 title_id); +s64 GetLaunchTimestamp(u64 title_id); + +} // namespace Core::LaunchTimestampCache diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp index 4542b63100..a694da8d84 100644 --- a/src/yuzu/game_list_worker.cpp +++ b/src/yuzu/game_list_worker.cpp @@ -400,6 +400,11 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 && (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) { for (const auto id : program_ids) { + // dravee suggested this, only viable way to + // not show sub-games in qlaunch for now. + if ((id & 0xFFF) != 0) { + continue; + } loader = Loader::GetLoader(system, file, id); if (!loader) { continue;