diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e866e80..b26b7b13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 5.0.0 + +### Added + +- **Remote Configs With JSON**: Remote Configs now support JSON values, allowing for more complex configurations. +- **Playtime Metrics API**: Introduced new API to get total playtime and playtime in the current session. + ## 4.1.1 ### Added diff --git a/README.md b/README.md index 04a2cb97..fddbfb38 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Example: Example: ``` c++ - gameanalytics::GameAnalytics::initialize("", "", ""); ``` ### Send events diff --git a/include/GameAnalytics/GameAnalytics.h b/include/GameAnalytics/GameAnalytics.h index 622d30d0..c1da53c0 100644 --- a/include/GameAnalytics/GameAnalytics.h +++ b/include/GameAnalytics/GameAnalytics.h @@ -103,6 +103,8 @@ namespace gameanalytics static void endSession(); static std::string getRemoteConfigsValueAsString(std::string const& key, std::string const& defaultValue = ""); + static std::string getRemoteConfigsValueAsJson(std::string const& key); + static bool isRemoteConfigsReady(); static void addRemoteConfigsListener(const std::shared_ptr &listener); @@ -116,6 +118,9 @@ namespace gameanalytics static std::string getABTestingId(); static std::string getABTestingVariantId(); + static int64_t getElapsedSessionTime(); + static int64_t getElapsedTimeFromAllSessions(); + // game state changes // will affect how session is started / ended static void onResume(); @@ -125,10 +130,9 @@ namespace gameanalytics static bool isThreadEnding(); private: - static bool _endThread; - static bool isSdkReady(bool needsInitialized); - static bool isSdkReady(bool needsInitialized, bool warn); - static bool isSdkReady(bool needsInitialized, bool warn, std::string const& message); + static bool _endThread; + static bool isSdkReady(bool needsInitialized, bool warn = true, std::string const& message = ""); }; + } // namespace gameanalytics diff --git a/source/dependencies/miniz/miniz.c b/source/dependencies/miniz/GA_Zip.cpp similarity index 99% rename from source/dependencies/miniz/miniz.c rename to source/dependencies/miniz/GA_Zip.cpp index 2b787fbf..31f5841f 100644 --- a/source/dependencies/miniz/miniz.c +++ b/source/dependencies/miniz/GA_Zip.cpp @@ -219,9 +219,9 @@ #define MINIZ_HAS_64BIT_REGISTERS 1 #endif -#ifdef __cplusplus -extern "C" { -#endif +namespace gameanalytics { +namespace utilities { +namespace zip { // ------------------- zlib-style API Definitions. @@ -919,10 +919,6 @@ mz_uint32 tdefl_get_adler32(tdefl_compressor *d); mz_uint tdefl_create_comp_flags_from_zip_params(int level, int window_bits, int strategy); #endif // #ifndef MINIZ_NO_ZLIB_APIS -#ifdef __cplusplus -} -#endif - #endif // MINIZ_HEADER_INCLUDED // ------------------- End of Header: Implementation follows. (If you only want the header, define MINIZ_HEADER_FILE_ONLY.) @@ -968,10 +964,6 @@ typedef unsigned char mz_validate_uint64[sizeof(mz_uint64)==8 ? 1 : -1]; #define MZ_FORCEINLINE inline #endif -#ifdef __cplusplus - extern "C" { -#endif - // ------------------- zlib-style API's mz_ulong mz_adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len) @@ -4882,12 +4874,12 @@ void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char #endif // #ifndef MINIZ_NO_ARCHIVE_APIS -#ifdef __cplusplus -} -#endif - #endif // MINIZ_HEADER_FILE_ONLY +} // namespace zip +} // namespace utilities +} // namespace gameanalytics + /* This is free and unencumbered software released into the public domain. diff --git a/source/gameanalytics/GACommon.h b/source/gameanalytics/GACommon.h index 45467e2b..37f533c6 100644 --- a/source/gameanalytics/GACommon.h +++ b/source/gameanalytics/GACommon.h @@ -85,7 +85,7 @@ namespace gameanalytics class GAState; } - constexpr const char* GA_VERSION_STR = "cpp 4.1.1"; + constexpr const char* GA_VERSION_STR = "cpp 5.0.0"; constexpr int MAX_CUSTOM_FIELDS_COUNT = 50; constexpr int MAX_CUSTOM_FIELDS_KEY_LENGTH = 64; diff --git a/source/gameanalytics/GADevice.h b/source/gameanalytics/GADevice.h index 7ab1e825..eca493db 100644 --- a/source/gameanalytics/GADevice.h +++ b/source/gameanalytics/GADevice.h @@ -78,6 +78,7 @@ namespace gameanalytics std::string _osVersion; std::string _deviceModel; std::string _deviceManufacturer; + std::string _gpu; std::string _writablepath; bool _writablepathStatus{false}; diff --git a/source/gameanalytics/GAEvents.cpp b/source/gameanalytics/GAEvents.cpp index 9cbc710d..64d58ecd 100644 --- a/source/gameanalytics/GAEvents.cpp +++ b/source/gameanalytics/GAEvents.cpp @@ -116,7 +116,8 @@ namespace gameanalytics try { - int64_t sessionLength = state.calculateSessionLength(); + // get session length in seconds + int64_t sessionLength = state.calculateSessionLength(); if(sessionLength < 0ll) { diff --git a/source/gameanalytics/GAHTTPApi.cpp b/source/gameanalytics/GAHTTPApi.cpp index 394a340c..16e5bbe3 100644 --- a/source/gameanalytics/GAHTTPApi.cpp +++ b/source/gameanalytics/GAHTTPApi.cpp @@ -56,7 +56,7 @@ namespace gameanalytics std::string gameKey = state::GAState::getGameKey(); // Generate URL - std::string url = remoteConfigsBaseUrl + "/" + initializeUrlPath + "?game_key=" + gameKey + "&interval_seconds=0&configs_hash=" + configsHash; + std::string url = remoteConfigsBaseUrl + "/" + initializeUrlPath + "?game_key=" + gameKey + "&interval_seconds=0&configs_hash=" + configsHash + "&config_vsn_supported=3"; logging::GALogger::d("Sending 'init' URL: %s", url.c_str()); diff --git a/source/gameanalytics/GAHealth.cpp b/source/gameanalytics/GAHealth.cpp index 6edab39e..1f301d3b 100644 --- a/source/gameanalytics/GAHealth.cpp +++ b/source/gameanalytics/GAHealth.cpp @@ -24,10 +24,9 @@ namespace gameanalytics void GAHealth::doFpsReading(float fps) { int fpsBucket = std::round(fps); - if(fpsBucket >= 0 && fpsBucket < MAX_FPS_COUNT) - { - _fpsReadings[fpsBucket]++; - } + fpsBucket = std::clamp(fpsBucket, 0, MAX_FPS_VALUE); + + _fpsReadings[fpsBucket]++; } int GAHealth::getMemoryPercent(int64_t memory) @@ -65,6 +64,7 @@ namespace gameanalytics { utilities::addIfNotEmpty(out, "cpu_model", _cpuModel); utilities::addIfNotEmpty(out, "hardware", _hardware); + utilities::addIfNotEmpty(out, "gpu_model", _gpuModel); if(_numCores > 0) { @@ -169,7 +169,6 @@ namespace gameanalytics } } ); - } } } diff --git a/source/gameanalytics/GAHealth.h b/source/gameanalytics/GAHealth.h index 94361c19..014c2ed3 100644 --- a/source/gameanalytics/GAHealth.h +++ b/source/gameanalytics/GAHealth.h @@ -29,7 +29,8 @@ namespace gameanalytics protected: - static constexpr size_t MAX_FPS_COUNT = 120 + 1; + static constexpr int MAX_FPS_VALUE = 120; + static constexpr size_t MAX_FPS_COUNT = MAX_FPS_VALUE + 1; static constexpr size_t MAX_MEMORY_COUNT = 100 + 1; static constexpr std::chrono::milliseconds MEMORY_TRACK_FREQ {5000}; diff --git a/source/gameanalytics/GAState.cpp b/source/gameanalytics/GAState.cpp index 13fd7a90..ae3ba089 100644 --- a/source/gameanalytics/GAState.cpp +++ b/source/gameanalytics/GAState.cpp @@ -57,7 +57,7 @@ namespace gameanalytics return; } - getInstance()._userId = id; + getInstance()._customUserId = id; getInstance().cacheIdentifier(); } @@ -383,6 +383,7 @@ namespace gameanalytics logging::GALogger::i("Ending session."); if (GAState::isEnabled() && GAState::sessionIsStarted()) { + getInstance().updateTotalSessionTime(); events::GAEvents::addHealthEvent(); events::GAEvents::addSessionEndEvent(); getInstance()._sessionStart = 0; @@ -410,9 +411,9 @@ namespace gameanalytics out["user_id"] = getUserId(); // remote configs configurations - if(getInstance()._configurations.is_object() && !getInstance()._configurations.empty()) + if(getInstance()._trackingRemoteConfigsJson.is_array() && !getInstance()._trackingRemoteConfigsJson.empty()) { - out["configurations"] = getInstance()._configurations; + out["configurations_v3"] = getInstance().getRemoteConfigAnnotations(); } out["sdk_version"] = device::GADevice::getRelevantSdkVersion(); @@ -505,9 +506,9 @@ namespace gameanalytics void GAState::cacheIdentifier() { - if(!_userId.empty()) + if(!_customUserId.empty()) { - _identifier = _userId; + _identifier = _customUserId; } else { @@ -515,7 +516,6 @@ namespace gameanalytics } logging::GALogger::d("identifier, {clean:%s}", _identifier.c_str()); - } std::string setStateFromCache(json& dict, std::string const& key, std::string const& value) @@ -535,6 +535,11 @@ namespace gameanalytics return ""; } + int64_t GAState::getTotalSessionLength() const + { + return _totalElapsedSessionTime + calculateSessionLength(); + } + void GAState::ensurePersistedStates() { try @@ -555,6 +560,9 @@ namespace gameanalytics } } } + + std::string s = state_dict.dump(); + _gaLogger.d("state_dict: %s", s.c_str()); // insert into GAState instance std::string defaultId = utilities::getOptionalValue(state_dict, "default_user_id"); @@ -575,6 +583,20 @@ namespace gameanalytics _currentCustomDimension01 = setStateFromCache(state_dict, "dimension01", _currentCustomDimension01); _currentCustomDimension02 = setStateFromCache(state_dict, "dimension02", _currentCustomDimension02); _currentCustomDimension03 = setStateFromCache(state_dict, "dimension03", _currentCustomDimension03); + + try + { + std::string cachedSessionTime = utilities::getOptionalValue(state_dict, "total_session_time", "0"); + + _totalElapsedSessionTime = std::stoull(cachedSessionTime); + + } + catch(const std::exception& e) + { + _gaLogger.w("Failed to read total_session_time from cache!"); + _totalElapsedSessionTime = 0; + } + // get cached init call values if (state_dict.contains("sdk_config_cached") && state_dict["sdk_config_cached"].is_string()) @@ -843,24 +865,11 @@ namespace gameanalytics return getInstance()._sessionStart != 0; } - std::string GAState::getRemoteConfigsStringValue(std::string const& key, std::string const& defaultValue) - { - std::lock_guard lg(getInstance()._mtx); - std::string const value = utilities::getOptionalValue(getInstance()._configurations, key, defaultValue); - - return value; - } - bool GAState::isRemoteConfigsReady() { return getInstance()._remoteConfigsIsReady; } - int64_t GAState::calculateSessionLength() const - { - return std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - _startTimepoint).count(); - } - void GAState::addRemoteConfigsListener(const std::shared_ptr& listener) { if(std::find(getInstance()._remoteConfigsListeners.begin(), getInstance()._remoteConfigsListeners.end(), listener) == getInstance()._remoteConfigsListeners.end()) @@ -883,12 +892,52 @@ namespace gameanalytics std::string GAState::getRemoteConfigsContentAsString() { std::lock_guard lg(getInstance()._mtx); - return getInstance()._configurations.dump(JSON_PRINT_INDENT); + + json contents; + + for(auto& obj : getInstance()._gameRemoteConfigsJson) + { + if(obj.contains("key") && obj.contains("value")) + { + std::string key = utilities::getOptionalValue(obj, "key", ""); + if(!key.empty()) + { + contents[key] = obj["value"]; + } + } + } + + return contents.dump(JSON_PRINT_INDENT); + } + + void GAState::buildRemoteConfigsJsons(const json& remoteCfgs) + { + _gameRemoteConfigsJson = json::array(); + _trackingRemoteConfigsJson = json::array(); + + for (const auto& configuration : remoteCfgs) + { + _gameRemoteConfigsJson.push_back({ + {"key", configuration["key"]}, + {"value", configuration["value"]} + }); + + _trackingRemoteConfigsJson.push_back({ + {"key", configuration["key"]}, + {"id", configuration["id"]}, + {"vsn", configuration["vsn"]} + }); + } + + logging::GALogger::d("Remote configs: %s", _gameRemoteConfigsJson.dump(JSON_PRINT_INDENT).c_str()); + logging::GALogger::d("Remote configs for tracking: %s", _trackingRemoteConfigsJson.dump(JSON_PRINT_INDENT).c_str()); + logging::GALogger::i("Remote configs ready with %zu configurations", _gameRemoteConfigsJson.size()); } void GAState::populateConfigurations(json& sdkConfig) { - std::lock_guard guard(_mtx); + + json _tempRemoteConfigsJson = {}; try { @@ -901,29 +950,25 @@ namespace gameanalytics if (!configuration.empty()) { std::string key = utilities::getOptionalValue(configuration, "key", ""); - int64_t start_ts = utilities::getOptionalValue(configuration, "start_ts", std::numeric_limits::min()); - int64_t end_ts = utilities::getOptionalValue(configuration, "end_ts", std::numeric_limits::max()); int64_t client_ts_adjusted = getClientTsAdjusted(); if (!key.empty() && configuration.contains("value") && client_ts_adjusted > start_ts && client_ts_adjusted < end_ts) { - json& value = configuration["value"]; - if (value.is_string() || value.is_number()) - { - _configurations[key] = value; - logging::GALogger::d("configuration added: %s", configuration.dump(JSON_PRINT_INDENT).c_str()); - } + _tempRemoteConfigsJson[key] = configuration; + logging::GALogger::d("configuration added: %s", configuration.dump(JSON_PRINT_INDENT).c_str()); } } } } + buildRemoteConfigsJsons(_tempRemoteConfigsJson); + _remoteConfigsIsReady = true; - std::string const configStr = _configurations.dump(); + std::string const configStr = _gameRemoteConfigsJson.dump(); for (auto& listener : _remoteConfigsListeners) { listener->onRemoteConfigsUpdated(configStr); @@ -1043,6 +1088,12 @@ namespace gameanalytics return utilities::GAUtilities::timeIntervalSince1970(); } + void GAState::updateTotalSessionTime() + { + int64_t totalSessionTime = getTotalSessionLength(); + _gaStore.setState("total_session_time", std::to_string(totalSessionTime)); + } + std::string GAState::getBuild() { return getInstance()._build; @@ -1132,5 +1183,10 @@ namespace gameanalytics return cleanedFields; } + + json GAState::getRemoteConfigAnnotations() + { + return _trackingRemoteConfigsJson; + } } } diff --git a/source/gameanalytics/GAState.h b/source/gameanalytics/GAState.h index 020438ea..346a0b86 100644 --- a/source/gameanalytics/GAState.h +++ b/source/gameanalytics/GAState.h @@ -134,7 +134,6 @@ namespace gameanalytics static void setEnabledEventSubmission(bool flag); static bool isEventSubmissionEnabled(); static bool sessionIsStarted(); - static std::string getRemoteConfigsStringValue(std::string const& key, std::string const& defaultValue); static bool isRemoteConfigsReady(); static void addRemoteConfigsListener(const std::shared_ptr& listener); static void removeRemoteConfigsListener(const std::shared_ptr& listener); @@ -147,7 +146,32 @@ namespace gameanalytics static json getValidatedCustomFields(); static json getValidatedCustomFields(const json& withEventFields); - int64_t calculateSessionLength() const; + template + inline static T getRemoteConfigsValue(std::string const& key, T const& defaultValue) + { + std::lock_guard lg(getInstance()._mtx); + if(getInstance()._gameRemoteConfigsJson.contains(key)) + { + json& config = getInstance()._gameRemoteConfigsJson[key]; + T value = utilities::getOptionalValue(config, "value", defaultValue); + return value; + } + + return defaultValue; + } + + template + inline int64_t calculateSessionLength() const + { + auto len = std::chrono::high_resolution_clock::now() - _startTimepoint; + return std::chrono::duration_cast(len).count(); + } + + int64_t getTotalSessionLength() const; + + void populateConfigurations(json& sdkConfig); + + json getRemoteConfigAnnotations(); private: @@ -174,8 +198,9 @@ namespace gameanalytics void validateAndFixCurrentDimensions(); std::string getBuild(); - int64_t calculateServerTimeOffset(int64_t serverTs); - void populateConfigurations(json& sdkConfig); + void updateTotalSessionTime(); + + int64_t calculateServerTimeOffset(int64_t serverTs); void validateAndCleanCustomFields(const json& fields, json& out); @@ -185,6 +210,8 @@ namespace gameanalytics void addErrorEvent(EGAErrorSeverity severity, std::string const& message); + void buildRemoteConfigsJsons(const json& remoteCfgs); + threading::GAThreading _gaThread; events::GAEvents _gaEvents; device::GADevice _gaDevice; @@ -192,7 +219,7 @@ namespace gameanalytics store::GAStore _gaStore; http::GAHTTPApi _gaHttp; - std::string _userId; + std::string _customUserId; std::string _identifier; bool _initialized = false; @@ -202,6 +229,7 @@ namespace gameanalytics int64_t _sessionNum = 0; int64_t _transactionNum = 0; + int64_t _totalElapsedSessionTime = 0; std::chrono::high_resolution_clock::time_point _startTimepoint; std::string _sessionId; @@ -245,7 +273,9 @@ namespace gameanalytics bool _enableIdTracking = true; - json _configurations; + json _gameRemoteConfigsJson; + json _trackingRemoteConfigsJson; + bool _remoteConfigsIsReady; std::vector> _remoteConfigsListeners; std::recursive_mutex _mtx; diff --git a/source/gameanalytics/GAUtilities.cpp b/source/gameanalytics/GAUtilities.cpp index bbb1807c..2f98626e 100644 --- a/source/gameanalytics/GAUtilities.cpp +++ b/source/gameanalytics/GAUtilities.cpp @@ -20,12 +20,14 @@ // From crypto #define MINIZ_HEADER_FILE_ONLY -#include "miniz.c" +#include "GA_Zip.cpp" namespace gameanalytics { namespace utilities { + using namespace zip; + std::string printArray(StringVector const& v, std::string const& delim) { if(v.empty()) diff --git a/source/gameanalytics/GameAnalytics.cpp b/source/gameanalytics/GameAnalytics.cpp index 60ec105b..76a8604f 100644 --- a/source/gameanalytics/GameAnalytics.cpp +++ b/source/gameanalytics/GameAnalytics.cpp @@ -740,7 +740,18 @@ namespace gameanalytics std::string GameAnalytics::getRemoteConfigsValueAsString(std::string const& key, std::string const& defaultValue) { - return state::GAState::getRemoteConfigsStringValue(key, defaultValue); + return state::GAState::getRemoteConfigsValue(key, defaultValue); + } + + std::string GameAnalytics::getRemoteConfigsValueAsJson(std::string const& key) + { + std::string jsonString = getRemoteConfigsValueAsString(key); + if(!json::accept(jsonString)) + { + return ""; + } + + return jsonString; } bool GameAnalytics::isRemoteConfigsReady() @@ -874,8 +885,9 @@ namespace gameanalytics std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } - catch (const std::exception&) + catch (const std::exception& e) { + logging::GALogger::e(e.what()); } } @@ -884,18 +896,6 @@ namespace gameanalytics return _endThread || threading::GAThreading::isThreadFinished(); } - // --------------PRIVATE HELPERS -------------- // - - bool GameAnalytics::isSdkReady(bool needsInitialized) - { - return isSdkReady(needsInitialized, true); - } - - bool GameAnalytics::isSdkReady(bool needsInitialized, bool warn) - { - return isSdkReady(needsInitialized, warn, ""); - } - bool GameAnalytics::isSdkReady(bool needsInitialized, bool warn, std::string const& message) { constexpr std::size_t maxMsgLen = 64u; @@ -906,7 +906,7 @@ namespace gameanalytics { if (warn) { - logging::GALogger::w("%sDatastore not initialized", m.c_str()); + logging::GALogger::w("%s; Datastore not initialized", m.c_str()); } return false; } @@ -915,7 +915,7 @@ namespace gameanalytics { if (warn) { - logging::GALogger::w("%sSDK is not initialized", m.c_str()); + logging::GALogger::w("%s; SDK is not initialized", m.c_str()); } return false; } @@ -924,7 +924,7 @@ namespace gameanalytics { if (warn) { - logging::GALogger::w("%s;SDK is disabled", m.c_str()); + logging::GALogger::w("%s; SDK is disabled", m.c_str()); } return false; } @@ -934,7 +934,7 @@ namespace gameanalytics { if (warn) { - logging::GALogger::w("%s;Session has not started yet", m.c_str()); + logging::GALogger::w("%s; Session has not started yet", m.c_str()); } return false; } @@ -988,4 +988,14 @@ namespace gameanalytics } } + int64_t GameAnalytics::getElapsedTimeFromAllSessions() + { + return state::GAState::getInstance().getTotalSessionLength(); + } + + int64_t GameAnalytics::getElapsedSessionTime() + { + return state::GAState::getInstance().calculateSessionLength(); + } + } // namespace gameanalytics diff --git a/source/gameanalytics/Platform/GAWin32.cpp b/source/gameanalytics/Platform/GAWin32.cpp index f63d8320..aaa8e2e0 100644 --- a/source/gameanalytics/Platform/GAWin32.cpp +++ b/source/gameanalytics/Platform/GAWin32.cpp @@ -233,16 +233,18 @@ std::string GAPlatformWin32::getGpuModel() const DISPLAY_DEVICE device; ZeroMemory(&device, sizeof(DISPLAY_DEVICE)); - if(EnumDisplayDevices(NULL, 0, &device, 0)) + device.cb = sizeof(DISPLAY_DEVICE); + + if(EnumDisplayDevices(NULL, 0, &device, EDD_GET_DEVICE_INTERFACE_NAME)) { #ifdef UNICODE - return utilities::GAUtilities::ws2s(device.DeviceName); + return utilities::GAUtilities::ws2s(device.DeviceString); #else - return device.DeviceName; + return device.DeviceString; #endif } - return UNKNOWN_VALUE; + return ""; } int GAPlatformWin32::getNumCpuCores() const