From 386472958ac3b53f667b4d4ecb51791d6b4be825 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Mon, 23 Feb 2026 20:47:11 -0500 Subject: [PATCH 1/5] Parse enablePlaytimeTracking and enablePlaytimeTrackingExtreme prefs. --- lib/include/fp/settings/fp-preferences.h | 2 ++ lib/src/settings/fp-preferences.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/include/fp/settings/fp-preferences.h b/lib/include/fp/settings/fp-preferences.h index 5863369..7c075ff 100644 --- a/lib/include/fp/settings/fp-preferences.h +++ b/lib/include/fp/settings/fp-preferences.h @@ -44,6 +44,8 @@ struct FP_FP_EXPORT Preferences : public Settings std::optional fpfssBaseUrl; std::optional> gameDataSources; std::optional> gameMetadataSources; + bool enablePlaytimeTracking; + bool enablePlaytimeTrackingExtreme; QString imageFolderPath; QString logoFolderPath; QString playlistFolderPath; diff --git a/lib/src/settings/fp-preferences.cpp b/lib/src/settings/fp-preferences.cpp index c1428ba..653014a 100644 --- a/lib/src/settings/fp-preferences.cpp +++ b/lib/src/settings/fp-preferences.cpp @@ -44,6 +44,8 @@ QX_JSON_STRUCT_OUTSIDE(Fp::Preferences, fpfssBaseUrl, gameDataSources, gameMetadataSources, + enablePlaytimeTracking, + enablePlaytimeTrackingExtreme, imageFolderPath, logoFolderPath, playlistFolderPath, From 66a4336c8c87abf5d391b479cce1a53e13e58c38 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Mon, 23 Feb 2026 23:43:34 -0500 Subject: [PATCH 2/5] Parse tagFilters --- lib/include/fp/settings/fp-preferences.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/include/fp/settings/fp-preferences.h b/lib/include/fp/settings/fp-preferences.h index 7c075ff..35bbd95 100644 --- a/lib/include/fp/settings/fp-preferences.h +++ b/lib/include/fp/settings/fp-preferences.h @@ -10,6 +10,18 @@ namespace Fp { +struct FP_FP_EXPORT TagFilter +{ + QString name; + QString description; + bool enabled; + QStringList tags; + QStringList categories; + QStringList childFilters; + bool extreme; + //QByteArray iconBase64; +}; + struct FP_FP_EXPORT AppPathOverride { QString path; @@ -41,6 +53,7 @@ struct FP_FP_EXPORT GameMetadataSource struct FP_FP_EXPORT Preferences : public Settings { + QList tagFilters; std::optional fpfssBaseUrl; std::optional> gameDataSources; std::optional> gameMetadataSources; From eb5ceec5ddfb127b1aff07c17192c9a456fd31a0 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Tue, 24 Feb 2026 11:57:28 -0500 Subject: [PATCH 3/5] Add Toolkit::entryIsExtreme() --- lib/include/fp/fp-toolkit.h | 7 ++- lib/src/fp-db.cpp | 2 +- lib/src/fp-toolkit.cpp | 87 ++++++++++++++++++++++++++++++++++++- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/lib/include/fp/fp-toolkit.h b/lib/include/fp/fp-toolkit.h index 63d9b80..ff7dcff 100644 --- a/lib/include/fp/fp-toolkit.h +++ b/lib/include/fp/fp-toolkit.h @@ -47,7 +47,7 @@ class FP_FP_EXPORT Toolkit //-Instance Variables----------------------------------------------------------------------------------------------- private: - const Install& mInstall; + Install& mInstall; // Implementation Details QString mEntryLocalLogoTemplate; @@ -59,7 +59,7 @@ class FP_FP_EXPORT Toolkit //-Constructor------------------------------------------------------------------------------------------------- public: - Toolkit(const Install& install, const Key&); + Toolkit(Install& install, const Key&); //-Class Functions----------------------------------------------------------------------------------------------- private: @@ -95,6 +95,9 @@ class FP_FP_EXPORT Toolkit QUrl datapackUrl(const Fp::GameData& gameData) const; bool datapackIsPresent(const Fp::GameData& gameData) const; + // Tags + bool entryIsExtreme(const QUuid& id) const; + bool entryIsExtreme(const Entry& entry) const; }; } diff --git a/lib/src/fp-db.cpp b/lib/src/fp-db.cpp index 7d4d5d8..cca634d 100644 --- a/lib/src/fp-db.cpp +++ b/lib/src/fp-db.cpp @@ -415,7 +415,7 @@ Qx::SqlError Db::populateTags() .primaryAlias = tagAliasMap.value(sql.primaryAliasId), .category = {} }; - int catId = sql.categoryId; + int catId = sql.categoryId; // NOTE: Ignore garbage value warning Q_ASSERT(mTagDirectory.contains(catId)); TagCategory& tc = mTagDirectory[catId]; tag.category = tc.name; // CoW reduces overhead diff --git a/lib/src/fp-toolkit.cpp b/lib/src/fp-toolkit.cpp index 6da5e81..57c414a 100644 --- a/lib/src/fp-toolkit.cpp +++ b/lib/src/fp-toolkit.cpp @@ -1,6 +1,12 @@ // Unit Includes #include "fp/fp-toolkit.h" +// Standard Library Includes +#include + +// Qx Includes +#include + // Project Includes #include "fp/fp-install.h" @@ -13,7 +19,7 @@ namespace Fp //-Constructor------------------------------------------------------------------------------------------------ //Public: -Toolkit::Toolkit(const Install& install, const Key&) : +Toolkit::Toolkit(Install& install, const Key&) : mInstall(install) { // Setup (if this class grows expansive enough, these should be shifted towards RAII instead of doing it all at construction @@ -218,4 +224,83 @@ bool Toolkit::datapackIsPresent(const Fp::GameData& gameData) const return true; } +bool Toolkit::entryIsExtreme(const QUuid& id) const +{ + /* Going from an ID here to the Entry version, which just goes back to + * an ID seems wasteful, but we don't know if `id` belongs to a game or + * additional app, and in the case of an additional app we'd need to + * retreive it's data anyway to get `parentGameId`, so this is really only + * slightly wasteful in the case where `id` is a valid game id. + */ + + // Maybe this should return an error, but for now a warning is fine if the. + Entry e; + if(auto err = mInstall.database()->getEntry(e, id); err) + { + qWarning("could not check if %s is extreme, entry not found", qPrintable(id.toString())); + return false; + } + + return entryIsExtreme(e); +} + +bool Toolkit::entryIsExtreme(const Entry& entry) const +{ + QUuid id = entry.visit(qxFuncAggregate{ + [](const Game& g) { return g.id(); }, + [](const AddApp& aa) { return aa.parentGameId(); } + }); + + // Maybe this should return an error, but for now a warning is fine if the. + GameTags gt; + if(auto err = mInstall.database()->getGameTags(gt, id); err) + { + qWarning("could not check if %s is extreme, entry tags not found", qPrintable(id.toString())); + return false; + } + QStringList tags = gt.tags(); + + auto& tagFilters = mInstall.preferences().tagFilters; + +// Clang++15 does not support range adapters (i.e. which make Qt containers work with ranges) +#if defined(__clang__) && __clang_major__ <= 15 + return std::ranges::any_of(tags, [&](const QString& tag) { + const auto ttag = tag.trimmed(); + for(const auto& tf : tagFilters) + { + if(!tf.extreme) + continue; + + for(const QString& extremeTag : tf.tags) + if(extremeTag == ttag) + return true; + } + + return false; + }); + +#else + auto extremeTagsView = + //tagFilters | + std::ranges::subrange(tagFilters.begin(), tagFilters.end()) | + std::views::filter([](const auto& tf) { return tf.extreme; }) | + std::views::transform([](const auto& tf) -> const QStringList& { + return tf.tags; + }) | + std::views::join; + + /* TODO: Perhaps the tags in GameTags should be pre-trimmed during construction, but for now we just + * trim here as the vanilia launcher does in order to prevent any unexpeted edge cases where needing + * to match tags exactly (including errant whitespace) matters. + */ + return std::ranges::any_of(tags, [&](const QString& tag){ + const auto ttag = tag.trimmed(); + // TODO: C++23, replace with std::ranges::contains() + return std::ranges::find(extremeTagsView, ttag) != std::ranges::end(extremeTagsView); + }); +#endif +} + + + } From 200ae32ea95057170ee7ac1e70a5124ed546b69b Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Tue, 24 Feb 2026 15:08:07 -0500 Subject: [PATCH 4/5] Add Db::updateGamePlayRecords() --- CMakeLists.txt | 2 +- lib/include/fp/fp-db.h | 1 + lib/include/fp/fp-items.h | 4 ++++ lib/src/fp-db.cpp | 27 ++++++++++++++++++++++++++- lib/src/fp-items.cpp | 2 ++ 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 53f8940..782e1dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,7 +53,7 @@ set(LIBFP_QX_COMPONENTS include(OB/FetchQx) ob_fetch_qx( - REF "v0.7" + REF "d09196e8198a806731d8e8bae5d4c06e07bece16" COMPONENTS ${LIBFP_QX_COMPONENTS} ) diff --git a/lib/include/fp/fp-db.h b/lib/include/fp/fp-db.h index 05c3698..b4e3048 100644 --- a/lib/include/fp/fp-db.h +++ b/lib/include/fp/fp-db.h @@ -234,6 +234,7 @@ class FP_FP_EXPORT Db // Write DbError updateGameDataOnDiskState(QList packIds, bool onDisk); + DbError updateGamePlayRecords(const QUuid& gameId, qint64 additionalSeconds); // Helper DbError entryUsesDataPack(bool& resultBuffer, const QUuid& gameId); diff --git a/lib/include/fp/fp-items.h b/lib/include/fp/fp-items.h index a59687d..ac9e60c 100644 --- a/lib/include/fp/fp-items.h +++ b/lib/include/fp/fp-items.h @@ -60,6 +60,8 @@ class FP_FP_EXPORT Game QString language; QString orderTitle; QString library; + qint64 playtime; + qint64 playCounter; QString platformName; QString ruffleSupport; // Could be an enum }; @@ -97,6 +99,8 @@ class FP_FP_EXPORT Game QString language() const; QString orderTitle() const; Library library() const; + qint64 playtime() const; + qint64 playCounter() const; QString platformName() const; QString ruffleSupport() const; }; diff --git a/lib/src/fp-db.cpp b/lib/src/fp-db.cpp index cca634d..5c6c989 100644 --- a/lib/src/fp-db.cpp +++ b/lib/src/fp-db.cpp @@ -55,7 +55,7 @@ QX_SQL_STRUCT_OUTSIDE_FULL(Fp::Game::Sql, "game", GameSqlQ, dateModified, broken, playMode, status, notes, source, applicationPath, launchCommand, releaseDate, version, originalDescription, language, orderTitle, library, - platformName, ruffleSupport + playtime, playCounter, platformName, ruffleSupport ); QX_SQL_STRUCT_OUTSIDE_FULL(Fp::GameData::Sql, "game_data", GameDataSqlQ, @@ -877,6 +877,31 @@ DbError Db::updateGameDataOnDiskState(QList packIds, bool onDisk) return DbError(); } +DbError Db::updateGamePlayRecords(const QUuid& gameId, qint64 additionalSeconds) +{ + // Make query + auto updateQuery = mDatabase.UPDATE() + .SET( + GameSqlQ::playtime ^= GameSqlQ::playtime + additionalSeconds, + GameSqlQ::playCounter ^= GameSqlQ::playCounter + 1 + ) + .WHERE(GameSqlQ::id == gameId); + + int affected; + if(auto err = updateQuery.execute(affected); err.isValid()) + return err; + + // Check that expected count was affected + int expected = 1; + if(affected != expected) + { + return DbError(DbError::UpdateRowMismatch, GameSqlQ::_.toString() + u" SET "_s + GameSqlQ::playtime.toString() + u", "_s + GameSqlQ::playCounter.toString(), + u"%1 instead of %2"_s.arg(affected, expected)); + } + + return DbError(); +} + /* TODO: Technically this is a shortcut. The regular launcher will check for Game Redirects in all cases where an ID is searched * for, often using coalesce (see https://github.com/FlashpointProject/FPA-Rust/blob/03a4ddc4af9ae0b2773c5f678268cb9c944d893f/crates/flashpoint-archive/src/game/mod.rs#L323). * This makes sense if anyone is using this lib for any reason (which although that is the intention, currently no one is); but in the case of CLIFp/FIL, where diff --git a/lib/src/fp-items.cpp b/lib/src/fp-items.cpp index 6e56a9d..89bdc0d 100644 --- a/lib/src/fp-items.cpp +++ b/lib/src/fp-items.cpp @@ -46,6 +46,8 @@ QDateTime Game::releaseDate() const { return mData.releaseDate; } QString Game::version() const { return mData.version; } QString Game::originalDescription() const { return mData.originalDescription; } QString Game::language() const { return mData.language; } +qint64 Game::playtime() const { return mData.playtime; } +qint64 Game::playCounter() const { return mData.playCounter; } QString Game::orderTitle() const { return mData.orderTitle; } Library Game::library() const From a833c5eb76d9d6c307b34f227af9b43fe97e27a1 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Tue, 24 Feb 2026 18:15:57 -0500 Subject: [PATCH 5/5] Bump --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 782e1dc..c5476c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.23.0...3.26.0) # Project # NOTE: DON'T USE TRAILING ZEROS IN VERSIONS project(libfp - VERSION 0.6 + VERSION 0.6.1 LANGUAGES CXX DESCRIPTION "C++ support library for Flashpoint Archive" ) @@ -53,7 +53,7 @@ set(LIBFP_QX_COMPONENTS include(OB/FetchQx) ob_fetch_qx( - REF "d09196e8198a806731d8e8bae5d4c06e07bece16" + REF "v0.7.0.3" COMPONENTS ${LIBFP_QX_COMPONENTS} )