Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/game/StarStatistics.cpp
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarStatistics.cpp
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/game/StarStatistics.cpp')
-rw-r--r--source/game/StarStatistics.cpp235
1 files changed, 235 insertions, 0 deletions
diff --git a/source/game/StarStatistics.cpp b/source/game/StarStatistics.cpp
new file mode 100644
index 0000000..f4a1595
--- /dev/null
+++ b/source/game/StarStatistics.cpp
@@ -0,0 +1,235 @@
+#include "StarStatistics.hpp"
+#include "StarStatisticsDatabase.hpp"
+#include "StarStatisticsService.hpp"
+#include "StarConfigLuaBindings.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarLogging.hpp"
+
+namespace Star {
+
+Statistics::Statistics(String const& storageDirectory, StatisticsServicePtr service) {
+ m_service = move(service);
+ m_initialized = !m_service;
+ m_storageDirectory = storageDirectory;
+ readStatistics();
+
+ auto assets = Root::singleton().assets();
+
+ m_luaRoot = make_shared<LuaRoot>();
+}
+
+void Statistics::writeStatistics() {
+ auto versioningDatabase = Root::singleton().versioningDatabase();
+ String filename = File::relativeTo(m_storageDirectory, "statistics");
+
+ Json stats = JsonObject::from(m_stats.pairs().transformed([](auto const& entry) {
+ return make_pair(entry.first, entry.second.toJson());
+ }));
+ JsonObject storage = {
+ { "stats", stats },
+ { "achievements", jsonFromStringSet(m_achievements) }
+ };
+
+ auto versionedStorage = versioningDatabase->makeCurrentVersionedJson("Statistics", storage);
+ VersionedJson::writeFile(versionedStorage, filename);
+}
+
+Json Statistics::stat(String const& name, Json def) const {
+ if (m_stats.contains(name))
+ return m_stats.get(name).value;
+ return def;
+}
+
+Maybe<String> Statistics::statType(String const& name) const {
+ if (m_stats.contains(name))
+ return m_stats.get(name).type;
+ return {};
+}
+
+bool Statistics::achievementUnlocked(String const& name) const {
+ return m_achievements.contains(name);
+}
+
+void Statistics::recordEvent(String const& name, Json const& fields) {
+ m_pendingEvents.append(make_pair(name, fields));
+}
+
+bool Statistics::reset() {
+ if (m_initialized) {
+ if (!m_service || m_service->reset()) {
+ m_stats = {};
+ m_achievements = {};
+ return true;
+ }
+ }
+ return false;
+}
+
+void Statistics::update() {
+ if (m_service) {
+ if (auto error = m_service->error()) {
+ Logger::error("Statistics platform service error: %s", *error);
+ // Service failed. Continue with local stats and achievements only.
+ m_service = {};
+ m_initialized = true;
+ return;
+ }
+ }
+
+ if (!m_initialized) {
+ if (m_service->initialized()) {
+ mergeServiceStatistics();
+ m_initialized = true;
+ }
+ }
+
+ for (auto event : m_pendingEvents) {
+ processEvent(event.first, event.second);
+ }
+
+ for (String const& achievement : m_pendingAchievementChecks) {
+ if (checkAchievement(achievement))
+ unlockAchievement(achievement);
+ }
+
+ if (m_service && (m_pendingEvents.size() > 0 || m_pendingAchievementChecks.size() > 0))
+ m_service->flush();
+ m_pendingEvents = {};
+ m_pendingAchievementChecks = {};
+}
+
+Statistics::Stat Statistics::Stat::fromJson(Json const& json) {
+ return Stat {
+ json.getString("type"),
+ json.get("value")
+ };
+}
+
+Json Statistics::Stat::toJson() const {
+ return JsonObject {
+ {"type", type},
+ {"value", value}
+ };
+}
+
+void Statistics::processEvent(String const& name, Json const& fields) {
+ if (m_service)
+ m_service->reportEvent(name, fields);
+ Logger::debug("Event %s %s", name, fields);
+
+ auto statisticsDatabase = Root::singleton().statisticsDatabase();
+ if (auto const& event = statisticsDatabase->event(name)) {
+ runStatScript(event->scripts, event->config, "event", name, fields);
+ }
+}
+
+void Statistics::setStat(String const& name, String const& type, Json const& value) {
+ Logger::debug("Stat %s (%s) : %s", name, type, value);
+ m_stats[name] = Stat { type, value };
+ if (m_service)
+ m_service->setStat(name, type, value);
+
+ auto statisticsDatabase = Root::singleton().statisticsDatabase();
+ m_pendingAchievementChecks.addAll(statisticsDatabase->achievementsForStat(name));
+}
+
+void Statistics::unlockAchievement(String const& name) {
+ if (achievementUnlocked(name))
+ return;
+ m_achievements.add(name);
+ if (m_service)
+ m_service->unlockAchievement(name);
+ Logger::debug("Achievement get %s", name);
+}
+
+bool Statistics::checkAchievement(String const& achievementName) {
+ auto statisticsDatabase = Root::singleton().statisticsDatabase();
+ auto achievement = statisticsDatabase->achievement(achievementName);
+ if (achievementUnlocked(achievement->name))
+ return true;
+
+ Maybe<bool> result = runStatScript<bool>(achievement->scripts, achievement->config, "check", achievementName);
+ return result && *result;
+}
+
+void Statistics::readStatistics() {
+ auto versioningDatabase = Root::singleton().versioningDatabase();
+ try {
+ String filename = File::relativeTo(m_storageDirectory, "statistics");
+ if (File::exists(filename)) {
+ Json storage = versioningDatabase->loadVersionedJson(VersionedJson::readFile(filename), "Statistics");
+
+ m_stats = StringMap<Stat>::from(storage.getObject("stats", {}).pairs().transformed([](auto const& entry) {
+ return make_pair(entry.first, Stat::fromJson(entry.second));
+ }));
+ m_achievements = jsonToStringSet(storage.get("achievements", JsonArray{}));
+
+ }
+ } catch (std::exception const& e) {
+ Logger::warn("Error loading local player statistics file, resetting: %s", outputException(e, false));
+ }
+}
+
+void Statistics::mergeServiceStatistics() {
+ if (!m_service || !m_service->initialized() || m_service->error()) return;
+
+ // Publish achievements we unlocked when the platform service was unavailable.
+ StringSet serviceAchievements = m_service->achievementsUnlocked();
+ for (String const& achievement : m_achievements.difference(serviceAchievements)) {
+ m_service->unlockAchievement(achievement);
+ }
+ // Locally store all the achievements we unlocked in a different install:
+ m_achievements.addAll(serviceAchievements);
+
+ // Publish our local statistics, in case we made progress while the service
+ // was unavailable.
+ for (auto const& stat : m_stats) {
+ m_service->setStat(stat.first, stat.second.type, stat.second.value);
+ }
+
+ // However, don't _pull_ stats from the service - not all stats are recorded
+ // so inconsistencies will creep in if we try. For example, if the service
+ // is recording the number of poptop kills but not the total number of kills,
+ // we could end up with a situation like "2 total kills, 8 poptops killed."
+
+ // The best we can do is let the client be authoritative over its stats and
+ // have the service validate changes it receives to make sure they only
+ // ever increase.
+
+ m_service->flush();
+}
+
+LuaCallbacks Statistics::makeStatisticsCallbacks() {
+ LuaCallbacks callbacks;
+
+ callbacks.registerCallbackWithSignature<void, String, String, Json>("setStat", bind(&Statistics::setStat, this, _1, _2, _3));
+
+ callbacks.registerCallbackWithSignature<Json, String, Json>("stat", bind(&Statistics::stat, this, _1, _2));
+
+ callbacks.registerCallbackWithSignature<Maybe<String>, String>("statType", bind(&Statistics::statType, this, _1));
+
+ callbacks.registerCallbackWithSignature<bool, String>("achievementUnlocked", bind(&Statistics::achievementUnlocked, this, _1));
+
+ callbacks.registerCallbackWithSignature<bool, String>("checkAchievement", bind(&Statistics::checkAchievement, this, _1));
+
+ callbacks.registerCallbackWithSignature<void, String>("unlockAchievement", bind(&Statistics::unlockAchievement, this, _1));
+
+ return callbacks;
+}
+
+template <typename Result, typename... V>
+Maybe<Result> Statistics::runStatScript(StringList const& scripts, Json const& config, String const& functionName, V&&... args) {
+ LuaBaseComponent script;
+ script.setLuaRoot(m_luaRoot);
+ script.setScripts(scripts);
+ script.addCallbacks("config", LuaBindings::makeConfigCallbacks([config] (String const& name, Json const& def) {
+ return config.query(name, def);
+ }));
+ script.addCallbacks("statistics", makeStatisticsCallbacks());
+ script.init();
+ Maybe<Result> result = script.invoke<Result>(functionName, args...);
+ script.uninit();
+ return result;
+}
+
+}