diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index da2024c5a..b9e0ae859 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -62,7 +62,7 @@ jobs: if: contains(matrix.os, 'ubuntu') run: | sudo apt-get update -y - sudo apt-get install pkg-config cmake libfreetype6-dev libnotify-dev libsdl2-dev -y + sudo apt-get install pkg-config cmake libfreetype6-dev libnotify-dev libsdl2-dev libsqlite3-dev -y - name: Prepare Linux (fancy) if: contains(matrix.os, 'ubuntu') && matrix.fancy diff --git a/CMakeLists.txt b/CMakeLists.txt index b3b66554c..075c756ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -317,12 +317,18 @@ endif() find_package(ZLIB) find_package(Crypto) find_package(Curl) +if(VIDEORECORDER) + find_package(FFMPEG) +endif() find_package(Freetype) if(DOWNLOAD_GTEST) find_package(Git) endif() find_package(GLEW) find_package(GTest) +if(UPNP) + find_package(Miniupnpc) +endif() if(MYSQL) find_package(MySQL) else() @@ -331,15 +337,10 @@ endif() find_package(Ogg) find_package(Opus) find_package(Opusfile) -if(UPNP) - find_package(Miniupnpc) -endif() find_package(Pnglite) find_package(PythonInterp 3) find_package(SDL2) -if(VIDEORECORDER) - find_package(FFMPEG) -endif() +find_package(SQLite3) find_package(Threads) find_package(Wavpack) if(WEBSOCKETS) @@ -395,6 +396,9 @@ show_dependency_status("Curl" CURL) if(TARGET_OS AND TARGET_OS STREQUAL "mac") show_dependency_status("Dmg tools" DMGTOOLS) endif() +if(VIDEORECORDER) + show_dependency_status("FFmpeg" FFMPEG) +endif() show_dependency_status("Freetype" FREETYPE) if(DOWNLOAD_GTEST) show_dependency_status("Git" GIT) @@ -404,6 +408,9 @@ show_dependency_status("GTest" GTEST) if(TARGET_OS AND TARGET_OS STREQUAL "mac") show_dependency_status("Hdiutil" HDIUTIL) endif() +if(UPNP) + show_dependency_status("Miniupnpc" MINIUPNPC) +endif() if(MYSQL) show_dependency_status("MySQL" MYSQL) endif() @@ -411,15 +418,10 @@ show_dependency_status("Ogg" OGG) show_dependency_status("OpenSSL Crypto" CRYPTO) show_dependency_status("Opus" OPUS) show_dependency_status("Opusfile" OPUSFILE) -if(UPNP) - show_dependency_status("Miniupnpc" MINIUPNPC) -endif() show_dependency_status("Pnglite" PNGLITE) show_dependency_status("PythonInterp" PYTHONINTERP) show_dependency_status("SDL2" SDL2) -if(VIDEORECORDER) - show_dependency_status("FFmpeg" FFMPEG) -endif() +show_dependency_status("SQLite3" SQLite3) show_dependency_status("Wavpack" WAVPACK) show_dependency_status("Zlib" ZLIB) if(WEBSOCKETS) @@ -432,6 +434,9 @@ endif() if(NOT(PYTHONINTERP_FOUND)) message(SEND_ERROR "You must install Python to compile DDNet") endif() +if(NOT(SQLite3_FOUND)) + message(SEND_ERROR "You must install SQLite3 to compile DDNet") +endif() if(MYSQL AND NOT(MYSQL_FOUND)) message(SEND_ERROR "You must install MySQL to compile the DDNet server with MySQL support") @@ -1814,21 +1819,25 @@ set_src(ANTIBOT_SRC GLOB src/antibot antibot_null.cpp ) -set_src(ENGINE_SERVER GLOB src/engine/server +set_src(ENGINE_SERVER GLOB_RECURSE src/engine/server antibot.cpp antibot.h authmanager.cpp authmanager.h + databases/connection.cpp + databases/connection.h + databases/connection_pool.cpp + databases/connection_pool.h + databases/mysql.cpp + databases/mysql.h + databases/sqlite.cpp + databases/sqlite.h name_ban.cpp name_ban.h register.cpp register.h server.cpp server.h - sql_connector.cpp - sql_connector.h - sql_server.cpp - sql_server.h sql_string_helpers.cpp sql_string_helpers.h upnp.cpp @@ -1877,10 +1886,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 @@ -1910,6 +1915,7 @@ endif() set(LIBS_SERVER ${LIBS} ${MYSQL_LIBRARIES} + ${SQLite3_LIBRARIES} ${TARGET_ANTIBOT} ${MINIUPNPC_LIBRARIES} # Add pthreads (on non-Windows) at the end, so that other libraries can depend @@ -2484,7 +2490,7 @@ foreach(target ${TARGETS_OWN}) target_include_directories(${target} PRIVATE ${PROJECT_BINARY_DIR}/src) target_include_directories(${target} PRIVATE src) target_compile_definitions(${target} PRIVATE $<$:CONF_DEBUG>) - target_include_directories(${target} PRIVATE ${ZLIB_INCLUDE_DIRS}) + target_include_directories(${target} PRIVATE ${SQLite3_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS}) target_compile_definitions(${target} PRIVATE GLEW_STATIC) if(CRYPTO_FOUND) target_compile_definitions(${target} PRIVATE CONF_OPENSSL) diff --git a/cmake/FindSQLite3.cmake b/cmake/FindSQLite3.cmake new file mode 100644 index 000000000..722a96ed9 --- /dev/null +++ b/cmake/FindSQLite3.cmake @@ -0,0 +1,37 @@ +if(NOT PREFER_BUNDLED_LIBS) + set(CMAKE_MODULE_PATH ${ORIGINAL_CMAKE_MODULE_PATH}) + find_package(SQLite3) + set(CMAKE_MODULE_PATH ${OWN_CMAKE_MODULE_PATH}) +endif() + +if(NOT SQLite3_FOUND) + if(NOT CMAKE_CROSSCOMPILING) + find_package(PkgConfig QUIET) + pkg_check_modules(PC_SQLite3 sqlite3) + endif() + + set_extra_dirs_lib(SQLite3 sqlite3) + find_library(SQLite3_LIBRARY + NAMES sqlite3 + HINTS ${HINTS_SQLite3_LIBDIR} ${PC_SQLite3_LIBDIR} ${PC_SQLite3_LIBRARY_DIRS} + PATHS ${PATHS_SQLite3_LIBDIR} + ${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH} + ) + set_extra_dirs_include(SQLite3 sqlite3 "${SQLite3_LIBRARY}") + find_path(SQLite3_INCLUDEDIR sqlite3.h + PATH_SUFFIXES sqlite3 + HINTS ${HINTS_SQLite3_INCLUDEDIR} ${PC_SQLite3_INCLUDEDIR} ${PC_SQLite3_INCLUDE_DIRS} + PATHS ${PATHS_SQLite3_INCLUDEDIR} + ${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(SQLite3 DEFAULT_MSG SQLite3_INCLUDEDIR SQLite3_LIBRARY) + mark_as_advanced(SQLite3_INCLUDEDIR SQLite3_LIBRARY) +endif() + +if(SQLite3_FOUND) + is_bundled(SQLite3_BUNDLED "${SQLite3_LIBRARY}") + set(SQLite3_LIBRARIES ${SQLite3_LIBRARY}) + set(SQLite3_INCLUDE_DIRS ${SQLite3_INCLUDEDIR}) +endif() diff --git a/scripts/import_file_score.py b/scripts/import_file_score.py new file mode 100755 index 000000000..72564a03d --- /dev/null +++ b/scripts/import_file_score.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +from collections import namedtuple +from decimal import Decimal +import os.path +import re +import sqlite3 +import sys + +def chunks(l, n): + for i in range(0, len(l), n): + yield l[i:i+n] + +class Record(namedtuple('Record', 'name time checkpoints')): + @staticmethod + def parse(lines): + if len(lines) != 3: + raise ValueError("wrong amount of lines for record") + name = lines[0] + time = Decimal(lines[1]) + checkpoints_str = lines[2].split(' ') + if len(checkpoints_str) != 26 or checkpoints_str[25] != "": + raise ValueError("wrong amount of checkpoint times: {}".format(len(checkpoints_str))) + checkpoints_str = checkpoints_str[:25] + checkpoints = tuple(Decimal(c) for c in checkpoints_str) + return Record(name=name, time=time, checkpoints=checkpoints) + + def unparse(self): + return "\n".join([self.name, str(self.time), " ".join([str(cp) for cp in self.checkpoints] + [""]), ""]) + +def read_records(file): + contents = file.read().splitlines() + return [Record.parse(c) for c in chunks(contents, 3)] + +MAP_RE=re.compile(r"^(?P.*)_record\.dtb$") +def main(): + import argparse + p = argparse.ArgumentParser(description="Merge multiple DDNet race database files", formatter_class=argparse.ArgumentDefaultsHelpFormatter) + p.add_argument("--out", default="ddnet-server.sqlite", help="Output SQLite database") + p.add_argument("in_", metavar="IN", nargs='+', help="Text score databases to import; must have the format MAPNAME_record.dtb") + p.add_argument("--dry-run", "-n", action='store_true', help="Don't write out the resulting SQLite database") + p.add_argument("--stats", action='store_true', help="Display some stats at the end of the import process") + args = p.parse_args() + + records = {} + for in_ in args.in_: + m = MAP_RE.match(os.path.basename(in_)) + if not m: + raise ValueError("Invalid text score database name, does not end in '_record.dtb': {}".format(in_)) + map = m.group("map") + if map in records: + raise ValueError("Two text score databases refer to the same map: {}".format(in_)) + with open(in_) as f: + records[map] = read_records(f) + + if not args.dry_run: + conn = sqlite3.connect(args.out) + c = conn.cursor() + c.execute("CREATE TABLE IF NOT EXISTS record_race (" + "Map VARCHAR(128) COLLATE BINARY NOT NULL, " + "Name VARCHAR(16) COLLATE BINARY NOT NULL, " + "Timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "Time FLOAT DEFAULT 0, " + "Server CHAR(4), " + + "".join("cp{} FLOAT DEFAULT 0, ".format(i + 1) for i in range(25)) + + "GameID VARCHAR(64), " + "DDNet7 BOOL DEFAULT FALSE" + ");"); + c.executemany( + "INSERT INTO record_race (Map, Name, Time, Server, " + + "".join("cp{}, ".format(i + 1) for i in range(25)) + + "GameID, DDNet7) " + + "VALUES ({})".format(",".join("?" * 31)), + [(map, r.name, float(r.time), "TEXT", *[float(c) for c in r.checkpoints], None, False) for map in records for r in records[map]] + ) + conn.commit() + conn.close() + + if args.stats: + print("Number of imported text databases: {}".format(len(records)), file=sys.stderr) + print("Number of imported ranks: {}".format(sum(len(r) for r in records.values()), file=sys.stderr)) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/move_sqlite.py b/scripts/move_sqlite.py new file mode 100755 index 000000000..01e7bcf0b --- /dev/null +++ b/scripts/move_sqlite.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +# This script is intended to be called automatically every day (e.g. via cron). +# It only output stuff if new ranks have to be inserted. Therefore the output +# may be redirected to email notifying about manual action to transfer the +# ranks to MySQL. +# +# Configure cron as the user running the DDNet-Server processes +# +# $ crontab -e +# 30 5 * * * /path/to/this/script/move_sqlite.py --from /path/to/ddnet-server.sqlite +# +# Afterwards configure a MTA (e.g. postfix) and the users email address. + +import sqlite3 +import argparse +from time import strftime +import os + +def sqlite_num_transfer(conn, table): + c = conn.cursor() + c.execute('SELECT COUNT(*) FROM {}'.format(table)) + num = c.fetchone()[0] + return num + +def transfer(file_from, file_to): + conn_to = sqlite3.connect(file_to, isolation_level='EXCLUSIVE') + cursor_to = conn_to.cursor() + + conn_from = sqlite3.connect(file_from, isolation_level='EXCLUSIVE') + for line in conn_from.iterdump(): + cursor_to.execute(line) + print(line) + cursor_to.close() + conn_to.commit() + conn_to.close() + + cursor_from = conn_from.cursor() + cursor_from.execute('DELETE FROM record_race') + cursor_from.execute('DELETE FROM record_teamrace') + cursor_from.execute('DELETE FROM record_saves') + cursor_from.close() + conn_from.commit() + conn_from.close() + +def main(): + default_output = 'ddnet-server-' + strftime('%Y-%m-%d') + '.sqlite' + parser = argparse.ArgumentParser( + description='Move DDNet ranks, teamranks and saves from a possible active SQLite3 to a new one', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('--from', '-f', dest='f', + default='ddnet-server.sqlite', + help='Input file where ranks are deleted from when moved successfully (default: ddnet-server.sqlite)') + parser.add_argument('--to', '-t', + default=default_output, + help='Output file where ranks are saved adds current date by default') + args = parser.parse_args() + + conn = sqlite3.connect(args.f) + num_ranks = sqlite_num_transfer(conn, 'record_race') + num_teamranks = sqlite_num_transfer(conn, 'record_teamrace') + num_saves = sqlite_num_transfer(conn, 'record_saves') + num = num_ranks + num_teamranks + num_saves + conn.close() + if num == 0: + return + + print('{} new entries in backup database found ({} ranks, {} teamranks, {} saves'.format(num, num_ranks, num_teamranks, num_saves)) + print('Moving entries from {} to {}'.format( + os.path.abspath(args.f), + os.path.abspath(args.to))) + sql_file = 'ddnet-server-' + strftime('%Y-%m-%d') + '.sql' + print("You can use the following commands to import the entries to MySQL (use sed 's/record_/prefix_/' for other database prefixes):") + print() + print((" echo '.dump --preserve-rowids' | sqlite3 {} | " + # including rowids, this forces sqlite to name all columns in each INSERT statement + "grep -E '^INSERT INTO record_(race|teamrace|saves)' | " + # filter out inserts + "sed -e 's/rowid,//' -e 's/VALUES([0-9]*,/VALUES(/' > {}") # filter out rowids again + .format(os.path.abspath(args.to), sql_file)) + print(" mysql -u teeworlds -p'PW2' teeworlds < {}".format(sql_file)) + print() + print("When the ranks are transfered successfully to mysql {} and {} can be removed".format( + os.path.abspath(args.f), os.path.abspath(args.to))) + print() + print("Log of the transfer:") + print() + + transfer(args.f, args.to) + +if __name__ == '__main__': + main() diff --git a/src/base/tl/threading.h b/src/base/tl/threading.h index c5a972adc..55b69e3cf 100644 --- a/src/base/tl/threading.h +++ b/src/base/tl/threading.h @@ -3,64 +3,6 @@ #include "../system.h" -/* - atomic_inc - should return the value after increment - atomic_dec - should return the value after decrement - atomic_compswap - should return the value before the eventual swap - sync_barrier - creates a full hardware fence -*/ - -#if defined(__GNUC__) - - inline unsigned atomic_inc(volatile unsigned *pValue) - { - return __sync_add_and_fetch(pValue, 1); - } - - inline unsigned atomic_dec(volatile unsigned *pValue) - { - return __sync_add_and_fetch(pValue, -1); - } - - inline unsigned atomic_compswap(volatile unsigned *pValue, unsigned comperand, unsigned value) - { - return __sync_val_compare_and_swap(pValue, comperand, value); - } - - inline void sync_barrier() - { - __sync_synchronize(); - } - -#elif defined(_MSC_VER) - #include - - #define WIN32_LEAN_AND_MEAN - #include - - inline unsigned atomic_inc(volatile unsigned *pValue) - { - return _InterlockedIncrement((volatile long *)pValue); - } - - inline unsigned atomic_dec(volatile unsigned *pValue) - { - return _InterlockedDecrement((volatile long *)pValue); - } - - inline unsigned atomic_compswap(volatile unsigned *pValue, unsigned comperand, unsigned value) - { - return _InterlockedCompareExchange((volatile long *)pValue, (long)value, (long)comperand); - } - - inline void sync_barrier() - { - MemoryBarrier(); - } -#else - #error missing atomic implementation for this compiler -#endif - class semaphore { SEMAPHORE sem; diff --git a/src/engine/client/backend_sdl.cpp b/src/engine/client/backend_sdl.cpp index e44cb87ba..d6b6177dc 100644 --- a/src/engine/client/backend_sdl.cpp +++ b/src/engine/client/backend_sdl.cpp @@ -43,6 +43,25 @@ extern "C" } #endif +/* + sync_barrier - creates a full hardware fence +*/ +#if defined(__GNUC__) + inline void sync_barrier() + { + __sync_synchronize(); + } +#elif defined(_MSC_VER) + #define WIN32_LEAN_AND_MEAN + #include + inline void sync_barrier() + { + MemoryBarrier(); + } +#else + #error missing atomic implementation for this compiler +#endif + // ------------ CGraphicsBackend_Threaded void CGraphicsBackend_Threaded::ThreadFunc(void *pUser) 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.cpp b/src/engine/server/databases/connection.cpp new file mode 100644 index 000000000..c8b32c5d2 --- /dev/null +++ b/src/engine/server/databases/connection.cpp @@ -0,0 +1,84 @@ +#include "connection.h" + +#include + +void IDbConnection::FormatCreateRace(char *aBuf, unsigned int BufferSize) +{ + str_format(aBuf, BufferSize, + "CREATE TABLE IF NOT EXISTS %s_race (" + "Map VARCHAR(128) COLLATE %s NOT NULL, " + "Name VARCHAR(%d) COLLATE %s 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" + ");", + GetPrefix(), BinaryCollate(), MAX_NAME_LENGTH, BinaryCollate()); +} + +void IDbConnection::FormatCreateTeamrace(char *aBuf, unsigned int BufferSize, const char *pIdType) +{ + str_format(aBuf, BufferSize, + "CREATE TABLE IF NOT EXISTS %s_teamrace (" + "Map VARCHAR(128) COLLATE %s NOT NULL, " + "Name VARCHAR(%d) COLLATE %s NOT NULL, " + "Timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "Time FLOAT DEFAULT 0, " + "ID %s NOT NULL, " // VARBINARY(16) for MySQL and BLOB for SQLite + "GameID VARCHAR(64), " + "DDNet7 BOOL DEFAULT FALSE" + ");", + GetPrefix(), BinaryCollate(), MAX_NAME_LENGTH, BinaryCollate(), pIdType); +} + +void IDbConnection::FormatCreateMaps(char *aBuf, unsigned int BufferSize) +{ + str_format(aBuf, BufferSize, + "CREATE TABLE IF NOT EXISTS %s_maps (" + "Map VARCHAR(128) COLLATE %s NOT NULL, " + "Server VARCHAR(32) COLLATE %s NOT NULL, " + "Mapper VARCHAR(128) COLLATE %s NOT NULL, " + "Points INT DEFAULT 0, " + "Stars INT DEFAULT 0, " + "Timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + "PRIMARY KEY (Map)" + ");", + GetPrefix(), BinaryCollate(), BinaryCollate(), BinaryCollate()); +} + +void IDbConnection::FormatCreateSaves(char *aBuf, unsigned int BufferSize) +{ + str_format(aBuf, BufferSize, + "CREATE TABLE IF NOT EXISTS %s_saves (" + "Savegame TEXT COLLATE %s NOT NULL, " + "Map VARCHAR(128) COLLATE %s NOT NULL, " + "Code VARCHAR(128) COLLATE %s NOT NULL, " + "Timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "Server CHAR(4), " + "DDNet7 BOOL DEFAULT FALSE, " + "SaveID VARCHAR(36) DEFAULT NULL, " + "PRIMARY KEY (Map, Code)" + ");", + GetPrefix(), BinaryCollate(), BinaryCollate(), BinaryCollate()); +} + +void IDbConnection::FormatCreatePoints(char *aBuf, unsigned int BufferSize) +{ + str_format(aBuf, BufferSize, + "CREATE TABLE IF NOT EXISTS %s_points (" + "Name VARCHAR(%d) COLLATE %s NOT NULL, " + "Points INT DEFAULT 0, " + "PRIMARY KEY (Name)" + ");", + GetPrefix(), MAX_NAME_LENGTH, BinaryCollate()); +} diff --git a/src/engine/server/databases/connection.h b/src/engine/server/databases/connection.h new file mode 100644 index 000000000..b1467ef18 --- /dev/null +++ b/src/engine/server/databases/connection.h @@ -0,0 +1,85 @@ +#ifndef ENGINE_SERVER_DATABASES_CONNECTION_H +#define ENGINE_SERVER_DATABASES_CONNECTION_H + +#include + +class IConsole; + +// can hold one PreparedStatement with Results +class IDbConnection +{ +public: + IDbConnection(const char *pPrefix) + { + str_copy(m_aPrefix, pPrefix, sizeof(m_aPrefix)); + } + virtual ~IDbConnection() {} + IDbConnection& operator=(const IDbConnection&) = delete; + virtual void Print(IConsole *pConsole, const char *Mode) = 0; + + // copies the credentials, not the active connection + virtual IDbConnection *Copy() = 0; + + // returns the database prefix + const char *GetPrefix() { return m_aPrefix; } + virtual const char *BinaryCollate() const = 0; + // can be inserted into queries to convert a timestamp variable to the unix timestamp + virtual void ToUnixTimestamp(const char *pTimestamp, char *aBuf, unsigned int BufferSize) = 0; + // since MySQL automatically converts timestamps to utc, meanwhile sqlite code has to + // explicitly convert before inserting timestamps, NOTE: CURRENT_TIMESTAMP in SQLite is UTC by + // default and doesn't have to be converted + virtual const char *InsertTimestampAsUtc() const = 0; + // can be used in the context of `LIKE Map`, adds `? COLLATE` + virtual const char *CollateNocase() const = 0; + + enum Status + { + IN_USE, + SUCCESS, + FAILURE, + }; + // tries to allocate the connection from the pool established + virtual Status Connect() = 0; + // has to be called to return the connection back to the pool + virtual void Disconnect() = 0; + + // get exclusive read/write access to the database + virtual void Lock(const char *pTable) = 0; + virtual void Unlock() = 0; + + // ? for Placeholders, connection has to be established, can overwrite previous prepared statements + virtual void PrepareStatement(const char *pStmt) = 0; + + // PrepareStatement has to be called beforehand, + virtual void BindString(int Idx, const char *pString) = 0; + virtual void BindBlob(int Idx, unsigned char *pBlob, int Size) = 0; + virtual void BindInt(int Idx, int Value) = 0; + virtual void BindFloat(int Idx, float Value) = 0; + + // 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; + // returns number of bytes read into the buffer + virtual int GetBlob(int Col, unsigned char *pBuffer, int BufferSize) const = 0; + + // SQL statements, that can't be abstracted, has side effects to the result + virtual void AddPoints(const char *pPlayer, int Points) = 0; + +private: + char m_aPrefix[64]; + +protected: + void FormatCreateRace(char *aBuf, unsigned int BufferSize); + void FormatCreateTeamrace(char *aBuf, unsigned int BufferSize, const char *pIdType); + void FormatCreateMaps(char *aBuf, unsigned int BufferSize); + void FormatCreateSaves(char *aBuf, unsigned int BufferSize); + void FormatCreatePoints(char *aBuf, unsigned int BufferSize); +}; + +#endif // ENGINE_SERVER_DATABASES_CONNECTION_H diff --git a/src/engine/server/databases/connection_pool.cpp b/src/engine/server/databases/connection_pool.cpp new file mode 100644 index 000000000..b48f51181 --- /dev/null +++ b/src/engine/server/databases/connection_pool.cpp @@ -0,0 +1,228 @@ +#include "connection_pool.h" +#include "connection.h" + +#include +#if defined(CONF_SQL) +#include +#endif +#include + +// helper struct to hold thread data +struct CSqlExecData +{ + CSqlExecData( + CDbConnectionPool::FRead pFunc, + std::unique_ptr pThreadData, + const char *pName); + CSqlExecData( + CDbConnectionPool::FWrite pFunc, + std::unique_ptr pThreadData, + const char *pName); + ~CSqlExecData() {} + + enum + { + READ_ACCESS, + WRITE_ACCESS, + } m_Mode; + union + { + CDbConnectionPool::FRead m_pReadFunc; + CDbConnectionPool::FWrite m_pWriteFunc; + } m_Ptr; + + std::unique_ptr m_pThreadData; + const char *m_pName; +}; + +CSqlExecData::CSqlExecData( + CDbConnectionPool::FRead pFunc, + std::unique_ptr pThreadData, + const char *pName) : + m_Mode(READ_ACCESS), + m_pThreadData(std::move(pThreadData)), + m_pName(pName) +{ + m_Ptr.m_pReadFunc = pFunc; +} + +CSqlExecData::CSqlExecData( + CDbConnectionPool::FWrite pFunc, + std::unique_ptr pThreadData, + const char *pName) : + m_Mode(WRITE_ACCESS), + m_pThreadData(std::move(pThreadData)), + m_pName(pName) +{ + m_Ptr.m_pWriteFunc = pFunc; +} + +CDbConnectionPool::CDbConnectionPool() : + m_NumElem(), + FirstElem(0), + LastElem(0) +{ + thread_init_and_detach(CDbConnectionPool::Worker, this, "database worker thread"); +} + +CDbConnectionPool::~CDbConnectionPool() +{ +} + +void CDbConnectionPool::Print(IConsole *pConsole, Mode DatabaseMode) +{ + const char *ModeDesc[] = {"Read", "Write", "WriteBackup"}; + for(unsigned int i = 0; i < m_aapDbConnections[DatabaseMode].size(); i++) + { + m_aapDbConnections[DatabaseMode][i]->Print(pConsole, ModeDesc[DatabaseMode]); + } +} + +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( + FRead pFunc, + std::unique_ptr pThreadData, + const char *pName) +{ + m_aTasks[FirstElem++].reset(new CSqlExecData(pFunc, std::move(pThreadData), pName)); + FirstElem %= sizeof(m_aTasks) / sizeof(m_aTasks[0]); + m_NumElem.signal(); +} + +void CDbConnectionPool::ExecuteWrite( + FWrite pFunc, + std::unique_ptr pThreadData, + const char *pName) +{ + m_aTasks[FirstElem++].reset(new CSqlExecData(pFunc, std::move(pThreadData), pName)); + FirstElem %= sizeof(m_aTasks) / sizeof(m_aTasks[0]); + m_NumElem.signal(); +} + +void CDbConnectionPool::OnShutdown() +{ + m_Shutdown.store(true); + m_NumElem.signal(); + int i = 0; + while(m_Shutdown.load()) + { + 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 (%ds)", i / 10); + ++i; + thread_sleep(100000); + } +} + +void CDbConnectionPool::Worker(void *pUser) +{ + CDbConnectionPool *pThis = (CDbConnectionPool *)pUser; + pThis->Worker(); +} + +void CDbConnectionPool::Worker() +{ + while(1) + { + m_NumElem.wait(); + auto pThreadData = std::move(m_aTasks[LastElem++]); + // work through all database jobs after OnShutdown is called before exiting the thread + if(pThreadData == nullptr) + { + m_Shutdown.store(false); + return; + } + 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 (std::runtime_error &e) + { + dbg_msg("sql", "SQLite Error: %s", e.what()); + } + 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 new file mode 100644 index 000000000..7fb87e76c --- /dev/null +++ b/src/engine/server/databases/connection_pool.h @@ -0,0 +1,66 @@ +#ifndef ENGINE_SERVER_DATABASES_CONNECTION_POOL_H +#define ENGINE_SERVER_DATABASES_CONNECTION_POOL_H + +#include +#include +#include +#include + +class IDbConnection; + +struct ISqlData +{ + virtual ~ISqlData() {}; +}; + +class IConsole; + +class CDbConnectionPool +{ +public: + CDbConnectionPool(); + ~CDbConnectionPool(); + CDbConnectionPool& operator=(const CDbConnectionPool&) = delete; + + typedef bool (*FRead)(IDbConnection *, const ISqlData *); + typedef bool (*FWrite)(IDbConnection *, const ISqlData *, bool); + + enum Mode + { + READ, + WRITE, + WRITE_BACKUP, + NUM_MODES, + }; + + void Print(IConsole *pConsole, Mode DatabaseMode); + + void RegisterDatabase(std::unique_ptr pDatabase, Mode DatabaseMode); + + void Execute( + FRead pFunc, + std::unique_ptr pSqlRequestData, + const char *pName); + // writes to WRITE_BACKUP server in case of failure + void ExecuteWrite( + FWrite pFunc, + std::unique_ptr pSqlRequestData, + const char *pName); + + void OnShutdown(); + +private: + std::vector> m_aapDbConnections[NUM_MODES]; + + static void Worker(void *pUser); + void Worker(); + bool ExecSqlFunc(IDbConnection *pConnection, struct CSqlExecData *pData, bool Failure); + + std::atomic_bool m_Shutdown; + 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 new file mode 100644 index 000000000..6a90a20f3 --- /dev/null +++ b/src/engine/server/databases/mysql.cpp @@ -0,0 +1,328 @@ +#include "mysql.h" + +#include +#include +#if defined(CONF_SQL) +#include +#include +#include +#include +#endif + +#include + +lock CMysqlConnection::m_SqlDriverLock; + +CMysqlConnection::CMysqlConnection( + const char *pDatabase, + const char *pPrefix, + const char *pUser, + const char *pPass, + const char *pIp, + int Port, + bool Setup) : + IDbConnection(pPrefix), +#if defined(CONF_SQL) + m_NewQuery(false), + m_Locked(false), +#endif + m_Port(Port), + m_Setup(Setup), + m_InUse(false) +{ + str_copy(m_aDatabase, pDatabase, sizeof(m_aDatabase)); + str_copy(m_aUser, pUser, sizeof(m_aUser)); + str_copy(m_aPass, pPass, sizeof(m_aPass)); + str_copy(m_aIp, pIp, sizeof(m_aIp)); +#ifndef CONF_SQL + dbg_msg("sql", "Adding MySQL server failed due to MySQL support not enabled during compile time"); +#endif +} + +CMysqlConnection::~CMysqlConnection() +{ +#if defined(CONF_SQL) + m_pStmt.release(); + m_pPreparedStmt.release(); + m_pConnection.release(); +#endif +} + +void CMysqlConnection::Print(IConsole *pConsole, const char *Mode) +{ + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), + "MySQL-%s: DB: '%s' Prefix: '%s' User: '%s' IP: <{'%s'}> Port: %d", + Mode, m_aDatabase, GetPrefix(), m_aUser, m_aIp, m_Port); + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); +} + +CMysqlConnection *CMysqlConnection::Copy() +{ + return new CMysqlConnection(m_aDatabase, GetPrefix(), m_aUser, m_aPass, m_aIp, m_Port, m_Setup); +} + +void CMysqlConnection::ToUnixTimestamp(const char *pTimestamp, char *aBuf, unsigned int BufferSize) +{ + str_format(aBuf, BufferSize, "UNIX_TIMESTAMP(%s)", pTimestamp); +} + +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"); + } + + dbg_msg("sql", "FAILURE: SQL connection failed, trying to reconnect"); + } + + 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); + FormatCreateRace(aBuf, sizeof(aBuf)); + m_pStmt->execute(aBuf); + FormatCreateTeamrace(aBuf, sizeof(aBuf), "VARBINARY(16)"); + m_pStmt->execute(aBuf); + FormatCreateMaps(aBuf, sizeof(aBuf)); + m_pStmt->execute(aBuf); + FormatCreateSaves(aBuf, sizeof(aBuf)); + m_pStmt->execute(aBuf); + FormatCreatePoints(aBuf, sizeof(aBuf)); + 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"); + } + m_InUse.store(false); + +#endif + dbg_msg("sql", "FAILURE: sql connection failed"); + return Status::FAILURE; +} + +void CMysqlConnection::Disconnect() +{ + m_InUse.store(false); +} + +void CMysqlConnection::Lock(const char *pTable) +{ +#if defined(CONF_SQL) + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), "LOCK TABLES %s;", 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::BindBlob(int Idx, unsigned char *pBlob, int Size) +{ +#if defined(CONF_SQL) + // copy blob into string + auto Blob = std::string(pBlob, pBlob+Size); + m_pPreparedStmt->setString(Idx, Blob); + m_NewQuery = true; +#endif +} + +void CMysqlConnection::BindInt(int Idx, int Value) +{ +#if defined(CONF_SQL) + m_pPreparedStmt->setInt(Idx, Value); + m_NewQuery = true; +#endif +} + +void CMysqlConnection::BindFloat(int Idx, float Value) +{ +#if defined(CONF_SQL) + m_pPreparedStmt->setDouble(Idx, (double)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 +} + +void CMysqlConnection::AddPoints(const char *pPlayer, int Points) +{ + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), + "INSERT INTO %s_points(Name, Points) " + "VALUES (?, ?) " + "ON DUPLICATE KEY UPDATE Points=Points+?;", + GetPrefix()); + PrepareStatement(aBuf); + BindString(1, pPlayer); + BindInt(2, Points); + BindInt(3, Points); + Step(); +} diff --git a/src/engine/server/databases/mysql.h b/src/engine/server/databases/mysql.h new file mode 100644 index 000000000..effc691ea --- /dev/null +++ b/src/engine/server/databases/mysql.h @@ -0,0 +1,82 @@ +#ifndef ENGINE_SERVER_DATABASES_MYSQL_H +#define ENGINE_SERVER_DATABASES_MYSQL_H + +#include +#include +#include + +class lock; +namespace sql { +class Connection; +class PreparedStatement; +class ResultSet; +class Statement; +} /* namespace sql */ + +class CMysqlConnection : public IDbConnection +{ +public: + CMysqlConnection( + const char *pDatabase, + const char *pPrefix, + const char *pUser, + const char *pPass, + const char *pIp, + int Port, + bool Setup); + virtual ~CMysqlConnection(); + virtual void Print(IConsole *pConsole, const char *Mode); + + virtual CMysqlConnection *Copy(); + + virtual const char *BinaryCollate() const { return "utf8mb4_bin"; } + virtual void ToUnixTimestamp(const char *pTimestamp, char *aBuf, unsigned int BufferSize); + virtual const char *InsertTimestampAsUtc() const { return "?"; } + virtual const char *CollateNocase() const { return "CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci"; } + + virtual Status Connect(); + virtual void Disconnect(); + + virtual void Lock(const char *pTable); + virtual void Unlock(); + + virtual void PrepareStatement(const char *pStmt); + + virtual void BindString(int Idx, const char *pString); + virtual void BindBlob(int Idx, unsigned char *pBlob, int Size); + virtual void BindInt(int Idx, int Value); + virtual void BindFloat(int Idx, float Value); + + 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; + + virtual void AddPoints(const char *pPlayer, int Points); + +private: +#if defined(CONF_SQL) + std::unique_ptr m_pConnection; + 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]; + char m_aUser[64]; + char m_aPass[64]; + char m_aIp[64]; + int m_Port; + bool m_Setup; + + std::atomic_bool m_InUse; + static lock m_SqlDriverLock; +}; + +#endif // ENGINE_SERVER_DATABASES_MYSQL_H diff --git a/src/engine/server/databases/sqlite.cpp b/src/engine/server/databases/sqlite.cpp new file mode 100644 index 000000000..aac948983 --- /dev/null +++ b/src/engine/server/databases/sqlite.cpp @@ -0,0 +1,241 @@ +#include "sqlite.h" + +#include +#include + +#include +#include + +CSqliteConnection::CSqliteConnection(const char *pFilename, bool Setup) : + IDbConnection("record"), + m_Setup(Setup), + m_pDb(nullptr), + m_pStmt(nullptr), + m_Done(true), + m_Locked(false), + m_InUse(false) +{ + str_copy(m_aFilename, pFilename, sizeof(m_aFilename)); +} + +CSqliteConnection::~CSqliteConnection() +{ + if(m_pStmt != nullptr) + sqlite3_finalize(m_pStmt); + sqlite3_close(m_pDb); + m_pDb = nullptr; +} + + +void CSqliteConnection::Print(IConsole *pConsole, const char *Mode) +{ + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), + "SQLite-%s: DB: '%s'", + Mode, m_aFilename); + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); +} + + +void CSqliteConnection::ToUnixTimestamp(const char *pTimestamp, char *aBuf, unsigned int BufferSize) +{ + str_format(aBuf, BufferSize, "strftime('%%s', %s)", pTimestamp); +} + +CSqliteConnection *CSqliteConnection::Copy() +{ + return new CSqliteConnection(m_aFilename, m_Setup); +} + +IDbConnection::Status CSqliteConnection::Connect() +{ + if(m_InUse.exchange(true)) + return Status::IN_USE; + + if(m_pDb != nullptr) + return Status::SUCCESS; + + int Result = sqlite3_open(m_aFilename, &m_pDb); + if(Result != SQLITE_OK) + { + dbg_msg("sql", "Can't open sqlite database: '%s'", sqlite3_errmsg(m_pDb)); + return Status::FAILURE; + } + + // wait for database to unlock so we don't have to handle SQLITE_BUSY errors + sqlite3_busy_timeout(m_pDb, -1); + + if(m_Setup) + { + char aBuf[1024]; + FormatCreateRace(aBuf, sizeof(aBuf)); + if(!Execute(aBuf)) + return Status::FAILURE; + FormatCreateTeamrace(aBuf, sizeof(aBuf), "BLOB"); + if(!Execute(aBuf)) + return Status::FAILURE; + FormatCreateMaps(aBuf, sizeof(aBuf)); + if(!Execute(aBuf)) + return Status::FAILURE; + FormatCreateSaves(aBuf, sizeof(aBuf)); + if(!Execute(aBuf)) + return Status::FAILURE; + FormatCreatePoints(aBuf, sizeof(aBuf)); + if(!Execute(aBuf)) + return Status::FAILURE; + m_Setup = false; + } + m_Locked = false; + return Status::SUCCESS; +} + +void CSqliteConnection::Disconnect() +{ + if(m_pStmt != nullptr) + sqlite3_finalize(m_pStmt); + m_pStmt = nullptr; + m_InUse.store(false); +} + +void CSqliteConnection::Lock(const char *pTable) +{ + // locks the whole database read/write + Execute("BEGIN EXCLUSIVE TRANSACTION;"); + m_Locked = true; +} + +void CSqliteConnection::Unlock() +{ + if(m_Locked) + { + Execute("COMMIT TRANSACTION;"); + m_Locked = false; + } +} + +void CSqliteConnection::PrepareStatement(const char *pStmt) +{ + if(m_pStmt != nullptr) + sqlite3_finalize(m_pStmt); + m_pStmt = nullptr; + int Result = sqlite3_prepare_v2( + m_pDb, + pStmt, + -1, // pStmt can be any length + &m_pStmt, + NULL); + ExceptionOnError(Result); + m_Done = false; +} + +void CSqliteConnection::BindString(int Idx, const char *pString) +{ + int Result = sqlite3_bind_text(m_pStmt, Idx, pString, -1, NULL); + ExceptionOnError(Result); + m_Done = false; +} + +void CSqliteConnection::BindBlob(int Idx, unsigned char *pBlob, int Size) +{ + int Result = sqlite3_bind_blob(m_pStmt, Idx, pBlob, Size, NULL); + ExceptionOnError(Result); + m_Done = false; +} + +void CSqliteConnection::BindInt(int Idx, int Value) +{ + int Result = sqlite3_bind_int(m_pStmt, Idx, Value); + ExceptionOnError(Result); + m_Done = false; +} + +void CSqliteConnection::BindFloat(int Idx, float Value) +{ + int Result = sqlite3_bind_double(m_pStmt, Idx, (double)Value); + ExceptionOnError(Result); + m_Done = false; +} + +bool CSqliteConnection::Step() +{ + if(m_Done) + return false; + int Result = sqlite3_step(m_pStmt); + if(Result == SQLITE_ROW) + { + return true; + } + else if(Result == SQLITE_DONE) + { + m_Done = true; + return false; + } + else + { + ExceptionOnError(Result); + } + return false; +} + +bool CSqliteConnection::IsNull(int Col) const +{ + return sqlite3_column_type(m_pStmt, Col - 1) == SQLITE_NULL; +} + +float CSqliteConnection::GetFloat(int Col) const +{ + return (float)sqlite3_column_double(m_pStmt, Col - 1); +} + +int CSqliteConnection::GetInt(int Col) const +{ + return sqlite3_column_int(m_pStmt, Col - 1); +} + +void CSqliteConnection::GetString(int Col, char *pBuffer, int BufferSize) const +{ + str_copy(pBuffer, (const char *)sqlite3_column_text(m_pStmt, Col - 1), BufferSize); +} + +int CSqliteConnection::GetBlob(int Col, unsigned char *pBuffer, int BufferSize) const +{ + int Size = sqlite3_column_bytes(m_pStmt, Col - 1); + Size = minimum(Size, BufferSize); + mem_copy(pBuffer, sqlite3_column_blob(m_pStmt, Col - 1), Size); + return Size; +} + +bool CSqliteConnection::Execute(const char *pQuery) +{ + char *pErrorMsg; + int Result = sqlite3_exec(m_pDb, pQuery, NULL, NULL, &pErrorMsg); + if(Result != SQLITE_OK) + { + dbg_msg("sql", "error executing query: '%s'", pErrorMsg); + sqlite3_free(pErrorMsg); + } + return Result == SQLITE_OK; +} + +void CSqliteConnection::ExceptionOnError(int Result) +{ + if(Result != SQLITE_OK) + { + throw std::runtime_error(sqlite3_errmsg(m_pDb)); + } +} + +void CSqliteConnection::AddPoints(const char *pPlayer, int Points) +{ + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), + "INSERT INTO %s_points(Name, Points) " + "VALUES (?, ?) " + "ON CONFLICT(Name) UPDATE SET Points=Points+?;", + GetPrefix()); + PrepareStatement(aBuf); + BindString(1, pPlayer); + BindInt(2, Points); + BindInt(3, Points); + Step(); +} diff --git a/src/engine/server/databases/sqlite.h b/src/engine/server/databases/sqlite.h new file mode 100644 index 000000000..e3c43efe3 --- /dev/null +++ b/src/engine/server/databases/sqlite.h @@ -0,0 +1,65 @@ +#ifndef ENGINE_SERVER_DATABASES_SQLITE_H +#define ENGINE_SERVER_DATABASES_SQLITE_H + +#include "connection.h" +#include + +struct sqlite3; +struct sqlite3_stmt; + +class CSqliteConnection : public IDbConnection +{ +public: + CSqliteConnection(const char *pFilename, bool Setup); + virtual ~CSqliteConnection(); + virtual void Print(IConsole *pConsole, const char *Mode); + + virtual CSqliteConnection *Copy(); + + virtual const char *BinaryCollate() const { return "BINARY"; } + virtual void ToUnixTimestamp(const char *pTimestamp, char *aBuf, unsigned int BufferSize); + virtual const char *InsertTimestampAsUtc() const { return "DATETIME(?, 'utc')"; } + virtual const char *CollateNocase() const { return "? COLLATE NOCASE"; } + + virtual Status Connect(); + virtual void Disconnect(); + + virtual void Lock(const char *pTable); + virtual void Unlock(); + + virtual void PrepareStatement(const char *pStmt); + + virtual void BindString(int Idx, const char *pString); + virtual void BindBlob(int Idx, unsigned char *pBlob, int Size); + virtual void BindInt(int Idx, int Value); + virtual void BindFloat(int Idx, float Value); + + 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; + // passing a negative buffer size is undefined behavior + virtual int GetBlob(int Col, unsigned char *pBuffer, int BufferSize) const; + + virtual void AddPoints(const char *pPlayer, int Points); + +private: + // copy of config vars + char m_aFilename[512]; + bool m_Setup; + + sqlite3 *m_pDb; + sqlite3_stmt *m_pStmt; + bool m_Done; // no more rows available for Step + bool m_Locked; + // returns true, if the query succeded + bool Execute(const char *pQuery); + + void ExceptionOnError(int Result); + + std::atomic_bool m_InUse; +}; + +#endif // ENGINE_SERVER_DATABASES_SQLITE_H diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index 7c9d09d14..cdf4cba1a 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -44,6 +44,10 @@ #include #endif +#include +#include +#include + CSnapIDPool::CSnapIDPool() { @@ -297,16 +301,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; @@ -2307,6 +2302,23 @@ int CServer::Run() return -1; } + if(g_Config.m_SvSqliteFile[0] != '\0') + { + auto pSqlServers = std::unique_ptr(new CSqliteConnection( + g_Config.m_SvSqliteFile, true)); + + if(g_Config.m_SvUseSQL) + { + DbPool()->RegisterDatabase(std::move(pSqlServers), CDbConnectionPool::WRITE_BACKUP); + } + else + { + auto pCopy = std::unique_ptr(pSqlServers->Copy()); + DbPool()->RegisterDatabase(std::move(pSqlServers), CDbConnectionPool::READ); + DbPool()->RegisterDatabase(std::move(pCopy), CDbConnectionPool::WRITE); + } + } + // start server NETADDR BindAddr; int NetType = g_Config.m_SvIpv4Only ? NETTYPE_IPV4 : NETTYPE_ALL; @@ -2584,22 +2596,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(); @@ -3065,6 +3069,8 @@ void CServer::ConLogout(IConsole::IResult *pResult, void *pUser) void CServer::ConShowIps(IConsole::IResult *pResult, void *pUser) { + if(!g_Config.m_SvUseSQL) + return; CServer *pServer = (CServer *)pUser; if(pServer->m_RconClientID >= 0 && pServer->m_RconClientID < MAX_CLIENTS && @@ -3084,10 +3090,10 @@ void CServer::ConShowIps(IConsole::IResult *pResult, void *pUser) } } -#if defined (CONF_SQL) - void CServer::ConAddSqlServer(IConsole::IResult *pResult, void *pUserData) { + if(!g_Config.m_SvUseSQL) + return; CServer *pSelf = (CServer *)pUserData; if (pResult->NumArguments() != 7 && pResult->NumArguments() != 8) @@ -3109,60 +3115,42 @@ 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++) - { - 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; - } - } - pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "failed to add new sqlserver: limit of sqlservers reached"); + 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); + pSelf->DbPool()->RegisterDatabase(std::move(pSqlServers), ReadOnly ? CDbConnectionPool::READ : CDbConnectionPool::WRITE); } void CServer::ConDumpSqlServers(IConsole::IResult *pResult, void *pUserData) { CServer *pSelf = (CServer *)pUserData; - bool ReadOnly; - if (str_comp_nocase(pResult->GetString(0), "w") == 0) - ReadOnly = false; - else if (str_comp_nocase(pResult->GetString(0), "r") == 0) - ReadOnly = true; + if(str_comp_nocase(pResult->GetString(0), "w") == 0) + { + pSelf->DbPool()->Print(pSelf->Console(), CDbConnectionPool::WRITE); + pSelf->DbPool()->Print(pSelf->Console(), CDbConnectionPool::WRITE_BACKUP); + } + else if(str_comp_nocase(pResult->GetString(0), "r") == 0) + { + pSelf->DbPool()->Print(pSelf->Console(), CDbConnectionPool::READ); + } else { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "choose either 'r' for SqlReadServer or 'w' for SqlWriteServer"); return; } - CSqlServer** apSqlServers = ReadOnly ? pSelf->m_apSqlReadServers : pSelf->m_apSqlWriteServers; - - for (int i = 0; i < MAX_SQLSERVERS; i++) - if (apSqlServers[i]) - { - char aBuf[512]; - 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 +3351,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_connector.cpp b/src/engine/server/sql_connector.cpp deleted file mode 100644 index a702d44d5..000000000 --- a/src/engine/server/sql_connector.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#if defined(CONF_SQL) - -#include - -#include "sql_connector.h" - -CSqlServer** CSqlConnector::ms_ppSqlReadServers = 0; -CSqlServer** CSqlConnector::ms_ppSqlWriteServers = 0; - -int CSqlConnector::ms_ReachableReadServer = 0; -int CSqlConnector::ms_ReachableWriteServer = 0; - -CSqlConnector::CSqlConnector() : -m_pSqlServer(0), -m_NumReadRetries(0), -m_NumWriteRetries(0) -{} - -bool CSqlConnector::ConnectSqlServer(bool ReadOnly) -{ - ReadOnly ? ++m_NumReadRetries : ++m_NumWriteRetries; - int& ReachableServer = ReadOnly ? ms_ReachableReadServer : ms_ReachableWriteServer; - int NumServers = ReadOnly ? CSqlServer::ms_NumReadServer : CSqlServer::ms_NumWriteServer; - - for (int i = ReachableServer, ID = ReachableServer; i < ReachableServer + NumServers && SqlServer(i % NumServers, ReadOnly); i++, ID = i % NumServers) - { - if (SqlServer(ID, ReadOnly) && SqlServer(ID, ReadOnly)->Connect()) - { - m_pSqlServer = SqlServer(ID, ReadOnly); - ReachableServer = ID; - return true; - } - if (SqlServer(ID, ReadOnly)) - dbg_msg("sql", "Warning: Unable to connect to Sql%sServer %d ('%s'), trying next...", ReadOnly ? "Read" : "Write", ID, SqlServer(ID, ReadOnly)->GetIP()); - } - dbg_msg("sql", "FATAL ERROR: No Sql%sServers available", ReadOnly ? "Read" : "Write"); - m_pSqlServer = 0; - return false; -} - -#endif diff --git a/src/engine/server/sql_connector.h b/src/engine/server/sql_connector.h deleted file mode 100644 index 4d24fdff5..000000000 --- a/src/engine/server/sql_connector.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef ENGINE_SERVER_SQL_CONNECTOR_H -#define ENGINE_SERVER_SQL_CONNECTOR_H - -#include "sql_server.h" - -enum -{ - MAX_SQLSERVERS=15 -}; - -// implementation to provide sqlservers -class CSqlConnector -{ -public: - CSqlConnector(); - - CSqlServer* SqlServer(int i, bool ReadOnly = true) { return ReadOnly ? ms_ppSqlReadServers[i] : ms_ppSqlWriteServers[i]; } - - // always returns the last connected sql-server - CSqlServer* SqlServer() { return m_pSqlServer; } - - static void SetReadServers(CSqlServer** ppReadServers) { ms_ppSqlReadServers = ppReadServers; } - static void SetWriteServers(CSqlServer** ppWriteServers) { ms_ppSqlWriteServers = ppWriteServers; } - - static void ResetReachable() { ms_ReachableReadServer = 0; ms_ReachableWriteServer = 0; } - - bool ConnectSqlServer(bool ReadOnly = true); - - bool MaxTriesReached(bool ReadOnly = true) { return ReadOnly ? m_NumReadRetries >= CSqlServer::ms_NumReadServer : m_NumWriteRetries >= CSqlServer::ms_NumWriteServer; } - -private: - - CSqlServer *m_pSqlServer; - static CSqlServer **ms_ppSqlReadServers; - static CSqlServer **ms_ppSqlWriteServers; - - static int ms_NumReadServer; - static int ms_NumWriteServer; - - static int ms_ReachableReadServer; - static int ms_ReachableWriteServer; - - int m_NumReadRetries; - int m_NumWriteRetries; -}; - -#endif diff --git a/src/engine/server/sql_server.cpp b/src/engine/server/sql_server.cpp deleted file mode 100644 index 285d59aba..000000000 --- a/src/engine/server/sql_server.cpp +++ /dev/null @@ -1,223 +0,0 @@ -#if defined(CONF_SQL) - -#include -#include -#include - -#include "sql_server.h" - - -int CSqlServer::ms_NumReadServer = 0; -int CSqlServer::ms_NumWriteServer = 0; - -CSqlServer::CSqlServer(const char *pDatabase, const char *pPrefix, const char *pUser, const char *pPass, const char *pIp, int Port, lock *pGlobalLock, bool ReadOnly, bool SetUpDb) : - m_Port(Port), - m_SetUpDB(SetUpDb), - m_SqlLock(), - m_pGlobalLock(pGlobalLock) -{ - str_copy(m_aDatabase, pDatabase, sizeof(m_aDatabase)); - str_copy(m_aPrefix, pPrefix, sizeof(m_aPrefix)); - str_copy(m_aUser, pUser, sizeof(m_aUser)); - str_copy(m_aPass, pPass, sizeof(m_aPass)); - str_copy(m_aIp, pIp, sizeof(m_aIp)); - - m_pDriver = 0; - m_pConnection = 0; - m_pResults = 0; - m_pStatement = 0; - - ReadOnly ? ms_NumReadServer++ : ms_NumWriteServer++; -} - -CSqlServer::~CSqlServer() -{ - scope_lock LockScope(&m_SqlLock); - try - { - if (m_pResults) - delete m_pResults; - if (m_pConnection) - { - delete m_pConnection; - m_pConnection = 0; - } - dbg_msg("sql", "SQL connection disconnected"); - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "ERROR: No SQL connection: %s", e.what()); - } - catch (const std::exception& ex) - { - dbg_msg("sql", "ERROR: No SQL connection: %s", ex.what()); - } - catch (const std::string& ex) - { - dbg_msg("sql", "ERROR: No SQL connection: %s", ex.c_str()); - } - catch (...) - { - dbg_msg("sql", "Unknown Error cause by the MySQL/C++ Connector"); - } -} - -bool CSqlServer::Connect() -{ - m_SqlLock.take(); - - if (m_pDriver != NULL && m_pConnection != NULL) - { - try - { - // Connect to specific database - m_pConnection->setSchema(m_aDatabase); - return true; - } - 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_SqlLock.release(); - dbg_msg("sql", "ERROR: SQL connection failed"); - return false; - } - - try - { - m_pDriver = 0; - m_pConnection = 0; - m_pStatement = 0; - - 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_pGlobalLock); - m_pDriver = get_driver_instance(); - } - m_pConnection = m_pDriver->connect(connection_properties); - - // Create Statement - m_pStatement = m_pConnection->createStatement(); - - // Apparently OPT_CHARSET_NAME and OPT_SET_CHARSET_NAME are not enough - m_pStatement->execute("SET CHARACTER SET utf8mb4;"); - - if (m_SetUpDB) - { - char aBuf[128]; - // create database - str_format(aBuf, sizeof(aBuf), "CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4", m_aDatabase); - m_pStatement->execute(aBuf); - } - - // Connect to specific database - m_pConnection->setSchema(m_aDatabase); - dbg_msg("sql", "sql connection established"); - return true; - } - 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"); - } - - dbg_msg("sql", "ERROR: sql connection failed"); - m_SqlLock.release(); - return false; -} - -void CSqlServer::Disconnect() -{ - m_SqlLock.release(); -} - -bool CSqlServer::CreateTables() -{ - if (!Connect()) - return false; - - bool Success = false; - try - { - char aBuf[1024]; - - // create tables - str_format(aBuf, sizeof(aBuf), "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;", m_aPrefix, MAX_NAME_LENGTH); - executeSql(aBuf); - - str_format(aBuf, sizeof(aBuf), "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;", m_aPrefix, MAX_NAME_LENGTH); - executeSql(aBuf); - - str_format(aBuf, sizeof(aBuf), "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;", m_aPrefix); - executeSql(aBuf); - - str_format(aBuf, sizeof(aBuf), "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;", m_aPrefix); - executeSql(aBuf); - - str_format(aBuf, sizeof(aBuf), "CREATE TABLE IF NOT EXISTS %s_points (Name VARCHAR(%d) BINARY NOT NULL, Points INT DEFAULT 0, UNIQUE KEY Name (Name)) CHARACTER SET utf8mb4;", m_aPrefix, MAX_NAME_LENGTH); - executeSql(aBuf); - - dbg_msg("sql", "Tables were created successfully"); - Success = true; - } - catch (sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - } - - Disconnect(); - return Success; -} - -void CSqlServer::executeSql(const char *pCommand) -{ - m_pStatement->execute(pCommand); -} - -void CSqlServer::executeSqlQuery(const char *pQuery) -{ - if (m_pResults) - delete m_pResults; - - // set it to 0, so exceptions raised from executeQuery can not make m_pResults point to invalid memory - m_pResults = 0; - m_pResults = m_pStatement->executeQuery(pQuery); -} - -#endif diff --git a/src/engine/server/sql_server.h b/src/engine/server/sql_server.h deleted file mode 100644 index 3b6a39a8e..000000000 --- a/src/engine/server/sql_server.h +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef ENGINE_SERVER_SQL_SERVER_H -#define ENGINE_SERVER_SQL_SERVER_H - -#include - -#include - -#include -#include -#include - -class CSqlServer -{ -public: - CSqlServer(const char *pDatabase, const char *pPrefix, const char *pUser, const char *pPass, const char *pIp, int Port, lock *pGlobalLock, bool ReadOnly = true, bool SetUpDb = false); - ~CSqlServer(); - - bool Connect(); - void Disconnect(); - bool CreateTables(); - - void executeSql(const char *pCommand); - void executeSqlQuery(const char *pQuery); - - sql::ResultSet* GetResults() { return m_pResults; } - - const char* GetDatabase() { return m_aDatabase; } - const char* GetPrefix() { return m_aPrefix; } - const char* GetUser() { return m_aUser; } - const char* GetPass() { return m_aPass; } - const char* GetIP() { return m_aIp; } - int GetPort() { return m_Port; } - sql::Connection *Connection() const { return m_pConnection; } - - static int ms_NumReadServer; - static int ms_NumWriteServer; - -private: - sql::Driver *m_pDriver; - sql::Connection *m_pConnection; - sql::Statement *m_pStatement; - sql::ResultSet *m_pResults; - - // copy of config vars - char m_aDatabase[64]; - char m_aPrefix[64]; - char m_aUser[64]; - char m_aPass[64]; - char m_aIp[64]; - int m_Port; - - bool m_SetUpDB; - - lock m_SqlLock; - lock *m_pGlobalLock; -}; - -#endif diff --git a/src/engine/server/sql_string_helpers.cpp b/src/engine/server/sql_string_helpers.cpp index 44b7d8a1e..5de0c4bf2 100644 --- a/src/engine/server/sql_string_helpers.cpp +++ b/src/engine/server/sql_string_helpers.cpp @@ -1,8 +1,8 @@ +#include "sql_string_helpers.h" + +#include #include #include -#include - -#include "sql_string_helpers.h" void sqlstr::FuzzyString(char *pString, int size) { @@ -24,39 +24,21 @@ void sqlstr::FuzzyString(char *pString, int size) delete [] newString; } -// anti SQL injection -void sqlstr::ClearString(char *pString, int size) +int sqlstr::EscapeLike(char *pDst, const char *pSrc, int DstSize) { - char *newString = new char [size * 2 - 1]; - int pos = 0; - - for(int i = 0; i < size; i++) + int Pos = 0; + int DstPos = 0; + while(DstPos + 2 < DstSize) { - if(pString[i] == '\\') - { - newString[pos++] = '\\'; - newString[pos++] = '\\'; - } - else if(pString[i] == '\'') - { - newString[pos++] = '\\'; - newString[pos++] = '\''; - } - else if(pString[i] == '"') - { - newString[pos++] = '\\'; - newString[pos++] = '"'; - } - else - { - newString[pos++] = pString[i]; - } + if(pSrc[Pos] == '\0') + break; + if(pSrc[Pos] == '\\' || pSrc[Pos] == '%' || pSrc[Pos] == '_' || pSrc[Pos] == '[') + pDst[DstPos++] = '\\'; + pDst[DstPos++] = pSrc[Pos++]; + } - - newString[pos] = '\0'; - - str_copy(pString, newString, size); - delete [] newString; + pDst[DstPos++] = '\0'; + return DstPos; } void sqlstr::AgoTimeToString(int AgoTime, char *pAgoString) diff --git a/src/engine/server/sql_string_helpers.h b/src/engine/server/sql_string_helpers.h index c20408e4b..a827343e5 100644 --- a/src/engine/server/sql_string_helpers.h +++ b/src/engine/server/sql_string_helpers.h @@ -1,52 +1,16 @@ #ifndef ENGINE_SERVER_SQL_STRING_HELPERS_H #define ENGINE_SERVER_SQL_STRING_HELPERS_H -#include - namespace sqlstr { 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 -class CSqlString -{ -public: - CSqlString() {} - - CSqlString(const char *pStr) - { - str_copy(m_aString, pStr, size); - str_copy(m_aClearString, pStr, size); - ClearString(m_aClearString, sizeof(m_aClearString)); - } - - const char* Str() const { return m_aString; } - const char* ClrStr() const { return m_aClearString; } - - CSqlString& operator=(const char *pStr) - { - str_copy(m_aString, pStr, size); - str_copy(m_aClearString, pStr, size); - ClearString(m_aClearString, sizeof(m_aClearString)); - return *this; - } - - bool operator<(const CSqlString& other) const - { - return str_comp(m_aString, other.m_aString) < 0; - } - -private: - char m_aString[size]; - char m_aClearString[size * 2 - 1]; -}; - } #endif diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index c62a1eeec..3d27fccb6 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-server.sqlite", CFGFLAG_SERVER, "File to store ranks in case sv_use_sql is turned off or used as backup sql server") #if defined(CONF_UPNP) MACRO_CONFIG_INT(SvUseUPnP, sv_use_upnp, 0, 0, 1, CFGFLAG_SERVER, "Enables UPnP support.") diff --git a/src/engine/shared/uuid_manager.cpp b/src/engine/shared/uuid_manager.cpp index a27f3482e..b4a69eb89 100644 --- a/src/engine/shared/uuid_manager.cpp +++ b/src/engine/shared/uuid_manager.cpp @@ -55,6 +55,14 @@ void FormatUuid(CUuid Uuid, char *pBuffer, unsigned BufferLength) p[8], p[9], p[10], p[11], p[12], p[13], p[14], p[15]); } +void ParseUuid(CUuid *pUuid, char *pBuffer) +{ + unsigned char *p = pUuid->m_aData; + sscanf(pBuffer, "%02hhX%02hhX%02hhX%02hhX-%02hhX%02hhX-%02hhX%02hhX-%02hhX%02hhX-%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX", + &p[0], &p[1], &p[2], &p[3], &p[4], &p[5], &p[6], &p[7], + &p[8], &p[9], &p[10], &p[11], &p[12], &p[13], &p[14], &p[15]); +} + bool CUuid::operator==(const CUuid& Other) { return mem_comp(this, &Other, sizeof(*this)) == 0; diff --git a/src/engine/shared/uuid_manager.h b/src/engine/shared/uuid_manager.h index e05ad2d3e..0e45cbda0 100644 --- a/src/engine/shared/uuid_manager.h +++ b/src/engine/shared/uuid_manager.h @@ -25,6 +25,7 @@ CUuid RandomUuid(); CUuid CalculateUuid(const char *pName); // The buffer length should be at least UUID_MAXSTRSIZE. void FormatUuid(CUuid Uuid, char *pBuffer, unsigned BufferLength); +void ParseUuid(CUuid *pUuid, char *pBuffer); struct CName { diff --git a/src/game/server/entities/character.h b/src/game/server/entities/character.h index 40114cb79..8a7657a9a 100644 --- a/src/game/server/entities/character.h +++ b/src/game/server/entities/character.h @@ -13,7 +13,6 @@ class CAntibot; class CGameTeams; -class CSaveTee; struct CAntibotCharacterData; enum diff --git a/src/game/server/entities/plasma.cpp b/src/game/server/entities/plasma.cpp index 6172173c8..b71ff5faa 100644 --- a/src/game/server/entities/plasma.cpp +++ b/src/game/server/entities/plasma.cpp @@ -7,7 +7,7 @@ #include #include "plasma.h" -const float ACCEL = 1.1f; +const float PLASMA_ACCEL = 1.1f; CPlasma::CPlasma(CGameWorld *pGameWorld, vec2 Pos, vec2 Dir, bool Freeze, bool Explosive, int ResponsibleTeam) : @@ -44,7 +44,7 @@ bool CPlasma::HitCharacter() void CPlasma::Move() { m_Pos += m_Core; - m_Core *= ACCEL; + m_Core *= PLASMA_ACCEL; } void CPlasma::Reset() diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index c2c2c6833..a0f6391e3 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 { @@ -3124,17 +3120,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; @@ -3384,11 +3374,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 ed4255f04..12ed2d3e2 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; @@ -228,7 +228,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(); @@ -279,7 +279,7 @@ public: private: bool m_VoteWillPass; - class IScore *m_pScore; + class CScore *m_pScore; //DDRace Console Commands @@ -412,7 +412,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/save.cpp b/src/game/server/save.cpp index f5563c021..dba931519 100644 --- a/src/game/server/save.cpp +++ b/src/game/server/save.cpp @@ -290,7 +290,7 @@ char* CSaveTee::GetString(const CSaveTeam *pTeam) return m_aString; } -int CSaveTee::LoadString(const char* String) +int CSaveTee::FromString(const char* String) { int Num; Num = sscanf(String, @@ -552,7 +552,7 @@ char* CSaveTeam::GetString() return m_aString; } -int CSaveTeam::LoadString(const char* String) +int CSaveTeam::FromString(const char* String) { char TeamStats[MAX_CLIENTS]; char Switcher[64]; @@ -636,7 +636,7 @@ int CSaveTeam::LoadString(const char* String) if(StrSize < sizeof(SaveTee)) { str_copy(SaveTee, CopyPos, StrSize); - int Num = m_pSavedTees[n].LoadString(SaveTee); + int Num = m_pSavedTees[n].FromString(SaveTee); if(Num) { dbg_msg("load", "failed to load tee"); diff --git a/src/game/server/save.h b/src/game/server/save.h index 7b6e48bd2..6eaafd53c 100644 --- a/src/game/server/save.h +++ b/src/game/server/save.h @@ -18,7 +18,7 @@ public: void save(CCharacter* pchr); void load(CCharacter* pchr, int Team); char* GetString(const CSaveTeam *pTeam); - int LoadString(const char* String); + int FromString(const char* String); void LoadHookedPlayer(const CSaveTeam *pTeam); vec2 GetPos() const { return m_Pos; } const char* GetName() const { return m_aName; } @@ -115,7 +115,7 @@ public: char* GetString(); int GetMembersCount() const { return m_MembersCount; } // MatchPlayers has to be called afterwards - int LoadString(const char* String); + int FromString(const char* String); // returns true if a team can load, otherwise writes a nice error Message in pMessage bool MatchPlayers(const char (*paNames)[MAX_NAME_LENGTH], const int *pClientID, int NumPlayer, char *pMessage, int MessageLen); int save(int Team); diff --git a/src/game/server/score.cpp b/src/game/server/score.cpp index 39edec3ad..7231d5ba4 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,1541 @@ void CScorePlayerResult::SetVariant(Variant v) m_Data.m_Info.m_CpTime[i] = 0; } } + +CTeamrank::CTeamrank() : + m_NumNames(0) +{ + for(int i = 0; i < MAX_CLIENTS; i++) + m_aaNames[i][0] = '\0'; + mem_zero(&m_TeamID.m_aData, sizeof(m_TeamID)); +} + +bool CTeamrank::NextSqlResult(IDbConnection *pSqlServer) +{ + pSqlServer->GetBlob(1, m_TeamID.m_aData, sizeof(m_TeamID.m_aData)); + pSqlServer->GetString(2, m_aaNames[0], sizeof(m_aaNames[0])); + m_NumNames = 1; + while(pSqlServer->Step()) + { + CUuid TeamID; + pSqlServer->GetBlob(1, TeamID.m_aData, sizeof(TeamID.m_aData)); + if(m_TeamID != TeamID) + return true; + pSqlServer->GetString(2, m_aaNames[m_NumNames], sizeof(m_aaNames[m_NumNames])); + m_NumNames++; + } + return false; +} + +bool CTeamrank::SamePlayers(const std::vector *aSortedNames) +{ + if(aSortedNames->size() != m_NumNames) + return false; + for(unsigned int i = 0; i < m_NumNames; i++) + { + if(str_comp(aSortedNames->at(i).c_str(), m_aaNames[i]) != 0) + return false; + } + return true; +} + +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 CURRENT_TIMESTAMP AS Current, MIN(Timestamp) AS Stamp " + "FROM %s_race " + "WHERE Name = ?", + pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_RequestingPlayer); + + if(pSqlServer->Step() && !pSqlServer->IsNull(2)) + { + char aCurrent[TIMESTAMP_STR_LENGTH]; + pSqlServer->GetString(1, aCurrent, sizeof(aCurrent)); + char aStamp[TIMESTAMP_STR_LENGTH]; + pSqlServer->GetString(2, aStamp, sizeof(aStamp)); + int CurrentYear, CurrentMonth, CurrentDay; + int StampYear, StampMonth, StampDay; + if(sscanf(aCurrent, "%d-%d-%d", &CurrentYear, &CurrentMonth, &CurrentDay) == 3 + && sscanf(aStamp, "%d-%d-%d", &StampYear, &StampMonth, &StampDay) == 3 + && CurrentMonth == StampMonth && CurrentDay == StampDay) + pData->m_pResult->m_Data.m_Info.m_Birthday = CurrentYear - StampYear; + } + 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 %s " + "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->CollateNocase()); + 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 aCurrentTimestamp[512]; + pSqlServer->ToUnixTimestamp("CURRENT_TIMESTAMP", aCurrentTimestamp, sizeof(aCurrentTimestamp)); + char aTimestamp[512]; + pSqlServer->ToUnixTimestamp("l.Timestamp", aTimestamp, sizeof(aTimestamp)); + + 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, " + "%s AS Stamp, " + "%s-%s AS Ago, " + "(SELECT MIN(Time) FROM %s_race WHERE Map = l.Map AND Name = ?) AS OwnTime " + "FROM (" + "SELECT * FROM %s_maps " + "WHERE Map LIKE %s " + "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(), + aTimestamp, aCurrentTimestamp, aTimestamp, + pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), + pSqlServer->CollateNocase() + ); + 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); + pSqlServer->AddPoints(pData->m_Name, Points); + str_format(paMessages[0], sizeof(paMessages[0]), + "You earned %d point%s for finishing this map!", + Points, Points == 1 ? "" : "s"); + } + } + + // save score. Can't fail, because no UNIQUE/PRIMARY KEY constrain is defined. + str_format(aBuf, sizeof(aBuf), + "INSERT 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, %.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, " + "?, false);", + pSqlServer->GetPrefix(), pSqlServer->InsertTimestampAsUtc(), 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]); + 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->BindString(5, pData->m_GameUuid); + 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[512]; + + // 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]); + + char aTable[512]; + str_format(aTable, sizeof(aTable), + "%s_teamrace WRITE, %s_teamrace AS r WRITE", + pSqlServer->GetPrefix(), pSqlServer->GetPrefix()); + pSqlServer->Lock(aTable); + std::sort(aNames.begin(), aNames.end()); + str_format(aBuf, sizeof(aBuf), + "SELECT l.ID, Name, 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 " + "ORDER BY l.ID, Name ", + pSqlServer->GetPrefix(), pSqlServer->GetPrefix()); + pSqlServer->PrepareStatement(aBuf); + pSqlServer->BindString(1, pData->m_Map); + pSqlServer->BindString(2, pData->m_aNames[0]); + + CUuid GameID; + bool FoundTeam = false; + float Time; + CTeamrank Teamrank; + if(pSqlServer->Step()) + { + bool SearchTeam = true; + while(SearchTeam) + { + Time = pSqlServer->GetFloat(3); + SearchTeam = Teamrank.NextSqlResult(pSqlServer); + if(str_comp(Teamrank.m_aaNames[0], aNames[0].c_str()) != 0) + { + dbg_msg("sql", "insert team rank logic error: " + "first team member from sql (%s) should be first name in array (%s), " + "because both are sorted by binary values", Teamrank.m_aaNames[0], aNames[0].c_str()); + return false; + } + if(Teamrank.SamePlayers(&aNames)) + { + FoundTeam = true; + break; + } + } + } + if(FoundTeam) + { + 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->BindBlob(3, GameID.m_aData, sizeof(GameID.m_aData)); + pSqlServer->Step(); + } + } + else + { + CUuid GameID = RandomUuid(); + for(unsigned int i = 0; i < pData->m_Size; i++) + { + // if no entry found... create a new one + str_format(aBuf, sizeof(aBuf), + "INSERT INTO %s_teamrace(Map, Name, Timestamp, Time, ID, GameID, DDNet7) " + "VALUES (?, ?, %s, %.2f, ?, ?, false);", + pSqlServer->GetPrefix(), pSqlServer->InsertTimestampAsUtc(), 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->BindBlob(4, GameID.m_aData, sizeof(GameID.m_aData)); + pSqlServer->BindString(5, pData->m_GameUuid); + pSqlServer->Step(); + } + } + pSqlServer->Unlock(); + + 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 l.ID, Name, 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 ", + 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()) + { + float Time = pSqlServer->GetFloat(3); + int Rank = pSqlServer->GetInt(4); + CTeamrank Teamrank; + Teamrank.NextSqlResult(pSqlServer); + + char aFormattedNames[512] = ""; + for(unsigned int Name = 0; Name < Teamrank.m_NumNames; Name++) + { + str_append(aFormattedNames, Teamrank.m_aaNames[Name], sizeof(aFormattedNames)); + + if (Name < Teamrank.m_NumNames - 2) + str_append(aFormattedNames, ", ", sizeof(aFormattedNames)); + else if (Name < Teamrank.m_NumNames - 1) + str_append(aFormattedNames, " & ", sizeof(aFormattedNames)); + } + + 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 Last = 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()) + { + Last = 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(Last) + { + Line++; + 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 aCurrentTimestamp[512]; + pSqlServer->ToUnixTimestamp("CURRENT_TIMESTAMP", aCurrentTimestamp, sizeof(aCurrentTimestamp)); + char aTimestamp[512]; + pSqlServer->ToUnixTimestamp("Timestamp", aTimestamp, sizeof(aTimestamp)); + char aBuf[512]; + if(pData->m_Name[0] != '\0') // last 5 times of a player + { + str_format(aBuf, sizeof(aBuf), + "SELECT Time, (%s-%s) as Ago, %s as Stamp " + "FROM %s_race " + "WHERE Map = ? AND Name = ? " + "ORDER BY Timestamp %s " + "LIMIT ?, 5;", + aCurrentTimestamp, aTimestamp, aTimestamp, + 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, (%s-%s) as Ago, %s as Stamp, Name " + "FROM %s_race " + "WHERE Map = ? " + "ORDER BY Timestamp %s " + "LIMIT ?, 5;", + aCurrentTimestamp, aTimestamp, aTimestamp, + 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 WRITE", 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 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->ExecuteWrite(LoadTeamThread, std::move(Tmp), "load team"); +} + +bool CScore::LoadTeamThread(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure) +{ + const CSqlTeamLoad *pData = dynamic_cast(pGameData); + pData->m_pResult->m_Status = CScoreSaveResult::LOAD_FAILED; + + char aTable[512]; + str_format(aTable, sizeof(aTable), "%s_saves WRITE", 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 aCurrentTimestamp[512]; + pSqlServer->ToUnixTimestamp("CURRENT_TIMESTAMP", aCurrentTimestamp, sizeof(aCurrentTimestamp)); + char aTimestamp[512]; + pSqlServer->ToUnixTimestamp("Timestamp", aTimestamp, sizeof(aTimestamp)); + + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), + "SELECT Savegame, Server, %s-%s AS Ago, SaveID " + "FROM %s_saves " + "where Code = ? AND Map = ? AND DDNet7 = false AND Savegame LIKE ?;", + aCurrentTimestamp, aTimestamp, + 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; + } + + char aSaveID[UUID_MAXSTRSIZE]; + memset(pData->m_pResult->m_SaveID.m_aData, 0, sizeof(pData->m_pResult->m_SaveID.m_aData)); + if(!pSqlServer->IsNull(4)) + { + pSqlServer->GetString(4, aSaveID, sizeof(aSaveID)); + if(str_length(aSaveID) + 1 != UUID_MAXSTRSIZE) + { + strcpy(pData->m_pResult->m_aMessage, "Unable to load savegame: SaveID corrupted"); + goto end; + } + ParseUuid(&pData->m_pResult->m_SaveID, aSaveID); + } + + char aSaveString[65536]; + pSqlServer->GetString(1, aSaveString, sizeof(aSaveString)); + int Num = pData->m_pResult->m_SavedTeam.FromString(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 aCurrentTimestamp[512]; + pSqlServer->ToUnixTimestamp("CURRENT_TIMESTAMP", aCurrentTimestamp, sizeof(aCurrentTimestamp)); + char aMaxTimestamp[512]; + pSqlServer->ToUnixTimestamp("MAX(Timestamp)", aMaxTimestamp, sizeof(aMaxTimestamp)); + + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), + "SELECT COUNT(*) AS NumSaves, %s-%s AS Ago " + "FROM %s_saves " + "WHERE Map = ? AND Savegame LIKE ?;", + aCurrentTimestamp, aMaxTimestamp, + 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..c2031fa36 100644 --- a/src/game/server/score.h +++ b/src/game/server/score.h @@ -3,11 +3,21 @@ #include #include +#include +#include -#include +#include #include +#include +#include + #include "save.h" +struct ISqlData; +class IDbConnection; +class IServer; +class CGameContext; + enum { NUM_CHECKPOINTS = 25, @@ -132,43 +142,213 @@ 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; +}; + +struct CTeamrank { + CUuid m_TeamID; + char m_aaNames[MAX_CLIENTS][MAX_NAME_LENGTH]; + unsigned int m_NumNames; + CTeamrank(); + + // Assumes that a database query equivalent to + // + // SELECT TeamID, Name [, ...] -- the order is important + // FROM record_teamrace + // ORDER BY TeamID, Name + // + // was executed and that the result line of the first team member is already selected. + // Afterwards the team member of the next team is selected. + // Returns if another team can be extracted + bool NextSqlResult(IDbConnection *pSqlServer); + + bool SamePlayers(const std::vector *aSortedNames); +}; + +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, bool Failure); + + 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