From 6bdf479488dd87a35bf5ccf9ec334f49bf43ffc7 Mon Sep 17 00:00:00 2001 From: inix Date: Fri, 17 Oct 2025 22:47:43 +0200 Subject: [PATCH] [core, android] Initial playtime implementation (#2535) So firstly, playtime code is moved to src/common and qt specific code to yuzu/utils.cpp. The dependency on ProfileManager was removed because it was working properly on Android, and I think a shared playtime is better behavior. Now, playtime is stored in a file called "playtime.bin". JNI code is from Azahar although modified by me, as well as that I added code to reset the game's playtime which was missing for some reason on there. Before this gets merged, I plan to add the ability to manually edit the database as well. Note: Code still needs a bit of cleanup. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2535 Reviewed-by: CamilleLaVey Reviewed-by: Lizzie Reviewed-by: crueter Reviewed-by: MaranBr Co-authored-by: inix Co-committed-by: inix --- .../java/org/yuzu/yuzu_emu/NativeLibrary.kt | 11 ++ .../java/org/yuzu/yuzu_emu/YuzuApplication.kt | 1 + .../yuzu_emu/activities/EmulationActivity.kt | 8 ++ .../yuzu_emu/fragments/EmulationFragment.kt | 2 + .../fragments/GamePropertiesFragment.kt | 135 +++++++++++++++++- src/android/app/src/main/jni/native.cpp | 54 +++++++ .../fragment_game_properties.xml | 10 ++ .../main/res/layout/dialog_edit_playtime.xml | 59 ++++++++ .../res/layout/fragment_game_properties.xml | 12 +- .../app/src/main/res/values/strings.xml | 12 ++ src/frontend_common/CMakeLists.txt | 2 + .../play_time_manager.cpp | 73 ++++++---- .../play_time_manager.h | 12 +- src/yuzu/CMakeLists.txt | 4 +- src/yuzu/game_list.cpp | 6 +- src/yuzu/game_list.h | 3 +- src/yuzu/game_list_p.h | 4 +- src/yuzu/game_list_worker.h | 2 +- src/yuzu/main.cpp | 21 ++- src/yuzu/main.h | 1 + src/yuzu/set_play_time_dialog.cpp | 49 +++++++ src/yuzu/set_play_time_dialog.h | 27 ++++ src/yuzu/set_play_time_dialog.ui | 123 ++++++++++++++++ 23 files changed, 586 insertions(+), 45 deletions(-) create mode 100644 src/android/app/src/main/res/layout/dialog_edit_playtime.xml rename src/{yuzu => frontend_common}/play_time_manager.cpp (75%) rename src/{yuzu => frontend_common}/play_time_manager.h (76%) create mode 100644 src/yuzu/set_play_time_dialog.cpp create mode 100644 src/yuzu/set_play_time_dialog.h create mode 100644 src/yuzu/set_play_time_dialog.ui 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..ef8e2bafe9 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,17 @@ 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) + external fun playTimeManagerSetPlayTime(programId: String, playTimeSeconds: Long) + 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 ba60aeace2..cee68689c3 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 @@ -326,6 +327,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) @@ -530,6 +536,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 6979409cad..bc6272b857 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 @@ -1,9 +1,6 @@ // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: 2025 Eden Emulator Project -// SPDX-License-Identifier: GPL-3.0-or-later - package org.yuzu.yuzu_emu.fragments import android.content.Intent @@ -31,6 +28,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 @@ -112,6 +110,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) @@ -136,6 +136,109 @@ 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(getString(R.string.playtime)) + append(readablePlayTime) + } + + binding.playtime.setOnClickListener { + showEditPlaytimeDialog() + } + } + + private fun showEditPlaytimeDialog() { + val dialogView = layoutInflater.inflate(R.layout.dialog_edit_playtime, null) + val hoursLayout = + dialogView.findViewById(R.id.layout_hours) + val minutesLayout = + dialogView.findViewById(R.id.layout_minutes) + val secondsLayout = + dialogView.findViewById(R.id.layout_seconds) + val hoursInput = + dialogView.findViewById(R.id.input_hours) + val minutesInput = + dialogView.findViewById(R.id.input_minutes) + val secondsInput = + dialogView.findViewById(R.id.input_seconds) + + val playTimeSeconds = NativeLibrary.playTimeManagerGetPlayTime(args.game.programId) + val hours = playTimeSeconds / 3600 + val minutes = (playTimeSeconds % 3600) / 60 + val seconds = playTimeSeconds % 60 + + hoursInput.setText(hours.toString()) + minutesInput.setText(minutes.toString()) + secondsInput.setText(seconds.toString()) + + val dialog = com.google.android.material.dialog.MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.edit_playtime) + .setView(dialogView) + .setPositiveButton(android.R.string.ok, null) + .setNegativeButton(android.R.string.cancel, null) + .create() + + dialog.setOnShowListener { + val positiveButton = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE) + positiveButton.setOnClickListener { + hoursLayout.error = null + minutesLayout.error = null + secondsLayout.error = null + + val hoursText = hoursInput.text.toString() + val minutesText = minutesInput.text.toString() + val secondsText = secondsInput.text.toString() + + val hoursValue = hoursText.toLongOrNull() ?: 0 + val minutesValue = minutesText.toLongOrNull() ?: 0 + val secondsValue = secondsText.toLongOrNull() ?: 0 + + var hasError = false + + // normally cant be above 9999 + if (hoursValue < 0 || hoursValue > 9999) { + hoursLayout.error = getString(R.string.hours_must_be_between_0_and_9999) + hasError = true + } + + if (minutesValue < 0 || minutesValue > 59) { + minutesLayout.error = getString(R.string.minutes_must_be_between_0_and_59) + hasError = true + } + + if (secondsValue < 0 || secondsValue > 59) { + secondsLayout.error = getString(R.string.seconds_must_be_between_0_and_59) + hasError = true + } + + if (!hasError) { + val totalSeconds = hoursValue * 3600 + minutesValue * 60 + secondsValue + NativeLibrary.playTimeManagerSetPlayTime(args.game.programId, totalSeconds) + getPlayTime() + Toast.makeText( + requireContext(), + R.string.playtime_updated_successfully, + Toast.LENGTH_SHORT + ).show() + dialog.dismiss() + } + } + } + + dialog.show() + } private fun reloadList() { _binding ?: return @@ -324,6 +427,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 { @@ -336,6 +464,7 @@ class GamePropertiesFragment : Fragment() { override fun onResume() { super.onResume() driverViewModel.updateDriverNameForGame(args.game) + getPlayTime() reloadList() } diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 306b7e2a4c..78f0ec1521 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 "frontend_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,56 @@ 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); + } +} + +void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerSetPlayTime(JNIEnv* env, jobject obj, + jstring jprogramId, jlong playTimeSeconds) { + u64 program_id = EmulationSession::GetProgramId(env, jprogramId); + if (play_time_manager) { + play_time_manager->SetPlayTime(program_id, static_cast(playTimeSeconds)); + } +} + 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..0fa2fa2a29 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" /> + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/layout/fragment_game_properties.xml b/src/android/app/src/main/res/layout/fragment_game_properties.xml index 5e3f3cf280..9a3437404f 100644 --- a/src/android/app/src/main/res/layout/fragment_game_properties.xml +++ b/src/android/app/src/main/res/layout/fragment_game_properties.xml @@ -74,12 +74,22 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:layout_marginBottom="12dp" + android:layout_marginBottom="2dp" android:layout_marginHorizontal="16dp" android:requiresFadingEdge="horizontal" android:textAlignment="center" tools:text="deko_basic" /> + + + Copy details Add-ons Toggle mods, updates and DLC + Playtime: + 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 + Edit Playtime + Hours + Minutes + Hours must be between 0 and 9999 + Minutes must be between 0 and 59 + Seconds must be between 0 and 59 + Playtime updated successfully Clear shader cache Removes all shaders built while playing this game You will experience more stuttering as the shader cache regenerates diff --git a/src/frontend_common/CMakeLists.txt b/src/frontend_common/CMakeLists.txt index aa44740596..c9ce6f7ad4 100644 --- a/src/frontend_common/CMakeLists.txt +++ b/src/frontend_common/CMakeLists.txt @@ -11,6 +11,8 @@ add_library(frontend_common STATIC firmware_manager.h firmware_manager.cpp data_manager.h data_manager.cpp + play_time_manager.cpp + play_time_manager.h ) create_target_directory_groups(frontend_common) diff --git a/src/yuzu/play_time_manager.cpp b/src/frontend_common/play_time_manager.cpp similarity index 75% rename from src/yuzu/play_time_manager.cpp rename to src/frontend_common/play_time_manager.cpp index 6d06bc7614..782433392f 100644 --- a/src/yuzu/play_time_manager.cpp +++ b/src/frontend_common/play_time_manager.cpp @@ -11,7 +11,10 @@ #include "common/settings.h" #include "common/thread.h" #include "core/hle/service/acc/profile_manager.h" -#include "yuzu/play_time_manager.h" +#include "play_time_manager.h" + +#include +#include namespace PlayTime { @@ -22,19 +25,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 +66,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 +96,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 +143,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!"); } } @@ -161,24 +157,47 @@ u64 PlayTimeManager::GetPlayTime(u64 program_id) const { } } +void PlayTimeManager::SetPlayTime(u64 program_id, u64 play_time) { + database[program_id] = play_time; + Save(); +} + void PlayTimeManager::ResetProgramPlayTime(u64 program_id) { database.erase(program_id); Save(); } -QString ReadablePlayTime(qulonglong time_seconds) { +std::string PlayTimeManager::GetReadablePlayTime(u64 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)); + const auto time_minutes = std::max(static_cast(time_seconds) / 60.0, 1.0); + const auto time_hours = static_cast(time_seconds) / 3600.0; + const bool is_minutes = time_minutes < 60.0; + + if (is_minutes) { + return fmt::format("{:.0f} m", time_minutes); + } else { + const bool has_remainder = time_seconds % 60 != 0; + if (has_remainder) { + return fmt::format("{:.1f} h", time_hours); + } else { + return fmt::format("{:.0f} h", time_hours); + } + } +} + +std::string PlayTimeManager::GetPlayTimeHours(u64 time_seconds) { + return fmt::format("{}", time_seconds / 3600); +} + +std::string PlayTimeManager::GetPlayTimeMinutes(u64 time_seconds) { + return fmt::format("{}", (time_seconds % 3600) / 60); +} + +std::string PlayTimeManager::GetPlayTimeSeconds(u64 time_seconds) { + return fmt::format("{}", time_seconds % 60); } } // namespace PlayTime diff --git a/src/yuzu/play_time_manager.h b/src/frontend_common/play_time_manager.h similarity index 76% rename from src/yuzu/play_time_manager.h rename to src/frontend_common/play_time_manager.h index cd81bdb061..a99ccebb1e 100644 --- a/src/yuzu/play_time_manager.h +++ b/src/frontend_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); @@ -36,9 +34,15 @@ public: u64 GetPlayTime(u64 program_id) const; void ResetProgramPlayTime(u64 program_id); void SetProgramId(u64 program_id); + void SetPlayTime(u64 program_id, u64 play_time); void Start(); void Stop(); + static std::string GetReadablePlayTime(u64 time_seconds); + static std::string GetPlayTimeHours(u64 time_seconds); + static std::string GetPlayTimeMinutes(u64 time_seconds); + static std::string GetPlayTimeSeconds(u64 time_seconds); + private: void AutoTimestamp(std::stop_token stop_token); void Save(); @@ -46,9 +50,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 9d584f1ac9..ee45b2a79a 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -198,11 +198,11 @@ 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 + set_play_time_dialog.cpp + set_play_time_dialog.h util/clickable_label.cpp util/clickable_label.h util/controller_navigation.cpp diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 523071eef5..875e794181 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -557,13 +557,15 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri QAction* remove_update = remove_menu->addAction(tr("Remove Installed Update")); QAction* remove_dlc = remove_menu->addAction(tr("Remove All Installed DLC")); QAction* remove_custom_config = remove_menu->addAction(tr("Remove Custom Configuration")); - QAction* remove_play_time_data = remove_menu->addAction(tr("Remove Play Time Data")); QAction* remove_cache_storage = remove_menu->addAction(tr("Remove Cache Storage")); QAction* remove_gl_shader_cache = remove_menu->addAction(tr("Remove OpenGL Pipeline Cache")); QAction* remove_vk_shader_cache = remove_menu->addAction(tr("Remove Vulkan Pipeline Cache")); remove_menu->addSeparator(); QAction* remove_shader_cache = remove_menu->addAction(tr("Remove All Pipeline Caches")); QAction* remove_all_content = remove_menu->addAction(tr("Remove All Installed Contents")); + QMenu* play_time_menu = context_menu.addMenu(tr("Manage Play Time")); + QAction* set_play_time = play_time_menu->addAction(tr("Edit Play Time Data")); + QAction* remove_play_time_data = play_time_menu->addAction(tr("Remove Play Time Data")); QMenu* dump_romfs_menu = context_menu.addMenu(tr("Dump RomFS")); QAction* dump_romfs = dump_romfs_menu->addAction(tr("Dump RomFS")); QAction* dump_romfs_sdmc = dump_romfs_menu->addAction(tr("Dump RomFS to SDMC")); @@ -629,6 +631,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri connect(remove_custom_config, &QAction::triggered, [this, program_id, path]() { emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::CustomConfiguration, path); }); + connect(set_play_time, &QAction::triggered, + [this, program_id]() { emit SetPlayTimeRequested(program_id); }); connect(remove_play_time_data, &QAction::triggered, [this, program_id]() { emit RemovePlayTimeRequested(program_id); }); connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] { diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index 802c26f971..b4cdf61f0d 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -23,7 +23,7 @@ #include "qt_common/config/uisettings.h" #include "qt_common/util/game.h" #include "yuzu/compatibility_list.h" -#include "yuzu/play_time_manager.h" +#include "frontend_common/play_time_manager.h" namespace Core { class System; @@ -104,6 +104,7 @@ signals: void RemoveFileRequested(u64 program_id, QtCommon::Game::GameListRemoveTarget target, const std::string& game_path); void RemovePlayTimeRequested(u64 program_id); + void SetPlayTimeRequested(u64 program_id); void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target); void VerifyIntegrityRequested(const std::string& game_path); void CopyTIDRequested(u64 program_id); diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index 21bb9238a9..ea11d34865 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 "frontend_common/play_time_manager.h" #include "qt_common/config/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(QString::fromStdString(PlayTime::PlayTimeManager::GetReadablePlayTime(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 edcb3e531c..1bbb024df3 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/config/uisettings.h" #include "yuzu/compatibility_list.h" -#include "yuzu/play_time_manager.h" +#include "frontend_common/play_time_manager.h" namespace Core { class System; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 98915c48ef..4f9dd4f9cd 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -15,6 +15,8 @@ #include #include +#include "set_play_time_dialog.h" + #ifdef __APPLE__ #include // for chdir #endif @@ -164,7 +166,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 "frontend_common/play_time_manager.h" #include "yuzu/startup_checks.h" #include "qt_common/config/uisettings.h" #include "yuzu/util/clickable_label.h" @@ -448,7 +450,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(); @@ -1576,6 +1578,8 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile); connect(game_list, &GameList::RemovePlayTimeRequested, this, &GMainWindow::OnGameListRemovePlayTimeData); + connect(game_list, &GameList::SetPlayTimeRequested, this, + &GMainWindow::OnGameListSetPlayTime); connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); connect(game_list, &GameList::VerifyIntegrityRequested, this, &GMainWindow::OnGameListVerifyIntegrity); @@ -2636,6 +2640,19 @@ void GMainWindow::OnGameListRemoveFile(u64 program_id, QtCommon::Game::GameListR } } +void GMainWindow::OnGameListSetPlayTime(u64 program_id) { + const u64 current_play_time = play_time_manager->GetPlayTime(program_id); + + SetPlayTimeDialog dialog(this, current_play_time); + + if (dialog.exec() == QDialog::Accepted) { + const u64 total_seconds = dialog.GetTotalSeconds(); + play_time_manager->SetPlayTime(program_id, total_seconds); + game_list->PopulateAsync(UISettings::values.game_dirs); + } +} + + void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) { if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"), QMessageBox::Yes | QMessageBox::No, diff --git a/src/yuzu/main.h b/src/yuzu/main.h index d1f47f42e2..1c2035fbab 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -345,6 +345,7 @@ private slots: void OnGameListRemoveFile(u64 program_id, QtCommon::Game::GameListRemoveTarget target, const std::string& game_path); void OnGameListRemovePlayTimeData(u64 program_id); + void OnGameListSetPlayTime(u64 program_id); void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target); void OnGameListVerifyIntegrity(const std::string& game_path); void OnGameListCopyTID(u64 program_id); diff --git a/src/yuzu/set_play_time_dialog.cpp b/src/yuzu/set_play_time_dialog.cpp new file mode 100644 index 0000000000..c0f1f0be22 --- /dev/null +++ b/src/yuzu/set_play_time_dialog.cpp @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "yuzu/set_play_time_dialog.h" +#include "frontend_common/play_time_manager.h" +#include "ui_set_play_time_dialog.h" + +SetPlayTimeDialog::SetPlayTimeDialog(QWidget* parent, u64 current_play_time) + : QDialog(parent), ui{std::make_unique()} { + ui->setupUi(this); + + ui->hoursSpinBox->setValue( + QString::fromStdString(PlayTime::PlayTimeManager::GetPlayTimeHours(current_play_time)).toInt()); + ui->minutesSpinBox->setValue( + QString::fromStdString(PlayTime::PlayTimeManager::GetPlayTimeMinutes(current_play_time)).toInt()); + ui->secondsSpinBox->setValue( + QString::fromStdString(PlayTime::PlayTimeManager::GetPlayTimeSeconds(current_play_time)).toInt()); + + connect(ui->hoursSpinBox, QOverload::of(&QSpinBox::valueChanged), this, + &SetPlayTimeDialog::OnValueChanged); + connect(ui->minutesSpinBox, QOverload::of(&QSpinBox::valueChanged), this, + &SetPlayTimeDialog::OnValueChanged); + connect(ui->secondsSpinBox, QOverload::of(&QSpinBox::valueChanged), this, + &SetPlayTimeDialog::OnValueChanged); +} + +SetPlayTimeDialog::~SetPlayTimeDialog() = default; + +u64 SetPlayTimeDialog::GetTotalSeconds() const { + const u64 hours = static_cast(ui->hoursSpinBox->value()); + const u64 minutes = static_cast(ui->minutesSpinBox->value()); + const u64 seconds = static_cast(ui->secondsSpinBox->value()); + + return hours * 3600 + minutes * 60 + seconds; +} + +void SetPlayTimeDialog::OnValueChanged() { + if (ui->errorLabel->isVisible()) { + ui->errorLabel->setVisible(false); + } + + const u64 total_seconds = GetTotalSeconds(); + constexpr u64 max_reasonable_time = 9999ULL * 3600; + + if (total_seconds > max_reasonable_time) { + ui->errorLabel->setText(tr("Total play time reached maximum.")); + ui->errorLabel->setVisible(true); + } +} diff --git a/src/yuzu/set_play_time_dialog.h b/src/yuzu/set_play_time_dialog.h new file mode 100644 index 0000000000..75513539e5 --- /dev/null +++ b/src/yuzu/set_play_time_dialog.h @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include "common/common_types.h" + +namespace Ui { +class SetPlayTimeDialog; +} + +class SetPlayTimeDialog : public QDialog { + Q_OBJECT + +public: + explicit SetPlayTimeDialog(QWidget* parent, u64 current_play_time); + ~SetPlayTimeDialog() override; + + u64 GetTotalSeconds() const; + +private: + void OnValueChanged(); + + std::unique_ptr ui; +}; diff --git a/src/yuzu/set_play_time_dialog.ui b/src/yuzu/set_play_time_dialog.ui new file mode 100644 index 0000000000..dca1c7f1a7 --- /dev/null +++ b/src/yuzu/set_play_time_dialog.ui @@ -0,0 +1,123 @@ + + + SetPlayTimeDialog + + + + 0 + 0 + 400 + 150 + + + + Set Play Time Data + + + true + + + + + + + + Hours: + + + + + + + 9999 + + + + + + + Minutes: + + + + + + + 59 + + + + + + + Seconds: + + + + + + + 59 + + + + + + + + + QLabel { color : red; } + + + + + + false + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + SetPlayTimeDialog + accept() + + + 199 + 129 + + + 199 + 74 + + + + + buttonBox + rejected() + SetPlayTimeDialog + reject() + + + 199 + 129 + + + 199 + 74 + + + + +