aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDark1-dev <shansarkar272@gmail.com>2023-03-01 21:33:55 +0600
committerGitHub <noreply@github.com>2023-03-01 21:33:55 +0600
commit3c7253d6cdc23aac36208fa87dc6571c7cb7c5ff (patch)
tree8b5f9425bbfc4fdd5a29155aec38893b77481359
parentde57bc38217c09a0ae4a143f631896652368ecc3 (diff)
downloadMasterserver-Qt5-3c7253d6cdc23aac36208fa87dc6571c7cb7c5ff.tar.gz
Masterserver-Qt5-3c7253d6cdc23aac36208fa87dc6571c7cb7c5ff.zip
Add files via upload
-rw-r--r--Core/CoreObject/coreobject.cpp5
-rw-r--r--Core/CoreObject/coreobject.h30
-rw-r--r--Core/CoreObject/serverinfostructure.h31
-rw-r--r--Core/GameInfo/gameinfostructure.h24
-rw-r--r--Core/GameInfo/loadsupportedgames.cpp97
-rw-r--r--Core/GameInfo/loadsupportedgames.h16
-rw-r--r--Core/core.cpp15
-rw-r--r--Core/core.h43
-rw-r--r--Core/corerun.cpp75
-rw-r--r--Core/version.h43
-rw-r--r--Database/Common/commonactions.h31
-rw-r--r--Database/Common/existserver.cpp21
-rw-r--r--Database/Common/getgamenames.cpp28
-rw-r--r--Database/Common/getnumgames.cpp30
-rw-r--r--Database/Common/insertserver.cpp26
-rw-r--r--Database/Common/selectserverlist.cpp31
-rw-r--r--Database/Common/updateserver.cpp47
-rw-r--r--Database/closedatabase.cpp7
-rw-r--r--Database/createtables.cpp129
-rw-r--r--Database/databaseinterface.h24
-rw-r--r--Database/initdatabase.cpp77
-rw-r--r--Database/reportquery.cpp11
-rw-r--r--Logger/cyclelogfile.cpp48
-rw-r--r--Logger/initlog.cpp37
-rw-r--r--Logger/logevent.cpp20
-rw-r--r--Logger/logger.cpp10
-rw-r--r--Logger/logger.h46
-rw-r--r--Logger/logprimitive.h12
-rw-r--r--Logger/openlogfile.cpp21
-rw-r--r--Logger/writelogfile.cpp18
-rw-r--r--Maintenance/maintenance.cpp7
-rw-r--r--Maintenance/maintenance.h33
-rw-r--r--Maintenance/onmaintenancetimeraction.cpp28
-rw-r--r--Maintenance/prunebeacons.cpp16
-rw-r--r--Maintenance/pruneplayers.cpp18
-rw-r--r--Maintenance/pruneserverinfo.cpp15
-rw-r--r--Maintenance/schedulemaintenance.cpp16
-rw-r--r--Maintenance/updatestats.cpp59
38 files changed, 1245 insertions, 0 deletions
diff --git a/Core/CoreObject/coreobject.cpp b/Core/CoreObject/coreobject.cpp
new file mode 100644
index 0000000..11d1ab6
--- /dev/null
+++ b/Core/CoreObject/coreobject.cpp
@@ -0,0 +1,5 @@
+#include "coreobject.h"
+
+CoreObject::CoreObject()
+{
+}
diff --git a/Core/CoreObject/coreobject.h b/Core/CoreObject/coreobject.h
new file mode 100644
index 0000000..91bd33b
--- /dev/null
+++ b/Core/CoreObject/coreobject.h
@@ -0,0 +1,30 @@
+#ifndef COREOBJECT_H
+#define COREOBJECT_H
+
+#include "Core/CoreObject/serverinfostructure.h"
+#include "Core/GameInfo/gameinfostructure.h"
+#include "Logger/logger.h"
+#include "Settings/settingstructure.h"
+
+class CoreObject
+{
+public:
+ CoreObject();
+
+ // struct with internal and external settings
+ SettingStructure Settings;
+
+ // list of game details: gamename, cipher
+ QHash<QString, GameInfo> SupportedGames;
+
+ // logging functions
+ Logger Log;
+
+ // server address list acquired through third party masterservers
+ QList<ServerInfo> PendingServers;
+
+ // generate our session/identification string, to prevent self-syncing
+ QString masterserverIdentity; // msid value
+};
+
+#endif // COREOBJECT_H
diff --git a/Core/CoreObject/serverinfostructure.h b/Core/CoreObject/serverinfostructure.h
new file mode 100644
index 0000000..4f86adb
--- /dev/null
+++ b/Core/CoreObject/serverinfostructure.h
@@ -0,0 +1,31 @@
+#ifndef SERVERINFOSTRUCTURE_H
+#define SERVERINFOSTRUCTURE_H
+
+#include <QDateTime>
+#include <QHostAddress>
+
+struct ServerInfo
+{
+ // server address
+ QHostAddress ip;
+
+ // server port
+ unsigned short port = 0;
+
+ // gamename
+ QString gamename = "";
+
+ // date that the serverinfo was added or last updated
+ qint64 time = QDateTime::currentSecsSinceEpoch();
+};
+
+// compare operator
+inline bool operator== (const ServerInfo serverInfo1, const ServerInfo serverInfo2)
+{
+ // compare address, port and gamename. ignore time.
+ return ( serverInfo1.ip.isEqual(serverInfo2.ip) and
+ serverInfo1.port == serverInfo2.port and
+ serverInfo1.gamename == serverInfo2.gamename );
+}
+
+#endif // SERVERINFOSTRUCTURE_H
diff --git a/Core/GameInfo/gameinfostructure.h b/Core/GameInfo/gameinfostructure.h
new file mode 100644
index 0000000..b73e9ef
--- /dev/null
+++ b/Core/GameInfo/gameinfostructure.h
@@ -0,0 +1,24 @@
+#ifndef STRUCTGAMEINFO_H
+#define STRUCTGAMEINFO_H
+
+#include <QString>
+
+struct GameInfo
+{
+ // gamename is the unique identifier
+ QString gamename;
+
+ // 6-byte GameSpy identifier
+ QString cipher;
+
+ // game label
+ QString label;
+
+ // default port
+ unsigned short port = 0;
+
+ // known protocol
+ QString protocol;
+};
+
+#endif // STRUCTGAMEINFO_H
diff --git a/Core/GameInfo/loadsupportedgames.cpp b/Core/GameInfo/loadsupportedgames.cpp
new file mode 100644
index 0000000..9a73947
--- /dev/null
+++ b/Core/GameInfo/loadsupportedgames.cpp
@@ -0,0 +1,97 @@
+#include "loadsupportedgames.h"
+
+QHash<QString, GameInfo> loadSupportedGames (const QString &applicationPath)
+{
+ // return hash
+ QHash<QString, GameInfo> supportedGames;
+
+ // supported games file path (following README structure)
+ const QString supportPath = applicationPath + "/" + _supportedPath;
+
+ // determine if file exists
+ if ( ! QFile(supportPath).exists() )
+ {
+ // no game info file exists
+ logPrimitive() << "No games file found at " << supportPath << endl
+ << "Please provide the correct file . " << endl;
+ return supportedGames;
+ }
+
+ // load config file
+ QFile supportedFile(supportPath);
+ if ( ! supportedFile.open(QIODevice::ReadOnly) )
+ {
+ // error occurred. report and quit.
+ logPrimitive() << "Unable to open the game file. Is the file open or in use?" << endl;
+ return supportedGames;
+ }
+
+ // stats
+ int total = 0;
+
+ // load as json object
+ QJsonDocument supportedJsonData(QJsonDocument::fromJson(supportedFile.readAll()));
+
+ /* Optimisation for web interface: add games to the database.
+ *
+ * If database/json data do not match, replace database table
+ * with json data. This should only occur during database creation
+ * or when manual changes were made to the json file (which is rare).
+ */
+ bool dbInsert = supportedJsonData.array().count() != getNumGames(0).value("numTotal", 0);
+ QSqlQuery q;
+ if (dbInsert)
+ {
+ // no game info file exists
+ logPrimitive() << "Game info mismatch in database. Reloading json data. ("
+ << supportedJsonData.array().count() << "/"
+ << getNumGames(0).value("numTotal", 0) << ")" << endl;
+
+ // void existing data
+ q.prepare("DELETE FROM gameinfo");
+ if ( ! q.exec() )
+ reportQuery(q);
+ }
+
+ for (int i = 0; i < supportedJsonData.array().count(); i++)
+ {
+ // get the game object
+ QJsonObject thisGame = supportedJsonData.array().at(i).toObject();
+
+ GameInfo gameInfo;
+ gameInfo.gamename = thisGame.value("gamename").toString("");
+ gameInfo.cipher = thisGame.value("cipher").toString("");
+ gameInfo.port = static_cast<unsigned short>(thisGame.value("port").toDouble(0));
+ gameInfo.label = thisGame.value("label").toString("");
+
+ // insert in db if needed
+ if (dbInsert)
+ {
+ QString insertString = "INSERT INTO gameinfo (gamename, label) "
+ "VALUES (:gamename, :label)";
+
+ q.prepare(insertString);
+ q.bindValue(":gamename", gameInfo.gamename );
+ q.bindValue(":label", gameInfo.label);
+
+ if ( ! q.exec() )
+ reportQuery(q);
+ }
+
+ // add to list
+ supportedGames.insert(gameInfo.gamename, gameInfo);
+ total++;
+ }
+
+ // no games found? report this!
+ if (total <= 0)
+ {
+ // no game info exists
+ logPrimitive() << "No game info found in file at " << supportPath << endl
+ << "Please provide the correct file . " << endl;
+ return supportedGames;
+ }
+
+ // all games parsed. done.
+ return supportedGames;
+}
diff --git a/Core/GameInfo/loadsupportedgames.h b/Core/GameInfo/loadsupportedgames.h
new file mode 100644
index 0000000..333dd0b
--- /dev/null
+++ b/Core/GameInfo/loadsupportedgames.h
@@ -0,0 +1,16 @@
+#ifndef LOADSUPPORTEDGAMES_H
+#define LOADSUPPORTEDGAMES_H
+
+#include <QFile>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include "Database/Common/commonactions.h"
+#include "gameinfostructure.h"
+
+// load supported games from json file
+
+const QString _supportedPath = "../data/SupportedGames.json";
+QHash<QString, GameInfo> loadSupportedGames (const QString &applicationPath);
+
+#endif // LOADSUPPORTEDGAMES_H
diff --git a/Core/core.cpp b/Core/core.cpp
new file mode 100644
index 0000000..04c7e53
--- /dev/null
+++ b/Core/core.cpp
@@ -0,0 +1,15 @@
+#include "core.h"
+
+Core::Core(QString applicationPath)
+{
+ _applicationPath = applicationPath;
+}
+
+// TODO
+void Core::shutdown()
+{
+ logPrimitive() << "[stop] quitting masterserver application" << endl;
+
+ // end application
+ exit(0);
+}
diff --git a/Core/core.h b/Core/core.h
new file mode 100644
index 0000000..bde3a04
--- /dev/null
+++ b/Core/core.h
@@ -0,0 +1,43 @@
+#ifndef CORE_H
+#define CORE_H
+
+#include "Core/CoreObject/coreobject.h"
+#include "Core/GameInfo/loadsupportedgames.h"
+#include "Core/version.h"
+#include "Database/databaseinterface.h"
+#include "Settings/loadsettings.h"
+#include "Maintenance/maintenance.h"
+#include "Protocols/GameSpy0/securevalidate.h"
+#include "UdpTasks/BeaconServer/beaconserver.h"
+#include "UdpTasks/StatusChecker/statuschecker.h"
+#include "TcpTasks/ListenServer/listenserver.h"
+#include "TcpTasks/Updater/syncupdater.h"
+
+class Core : public QObject
+{
+ Q_OBJECT
+public:
+ Core(QString applicationPath);
+ void run();
+ void shutdown();
+
+private:
+ // root path
+ QString _applicationPath = "";
+
+ // internal dataobject
+ QSharedPointer<CoreObject> _coreObject = QSharedPointer<CoreObject>(new CoreObject);
+
+ // Networking services
+ QSharedPointer<BeaconServer> _udpBeaconServer = QSharedPointer<BeaconServer>(new BeaconServer(_coreObject));
+ QSharedPointer<ListenServer> _tcpListenServer = QSharedPointer<ListenServer>(new ListenServer(_coreObject));
+
+ // Updaters
+ QSharedPointer<SyncUpdater> _syncUpdater = QSharedPointer<SyncUpdater> (new SyncUpdater (_coreObject));
+ QSharedPointer<StatusChecker> _statusChecker = QSharedPointer<StatusChecker>(new StatusChecker(_coreObject));
+
+ // Maintenance
+ QSharedPointer<Maintenance> _maintenance = QSharedPointer<Maintenance>(new Maintenance(_coreObject));
+};
+
+#endif // CORE_H
diff --git a/Core/corerun.cpp b/Core/corerun.cpp
new file mode 100644
index 0000000..c978026
--- /dev/null
+++ b/Core/corerun.cpp
@@ -0,0 +1,75 @@
+#include "core.h"
+
+void Core::run()
+{
+ // randomize
+ qsrand(static_cast<unsigned int>(QDateTime::currentMSecsSinceEpoch()));
+
+ // announce startup
+ logPrimitive() << "*** Starting 333networks Master Server v" << BUILD_VERSION << " ***" << endl;
+
+ // can not set file paths
+ if (_applicationPath.length() <= 0)
+ this->shutdown();
+
+ // debug info (hardcoded disable for releases)
+ if ( false )
+ logPrimitive() << "Using Qt " << qVersion() << endl;
+
+ // set our own 12-byte identifier
+ _coreObject->masterserverIdentity = genChallengeString(12, true);
+
+ // load config settings from file
+ _coreObject->Settings = loadSettings(_applicationPath);
+ if ( ! _coreObject->Settings.init )
+ this->shutdown();
+
+ // initialise database
+ if ( ! initDatabase(_applicationPath) )
+ this->shutdown();
+
+ // load game info from file and into database
+ _coreObject->SupportedGames = loadSupportedGames(_applicationPath);
+ if ( _coreObject->SupportedGames.count() <= 0 )
+ this->shutdown();
+
+ // logger init
+ if ( ! _coreObject->Log.init(_applicationPath, _coreObject->Settings) )
+ this->shutdown();
+
+ /*
+ * enter runmode
+ */
+
+ // udp beacon server
+ if ( ! _udpBeaconServer->listen() )
+ this->shutdown();
+
+ // tcp listen server
+ if ( ! _tcpListenServer->listen() )
+ this->shutdown();
+
+ /*
+ * advanced functionality
+ */
+
+ // maintenance and statistics
+ if ( _coreObject->Settings.MaintenanceSettings.doMaintenance )
+ _maintenance->scheduleMaintenance();
+
+ // udp uplink broadcast every X minutes
+ if ( _coreObject->Settings.BeaconServerSettings.doUplink )
+ _udpBeaconServer->uplink();
+
+ // syncing with other masterservers
+ if ( _coreObject->Settings.SyncerSettings.doSync )
+ _syncUpdater->scheduleUpdater();
+
+ // server checker
+ if ( _coreObject->Settings.CheckerSettings.doCheck )
+ _statusChecker->startTicker();
+
+ /*
+ * all services running
+ */
+}
diff --git a/Core/version.h b/Core/version.h
new file mode 100644
index 0000000..6dbc808
--- /dev/null
+++ b/Core/version.h
@@ -0,0 +1,43 @@
+#ifndef VERSION_H
+#define VERSION_H
+
+#include <QString>
+
+/*
+ * Version and author information
+ *
+ * Only when you make (significant) modifications to the master server source
+ * code, you should edit these variables to reflect your changes.
+ *
+ * For example,
+ * if you limit functionality to only one game, you should change the variable
+ * BUILD_TYPE and SHORT_VER to something that reflects the change in function.
+ *
+ * In addition, if you have the actual interest to go through all this source
+ * code to end up here, consider sending Darkelarious (the original author) a
+ * postcard or (e)mail with your compliments. Or buy us a coffee. We like the
+ * appreciation.
+ */
+
+// gamename for the 333networks-type MasterServer
+#define TYPE_GAMENAME QString("333networks")
+
+// build type: type of software
+#define BUILD_TYPE QString("MasterServer Qt5")
+
+// software version (of this particular type)
+#define BUILD_VERSION QString("0.27")
+
+// short version (in query) -- Qt v0.n
+#define SHORT_VER QString("Qt-" + BUILD_VERSION)
+
+// build time/date
+#define BUILD_TIME QStringLiteral("%1 %2").arg(__DATE__).arg(__TIME__)
+
+// software author, contact
+#define BUILD_AUTHOR QString("Darkelarious <darkelarious@333networks.com>")
+
+// minimum required database version
+#define DATABASE_VERSION 0.27
+
+#endif // VERSION_H
diff --git a/Database/Common/commonactions.h b/Database/Common/commonactions.h
new file mode 100644
index 0000000..3bce0b6
--- /dev/null
+++ b/Database/Common/commonactions.h
@@ -0,0 +1,31 @@
+#ifndef COMMONACTIONS_H
+#define COMMONACTIONS_H
+
+#include <QDateTime>
+#include "Database/databaseinterface.h"
+#include "Logger/logprimitive.h"
+
+// insert, update or delete a server from the list
+bool insertServer(const QString &serverAddress,
+ const unsigned short &serverPort,
+ const QString &gamename,
+ const bool &directBeacon);
+
+bool updateServer(const QString &serverAddress,
+ const unsigned short &serverPort,
+ const QString &gamename,
+ const bool &directBeacon,
+ const bool &authenticated);
+
+bool existServer(const QString &serverAddress,
+ const unsigned short &serverPort);
+
+QSqlQuery selectServerList(const QString &gamename,
+ const int &serverAge_s,
+ const bool &withSyncData);
+
+QStringList getGamenames(const int &serverAge_s);
+
+QHash<QString, int> getNumGames(const int &serverAge_s);
+
+#endif // COMMONACTIONS_H
diff --git a/Database/Common/existserver.cpp b/Database/Common/existserver.cpp
new file mode 100644
index 0000000..9609948
--- /dev/null
+++ b/Database/Common/existserver.cpp
@@ -0,0 +1,21 @@
+#include "commonactions.h"
+
+bool existServer(const QString &serverAddress,
+ const unsigned short &serverPort)
+{
+ // find existing entry
+ QSqlQuery q;
+ QString selectString = "SELECT id FROM serverlist "
+ "WHERE ip = :ip AND queryport = :queryport ";
+
+ // bind values and execute
+ q.prepare(selectString);
+ q.bindValue(":ip", serverAddress);
+ q.bindValue(":queryport", serverPort);
+
+ if ( ! q.exec() )
+ return reportQuery(q);
+
+ // was a row/server found?
+ return q.next();
+}
diff --git a/Database/Common/getgamenames.cpp b/Database/Common/getgamenames.cpp
new file mode 100644
index 0000000..8093f7c
--- /dev/null
+++ b/Database/Common/getgamenames.cpp
@@ -0,0 +1,28 @@
+#include "commonactions.h"
+
+QStringList getGamenames(const int &serverAge_s)
+{
+ // init output
+ QStringList gamenameList;
+
+ // retrieve active gamenames from database
+ QSqlQuery q;
+ QString selectString = "SELECT DISTINCT gamename FROM serverlist "
+ "WHERE dt_updated > :timestamp "
+ "ORDER BY gamename ASC";
+
+ // bind and execute
+ q.prepare(selectString);
+ q.bindValue(":timestamp", QDateTime::currentDateTime().addSecs(-serverAge_s ).toSecsSinceEpoch());
+
+ if ( ! q.exec() )
+ reportQuery(q);
+
+ // parse to stringlist
+ while ( q.next() )
+ {
+ gamenameList.append( q.value(0).toString() );
+ }
+
+ return gamenameList;
+}
diff --git a/Database/Common/getnumgames.cpp b/Database/Common/getnumgames.cpp
new file mode 100644
index 0000000..1425427
--- /dev/null
+++ b/Database/Common/getnumgames.cpp
@@ -0,0 +1,30 @@
+#include "commonactions.h"
+
+QHash<QString, int> getNumGames(const int &serverAge_s)
+{
+ QSqlQuery q;
+ QHash<QString, int> numGames;
+
+ // number of games
+ q.prepare("SELECT count(gamename) FROM gameinfo");
+ if ( ! q.exec() )
+ reportQuery(q);
+ if (q.next())
+ numGames["numTotal"] = q.value(0).toInt();
+
+ // number of active games
+ QString selectString = "SELECT count(gamename) FROM gameinfo "
+ "WHERE num_direct > 0 "
+ "OR num_total > 0 ";
+ q.prepare(selectString);
+ q.bindValue(":timestamp", QDateTime::currentDateTime().addSecs(-serverAge_s ).toSecsSinceEpoch());
+ if ( ! q.exec() )
+ reportQuery(q);
+ if (q.next())
+ numGames["numActive"] = q.value(0).toInt();
+
+
+ // TODO: get more relevant stats
+
+ return numGames;
+}
diff --git a/Database/Common/insertserver.cpp b/Database/Common/insertserver.cpp
new file mode 100644
index 0000000..05b2eaa
--- /dev/null
+++ b/Database/Common/insertserver.cpp
@@ -0,0 +1,26 @@
+#include "commonactions.h"
+
+// insert a server into the list
+bool insertServer(const QString &serverAddress,
+ const unsigned short &serverPort,
+ const QString &gamename,
+ const bool &directBeacon)
+{
+ // insert query string
+ QSqlQuery q;
+ QString insertString = "INSERT INTO serverlist (ip, queryport, gamename, f_direct) "
+ "VALUES (:ip, :queryport, :gamename, :directbeacon)";
+
+ // bind values and execute
+ q.prepare(insertString);
+ q.bindValue(":ip", serverAddress);
+ q.bindValue(":queryport", serverPort);
+ q.bindValue(":gamename", gamename);
+ q.bindValue(":directbeacon", ( directBeacon ? 1 : 0 ) ); // bool to int
+
+ if ( ! q.exec() )
+ return reportQuery(q);
+
+ // was a row inserted?
+ return (q.numRowsAffected() > 0);
+}
diff --git a/Database/Common/selectserverlist.cpp b/Database/Common/selectserverlist.cpp
new file mode 100644
index 0000000..c4797ed
--- /dev/null
+++ b/Database/Common/selectserverlist.cpp
@@ -0,0 +1,31 @@
+#include "commonactions.h"
+
+QSqlQuery selectServerList(const QString &gamename,
+ const int &serverAge_s,
+ const bool &withSyncData)
+{
+ // retrieve servers from database
+ QSqlQuery q;
+ QString selectString = "SELECT ip, queryport FROM serverlist "
+ "WHERE gamename = :gamename ";
+
+ if ( withSyncData )
+ {
+ // relies on sync data to be accurate (if checker is not enabled)
+ selectString += "AND (dt_updated > :timestamp OR dt_sync > :timestamp)";
+ }
+ else
+ {
+ // sync data may not be (sufficiently) accurate, only use data that we verified directly
+ selectString += "AND dt_updated > :timestamp";
+ }
+
+ // bind values and execute
+ q.prepare(selectString);
+ q.bindValue(":gamename", gamename);
+ q.bindValue(":timestamp", QDateTime::currentDateTime().addSecs(-serverAge_s ).toSecsSinceEpoch());
+ if ( ! q.exec() )
+ reportQuery(q);
+
+ return q;
+}
diff --git a/Database/Common/updateserver.cpp b/Database/Common/updateserver.cpp
new file mode 100644
index 0000000..24fa26f
--- /dev/null
+++ b/Database/Common/updateserver.cpp
@@ -0,0 +1,47 @@
+#include "commonactions.h"
+
+bool updateServer(const QString &serverAddress,
+ const unsigned short &serverPort,
+ const QString &gamename,
+ const bool &directBeacon,
+ const bool &authenticated
+ )
+{
+ // update existing entry, but do not insert.
+ QSqlQuery q;
+ QString updateString;
+
+ // update with available values
+ updateString = "UPDATE serverlist SET ";
+
+ /*
+ * Note that direct/auth set to 'false' will NOT override a previous value in the database
+ */
+
+ // is this a direct beacon?
+ if (directBeacon)
+ {
+ updateString += "f_direct = 1, ";
+ updateString += "dt_beacon = :timestamp, ";
+ }
+
+ // did the server authenticate?
+ if (authenticated) updateString += "f_auth = 1, ";
+
+ updateString += "gamename = :gamename, "
+ "dt_updated = :timestamp "
+ "WHERE ip = :ip AND queryport = :queryport";
+
+ // bind values and execute
+ q.prepare(updateString);
+ q.bindValue(":ip", serverAddress);
+ q.bindValue(":queryport", serverPort);
+ q.bindValue(":gamename", gamename);
+ q.bindValue(":timestamp", QDateTime::currentSecsSinceEpoch() );
+
+ if ( ! q.exec() )
+ return reportQuery(q);
+
+ // was a row updated?
+ return (q.numRowsAffected() > 0);
+}
diff --git a/Database/closedatabase.cpp b/Database/closedatabase.cpp
new file mode 100644
index 0000000..6f3ff89
--- /dev/null
+++ b/Database/closedatabase.cpp
@@ -0,0 +1,7 @@
+#include "databaseinterface.h"
+
+void closeDatabase()
+{
+ QSqlDatabase dbi;
+ dbi.close();
+}
diff --git a/Database/createtables.cpp b/Database/createtables.cpp
new file mode 100644
index 0000000..b437163
--- /dev/null
+++ b/Database/createtables.cpp
@@ -0,0 +1,129 @@
+#include "databaseinterface.h"
+
+bool createTables()
+{
+ { // create serverlist
+ QSqlQuery q;
+ QString createServerlist = QStringLiteral(
+ "CREATE TABLE serverlist("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "gamename TEXT NOT NULL DEFAULT '',"
+ "ip TEXT NOT NULL ,"
+ "queryport INTEGER NOT NULL DEFAULT 0,"
+ "t_protocol INTEGER NOT NULL DEFAULT 0,"
+ "f_blacklist INTEGER NOT NULL DEFAULT 0,"
+ "f_auth INTEGER NOT NULL DEFAULT 0,"
+ "f_direct INTEGER NOT NULL DEFAULT 0,"
+ "dt_added BIGINT DEFAULT (CAST(strftime('%s','now') AS BIGINT)),"
+ "dt_beacon BIGINT DEFAULT (CAST(strftime('%s','now') AS BIGINT)),"
+ "dt_sync BIGINT DEFAULT (CAST(strftime('%s','now') AS BIGINT)),"
+ "dt_updated BIGINT DEFAULT (CAST(strftime('%s','now') AS BIGINT))"
+ ")");
+
+ // bind values and execute (not all db-interfaces support prepare(create table), exec directly)
+ if ( ! q.exec(createServerlist) )
+ return reportQuery(q);
+ }
+
+ { // create serverinfo
+ QSqlQuery q;
+ QString createServerinfo = QStringLiteral(
+ "CREATE TABLE serverinfo("
+ "sid INTEGER NOT NULL DEFAULT 0,"
+ "hostport INTEGER NOT NULL DEFAULT 0,"
+ "hostname TEXT,"
+ "gamever TEXT,"
+ "minnetver TEXT,"
+ "country TEXT,"
+ "location TEXT,"
+ "listenserver TEXT,"
+ "adminname TEXT,"
+ "adminemail TEXT,"
+ "password TEXT,"
+ "gametype TEXT,"
+ "gamestyle TEXT,"
+ "changelevels TEXT,"
+ "maptitle TEXT,"
+ "mapname TEXT,"
+ "numplayers INTEGER DEFAULT 0,"
+ "maxplayers INTEGER DEFAULT 0,"
+ "minplayers INTEGER DEFAULT 0,"
+ "botskill TEXT,"
+ "balanceteams TEXT,"
+ "playersbalanceteams TEXT,"
+ "friendlyfire TEXT,"
+ "maxteams TEXT,"
+ "timelimit TEXT,"
+ "goalteamscore TEXT,"
+ "fraglimit TEXT,"
+ "mutators TEXT DEFAULT 'None',"
+ "misc TEXT,"
+ "dt_serverinfo BIGINT DEFAULT (CAST(strftime('%s','now') AS BIGINT))"
+ ")");
+
+ // bind values and execute
+ if ( ! q.exec(createServerinfo) )
+ return reportQuery(q);
+ }
+
+ { // create playerinfo
+ QSqlQuery q;
+ QString createPlayerlist = QStringLiteral(
+ "CREATE TABLE playerinfo("
+ "sid INTEGER NOT NULL DEFAULT 0,"
+ "name TEXT DEFAULT 'Player',"
+ "team TEXT,"
+ "frags INTEGER DEFAULT 0,"
+ "mesh TEXT,"
+ "skin TEXT,"
+ "face TEXT,"
+ "ping INTEGER DEFAULT 0,"
+ "misc TEXT,"
+ "dt_player BIGINT DEFAULT (CAST(strftime('%s','now') AS BIGINT))"
+ ")");
+
+ // bind values and execute
+ if ( ! q.exec(createPlayerlist) )
+ return reportQuery(q);
+ }
+
+ { // create gameinfo
+ QSqlQuery q;
+ QString createGamelist = QStringLiteral(
+ "CREATE TABLE gameinfo("
+ "gamename TEXT NOT NULL DEFAULT '',"
+ "label TEXT NOT NULL DEFAULT '',"
+ "num_total INTEGER NOT NULL DEFAULT 0,"
+ "num_direct INTEGER NOT NULL DEFAULT 0"
+ ")");
+
+ // bind values and execute
+ if ( ! q.exec(createGamelist) )
+ return reportQuery(q);
+ }
+
+ { // create version info
+ QSqlQuery q;
+ QString createVersion = QStringLiteral(
+ "CREATE TABLE versioninfo("
+ "type TEXT NOT NULL DEFAULT '',"
+ "version TEXT NOT NULL DEFAULT ''"
+ ")");
+
+ // bind values and execute
+ if ( ! q.exec(createVersion) )
+ return reportQuery(q);
+
+ // insert current version directly into the db during creation
+ q.prepare("INSERT INTO versioninfo (type, version) VALUES (\"database\", :currentver)");
+ q.bindValue(":currentver", BUILD_VERSION);
+
+ if ( ! q.exec() )
+ reportQuery(q);
+ }
+
+ // TODO: optional: write an sql file to /data/tables.sql for easy access on generated tables.
+
+ // no errors
+ return true;
+}
diff --git a/Database/databaseinterface.h b/Database/databaseinterface.h
new file mode 100644
index 0000000..f80c93a
--- /dev/null
+++ b/Database/databaseinterface.h
@@ -0,0 +1,24 @@
+#ifndef DATABASEINTERFACE_H
+#define DATABASEINTERFACE_H
+
+#include <QSqlDatabase>
+#include <QSqlQuery>
+#include <QSqlError>
+#include <QDataStream>
+#include "Core/version.h"
+#include "Logger/logprimitive.h"
+
+// database file path (following README structure)
+const QString _sqlitePath = "../data/masterserver.db";
+
+// init and close
+bool initDatabase(const QString applicationPath);
+void closeDatabase();
+
+// report execution errors (terminal/display only)
+bool reportQuery(const QSqlQuery &q); // always returns false!
+
+// generate tables in first run
+bool createTables();
+
+#endif // DATABASEINTERFACE_H
diff --git a/Database/initdatabase.cpp b/Database/initdatabase.cpp
new file mode 100644
index 0000000..06cd37c
--- /dev/null
+++ b/Database/initdatabase.cpp
@@ -0,0 +1,77 @@
+#include "databaseinterface.h"
+
+bool initDatabase(const QString applicationPath)
+{
+ // open SQLite database
+ QSqlDatabase dbi = QSqlDatabase::addDatabase( "QSQLITE" );
+ dbi.setDatabaseName(applicationPath + "/" + _sqlitePath);
+
+ // open database
+ if ( ! dbi.open() )
+ {
+ QSqlError sqlError = dbi.lastError();
+ logPrimitive() << "Error opening database: " << sqlError.text() << endl;
+ return false;
+ }
+
+ // speed up SQLite with keeping journals in memory and asynchronous writing
+ dbi.exec("PRAGMA synchronous = OFF");
+ dbi.exec("PRAGMA journal_mode = MEMORY");
+
+ { // check if the database was generated with this version of the software
+
+ // serverlist exists (and thus the other tables exist) but not version
+ if ( dbi.tables().contains("serverlist") and ! dbi.tables().contains("versioninfo"))
+ {
+ // outdated because the "version" table does not even exist yet (pre-0.14)
+ logPrimitive() << "The database tables are outdated (pre-0.14). Please remove the current database." << endl;
+ return false;
+ }
+
+ // does the version table exist, and does it contain the correct version?
+ if ( dbi.tables().contains("versioninfo") )
+ {
+ float dbVersion = 0.0;
+ QSqlQuery versionQuery;
+ versionQuery.prepare("SELECT version FROM versioninfo WHERE type = \"database\"");
+
+ // failed query? (unlikely)
+ if ( ! versionQuery.exec() )
+ return reportQuery(versionQuery);
+
+ if (versionQuery.next())
+ dbVersion = versionQuery.value("version").toFloat();
+
+ // version check is intentionally hardcoded since the last change in database structure (currently v0.15)
+ if ( dbVersion < DATABASE_VERSION )
+ {
+ logPrimitive() << "The database tables are outdated (" << QString::number(dbVersion) << "). Please remove the current database." << endl;
+ return false;
+ }
+
+ // tables up to date
+ return true;
+ }
+ }
+
+ // specifying AUTO_CREATE_DB=y in the environment allows bypassing of the interactive prompt for non-interactive environments
+ if (qgetenv("AUTO_CREATE_DB") != "y")
+ {
+ // relevant tables do not exist. ask to generate.
+ logPrimitive() << "The database tables do not exist. Do you want to generate these? [y/N]" << endl;
+
+ // if no, do not proceed.
+ if ( ! QTextStream(stdin).readLine().startsWith("y") )
+ {
+ // do not generate. failed to load database.
+ return false;
+ }
+ }
+ else
+ {
+ logPrimitive() << "The database tables do not exist and will now be created." << endl;
+ }
+
+ // create tables and proceed
+ return createTables();
+}
diff --git a/Database/reportquery.cpp b/Database/reportquery.cpp
new file mode 100644
index 0000000..2a5d7f7
--- /dev/null
+++ b/Database/reportquery.cpp
@@ -0,0 +1,11 @@
+#include "databaseinterface.h"
+
+bool reportQuery(const QSqlQuery &q)
+{
+ logPrimitive() << "Database query error." << endl << "" << endl
+ << "Technical info: " << q.lastQuery() << endl << "" << endl
+ << "Reported error: " << q.lastError().text() << endl;
+
+ // always return false, so we can report error and return false in one line.
+ return false;
+}
diff --git a/Logger/cyclelogfile.cpp b/Logger/cyclelogfile.cpp
new file mode 100644
index 0000000..cb0266e
--- /dev/null
+++ b/Logger/cyclelogfile.cpp
@@ -0,0 +1,48 @@
+#include "logger.h"
+
+// if "now" matches the previous logfile name, no cycling needed. otherwise, determine new logfile name.
+bool Logger::cycleLogFile()
+{
+ // calculate the expected log filename
+ QString newFilename = _logLabel;
+
+ // get date to see if we need to cycle
+ QDate date(QDate::currentDate());
+
+ // when is it necessary to cycle?
+ switch ( _cyclePeriod )
+ {
+ case 1: // yearly
+ newFilename += date.toString("-yyyy");
+ break;
+ case 2: // monthly
+ newFilename += date.toString("-yyyy-MM");
+ break;
+ case 3: // weekly
+ newFilename += date.toString("-yyyy") + "-week" + QString("%1").arg(date.weekNumber(), 2, 10, QChar('0'));
+ break;
+ case 4: // daily
+ newFilename += date.toString("-yyyy-MM-dd");
+ break;
+ default:; // 0 = never
+ }
+
+ // add log file extension
+ newFilename += ".log";
+
+ // if current and new filename are not the same, cycle log file
+ if (_logFileName != newFilename)
+ {
+ // cycle log file
+ _logFileName = newFilename;
+
+ // close the existing/opened log file
+ closeLogFile();
+
+ // open the new log file (pass possible error to caller)
+ return openLogFile();
+ }
+
+ // else: no cycling necessary
+ return true;
+}
diff --git a/Logger/initlog.cpp b/Logger/initlog.cpp
new file mode 100644
index 0000000..d852c86
--- /dev/null
+++ b/Logger/initlog.cpp
@@ -0,0 +1,37 @@
+#include "logger.h"
+
+bool Logger::init(const QString &applicationPath,
+ const SettingStructure &settings)
+{
+ // test if log directory exists
+ _logPath = applicationPath + "/" + _logDirectory;
+ QDir logDir(_logPath);
+ if ( ! logDir.mkpath(".") )
+ {
+ QTextStream(stdout) << "Failed to access log directory at " + _logPath << endl
+ << "Please create the director and/or apply relevant permissions." << endl;
+ return false;
+ }
+
+ // log cycling -- determine log cycling setting
+ const QHash<QString, int> logCycling = {
+ {"never", 0},
+ {"yearly", 1},
+ {"monthly", 2},
+ {"weekly", 3},
+ {"daily", 4}
+ };
+
+ // load log-specific settings
+ _cyclePeriod = logCycling.value( settings.LoggingSettings.cycle );
+ _suppressLog = settings.LoggingSettings.suppressLog;
+ _suppressDisplay = settings.LoggingSettings.suppressDisplay;
+
+ // cycling initialisation ("never" is also a cycle)
+ if ( ! cycleLogFile() )
+ {
+ QTextStream(stdout) << "Failed to access log file " + _logFileName + " at " + _logPath << endl;
+ return false;
+ }
+ return true;
+}
diff --git a/Logger/logevent.cpp b/Logger/logevent.cpp
new file mode 100644
index 0000000..9d1858c
--- /dev/null
+++ b/Logger/logevent.cpp
@@ -0,0 +1,20 @@
+#include "logger.h"
+
+void Logger::logEvent(const QString &messageType,
+ const QString &message)
+{
+ // printing to display suppressed?
+ if ( ! _suppressDisplay.contains(messageType) and ! _suppressDisplay.contains("all") )
+ {
+ QString dateTimeStr(QDateTime::currentDateTime().toString("dd-MM-yyyy HH:mm:ss:zzz"));
+ logPrimitive() << QStringLiteral("[%1][%2]\t%3").arg(dateTimeStr, messageType, message.toLatin1()) << endl;
+ }
+
+ // printing to logfile suppressed?
+ if ( ! _suppressLog.contains(messageType) and ! _suppressLog.contains("all") )
+ {
+ // write message to log
+ QString dateTimeStr(QDateTime::currentDateTime().toString("dd-MM-yyyy HH:mm:ss:zzz"));
+ writeLogFile( QStringLiteral("[%1][%2]\t%3").arg(dateTimeStr, messageType, message.toLatin1()) );
+ }
+}
diff --git a/Logger/logger.cpp b/Logger/logger.cpp
new file mode 100644
index 0000000..3bf7def
--- /dev/null
+++ b/Logger/logger.cpp
@@ -0,0 +1,10 @@
+#include "logger.h"
+
+Logger::Logger()
+{
+}
+
+void Logger::stop()
+{
+ closeLogFile();
+}
diff --git a/Logger/logger.h b/Logger/logger.h
new file mode 100644
index 0000000..9880c44
--- /dev/null
+++ b/Logger/logger.h
@@ -0,0 +1,46 @@
+#ifndef LOGGER_H
+#define LOGGER_H
+
+#include <QDir>
+#include <QFile>
+#include <QDateTime>
+#include <QTextStream>
+
+#include "Settings/settingstructure.h"
+#include "Core/version.h"
+#include "logprimitive.h"
+
+class Logger
+{
+public:
+ Logger();
+ bool init(const QString &applicationPath,
+ const SettingStructure &settings);
+ void stop();
+ void logEvent(const QString &messageType,
+ const QString &message);
+
+private:
+ // path variables
+ const QString _logDirectory = "../log";
+ const QString _logLabel = "MasterServer-" + SHORT_VER;
+ QString _logPath = "";
+
+ // local variables
+ int _cyclePeriod;
+ QString _logFileName;
+ QString _suppressLog;
+ QString _suppressDisplay;
+
+ // file i/o
+ QFile _logFile;
+
+private:
+ bool cycleLogFile();
+ bool openLogFile();
+ void closeLogFile();
+ bool writeLogFile(const QString message);
+
+};
+
+#endif // LOGGER_H
diff --git a/Logger/logprimitive.h b/Logger/logprimitive.h
new file mode 100644
index 0000000..1630566
--- /dev/null
+++ b/Logger/logprimitive.h
@@ -0,0 +1,12 @@
+#ifndef LOGPRIMITIVE_H
+#define LOGPRIMITIVE_H
+
+#include <QTextStream>
+
+inline QTextStream& logPrimitive()
+{
+ static QTextStream r{stdout};
+ return r;
+}
+
+#endif // LOGPRIMITIVE_H
diff --git a/Logger/openlogfile.cpp b/Logger/openlogfile.cpp
new file mode 100644
index 0000000..349d058
--- /dev/null
+++ b/Logger/openlogfile.cpp
@@ -0,0 +1,21 @@
+#include "logger.h"
+
+bool Logger::openLogFile()
+{
+ // verify that there is no open logfile already
+ if ( _logFile.openMode() == QIODevice::NotOpen)
+ {
+ // set new log file
+ _logFile.setFileName(_logPath + "/" + _logFileName);
+ return _logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text);
+ }
+ // else:
+ // file already open, can not get a lock
+ return false;
+}
+
+void Logger::closeLogFile()
+{
+ // close file if open. if already closed, ignore.
+ _logFile.close();
+}
diff --git a/Logger/writelogfile.cpp b/Logger/writelogfile.cpp
new file mode 100644
index 0000000..954b5b2
--- /dev/null
+++ b/Logger/writelogfile.cpp
@@ -0,0 +1,18 @@
+#include "logger.h"
+
+bool Logger::writeLogFile(const QString message)
+{
+ // first see if we need to cycle the log file already
+ cycleLogFile();
+
+ // write to file
+ if ( _logFile.isOpen() )
+ {
+ QTextStream logStream(&_logFile);
+ logStream << message << endl;
+ return true;
+ }
+
+ logPrimitive() << "[log error]" << message;
+ return false;
+}
diff --git a/Maintenance/maintenance.cpp b/Maintenance/maintenance.cpp
new file mode 100644
index 0000000..8869f99
--- /dev/null
+++ b/Maintenance/maintenance.cpp
@@ -0,0 +1,7 @@
+#include "maintenance.h"
+
+Maintenance::Maintenance(const QSharedPointer<CoreObject> &coreObject)
+{
+ // create local access
+ this->_coreObject = coreObject;
+}
diff --git a/Maintenance/maintenance.h b/Maintenance/maintenance.h
new file mode 100644
index 0000000..b7a63a2
--- /dev/null
+++ b/Maintenance/maintenance.h
@@ -0,0 +1,33 @@
+#ifndef MAINTENANCE_H
+#define MAINTENANCE_H
+
+#include <QTimer>
+
+#include "Core/CoreObject/coreobject.h"
+#include "Database/databaseinterface.h"
+
+class Maintenance: public QObject
+{
+ Q_OBJECT
+
+public:
+ Maintenance(const QSharedPointer<CoreObject> &coreObject);
+ bool scheduleMaintenance();
+
+private:
+ QSharedPointer<CoreObject> _coreObject;
+
+ // schedule timer
+ QTimer _maintenanceTimer;
+
+private:
+ int updateStats();
+ int pruneBeacons();
+ int pruneServerInfo();
+ int prunePlayers();
+
+private slots:
+ void onMaintenanceTimerAction();
+};
+
+#endif // MAINTENANCE_H
diff --git a/Maintenance/onmaintenancetimeraction.cpp b/Maintenance/onmaintenancetimeraction.cpp
new file mode 100644
index 0000000..97e5c51
--- /dev/null
+++ b/Maintenance/onmaintenancetimeraction.cpp
@@ -0,0 +1,28 @@
+#include "maintenance.h"
+
+// perform maintenance actions
+void Maintenance::onMaintenanceTimerAction()
+{
+ // announce
+ _coreObject->Log.logEvent("main", QStringLiteral("performing maintenance"));
+
+ // update statistics
+ int numUpdated = updateStats();
+ if (numUpdated > 0)
+ _coreObject->Log.logEvent("stat", QStringLiteral("updated %1 game stats").arg(QString::number(numUpdated)));
+
+ // prune direct beacons
+ int numPrunedBeacons = pruneBeacons();
+ if (numPrunedBeacons > 0)
+ _coreObject->Log.logEvent("prune", QStringLiteral("pruned %1 direct beacons").arg(QString::number(numPrunedBeacons)));
+
+ // prune serverinfo, serverinfo from which the serverlist entry is gone already
+ int numPrunedInfo = pruneServerInfo();
+ if (numPrunedInfo > 0)
+ _coreObject->Log.logEvent("prune", QStringLiteral("pruned %1 server info entries").arg(QString::number(numPrunedInfo)));
+
+ // prune orphaned players from which the serverinfo entry is gone already OR updateinfo is outdated
+ int numPrunedPlayers = prunePlayers();
+ if (numPrunedPlayers > 0)
+ _coreObject->Log.logEvent("prune", QStringLiteral("pruned %1 players").arg(QString::number(numPrunedPlayers)));
+}
diff --git a/Maintenance/prunebeacons.cpp b/Maintenance/prunebeacons.cpp
new file mode 100644
index 0000000..10e4630
--- /dev/null
+++ b/Maintenance/prunebeacons.cpp
@@ -0,0 +1,16 @@
+#include "maintenance.h"
+
+int Maintenance::pruneBeacons()
+{
+ QString updateString = "UPDATE serverlist "
+ "SET f_direct = 0 "
+ "WHERE f_direct = 1 AND dt_beacon < :timestamp";
+ QSqlQuery updateQuery;
+ updateQuery.prepare(updateString);
+ updateQuery.bindValue(":timestamp", QDateTime::currentDateTime().addSecs(-600).toSecsSinceEpoch());
+
+ if ( ! updateQuery.exec() )
+ return reportQuery(updateQuery);
+
+ return updateQuery.numRowsAffected();
+}
diff --git a/Maintenance/pruneplayers.cpp b/Maintenance/pruneplayers.cpp
new file mode 100644
index 0000000..0bae4f9
--- /dev/null
+++ b/Maintenance/pruneplayers.cpp
@@ -0,0 +1,18 @@
+#include "maintenance.h"
+
+int Maintenance::prunePlayers()
+{
+ QString deleteString = "DELETE FROM playerinfo "
+ "WHERE playerinfo.sid NOT IN ( "
+ "SELECT serverinfo.sid FROM serverinfo) "
+ "OR dt_player < :timestamp";
+ QSqlQuery deleteQuery;
+ deleteQuery.prepare(deleteString);
+ deleteQuery.bindValue(":timestamp", QDateTime::currentDateTime()
+ .addSecs(-7200).toSecsSinceEpoch()); // 2 hours
+
+ if ( ! deleteQuery.exec() )
+ return reportQuery(deleteQuery);
+
+ return deleteQuery.numRowsAffected();
+}
diff --git a/Maintenance/pruneserverinfo.cpp b/Maintenance/pruneserverinfo.cpp
new file mode 100644
index 0000000..e3f2bd8
--- /dev/null
+++ b/Maintenance/pruneserverinfo.cpp
@@ -0,0 +1,15 @@
+#include "maintenance.h"
+
+int Maintenance::pruneServerInfo()
+{
+ QString deleteString = "DELETE FROM serverinfo "
+ "WHERE sid NOT IN ( "
+ "SELECT id FROM serverlist)";
+ QSqlQuery deleteQuery;
+ deleteQuery.prepare(deleteString);
+
+ if ( ! deleteQuery.exec() )
+ return reportQuery(deleteQuery);
+
+ return deleteQuery.numRowsAffected();
+}
diff --git a/Maintenance/schedulemaintenance.cpp b/Maintenance/schedulemaintenance.cpp
new file mode 100644
index 0000000..3a2decd
--- /dev/null
+++ b/Maintenance/schedulemaintenance.cpp
@@ -0,0 +1,16 @@
+#include "maintenance.h"
+
+bool Maintenance::scheduleMaintenance()
+{
+ // set update timer
+ connect(&_maintenanceTimer, &QTimer::timeout, this, &Maintenance::onMaintenanceTimerAction);
+
+ _maintenanceTimer.setInterval( _coreObject->Settings.MaintenanceSettings.timeMaintenanceInterval_s * 1000);
+ _maintenanceTimer.start();
+
+ // complete startup
+ _coreObject->Log.logEvent("info", QStringLiteral("performing maintenance every %1 seconds")
+ .arg(_coreObject->Settings.MaintenanceSettings.timeMaintenanceInterval_s));
+
+ return true;
+}
diff --git a/Maintenance/updatestats.cpp b/Maintenance/updatestats.cpp
new file mode 100644
index 0000000..a19e7d9
--- /dev/null
+++ b/Maintenance/updatestats.cpp
@@ -0,0 +1,59 @@
+#include "maintenance.h"
+
+int Maintenance::updateStats()
+{
+ // result
+ int numOfUpdatedStats = 0;
+
+ // get list of gamenames in database
+ QString selectGamenames = "SELECT DISTINCT gamename FROM serverlist";
+ QSqlQuery gamenameQuery;
+ gamenameQuery.prepare(selectGamenames);
+ if ( ! gamenameQuery.exec() )
+ return reportQuery(gamenameQuery);
+
+ // update stats for every gamename
+ while ( gamenameQuery.next() )
+ {
+ // get next gamename
+ QString gamename = gamenameQuery.value(0).toString();
+
+ // determine beacon and server counts
+ QString selectStats = "SELECT COUNT(CASE WHEN f_direct THEN 1 END) AS num_direct, "
+ "count(*) AS num_total "
+ "FROM serverlist "
+ "WHERE gamename = :gamename AND dt_updated > :timestamp";
+ QSqlQuery statQuery;
+ statQuery.prepare(selectStats);
+ statQuery.bindValue(":gamename", gamename);
+ statQuery.bindValue(":timestamp", QDateTime::currentDateTime()
+ .addSecs(-_coreObject->Settings.ListenServerSettings.serverttl_s).toSecsSinceEpoch());
+ if ( ! statQuery.exec() )
+ return reportQuery(statQuery);
+
+ // get values
+ int num_direct = -1;
+ int num_total = -1;
+ if ( statQuery.next() )
+ {
+ num_direct = statQuery.value("num_direct").toInt();
+ num_total = statQuery.value("num_total").toInt();
+ }
+
+ // write to db
+ QString updateStatQuery = "UPDATE gameinfo "
+ "SET num_direct = :num_direct, num_total = :num_total "
+ "WHERE gamename = :gamename ";
+ statQuery.prepare(updateStatQuery);
+ statQuery.bindValue(":num_direct", num_direct);
+ statQuery.bindValue(":num_total", num_total);
+ statQuery.bindValue(":gamename", gamename);
+ if ( ! statQuery.exec() )
+ return reportQuery(statQuery);
+
+ // update counter
+ numOfUpdatedStats += statQuery.numRowsAffected();
+ }
+
+ return numOfUpdatedStats;
+}