Skip to content

Commit

Permalink
Merge pull request #18442 from hrydgard/time-tracking
Browse files Browse the repository at this point in the history
Track time-played per game
  • Loading branch information
hrydgard committed Nov 27, 2023
2 parents 4d94b70 + a9c1bc2 commit 04c6b1a
Show file tree
Hide file tree
Showing 60 changed files with 368 additions and 24 deletions.
16 changes: 10 additions & 6 deletions Common/Data/Format/IniFile.cpp
Expand Up @@ -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))
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions Common/Data/Format/IniFile.h
Expand Up @@ -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_;
}
Expand Down
2 changes: 1 addition & 1 deletion Common/Data/Text/Parsers.cpp
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion Common/GPU/OpenGL/GLQueueRunner.cpp
Expand Up @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion Common/Net/HTTPHeaders.cpp
Expand Up @@ -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;
Expand Down
33 changes: 31 additions & 2 deletions Common/TimeUtil.cpp
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions Common/TimeUtil.h
Expand Up @@ -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);

Expand Down
38 changes: 35 additions & 3 deletions Common/UI/Screen.cpp
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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]);
}
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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()) {
Expand Down
7 changes: 7 additions & 0 deletions Common/UI/Screen.h
Expand Up @@ -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) { }
Expand All @@ -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.
Expand Down
94 changes: 94 additions & 0 deletions Core/Config.cpp
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

0 comments on commit 04c6b1a

Please sign in to comment.