Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track time-played per game #18442

Merged
merged 5 commits into from
Nov 27, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions Common/Data/Format/IniFile.cpp
Original file line number Diff line number Diff line change
@@ -189,6 +189,15 @@ void Section::Clear() {
lines_.clear();
}

bool Section::GetKeys(std::vector<std::string> &keys) const {
keys.clear();
for (auto liter = lines_.begin(); liter != lines_.end(); ++liter) {
if (!liter->Key().empty())
keys.push_back(std::string(liter->Key()));
}
return true;
}

ParsedIniLine *Section::GetLine(const char *key) {
for (auto &line : lines_) {
if (equalsNoCase(line.Key(), key))
@@ -482,12 +491,7 @@ bool IniFile::GetKeys(const char* sectionName, std::vector<std::string>& keys) c
const Section *section = GetSection(sectionName);
if (!section)
return false;
keys.clear();
for (auto liter = section->lines_.begin(); liter != section->lines_.end(); ++liter) {
if (!liter->Key().empty())
keys.push_back(std::string(liter->Key()));
}
return true;
return section->GetKeys(keys);
}

void IniFile::SortSections()
3 changes: 3 additions & 0 deletions Common/Data/Format/IniFile.h
Original file line number Diff line number Diff line change
@@ -105,6 +105,9 @@ class Section {
bool Get(const char* key, double* value, double defaultValue = false) const;
bool Get(const char* key, std::vector<std::string>& values) const;

// Return a list of all keys in this section
bool GetKeys(std::vector<std::string> &keys) const;

bool operator < (const Section& other) const {
return name_ < other.name_;
}
2 changes: 1 addition & 1 deletion Common/Data/Text/Parsers.cpp
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ void NiceSizeFormat(uint64_t size, char *out, size_t bufSize) {
if (s == 0)
snprintf(out, bufSize, "%d B", (int)size);
else
snprintf(out, bufSize, "%3.1f %s", f, sizes[s]);
snprintf(out, bufSize, "%3.2f %s", f, sizes[s]);
}

std::string NiceSizeFormat(uint64_t size) {
2 changes: 1 addition & 1 deletion Common/GPU/OpenGL/GLQueueRunner.cpp
Original file line number Diff line number Diff line change
@@ -337,7 +337,7 @@ void GLQueueRunner::RunInitSteps(const FastVec<GLRInitStep> &steps, bool skipGLC
std::vector<std::string_view> lines;
SplitString(errorString, '\n', lines);
for (auto line : lines) {
ERROR_LOG(G3D, "%.*s", line.size(), line.data());
ERROR_LOG(G3D, "%.*s", (int)line.size(), line.data());
}
if (errorCallback_) {
std::string desc = StringFromFormat("Shader compilation failed: %s", step.create_shader.stage == GL_VERTEX_SHADER ? "vertex" : "fragment");
2 changes: 1 addition & 1 deletion Common/Net/HTTPHeaders.cpp
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ bool RequestHeader::GetParamValue(const char *param_name, std::string *value) co
for (size_t i = 0; i < v.size(); i++) {
std::vector<std::string_view> parts;
SplitString(v[i], '=', parts);
DEBUG_LOG(IO, "Param: %.*s Value: %.*s", parts[0].size(), parts[0].data(), parts[1].size(), parts[1].data());
DEBUG_LOG(IO, "Param: %.*s Value: %.*s", (int)parts[0].size(), parts[0].data(), (int)parts[1].size(), parts[1].data());
if (parts[0] == param_name) {
*value = parts[1];
return true;
33 changes: 31 additions & 2 deletions Common/TimeUtil.cpp
Original file line number Diff line number Diff line change
@@ -68,6 +68,20 @@ double from_time_raw_relative(uint64_t raw_time) {
return from_time_raw(raw_time);
}

double time_now_unix_utc() {
const int64_t UNIX_TIME_START = 0x019DB1DED53E8000; //January 1, 1970 (start of Unix epoch) in "ticks"
const double TICKS_PER_SECOND = 10000000; //a tick is 100ns

FILETIME ft;
GetSystemTimeAsFileTime(&ft); //returns ticks in UTC
// Copy the low and high parts of FILETIME into a LARGE_INTEGER
LARGE_INTEGER li;
li.LowPart = ft.dwLowDateTime;
li.HighPart = ft.dwHighDateTime;
//Convert ticks since 1/1/1970 into seconds
return (double)(li.QuadPart - UNIX_TIME_START) / TICKS_PER_SECOND;
}

void yield() {
YieldProcessor();
}
@@ -99,6 +113,12 @@ double from_time_raw_relative(uint64_t raw_time) {
return (double)raw_time * (1.0 / nanos);
}

double time_now_unix_utc() {
struct timespec tp;
clock_gettime(CLOCK_REALTIME, &tp);
return tp.tv_sec * 1000000000ULL + tp.tv_nsec;
}

void yield() {
#if PPSSPP_ARCH(X86) || PPSSPP_ARCH(AMD64)
_mm_pause();
@@ -120,9 +140,14 @@ double time_now_d() {
return (double)(tv.tv_sec - start) + (double)tv.tv_usec * (1.0 / micros);
}

// Fake, but usable in a pinch. Don't, though.
uint64_t time_now_raw() {
return (uint64_t)(time_now_d() * nanos);
static time_t start;
struct timeval tv;
gettimeofday(&tv, nullptr);
if (start == 0) {
start = tv.tv_sec;
}
return (double)tv.tv_sec + (double)tv.tv_usec * (1.0 / micros);
}

double from_time_raw(uint64_t raw_time) {
@@ -135,6 +160,10 @@ double from_time_raw_relative(uint64_t raw_time) {

void yield() {}

double time_now_unix_utc() {
return time_now_raw();
}

#endif

void sleep_ms(int ms) {
3 changes: 3 additions & 0 deletions Common/TimeUtil.h
Original file line number Diff line number Diff line change
@@ -13,6 +13,9 @@ uint64_t time_now_raw();
double from_time_raw(uint64_t raw_time);
double from_time_raw_relative(uint64_t raw_time);

// Seconds, Unix UTC time
double time_now_unix_utc();

// Sleep. Does not necessarily have millisecond granularity, especially on Windows.
void sleep_ms(int ms);

38 changes: 35 additions & 3 deletions Common/UI/Screen.cpp
Original file line number Diff line number Diff line change
@@ -13,6 +13,15 @@

#include "Core/KeyMap.h"

void Screen::focusChanged(ScreenFocusChange focusChange) {
char *eventName = "";
switch (focusChange) {
case ScreenFocusChange::FOCUS_LOST_TOP: eventName = "FOCUS_LOST_TOP"; break;
case ScreenFocusChange::FOCUS_BECAME_TOP: eventName = "FOCUS_BECAME_TOP"; break;
}
DEBUG_LOG(SYSTEM, "Screen %s got %s", this->tag(), eventName);
}

ScreenManager::~ScreenManager() {
shutdown();
}
@@ -68,14 +77,17 @@ void ScreenManager::switchToNext() {
Layer temp = {nullptr, 0};
if (!stack_.empty()) {
temp = stack_.back();
temp.screen->focusChanged(ScreenFocusChange::FOCUS_LOST_TOP);
stack_.pop_back();
}
stack_.push_back(nextStack_.front());
nextStack_.front().screen->focusChanged(ScreenFocusChange::FOCUS_BECAME_TOP);
if (temp.screen) {
delete temp.screen;
}
UI::SetFocusedView(nullptr);

// When will this ever happen? Should handle focus here too?
for (size_t i = 1; i < nextStack_.size(); ++i) {
stack_.push_back(nextStack_[i]);
}
@@ -264,17 +276,30 @@ void ScreenManager::push(Screen *screen, int layerFlags) {
touch(input);

Layer layer = {screen, layerFlags};
if (nextStack_.empty())

if (!stack_.empty()) {
stack_.back().screen->focusChanged(ScreenFocusChange::FOCUS_LOST_TOP);
}

if (nextStack_.empty()) {
layer.screen->focusChanged(ScreenFocusChange::FOCUS_BECAME_TOP);
stack_.push_back(layer);
else
} else {
nextStack_.push_back(layer);
}
}

void ScreenManager::pop() {
std::lock_guard<std::recursive_mutex> guard(inputLock_);
if (stack_.size()) {
if (!stack_.empty()) {
stack_.back().screen->focusChanged(ScreenFocusChange::FOCUS_LOST_TOP);

delete stack_.back().screen;
stack_.pop_back();

if (!stack_.empty()) {
stack_.back().screen->focusChanged(ScreenFocusChange::FOCUS_LOST_TOP);
}
} else {
ERROR_LOG(SYSTEM, "Can't pop when stack empty");
}
@@ -318,12 +343,19 @@ void ScreenManager::processFinishDialog() {
std::lock_guard<std::recursive_mutex> guard(inputLock_);
// Another dialog may have been pushed before the render, so search for it.
Screen *caller = dialogParent(dialogFinished_);
bool erased = false;
for (size_t i = 0; i < stack_.size(); ++i) {
if (stack_[i].screen == dialogFinished_) {
stack_[i].screen->focusChanged(ScreenFocusChange::FOCUS_LOST_TOP);
stack_.erase(stack_.begin() + i);
erased = true;
}
}

if (erased && !stack_.empty()) {
stack_.back().screen->focusChanged(ScreenFocusChange::FOCUS_BECAME_TOP);
}

if (!caller) {
ERROR_LOG(SYSTEM, "ERROR: no top screen when finishing dialog");
} else if (caller != topScreen()) {
7 changes: 7 additions & 0 deletions Common/UI/Screen.h
Original file line number Diff line number Diff line change
@@ -42,6 +42,11 @@ namespace Draw {
class DrawContext;
}

enum class ScreenFocusChange {
FOCUS_LOST_TOP, // Another screen was pushed on top
FOCUS_BECAME_TOP, // Became the top screen again
};

class Screen {
public:
Screen() : screenManager_(nullptr) { }
@@ -60,6 +65,8 @@ class Screen {
virtual void deviceLost() {}
virtual void deviceRestored() {}

virtual void focusChanged(ScreenFocusChange focusChange);

// Return value of UnsyncTouch is only used to let the overlay screen block touches.
virtual bool UnsyncTouch(const TouchInput &touch) = 0;
// Return value of UnsyncKey is used to not block certain system keys like volume when unhandled, on Android.
94 changes: 94 additions & 0 deletions Core/Config.cpp
Original file line number Diff line number Diff line change
@@ -1157,6 +1157,10 @@ void Config::Load(const char *iniFileName, const char *controllerIniFilename) {
}
}

// Time tracking
Section *playTime = iniFile.GetOrCreateSection("PlayTime");
playTimeTracker_.Load(playTime);

auto pinnedPaths = iniFile.GetOrCreateSection("PinnedPaths")->ToMap();
vPinnedPaths.clear();
for (auto it = pinnedPaths.begin(), end = pinnedPaths.end(); it != end; ++it) {
@@ -1317,6 +1321,10 @@ bool Config::Save(const char *saveReason) {
if (LogManager::GetInstance())
LogManager::GetInstance()->SaveConfig(log);

// Time tracking
Section *playTime = iniFile.GetOrCreateSection("PlayTime");
playTimeTracker_.Save(playTime);

if (!iniFile.Save(iniFilename_)) {
ERROR_LOG(LOADER, "Error saving config (%s)- can't write ini '%s'", saveReason, iniFilename_.c_str());
return false;
@@ -1827,3 +1835,89 @@ int Config::GetPSPLanguage() {
return g_Config.iLanguage;
}
}

void PlayTimeTracker::Start(std::string gameId) {
INFO_LOG(SYSTEM, "GameTimeTracker::Start(%s)", gameId.c_str());
if (gameId.empty()) {
return;
}

auto iter = tracker_.find(std::string(gameId));
if (iter != tracker_.end()) {
if (iter->second.startTime == 0.0) {
iter->second.lastTimePlayed = time_now_unix_utc();
iter->second.startTime = time_now_d();
}
return;
}

PlayTime playTime;
playTime.lastTimePlayed = time_now_unix_utc();
playTime.totalTimePlayed = 0.0;
playTime.startTime = time_now_d();
tracker_[gameId] = playTime;
}

void PlayTimeTracker::Stop(std::string gameId) {
INFO_LOG(SYSTEM, "GameTimeTracker::Stop(%s)", gameId.c_str());
_dbg_assert_(!gameId.empty());

auto iter = tracker_.find(std::string(gameId));
if (iter != tracker_.end()) {
if (iter->second.startTime != 0.0) {
iter->second.totalTimePlayed += time_now_d() - iter->second.startTime;
iter->second.startTime = 0.0;
}
iter->second.lastTimePlayed = time_now_unix_utc();
return;
}

// Shouldn't happen, ignore this case.
WARN_LOG(SYSTEM, "GameTimeTracker::Stop called without corresponding GameTimeTracker::Start");
}

void PlayTimeTracker::Load(const Section *section) {
tracker_.clear();

std::vector<std::string> keys;
section->GetKeys(keys);

for (auto key : keys) {
std::string value;
if (!section->Get(key.c_str(), &value, nullptr)) {
continue;
}

// Parse the string.
PlayTime gameTime{};
if (2 == sscanf(value.c_str(), "%d,%llu", &gameTime.totalTimePlayed, &gameTime.lastTimePlayed)) {
tracker_[key] = gameTime;
}
}
}

void PlayTimeTracker::Save(Section *section) {
for (auto iter : tracker_) {
std::string formatted = StringFromFormat("%d,%llu", iter.second.totalTimePlayed, iter.second.lastTimePlayed);
section->Set(iter.first.c_str(), formatted);
}
}

bool PlayTimeTracker::GetPlayedTimeString(const std::string &gameId, std::string *str) const {
auto ga = GetI18NCategory(I18NCat::GAME);

auto iter = tracker_.find(gameId);
if (iter == tracker_.end()) {
return false;
}

int totalSeconds = iter->second.totalTimePlayed;
int seconds = totalSeconds % 60;
totalSeconds /= 60;
int minutes = totalSeconds % 60;
totalSeconds /= 60;
int hours = totalSeconds;

*str = ApplySafeSubstitutions(ga->T("Time Played: %1h %2m %3s"), hours, minutes, seconds);
return true;
}
Loading