[core, android] Initial playtime implementation #2535
19 changed files with 178 additions and 47 deletions
|
@ -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()
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ class YuzuApplication : Application() {
|
|||
application = this
|
||||
documentsTree = DocumentsTree()
|
||||
DirectoryInitialization.start()
|
||||
NativeLibrary.playTimeManagerInit()
|
||||
GpuDriverHelper.initializeDriverParameters()
|
||||
NativeInput.reloadInputDevices()
|
||||
NativeLibrary.logDeviceInfo()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<int> g_battery_percentage = {100};
|
|||
std::atomic<bool> g_is_charging = {false};
|
||||
std::atomic<bool> g_has_battery = {true};
|
||||
|
||||
// playtime
|
||||
std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
|
||||
|
||||
EmulationSession::EmulationSession() {
|
||||
m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
|
||||
}
|
||||
|
@ -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<PlayTime::PlayTimeManager>();
|
||||
}
|
||||
|
||||
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 =
|
||||
|
|
|
@ -105,6 +105,16 @@
|
|||
android:textAlignment="center"
|
||||
tools:text="deko_basic" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/playtime"
|
||||
style="?attr/textAppearanceBodyMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="@id/title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/about_game_filename"
|
||||
android:ellipsize="none"
|
||||
tools:text="Game Playtime" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
|
|
|
@ -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" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/playtime"
|
||||
style="?attr/textAppearanceBodyMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="@id/title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/about_game_filename"
|
||||
tools:text="Game Playtime" />
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list_properties"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -1632,4 +1632,9 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
</string>
|
||||
<string name="playtime_format">Playtime: %1$d h, %2$d m</string>
|
||||
<string name="reset_playtime">Clear Playtime</string>
|
||||
<string name="reset_playtime_description">Reset the current game\'s playtime back to 0 seconds</string>
|
||||
<string name="reset_playtime_warning_description">This will clear the current game\'s playtime data. Are you sure?</string>
|
||||
<string name="playtime_reset_successfully">Playtime has been reset</string>
|
||||
</resources>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<std::filesystem::path> GetCurrentUserPlayTimePath(
|
||||
const Service::Account::ProfileManager& manager) {
|
||||
const auto uuid = manager.GetUser(static_cast<s32>(Settings::values.current_user));
|
||||
if (!uuid.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<std::filesystem::path> 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<std::filesystem::path> 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<std::filesystem::path> 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<double>(time_seconds) / 60, 1.0);
|
||||
const auto time_hours = static_cast<double>(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
|
|
@ -6,8 +6,6 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "common/common_funcs.h"
|
||||
|
@ -27,7 +25,7 @@ using PlayTimeDatabase = std::map<ProgramId, PlayTime>;
|
|||
|
||||
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
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<PlayTime::PlayTimeManager>(QtCommon::system->GetProfileManager());
|
||||
play_time_manager = std::make_unique<PlayTime::PlayTimeManager>();
|
||||
|
||||
Network::Init();
|
||||
|
||||
|
|
|
@ -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<double>(time_seconds) / 60, 1.0);
|
||||
const auto time_hours = static_cast<double>(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)
|
||||
|
|
|
@ -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);
|
Loading…
Add table
Add a link
Reference in a new issue