| | |
| | |
| |
|
| | #include "common/alignment.h" |
| | #include "common/fs/file.h" |
| | #include "common/fs/fs.h" |
| | #include "common/fs/path_util.h" |
| | #include "common/logging/log.h" |
| | #include "common/settings.h" |
| | #include "common/thread.h" |
| | #include "core/hle/service/acc/profile_manager.h" |
| | #include "yuzu/play_time_manager.h" |
| |
|
| | namespace PlayTime { |
| |
|
| | namespace { |
| |
|
| | struct PlayTimeElement { |
| | ProgramId program_id; |
| | 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; |
| | } |
| | return Common::FS::GetYuzuPath(Common::FS::YuzuPath::PlayTimeDir) / |
| | uuid->RawString().append(".bin"); |
| | } |
| |
|
| | [[nodiscard]] bool ReadPlayTimeFile(PlayTimeDatabase& out_play_time_db, |
| | const Service::Account::ProfileManager& manager) { |
| | const auto filename = GetCurrentUserPlayTimePath(manager); |
| |
|
| | if (!filename.has_value()) { |
| | LOG_ERROR(Frontend, "Failed to get current user path"); |
| | return false; |
| | } |
| |
|
| | out_play_time_db.clear(); |
| |
|
| | if (Common::FS::Exists(filename.value())) { |
| | Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Read, |
| | Common::FS::FileType::BinaryFile}; |
| | if (!file.IsOpen()) { |
| | LOG_ERROR(Frontend, "Failed to open play time file: {}", |
| | Common::FS::PathToUTF8String(filename.value())); |
| | return false; |
| | } |
| |
|
| | const size_t num_elements = file.GetSize() / sizeof(PlayTimeElement); |
| | std::vector<PlayTimeElement> elements(num_elements); |
| |
|
| | if (file.ReadSpan<PlayTimeElement>(elements) != num_elements) { |
| | return false; |
| | } |
| |
|
| | for (const auto& [program_id, play_time] : elements) { |
| | if (program_id != 0) { |
| | out_play_time_db[program_id] = play_time; |
| | } |
| | } |
| | } |
| |
|
| | return true; |
| | } |
| |
|
| | [[nodiscard]] bool WritePlayTimeFile(const PlayTimeDatabase& play_time_db, |
| | const Service::Account::ProfileManager& manager) { |
| | const auto filename = GetCurrentUserPlayTimePath(manager); |
| |
|
| | if (!filename.has_value()) { |
| | LOG_ERROR(Frontend, "Failed to get current user path"); |
| | return false; |
| | } |
| |
|
| | Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Write, |
| | Common::FS::FileType::BinaryFile}; |
| | if (!file.IsOpen()) { |
| | LOG_ERROR(Frontend, "Failed to open play time file: {}", |
| | Common::FS::PathToUTF8String(filename.value())); |
| | return false; |
| | } |
| |
|
| | std::vector<PlayTimeElement> elements; |
| | elements.reserve(play_time_db.size()); |
| |
|
| | for (auto& [program_id, play_time] : play_time_db) { |
| | if (program_id != 0) { |
| | elements.push_back(PlayTimeElement{program_id, play_time}); |
| | } |
| | } |
| |
|
| | return file.WriteSpan<PlayTimeElement>(elements) == elements.size(); |
| | } |
| |
|
| | } |
| |
|
| | PlayTimeManager::PlayTimeManager(Service::Account::ProfileManager& profile_manager) |
| | : manager{profile_manager} { |
| | if (!ReadPlayTimeFile(database, manager)) { |
| | LOG_ERROR(Frontend, "Failed to read play time database! Resetting to default."); |
| | } |
| | } |
| |
|
| | PlayTimeManager::~PlayTimeManager() { |
| | Save(); |
| | } |
| |
|
| | void PlayTimeManager::SetProgramId(u64 program_id) { |
| | running_program_id = program_id; |
| | } |
| |
|
| | void PlayTimeManager::Start() { |
| | play_time_thread = std::jthread([&](std::stop_token stop_token) { AutoTimestamp(stop_token); }); |
| | } |
| |
|
| | void PlayTimeManager::Stop() { |
| | play_time_thread = {}; |
| | } |
| |
|
| | void PlayTimeManager::AutoTimestamp(std::stop_token stop_token) { |
| | Common::SetCurrentThreadName("PlayTimeReport"); |
| |
|
| | using namespace std::literals::chrono_literals; |
| | using std::chrono::seconds; |
| | using std::chrono::steady_clock; |
| |
|
| | auto timestamp = steady_clock::now(); |
| |
|
| | const auto GetDuration = [&]() -> u64 { |
| | const auto last_timestamp = std::exchange(timestamp, steady_clock::now()); |
| | const auto duration = std::chrono::duration_cast<seconds>(timestamp - last_timestamp); |
| | return static_cast<u64>(duration.count()); |
| | }; |
| |
|
| | while (!stop_token.stop_requested()) { |
| | Common::StoppableTimedWait(stop_token, 30s); |
| |
|
| | database[running_program_id] += GetDuration(); |
| | Save(); |
| | } |
| | } |
| |
|
| | void PlayTimeManager::Save() { |
| | if (!WritePlayTimeFile(database, manager)) { |
| | LOG_ERROR(Frontend, "Failed to update play time database!"); |
| | } |
| | } |
| |
|
| | u64 PlayTimeManager::GetPlayTime(u64 program_id) const { |
| | auto it = database.find(program_id); |
| | if (it != database.end()) { |
| | return it->second; |
| | } else { |
| | return 0; |
| | } |
| | } |
| |
|
| | void PlayTimeManager::ResetProgramPlayTime(u64 program_id) { |
| | database.erase(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)); |
| | } |
| |
|
| | } |
| |
|