From 2eb3d23ef46661e748d3de3d8332856d186c02ba Mon Sep 17 00:00:00 2001 From: Zwelf Date: Sat, 4 Jul 2020 19:53:27 +0200 Subject: [PATCH] Rewrite score using the new interface delete file_score for now as it will be replaced by a sqlite backend --- CMakeLists.txt | 4 - src/engine/server.h | 2 +- src/engine/server/databases/connection.h | 69 +- .../server/databases/connection_pool.cpp | 167 +- src/engine/server/databases/connection_pool.h | 24 +- src/engine/server/databases/mysql.cpp | 195 +- src/engine/server/databases/mysql.h | 21 +- src/engine/server/server.cpp | 74 +- src/engine/server/server.h | 17 +- src/engine/server/sql_string_helpers.cpp | 17 + src/engine/server/sql_string_helpers.h | 3 + src/engine/shared/config_variables.h | 4 +- src/game/server/gamecontext.cpp | 23 +- src/game/server/gamecontext.h | 8 +- src/game/server/score.cpp | 1497 +++++++++++++ src/game/server/score.h | 206 +- src/game/server/score/file_score.cpp | 372 ---- src/game/server/score/file_score.h | 92 - src/game/server/score/sql_score.cpp | 1886 ----------------- src/game/server/score/sql_score.h | 220 -- 20 files changed, 2181 insertions(+), 2720 deletions(-) delete mode 100644 src/game/server/score/file_score.cpp delete mode 100644 src/game/server/score/file_score.h delete mode 100644 src/game/server/score/sql_score.cpp delete mode 100644 src/game/server/score/sql_score.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ab44769fe..d1cde6fbc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1902,10 +1902,6 @@ set_src(GAME_SERVER GLOB_RECURSE src/game/server save.h score.cpp score.h - score/file_score.cpp - score/file_score.h - score/sql_score.cpp - score/sql_score.h teams.cpp teams.h teehistorian.cpp diff --git a/src/engine/server.h b/src/engine/server.h index fb1192f19..3e3e35b1f 100644 --- a/src/engine/server.h +++ b/src/engine/server.h @@ -252,7 +252,7 @@ public: virtual void OnMapChange(char *pNewMapName, int MapNameSize) = 0; // FullShutdown is true if the program is about to exit (not if the map is changed) - virtual void OnShutdown(bool FullShutdown = false) = 0; + virtual void OnShutdown() = 0; virtual void OnTick() = 0; virtual void OnPreSnap() = 0; diff --git a/src/engine/server/databases/connection.h b/src/engine/server/databases/connection.h index a0aff2453..0d67e5f65 100644 --- a/src/engine/server/databases/connection.h +++ b/src/engine/server/databases/connection.h @@ -12,6 +12,7 @@ public: str_copy(m_aPrefix, pPrefix, sizeof(m_aPrefix)); } virtual ~IDbConnection() {} + IDbConnection& operator=(const IDbConnection&) = delete; // copies the credentials, not the active connection virtual IDbConnection *Copy() = 0; @@ -31,7 +32,7 @@ public: virtual void Disconnect() = 0; // get exclusive read/write access to the database - virtual void Lock() = 0; + virtual void Lock(const char *pTable) = 0; virtual void Unlock() = 0; // ? for Placeholders, connection has to be established, can overwrite previous prepared statements @@ -41,11 +42,12 @@ public: virtual void BindString(int Idx, const char *pString) = 0; virtual void BindInt(int Idx, int Value) = 0; - virtual void Execute() = 0; - - // if true if another result row exists and selects it + // executes the query and returns if a result row exists and selects it + // when called multiple times the next row is selected virtual bool Step() = 0; + virtual bool IsNull(int Col) const = 0; + virtual float GetFloat(int Col) const = 0; virtual int GetInt(int Col) const = 0; // ensures that the string is null terminated virtual void GetString(int Col, char *pBuffer, int BufferSize) const = 0; @@ -54,6 +56,65 @@ public: protected: char m_aPrefix[64]; + + static constexpr const char *m_pCreateRace = + "CREATE TABLE IF NOT EXISTS %s_race (" + "Map VARCHAR(128) BINARY NOT NULL, " + "Name VARCHAR(%d) BINARY NOT NULL, " + "Timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "Time FLOAT DEFAULT 0, " + "Server CHAR(4), " + "cp1 FLOAT DEFAULT 0, cp2 FLOAT DEFAULT 0, cp3 FLOAT DEFAULT 0, " + "cp4 FLOAT DEFAULT 0, cp5 FLOAT DEFAULT 0, cp6 FLOAT DEFAULT 0, " + "cp7 FLOAT DEFAULT 0, cp8 FLOAT DEFAULT 0, cp9 FLOAT DEFAULT 0, " + "cp10 FLOAT DEFAULT 0, cp11 FLOAT DEFAULT 0, cp12 FLOAT DEFAULT 0, " + "cp13 FLOAT DEFAULT 0, cp14 FLOAT DEFAULT 0, cp15 FLOAT DEFAULT 0, " + "cp16 FLOAT DEFAULT 0, cp17 FLOAT DEFAULT 0, cp18 FLOAT DEFAULT 0, " + "cp19 FLOAT DEFAULT 0, cp20 FLOAT DEFAULT 0, cp21 FLOAT DEFAULT 0, " + "cp22 FLOAT DEFAULT 0, cp23 FLOAT DEFAULT 0, cp24 FLOAT DEFAULT 0, " + "cp25 FLOAT DEFAULT 0, " + "GameID VARCHAR(64), " + "DDNet7 BOOL DEFAULT FALSE, " + "KEY (Map, Name)" + ") CHARACTER SET utf8mb4;"; + static constexpr const char *m_pCreateTeamrace = + "CREATE TABLE IF NOT EXISTS %s_teamrace (" + "Map VARCHAR(128) BINARY NOT NULL, " + "Name VARCHAR(%d) BINARY NOT NULL, " + "Timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "Time FLOAT DEFAULT 0, " + "ID VARBINARY(16) NOT NULL, " + "GameID VARCHAR(64), " + "DDNet7 BOOL DEFAULT FALSE, " + "KEY Map (Map)" + ") CHARACTER SET utf8mb4;"; + static constexpr const char *m_pCreateMaps = + "CREATE TABLE IF NOT EXISTS %s_maps (" + "Map VARCHAR(128) BINARY NOT NULL, " + "Server VARCHAR(32) BINARY NOT NULL, " + "Mapper VARCHAR(128) BINARY NOT NULL, " + "Points INT DEFAULT 0, " + "Stars INT DEFAULT 0, " + "Timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + "UNIQUE KEY Map (Map)" + ") CHARACTER SET utf8mb4;"; + static constexpr const char *m_pCreateSaves = + "CREATE TABLE IF NOT EXISTS %s_saves (" + "Savegame TEXT CHARACTER SET utf8mb4 BINARY NOT NULL, " + "Map VARCHAR(128) BINARY NOT NULL, " + "Code VARCHAR(128) BINARY NOT NULL, " + "Timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "Server CHAR(4), " + "DDNet7 BOOL DEFAULT FALSE, " + "SaveID VARCHAR(36) DEFAULT NULL, " + "UNIQUE KEY (Map, Code)" + ") CHARACTER SET utf8mb4;"; + static constexpr const char *m_pCreatePoints = + "CREATE TABLE IF NOT EXISTS %s_points (" + "Name VARCHAR(%d) BINARY NOT NULL, " + "Points INT DEFAULT 0, " + "UNIQUE KEY Name (Name)" + ") CHARACTER SET utf8mb4;"; }; #endif // ENGINE_SERVER_DATABASES_CONNECTION_H diff --git a/src/engine/server/databases/connection_pool.cpp b/src/engine/server/databases/connection_pool.cpp index a787b1b29..5ff648fcc 100644 --- a/src/engine/server/databases/connection_pool.cpp +++ b/src/engine/server/databases/connection_pool.cpp @@ -1,20 +1,65 @@ #include "connection_pool.h" +#if defined(CONF_SQL) +#include +#endif + // helper struct to hold thread data struct CSqlExecData { CSqlExecData( bool (*pFuncPtr) (IDbConnection *, const ISqlData *), - const ISqlData *pSqlRequestData - ); - ~CSqlExecData(); + std::unique_ptr pThreadData, + const char *pName); + CSqlExecData( + bool (*pFuncPtr) (IDbConnection *, const ISqlData *, bool), + std::unique_ptr pThreadData, + const char *pName); + ~CSqlExecData() {} - bool (*m_pFuncPtr) (IDbConnection*, const ISqlData *); - std::unique_ptr m_pSqlRequestData; + enum + { + READ_ACCESS, + WRITE_ACCESS, + } m_Mode; + union + { + bool (*m_pWriteFunc) (IDbConnection*, const ISqlData *, bool); + bool (*m_pReadFunc) (IDbConnection*, const ISqlData *); + } m_Ptr; + + std::unique_ptr m_pThreadData; + const char *m_pName; }; -CDbConnectionPool::CDbConnectionPool() +CSqlExecData::CSqlExecData( + bool (*pFuncPtr) (IDbConnection *, const ISqlData *), + std::unique_ptr pThreadData, + const char *pName) : + m_Mode(READ_ACCESS), + m_pThreadData(std::move(pThreadData)), + m_pName(pName) { + m_Ptr.m_pReadFunc = pFuncPtr; +} + +CSqlExecData::CSqlExecData( + bool (*pFuncPtr) (IDbConnection *, const ISqlData *, bool), + std::unique_ptr pThreadData, + const char *pName) : + m_Mode(WRITE_ACCESS), + m_pThreadData(std::move(pThreadData)), + m_pName(pName) +{ + m_Ptr.m_pWriteFunc = pFuncPtr; +} + +CDbConnectionPool::CDbConnectionPool() : + m_NumElem(), + FirstElem(0), + LastElem(0) +{ + thread_init_and_detach(CDbConnectionPool::SqlWorker, this, "database worker thread"); } CDbConnectionPool::~CDbConnectionPool() @@ -23,20 +68,122 @@ CDbConnectionPool::~CDbConnectionPool() void CDbConnectionPool::RegisterDatabase(std::unique_ptr pDatabase, Mode DatabaseMode) { + if(DatabaseMode < 0 || NUM_MODES <= DatabaseMode) + return; + m_aapDbConnections[DatabaseMode].push_back(std::move(pDatabase)); } void CDbConnectionPool::Execute( bool (*pFuncPtr) (IDbConnection *, const ISqlData *), - std::unique_ptr pSqlRequestData) + std::unique_ptr pThreadData, + const char *pName) { + m_aTasks[FirstElem++].reset(new CSqlExecData(pFuncPtr, std::move(pThreadData), pName)); + FirstElem %= sizeof(m_aTasks) / sizeof(m_aTasks[0]); + m_NumElem.signal(); } void CDbConnectionPool::ExecuteWrite( - bool (*pFuncPtr) (IDbConnection *, const ISqlData *), - std::unique_ptr pSqlRequestData) + bool (*pFuncPtr) (IDbConnection *, const ISqlData *, bool), + std::unique_ptr pThreadData, + const char *pName) +{ + m_aTasks[FirstElem++].reset(new CSqlExecData(pFuncPtr, std::move(pThreadData), pName)); + FirstElem %= sizeof(m_aTasks) / sizeof(m_aTasks[0]); + m_NumElem.signal(); +} + +void CDbConnectionPool::OnShutdown() { } -void CDbConnectionPool::Shutdown() +void CDbConnectionPool::SqlWorker(void *pUser) { + CDbConnectionPool *pThis = (CDbConnectionPool *)pUser; + pThis->SqlWorker(); } + +void CDbConnectionPool::SqlWorker() +{ + while(1) + { + m_NumElem.wait(); + auto pThreadData = std::move(m_aTasks[LastElem++]); + LastElem %= sizeof(m_aTasks) / sizeof(m_aTasks[0]); + bool Success = false; + switch(pThreadData->m_Mode) + { + case CSqlExecData::READ_ACCESS: + { + for(int i = 0; i < (int)m_aapDbConnections[Mode::READ].size(); i++) + { + if(ExecSqlFunc(m_aapDbConnections[Mode::READ][i].get(), pThreadData.get(), false)) + { + Success = true; + break; + } + } + } break; + case CSqlExecData::WRITE_ACCESS: + { + for(int i = 0; i < (int)m_aapDbConnections[Mode::WRITE].size(); i++) + { + if(ExecSqlFunc(m_aapDbConnections[Mode::WRITE][i].get(), pThreadData.get(), false)) + { + Success = true; + break; + } + } + if(!Success) + { + for(int i = 0; i < (int)m_aapDbConnections[Mode::WRITE_BACKUP].size(); i++) + { + if(ExecSqlFunc(m_aapDbConnections[Mode::WRITE_BACKUP][i].get(), pThreadData.get(), true)) + { + Success = true; + break; + } + } + } + } break; + } + if(Success) + dbg_msg("sql", "%s done", pThreadData->m_pName); + } +} + +bool CDbConnectionPool::ExecSqlFunc(IDbConnection *pConnection, CSqlExecData *pData, bool Failure) +{ + if(pConnection->Connect() != IDbConnection::SUCCESS) + return false; + bool Success = false; + try { + switch(pData->m_Mode) + { + case CSqlExecData::READ_ACCESS: + if(pData->m_Ptr.m_pReadFunc(pConnection, pData->m_pThreadData.get())) + Success = true; + break; + case CSqlExecData::WRITE_ACCESS: + if(pData->m_Ptr.m_pWriteFunc(pConnection, pData->m_pThreadData.get(), Failure)) + Success = true; + break; + } + } +#if defined(CONF_SQL) + catch (sql::SQLException &e) + { + dbg_msg("sql", "MySQL Error: %s", e.what()); + } +#endif + catch (...) + { + dbg_msg("sql", "Unexpected exception caught"); + } + pConnection->Unlock(); + pConnection->Disconnect(); + if(!Success) + dbg_msg("sql", "%s failed", pData->m_pName); + return Success; +} + diff --git a/src/engine/server/databases/connection_pool.h b/src/engine/server/databases/connection_pool.h index 8497ccc98..364c6fa4f 100644 --- a/src/engine/server/databases/connection_pool.h +++ b/src/engine/server/databases/connection_pool.h @@ -4,12 +4,13 @@ #include "connection.h" #include +#include #include #include struct ISqlData { - virtual ~ISqlData(); + virtual ~ISqlData() {}; }; class CDbConnectionPool @@ -17,6 +18,7 @@ class CDbConnectionPool public: CDbConnectionPool(); ~CDbConnectionPool(); + CDbConnectionPool& operator=(const CDbConnectionPool&) = delete; enum Mode { @@ -30,17 +32,27 @@ public: void Execute( bool (*pFuncPtr) (IDbConnection *, const ISqlData *), - std::unique_ptr pSqlRequestData); + std::unique_ptr pSqlRequestData, + const char *pName); // writes to WRITE_BACKUP server in case of failure void ExecuteWrite( - bool (*pFuncPtr) (IDbConnection *, const ISqlData *), - std::unique_ptr pSqlRequestData); + bool (*pFuncPtr) (IDbConnection *, const ISqlData *, bool), + std::unique_ptr pSqlRequestData, + const char *pName); - void Shutdown(); + void OnShutdown(); private: std::vector> m_aapDbConnections[NUM_MODES]; - lock m_ConnectionLookupLock[NUM_MODES]; + + static void SqlWorker(void *pUser); + void SqlWorker(); + bool ExecSqlFunc(IDbConnection *pConnection, struct CSqlExecData *pData, bool Failure); + + semaphore m_NumElem; + int FirstElem; + int LastElem; + std::unique_ptr m_aTasks[512]; }; #endif // ENGINE_SERVER_DATABASES_CONNECTION_POOL_H diff --git a/src/engine/server/databases/mysql.cpp b/src/engine/server/databases/mysql.cpp index cebce7127..d4e3c0f43 100644 --- a/src/engine/server/databases/mysql.cpp +++ b/src/engine/server/databases/mysql.cpp @@ -1,6 +1,11 @@ #include "mysql.h" +#if defined(CONF_SQL) #include +#include +#endif + +lock CMysqlConnection::m_SqlDriverLock; CMysqlConnection::CMysqlConnection( const char *pDatabase, @@ -11,7 +16,10 @@ CMysqlConnection::CMysqlConnection( int Port, bool Setup) : IDbConnection(pPrefix), - m_pDriver(nullptr), +#if defined(CONF_SQL) + m_NewQuery(false), + m_Locked(false), +#endif m_Port(Port), m_Setup(Setup), m_InUse(false) @@ -20,6 +28,9 @@ CMysqlConnection::CMysqlConnection( str_copy(m_aUser, pUser, sizeof(m_aUser)); str_copy(m_aPass, pPass, sizeof(m_aPass)); str_copy(m_aIp, pIp, sizeof(m_aIp)); +#if not defined(CONF_SQL) + dbg_msg("sql", "Adding MySQL server failed due to MySQL support not enabled during compile time"); +#endif } CMysqlConnection::~CMysqlConnection() @@ -33,9 +44,119 @@ CMysqlConnection *CMysqlConnection::Copy() IDbConnection::Status CMysqlConnection::Connect() { +#if defined(CONF_SQL) if(m_InUse.exchange(true)) return Status::IN_USE; + m_NewQuery = true; + if(m_pConnection != nullptr) + { + try + { + // Connect to specific database + m_pConnection->setSchema(m_aDatabase); + return Status::SUCCESS; + } + catch (sql::SQLException &e) + { + dbg_msg("sql", "MySQL Error: %s", e.what()); + } + catch (const std::exception& ex) + { + dbg_msg("sql", "MySQL Error: %s", ex.what()); + } + catch (const std::string& ex) + { + dbg_msg("sql", "MySQL Error: %s", ex.c_str()); + } + catch (...) + { + dbg_msg("sql", "Unknown Error cause by the MySQL/C++ Connector"); + } + + m_InUse.store(false); + dbg_msg("sql", "ERROR: SQL connection failed"); + return Status::ERROR; + } + + try + { + m_pConnection.release(); + m_pPreparedStmt.release(); + m_pResults.release(); + + sql::ConnectOptionsMap connection_properties; + connection_properties["hostName"] = sql::SQLString(m_aIp); + connection_properties["port"] = m_Port; + connection_properties["userName"] = sql::SQLString(m_aUser); + connection_properties["password"] = sql::SQLString(m_aPass); + connection_properties["OPT_CONNECT_TIMEOUT"] = 10; + connection_properties["OPT_READ_TIMEOUT"] = 10; + connection_properties["OPT_WRITE_TIMEOUT"] = 20; + connection_properties["OPT_RECONNECT"] = true; + connection_properties["OPT_CHARSET_NAME"] = sql::SQLString("utf8mb4"); + connection_properties["OPT_SET_CHARSET_NAME"] = sql::SQLString("utf8mb4"); + + // Create connection + { + scope_lock GlobalLockScope(&m_SqlDriverLock); + sql::Driver *pDriver = get_driver_instance(); + m_pConnection.reset(pDriver->connect(connection_properties)); + } + + // Create Statement + m_pStmt = std::unique_ptr(m_pConnection->createStatement()); + + // Apparently OPT_CHARSET_NAME and OPT_SET_CHARSET_NAME are not enough + m_pStmt->execute("SET CHARACTER SET utf8mb4;"); + + if(m_Setup) + { + char aBuf[1024]; + // create database + str_format(aBuf, sizeof(aBuf), "CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4", m_aDatabase); + m_pStmt->execute(aBuf); + // Connect to specific database + m_pConnection->setSchema(m_aDatabase); + str_format(aBuf, sizeof(aBuf), m_pCreateRace, GetPrefix(), MAX_NAME_LENGTH); + m_pStmt->execute(aBuf); + str_format(aBuf, sizeof(aBuf), m_pCreateTeamrace, GetPrefix(), MAX_NAME_LENGTH); + m_pStmt->execute(aBuf); + str_format(aBuf, sizeof(aBuf), m_pCreateMaps, GetPrefix()); + m_pStmt->execute(aBuf); + str_format(aBuf, sizeof(aBuf), m_pCreateSaves, GetPrefix()); + m_pStmt->execute(aBuf); + str_format(aBuf, sizeof(aBuf), m_pCreatePoints, GetPrefix(), MAX_NAME_LENGTH); + m_pStmt->execute(aBuf); + m_Setup = false; + } + else + { + // Connect to specific database + m_pConnection->setSchema(m_aDatabase); + } + dbg_msg("sql", "sql connection established"); + return Status::SUCCESS; + } + catch (sql::SQLException &e) + { + dbg_msg("sql", "MySQL Error: %s", e.what()); + } + catch (const std::exception& ex) + { + dbg_msg("sql", "MySQL Error: %s", ex.what()); + } + catch (const std::string& ex) + { + dbg_msg("sql", "MySQL Error: %s", ex.c_str()); + } + catch (...) + { + dbg_msg("sql", "Unknown Error cause by the MySQL/C++ Connector"); + } + +#endif + dbg_msg("sql", "ERROR: sql connection failed"); return Status::ERROR; } @@ -44,45 +165,107 @@ void CMysqlConnection::Disconnect() m_InUse.store(false); } -void CMysqlConnection::Lock() +void CMysqlConnection::Lock(const char *pTable) { +#if defined(CONF_SQL) + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), "lock tables %s write;", pTable); + m_pStmt->execute(aBuf); + m_Locked = true; +#endif } void CMysqlConnection::Unlock() { +#if defined(CONF_SQL) + if(m_Locked) + { + m_pStmt->execute("unlock tables;"); + m_Locked = false; + } +#endif } void CMysqlConnection::PrepareStatement(const char *pStmt) { +#if defined(CONF_SQL) + m_pPreparedStmt.reset(m_pConnection->prepareStatement(pStmt)); + m_NewQuery = true; +#endif } void CMysqlConnection::BindString(int Idx, const char *pString) { +#if defined(CONF_SQL) + m_pPreparedStmt->setString(Idx, pString); + m_NewQuery = true; +#endif } void CMysqlConnection::BindInt(int Idx, int Value) { -} - -void CMysqlConnection::Execute() -{ +#if defined(CONF_SQL) + m_pPreparedStmt->setInt(Idx, Value); + m_NewQuery = true; +#endif } bool CMysqlConnection::Step() { +#if defined(CONF_SQL) + if(m_NewQuery) + { + m_NewQuery = false; + m_pResults.reset(m_pPreparedStmt->executeQuery()); + } + return m_pResults->next(); +#else return false; +#endif +} + +bool CMysqlConnection::IsNull(int Col) const +{ +#if defined(CONF_SQL) + return m_pResults->isNull(Col); +#else + return false; +#endif +} + +float CMysqlConnection::GetFloat(int Col) const +{ +#if defined(CONF_SQL) + return (float)m_pResults->getDouble(Col); +#else + return 0.0; +#endif } int CMysqlConnection::GetInt(int Col) const { +#if defined(CONF_SQL) + return m_pResults->getInt(Col); +#else return 0; +#endif } void CMysqlConnection::GetString(int Col, char *pBuffer, int BufferSize) const { +#if defined(CONF_SQL) + auto String = m_pResults->getString(Col); + str_copy(pBuffer, String.c_str(), BufferSize); +#endif } int CMysqlConnection::GetBlob(int Col, unsigned char *pBuffer, int BufferSize) const { +#if defined(CONF_SQL) + auto Blob = m_pResults->getBlob(Col); + Blob->read((char *)pBuffer, BufferSize); + return Blob->gcount(); +#else return 0; +#endif } diff --git a/src/engine/server/databases/mysql.h b/src/engine/server/databases/mysql.h index f9885569b..5cd5cb2a2 100644 --- a/src/engine/server/databases/mysql.h +++ b/src/engine/server/databases/mysql.h @@ -2,16 +2,16 @@ #define ENGINE_SERVER_DATABASES_MYSQL_H #include "connection.h" +#include + #include #include +#if defined(CONF_SQL) #include #include #include - -namespace sql { -class Driver; -} /* namespace sql */ +#endif class CMysqlConnection : public IDbConnection { @@ -31,7 +31,7 @@ public: virtual Status Connect(); virtual void Disconnect(); - virtual void Lock(); + virtual void Lock(const char *pTable); virtual void Unlock(); virtual void PrepareStatement(const char *pStmt); @@ -39,19 +39,23 @@ public: virtual void BindString(int Idx, const char *pString); virtual void BindInt(int Idx, int Value); - virtual void Execute(); - virtual bool Step(); + virtual bool IsNull(int Col) const; + virtual float GetFloat(int Col) const; virtual int GetInt(int Col) const; virtual void GetString(int Col, char *pBuffer, int BufferSize) const; virtual int GetBlob(int Col, unsigned char *pBuffer, int BufferSize) const; private: +#if defined(CONF_SQL) std::unique_ptr m_pConnection; - sql::Driver *m_pDriver; std::unique_ptr m_pPreparedStmt; + std::unique_ptr m_pStmt; std::unique_ptr m_pResults; + bool m_NewQuery; + bool m_Locked; +#endif // copy of config vars char m_aDatabase[64]; @@ -62,6 +66,7 @@ private: bool m_Setup; std::atomic_bool m_InUse; + static lock m_SqlDriverLock; }; #endif // ENGINE_SERVER_DATABASES_MYSQL_H diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index 7c9d09d14..cd7a5634c 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -44,6 +44,9 @@ #include #endif +#include +#include + CSnapIDPool::CSnapIDPool() { @@ -297,16 +300,7 @@ CServer::CServer(): m_Register(false), m_RegSixup(true) m_ConnLoggingSocketCreated = false; #endif -#if defined (CONF_SQL) - for (int i = 0; i < MAX_SQLSERVERS; i++) - { - m_apSqlReadServers[i] = 0; - m_apSqlWriteServers[i] = 0; - } - - CSqlConnector::SetReadServers(m_apSqlReadServers); - CSqlConnector::SetWriteServers(m_apSqlWriteServers); -#endif + m_pConnectionPool = new CDbConnectionPool(); m_aErrorShutdownReason[0] = 0; @@ -2584,22 +2578,14 @@ int CServer::Run() m_Fifo.Shutdown(); #endif - GameServer()->OnShutdown(true); + GameServer()->OnShutdown(); m_pMap->Unload(); for(int i = 0; i < 2; i++) free(m_apCurrentMapData[i]); -#if defined (CONF_SQL) - for (int i = 0; i < MAX_SQLSERVERS; i++) - { - if (m_apSqlReadServers[i]) - delete m_apSqlReadServers[i]; - - if (m_apSqlWriteServers[i]) - delete m_apSqlWriteServers[i]; - } -#endif + DbPool()->OnShutdown(); + delete m_pConnectionPool; #if defined (CONF_UPNP) m_UPnP.Shutdown(); @@ -3084,8 +3070,6 @@ void CServer::ConShowIps(IConsole::IResult *pResult, void *pUser) } } -#if defined (CONF_SQL) - void CServer::ConAddSqlServer(IConsole::IResult *pResult, void *pUserData) { CServer *pSelf = (CServer *)pUserData; @@ -3109,34 +3093,31 @@ void CServer::ConAddSqlServer(IConsole::IResult *pResult, void *pUserData) bool SetUpDb = pResult->NumArguments() == 8 ? pResult->GetInteger(7) : true; - CSqlServer** apSqlServers = ReadOnly ? pSelf->m_apSqlReadServers : pSelf->m_apSqlWriteServers; + auto pSqlServers = std::unique_ptr(new CMysqlConnection( + pResult->GetString(1), pResult->GetString(2), pResult->GetString(3), + pResult->GetString(4), pResult->GetString(5), pResult->GetInteger(6), + SetUpDb)); - for (int i = 0; i < MAX_SQLSERVERS; i++) + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), + "Added new Sql%sServer: DB: '%s' Prefix: '%s' User: '%s' IP: <{'%s'}> Port: %d", + ReadOnly ? "Read" : "Write", + pResult->GetString(1), pResult->GetString(2), pResult->GetString(3), + pResult->GetString(5), pResult->GetInteger(6)); + pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); + if(SetUpDb) { - if (!apSqlServers[i]) - { - apSqlServers[i] = new CSqlServer(pResult->GetString(1), pResult->GetString(2), pResult->GetString(3), pResult->GetString(4), pResult->GetString(5), pResult->GetInteger(6), &pSelf->m_GlobalSqlLock, ReadOnly, SetUpDb); - - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), - "Added new Sql%sServer: %d: DB: '%s' Prefix: '%s' User: '%s' IP: <{'%s'}> Port: %d", - ReadOnly ? "Read" : "Write", i, apSqlServers[i]->GetDatabase(), - apSqlServers[i]->GetPrefix(), apSqlServers[i]->GetUser(), - apSqlServers[i]->GetIP(), apSqlServers[i]->GetPort()); - pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); - if(SetUpDb) - { - if(!apSqlServers[i]->CreateTables()) - pSelf->SetErrorShutdown("database create tables failed"); - } - return; - } + /* TODO + if(!pSqlServers->CreateTables()) + pSelf->SetErrorShutdown("database create tables failed"); + */ } - pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "failed to add new sqlserver: limit of sqlservers reached"); + pSelf->DbPool()->RegisterDatabase(std::move(pSqlServers), ReadOnly ? CDbConnectionPool::READ : CDbConnectionPool::WRITE); } void CServer::ConDumpSqlServers(IConsole::IResult *pResult, void *pUserData) { + /* TODO CServer *pSelf = (CServer *)pUserData; bool ReadOnly; @@ -3159,10 +3140,9 @@ void CServer::ConDumpSqlServers(IConsole::IResult *pResult, void *pUserData) str_format(aBuf, sizeof(aBuf), "SQL-%s %d: DB: '%s' Prefix: '%s' User: '%s' Pass: '%s' IP: <{'%s'}> Port: %d", ReadOnly ? "Read" : "Write", i, apSqlServers[i]->GetDatabase(), apSqlServers[i]->GetPrefix(), apSqlServers[i]->GetUser(), apSqlServers[i]->GetPass(), apSqlServers[i]->GetIP(), apSqlServers[i]->GetPort()); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); } + */ } -#endif - void CServer::ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) { pfnCallback(pResult, pCallbackUserData); @@ -3363,10 +3343,8 @@ void CServer::RegisterCommands() Console()->Register("reload", "", CFGFLAG_SERVER, ConMapReload, this, "Reload the map"); -#if defined(CONF_SQL) Console()->Register("add_sqlserver", "s['r'|'w'] s[Database] s[Prefix] s[User] s[Password] s[IP] i[Port] ?i[SetUpDatabase ?]", CFGFLAG_SERVER|CFGFLAG_NONTEEHISTORIC, ConAddSqlServer, this, "add a sqlserver"); Console()->Register("dump_sqlservers", "s['r'|'w']", CFGFLAG_SERVER, ConDumpSqlServers, this, "dumps all sqlservers readservers = r, writeservers = w"); -#endif Console()->Register("auth_add", "s[ident] s[level] s[pw]", CFGFLAG_SERVER|CFGFLAG_NONTEEHISTORIC, ConAuthAdd, this, "Add a rcon key"); Console()->Register("auth_add_p", "s[ident] s[level] s[hash] s[salt]", CFGFLAG_SERVER|CFGFLAG_NONTEEHISTORIC, ConAuthAddHashed, this, "Add a prehashed rcon key"); diff --git a/src/engine/server/server.h b/src/engine/server/server.h index 4e8fc87c9..c68bb1f9b 100644 --- a/src/engine/server/server.h +++ b/src/engine/server/server.h @@ -33,11 +33,6 @@ #include "upnp.h" #endif -#if defined (CONF_SQL) - #include "sql_connector.h" - #include "sql_server.h" -#endif - class CSnapIDPool { enum @@ -102,24 +97,20 @@ class CServer : public IServer CUPnP m_UPnP; #endif -#if defined(CONF_SQL) - lock m_GlobalSqlLock; - - CSqlServer *m_apSqlReadServers[MAX_SQLSERVERS]; - CSqlServer *m_apSqlWriteServers[MAX_SQLSERVERS]; -#endif - #if defined(CONF_FAMILY_UNIX) UNIXSOCKETADDR m_ConnLoggingDestAddr; bool m_ConnLoggingSocketCreated; UNIXSOCKET m_ConnLoggingSocket; #endif + class CDbConnectionPool *m_pConnectionPool; + public: class IGameServer *GameServer() { return m_pGameServer; } class IConsole *Console() { return m_pConsole; } class IStorage *Storage() { return m_pStorage; } class IEngineAntibot *Antibot() { return m_pAntibot; } + class CDbConnectionPool *DbPool() { return m_pConnectionPool; } enum { @@ -395,11 +386,9 @@ public: static void ConNameUnban(IConsole::IResult *pResult, void *pUser); static void ConNameBans(IConsole::IResult *pResult, void *pUser); -#if defined (CONF_SQL) // console commands for sqlmasters static void ConAddSqlServer(IConsole::IResult *pResult, void *pUserData); static void ConDumpSqlServers(IConsole::IResult *pResult, void *pUserData); -#endif static void ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainMaxclientsperipUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); diff --git a/src/engine/server/sql_string_helpers.cpp b/src/engine/server/sql_string_helpers.cpp index 44b7d8a1e..961561c45 100644 --- a/src/engine/server/sql_string_helpers.cpp +++ b/src/engine/server/sql_string_helpers.cpp @@ -24,6 +24,23 @@ void sqlstr::FuzzyString(char *pString, int size) delete [] newString; } +int sqlstr::EscapeLike(char *pDst, const char *pSrc, int DstSize) +{ + int Pos = 0; + int DstPos = 0; + while(DstPos + 2 < DstSize) + { + if(pSrc[Pos] == '\0') + break; + if(pSrc[Pos] == '\\' || pSrc[Pos] == '%' || pSrc[Pos] == '_' || pSrc[Pos] == '[') + pDst[DstPos++] = '\\'; + pDst[DstPos++] = pSrc[Pos++]; + + } + pDst[DstPos++] = '\0'; + return DstPos; +} + // anti SQL injection void sqlstr::ClearString(char *pString, int size) { diff --git a/src/engine/server/sql_string_helpers.h b/src/engine/server/sql_string_helpers.h index c20408e4b..edbaa1118 100644 --- a/src/engine/server/sql_string_helpers.h +++ b/src/engine/server/sql_string_helpers.h @@ -11,6 +11,9 @@ void FuzzyString(char *pString, int size); // anti SQL injection void ClearString(char *pString, int size = 32); +// written number of added bytes +int EscapeLike(char *pDst, const char *pSrc, int DstSize); + void AgoTimeToString(int agoTime, char *pAgoString); template diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index c62a1eeec..b4b19f01d 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -217,11 +217,9 @@ MACRO_CONFIG_STR(SvSqlServerName, sv_sql_servername, 5, "UNK", CFGFLAG_SERVER, " MACRO_CONFIG_STR(SvSqlValidServerNames, sv_sql_valid_servernames, 64, "UNK", CFGFLAG_SERVER, "Comma separated list of valid server names for saving a game to ([A-Z][A-Z][A-Z].?") MACRO_CONFIG_INT(SvSaveGames, sv_savegames, 1, 0, 1, CFGFLAG_SERVER, "Enables savegames (/save and /load)") MACRO_CONFIG_INT(SvSaveGamesDelay, sv_savegames_delay, 60, 0, 10000, CFGFLAG_SERVER, "Delay in seconds for loading a savegame") -#if defined(CONF_SQL) MACRO_CONFIG_INT(SvUseSQL, sv_use_sql, 0, 0, 1, CFGFLAG_SERVER, "Enables SQL DB instead of record file") -MACRO_CONFIG_STR(SvSqlFailureFile, sv_sql_failure_file, 64, "failed_sql.sql", CFGFLAG_SERVER, "File to store failed Sql-Inserts (ranks)") MACRO_CONFIG_INT(SvSqlQueriesDelay, sv_sql_queries_delay, 1, 0, 20, CFGFLAG_SERVER, "Delay in seconds between SQL queries of a single player") -#endif +MACRO_CONFIG_STR(SvSqliteFile, sv_sqlite_file, 64, "ddnet.sqlite", CFGFLAG_SERVER, "File to store ranks in case sv_use_sql is turned off") #if defined(CONF_UPNP) MACRO_CONFIG_INT(SvUseUPnP, sv_use_upnp, 0, 0, 1, CFGFLAG_SERVER, "Enables UPnP support.") diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index 09a2c6754..c588a222f 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -24,10 +24,6 @@ #include "gamemodes/DDRace.h" #include "score.h" -#include "score/file_score.h" -#if defined(CONF_SQL) -#include "score/sql_score.h" -#endif enum { @@ -3116,17 +3112,11 @@ void CGameContext::OnInit(/*class IKernel *pKernel*/) } } - // delete old score object - if(m_pScore) - delete m_pScore; + if(!m_pScore) + { + m_pScore = new CScore(this, ((CServer *)Server())->DbPool()); + } - // create score object (add sql later) -#if defined(CONF_SQL) - if(g_Config.m_SvUseSQL) - m_pScore = new CSqlScore(this); - else -#endif - m_pScore = new CFileScore(this); // setup core world //for(int i = 0; i < MAX_CLIENTS; i++) // game.players[i].core.world = &game.world.core; @@ -3376,11 +3366,8 @@ void CGameContext::OnMapChange(char *pNewMapName, int MapNameSize) str_copy(m_aDeleteTempfile, aTemp, sizeof(m_aDeleteTempfile)); } -void CGameContext::OnShutdown(bool FullShutdown) +void CGameContext::OnShutdown() { - if (FullShutdown) - Score()->OnShutdown(); - Antibot()->RoundEnd(); if(m_TeeHistorianActive) diff --git a/src/game/server/gamecontext.h b/src/game/server/gamecontext.h index 3e64ca7d7..ce7266ea2 100644 --- a/src/game/server/gamecontext.h +++ b/src/game/server/gamecontext.h @@ -18,7 +18,6 @@ #include "player.h" #include "teehistorian.h" -#include "score.h" #ifdef _MSC_VER typedef __int32 int32_t; typedef unsigned __int32 uint32_t; @@ -54,6 +53,7 @@ enum NUM_TUNEZONES = 256 }; +class CScore; class IConsole; class IEngine; class IStorage; @@ -227,7 +227,7 @@ public: virtual void OnInit(); virtual void OnConsoleInit(); virtual void OnMapChange(char *pNewMapName, int MapNameSize); - virtual void OnShutdown(bool FullShutdown = false); + virtual void OnShutdown(); virtual void OnTick(); virtual void OnPreSnap(); @@ -278,7 +278,7 @@ public: private: bool m_VoteWillPass; - class IScore *m_pScore; + class CScore *m_pScore; //DDRace Console Commands @@ -411,7 +411,7 @@ private: public: CLayers *Layers() { return &m_Layers; } - class IScore *Score() { return m_pScore; } + class CScore *Score() { return m_pScore; } enum { diff --git a/src/game/server/score.cpp b/src/game/server/score.cpp index 39edec3ad..615c22fdb 100644 --- a/src/game/server/score.cpp +++ b/src/game/server/score.cpp @@ -1,4 +1,21 @@ #include "score.h" +#include "entities/character.h" +#include "gamemodes/DDRace.h" +#include "save.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include CScorePlayerResult::CScorePlayerResult() : m_Done(false) @@ -33,3 +50,1483 @@ void CScorePlayerResult::SetVariant(Variant v) m_Data.m_Info.m_CpTime[i] = 0; } } + +std::shared_ptr CScore::NewSqlPlayerResult(int ClientID) +{ + CPlayer *pCurPlayer = GameServer()->m_apPlayers[ClientID]; + if(pCurPlayer->m_ScoreQueryResult != nullptr) // TODO: send player a message: "too many requests" + return nullptr; + pCurPlayer->m_ScoreQueryResult = std::make_shared(); + return pCurPlayer->m_ScoreQueryResult; +} + +void CScore::ExecPlayerThread( + bool (*pFuncPtr) (IDbConnection *, const ISqlData *), + const char *pThreadName, + int ClientID, + const char *pName, + int Offset) +{ + auto pResult = NewSqlPlayerResult(ClientID); + if(pResult == nullptr) + return; + auto Tmp = std::unique_ptr(new CSqlPlayerRequest(pResult)); + str_copy(Tmp->m_Name, pName, sizeof(Tmp->m_Name)); + str_copy(Tmp->m_Map, g_Config.m_SvMap, sizeof(Tmp->m_Map)); + str_copy(Tmp->m_RequestingPlayer, Server()->ClientName(ClientID), sizeof(Tmp->m_RequestingPlayer)); + Tmp->m_Offset = Offset; + + m_pPool->Execute(pFuncPtr, std::move(Tmp), pThreadName); +} + +bool CScore::RateLimitPlayer(int ClientID) +{ + CPlayer *pPlayer = GameServer()->m_apPlayers[ClientID]; + if(pPlayer == 0) + return true; + if(pPlayer->m_LastSQLQuery + g_Config.m_SvSqlQueriesDelay * Server()->TickSpeed() >= Server()->Tick()) + return true; + pPlayer->m_LastSQLQuery = Server()->Tick(); + return false; +} + +void CScore::GeneratePassphrase(char *pBuf, int BufSize) +{ + for(int i = 0; i < 3; i++) + { + if(i != 0) + str_append(pBuf, " ", BufSize); + // TODO: decide if the slight bias towards lower numbers is ok + int Rand = m_Prng.RandomBits() % m_aWordlist.size(); + str_append(pBuf, m_aWordlist[Rand].c_str(), BufSize); + } +} + +CScore::CScore(CGameContext *pGameServer, CDbConnectionPool *pPool) : + m_pPool(pPool), + m_pGameServer(pGameServer), + m_pServer(pGameServer->Server()) +{ + auto InitResult = std::make_shared(); + auto Tmp = std::unique_ptr(new CSqlInitData(InitResult)); + ((CGameControllerDDRace*)(pGameServer->m_pController))->m_pInitResult = InitResult; + str_copy(Tmp->m_Map, g_Config.m_SvMap, sizeof(Tmp->m_Map)); + + IOHANDLE File = GameServer()->Storage()->OpenFile("wordlist.txt", IOFLAG_READ, IStorage::TYPE_ALL); + if(!File) + { + dbg_msg("sql", "failed to open wordlist"); + Server()->SetErrorShutdown("sql open wordlist error"); + return; + } + + uint64 aSeed[2]; + secure_random_fill(aSeed, sizeof(aSeed)); + m_Prng.Seed(aSeed); + CLineReader LineReader; + LineReader.Init(File); + char *pLine; + while((pLine = LineReader.Get())) + { + char Word[32] = {0}; + sscanf(pLine, "%*s %31s", Word); + Word[31] = 0; + m_aWordlist.push_back(Word); + } + if(m_aWordlist.size() < 1000) + { + dbg_msg("sql", "too few words in wordlist"); + Server()->SetErrorShutdown("sql too few words in wordlist"); + return; + } + m_pPool->Execute(Init, std::move(Tmp), "load best time"); +} + +bool CScore::Init(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlInitData *pData = dynamic_cast(pGameData); + + char aBuf[512]; + // get the best time + str_format(aBuf, sizeof(aBuf), + "SELECT Time FROM %s_race WHERE Map=? ORDER BY `Time` ASC LIMIT 1;", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + + if(pSqlServer->Step()) + pData->m_pResult->m_CurrentRecord = pSqlServer->GetFloat(1); + + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::LoadPlayerData(int ClientID) +{ + ExecPlayerThread(LoadPlayerDataThread, "load player data", ClientID, "", 0); +} + +// update stuff +bool CScore::LoadPlayerDataThread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlPlayerRequest *pData = dynamic_cast(pGameData); + pData->m_pResult->SetVariant(CScorePlayerResult::PLAYER_INFO); + + char aBuf[512]; + // get best race time + str_format(aBuf, sizeof(aBuf), + "SELECT Time, cp1, cp2, cp3, cp4, cp5, cp6, cp7, cp8, cp9, cp10, " + "cp11, cp12, cp13, cp14, cp15, cp16, cp17, cp18, cp19, cp20, " + "cp21, cp22, cp23, cp24, cp25 " + "FROM %s_race " + "WHERE Map = ? AND Name = ? " + "ORDER BY Time ASC " + "LIMIT 1;", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + pSqlServer->BindString(2, pData->m_RequestingPlayer); + + if(pSqlServer->Step()) + { + // get the best time + float Time = pSqlServer->GetFloat(1); + pData->m_pResult->m_Data.m_Info.m_Time = Time; + pData->m_pResult->m_Data.m_Info.m_Score = -Time; + pData->m_pResult->m_Data.m_Info.m_HasFinishScore = true; + + if(g_Config.m_SvCheckpointSave) + { + for(int i = 0; i < NUM_CHECKPOINTS; i++) + { + pData->m_pResult->m_Data.m_Info.m_CpTime[i] = pSqlServer->GetFloat(i+2); + } + } + } + + // birthday check + str_format(aBuf, sizeof(aBuf), + "SELECT YEAR(Current) - YEAR(Stamp) AS YearsAgo " + "FROM (" + "SELECT CURRENT_TIMESTAMP AS Current, MIN(Timestamp) AS Stamp " + "FROM %s_race " + "WHERE Name=?" + ") AS l " + "WHERE DAYOFMONTH(Current) = DAYOFMONTH(Stamp) AND MONTH(Current) = MONTH(Stamp) " + "AND YEAR(Current) > YEAR(Stamp);", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_RequestingPlayer); + + if(pSqlServer->Step()) + { + int YearsAgo = pSqlServer->GetInt(1); + pData->m_pResult->m_Data.m_Info.m_Birthday = YearsAgo; + } + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::MapVote(int ClientID, const char* MapName) +{ + if(RateLimitPlayer(ClientID)) + return; + ExecPlayerThread(MapVoteThread, "map vote", ClientID, MapName, 0); +} + +bool CScore::MapVoteThread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlPlayerRequest *pData = dynamic_cast(pGameData); + auto paMessages = pData->m_pResult->m_Data.m_aaMessages; + + char aFuzzyMap[128]; + str_copy(aFuzzyMap, pData->m_Name, sizeof(aFuzzyMap)); + sqlstr::FuzzyString(aFuzzyMap, sizeof(aFuzzyMap)); + + char aMapPrefix[128]; + str_copy(aMapPrefix, pData->m_Name, sizeof(aMapPrefix)); + str_append(aMapPrefix, "%", sizeof(aMapPrefix)); + + char aBuf[768]; + str_format(aBuf, sizeof(aBuf), + "SELECT Map, Server " + "FROM %s_maps " + "WHERE Map LIKE convert(? using utf8mb4) COLLATE utf8mb4_general_ci " + "ORDER BY " + "CASE WHEN Map = ? THEN 0 ELSE 1 END, " + "CASE WHEN Map LIKE ? THEN 0 ELSE 1 END, " + "LENGTH(Map), Map " + "LIMIT 1;", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, aFuzzyMap); + pSqlServer->BindString(2, pData->m_Name); + pSqlServer->BindString(3, aMapPrefix); + + if(pSqlServer->Step()) + { + pData->m_pResult->SetVariant(CScorePlayerResult::MAP_VOTE); + auto MapVote = &pData->m_pResult->m_Data.m_MapVote; + pSqlServer->GetString(1, MapVote->m_Map, sizeof(MapVote->m_Map)); + pSqlServer->GetString(2, MapVote->m_Server, sizeof(MapVote->m_Server)); + strcpy(MapVote->m_Reason, "/map"); + + for(char *p = MapVote->m_Server; *p; p++) // lower case server + *p = tolower(*p); + } + else + { + pData->m_pResult->SetVariant(CScorePlayerResult::DIRECT); + str_format(paMessages[0], sizeof(paMessages[0]), + "No map like \"%s\" found. " + "Try adding a '%%' at the start if you don't know the first character. " + "Example: /map %%castle for \"Out of Castle\"", + pData->m_Name); + } + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::MapInfo(int ClientID, const char* MapName) +{ + if(RateLimitPlayer(ClientID)) + return; + ExecPlayerThread(MapInfoThread, "map info", ClientID, MapName, 0); +} + +bool CScore::MapInfoThread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlPlayerRequest *pData = dynamic_cast(pGameData); + + char aFuzzyMap[128]; + str_copy(aFuzzyMap, pData->m_Name, sizeof(aFuzzyMap)); + sqlstr::FuzzyString(aFuzzyMap, sizeof(aFuzzyMap)); + + char aMapPrefix[128]; + str_copy(aMapPrefix, pData->m_Name, sizeof(aMapPrefix)); + str_append(aMapPrefix, "%", sizeof(aMapPrefix)); + + char aBuf[1024]; + str_format(aBuf, sizeof(aBuf), + "SELECT l.Map, l.Server, Mapper, Points, Stars, " + "(SELECT COUNT(Name) FROM %s_race WHERE Map = l.Map) AS Finishes, " + "(SELECT COUNT(DISTINCT Name) FROM %s_race WHERE Map = l.Map) AS Finishers, " + "(SELECT ROUND(AVG(Time)) FROM %s_race WHERE Map = l.Map) AS Average, " + "UNIX_TIMESTAMP(l.Timestamp) AS Stamp, " + "UNIX_TIMESTAMP(CURRENT_TIMESTAMP)-UNIX_TIMESTAMP(l.Timestamp) AS Ago, " + "(SELECT MIN(Time) FROM %s_race WHERE Map = l.Map AND Name = ?) AS OwnTime " + "FROM (" + "SELECT * FROM %s_maps " + "WHERE Map LIKE convert(? using utf8mb4) COLLATE utf8mb4_general_ci " + "ORDER BY " + "CASE WHEN Map = ? THEN 0 ELSE 1 END, " + "CASE WHEN Map LIKE ? THEN 0 ELSE 1 END, " + "LENGTH(Map), " + "Map " + "LIMIT 1" + ") as l;", + pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), + pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), + pSqlServer->GetPrefix() + ); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_RequestingPlayer); + pSqlServer->BindString(2, aFuzzyMap); + pSqlServer->BindString(3, pData->m_Name); + pSqlServer->BindString(4, aMapPrefix); + + if(pSqlServer->Step()) + { + char aMap[MAX_MAP_LENGTH]; + pSqlServer->GetString(1, aMap, sizeof(aMap)); + char aServer[32]; + pSqlServer->GetString(2, aServer, sizeof(aServer)); + char aMapper[128]; + pSqlServer->GetString(3, aMapper, sizeof(aMapper)); + int Points = pSqlServer->GetInt(4); + int Stars = pSqlServer->GetInt(5); + int Finishes = pSqlServer->GetInt(6); + int Finishers = pSqlServer->GetInt(7); + int Average = pSqlServer->GetInt(8); + int Stamp = pSqlServer->GetInt(9); + int Ago = pSqlServer->GetInt(10); + float OwnTime = pSqlServer->GetFloat(11); + + char aAgoString[40] = "\0"; + char aReleasedString[60] = "\0"; + if(Stamp != 0) + { + sqlstr::AgoTimeToString(Ago, aAgoString); + str_format(aReleasedString, sizeof(aReleasedString), ", released %s ago", aAgoString); + } + + char aAverageString[60] = "\0"; + if(Average > 0) + { + str_format(aAverageString, sizeof(aAverageString), " in %d:%02d average", Average / 60, Average % 60); + } + + char aStars[20]; + switch(Stars) + { + case 0: strcpy(aStars, "✰✰✰✰✰"); break; + case 1: strcpy(aStars, "★✰✰✰✰"); break; + case 2: strcpy(aStars, "★★✰✰✰"); break; + case 3: strcpy(aStars, "★★★✰✰"); break; + case 4: strcpy(aStars, "★★★★✰"); break; + case 5: strcpy(aStars, "★★★★★"); break; + default: aStars[0] = '\0'; + } + + char aOwnFinishesString[40] = "\0"; + if(OwnTime > 0) + { + str_format(aOwnFinishesString, sizeof(aOwnFinishesString), + ", your time: %02d:%05.2f", (int)(OwnTime/60), OwnTime-((int)OwnTime/60*60) + ); + } + + str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), + "\"%s\" by %s on %s, %s, %d %s%s, %d %s by %d %s%s%s", + aMap, aMapper, aServer, aStars, + Points, Points == 1 ? "point" : "points", + aReleasedString, + Finishes, Finishes == 1 ? "finish" : "finishes", + Finishers, Finishers == 1 ? "tee" : "tees", + aAverageString, aOwnFinishesString + ); + } + else + { + str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), + "No map like \"%s\" found.", pData->m_Name); + } + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::SaveScore(int ClientID, float Time, const char *pTimestamp, float CpTime[NUM_CHECKPOINTS], bool NotEligible) +{ + CConsole* pCon = (CConsole*)GameServer()->Console(); + if(pCon->m_Cheated || NotEligible) + return; + + CPlayer *pCurPlayer = GameServer()->m_apPlayers[ClientID]; + if(pCurPlayer->m_ScoreFinishResult != nullptr) + dbg_msg("sql", "WARNING: previous save score result didn't complete, overwriting it now"); + pCurPlayer->m_ScoreFinishResult = std::make_shared(); + auto Tmp = std::unique_ptr(new CSqlScoreData(pCurPlayer->m_ScoreFinishResult)); + str_copy(Tmp->m_Map, g_Config.m_SvMap, sizeof(Tmp->m_Map)); + FormatUuid(GameServer()->GameUuid(), Tmp->m_GameUuid, sizeof(Tmp->m_GameUuid)); + Tmp->m_ClientID = ClientID; + str_copy(Tmp->m_Name, Server()->ClientName(ClientID), sizeof(Tmp->m_Name)); + Tmp->m_Time = Time; + str_copy(Tmp->m_aTimestamp, pTimestamp, sizeof(Tmp->m_aTimestamp)); + for(int i = 0; i < NUM_CHECKPOINTS; i++) + Tmp->m_aCpCurrent[i] = CpTime[i]; + + m_pPool->ExecuteWrite(SaveScoreThread, std::move(Tmp), "save score"); +} + +bool CScore::SaveScoreThread(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure) +{ + const CSqlScoreData *pData = dynamic_cast(pGameData); + auto paMessages = pData->m_pResult->m_Data.m_aaMessages; + + char aBuf[1024]; + + str_format(aBuf, sizeof(aBuf), + "SELECT COUNT(*) AS NumFinished FROM %s_race WHERE Map=? AND Name=? ORDER BY time ASC LIMIT 1;", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + pSqlServer->BindString(2, pData->m_Name); + + pSqlServer->Step(); + int NumFinished = pSqlServer->GetInt(1); + if(NumFinished == 0) + { + str_format(aBuf, sizeof(aBuf), "SELECT Points FROM %s_maps WHERE Map=?", pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + + if(pSqlServer->Step()) + { + int Points = pSqlServer->GetInt(1); + str_format(paMessages[0], sizeof(paMessages[0]), + "You earned %d point%s for finishing this map!", + Points, Points == 1 ? "" : "s"); + + str_format(aBuf, sizeof(aBuf), + "INSERT INTO %s_points(Name, Points) " + "VALUES (?, ?) " + "ON duplicate key " + "UPDATE Name=VALUES(Name), Points=Points+VALUES(Points);", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Name); + pSqlServer->BindInt(1, Points); + pSqlServer->Step(); + } + } + + // save score + str_format(aBuf, sizeof(aBuf), + "INSERT IGNORE INTO %s_race(" + "Map, Name, Timestamp, Time, Server, " + "cp1, cp2, cp3, cp4, cp5, cp6, cp7, cp8, cp9, cp10, cp11, cp12, cp13, " + "cp14, cp15, cp16, cp17, cp18, cp19, cp20, cp21, cp22, cp23, cp24, cp25, " + "GameID, DDNet7) " + "VALUES (?, ?, ?, %.2f, ?, " + "%.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, " + "%.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, " + "%.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, " + "'%s', false);", + pSqlServer->GetPrefix(), pData->m_Time, + pData->m_aCpCurrent[0], pData->m_aCpCurrent[1], pData->m_aCpCurrent[2], + pData->m_aCpCurrent[3], pData->m_aCpCurrent[4], pData->m_aCpCurrent[5], + pData->m_aCpCurrent[6], pData->m_aCpCurrent[7], pData->m_aCpCurrent[8], + pData->m_aCpCurrent[9], pData->m_aCpCurrent[10], pData->m_aCpCurrent[11], + pData->m_aCpCurrent[12], pData->m_aCpCurrent[13], pData->m_aCpCurrent[14], + pData->m_aCpCurrent[15], pData->m_aCpCurrent[16], pData->m_aCpCurrent[17], + pData->m_aCpCurrent[18], pData->m_aCpCurrent[19], pData->m_aCpCurrent[20], + pData->m_aCpCurrent[21], pData->m_aCpCurrent[22], pData->m_aCpCurrent[23], + pData->m_aCpCurrent[24], pData->m_GameUuid); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + pSqlServer->BindString(2, pData->m_Name); + pSqlServer->BindString(3, pData->m_aTimestamp); + pSqlServer->BindString(4, g_Config.m_SvSqlServerName); + pSqlServer->Step(); + + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::SaveTeamScore(int* aClientIDs, unsigned int Size, float Time, const char *pTimestamp) +{ + CConsole* pCon = (CConsole*)GameServer()->Console(); + if(pCon->m_Cheated) + return; + for(unsigned int i = 0; i < Size; i++) + { + if(GameServer()->m_apPlayers[aClientIDs[i]]->m_NotEligibleForFinish) + return; + } + auto Tmp = std::unique_ptr(new CSqlTeamScoreData()); + for(unsigned int i = 0; i < Size; i++) + str_copy(Tmp->m_aNames[i], Server()->ClientName(aClientIDs[i]), sizeof(Tmp->m_aNames[i])); + Tmp->m_Size = Size; + Tmp->m_Time = Time; + str_copy(Tmp->m_aTimestamp, pTimestamp, sizeof(Tmp->m_aTimestamp)); + FormatUuid(GameServer()->GameUuid(), Tmp->m_GameUuid, sizeof(Tmp->m_GameUuid)); + str_copy(Tmp->m_Map, g_Config.m_SvMap, sizeof(Tmp->m_Map)); + + m_pPool->ExecuteWrite(SaveTeamScoreThread, std::move(Tmp), "save team score"); +} + +bool CScore::SaveTeamScoreThread(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure) +{ + const CSqlTeamScoreData *pData = dynamic_cast(pGameData); + + char aBuf[2300]; + + // get the names sorted in a tab separated string + std::vector aNames; + for(unsigned int i = 0; i < pData->m_Size; i++) + aNames.push_back(pData->m_aNames[i]); + + std::sort(aNames.begin(), aNames.end()); + char aSortedNames[2048] = {0}; + for(unsigned int i = 0; i < pData->m_Size; i++) + { + if(i != 0) + str_append(aSortedNames, "\t", sizeof(aSortedNames)); + str_append(aSortedNames, aNames[i].c_str(), sizeof(aSortedNames)); + } + str_format(aBuf, sizeof(aBuf), + "SELECT l.ID, Time " + "FROM ((" // preselect teams with first name in team + "SELECT ID " + "FROM %s_teamrace " + "WHERE Map = ? AND Name = ? AND DDNet7 = false" + ") as l" + ") INNER JOIN %s_teamrace AS r ON l.ID = r.ID " + "GROUP BY ID " + "HAVING GROUP_CONCAT(Name ORDER BY Name SEPARATOR '\t') = ?", + pSqlServer->GetPrefix(), pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + pSqlServer->BindString(2, pData->m_aNames[0]); + pSqlServer->BindString(3, aSortedNames); + + if (pSqlServer->Step()) + { + char aGameID[UUID_MAXSTRSIZE]; + pSqlServer->GetString(1, aGameID, sizeof(aGameID)); + float Time = pSqlServer->GetFloat(2); + dbg_msg("sql", "found team rank from same team (old time: %f, new time: %f)", Time, pData->m_Time); + if(pData->m_Time < Time) + { + str_format(aBuf, sizeof(aBuf), + "UPDATE %s_teamrace SET Time=%.2f, Timestamp=?, DDNet7=false, GameID=? WHERE ID = ?;", + pSqlServer->GetPrefix(), pData->m_Time); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_aTimestamp); + pSqlServer->BindString(2, pData->m_GameUuid); + pSqlServer->BindString(3, aGameID); + pSqlServer->Step(); + } + } + else + { + CUuid GameID = RandomUuid(); + char aGameID[UUID_MAXSTRSIZE]; + FormatUuid(GameID, aGameID, sizeof(aGameID)); + + for(unsigned int i = 0; i < pData->m_Size; i++) + { + // if no entry found... create a new one + str_format(aBuf, sizeof(aBuf), + "INSERT IGNORE INTO %s_teamrace(Map, Name, Timestamp, Time, ID, GameID, DDNet7) " + "VALUES (?, ?, ?, %.2f, ?, ?, false);", + pSqlServer->GetPrefix(), pData->m_Time); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + pSqlServer->BindString(2, pData->m_aNames[i]); + pSqlServer->BindString(3, pData->m_aTimestamp); + pSqlServer->BindString(4, aGameID); + pSqlServer->BindString(5, pData->m_GameUuid); + pSqlServer->Step(); + } + } + + return true; +} + +void CScore::ShowRank(int ClientID, const char* pName) +{ + if(RateLimitPlayer(ClientID)) + return; + ExecPlayerThread(ShowRankThread, "show rank", ClientID, pName, 0); +} + +bool CScore::ShowRankThread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlPlayerRequest *pData = dynamic_cast(pGameData); + + // check sort method + char aBuf[600]; + + str_format(aBuf, sizeof(aBuf), + "SELECT Rank, Name, Time " + "FROM (" + "SELECT RANK() OVER w AS Rank, Name, MIN(Time) AS Time " + "FROM %s_race " + "WHERE Map = ? " + "GROUP BY Name " + "WINDOW w AS (ORDER BY Time)" + ") as a " + "WHERE Name = ?;", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + pSqlServer->BindString(2, pData->m_Name); + + if(pSqlServer->Step()) + { + int Rank = pSqlServer->GetInt(1); + float Time = pSqlServer->GetFloat(3); + if(g_Config.m_SvHideScore) + { + str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), + "Your time: %02d:%05.2f", (int)(Time/60), Time-((int)Time/60*60)); + } + else + { + char aName[MAX_NAME_LENGTH]; + pSqlServer->GetString(2, aName, sizeof(aName)); + pData->m_pResult->m_MessageKind = CScorePlayerResult::ALL; + str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), + "%d. %s Time: %02d:%05.2f, requested by %s", + Rank, aName, (int)(Time/60), Time-((int)Time/60*60), pData->m_RequestingPlayer); + } + } + else + { + str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), + "%s is not ranked", pData->m_Name); + } + + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::ShowTeamRank(int ClientID, const char* pName) +{ + if(RateLimitPlayer(ClientID)) + return; + ExecPlayerThread(ShowTeamRankThread, "show team rank", ClientID, pName, 0); +} + +bool CScore::ShowTeamRankThread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlPlayerRequest *pData = dynamic_cast(pGameData); + + // check sort method + char aBuf[2400]; + + str_format(aBuf, sizeof(aBuf), + "SELECT GROUP_CONCAT(Name ORDER BY Name SEPARATOR '\t') AS Names, COUNT(Name) AS NumNames, Time, Rank " + "FROM (" // teamrank score board + "SELECT RANK() OVER w AS Rank, ID " + "FROM %s_teamrace " + "WHERE Map = ? " + "GROUP BY ID " + "WINDOW w AS (ORDER BY Time)" + ") AS TeamRank INNER JOIN (" // select rank with Name in team + "SELECT ID " + "FROM %s_teamrace " + "WHERE Map = ? AND Name = ? " + "ORDER BY Time " + "LIMIT 1" + ") AS l ON TeamRank.ID = l.ID " + "INNER JOIN %s_teamrace AS r ON l.ID = r.ID " + "GROUP BY l.ID", + pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + pSqlServer->BindString(2, pData->m_Map); + pSqlServer->BindString(3, pData->m_Name); + + if(pSqlServer->Step()) + { + char aNames[512]; + pSqlServer->GetString(1, aNames, sizeof(aNames)); + int NumNames = pSqlServer->GetInt(2); + float Time = pSqlServer->GetFloat(3); + int Rank = pSqlServer->GetInt(4); + + char aFormattedNames[512] = ""; + int StrPos = 0; + for(int Name = 0; Name < NumNames; Name++) + { + int NameStart = StrPos; + while(aNames[StrPos] != '\t' && aNames[StrPos] != '\0') + StrPos++; + aNames[StrPos] = '\0'; + + str_append(aFormattedNames, &aNames[NameStart], sizeof(aFormattedNames)); + + if (Name < NumNames - 2) + str_append(aFormattedNames, ", ", sizeof(aFormattedNames)); + else if (Name < NumNames - 1) + str_append(aFormattedNames, " & ", sizeof(aFormattedNames)); + StrPos++; + } + + if(g_Config.m_SvHideScore) + { + str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), + "Your team time: %02d:%05.02f", (int)(Time/60), Time-((int)Time/60*60)); + } + else + { + pData->m_pResult->m_MessageKind = CScorePlayerResult::ALL; + str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), + "%d. %s Team time: %02d:%05.02f, requested by %s", + Rank, aFormattedNames, (int)(Time/60), Time-((int)Time/60*60), pData->m_RequestingPlayer); + } + } + else + { + str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), + "%s has no team ranks", pData->m_Name); + } + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::ShowTop5(int ClientID, int Offset) +{ + if(RateLimitPlayer(ClientID)) + return; + ExecPlayerThread(ShowTop5Thread, "show top5", ClientID, "", Offset); +} + +bool CScore::ShowTop5Thread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlPlayerRequest *pData = dynamic_cast(pGameData); + + int LimitStart = maximum(abs(pData->m_Offset)-1, 0); + const char *pOrder = pData->m_Offset >= 0 ? "ASC" : "DESC"; + + // check sort method + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), + "SELECT Name, Time, Rank " + "FROM (" + "SELECT RANK() OVER w AS Rank, Name, MIN(Time) AS Time " + "FROM %s_race " + "WHERE Map = ? " + "GROUP BY Name " + "WINDOW w AS (ORDER BY Time)" + ") as a " + "ORDER BY Rank %s " + "LIMIT %d, 5;", + pSqlServer->GetPrefix(), + pOrder, + LimitStart); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + + // show top5 + strcpy(pData->m_pResult->m_Data.m_aaMessages[0], "----------- Top 5 -----------"); + + int Line = 1; + while(pSqlServer->Step()) + { + char aName[MAX_NAME_LENGTH]; + pSqlServer->GetString(1, aName, sizeof(aName)); + float Time = pSqlServer->GetFloat(2); + int Rank = pSqlServer->GetInt(3); + str_format(pData->m_pResult->m_Data.m_aaMessages[Line], sizeof(pData->m_pResult->m_Data.m_aaMessages[Line]), + "%d. %s Time: %02d:%05.2f", + Rank, aName, (int)(Time/60), Time-((int)Time/60*60) + ); + Line++; + } + strcpy(pData->m_pResult->m_Data.m_aaMessages[Line], "-------------------------------"); + + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::ShowTeamTop5(int ClientID, int Offset) +{ + if(RateLimitPlayer(ClientID)) + return; + ExecPlayerThread(ShowTeamTop5Thread, "show team top5", ClientID, "", Offset); +} + +bool CScore::ShowTeamTop5Thread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlPlayerRequest *pData = dynamic_cast(pGameData); + auto paMessages = pData->m_pResult->m_Data.m_aaMessages; + + int LimitStart = maximum(abs(pData->m_Offset)-1, 0); + const char *pOrder = pData->m_Offset >= 0 ? "ASC" : "DESC"; + + // check sort method + char aBuf[512]; + + str_format(aBuf, sizeof(aBuf), + "SELECT Name, Time, Rank, TeamSize " + "FROM (" // limit to 5 + "SELECT TeamSize, Rank, ID " + "FROM (" // teamrank score board + "SELECT RANK() OVER w AS Rank, ID, COUNT(*) AS Teamsize " + "FROM %s_teamrace " + "WHERE Map = ? " + "GROUP BY Id " + "WINDOW w AS (ORDER BY Time)" + ") as l1 " + "ORDER BY Rank %s " + "LIMIT %d, 5" + ") as l2 " + "INNER JOIN %s_teamrace as r ON l2.ID = r.ID " + "ORDER BY Rank %s, r.ID, Name ASC;", + pSqlServer->GetPrefix(), pOrder, LimitStart, pSqlServer->GetPrefix(), pOrder); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + + // show teamtop5 + int Line = 0; + strcpy(paMessages[Line++], "------- Team Top 5 -------"); + + if(pSqlServer->Step()) + { + for(Line = 1; Line < 6; Line++) // print + { + bool Break = false; + float Time = pSqlServer->GetFloat(2); + int Rank = pSqlServer->GetInt(3); + int TeamSize = pSqlServer->GetInt(4); + + char aNames[2300] = { 0 }; + for(int i = 0; i < TeamSize; i++) + { + char aName[MAX_NAME_LENGTH]; + pSqlServer->GetString(1, aName, sizeof(aName)); + str_append(aNames, aName, sizeof(aNames)); + if (i < TeamSize - 2) + str_append(aNames, ", ", sizeof(aNames)); + else if (i == TeamSize - 2) + str_append(aNames, " & ", sizeof(aNames)); + if(!pSqlServer->Step()) + { + Break = true; + break; + } + } + str_format(paMessages[Line], sizeof(paMessages[Line]), "%d. %s Team Time: %02d:%05.2f", + Rank, aNames, (int)(Time/60), Time-((int)Time/60*60)); + if(Break) + break; + } + } + + strcpy(paMessages[Line], "-------------------------------"); + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::ShowTimes(int ClientID, int Offset) +{ + if(RateLimitPlayer(ClientID)) + return; + ExecPlayerThread(ShowTimesThread, "show times", ClientID, "", Offset); +} + +void CScore::ShowTimes(int ClientID, const char* pName, int Offset) +{ + if(RateLimitPlayer(ClientID)) + return; + ExecPlayerThread(ShowTimesThread, "show times", ClientID, pName, Offset); +} + +bool CScore::ShowTimesThread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlPlayerRequest *pData = dynamic_cast(pGameData); + auto paMessages = pData->m_pResult->m_Data.m_aaMessages; + + int LimitStart = maximum(abs(pData->m_Offset)-1, 0); + const char *pOrder = pData->m_Offset >= 0 ? "DESC" : "ASC"; + + char aBuf[512]; + + if(pData->m_Name[0] != '\0') // last 5 times of a player + { + str_format(aBuf, sizeof(aBuf), + "SELECT Time, UNIX_TIMESTAMP(CURRENT_TIMESTAMP)-UNIX_TIMESTAMP(Timestamp) as Ago, UNIX_TIMESTAMP(Timestamp) as Stamp " + "FROM %s_race " + "WHERE Map = ? AND Name = ? " + "ORDER BY Timestamp %s " + "LIMIT ?, 5;", + pSqlServer->GetPrefix(), pOrder); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + pSqlServer->BindString(2, pData->m_Name); + pSqlServer->BindInt(3, LimitStart); + } + else // last 5 times of server + { + str_format(aBuf, sizeof(aBuf), + "SELECT Time, " + "UNIX_TIMESTAMP(CURRENT_TIMESTAMP)-UNIX_TIMESTAMP(Timestamp) as Ago, " + "UNIX_TIMESTAMP(Timestamp) as Stamp, " + "Name " + "FROM %s_race " + "WHERE Map = ? " + "ORDER BY Timestamp %s " + "LIMIT ?, 5;", + pSqlServer->GetPrefix(), pOrder); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + pSqlServer->BindInt(2, LimitStart); + } + + // show top5 + if(!pSqlServer->Step()) + { + strcpy(paMessages[0], "There are no times in the specified range"); + pData->m_pResult->m_Done = true; + return true; + } + + strcpy(paMessages[0], "------------- Last Times -------------"); + int Line = 1; + + do + { + float Time = pSqlServer->GetFloat(1); + int Ago = pSqlServer->GetInt(2); + int Stamp = pSqlServer->GetInt(3); + + char aAgoString[40] = "\0"; + sqlstr::AgoTimeToString(Ago, aAgoString); + + if(pData->m_Name[0] != '\0') // last 5 times of a player + { + if(Stamp == 0) // stamp is 00:00:00 cause it's an old entry from old times where there where no stamps yet + str_format(paMessages[Line], sizeof(paMessages[Line]), + "%02d:%05.02f, don't know how long ago", + (int)(Time/60), Time-((int)Time/60*60)); + else + str_format(paMessages[Line], sizeof(paMessages[Line]), + "%s ago, %02d:%05.02f", + aAgoString, (int)(Time/60), Time-((int)Time/60*60)); + } + else // last 5 times of the server + { + char aName[MAX_NAME_LENGTH]; + pSqlServer->GetString(4, aName, sizeof(aName)); + if(Stamp == 0) // stamp is 00:00:00 cause it's an old entry from old times where there where no stamps yet + { + str_format(paMessages[Line], sizeof(paMessages[Line]), + "%s, %02d:%05.02f, don't know when", + aName, (int)(Time/60), Time-((int)Time/60*60)); + } + else + { + str_format(paMessages[Line], sizeof(paMessages[Line]), + "%s, %s ago, %02d:%05.02f", + aName, aAgoString, (int)(Time/60), Time-((int)Time/60*60)); + } + } + Line++; + } while(pSqlServer->Step()); + strcpy(paMessages[Line], "----------------------------------------------------"); + + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::ShowPoints(int ClientID, const char* pName) +{ + if(RateLimitPlayer(ClientID)) + return; + ExecPlayerThread(ShowPointsThread, "show points", ClientID, pName, 0); +} + +bool CScore::ShowPointsThread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlPlayerRequest *pData = dynamic_cast(pGameData); + auto paMessages = pData->m_pResult->m_Data.m_aaMessages; + + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), + "SELECT Rank, Points, Name " + "FROM (" + "SELECT RANK() OVER w AS Rank, Points, Name " + "FROM %s_points " + "WINDOW w as (ORDER BY Points DESC)" + ") as a " + "WHERE Name = ?;", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Name); + + if(pSqlServer->Step()) + { + int Rank = pSqlServer->GetInt(1); + int Count = pSqlServer->GetInt(2); + char aName[MAX_NAME_LENGTH]; + pSqlServer->GetString(3, aName, sizeof(aName)); + pData->m_pResult->m_MessageKind = CScorePlayerResult::ALL; + str_format(paMessages[0], sizeof(paMessages[0]), + "%d. %s Points: %d, requested by %s", + Rank, aName, Count, pData->m_RequestingPlayer); + } + else + { + str_format(paMessages[0], sizeof(paMessages[0]), + "%s has not collected any points so far", pData->m_Name); + } + + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::ShowTopPoints(int ClientID, int Offset) +{ + if(RateLimitPlayer(ClientID)) + return; + ExecPlayerThread(ShowTopPointsThread, "show top points", ClientID, "", Offset); +} + +bool CScore::ShowTopPointsThread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlPlayerRequest *pData = dynamic_cast(pGameData); + auto paMessages = pData->m_pResult->m_Data.m_aaMessages; + + int LimitStart = maximum(abs(pData->m_Offset)-1, 0); + const char *pOrder = pData->m_Offset >= 0 ? "ASC" : "DESC"; + + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), + "SELECT Rank, Points, Name " + "FROM (" + "SELECT RANK() OVER w AS Rank, Points, Name " + "FROM %s_points " + "WINDOW w as (ORDER BY Points DESC)" + ") as a " + "ORDER BY Rank %s " + "LIMIT ?, 5;", + pSqlServer->GetPrefix(), pOrder); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindInt(1, LimitStart); + + // show top points + strcpy(paMessages[0], "-------- Top Points --------"); + + int Line = 1; + while(pSqlServer->Step()) + { + int Rank = pSqlServer->GetInt(1); + int Points = pSqlServer->GetInt(2); + char aName[MAX_NAME_LENGTH]; + pSqlServer->GetString(3, aName, sizeof(aName)); + str_format(paMessages[Line], sizeof(paMessages[Line]), + "%d. %s Points: %d", Rank, aName, Points); + Line++; + } + strcpy(paMessages[Line], "-------------------------------"); + + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::RandomMap(int ClientID, int Stars) +{ + auto pResult = std::make_shared(ClientID); + GameServer()->m_SqlRandomMapResult = pResult; + + auto Tmp = std::unique_ptr(new CSqlRandomMapRequest(pResult)); + Tmp->m_Stars = Stars; + str_copy(Tmp->m_CurrentMap, g_Config.m_SvMap, sizeof(Tmp->m_CurrentMap)); + str_copy(Tmp->m_ServerType, g_Config.m_SvServerType, sizeof(Tmp->m_ServerType)); + str_copy(Tmp->m_RequestingPlayer, GameServer()->Server()->ClientName(ClientID), sizeof(Tmp->m_RequestingPlayer)); + + m_pPool->Execute(RandomMapThread, std::move(Tmp), "random map"); +} + +bool CScore::RandomMapThread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlRandomMapRequest *pData = dynamic_cast(pGameData); + + char aBuf[512]; + if(0 <= pData->m_Stars && pData->m_Stars <= 5) + { + str_format(aBuf, sizeof(aBuf), + "SELECT Map FROM %s_maps " + "WHERE Server = ? AND Map != ? AND Stars = ? " + "ORDER BY RAND() LIMIT 1;", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindInt(3, pData->m_Stars); + } + else + { + str_format(aBuf, sizeof(aBuf), + "SELECT Map FROM %s_maps " + "WHERE Server = ? AND Map != ? " + "ORDER BY RAND() LIMIT 1;", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + } + pSqlServer->BindString(1, pData->m_ServerType); + pSqlServer->BindString(2, pData->m_CurrentMap); + + if(pSqlServer->Step()) + { + pSqlServer->GetString(1, pData->m_pResult->m_Map, sizeof(pData->m_pResult->m_Map)); + } + else + { + str_copy(pData->m_pResult->m_aMessage, "No maps found on this server!", sizeof(pData->m_pResult->m_aMessage)); + } + + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::RandomUnfinishedMap(int ClientID, int Stars) +{ + auto pResult = std::make_shared(ClientID); + GameServer()->m_SqlRandomMapResult = pResult; + + auto Tmp = std::unique_ptr(new CSqlRandomMapRequest(pResult)); + Tmp->m_Stars = Stars; + str_copy(Tmp->m_CurrentMap, g_Config.m_SvMap, sizeof(Tmp->m_CurrentMap)); + str_copy(Tmp->m_ServerType, g_Config.m_SvServerType, sizeof(Tmp->m_ServerType)); + str_copy(Tmp->m_RequestingPlayer, GameServer()->Server()->ClientName(ClientID), sizeof(Tmp->m_RequestingPlayer)); + + m_pPool->Execute(RandomUnfinishedMapThread, std::move(Tmp), "random unfinished map"); +} + +bool CScore::RandomUnfinishedMapThread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlRandomMapRequest *pData = dynamic_cast(pGameData); + + char aBuf[512]; + if(pData->m_Stars >= 0) + { + str_format(aBuf, sizeof(aBuf), + "SELECT Map " + "FROM %s_maps " + "WHERE Server = ? AND Map != ? AND Stars = ? AND Map NOT IN (" + "SELECT Map " + "FROM %s_race " + "WHERE Name = ?" + ") ORDER BY RAND() " + "LIMIT 1;", + pSqlServer->GetPrefix(), pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_ServerType); + pSqlServer->BindString(2, pData->m_CurrentMap); + pSqlServer->BindInt(3, pData->m_Stars); + pSqlServer->BindString(4, pData->m_RequestingPlayer); + } + else + { + str_format(aBuf, sizeof(aBuf), + "SELECT Map " + "FROM %s_maps AS maps " + "WHERE Server = ? AND Map != ? AND Map NOT IN (" + "SELECT Map " + "FROM %s_race as race " + "WHERE Name = ?" + ") ORDER BY RAND() " + "LIMIT 1;", + pSqlServer->GetPrefix(), pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_ServerType); + pSqlServer->BindString(2, pData->m_CurrentMap); + pSqlServer->BindString(3, pData->m_RequestingPlayer); + } + + if(pSqlServer->Step()) + { + pSqlServer->GetString(1, pData->m_pResult->m_Map, sizeof(pData->m_pResult->m_Map)); + } + else + { + str_copy(pData->m_pResult->m_aMessage, "You have no more unfinished maps on this server!", sizeof(pData->m_pResult->m_aMessage)); + } + + pData->m_pResult->m_Done = true; + return true; +} + +void CScore::SaveTeam(int ClientID, const char* Code, const char* Server) +{ + if(RateLimitPlayer(ClientID)) + return; + auto pController = ((CGameControllerDDRace*)(GameServer()->m_pController)); + int Team = pController->m_Teams.m_Core.Team(ClientID); + if(pController->m_Teams.GetSaving(Team)) + return; + + auto SaveResult = std::make_shared(ClientID, pController); + int Result = SaveResult->m_SavedTeam.save(Team); + if(CSaveTeam::HandleSaveError(Result, ClientID, GameServer())) + return; + pController->m_Teams.SetSaving(Team, SaveResult); + + auto Tmp = std::unique_ptr(new CSqlTeamSave(SaveResult)); + str_copy(Tmp->m_Code, Code, sizeof(Tmp->m_Code)); + str_copy(Tmp->m_Map, g_Config.m_SvMap, sizeof(Tmp->m_Map)); + Tmp->m_pResult->m_SaveID = RandomUuid(); + str_copy(Tmp->m_Server, Server, sizeof(Tmp->m_Server)); + str_copy(Tmp->m_ClientName, this->Server()->ClientName(ClientID), sizeof(Tmp->m_ClientName)); + Tmp->m_aGeneratedCode[0] = '\0'; + GeneratePassphrase(Tmp->m_aGeneratedCode, sizeof(Tmp->m_aGeneratedCode)); + + pController->m_Teams.KillSavedTeam(ClientID, Team); + m_pPool->ExecuteWrite(SaveTeamThread, std::move(Tmp), "save team"); +} + +bool CScore::SaveTeamThread(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure) +{ + const CSqlTeamSave *pData = dynamic_cast(pGameData); + + char aSaveID[UUID_MAXSTRSIZE]; + FormatUuid(pData->m_pResult->m_SaveID, aSaveID, UUID_MAXSTRSIZE); + + char *pSaveState = pData->m_pResult->m_SavedTeam.GetString(); + char aBuf[65536]; + + char aTable[512]; + str_format(aTable, sizeof(aTable), "%s_saves", pSqlServer->GetPrefix()); + pSqlServer->Lock(aTable); + + char Code[128] = {0}; + str_format(aBuf, sizeof(aBuf), "SELECT Savegame FROM %s_saves WHERE Code = ? AND Map = ?", pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + bool UseCode = false; + if(pData->m_Code[0] != '\0' && !Failure) + { + pSqlServer->BindString(1, pData->m_Code); + pSqlServer->BindString(2, pData->m_Map); + // only allow saving when save code does not already exist + if(!pSqlServer->Step()) + { + UseCode = true; + str_copy(Code, pData->m_Code, sizeof(Code)); + } + } + if(!UseCode) + { + // use random generated passphrase if save code exists or no save code given + pSqlServer->BindString(1, pData->m_aGeneratedCode); + pSqlServer->BindString(2, pData->m_Map); + if(!pSqlServer->Step()) + { + UseCode = true; + str_copy(Code, pData->m_aGeneratedCode, sizeof(Code)); + } + } + + if(UseCode) + { + str_format(aBuf, sizeof(aBuf), + "INSERT IGNORE INTO %s_saves(Savegame, Map, Code, Timestamp, Server, SaveID, DDNet7) " + "VALUES (?, ?, ?, CURRENT_TIMESTAMP(), ?, ?, false)", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pSaveState); + pSqlServer->BindString(2, pData->m_Map); + pSqlServer->BindString(3, Code); + pSqlServer->BindString(4, pData->m_Server); + pSqlServer->BindString(5, aSaveID); + pSqlServer->Step(); + + if(!Failure) + { + if(str_comp(pData->m_Server, g_Config.m_SvSqlServerName) == 0) + { + str_format(pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage), + "Team successfully saved by %s. Use '/load %s' to continue", + pData->m_ClientName, Code); + } + else + { + str_format(pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage), + "Team successfully saved by %s. Use '/load %s' on %s to continue", + pData->m_ClientName, Code, pData->m_Server); + } + } + else + { + strcpy(pData->m_pResult->m_aBroadcast, + "Database connection failed, teamsave written to a file instead. Admins will add it manually in a few days."); + if(str_comp(pData->m_Server, g_Config.m_SvSqlServerName) == 0) + { + str_format(pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage), + "Team successfully saved by %s. The database connection failed, using generated save code instead to avoid collisions. Use '/load %s' to continue", + pData->m_ClientName, Code); + } + else + { + str_format(pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage), + "Team successfully saved by %s. The database connection failed, using generated save code instead to avoid collisions. Use '/load %s' on %s to continue", + pData->m_ClientName, Code, pData->m_Server); + } + } + + pData->m_pResult->m_Status = CScoreSaveResult::SAVE_SUCCESS; + } + else + { + dbg_msg("sql", "ERROR: This save-code already exists"); + pData->m_pResult->m_Status = CScoreSaveResult::SAVE_FAILED; + strcpy(pData->m_pResult->m_aMessage, "This save-code already exists"); + } + + pSqlServer->Unlock(); + return true; +} + +void CScore::LoadTeam(const char* Code, int ClientID) +{ + if(RateLimitPlayer(ClientID)) + return; + auto pController = ((CGameControllerDDRace*)(GameServer()->m_pController)); + int Team = pController->m_Teams.m_Core.Team(ClientID); + if(pController->m_Teams.GetSaving(Team)) + return; + if(Team < TEAM_FLOCK || Team >= MAX_CLIENTS || (g_Config.m_SvTeam != 3 && Team == TEAM_FLOCK)) + { + GameServer()->SendChatTarget(ClientID, "You have to be in a team (from 1-63)"); + return; + } + if(pController->m_Teams.GetTeamState(Team) != CGameTeams::TEAMSTATE_OPEN) + { + GameServer()->SendChatTarget(ClientID, "Team can't be loaded while racing"); + return; + } + auto SaveResult = std::make_shared(ClientID, pController); + SaveResult->m_Status = CScoreSaveResult::LOAD_FAILED; + pController->m_Teams.SetSaving(Team, SaveResult); + auto Tmp = std::unique_ptr(new CSqlTeamLoad(SaveResult)); + str_copy(Tmp->m_Code, Code, sizeof(Tmp->m_Code)); + str_copy(Tmp->m_Map, g_Config.m_SvMap, sizeof(Tmp->m_Map)); + Tmp->m_ClientID = ClientID; + str_copy(Tmp->m_RequestingPlayer, Server()->ClientName(ClientID), sizeof(Tmp->m_RequestingPlayer)); + Tmp->m_NumPlayer = 0; + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(pController->m_Teams.m_Core.Team(i) == Team) + { + // put all names at the beginning of the array + str_copy(Tmp->m_aClientNames[Tmp->m_NumPlayer], Server()->ClientName(i), sizeof(Tmp->m_aClientNames[Tmp->m_NumPlayer])); + Tmp->m_aClientID[Tmp->m_NumPlayer] = i; + Tmp->m_NumPlayer++; + } + } + m_pPool->Execute(LoadTeamThread, std::move(Tmp), "load team"); +} + +bool CScore::LoadTeamThread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlTeamLoad *pData = dynamic_cast(pGameData); + pData->m_pResult->m_Status = CScoreSaveResult::LOAD_FAILED; + + char aTable[512]; + str_format(aTable, sizeof(aTable), "%s_saves", pSqlServer->GetPrefix()); + pSqlServer->Lock(aTable); + + { + char aSaveLike[128] = ""; + str_append(aSaveLike, "%\n", sizeof(aSaveLike)); + sqlstr::EscapeLike(aSaveLike + str_length(aSaveLike), + pData->m_RequestingPlayer, + sizeof(aSaveLike) - str_length(aSaveLike)); + str_append(aSaveLike, "\t%", sizeof(aSaveLike)); + + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), + "SELECT " + "Savegame, Server, " + "UNIX_TIMESTAMP(CURRENT_TIMESTAMP)-UNIX_TIMESTAMP(Timestamp) AS Ago, " + "(UNHEX(REPLACE(SaveID, '-',''))) AS SaveID " + "FROM %s_saves " + "where Code = ? AND Map = ? AND DDNet7 = false AND Savegame LIKE ?;", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Code); + pSqlServer->BindString(2, pData->m_Map); + pSqlServer->BindString(3, aSaveLike); + + if(!pSqlServer->Step()) + { + strcpy(pData->m_pResult->m_aMessage, "No such savegame for this map"); + goto end; + } + char aServerName[32]; + pSqlServer->GetString(2, aServerName, sizeof(aServerName)); + if(str_comp(aServerName, g_Config.m_SvSqlServerName) != 0) + { + str_format(pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage), + "You have to be on the '%s' server to load this savegame", aServerName); + goto end; + } + + int Since = pSqlServer->GetInt(3); + if(Since < g_Config.m_SvSaveGamesDelay) + { + str_format(pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage), + "You have to wait %d seconds until you can load this savegame", + g_Config.m_SvSaveGamesDelay - Since); + goto end; + } + if(pSqlServer->IsNull(4)) + { + memset(pData->m_pResult->m_SaveID.m_aData, 0, sizeof(pData->m_pResult->m_SaveID.m_aData)); + } + else + { + if(pSqlServer->GetBlob(4, pData->m_pResult->m_SaveID.m_aData, sizeof(pData->m_pResult->m_SaveID.m_aData)) != 16) + { + strcpy(pData->m_pResult->m_aMessage, "Unable to load savegame: SaveID corrupted"); + goto end; + } + } + + char aSaveString[65536]; + pSqlServer->GetString(1, aSaveString, sizeof(aSaveString)); + int Num = pData->m_pResult->m_SavedTeam.LoadString(aSaveString); + + if(Num != 0) + { + strcpy(pData->m_pResult->m_aMessage, "Unable to load savegame: data corrupted"); + goto end; + } + + bool CanLoad = pData->m_pResult->m_SavedTeam.MatchPlayers( + pData->m_aClientNames, pData->m_aClientID, pData->m_NumPlayer, + pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage)); + + if(!CanLoad) + goto end; + + str_format(aBuf, sizeof(aBuf), + "DELETE FROM %s_saves " + "WHERE Code = ? AND Map = ?;", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Code); + pSqlServer->BindString(2, pData->m_Map); + pSqlServer->Step(); + + pData->m_pResult->m_Status = CScoreSaveResult::LOAD_SUCCESS; + strcpy(pData->m_pResult->m_aMessage, "Loading successfully done"); + } +end: + pSqlServer->Unlock(); + return true; +} + +void CScore::GetSaves(int ClientID) +{ + if(RateLimitPlayer(ClientID)) + return; + ExecPlayerThread(GetSavesThread, "get saves", ClientID, "", 0); +} + +bool CScore::GetSavesThread(IDbConnection *pSqlServer, const ISqlData *pGameData) +{ + const CSqlPlayerRequest *pData = dynamic_cast(pGameData); + auto paMessages = pData->m_pResult->m_Data.m_aaMessages; + + char aSaveLike[128] = ""; + str_append(aSaveLike, "%\n", sizeof(aSaveLike)); + sqlstr::EscapeLike(aSaveLike + str_length(aSaveLike), + pData->m_RequestingPlayer, + sizeof(aSaveLike) - str_length(aSaveLike)); + str_append(aSaveLike, "\t%", sizeof(aSaveLike)); + + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), + "SELECT COUNT(*) AS NumSaves, " + "UNIX_TIMESTAMP(CURRENT_TIMESTAMP)-UNIX_TIMESTAMP(MAX(Timestamp)) AS Ago " + "FROM %s_saves " + "WHERE Map = ? AND Savegame LIKE ?;", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + pSqlServer->BindString(2, aSaveLike); + + if(pSqlServer->Step()) + { + int NumSaves = pSqlServer->GetInt(1); + int Ago = pSqlServer->GetInt(2); + char aAgoString[40] = "\0"; + char aLastSavedString[60] = "\0"; + if(Ago) + { + sqlstr::AgoTimeToString(Ago, aAgoString); + str_format(aLastSavedString, sizeof(aLastSavedString), ", last saved %s ago", aAgoString); + } + + str_format(paMessages[0], sizeof(paMessages[0]), + "%s has %d save%s on %s%s", + pData->m_RequestingPlayer, + NumSaves, NumSaves == 1 ? "" : "s", + pData->m_Map, aLastSavedString); + } + + pData->m_pResult->m_Done = true; + return true; +} diff --git a/src/game/server/score.h b/src/game/server/score.h index ae1b87087..0eab0cb40 100644 --- a/src/game/server/score.h +++ b/src/game/server/score.h @@ -4,10 +4,18 @@ #include #include -#include #include +#include +#include +#include + #include "save.h" +struct ISqlData; +class IDbConnection; +class IServer; +class CGameContext; + enum { NUM_CHECKPOINTS = 25, @@ -132,43 +140,193 @@ public: float m_aBestCpTime[NUM_CHECKPOINTS]; }; -class IScore +struct CSqlInitData : ISqlData +{ + CSqlInitData(std::shared_ptr pResult) : + m_pResult(pResult) + {} + std::shared_ptr m_pResult; + + // current map + char m_Map[MAX_MAP_LENGTH]; +}; + +struct CSqlPlayerRequest : ISqlData +{ + CSqlPlayerRequest(std::shared_ptr pResult) : + m_pResult(pResult) + {} + std::shared_ptr m_pResult; + // object being requested, either map (128 bytes) or player (16 bytes) + char m_Name[MAX_MAP_LENGTH]; + // current map + char m_Map[MAX_MAP_LENGTH]; + char m_RequestingPlayer[MAX_NAME_LENGTH]; + // relevant for /top5 kind of requests + int m_Offset; +}; + +struct CSqlRandomMapRequest : ISqlData +{ + CSqlRandomMapRequest(std::shared_ptr pResult) : + m_pResult(pResult) + {} + std::shared_ptr m_pResult; + + char m_ServerType[32]; + char m_CurrentMap[MAX_MAP_LENGTH]; + char m_RequestingPlayer[MAX_NAME_LENGTH]; + int m_Stars; +}; + +struct CSqlScoreData : ISqlData +{ + CSqlScoreData(std::shared_ptr pResult) : + m_pResult(pResult) + {} + virtual ~CSqlScoreData() {}; + + std::shared_ptr m_pResult; + + char m_Map[MAX_MAP_LENGTH]; + char m_GameUuid[UUID_MAXSTRSIZE]; + char m_Name[MAX_MAP_LENGTH]; + + int m_ClientID; + float m_Time; + char m_aTimestamp[TIMESTAMP_STR_LENGTH]; + float m_aCpCurrent[NUM_CHECKPOINTS]; + int m_Num; + bool m_Search; + char m_aRequestingPlayer[MAX_NAME_LENGTH]; +}; + +struct CSqlTeamScoreData : ISqlData +{ + char m_GameUuid[UUID_MAXSTRSIZE]; + char m_Map[MAX_MAP_LENGTH]; + float m_Time; + char m_aTimestamp[TIMESTAMP_STR_LENGTH]; + unsigned int m_Size; + char m_aNames[MAX_CLIENTS][MAX_NAME_LENGTH]; +}; + +struct CSqlTeamSave : ISqlData +{ + CSqlTeamSave(std::shared_ptr pResult) : + m_pResult(pResult) + {} + virtual ~CSqlTeamSave() {}; + + std::shared_ptr m_pResult; + + char m_ClientName[MAX_NAME_LENGTH]; + char m_Map[MAX_MAP_LENGTH]; + char m_Code[128]; + char m_aGeneratedCode[128]; + char m_Server[5]; +}; + +struct CSqlTeamLoad : ISqlData +{ + CSqlTeamLoad(std::shared_ptr pResult) : + m_pResult(pResult) + {} + virtual ~CSqlTeamLoad() {}; + + std::shared_ptr m_pResult; + + char m_Code[128]; + char m_Map[MAX_MAP_LENGTH]; + char m_RequestingPlayer[MAX_NAME_LENGTH]; + int m_ClientID; + // struct holding all player names in the team or an empty string + char m_aClientNames[MAX_CLIENTS][MAX_NAME_LENGTH]; + int m_aClientID[MAX_CLIENTS]; + int m_NumPlayer; +}; + +class CScore { CPlayerData m_aPlayerData[MAX_CLIENTS]; + CDbConnectionPool *m_pPool; + + static bool Init(IDbConnection *pSqlServer, const ISqlData *pGameData); + + static bool RandomMapThread(IDbConnection *pSqlServer, const ISqlData *pGameData); + static bool RandomUnfinishedMapThread(IDbConnection *pSqlServer, const ISqlData *pGameData); + static bool MapVoteThread(IDbConnection *pSqlServer, const ISqlData *pGameData); + + static bool LoadPlayerDataThread(IDbConnection *pSqlServer, const ISqlData *pGameData); + static bool MapInfoThread(IDbConnection *pSqlServer, const ISqlData *pGameData); + static bool ShowRankThread(IDbConnection *pSqlServer, const ISqlData *pGameData); + static bool ShowTeamRankThread(IDbConnection *pSqlServer, const ISqlData *pGameData); + static bool ShowTop5Thread(IDbConnection *pSqlServer, const ISqlData *pGameData); + static bool ShowTeamTop5Thread(IDbConnection *pSqlServer, const ISqlData *pGameData); + static bool ShowTimesThread(IDbConnection *pSqlServer, const ISqlData *pGameData); + static bool ShowPointsThread(IDbConnection *pSqlServer, const ISqlData *pGameData); + static bool ShowTopPointsThread(IDbConnection *pSqlServer, const ISqlData *pGameData); + static bool GetSavesThread(IDbConnection *pSqlServer, const ISqlData *pGameData); + + static bool SaveTeamThread(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure); + static bool LoadTeamThread(IDbConnection *pSqlServer, const ISqlData *pGameData); + + static bool SaveScoreThread(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure); + static bool SaveTeamScoreThread(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure); + + CGameContext *GameServer() const { return m_pGameServer; } + IServer *Server() const { return m_pServer; } + CGameContext *m_pGameServer; + IServer *m_pServer; + + std::vector m_aWordlist; + CPrng m_Prng; + void GeneratePassphrase(char *pBuf, int BufSize); + + // returns new SqlResult bound to the player, if no current Thread is active for this player + std::shared_ptr NewSqlPlayerResult(int ClientID); + // Creates for player database requests + void ExecPlayerThread( + bool (*pFuncPtr) (IDbConnection *, const ISqlData *), + const char *pThreadName, + int ClientID, + const char *pName, + int Offset); + + // returns true if the player should be rate limited + bool RateLimitPlayer(int ClientID); public: - virtual ~IScore() {} + CScore(CGameContext *pGameServer, CDbConnectionPool *pPool); + ~CScore() {} CPlayerData *PlayerData(int ID) { return &m_aPlayerData[ID]; } - virtual void MapInfo(int ClientID, const char *pMapName) = 0; - virtual void MapVote(int ClientID, const char *pMapName) = 0; - virtual void LoadPlayerData(int ClientID) = 0; - virtual void SaveScore(int ClientID, float Time, const char *pTimestamp, float aCpTime[NUM_CHECKPOINTS], bool NotEligible) = 0; + void MapInfo(int ClientID, const char *pMapName); + void MapVote(int ClientID, const char *pMapName); + void LoadPlayerData(int ClientID); + void SaveScore(int ClientID, float Time, const char *pTimestamp, float aCpTime[NUM_CHECKPOINTS], bool NotEligible); - virtual void SaveTeamScore(int *pClientIDs, unsigned int Size, float Time, const char *pTimestamp) = 0; + void SaveTeamScore(int *pClientIDs, unsigned int Size, float Time, const char *pTimestamp); - virtual void ShowTop5(int ClientID, int Offset=1) = 0; - virtual void ShowRank(int ClientID, const char *pName) = 0; + void ShowTop5(int ClientID, int Offset=1); + void ShowRank(int ClientID, const char *pName); - virtual void ShowTeamTop5(int ClientID, int Offset=1) = 0; - virtual void ShowTeamRank(int ClientID, const char *pName) = 0; + void ShowTeamTop5(int ClientID, int Offset=1); + void ShowTeamRank(int ClientID, const char *pName); - virtual void ShowTopPoints(int ClientID, int Offset=1) = 0; - virtual void ShowPoints(int ClientID, const char *pName) = 0; + void ShowTopPoints(int ClientID, int Offset=1); + void ShowPoints(int ClientID, const char *pName); - virtual void ShowTimes(int ClientID, const char *pName, int Offset = 1) = 0; - virtual void ShowTimes(int ClientID, int Offset = 1) = 0; + void ShowTimes(int ClientID, const char *pName, int Offset = 1); + void ShowTimes(int ClientID, int Offset = 1); - virtual void RandomMap(int ClientID, int Stars) = 0; - virtual void RandomUnfinishedMap(int ClientID, int Stars) = 0; + void RandomMap(int ClientID, int Stars); + void RandomUnfinishedMap(int ClientID, int Stars); - virtual void SaveTeam(int ClientID, const char *pCode, const char *pServer) = 0; - virtual void LoadTeam(const char *pCode, int ClientID) = 0; - virtual void GetSaves(int ClientID) = 0; - - // called when the server is shut down but not on mapchange/reload - virtual void OnShutdown() = 0; + void SaveTeam(int ClientID, const char *pCode, const char *pServer); + void LoadTeam(const char *pCode, int ClientID); + void GetSaves(int ClientID); }; #endif // GAME_SERVER_SCORE_H diff --git a/src/game/server/score/file_score.cpp b/src/game/server/score/file_score.cpp deleted file mode 100644 index 7518ae50c..000000000 --- a/src/game/server/score/file_score.cpp +++ /dev/null @@ -1,372 +0,0 @@ -/* (c) Shereef Marzouk. See "licence DDRace.txt" and the readme.txt in the root of the distribution for more information. */ -/* Based on Race mod stuff and tweaked by GreYFoX@GTi and others to fit our DDRace needs. */ -/* copyright (c) 2008 rajh and gregwar. Score stuff */ -#include - -#include -#include -#include -#include "../gamemodes/DDRace.h" -#include "file_score.h" -#include - -static LOCK gs_ScoreLock = 0; - -CFileScore::CPlayerScore::CPlayerScore(const char *pName, float Score, - float aCpTime[NUM_CHECKPOINTS]) -{ - str_copy(m_aName, pName, sizeof(m_aName)); - m_Score = Score; - for (int i = 0; i < NUM_CHECKPOINTS; i++) - m_aCpTime[i] = aCpTime[i]; -} - -CFileScore::CFileScore(CGameContext *pGameServer) : - m_pGameServer(pGameServer), m_pServer(pGameServer->Server()) -{ - if (gs_ScoreLock == 0) - gs_ScoreLock = lock_create(); - - Init(); -} - -CFileScore::~CFileScore() -{ - lock_wait(gs_ScoreLock); - - // clear list - m_Top.clear(); - - lock_unlock(gs_ScoreLock); -} - -std::string CFileScore::SaveFile() -{ - std::ostringstream oss; - char aBuf[256]; - str_copy(aBuf, Server()->GetMapName(), sizeof(aBuf)); - for(int i = 0; i < 256; i++) if(aBuf[i] == '/') aBuf[i] = '-'; - if (g_Config.m_SvScoreFolder[0]) - oss << g_Config.m_SvScoreFolder << "/" << aBuf << "_record.dtb"; - else - oss << Server()->GetMapName() << "_record.dtb"; - return oss.str(); -} - -void CFileScore::MapInfo(int ClientID, const char* MapName) -{ - // TODO: implement -} - -void CFileScore::MapVote(int ClientID, const char* MapName) -{ - // TODO: implement -} - -void CFileScore::SaveScoreThread(void *pUser) -{ - CFileScore *pSelf = (CFileScore *) pUser; - lock_wait(gs_ScoreLock); - std::fstream f; - f.open(pSelf->SaveFile().c_str(), std::ios::out); - if(f.fail()) - { - dbg_msg("filescore", "opening '%s' for writing failed", pSelf->SaveFile().c_str()); - } - else - { - int t = 0; - for (sorted_array::range r = pSelf->m_Top.all(); - !r.empty(); r.pop_front()) - { - f << r.front().m_aName << std::endl << r.front().m_Score - << std::endl; - if (g_Config.m_SvCheckpointSave) - { - for (int c = 0; c < NUM_CHECKPOINTS; c++) - f << r.front().m_aCpTime[c] << " "; - f << std::endl; - } - t++; - if (t % 50 == 0) - thread_sleep(1000); - } - } - f.close(); - lock_unlock(gs_ScoreLock); -} - -void CFileScore::Save() -{ - thread_init_and_detach(SaveScoreThread, this, "FileScore save"); -} - -void CFileScore::Init() -{ - lock_wait(gs_ScoreLock); - - // create folder if not exist - if (g_Config.m_SvScoreFolder[0]) - fs_makedir(g_Config.m_SvScoreFolder); - - std::fstream f; - f.open(SaveFile().c_str(), std::ios::in); - - if(f.fail()) - { - dbg_msg("filescore", "opening '%s' for reading failed", SaveFile().c_str()); - } - while (!f.eof() && !f.fail()) - { - std::string TmpName, TmpScore, TmpCpLine; - std::getline(f, TmpName); - if (!f.eof() && TmpName != "") - { - std::getline(f, TmpScore); - float aTmpCpTime[NUM_CHECKPOINTS] = - { 0 }; - if (g_Config.m_SvCheckpointSave) - { - std::getline(f, TmpCpLine); - - std::istringstream iss(TmpCpLine); - int i = 0; - for(std::string p; std::getline(iss, p, ' '); i++) - aTmpCpTime[i] = std::stof(p, NULL); - } - m_Top.add( - *new CPlayerScore(TmpName.c_str(), atof(TmpScore.c_str()), - aTmpCpTime)); - } - } - f.close(); - lock_unlock(gs_ScoreLock); - - // save the current best score - if (m_Top.size()) - ((CGameControllerDDRace*) GameServer()->m_pController)->m_CurrentRecord = - m_Top[0].m_Score; -} - -CFileScore::CPlayerScore *CFileScore::SearchName(const char *pName, - int *pPosition, bool NoCase) -{ - CPlayerScore *pPlayer = 0; - int Pos = 1; - int Found = 0; - for (sorted_array::range r = m_Top.all(); !r.empty(); - r.pop_front()) - { - if (str_find_nocase(r.front().m_aName, pName)) - { - if (pPosition) - *pPosition = Pos; - if (NoCase) - { - Found++; - pPlayer = &r.front(); - } - if (!str_comp(r.front().m_aName, pName)) - return &r.front(); - } - Pos++; - } - if (Found > 1) - { - if (pPosition) - *pPosition = -1; - return 0; - } - return pPlayer; -} - -void CFileScore::UpdatePlayer(int ID, float Score, - float aCpTime[NUM_CHECKPOINTS]) -{ - const char *pName = Server()->ClientName(ID); - - lock_wait(gs_ScoreLock); - CPlayerScore *pPlayer = SearchScore(ID, 0); - - if (pPlayer) - { - for (int c = 0; c < NUM_CHECKPOINTS; c++) - pPlayer->m_aCpTime[c] = aCpTime[c]; - - pPlayer->m_Score = Score; - str_copy(pPlayer->m_aName, pName, sizeof(pPlayer->m_aName)); - - sort(m_Top.all()); - } - else - m_Top.add(*new CPlayerScore(pName, Score, aCpTime)); - - lock_unlock(gs_ScoreLock); - Save(); -} - -void CFileScore::LoadPlayerData(int ClientID) -{ - CPlayerScore *pPlayer = SearchScore(ClientID, 0); - if (pPlayer) - { - lock_wait(gs_ScoreLock); - lock_unlock(gs_ScoreLock); - Save(); - } - - // set score - if (pPlayer) - { - PlayerData(ClientID)->Set(pPlayer->m_Score, pPlayer->m_aCpTime); - GameServer()->m_apPlayers[ClientID]->m_HasFinishScore = true; - } -} - -void CFileScore::SaveTeamScore(int* ClientIDs, unsigned int Size, float Time, const char *pTimestamp) -{ - dbg_msg("filescore", "saveteamscore not implemented for filescore"); -} - -void CFileScore::SaveScore(int ClientID, float Time, const char *pTimestamp, - float CpTime[NUM_CHECKPOINTS], bool NotEligible) -{ - CConsole* pCon = (CConsole*) GameServer()->Console(); - if (!pCon->m_Cheated || g_Config.m_SvRankCheats) - UpdatePlayer(ClientID, Time, CpTime); -} - -void CFileScore::ShowTop5(int ClientID, int Offset) -{ - char aBuf[512]; - Offset = maximum(1, Offset < 0 ? m_Top.size() + Offset - 3 : Offset); - GameServer()->SendChatTarget(ClientID, "----------- Top 5 -----------"); - for (int i = 0; i < 5; i++) - { - if (i + Offset > m_Top.size()) - break; - CPlayerScore *r = &m_Top[i + Offset - 1]; - str_format(aBuf, sizeof(aBuf), - "%d. %s Time: %d minute(s) %5.2f second(s)", i + Offset, - r->m_aName, (int)r->m_Score / 60, - r->m_Score - ((int)r->m_Score / 60 * 60)); - GameServer()->SendChatTarget(ClientID, aBuf); - } - GameServer()->SendChatTarget(ClientID, "------------------------------"); -} - -void CFileScore::ShowRank(int ClientID, const char* pName) -{ - CPlayerScore *pScore; - int Pos = -2; - char aBuf[512]; - - pScore = SearchName(pName, &Pos, 1); - - if (pScore && Pos > -1) - { - float Time = pScore->m_Score; - if (g_Config.m_SvHideScore) - str_format(aBuf, sizeof(aBuf), - "Your time: %d minute(s) %5.2f second(s)", (int)Time / 60, - Time - ((int)Time / 60 * 60)); - else - str_format(aBuf, sizeof(aBuf), - "%d. %s Time: %d minute(s) %5.2f second(s), requested by (%s)", Pos, - pScore->m_aName, (int)Time / 60, - Time - ((int)Time / 60 * 60), Server()->ClientName(ClientID)); - if (g_Config.m_SvHideScore) - GameServer()->SendChatTarget(ClientID, aBuf); - else - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf, ClientID); - return; - } - else if (Pos == -1) - str_format(aBuf, sizeof(aBuf), "Several players were found."); - else - str_format(aBuf, sizeof(aBuf), "%s is not ranked", pName); - - GameServer()->SendChatTarget(ClientID, aBuf); -} - -void CFileScore::ShowTeamTop5(int ClientID, int Offset) -{ - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "Team ranks not supported in file based servers"); - GameServer()->SendChatTarget(ClientID, aBuf); -} - -void CFileScore::ShowTeamRank(int ClientID, const char* pName) -{ - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "Team ranks not supported in file based servers"); - GameServer()->SendChatTarget(ClientID, aBuf); -} - -void CFileScore::ShowTopPoints(int ClientID, int Offset) -{ - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "Team ranks not supported in file based servers"); - GameServer()->SendChatTarget(ClientID, aBuf); -} - -void CFileScore::ShowPoints(int ClientID, const char* pName) -{ - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "Points not supported in file based servers"); - GameServer()->SendChatTarget(ClientID, aBuf); -} - -void CFileScore::ShowTimes(int ClientID, const char *pName, int Offset) -{ - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "Show times not supported in file based servers"); - GameServer()->SendChatTarget(ClientID, aBuf); -} - -void CFileScore::ShowTimes(int ClientID, int Offset) -{ - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "Show times not supported in file based servers"); - GameServer()->SendChatTarget(ClientID, aBuf); -} - -void CFileScore::RandomMap(int ClientID, int Stars) -{ - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "Random map not supported in file based servers"); - GameServer()->SendChatTarget(ClientID, aBuf); -} - -void CFileScore::RandomUnfinishedMap(int ClientID, int Stars) -{ - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "Random unfinished map not supported in file based servers"); - GameServer()->SendChatTarget(ClientID, aBuf); -} - -void CFileScore::SaveTeam(int ClientID, const char* Code, const char* Server) -{ - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "Save-function not supported in file based servers"); - GameServer()->SendChatTarget(ClientID, aBuf); -} - -void CFileScore::LoadTeam(const char* Code, int ClientID) -{ - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "Save-function not supported in file based servers"); - GameServer()->SendChatTarget(ClientID, aBuf); -} - -void CFileScore::GetSaves(int ClientID) -{ - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "Save-function not supported in file based servers"); - GameServer()->SendChatTarget(ClientID, aBuf); -} - -void CFileScore::OnShutdown() -{ - ; -} diff --git a/src/game/server/score/file_score.h b/src/game/server/score/file_score.h deleted file mode 100644 index 429cc37b6..000000000 --- a/src/game/server/score/file_score.h +++ /dev/null @@ -1,92 +0,0 @@ -/* (c) Shereef Marzouk. See "licence DDRace.txt" and the readme.txt in the root of the distribution for more information. */ -/* Based on Race mod stuff and tweaked by GreYFoX@GTi and others to fit our DDRace needs. */ -/* copyright (c) 2008 rajh and gregwar. Score stuff */ -#ifndef GAME_SERVER_SCORE_FILE_SCORE_H -#define GAME_SERVER_SCORE_FILE_SCORE_H - -#include - -#include -#include "../score.h" - -class CFileScore: public IScore -{ - CGameContext *m_pGameServer; - IServer *m_pServer; - - class CPlayerScore - { - public: - char m_aName[MAX_NAME_LENGTH]; - float m_Score; - float m_aCpTime[NUM_CHECKPOINTS]; - - CPlayerScore() {} - - CPlayerScore(const char *pName, float Score, - float aCpTime[NUM_CHECKPOINTS]); - - bool operator<(const CPlayerScore& other) - { - return (this->m_Score < other.m_Score); - } - }; - - sorted_array m_Top; - - CGameContext *GameServer() - { - return m_pGameServer; - } - IServer *Server() - { - return m_pServer; - } - - CPlayerScore *SearchScore(int ID, int *pPosition) - { - return SearchName(Server()->ClientName(ID), pPosition, 0); - } - - CPlayerScore *SearchName(const char *pName, int *pPosition, bool MatchCase); - void UpdatePlayer(int ID, float Score, float aCpTime[NUM_CHECKPOINTS]); - - void Init(); - void Save(); - static void SaveScoreThread(void *pUser); - -public: - - CFileScore(CGameContext *pGameServer); - ~CFileScore(); - - virtual void LoadPlayerData(int ClientID); - virtual void MapInfo(int ClientID, const char* MapName); - virtual void MapVote(int ClientID, const char* MapName); - virtual void SaveScore(int ClientID, float Time, const char *pTimestamp, - float CpTime[NUM_CHECKPOINTS], bool NotEligible); - virtual void SaveTeamScore(int* ClientIDs, unsigned int Size, float Time, const char *pTimestamp); - - virtual void ShowTop5(int ClientID, int Offset = 1); - virtual void ShowRank(int ClientID, const char* pName); - - virtual void ShowTeamTop5(int ClientID, int Offset = 1); - virtual void ShowTeamRank(int ClientID, const char* pName); - - virtual void ShowTopPoints(int ClientID, int Offset); - virtual void ShowPoints(int ClientID, const char* pName); - virtual void ShowTimes(int ClientID, const char *pName, int Offset = 1); - virtual void ShowTimes(int ClientID, int Offset = 1); - virtual void RandomMap(int ClientID, int Stars); - virtual void RandomUnfinishedMap(int ClientID, int Stars); - virtual void SaveTeam(int ClientID, const char* Code, const char* Server); - virtual void LoadTeam(const char* Code, int ClientID); - virtual void GetSaves(int ClientID); - - virtual void OnShutdown(); - -private: - std::string SaveFile(); -}; - -#endif // GAME_SERVER_SCORE_FILE_SCORE_H diff --git a/src/game/server/score/sql_score.cpp b/src/game/server/score/sql_score.cpp deleted file mode 100644 index 02e64d497..000000000 --- a/src/game/server/score/sql_score.cpp +++ /dev/null @@ -1,1886 +0,0 @@ -/* (c) Shereef Marzouk. See "licence DDRace.txt" and the readme.txt in the root of the distribution for more information. */ -/* Based on Race mod stuff and tweaked by GreYFoX@GTi and others to fit our DDRace needs. */ -/* CSqlScore class by Sushi */ -#if defined(CONF_SQL) -#include "sql_score.h" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "../entities/character.h" -#include "../gamemodes/DDRace.h" -#include "../save.h" - -std::atomic_int CSqlScore::ms_InstanceCount(0); - -template < typename TResult > -CSqlExecData::CSqlExecData( - bool (*pFuncPtr) (CSqlServer*, const CSqlData *, bool), - CSqlData *pSqlResult, - bool ReadOnly -) : - m_pFuncPtr(pFuncPtr), - m_pSqlData(pSqlResult), - m_ReadOnly(ReadOnly) -{ - ++CSqlScore::ms_InstanceCount; -} - -template < typename TResult > -CSqlExecData::~CSqlExecData() -{ - --CSqlScore::ms_InstanceCount; -} - -std::shared_ptr CSqlScore::NewSqlPlayerResult(int ClientID) -{ - CPlayer *pCurPlayer = GameServer()->m_apPlayers[ClientID]; - if(pCurPlayer->m_ScoreQueryResult != nullptr) // TODO: send player a message: "too many requests" - return nullptr; - pCurPlayer->m_ScoreQueryResult = std::make_shared(); - return pCurPlayer->m_ScoreQueryResult; -} - -void CSqlScore::ExecPlayerThread( - bool (*pFuncPtr) (CSqlServer*, const CSqlData *, bool), - const char* pThreadName, - int ClientID, - const char* pName, - int Offset -) { - auto pResult = NewSqlPlayerResult(ClientID); - if(pResult == nullptr) - return; - CSqlPlayerRequest *Tmp = new CSqlPlayerRequest(pResult); - Tmp->m_Name = pName; - Tmp->m_Map = g_Config.m_SvMap; - Tmp->m_RequestingPlayer = Server()->ClientName(ClientID); - Tmp->m_Offset = Offset; - - thread_init_and_detach(CSqlExecData::ExecSqlFunc, - new CSqlExecData(pFuncPtr, Tmp), - pThreadName); -} - -bool CSqlScore::RateLimitPlayer(int ClientID) -{ - CPlayer *pPlayer = GameServer()->m_apPlayers[ClientID]; - if(pPlayer == 0) - return true; - if(pPlayer->m_LastSQLQuery + g_Config.m_SvSqlQueriesDelay * Server()->TickSpeed() >= Server()->Tick()) - return true; - pPlayer->m_LastSQLQuery = Server()->Tick(); - return false; -} - -void CSqlScore::GeneratePassphrase(char *pBuf, int BufSize) -{ - for(int i = 0; i < 3; i++) - { - if(i != 0) - str_append(pBuf, " ", BufSize); - // TODO: decide if the slight bias towards lower numbers is ok - int Rand = m_Prng.RandomBits() % m_aWordlist.size(); - str_append(pBuf, m_aWordlist[Rand].c_str(), BufSize); - } -} - -LOCK CSqlScore::ms_FailureFileLock = lock_create(); - -void CSqlScore::OnShutdown() -{ - int i = 0; - while (CSqlScore::ms_InstanceCount != 0) - { - if (i > 600) { - dbg_msg("sql", "Waited 60 seconds for score-threads to complete, quitting anyway"); - break; - } - - // print a log about every two seconds - if (i % 20 == 0) - dbg_msg("sql", "Waiting for score-threads to complete (%d left)", CSqlScore::ms_InstanceCount.load()); - ++i; - thread_sleep(100000); - } - - lock_destroy(ms_FailureFileLock); -} - -template < typename TResult > -void CSqlExecData::ExecSqlFunc(void *pUser) -{ - CSqlExecData* pData = (CSqlExecData *)pUser; - - CSqlConnector connector; - - bool Success = false; - - try { - // try to connect to a working database server - while (!Success && !connector.MaxTriesReached(pData->m_ReadOnly) && connector.ConnectSqlServer(pData->m_ReadOnly)) - { - try { - if (pData->m_pFuncPtr(connector.SqlServer(), pData->m_pSqlData, false)) - Success = true; - } catch (...) { - dbg_msg("sql", "Unexpected exception caught"); - } - - // disconnect from database server - connector.SqlServer()->Disconnect(); - } - - // handle failures - // eg write inserts to a file and print a nice error message - if (!Success) - pData->m_pFuncPtr(0, pData->m_pSqlData, true); - } catch (...) { - dbg_msg("sql", "Unexpected exception caught"); - } - - delete pData->m_pSqlData; - delete pData; -} - -CSqlScore::CSqlScore(CGameContext *pGameServer) : - m_pGameServer(pGameServer), - m_pServer(pGameServer->Server()) -{ - CSqlConnector::ResetReachable(); - - auto InitResult = std::make_shared(); - CSqlInitData *Tmp = new CSqlInitData(InitResult); - ((CGameControllerDDRace*)(pGameServer->m_pController))->m_pInitResult = InitResult; - Tmp->m_Map = g_Config.m_SvMap; - - IOHANDLE File = GameServer()->Storage()->OpenFile("wordlist.txt", IOFLAG_READ, IStorage::TYPE_ALL); - if(!File) - { - dbg_msg("sql", "failed to open wordlist"); - Server()->SetErrorShutdown("sql open wordlist error"); - return; - } - - uint64 aSeed[2]; - secure_random_fill(aSeed, sizeof(aSeed)); - m_Prng.Seed(aSeed); - CLineReader LineReader; - LineReader.Init(File); - char *pLine; - while((pLine = LineReader.Get())) - { - char Word[32] = {0}; - sscanf(pLine, "%*s %31s", Word); - Word[31] = 0; - m_aWordlist.push_back(Word); - } - if(m_aWordlist.size() < 1000) - { - dbg_msg("sql", "too few words in wordlist"); - Server()->SetErrorShutdown("sql too few words in wordlist"); - return; - } - thread_init_and_detach(CSqlExecData::ExecSqlFunc, - new CSqlExecData(Init, Tmp), - "SqlScore constructor"); -} - -bool CSqlScore::Init(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlInitData *pData = dynamic_cast(pGameData); - - if (HandleFailure) - { - dbg_msg("sql", "FATAL ERROR: Could not init SqlScore"); - return true; - } - - try - { - char aBuf[512]; - // get the best time - str_format(aBuf, sizeof(aBuf), - "SELECT Time FROM %s_race WHERE Map='%s' ORDER BY `Time` ASC LIMIT 1;", - pSqlServer->GetPrefix(), pData->m_Map.ClrStr()); - pSqlServer->executeSqlQuery(aBuf); - - if(pSqlServer->GetResults()->next()) - pData->m_pResult->m_CurrentRecord = (float)pSqlServer->GetResults()->getDouble("Time"); - - pData->m_pResult->m_Done = true; - dbg_msg("sql", "Getting best time on server done"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - } - return false; -} - -void CSqlScore::LoadPlayerData(int ClientID) -{ - ExecPlayerThread(LoadPlayerDataThread, "load player data", ClientID, "", 0); -} - -// update stuff -bool CSqlScore::LoadPlayerDataThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlPlayerRequest *pData = dynamic_cast(pGameData); - pData->m_pResult->SetVariant(CScorePlayerResult::PLAYER_INFO); - - if (HandleFailure) - return true; - - try - { - char aBuf[512]; - // get best race time - str_format(aBuf, sizeof(aBuf), - "SELECT * " - "FROM %s_race " - "WHERE Map='%s' AND Name='%s' " - "ORDER BY Time ASC " - "LIMIT 1;", - pSqlServer->GetPrefix(), pData->m_Map.ClrStr(), pData->m_RequestingPlayer.ClrStr()); - pSqlServer->executeSqlQuery(aBuf); - if(pSqlServer->GetResults()->next()) - { - // get the best time - float Time = (float)pSqlServer->GetResults()->getDouble("Time"); - pData->m_pResult->m_Data.m_Info.m_Time = Time; - pData->m_pResult->m_Data.m_Info.m_Score = -Time; - pData->m_pResult->m_Data.m_Info.m_HasFinishScore = true; - - char aColumn[8]; - if(g_Config.m_SvCheckpointSave) - { - for(int i = 0; i < NUM_CHECKPOINTS; i++) - { - str_format(aColumn, sizeof(aColumn), "cp%d", i+1); - pData->m_pResult->m_Data.m_Info.m_CpTime[i] = (float)pSqlServer->GetResults()->getDouble(aColumn); - } - } - } - - // birthday check - str_format(aBuf, sizeof(aBuf), - "SELECT YEAR(Current) - YEAR(Stamp) AS YearsAgo " - "FROM (" - "SELECT CURRENT_TIMESTAMP AS Current, MIN(Timestamp) AS Stamp " - "FROM %s_race " - "WHERE Name='%s'" - ") AS l " - "WHERE DAYOFMONTH(Current) = DAYOFMONTH(Stamp) AND MONTH(Current) = MONTH(Stamp) " - "AND YEAR(Current) > YEAR(Stamp);", - pSqlServer->GetPrefix(), pData->m_RequestingPlayer.ClrStr()); - pSqlServer->executeSqlQuery(aBuf); - - if(pSqlServer->GetResults()->next()) - { - int YearsAgo = pSqlServer->GetResults()->getInt("YearsAgo"); - pData->m_pResult->m_Data.m_Info.m_Birthday = YearsAgo; - } - pData->m_pResult->m_Done = true; - dbg_msg("sql", "Finished loading player data"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not update account"); - } - return false; -} - -void CSqlScore::MapVote(int ClientID, const char* MapName) -{ - if(RateLimitPlayer(ClientID)) - return; - ExecPlayerThread(MapVoteThread, "map vote", ClientID, MapName, 0); -} - -bool CSqlScore::MapVoteThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlPlayerRequest *pData = dynamic_cast(pGameData); - auto paMessages = pData->m_pResult->m_Data.m_aaMessages; - - if (HandleFailure) - return true; - - try - { - char aFuzzyMap[128]; - str_copy(aFuzzyMap, pData->m_Name.Str(), sizeof(aFuzzyMap)); - sqlstr::ClearString(aFuzzyMap, sizeof(aFuzzyMap)); - sqlstr::FuzzyString(aFuzzyMap, sizeof(aFuzzyMap)); - - char aBuf[768]; - str_format(aBuf, sizeof(aBuf), - "SELECT Map, Server " - "FROM %s_maps " - "WHERE Map LIKE '%s' COLLATE utf8mb4_general_ci " - "ORDER BY " - "CASE WHEN Map = '%s' THEN 0 ELSE 1 END, " - "CASE WHEN Map LIKE '%s%%' THEN 0 ELSE 1 END, " - "LENGTH(Map), Map " - "LIMIT 1;", - pSqlServer->GetPrefix(), aFuzzyMap, - pData->m_Name.ClrStr(), pData->m_Name.ClrStr() - ); - pSqlServer->executeSqlQuery(aBuf); - if(pSqlServer->GetResults()->rowsCount() != 1) - { - str_format(paMessages[0], sizeof(paMessages[0]), - "No map like \"%s\" found. " - "Try adding a '%%' at the start if you don't know the first character. " - "Example: /map %%castle for \"Out of Castle\"", - pData->m_Name.Str()); - } - else - { - pSqlServer->GetResults()->first(); - auto Server = pSqlServer->GetResults()->getString("Server"); - auto Map = pSqlServer->GetResults()->getString("Map"); - pData->m_pResult->SetVariant(CScorePlayerResult::MAP_VOTE); - auto MapVote = &pData->m_pResult->m_Data.m_MapVote; - strcpy(MapVote->m_Reason, "/map"); - str_copy(MapVote->m_Server, Server.c_str(), sizeof(MapVote->m_Server)); - str_copy(MapVote->m_Map, Map.c_str(), sizeof(MapVote->m_Map)); - - for(char *p = MapVote->m_Server; *p; p++) // lower case server - *p = tolower(*p); - } - pData->m_pResult->m_Done = true; - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not start Mapvote"); - } - return false; -} - -void CSqlScore::MapInfo(int ClientID, const char* MapName) -{ - if(RateLimitPlayer(ClientID)) - return; - ExecPlayerThread(MapInfoThread, "map info", ClientID, MapName, 0); -} - -bool CSqlScore::MapInfoThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlPlayerRequest *pData = dynamic_cast(pGameData); - - if (HandleFailure) - return true; - - try - { - char aFuzzyMap[128]; - str_copy(aFuzzyMap, pData->m_Name.Str(), sizeof(aFuzzyMap)); - sqlstr::ClearString(aFuzzyMap, sizeof(aFuzzyMap)); - sqlstr::FuzzyString(aFuzzyMap, sizeof(aFuzzyMap)); - - char aBuf[1024]; - str_format(aBuf, sizeof(aBuf), - "SELECT l.Map, l.Server, Mapper, Points, Stars, " - "(select count(Name) from %s_race where Map = l.Map) as Finishes, " - "(select count(distinct Name) from %s_race where Map = l.Map) as Finishers, " - "(select round(avg(Time)) from %s_race where Map = l.Map) as Average, " - "UNIX_TIMESTAMP(l.Timestamp) as Stamp, " - "UNIX_TIMESTAMP(CURRENT_TIMESTAMP)-UNIX_TIMESTAMP(l.Timestamp) as Ago, " - "(select min(Time) from %s_race where Map = l.Map and Name = '%s') as OwnTime " - "FROM (" - "SELECT * FROM %s_maps " - "WHERE Map LIKE '%s' COLLATE utf8mb4_general_ci " - "ORDER BY " - "CASE WHEN Map = '%s' THEN 0 ELSE 1 END, " - "CASE WHEN Map LIKE '%s%%' THEN 0 ELSE 1 END, " - "LENGTH(Map), " - "Map " - "LIMIT 1" - ") as l;", - pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), - pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), - pData->m_RequestingPlayer.ClrStr(), - pSqlServer->GetPrefix(), - aFuzzyMap, - pData->m_Name.ClrStr(), - pData->m_Name.ClrStr() - ); - pSqlServer->executeSqlQuery(aBuf); - - if(pSqlServer->GetResults()->rowsCount() != 1) - { - str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), - "No map like \"%s\" found.", pData->m_Name.Str()); - } - else - { - pSqlServer->GetResults()->next(); - int Points = pSqlServer->GetResults()->getInt("Points"); - int Stars = pSqlServer->GetResults()->getInt("Stars"); - int finishes = pSqlServer->GetResults()->getInt("Finishes"); - int finishers = pSqlServer->GetResults()->getInt("Finishers"); - int average = pSqlServer->GetResults()->getInt("Average"); - char aMap[128]; - strcpy(aMap, pSqlServer->GetResults()->getString("Map").c_str()); - char aServer[32]; - strcpy(aServer, pSqlServer->GetResults()->getString("Server").c_str()); - char aMapper[128]; - strcpy(aMapper, pSqlServer->GetResults()->getString("Mapper").c_str()); - int stamp = pSqlServer->GetResults()->getInt("Stamp"); - int ago = pSqlServer->GetResults()->getInt("Ago"); - float ownTime = (float)pSqlServer->GetResults()->getDouble("OwnTime"); - - char aAgoString[40] = "\0"; - char aReleasedString[60] = "\0"; - if(stamp != 0) - { - sqlstr::AgoTimeToString(ago, aAgoString); - str_format(aReleasedString, sizeof(aReleasedString), ", released %s ago", aAgoString); - } - - char aAverageString[60] = "\0"; - if(average > 0) - { - str_format(aAverageString, sizeof(aAverageString), " in %d:%02d average", average / 60, average % 60); - } - - char aStars[20]; - switch(Stars) - { - case 0: strcpy(aStars, "✰✰✰✰✰"); break; - case 1: strcpy(aStars, "★✰✰✰✰"); break; - case 2: strcpy(aStars, "★★✰✰✰"); break; - case 3: strcpy(aStars, "★★★✰✰"); break; - case 4: strcpy(aStars, "★★★★✰"); break; - case 5: strcpy(aStars, "★★★★★"); break; - default: aStars[0] = '\0'; - } - - char aOwnFinishesString[40] = "\0"; - if(ownTime > 0) - { - str_format(aOwnFinishesString, sizeof(aOwnFinishesString), - ", your time: %02d:%05.2f", (int)(ownTime/60), ownTime-((int)ownTime/60*60) - ); - } - - str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), - "\"%s\" by %s on %s, %s, %d %s%s, %d %s by %d %s%s%s", - aMap, aMapper, aServer, aStars, - Points, Points == 1 ? "point" : "points", - aReleasedString, - finishes, finishes == 1 ? "finish" : "finishes", - finishers, finishers == 1 ? "tee" : "tees", - aAverageString, aOwnFinishesString - ); - } - pData->m_pResult->m_Done = true; - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not get Mapinfo"); - } - return false; -} - -void CSqlScore::SaveScore(int ClientID, float Time, const char *pTimestamp, float CpTime[NUM_CHECKPOINTS], bool NotEligible) -{ - CConsole* pCon = (CConsole*)GameServer()->Console(); - if(pCon->m_Cheated || NotEligible) - return; - - CPlayer *pCurPlayer = GameServer()->m_apPlayers[ClientID]; - if(pCurPlayer->m_ScoreFinishResult != nullptr) - dbg_msg("sql", "WARNING: previous save score result didn't complete, overwriting it now"); - pCurPlayer->m_ScoreFinishResult = std::make_shared(); - CSqlScoreData *Tmp = new CSqlScoreData(pCurPlayer->m_ScoreFinishResult); - Tmp->m_Map = g_Config.m_SvMap; - FormatUuid(GameServer()->GameUuid(), Tmp->m_GameUuid, sizeof(Tmp->m_GameUuid)); - Tmp->m_ClientID = ClientID; - Tmp->m_Name = Server()->ClientName(ClientID); - Tmp->m_Time = Time; - str_copy(Tmp->m_aTimestamp, pTimestamp, sizeof(Tmp->m_aTimestamp)); - for(int i = 0; i < NUM_CHECKPOINTS; i++) - Tmp->m_aCpCurrent[i] = CpTime[i]; - - thread_init_and_detach(CSqlExecData::ExecSqlFunc, - new CSqlExecData(SaveScoreThread, Tmp), - "save score"); -} - -bool CSqlScore::SaveScoreThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlScoreData *pData = dynamic_cast(pGameData); - auto paMessages = pData->m_pResult->m_Data.m_aaMessages; - - if(HandleFailure) - { - if(!g_Config.m_SvSqlFailureFile[0]) - return true; - - lock_wait(ms_FailureFileLock); - IOHANDLE File = io_open(g_Config.m_SvSqlFailureFile, IOFLAG_APPEND); - if(File == 0) - { - lock_unlock(ms_FailureFileLock); - dbg_msg("sql", "ERROR: Could not save Score, NOT even to a file"); - return false; - } - dbg_msg("sql", "ERROR: Could not save Score, writing insert to a file now..."); - - char aBuf[1024]; - str_format(aBuf, sizeof(aBuf), - "INSERT IGNORE INTO %%s_race(Map, Name, Timestamp, Time, Server, " - "cp1, cp2, cp3, cp4, cp5, cp6, cp7, cp8, cp9, cp10, cp11, cp12, cp13, " - "cp14, cp15, cp16, cp17, cp18, cp19, cp20, cp21, cp22, cp23, cp24, cp25, " - "GameID, DDNet7) " - "VALUES ('%s', '%s', '%s', '%.2f', '%s'," - "'%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', " - "'%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', " - "'%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', " - "'%.2f', '%s', false);", - pData->m_Map.ClrStr(), pData->m_Name.ClrStr(), - pData->m_aTimestamp, pData->m_Time, g_Config.m_SvSqlServerName, - pData->m_aCpCurrent[0], pData->m_aCpCurrent[1], pData->m_aCpCurrent[2], - pData->m_aCpCurrent[3], pData->m_aCpCurrent[4], pData->m_aCpCurrent[5], - pData->m_aCpCurrent[6], pData->m_aCpCurrent[7], pData->m_aCpCurrent[8], - pData->m_aCpCurrent[9], pData->m_aCpCurrent[10], pData->m_aCpCurrent[11], - pData->m_aCpCurrent[12], pData->m_aCpCurrent[13], pData->m_aCpCurrent[14], - pData->m_aCpCurrent[15], pData->m_aCpCurrent[16], pData->m_aCpCurrent[17], - pData->m_aCpCurrent[18], pData->m_aCpCurrent[19], pData->m_aCpCurrent[20], - pData->m_aCpCurrent[21], pData->m_aCpCurrent[22], pData->m_aCpCurrent[23], - pData->m_aCpCurrent[24], - pData->m_GameUuid); - io_write(File, aBuf, str_length(aBuf)); - io_write_newline(File); - io_close(File); - lock_unlock(ms_FailureFileLock); - - pData->m_pResult->SetVariant(CScorePlayerResult::BROADCAST); - strcpy(pData->m_pResult->m_Data.m_Broadcast, - "Database connection failed, score written to a file instead. Admins will add it manually in a few days."); - pData->m_pResult->m_Done = true; - return true; - } - - try - { - char aBuf[1024]; - - str_format(aBuf, sizeof(aBuf), - "SELECT COUNT(*) AS NumFinished FROM %s_race WHERE Map='%s' AND Name='%s' ORDER BY time ASC LIMIT 1;", - pSqlServer->GetPrefix(), pData->m_Map.ClrStr(), pData->m_Name.ClrStr()); - pSqlServer->executeSqlQuery(aBuf); - pSqlServer->GetResults()->first(); - int NumFinished = pSqlServer->GetResults()->getInt("NumFinished"); - if(NumFinished == 0) - { - str_format(aBuf, sizeof(aBuf), "SELECT Points FROM %s_maps WHERE Map='%s'", pSqlServer->GetPrefix(), pData->m_Map.ClrStr()); - pSqlServer->executeSqlQuery(aBuf); - - if(pSqlServer->GetResults()->rowsCount() == 1) - { - pSqlServer->GetResults()->next(); - int Points = pSqlServer->GetResults()->getInt("Points"); - str_format(paMessages[0], sizeof(paMessages[0]), "You earned %d point%s for finishing this map!", Points, Points == 1 ? "" : "s"); - - str_format(aBuf, sizeof(aBuf), - "INSERT INTO %s_points(Name, Points) " - "VALUES ('%s', '%d') " - "ON duplicate key " - "UPDATE Name=VALUES(Name), Points=Points+VALUES(Points);", - pSqlServer->GetPrefix(), pData->m_Name.ClrStr(), Points); - pSqlServer->executeSql(aBuf); - } - } - - // save score - str_format(aBuf, sizeof(aBuf), - "INSERT IGNORE INTO %s_race(" - "Map, Name, Timestamp, Time, Server, " - "cp1, cp2, cp3, cp4, cp5, cp6, cp7, cp8, cp9, cp10, cp11, cp12, cp13, " - "cp14, cp15, cp16, cp17, cp18, cp19, cp20, cp21, cp22, cp23, cp24, cp25, " - "GameID, DDNet7) " - "VALUES ('%s', '%s', '%s', '%.2f', '%s', " - "'%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', " - "'%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', " - "'%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', '%.2f', " - "'%s', false);", - pSqlServer->GetPrefix(), pData->m_Map.ClrStr(), pData->m_Name.ClrStr(), - pData->m_aTimestamp, pData->m_Time, g_Config.m_SvSqlServerName, - pData->m_aCpCurrent[0], pData->m_aCpCurrent[1], pData->m_aCpCurrent[2], - pData->m_aCpCurrent[3], pData->m_aCpCurrent[4], pData->m_aCpCurrent[5], - pData->m_aCpCurrent[6], pData->m_aCpCurrent[7], pData->m_aCpCurrent[8], - pData->m_aCpCurrent[9], pData->m_aCpCurrent[10], pData->m_aCpCurrent[11], - pData->m_aCpCurrent[12], pData->m_aCpCurrent[13], pData->m_aCpCurrent[14], - pData->m_aCpCurrent[15], pData->m_aCpCurrent[16], pData->m_aCpCurrent[17], - pData->m_aCpCurrent[18], pData->m_aCpCurrent[19], pData->m_aCpCurrent[20], - pData->m_aCpCurrent[21], pData->m_aCpCurrent[22], pData->m_aCpCurrent[23], - pData->m_aCpCurrent[24], pData->m_GameUuid); - dbg_msg("sql", "%s", aBuf); - pSqlServer->executeSql(aBuf); - - pData->m_pResult->m_Done = true; - dbg_msg("sql", "Saving score done"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not insert time"); - } - return false; -} - -void CSqlScore::SaveTeamScore(int* aClientIDs, unsigned int Size, float Time, const char *pTimestamp) -{ - CConsole* pCon = (CConsole*)GameServer()->Console(); - if(pCon->m_Cheated) - return; - for(unsigned int i = 0; i < Size; i++) - { - if(GameServer()->m_apPlayers[aClientIDs[i]]->m_NotEligibleForFinish) - return; - } - CSqlTeamScoreData *Tmp = new CSqlTeamScoreData(nullptr); - for(unsigned int i = 0; i < Size; i++) - Tmp->m_aNames[i] = Server()->ClientName(aClientIDs[i]); - Tmp->m_Size = Size; - Tmp->m_Time = Time; - str_copy(Tmp->m_aTimestamp, pTimestamp, sizeof(Tmp->m_aTimestamp)); - FormatUuid(GameServer()->GameUuid(), Tmp->m_GameUuid, sizeof(Tmp->m_GameUuid)); - Tmp->m_Map = g_Config.m_SvMap; - - thread_init_and_detach(CSqlExecData::ExecSqlFunc, - new CSqlExecData(SaveTeamScoreThread, Tmp), - "save team score"); -} - -bool CSqlScore::SaveTeamScoreThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlTeamScoreData *pData = dynamic_cast(pGameData); - - if(HandleFailure) - { - if(!g_Config.m_SvSqlFailureFile[0]) - return true; - - dbg_msg("sql", "ERROR: Could not save TeamScore, writing insert to a file now..."); - - lock_wait(ms_FailureFileLock); - IOHANDLE File = io_open(g_Config.m_SvSqlFailureFile, IOFLAG_APPEND); - if(File) - { - const char aUUID[] = "SET @id = UUID();"; - io_write(File, aUUID, sizeof(aUUID) - 1); - io_write_newline(File); - - char aBuf[2300]; - for(unsigned int i = 0; i < pData->m_Size; i++) - { - str_format(aBuf, sizeof(aBuf), "INSERT IGNORE INTO %%s_teamrace(Map, Name, Timestamp, Time, ID, GameID, DDNet7) VALUES ('%s', '%s', '%s', '%.2f', @id, '%s', false);", pData->m_Map.ClrStr(), pData->m_aNames[i].ClrStr(), pData->m_aTimestamp, pData->m_Time, pData->m_GameUuid); - io_write(File, aBuf, str_length(aBuf)); - io_write_newline(File); - } - io_close(File); - lock_unlock(ms_FailureFileLock); - return true; - } - lock_unlock(ms_FailureFileLock); - return false; - } - - try - { - char aBuf[2300]; - - // get the names sorted in a tab separated string - const sqlstr::CSqlString *apNames[MAX_CLIENTS]; - for(unsigned int i = 0; i < pData->m_Size; i++) - apNames[i] = &pData->m_aNames[i]; - std::sort(apNames, apNames+pData->m_Size); - char aSortedNames[2048] = {0}; - for(unsigned int i = 0; i < pData->m_Size; i++) - { - if(i != 0) - str_append(aSortedNames, "\t", sizeof(aSortedNames)); - str_append(aSortedNames, apNames[i]->ClrStr(), sizeof(aSortedNames)); - } - str_format(aBuf, sizeof(aBuf), - "SELECT l.ID, Time " - "FROM ((" // preselect teams with first name in team - "SELECT ID " - "FROM %s_teamrace " - "WHERE Map = '%s' AND Name = '%s' AND DDNet7 = false" - ") as l" - ") INNER JOIN %s_teamrace AS r ON l.ID = r.ID " - "GROUP BY ID " - "HAVING GROUP_CONCAT(Name ORDER BY Name SEPARATOR '\t') = '%s'", - pSqlServer->GetPrefix(), pData->m_Map.ClrStr(), pData->m_aNames[0].ClrStr(), - pSqlServer->GetPrefix(), aSortedNames); - pSqlServer->executeSqlQuery(aBuf); - - if (pSqlServer->GetResults()->rowsCount() > 0) - { - pSqlServer->GetResults()->first(); - float Time = (float)pSqlServer->GetResults()->getDouble("Time"); - auto ID = pSqlServer->GetResults()->getString("ID"); - dbg_msg("sql", "found team rank from same team (old time: %f, new time: %f)", Time, pData->m_Time); - if(pData->m_Time < Time) - { - str_format(aBuf, sizeof(aBuf), - "UPDATE %s_teamrace SET Time='%.2f', Timestamp='%s', DDNet7=false, GameID='%s' WHERE ID = '%s';", - pSqlServer->GetPrefix(), pData->m_Time, pData->m_aTimestamp, pData->m_GameUuid, ID.c_str()); - dbg_msg("sql", "%s", aBuf); - pSqlServer->executeSql(aBuf); - } - } - else - { - pSqlServer->executeSql("SET @id = UUID();"); - - for(unsigned int i = 0; i < pData->m_Size; i++) - { - // if no entry found... create a new one - str_format(aBuf, sizeof(aBuf), - "INSERT IGNORE INTO %s_teamrace(Map, Name, Timestamp, Time, ID, GameID, DDNet7) " - "VALUES ('%s', '%s', '%s', '%.2f', @id, '%s', false);", - pSqlServer->GetPrefix(), pData->m_Map.ClrStr(), pData->m_aNames[i].ClrStr(), - pData->m_aTimestamp, pData->m_Time, pData->m_GameUuid); - dbg_msg("sql", "%s", aBuf); - pSqlServer->executeSql(aBuf); - } - } - - dbg_msg("sql", "Updating team time done"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not update time"); - } - return false; -} - -void CSqlScore::ShowRank(int ClientID, const char* pName) -{ - if(RateLimitPlayer(ClientID)) - return; - ExecPlayerThread(ShowRankThread, "show rank", ClientID, pName, 0); -} - -bool CSqlScore::ShowRankThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlPlayerRequest *pData = dynamic_cast(pGameData); - if (HandleFailure) - { - pData->m_pResult->m_Done = true; - return true; - } - - try - { - // check sort method - char aBuf[600]; - - str_format(aBuf, sizeof(aBuf), - "SELECT Rank, Name, Time " - "FROM (" - "SELECT RANK() OVER w AS Rank, Name, MIN(Time) AS Time " - "FROM %s_race " - "WHERE Map = '%s' " - "GROUP BY Name " - "WINDOW w AS (ORDER BY Time)" - ") as a " - "WHERE Name = '%s';", - pSqlServer->GetPrefix(), - pData->m_Map.ClrStr(), - pData->m_Name.ClrStr() - ); - - pSqlServer->executeSqlQuery(aBuf); - - if(pSqlServer->GetResults()->rowsCount() != 1) - { - str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), - "%s is not ranked", pData->m_Name.Str()); - } - else - { - pSqlServer->GetResults()->next(); - - float Time = (float)pSqlServer->GetResults()->getDouble("Time"); - int Rank = pSqlServer->GetResults()->getInt("Rank"); - if(g_Config.m_SvHideScore) - { - str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), - "Your time: %02d:%05.2f", (int)(Time/60), Time-((int)Time/60*60)); - } - else - { - pData->m_pResult->m_MessageKind = CScorePlayerResult::ALL; - str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), - "%d. %s Time: %02d:%05.2f, requested by %s", - Rank, pSqlServer->GetResults()->getString("Name").c_str(), - (int)(Time/60), Time-((int)Time/60*60), pData->m_RequestingPlayer.Str()); - } - } - - pData->m_pResult->m_Done = true; - dbg_msg("sql", "Showing rank done"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not show rank"); - } - return false; -} - -void CSqlScore::ShowTeamRank(int ClientID, const char* pName) -{ - if(RateLimitPlayer(ClientID)) - return; - ExecPlayerThread(ShowTeamRankThread, "show team rank", ClientID, pName, 0); -} - -bool CSqlScore::ShowTeamRankThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlPlayerRequest *pData = dynamic_cast(pGameData); - if (HandleFailure) - { - pData->m_pResult->m_Done = true; - return true; - } - - try - { - // check sort method - char aBuf[2400]; - char aNames[2300]; - aNames[0] = '\0'; - - str_format(aBuf, sizeof(aBuf), - "SELECT Time, Rank, Name " - "FROM (" // teamrank score board - "SELECT RANK() OVER w AS Rank, Id " - "FROM %s_teamrace " - "WHERE Map = '%s' " - "GROUP BY Id " - "WINDOW w AS (ORDER BY Time)" - ") as l " - "INNER JOIN %s_teamrace as r ON l.ID = r.ID " - "WHERE l.ID = (" // find id for top teamrank of player - "SELECT Id " - "FROM %s_teamrace " - "WHERE Map = '%s' AND Name = '%s' " - "ORDER BY Time " - "LIMIT 1" - ") " - "ORDER BY Name;", - pSqlServer->GetPrefix(), - pData->m_Map.ClrStr(), - pSqlServer->GetPrefix(), - pSqlServer->GetPrefix(), - pData->m_Map.ClrStr(), - pData->m_Name.ClrStr() - ); - - pSqlServer->executeSqlQuery(aBuf); - - int Rows = pSqlServer->GetResults()->rowsCount(); - - if(Rows < 1) - { - str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), - "%s has no team ranks", pData->m_Name.Str()); - } - else - { - pSqlServer->GetResults()->first(); - float Time = (float)pSqlServer->GetResults()->getDouble("Time"); - int Rank = pSqlServer->GetResults()->getInt("Rank"); - - for(int Row = 0; Row < Rows; Row++) - { - str_append(aNames, pSqlServer->GetResults()->getString("Name").c_str(), sizeof(aNames)); - pSqlServer->GetResults()->next(); - - if (Row < Rows - 2) - str_append(aNames, ", ", sizeof(aNames)); - else if (Row < Rows - 1) - str_append(aNames, " & ", sizeof(aNames)); - } - - if(g_Config.m_SvHideScore) - { - str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), - "Your team time: %02d:%05.02f", (int)(Time/60), Time-((int)Time/60*60)); - } - else - { - pData->m_pResult->m_MessageKind = CScorePlayerResult::ALL; - str_format(pData->m_pResult->m_Data.m_aaMessages[0], sizeof(pData->m_pResult->m_Data.m_aaMessages[0]), - "%d. %s Team time: %02d:%05.02f, requested by %s", - Rank, aNames, (int)(Time/60), Time-((int)Time/60*60), pData->m_RequestingPlayer.Str()); - } - } - - pData->m_pResult->m_Done = true; - dbg_msg("sql", "Showing teamrank done"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not show team rank"); - } - return false; -} - -void CSqlScore::ShowTop5(int ClientID, int Offset) -{ - if(RateLimitPlayer(ClientID)) - return; - ExecPlayerThread(ShowTop5Thread, "show top5", ClientID, "", Offset); -} - -bool CSqlScore::ShowTop5Thread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlPlayerRequest *pData = dynamic_cast(pGameData); - if (HandleFailure) - return true; - - int LimitStart = maximum(abs(pData->m_Offset)-1, 0); - const char *pOrder = pData->m_Offset >= 0 ? "ASC" : "DESC"; - - try - { - // check sort method - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), - "SELECT Name, Time, Rank " - "FROM (" - "SELECT RANK() OVER w AS Rank, Name, MIN(Time) AS Time " - "FROM %s_race " - "WHERE Map = '%s' " - "GROUP BY Name " - "WINDOW w AS (ORDER BY Time)" - ") as a " - "ORDER BY Rank %s " - "LIMIT %d, 5;", - pSqlServer->GetPrefix(), - pData->m_Map.ClrStr(), - pOrder, - LimitStart - ); - pSqlServer->executeSqlQuery(aBuf); - - // show top5 - strcpy(pData->m_pResult->m_Data.m_aaMessages[0], "----------- Top 5 -----------"); - - int Line = 1; - while(pSqlServer->GetResults()->next()) - { - float Time = (float)pSqlServer->GetResults()->getDouble("Time"); - int Rank = pSqlServer->GetResults()->getInt("Rank"); - str_format(pData->m_pResult->m_Data.m_aaMessages[Line], sizeof(pData->m_pResult->m_Data.m_aaMessages[Line]), - "%d. %s Time: %02d:%05.2f", - Rank, pSqlServer->GetResults()->getString("Name").c_str(), - (int)(Time/60), Time-((int)Time/60*60) - ); - Line++; - } - strcpy(pData->m_pResult->m_Data.m_aaMessages[Line], "-------------------------------"); - - pData->m_pResult->m_Done = true; - dbg_msg("sql", "Showing top5 done"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not show top5"); - } - - return false; -} - -void CSqlScore::ShowTeamTop5(int ClientID, int Offset) -{ - if(RateLimitPlayer(ClientID)) - return; - ExecPlayerThread(ShowTeamTop5Thread, "show team top5", ClientID, "", Offset); -} - -bool CSqlScore::ShowTeamTop5Thread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlPlayerRequest *pData = dynamic_cast(pGameData); - auto paMessages = pData->m_pResult->m_Data.m_aaMessages; - if (HandleFailure) - return true; - - int LimitStart = maximum(abs(pData->m_Offset)-1, 0); - const char *pOrder = pData->m_Offset >= 0 ? "ASC" : "DESC"; - - try - { - // check sort method - char aBuf[512]; - - str_format(aBuf, sizeof(aBuf), - "SELECT Name, Time, Rank, TeamSize " - "FROM (" // limit to 5 - "SELECT Rank, ID, TeamSize " - "FROM (" // teamrank score board - "SELECT RANK() OVER w AS Rank, ID, COUNT(*) AS Teamsize " - "FROM %s_teamrace " - "WHERE Map = '%s' " - "GROUP BY Id " - "WINDOW w AS (ORDER BY Time)" - ") as l1 " - "ORDER BY Rank %s " - "LIMIT %d, 5" - ") as l2 " - "INNER JOIN %s_teamrace as r ON l2.ID = r.ID " - "ORDER BY Rank %s, r.ID, Name ASC;", - pSqlServer->GetPrefix(), pData->m_Map.ClrStr(), pOrder, LimitStart, pSqlServer->GetPrefix(), pOrder - ); - pSqlServer->executeSqlQuery(aBuf); - - // show teamtop5 - int Line = 0; - strcpy(paMessages[Line++], "------- Team Top 5 -------"); - - int Rows = pSqlServer->GetResults()->rowsCount(); - - if(Rows > 0) - { - pSqlServer->GetResults()->first(); - - for(Line = 1; Line < 6; Line++) // print - { - if(pSqlServer->GetResults()->isAfterLast()) - break; - int TeamSize = pSqlServer->GetResults()->getInt("TeamSize"); - float Time = (float)pSqlServer->GetResults()->getDouble("Time"); - int Rank = pSqlServer->GetResults()->getInt("Rank"); - - char aNames[2300] = { 0 }; - for(int i = 0; i < TeamSize; i++) - { - auto Name = pSqlServer->GetResults()->getString("Name"); - str_append(aNames, Name.c_str(), sizeof(aNames)); - if (i < TeamSize - 2) - str_append(aNames, ", ", sizeof(aNames)); - else if (i == TeamSize - 2) - str_append(aNames, " & ", sizeof(aNames)); - pSqlServer->GetResults()->next(); - } - str_format(paMessages[Line], sizeof(paMessages[Line]), "%d. %s Team Time: %02d:%05.2f", - Rank, aNames, (int)(Time/60), Time-((int)Time/60*60)); - } - } - - strcpy(paMessages[Line], "-------------------------------"); - - pData->m_pResult->m_Done = true; - dbg_msg("sql", "Showing teamtop5 done"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not show teamtop5"); - } - return false; -} - -void CSqlScore::ShowTimes(int ClientID, int Offset) -{ - if(RateLimitPlayer(ClientID)) - return; - ExecPlayerThread(ShowTimesThread, "show times", ClientID, "", Offset); -} - -void CSqlScore::ShowTimes(int ClientID, const char* pName, int Offset) -{ - if(RateLimitPlayer(ClientID)) - return; - ExecPlayerThread(ShowTimesThread, "show times", ClientID, pName, Offset); -} - -bool CSqlScore::ShowTimesThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlPlayerRequest *pData = dynamic_cast(pGameData); - auto paMessages = pData->m_pResult->m_Data.m_aaMessages; - - if (HandleFailure) - return true; - - int LimitStart = maximum(abs(pData->m_Offset)-1, 0); - const char *pOrder = pData->m_Offset >= 0 ? "DESC" : "ASC"; - - try - { - char aBuf[512]; - - if(pData->m_Name.Str()[0] != '\0') // last 5 times of a player - { - str_format(aBuf, sizeof(aBuf), - "SELECT Time, UNIX_TIMESTAMP(CURRENT_TIMESTAMP)-UNIX_TIMESTAMP(Timestamp) as Ago, UNIX_TIMESTAMP(Timestamp) as Stamp " - "FROM %s_race " - "WHERE Map = '%s' AND Name = '%s' " - "ORDER BY Timestamp %s " - "LIMIT %d, 5;", - pSqlServer->GetPrefix(), pData->m_Map.ClrStr(), pData->m_Name.ClrStr(), pOrder, LimitStart - ); - } - else // last 5 times of server - { - str_format(aBuf, sizeof(aBuf), - "SELECT Name, Time, " - "UNIX_TIMESTAMP(CURRENT_TIMESTAMP)-UNIX_TIMESTAMP(Timestamp) as Ago, " - "UNIX_TIMESTAMP(Timestamp) as Stamp " - "FROM %s_race " - "WHERE Map = '%s' " - "ORDER BY Timestamp %s " - "LIMIT %d, 5;", - pSqlServer->GetPrefix(), pData->m_Map.ClrStr(), pOrder, LimitStart - ); - } - pSqlServer->executeSqlQuery(aBuf); - - // show top5 - if(pSqlServer->GetResults()->rowsCount() == 0) - { - strcpy(paMessages[0], "There are no times in the specified range"); - pData->m_pResult->m_Done = true; - return true; - } - - strcpy(paMessages[0], "------------- Last Times -------------"); - int Line = 1; - while(pSqlServer->GetResults()->next()) - { - char aAgoString[40] = "\0"; - int pSince = pSqlServer->GetResults()->getInt("Ago"); - int pStamp = pSqlServer->GetResults()->getInt("Stamp"); - float pTime = (float)pSqlServer->GetResults()->getDouble("Time"); - - sqlstr::AgoTimeToString(pSince, aAgoString); - - if(pData->m_Name.Str()[0] != '\0') // last 5 times of a player - { - if(pStamp == 0) // stamp is 00:00:00 cause it's an old entry from old times where there where no stamps yet - str_format(paMessages[Line], sizeof(paMessages[Line]), - "%02d:%05.02f, don't know how long ago", - (int)(pTime/60), pTime-((int)pTime/60*60)); - else - str_format(paMessages[Line], sizeof(paMessages[Line]), - "%s ago, %02d:%05.02f", - aAgoString, (int)(pTime/60), pTime-((int)pTime/60*60)); - } - else // last 5 times of the server - { - auto Name = pSqlServer->GetResults()->getString("Name"); - if(pStamp == 0) // stamp is 00:00:00 cause it's an old entry from old times where there where no stamps yet - str_format(paMessages[Line], sizeof(paMessages[Line]), - "%s, %02d:%05.02f, don't know when", - Name.c_str(), (int)(pTime/60), pTime-((int)pTime/60*60)); - else - str_format(paMessages[Line], sizeof(paMessages[Line]), - "%s, %s ago, %02d:%05.02f", - Name.c_str(), aAgoString, (int)(pTime/60), pTime-((int)pTime/60*60)); - } - Line++; - } - strcpy(paMessages[Line], "----------------------------------------------------"); - - pData->m_pResult->m_Done = true; - dbg_msg("sql", "Showing times done"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not show times"); - } - return false; -} - -void CSqlScore::ShowPoints(int ClientID, const char* pName) -{ - if(RateLimitPlayer(ClientID)) - return; - ExecPlayerThread(ShowPointsThread, "show points", ClientID, pName, 0); -} - -bool CSqlScore::ShowPointsThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlPlayerRequest *pData = dynamic_cast(pGameData); - auto paMessages = pData->m_pResult->m_Data.m_aaMessages; - - if (HandleFailure) - return true; - - try - { - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), - "SELECT Rank, Points, Name " - "FROM (" - "SELECT RANK() OVER w AS Rank, Points, Name " - "FROM %s_points " - "WINDOW w as (ORDER BY Points DESC)" - ") as a " - "WHERE Name = '%s';", - pSqlServer->GetPrefix(), pData->m_Name.ClrStr() - ); - pSqlServer->executeSqlQuery(aBuf); - - if(pSqlServer->GetResults()->rowsCount() != 1) - { - str_format(paMessages[0], sizeof(paMessages[0]), - "%s has not collected any points so far", pData->m_Name.Str()); - } - else - { - pSqlServer->GetResults()->next(); - int Count = pSqlServer->GetResults()->getInt("Points"); - int Rank = pSqlServer->GetResults()->getInt("Rank"); - auto Name = pSqlServer->GetResults()->getString("Name"); - pData->m_pResult->m_MessageKind = CScorePlayerResult::ALL; - str_format(paMessages[0], sizeof(paMessages[0]), - "%d. %s Points: %d, requested by %s", - Rank, Name.c_str(), Count, pData->m_RequestingPlayer.Str()); - } - - pData->m_pResult->m_Done = true; - dbg_msg("sql", "Showing points done"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not show points"); - } - return false; -} - -void CSqlScore::ShowTopPoints(int ClientID, int Offset) -{ - if(RateLimitPlayer(ClientID)) - return; - ExecPlayerThread(ShowTopPointsThread, "show top points", ClientID, "", Offset); -} - -bool CSqlScore::ShowTopPointsThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlPlayerRequest *pData = dynamic_cast(pGameData); - auto paMessages = pData->m_pResult->m_Data.m_aaMessages; - - if (HandleFailure) - return true; - - int LimitStart = maximum(abs(pData->m_Offset)-1, 0); - const char *pOrder = pData->m_Offset >= 0 ? "ASC" : "DESC"; - - try - { - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), - "SELECT Rank, Points, Name " - "FROM (" - "SELECT RANK() OVER w AS Rank, Points, Name " - "FROM %s_points " - "WINDOW w as (ORDER BY Points DESC)" - ") as a " - "ORDER BY Rank %s " - "LIMIT %d, 5;", - pSqlServer->GetPrefix(), pOrder, LimitStart - ); - - pSqlServer->executeSqlQuery(aBuf); - - // show top points - strcpy(paMessages[0], "-------- Top Points --------"); - - int Line = 1; - while(pSqlServer->GetResults()->next()) - { - int Rank = pSqlServer->GetResults()->getInt("Rank"); - auto Name = pSqlServer->GetResults()->getString("Name"); - int Points = pSqlServer->GetResults()->getInt("Points"); - str_format(paMessages[Line], sizeof(paMessages[Line]), - "%d. %s Points: %d", Rank, Name.c_str(), Points); - Line++; - } - strcpy(paMessages[Line], "-------------------------------"); - - pData->m_pResult->m_Done = true; - dbg_msg("sql", "Showing toppoints done"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not show toppoints"); - } - return false; -} - -void CSqlScore::RandomMap(int ClientID, int Stars) -{ - auto pResult = std::make_shared(ClientID); - GameServer()->m_SqlRandomMapResult = pResult; - - auto *Tmp = new CSqlRandomMapRequest(pResult); - Tmp->m_Stars = Stars; - Tmp->m_CurrentMap = g_Config.m_SvMap; - Tmp->m_ServerType = g_Config.m_SvServerType; - Tmp->m_RequestingPlayer = GameServer()->Server()->ClientName(ClientID); - - thread_init_and_detach( - CSqlExecData::ExecSqlFunc, - new CSqlExecData(RandomMapThread, Tmp), - "random map"); -} - -bool CSqlScore::RandomMapThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlRandomMapRequest *pData = dynamic_cast(pGameData); - - if (HandleFailure) - return true; - - try - { - char aBuf[512]; - if(0 <= pData->m_Stars && pData->m_Stars <= 5) - { - str_format(aBuf, sizeof(aBuf), - "SELECT * FROM %s_maps " - "WHERE Server = \"%s\" AND Map != \"%s\" AND Stars = \"%d\" " - "ORDER BY RAND() LIMIT 1;", - pSqlServer->GetPrefix(), - pData->m_ServerType.ClrStr(), - pData->m_CurrentMap.ClrStr(), - pData->m_Stars - ); - } - else - { - str_format(aBuf, sizeof(aBuf), - "SELECT * FROM %s_maps " - "WHERE Server = \"%s\" AND Map != \"%s\" " - "ORDER BY RAND() LIMIT 1;", - pSqlServer->GetPrefix(), - pData->m_ServerType.ClrStr(), - pData->m_CurrentMap.ClrStr() - ); - } - pSqlServer->executeSqlQuery(aBuf); - - if(pSqlServer->GetResults()->rowsCount() != 1) - { - str_copy(pData->m_pResult->m_aMessage, "No maps found on this server!", sizeof(pData->m_pResult->m_aMessage)); - } - else - { - pSqlServer->GetResults()->next(); - auto Map = pSqlServer->GetResults()->getString("Map"); - str_copy(pData->m_pResult->m_Map, Map.c_str(), sizeof(pData->m_pResult->m_Map)); - } - - dbg_msg("sql", "voting random map done"); - pData->m_pResult->m_Done = true; - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not vote random map"); - } - return false; -} - -void CSqlScore::RandomUnfinishedMap(int ClientID, int Stars) -{ - auto pResult = std::make_shared(ClientID); - GameServer()->m_SqlRandomMapResult = pResult; - - auto *Tmp = new CSqlRandomMapRequest(pResult); - Tmp->m_Stars = Stars; - Tmp->m_CurrentMap = g_Config.m_SvMap; - Tmp->m_ServerType = g_Config.m_SvServerType; - Tmp->m_RequestingPlayer = GameServer()->Server()->ClientName(ClientID); - - thread_init_and_detach( - CSqlExecData::ExecSqlFunc, - new CSqlExecData(RandomUnfinishedMapThread, Tmp), - "random unfinished map"); -} - -bool CSqlScore::RandomUnfinishedMapThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlRandomMapRequest *pData = dynamic_cast(pGameData); - - if (HandleFailure) - return true; - - try - { - char aBuf[512]; - if(pData->m_Stars >= 0) - { - str_format(aBuf, sizeof(aBuf), - "SELECT Map " - "FROM %s_maps " - "WHERE Server = \"%s\" AND Map != \"%s\" AND Stars = \"%d\" AND Map NOT IN (" - "SELECT Map " - "FROM %s_race " - "WHERE Name = \"%s\"" - ") ORDER BY RAND() " - "LIMIT 1;", - pSqlServer->GetPrefix(), pData->m_ServerType.ClrStr(), pData->m_CurrentMap.ClrStr(), - pData->m_Stars, pSqlServer->GetPrefix(), pData->m_RequestingPlayer.ClrStr()); - } - else - { - str_format(aBuf, sizeof(aBuf), - "SELECT Map " - "FROM %s_maps AS maps " - "WHERE Server = \"%s\" AND Map != \"%s\" AND Map NOT IN (" - "SELECT Map " - "FROM %s_race as race " - "WHERE Name = \"%s\"" - ") ORDER BY RAND() " - "LIMIT 1;", - pSqlServer->GetPrefix(), pData->m_ServerType.ClrStr(), pData->m_CurrentMap.ClrStr(), - pSqlServer->GetPrefix(), pData->m_RequestingPlayer.ClrStr()); - } - pSqlServer->executeSqlQuery(aBuf); - - if(pSqlServer->GetResults()->rowsCount() != 1) - { - str_copy(pData->m_pResult->m_aMessage, "You have no more unfinished maps on this server!", sizeof(pData->m_pResult->m_aMessage)); - } - else - { - pSqlServer->GetResults()->next(); - auto Map = pSqlServer->GetResults()->getString("Map"); - str_copy(pData->m_pResult->m_Map, Map.c_str(), sizeof(pData->m_pResult->m_Map)); - } - - pData->m_pResult->m_Done = true; - dbg_msg("sql", "voting random unfinished map done"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not vote random unfinished map"); - } - return false; -} - -void CSqlScore::SaveTeam(int ClientID, const char* Code, const char* Server) -{ - if(RateLimitPlayer(ClientID)) - return; - auto pController = ((CGameControllerDDRace*)(GameServer()->m_pController)); - int Team = pController->m_Teams.m_Core.Team(ClientID); - if(pController->m_Teams.GetSaving(Team)) - return; - - auto SaveResult = std::make_shared(ClientID, pController); - int Result = SaveResult->m_SavedTeam.save(Team); - if(CSaveTeam::HandleSaveError(Result, ClientID, GameServer())) - return; - pController->m_Teams.SetSaving(Team, SaveResult); - - CSqlTeamSave *Tmp = new CSqlTeamSave(SaveResult); - str_copy(Tmp->m_Code, Code, sizeof(Tmp->m_Code)); - str_copy(Tmp->m_Map, g_Config.m_SvMap, sizeof(Tmp->m_Map)); - Tmp->m_pResult->m_SaveID = RandomUuid(); - str_copy(Tmp->m_Server, Server, sizeof(Tmp->m_Server)); - str_copy(Tmp->m_ClientName, this->Server()->ClientName(ClientID), sizeof(Tmp->m_ClientName)); - Tmp->m_aGeneratedCode[0] = '\0'; - GeneratePassphrase(Tmp->m_aGeneratedCode, sizeof(Tmp->m_aGeneratedCode)); - - pController->m_Teams.KillSavedTeam(ClientID, Team); - thread_init_and_detach( - CSqlExecData::ExecSqlFunc, - new CSqlExecData(SaveTeamThread, Tmp, false), - "save team"); -} - -bool CSqlScore::SaveTeamThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlTeamSave *pData = dynamic_cast(pGameData); - - char aSaveID[UUID_MAXSTRSIZE]; - FormatUuid(pData->m_pResult->m_SaveID, aSaveID, UUID_MAXSTRSIZE); - - char *pSaveState = pData->m_pResult->m_SavedTeam.GetString(); - if(HandleFailure) - { - if (!g_Config.m_SvSqlFailureFile[0]) - return true; - - lock_wait(ms_FailureFileLock); - IOHANDLE File = io_open(g_Config.m_SvSqlFailureFile, IOFLAG_APPEND); - if(File) - { - dbg_msg("sql", "ERROR: Could not save Teamsave, writing insert to a file now..."); - sqlstr::CSqlString<65536> SaveState = pSaveState; - sqlstr::CSqlString<128> Code = pData->m_aGeneratedCode; - sqlstr::CSqlString<128> Map = pData->m_Map; - - char aBuf[65536]; - str_format(aBuf, sizeof(aBuf), - "INSERT IGNORE INTO %%s_saves(Savegame, Map, Code, Timestamp, Server, SaveID, DDNet7) " - "VALUES ('%s', '%s', '%s', CURRENT_TIMESTAMP(), '%s', '%s', false);", - SaveState.ClrStr(), Map.ClrStr(), - Code.ClrStr(), pData->m_Server, aSaveID - ); - io_write(File, aBuf, str_length(aBuf)); - io_write_newline(File); - io_close(File); - lock_unlock(ms_FailureFileLock); - - pData->m_pResult->m_Status = CScoreSaveResult::SAVE_SUCCESS; - strcpy(pData->m_pResult->m_aBroadcast, - "Database connection failed, teamsave written to a file instead. Admins will add it manually in a few days."); - if(str_comp(pData->m_Server, g_Config.m_SvSqlServerName) == 0) - { - str_format(pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage), - "Team successfully saved by %s. The database connection failed, using generated save code instead to avoid collisions. Use '/load %s' to continue", - pData->m_ClientName, Code.ClrStr()); - } - else - { - str_format(pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage), - "Team successfully saved by %s. The database connection failed, using generated save code instead to avoid collisions. Use '/load %s' on %s to continue", - pData->m_ClientName, Code.ClrStr(), pData->m_Server); - } - return true; - } - lock_unlock(ms_FailureFileLock); - dbg_msg("sql", "ERROR: Could not save Teamsave, NOT even to a file"); - return false; - } - - try - { - char aBuf[65536]; - str_format(aBuf, sizeof(aBuf), "lock tables %s_saves write;", pSqlServer->GetPrefix()); - pSqlServer->executeSql(aBuf); - - char Code[128] = {0}; - str_format(aBuf, sizeof(aBuf), "SELECT Savegame FROM %s_saves WHERE Code = ? AND Map = ?", pSqlServer->GetPrefix()); - std::unique_ptr pPrepStmt; - std::unique_ptr pResult; - pPrepStmt.reset(pSqlServer->Connection()->prepareStatement(aBuf)); - bool UseCode = false; - if(pData->m_Code[0] != '\0') - { - pPrepStmt->setString(1, pData->m_Code); - pPrepStmt->setString(2, pData->m_Map); - pResult.reset(pPrepStmt->executeQuery()); - if(pResult->rowsCount() == 0) - { - UseCode = true; - str_copy(Code, pData->m_Code, sizeof(Code)); - } - } - if(!UseCode) - { - // use random generated passphrase if save code exists or no save code given - pPrepStmt->setString(1, pData->m_aGeneratedCode); - pPrepStmt->setString(2, pData->m_Map); - pResult.reset(pPrepStmt->executeQuery()); - if(pResult->rowsCount() == 0) - { - UseCode = true; - str_copy(Code, pData->m_aGeneratedCode, sizeof(Code)); - } - } - - if(UseCode) - { - str_format(aBuf, sizeof(aBuf), - "INSERT IGNORE INTO %s_saves(Savegame, Map, Code, Timestamp, Server, SaveID, DDNet7) " - "VALUES (?, ?, ?, CURRENT_TIMESTAMP(), ?, ?, false)", - pSqlServer->GetPrefix()); - pPrepStmt.reset(pSqlServer->Connection()->prepareStatement(aBuf)); - pPrepStmt->setString(1, pSaveState); - pPrepStmt->setString(2, pData->m_Map); - pPrepStmt->setString(3, Code); - pPrepStmt->setString(4, pData->m_Server); - pPrepStmt->setString(5, aSaveID); - dbg_msg("sql", "%s", aBuf); - pPrepStmt->execute(); - - if(str_comp(pData->m_Server, g_Config.m_SvSqlServerName) == 0) - { - str_format(pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage), - "Team successfully saved by %s. Use '/load %s' to continue", - pData->m_ClientName, Code); - } - else - { - str_format(pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage), - "Team successfully saved by %s. Use '/load %s' on %s to continue", - pData->m_ClientName, Code, pData->m_Server); - } - pData->m_pResult->m_Status = CScoreSaveResult::SAVE_SUCCESS; - } - else - { - dbg_msg("sql", "ERROR: This save-code already exists"); - pData->m_pResult->m_Status = CScoreSaveResult::SAVE_FAILED; - strcpy(pData->m_pResult->m_aMessage, "This save-code already exists"); - } - } - catch (sql::SQLException &e) - { - pData->m_pResult->m_Status = CScoreSaveResult::SAVE_FAILED; - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not save the team"); - - strcpy(pData->m_pResult->m_aMessage, "MySQL Error: Could not save the team"); - pSqlServer->executeSql("unlock tables;"); - return false; - } - - pSqlServer->executeSql("unlock tables;"); - return true; -} - -void CSqlScore::LoadTeam(const char* Code, int ClientID) -{ - if(RateLimitPlayer(ClientID)) - return; - auto pController = ((CGameControllerDDRace*)(GameServer()->m_pController)); - int Team = pController->m_Teams.m_Core.Team(ClientID); - if(pController->m_Teams.GetSaving(Team)) - return; - if(Team < TEAM_FLOCK || Team >= MAX_CLIENTS || (g_Config.m_SvTeam != 3 && Team == TEAM_FLOCK)) - { - GameServer()->SendChatTarget(ClientID, "You have to be in a team (from 1-63)"); - return; - } - if(pController->m_Teams.GetTeamState(Team) != CGameTeams::TEAMSTATE_OPEN) - { - GameServer()->SendChatTarget(ClientID, "Team can't be loaded while racing"); - return; - } - auto SaveResult = std::make_shared(ClientID, pController); - pController->m_Teams.SetSaving(Team, SaveResult); - CSqlTeamLoad *Tmp = new CSqlTeamLoad(SaveResult); - Tmp->m_Code = Code; - Tmp->m_Map = g_Config.m_SvMap; - Tmp->m_ClientID = ClientID; - Tmp->m_RequestingPlayer = Server()->ClientName(ClientID); - Tmp->m_NumPlayer = 0; - for(int i = 0; i < MAX_CLIENTS; i++) - { - if(pController->m_Teams.m_Core.Team(i) == Team) - { - // put all names at the beginning of the array - str_copy(Tmp->m_aClientNames[Tmp->m_NumPlayer], Server()->ClientName(i), sizeof(Tmp->m_aClientNames[Tmp->m_NumPlayer])); - Tmp->m_aClientID[Tmp->m_NumPlayer] = i; - Tmp->m_NumPlayer++; - } - } - thread_init_and_detach( - CSqlExecData::ExecSqlFunc, - new CSqlExecData(LoadTeamThread, Tmp, false), - "load team"); -} - -bool CSqlScore::LoadTeamThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlTeamLoad *pData = dynamic_cast(pGameData); - pData->m_pResult->m_Status = CScoreSaveResult::LOAD_FAILED; - - if (HandleFailure) - return true; - - try - { - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "lock tables %s_saves write;", pSqlServer->GetPrefix()); - pSqlServer->executeSql(aBuf); - str_format(aBuf, sizeof(aBuf), - "SELECT " - "Savegame, Server, " - "UNIX_TIMESTAMP(CURRENT_TIMESTAMP)-UNIX_TIMESTAMP(Timestamp) AS Ago, " - "(UNHEX(REPLACE(SaveID, '-',''))) AS SaveID " - "FROM %s_saves " - "where Code = '%s' AND Map = '%s' AND DDNet7 = false AND Savegame LIKE '%%\\n%s\\t%%';", - pSqlServer->GetPrefix(), pData->m_Code.ClrStr(), pData->m_Map.ClrStr(), pData->m_RequestingPlayer.ClrStr()); - pSqlServer->executeSqlQuery(aBuf); - - if(pSqlServer->GetResults()->rowsCount() == 0) - { - strcpy(pData->m_pResult->m_aMessage, "No such savegame for this map"); - goto end; - } - pSqlServer->GetResults()->first(); - auto ServerName = pSqlServer->GetResults()->getString("Server"); - if(str_comp(ServerName.c_str(), g_Config.m_SvSqlServerName) != 0) - { - str_format(pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage), - "You have to be on the '%s' server to load this savegame", ServerName.c_str()); - goto end; - } - - int Since = pSqlServer->GetResults()->getInt("Ago"); - if(Since < g_Config.m_SvSaveGamesDelay) - { - str_format(pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage), - "You have to wait %d seconds until you can load this savegame", - g_Config.m_SvSaveGamesDelay - Since); - goto end; - } - if(pSqlServer->GetResults()->isNull("SaveID")) - { - memset(pData->m_pResult->m_SaveID.m_aData, 0, sizeof(pData->m_pResult->m_SaveID.m_aData)); - } - else - { - auto SaveID = pSqlServer->GetResults()->getBlob("SaveID"); - SaveID->read((char *) pData->m_pResult->m_SaveID.m_aData, 16); - if(SaveID->gcount() != 16) - { - strcpy(pData->m_pResult->m_aMessage, "Unable to load savegame: SaveID corrupted"); - goto end; - } - } - - auto SaveString = pSqlServer->GetResults()->getString("Savegame"); - int Num = pData->m_pResult->m_SavedTeam.LoadString(SaveString.c_str()); - - if(Num != 0) - { - strcpy(pData->m_pResult->m_aMessage, "Unable to load savegame: data corrupted"); - goto end; - } - - bool CanLoad = pData->m_pResult->m_SavedTeam.MatchPlayers( - pData->m_aClientNames, pData->m_aClientID, pData->m_NumPlayer, - pData->m_pResult->m_aMessage, sizeof(pData->m_pResult->m_aMessage)); - - if(!CanLoad) - goto end; - - str_format(aBuf, sizeof(aBuf), - "DELETE FROM %s_saves " - "WHERE Code='%s' AND Map='%s';", - pSqlServer->GetPrefix(), pData->m_Code.ClrStr(), pData->m_Map.ClrStr()); - pSqlServer->executeSql(aBuf); - - pData->m_pResult->m_Status = CScoreSaveResult::LOAD_SUCCESS; - strcpy(pData->m_pResult->m_aMessage, "Loading successfully done"); - - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not load the team"); - strcpy(pData->m_pResult->m_aMessage, "MySQL Error: Could not load the team"); - pSqlServer->executeSql("unlock tables;"); - - return false; - } - -end: - pSqlServer->executeSql("unlock tables;"); - return true; -} - -void CSqlScore::GetSaves(int ClientID) -{ - if(RateLimitPlayer(ClientID)) - return; - ExecPlayerThread(GetSavesThread, "get saves", ClientID, "", 0); -} - -bool CSqlScore::GetSavesThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure) -{ - const CSqlPlayerRequest *pData = dynamic_cast(pGameData); - auto paMessages = pData->m_pResult->m_Data.m_aaMessages; - - if (HandleFailure) - return true; - - try - { - char aBuf[512]; - - str_format(aBuf, sizeof(aBuf), - "SELECT COUNT(*) as NumSaves, " - "UNIX_TIMESTAMP(CURRENT_TIMESTAMP)-UNIX_TIMESTAMP(max(Timestamp)) as Ago " - "FROM %s_saves " - "WHERE Map='%s' AND Savegame LIKE '%%\\n%s\\t%%';", - pSqlServer->GetPrefix(), - pData->m_Map.ClrStr(), - pData->m_RequestingPlayer.ClrStr() - ); - pSqlServer->executeSqlQuery(aBuf); - if(pSqlServer->GetResults()->next()) - { - int NumSaves = pSqlServer->GetResults()->getInt("NumSaves"); - - int Ago = pSqlServer->GetResults()->getInt("Ago"); - char aAgoString[40] = "\0"; - char aLastSavedString[60] = "\0"; - if(Ago) - { - sqlstr::AgoTimeToString(Ago, aAgoString); - str_format(aLastSavedString, sizeof(aLastSavedString), ", last saved %s ago", aAgoString); - } - - str_format(paMessages[0], sizeof(paMessages[0]), - "%s has %d save%s on %s%s", - pData->m_RequestingPlayer.Str(), - NumSaves, NumSaves == 1 ? "" : "s", - pData->m_Map.Str(), aLastSavedString); - } - - pData->m_pResult->m_Done = true; - dbg_msg("sql", "Showing saves done"); - return true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - dbg_msg("sql", "ERROR: Could not get saves"); - } - return false; -} - -#endif diff --git a/src/game/server/score/sql_score.h b/src/game/server/score/sql_score.h deleted file mode 100644 index e72e08f53..000000000 --- a/src/game/server/score/sql_score.h +++ /dev/null @@ -1,220 +0,0 @@ -/* (c) Shereef Marzouk. See "licence DDRace.txt" and the readme.txt in the root of the distribution for more information. */ -/* Based on Race mod stuff and tweaked by GreYFoX@GTi and others to fit our DDRace needs. */ -/* CSqlScore Class by Sushi Tee*/ -#ifndef GAME_SERVER_SCORE_SQL_H -#define GAME_SERVER_SCORE_SQL_H - -#include -#include - -#include -#include -#include - -#include "../score.h" - -class CSqlServer; - -// holding relevant data for one thread, and function pointer for return values -template < typename TResult > -struct CSqlData -{ - CSqlData(std::shared_ptr pSqlResult) : - m_pResult(pSqlResult) - { } - std::shared_ptr m_pResult; - virtual ~CSqlData() = default; -}; - -struct CSqlInitData : CSqlData -{ - using CSqlData::CSqlData; - // current map - sqlstr::CSqlString m_Map; -}; - -struct CSqlPlayerRequest : CSqlData -{ - using CSqlData::CSqlData; - // object being requested, either map (128 bytes) or player (16 bytes) - sqlstr::CSqlString m_Name; - // current map - sqlstr::CSqlString m_Map; - sqlstr::CSqlString m_RequestingPlayer; - // relevant for /top5 kind of requests - int m_Offset; -}; - -struct CSqlRandomMapRequest : CSqlData -{ - using CSqlData::CSqlData; - sqlstr::CSqlString<32> m_ServerType; - sqlstr::CSqlString m_CurrentMap; - sqlstr::CSqlString m_RequestingPlayer; - int m_Stars; -}; - -struct CSqlScoreData : CSqlData -{ - using CSqlData::CSqlData; - - sqlstr::CSqlString m_Map; - char m_GameUuid[UUID_MAXSTRSIZE]; - sqlstr::CSqlString m_Name; - - int m_ClientID; - float m_Time; - char m_aTimestamp[TIMESTAMP_STR_LENGTH]; - float m_aCpCurrent[NUM_CHECKPOINTS]; - int m_Num; - bool m_Search; - char m_aRequestingPlayer[MAX_NAME_LENGTH]; -}; - -struct CSqlTeamScoreData : CSqlData -{ - using CSqlData::CSqlData; - char m_GameUuid[UUID_MAXSTRSIZE]; - sqlstr::CSqlString m_Map; - float m_Time; - char m_aTimestamp[TIMESTAMP_STR_LENGTH]; - unsigned int m_Size; - sqlstr::CSqlString m_aNames[MAX_CLIENTS]; -}; - -struct CSqlTeamSave : CSqlData -{ - using CSqlData::CSqlData; - virtual ~CSqlTeamSave() {}; - - char m_ClientName[MAX_NAME_LENGTH]; - - char m_Map[MAX_MAP_LENGTH]; - char m_Code[128]; - char m_aGeneratedCode[128]; - char m_Server[5]; -}; - -struct CSqlTeamLoad : CSqlData -{ - using CSqlData::CSqlData; - sqlstr::CSqlString<128> m_Code; - sqlstr::CSqlString m_Map; - sqlstr::CSqlString m_RequestingPlayer; - int m_ClientID; - // struct holding all player names in the team or an empty string - char m_aClientNames[MAX_CLIENTS][MAX_NAME_LENGTH]; - int m_aClientID[MAX_CLIENTS]; - int m_NumPlayer; -}; - -// controls one thread -template < typename TResult > -struct CSqlExecData -{ - CSqlExecData( - bool (*pFuncPtr) (CSqlServer*, const CSqlData *, bool), - CSqlData *pSqlResult, - bool ReadOnly = true - ); - ~CSqlExecData(); - - bool (*m_pFuncPtr) (CSqlServer*, const CSqlData *, bool); - CSqlData *m_pSqlData; - bool m_ReadOnly; - - static void ExecSqlFunc(void *pUser); -}; - -class IServer; -class CGameContext; - -class CSqlScore: public IScore -{ - static LOCK ms_FailureFileLock; - - static bool Init(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure); - - static bool RandomMapThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool RandomUnfinishedMapThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool MapVoteThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - - static bool LoadPlayerDataThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool MapInfoThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool ShowRankThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool ShowTeamRankThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool ShowTop5Thread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool ShowTeamTop5Thread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool ShowTimesThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool ShowPointsThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool ShowTopPointsThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool GetSavesThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - - static bool SaveTeamThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool LoadTeamThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - - static bool SaveScoreThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - static bool SaveTeamScoreThread(CSqlServer* pSqlServer, const CSqlData *pGameData, bool HandleFailure = false); - - CGameContext *GameServer() { return m_pGameServer; } - IServer *Server() { return m_pServer; } - - CGameContext *m_pGameServer; - IServer *m_pServer; - - std::vector m_aWordlist; - CPrng m_Prng; - void GeneratePassphrase(char *pBuf, int BufSize); - - // returns new SqlResult bound to the player, if no current Thread is active for this player - std::shared_ptr NewSqlPlayerResult(int ClientID); - // Creates for player database requests - void ExecPlayerThread( - bool (*pFuncPtr) (CSqlServer*, const CSqlData *, bool), - const char* pThreadName, - int ClientID, - const char* pName, - int Offset - ); - - // returns true if the player should be rate limited - bool RateLimitPlayer(int ClientID); - -public: - // keeps track of score-threads - static std::atomic_int ms_InstanceCount; - - CSqlScore(CGameContext *pGameServer); - ~CSqlScore() {} - - // Requested by game context, shouldn't fail in case the player started another thread - virtual void RandomMap(int ClientID, int Stars); - virtual void RandomUnfinishedMap(int ClientID, int Stars); - virtual void MapVote(int ClientID, const char* MapName); - - virtual void LoadPlayerData(int ClientID); - // Requested by players (fails if another request by this player is active) - virtual void MapInfo(int ClientID, const char* MapName); - virtual void ShowRank(int ClientID, const char* pName); - virtual void ShowTeamRank(int ClientID, const char* pName); - virtual void ShowPoints(int ClientID, const char* pName); - virtual void ShowTimes(int ClientID, const char* pName, int Offset = 1); - virtual void ShowTimes(int ClientID, int Offset = 1); - virtual void ShowTop5(int ClientID, int Offset = 1); - virtual void ShowTeamTop5(int ClientID, int Offset = 1); - virtual void ShowTopPoints(int ClientID, int Offset = 1); - virtual void GetSaves(int ClientID); - - // requested by teams - virtual void SaveTeam(int ClientID, const char* Code, const char* Server); - virtual void LoadTeam(const char* Code, int ClientID); - - // Game relevant not allowed to fail due to an ongoing SQL request. - virtual void SaveScore(int ClientID, float Time, const char *pTimestamp, - float CpTime[NUM_CHECKPOINTS], bool NotEligible); - virtual void SaveTeamScore(int* aClientIDs, unsigned int Size, float Time, const char *pTimestamp); - - virtual void OnShutdown(); -}; - -#endif // GAME_SERVER_SCORE_SQL_H