From 0d739b44ca67c003a17f899ce2cb67aead9efbfe Mon Sep 17 00:00:00 2001 From: inix Date: Thu, 9 Oct 2025 15:39:20 +0200 Subject: [PATCH] WIP: [core, android] Initial playtime implementation JNI code is from Azahar although modified --- .../java/org/yuzu/yuzu_emu/NativeLibrary.kt | 10 ++++ .../java/org/yuzu/yuzu_emu/YuzuApplication.kt | 1 + .../yuzu_emu/activities/EmulationActivity.kt | 8 ++++ .../yuzu_emu/fragments/EmulationFragment.kt | 2 + .../fragments/GamePropertiesFragment.kt | 47 +++++++++++++++++++ src/android/app/src/main/jni/native.cpp | 46 ++++++++++++++++++ .../fragment_game_properties.xml | 10 ++++ .../res/layout/fragment_game_properties.xml | 12 ++++- .../app/src/main/res/values/strings.xml | 5 ++ src/common/CMakeLists.txt | 2 + src/{yuzu => common}/play_time_manager.cpp | 44 +++++------------ src/{yuzu => common}/play_time_manager.h | 6 +-- src/yuzu/CMakeLists.txt | 2 - src/yuzu/game_list.h | 2 +- src/yuzu/game_list_p.h | 4 +- src/yuzu/game_list_worker.h | 2 +- src/yuzu/main.cpp | 4 +- src/yuzu/util/util.cpp | 15 ++++++ src/yuzu/util/util.h | 3 ++ 19 files changed, 178 insertions(+), 47 deletions(-) rename src/{yuzu => common}/play_time_manager.cpp (75%) rename src/{yuzu => common}/play_time_manager.h (84%) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index ba50bcad34..130e425ce3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -206,6 +206,16 @@ object NativeLibrary { ErrorUnknown } + /** + * playtime tracking + */ + external fun playTimeManagerInit() + external fun playTimeManagerStart() + external fun playTimeManagerStop() + external fun playTimeManagerGetPlayTime(programId: String): Long + external fun playTimeManagerGetCurrentTitleId(): Long + external fun playTimeManagerResetProgramPlayTime(programId: String) + var coreErrorAlertResult = false val coreErrorAlertLock = Object() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt index e9a0d5a75c..daea2b0370 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt @@ -53,6 +53,7 @@ class YuzuApplication : Application() { application = this documentsTree = DocumentsTree() DirectoryInitialization.start() + NativeLibrary.playTimeManagerInit() GpuDriverHelper.initializeDriverParameters() NativeInput.reloadInputDevices() NativeLibrary.logDeviceInfo() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index da40453497..f6451f4d2a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -61,6 +61,7 @@ import org.yuzu.yuzu_emu.utils.ThemeHelper import java.text.NumberFormat import kotlin.math.roundToInt import org.yuzu.yuzu_emu.utils.ForegroundService +import androidx.core.os.BundleCompat class EmulationActivity : AppCompatActivity(), SensorEventListener { private lateinit var binding: ActivityEmulationBinding @@ -322,6 +323,11 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { override fun onAccuracyChanged(sensor: Sensor, i: Int) {} + override fun onDestroy() { + super.onDestroy() + NativeLibrary.playTimeManagerStop() + } + private fun enableFullscreenImmersive() { WindowCompat.setDecorFitsSystemWindows(window, false) @@ -526,6 +532,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { fun onEmulationStarted() { emulationViewModel.setEmulationStarted(true) + NativeLibrary.playTimeManagerStart() + } fun onEmulationStopped(status: Int) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index b2d6135372..5f49399406 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -1635,6 +1635,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { Log.debug("[EmulationFragment] Pausing emulation.") NativeLibrary.pauseEmulation() + NativeLibrary.playTimeManagerStop() state = State.PAUSED } else { @@ -1725,6 +1726,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { State.PAUSED -> { Log.debug("[EmulationFragment] Resuming emulation.") NativeLibrary.unpauseEmulation() + NativeLibrary.playTimeManagerStart() } else -> Log.debug("[EmulationFragment] Bug, run called while already running.") diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt index f55edb418e..eb2d333206 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.yuzu.yuzu_emu.HomeNavigationDirections +import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter @@ -104,6 +105,8 @@ class GamePropertiesFragment : Fragment() { binding.title.text = args.game.title binding.title.marquee() + getPlayTime() + binding.buttonStart.setOnClickListener { LaunchGameDialogFragment.newInstance(args.game) .show(childFragmentManager, LaunchGameDialogFragment.TAG) @@ -128,6 +131,25 @@ class GamePropertiesFragment : Fragment() { gamesViewModel.reloadGames(true) } + private fun getPlayTime() { + binding.playtime.text = buildString { + val playTimeSeconds = NativeLibrary.playTimeManagerGetPlayTime(args.game.programId) + + val hours = playTimeSeconds / 3600 + val minutes = (playTimeSeconds % 3600) / 60 + val seconds = playTimeSeconds % 60 + + val readablePlayTime = when { + hours > 0 -> "${hours}h ${minutes}m ${seconds}s" + minutes > 0 -> "${minutes}m ${seconds}s" + else -> "${seconds}s" + } + + append("Playtime: ") + append(readablePlayTime) + } + } + private fun reloadList() { _binding ?: return @@ -272,6 +294,31 @@ class GamePropertiesFragment : Fragment() { } ) } + if (NativeLibrary.playTimeManagerGetPlayTime(args.game.programId) > 0) { + add( + SubmenuProperty( + R.string.reset_playtime, + R.string.reset_playtime_description, + R.drawable.ic_delete + ) { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.reset_playtime, + descriptionId = R.string.reset_playtime_warning_description, + positiveAction = { + NativeLibrary.playTimeManagerResetProgramPlayTime( args.game.programId) + Toast.makeText( + YuzuApplication.appContext, + R.string.playtime_reset_successfully, + Toast.LENGTH_SHORT + ).show() + getPlayTime() + homeViewModel.reloadPropertiesList(true) + } + ).show(parentFragmentManager, MessageDialogFragment.TAG) + } + ) + } } } binding.listProperties.apply { diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 306b7e2a4c..39bb2999ba 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -36,6 +36,7 @@ #include "common/scope_exit.h" #include "common/settings.h" #include "common/string_util.h" +#include "common/play_time_manager.h" #include "core/core.h" #include "core/cpu_manager.h" #include "core/crypto/key_manager.h" @@ -85,6 +86,9 @@ std::atomic g_battery_percentage = {100}; std::atomic g_is_charging = {false}; std::atomic g_has_battery = {true}; +// playtime +std::unique_ptr play_time_manager; + EmulationSession::EmulationSession() { m_vfs = std::make_shared(); } @@ -733,6 +737,48 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv* } } +void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerInit(JNIEnv* env, jobject obj) { + // for some reason the full user directory isnt initialized in Android, so we need to create it + const auto play_time_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::PlayTimeDir); + if (!Common::FS::IsDir(play_time_dir)) { + if (!Common::FS::CreateDir(play_time_dir)) { + LOG_WARNING(Frontend, "Failed to create play time directory"); + } + } + + play_time_manager = std::make_unique(); +} + +void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerStart(JNIEnv* env, jobject obj) { + if (play_time_manager) { + play_time_manager->SetProgramId(EmulationSession::GetInstance().System().GetApplicationProcessProgramID()); + play_time_manager->Start(); + } +} + +void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerStop(JNIEnv* env, jobject obj) { + play_time_manager->Stop(); +} + +jlong Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerGetPlayTime(JNIEnv* env, jobject obj, + jstring jprogramId) { + u64 program_id = EmulationSession::GetProgramId(env, jprogramId); + return play_time_manager->GetPlayTime(program_id); +} + +jlong Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerGetCurrentTitleId(JNIEnv* env, + jobject obj) { + return EmulationSession::GetInstance().System().GetApplicationProcessProgramID(); +} + +void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerResetProgramPlayTime(JNIEnv* env, jobject obj, + jstring jprogramId) { + u64 program_id = EmulationSession::GetProgramId(env, jprogramId); + if (play_time_manager) { + play_time_manager->ResetProgramPlayTime(program_id); + } +} + jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletLaunchPath(JNIEnv* env, jclass clazz, jlong jid) { auto bis_system = diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml b/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml index 7cdef569f6..5595b096f2 100644 --- a/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml +++ b/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml @@ -105,6 +105,16 @@ android:textAlignment="center" tools:text="deko_basic" /> + + + + + + Playtime: %1$d h, %2$d m + Clear Playtime + Reset the current game\'s playtime back to 0 seconds + This will clear the current game\'s playtime data. Are you sure? + Playtime has been reset diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 96ea429e5a..5f75fa0f53 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -104,6 +104,8 @@ add_library( parent_of_member.h point.h precompiled_headers.h + play_time_manager.cpp + play_time_manager.h quaternion.h range_map.h range_mutex.h diff --git a/src/yuzu/play_time_manager.cpp b/src/common/play_time_manager.cpp similarity index 75% rename from src/yuzu/play_time_manager.cpp rename to src/common/play_time_manager.cpp index 6d06bc7614..6ab08fc077 100644 --- a/src/yuzu/play_time_manager.cpp +++ b/src/common/play_time_manager.cpp @@ -11,7 +11,7 @@ #include "common/settings.h" #include "common/thread.h" #include "core/hle/service/acc/profile_manager.h" -#include "yuzu/play_time_manager.h" +#include "common/play_time_manager.h" namespace PlayTime { @@ -22,19 +22,13 @@ struct PlayTimeElement { PlayTime play_time; }; -std::optional GetCurrentUserPlayTimePath( - const Service::Account::ProfileManager& manager) { - const auto uuid = manager.GetUser(static_cast(Settings::values.current_user)); - if (!uuid.has_value()) { - return std::nullopt; - } +std::optional GetCurrentUserPlayTimePath() { return Common::FS::GetEdenPath(Common::FS::EdenPath::PlayTimeDir) / - uuid->RawString().append(".bin"); + "playtime.bin"; } -[[nodiscard]] bool ReadPlayTimeFile(PlayTimeDatabase& out_play_time_db, - const Service::Account::ProfileManager& manager) { - const auto filename = GetCurrentUserPlayTimePath(manager); +[[nodiscard]] bool ReadPlayTimeFile(PlayTimeDatabase& out_play_time_db) { + const auto filename = GetCurrentUserPlayTimePath(); if (!filename.has_value()) { LOG_ERROR(Frontend, "Failed to get current user path"); @@ -69,9 +63,8 @@ std::optional GetCurrentUserPlayTimePath( return true; } -[[nodiscard]] bool WritePlayTimeFile(const PlayTimeDatabase& play_time_db, - const Service::Account::ProfileManager& manager) { - const auto filename = GetCurrentUserPlayTimePath(manager); +[[nodiscard]] bool WritePlayTimeFile(const PlayTimeDatabase& play_time_db) { + const auto filename = GetCurrentUserPlayTimePath(); if (!filename.has_value()) { LOG_ERROR(Frontend, "Failed to get current user path"); @@ -100,9 +93,9 @@ std::optional GetCurrentUserPlayTimePath( } // namespace -PlayTimeManager::PlayTimeManager(Service::Account::ProfileManager& profile_manager) - : manager{profile_manager} { - if (!ReadPlayTimeFile(database, manager)) { +PlayTimeManager::PlayTimeManager() + : running_program_id() { + if (!ReadPlayTimeFile(database)) { LOG_ERROR(Frontend, "Failed to read play time database! Resetting to default."); } } @@ -147,7 +140,7 @@ void PlayTimeManager::AutoTimestamp(std::stop_token stop_token) { } void PlayTimeManager::Save() { - if (!WritePlayTimeFile(database, manager)) { + if (!WritePlayTimeFile(database)) { LOG_ERROR(Frontend, "Failed to update play time database!"); } } @@ -166,19 +159,4 @@ void PlayTimeManager::ResetProgramPlayTime(u64 program_id) { Save(); } -QString ReadablePlayTime(qulonglong time_seconds) { - if (time_seconds == 0) { - return {}; - } - const auto time_minutes = (std::max)(static_cast(time_seconds) / 60, 1.0); - const auto time_hours = static_cast(time_seconds) / 3600; - const bool is_minutes = time_minutes < 60; - const char* unit = is_minutes ? "m" : "h"; - const auto value = is_minutes ? time_minutes : time_hours; - - return QStringLiteral("%L1 %2") - .arg(value, 0, 'f', !is_minutes && time_seconds % 60 != 0) - .arg(QString::fromUtf8(unit)); -} - } // namespace PlayTime diff --git a/src/yuzu/play_time_manager.h b/src/common/play_time_manager.h similarity index 84% rename from src/yuzu/play_time_manager.h rename to src/common/play_time_manager.h index cd81bdb061..5209004896 100644 --- a/src/yuzu/play_time_manager.h +++ b/src/common/play_time_manager.h @@ -6,8 +6,6 @@ #pragma once -#include - #include #include "common/common_funcs.h" @@ -27,7 +25,7 @@ using PlayTimeDatabase = std::map; class PlayTimeManager { public: - explicit PlayTimeManager(Service::Account::ProfileManager& profile_manager); + explicit PlayTimeManager(); ~PlayTimeManager(); YUZU_NON_COPYABLE(PlayTimeManager); @@ -46,9 +44,7 @@ private: PlayTimeDatabase database; u64 running_program_id; std::jthread play_time_thread; - Service::Account::ProfileManager& manager; }; -QString ReadablePlayTime(qulonglong time_seconds); } // namespace PlayTime diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index c03f7a3abf..1edc01e540 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -198,8 +198,6 @@ add_executable(yuzu multiplayer/state.cpp multiplayer/state.h multiplayer/validation.h - play_time_manager.cpp - play_time_manager.h precompiled_headers.h startup_checks.cpp startup_checks.h diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index cd71fb2139..5f685a5648 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -23,7 +23,7 @@ #include "qt_common/uisettings.h" #include "qt_common/qt_game_util.h" #include "yuzu/compatibility_list.h" -#include "yuzu/play_time_manager.h" +#include "common/play_time_manager.h" namespace Core { class System; diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index 5a3b5829f5..2dfa42f71d 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h @@ -21,7 +21,7 @@ #include "common/common_types.h" #include "common/logging/log.h" #include "common/string_util.h" -#include "yuzu/play_time_manager.h" +#include "common/play_time_manager.h"" #include "qt_common/uisettings.h" #include "yuzu/util/util.h" @@ -241,7 +241,7 @@ public: void setData(const QVariant& value, int role) override { qulonglong time_seconds = value.toULongLong(); - GameListItem::setData(PlayTime::ReadablePlayTime(time_seconds), Qt::DisplayRole); + GameListItem::setData(ReadablePlayTime(time_seconds), Qt::DisplayRole); GameListItem::setData(value, PlayTimeRole); } diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h index f5d5f6341b..022898708b 100644 --- a/src/yuzu/game_list_worker.h +++ b/src/yuzu/game_list_worker.h @@ -20,7 +20,7 @@ #include "core/file_sys/registered_cache.h" #include "qt_common/uisettings.h" #include "yuzu/compatibility_list.h" -#include "yuzu/play_time_manager.h" +#include "common/play_time_manager.h"" namespace Core { class System; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 44ed29f141..3e3fe440dc 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -163,7 +163,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "yuzu/install_dialog.h" #include "yuzu/loading_screen.h" #include "yuzu/main.h" -#include "yuzu/play_time_manager.h" +#include "common/play_time_manager.h" #include "yuzu/startup_checks.h" #include "qt_common/uisettings.h" #include "yuzu/util/clickable_label.h" @@ -447,7 +447,7 @@ GMainWindow::GMainWindow(bool has_broken_vulkan) SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); discord_rpc->Update(); - play_time_manager = std::make_unique(QtCommon::system->GetProfileManager()); + play_time_manager = std::make_unique(); Network::Init(); diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index 844da5c401..f21200e059 100644 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp @@ -48,6 +48,21 @@ QPixmap CreateCirclePixmapFromColor(const QColor& color) { return circle_pixmap; } +QString ReadableDuration(qulonglong time_seconds) { + if (time_seconds == 0) { + return {}; + } + const auto time_minutes = std::max(static_cast(time_seconds) / 60, 1.0); + const auto time_hours = static_cast(time_seconds) / 3600; + const bool is_minutes = time_minutes < 60; + const char* unit = is_minutes ? "m" : "h"; + const auto value = is_minutes ? time_minutes : time_hours; + + return QStringLiteral("%L1 %2") + .arg(value, 0, 'f', !is_minutes && time_seconds % 60 != 0) + .arg(QString::fromUtf8(unit)); +} + bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image) { #if defined(WIN32) #pragma pack(push, 2) diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h index 4094cf6c2b..e845aea4cc 100644 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h @@ -27,3 +27,6 @@ * @return bool If the operation succeeded */ [[nodiscard]] bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image); + +// Converts a length of time in seconds into a readable format +QString ReadableDuration(qulonglong time_seconds); \ No newline at end of file