#include "scoreworker.h"

#include <base/log.h>
#include <base/system.h>

#include <engine/server/databases/connection.h>
#include <engine/server/databases/connection_pool.h>
#include <engine/server/sql_string_helpers.h>
#include <engine/shared/config.h>

#include <cmath>

// "6b407e81-8b77-3e04-a207-8da17f37d000"
// "save-no-save-id@ddnet.tw"
static const CUuid UUID_NO_SAVE_ID =
	{{0x6b, 0x40, 0x7e, 0x81, 0x8b, 0x77, 0x3e, 0x04,
		0xa2, 0x07, 0x8d, 0xa1, 0x7f, 0x37, 0xd0, 0x00}};

CScorePlayerResult::CScorePlayerResult()
{
	SetVariant(Variant::DIRECT);
}

void CScorePlayerResult::SetVariant(Variant v)
{
	m_MessageKind = v;
	switch(v)
	{
	case DIRECT:
	case ALL:
		for(auto &aMessage : m_Data.m_aaMessages)
			aMessage[0] = 0;
		break;
	case BROADCAST:
		m_Data.m_aBroadcast[0] = 0;
		break;
	case MAP_VOTE:
		m_Data.m_MapVote.m_aMap[0] = '\0';
		m_Data.m_MapVote.m_aReason[0] = '\0';
		m_Data.m_MapVote.m_aServer[0] = '\0';
		break;
	case PLAYER_INFO:
		m_Data.m_Info.m_Birthday = 0;
		m_Data.m_Info.m_Time.reset();
		for(float &TimeCp : m_Data.m_Info.m_aTimeCp)
			TimeCp = 0;
		break;
	case PLAYER_TIMECP:
		m_Data.m_Info.m_aRequestedPlayer[0] = '\0';
		m_Data.m_Info.m_Time.reset();
		for(float &TimeCp : m_Data.m_Info.m_aTimeCp)
			TimeCp = 0;
		break;
	}
}

CTeamrank::CTeamrank() :
	m_NumNames(0)
{
	for(auto &aName : m_aaNames)
		aName[0] = '\0';
	mem_zero(&m_TeamId.m_aData, sizeof(m_TeamId));
}

bool CTeamrank::NextSqlResult(IDbConnection *pSqlServer, bool *pEnd, char *pError, int ErrorSize)
{
	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;
	bool End = false;
	while(pSqlServer->Step(&End, pError, ErrorSize) && !End)
	{
		CUuid TeamId;
		pSqlServer->GetBlob(1, TeamId.m_aData, sizeof(TeamId.m_aData));
		if(m_TeamId != TeamId)
		{
			*pEnd = false;
			return true;
		}
		pSqlServer->GetString(2, m_aaNames[m_NumNames], sizeof(m_aaNames[m_NumNames]));
		m_NumNames++;
	}
	if(!End)
	{
		return false;
	}
	*pEnd = true;
	return true;
}

bool CTeamrank::SamePlayers(const std::vector<std::string> *pvSortedNames)
{
	if(pvSortedNames->size() != m_NumNames)
		return false;
	for(unsigned int i = 0; i < m_NumNames; i++)
	{
		if(str_comp(pvSortedNames->at(i).c_str(), m_aaNames[i]) != 0)
			return false;
	}
	return true;
}

bool CTeamrank::GetSqlTop5Team(IDbConnection *pSqlServer, bool *pEnd, char *pError, int ErrorSize, char (*paMessages)[512], int *Line, int Count)
{
	char aTime[32];
	int StartLine = *Line;
	for(*Line = StartLine; *Line < StartLine + Count; (*Line)++)
	{
		bool Last = false;
		float Time = pSqlServer->GetFloat(2);
		str_time_float(Time, TIME_HOURS_CENTISECS, aTime, sizeof(aTime));
		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);
			if(i < TeamSize - 2)
				str_append(aNames, ", ");
			else if(i == TeamSize - 2)
				str_append(aNames, " & ");
			if(!pSqlServer->Step(&Last, pError, ErrorSize))
			{
				return false;
			}
			if(Last)
			{
				break;
			}
		}
		str_format(paMessages[*Line], sizeof(paMessages[*Line]), "%d. %s Team Time: %s",
			Rank, aNames, aTime);
		if(Last)
		{
			(*Line)++;
			break;
		}
	}
	return true;
}

bool CScoreWorker::LoadBestTime(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlLoadBestTimeRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScoreLoadBestTimeResult *>(pGameData->m_pResult.get());

	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());
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aMap);

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		pResult->m_CurrentRecord = pSqlServer->GetFloat(1);
	}

	return true;
}

// update stuff
bool CScoreWorker::LoadPlayerData(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
	pResult->SetVariant(CScorePlayerResult::PLAYER_INFO);

	char aBuf[1024];
	// get best race time
	str_format(aBuf, sizeof(aBuf),
		"SELECT"
		"  (SELECT Time FROM %s_race WHERE Map = ? AND Name = ? ORDER BY Time ASC LIMIT 1) AS minTime, "
		"  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, "
		"  (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 > 0) AS hasCP, Time "
		"FROM %s_race "
		"WHERE Map = ? AND Name = ? "
		"ORDER BY hasCP DESC, Time ASC "
		"LIMIT 1",
		pSqlServer->GetPrefix(), pSqlServer->GetPrefix());
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}

	const char *pPlayer = pData->m_aName[0] != '\0' ? pData->m_aName : pData->m_aRequestingPlayer;
	pSqlServer->BindString(1, pData->m_aMap);
	pSqlServer->BindString(2, pData->m_aRequestingPlayer);
	pSqlServer->BindString(3, pData->m_aMap);
	pSqlServer->BindString(4, pPlayer);

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		if(!pSqlServer->IsNull(1))
		{
			// get the best time
			float Time = pSqlServer->GetFloat(1);
			pResult->m_Data.m_Info.m_Time = Time;
		}

		for(int i = 0; i < NUM_CHECKPOINTS; i++)
		{
			pResult->m_Data.m_Info.m_aTimeCp[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());
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aRequestingPlayer);

	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End && !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)
			pResult->m_Data.m_Info.m_Birthday = CurrentYear - StampYear;
	}
	return true;
}

bool CScoreWorker::LoadPlayerTimeCp(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
	auto *paMessages = pResult->m_Data.m_aaMessages;

	char aBuf[1024];
	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 = ? AND "
		"  (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) > 0 "
		"ORDER BY Time ASC "
		"LIMIT 1",
		pSqlServer->GetPrefix());
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}

	const char *pPlayer = pData->m_aName[0] != '\0' ? pData->m_aName : pData->m_aRequestingPlayer;
	pSqlServer->BindString(1, pData->m_aMap);
	pSqlServer->BindString(2, pPlayer);

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		pResult->SetVariant(CScorePlayerResult::PLAYER_TIMECP);
		pResult->m_Data.m_Info.m_Time = pSqlServer->GetFloat(1);
		for(int i = 0; i < NUM_CHECKPOINTS; i++)
		{
			pResult->m_Data.m_Info.m_aTimeCp[i] = pSqlServer->GetFloat(i + 2);
		}
		str_copy(pResult->m_Data.m_Info.m_aRequestedPlayer, pPlayer, sizeof(pResult->m_Data.m_Info.m_aRequestedPlayer));
	}
	else
	{
		pResult->SetVariant(CScorePlayerResult::DIRECT);
		str_format(paMessages[0], sizeof(paMessages[0]), "'%s' has no checkpoint times available", pPlayer);
	}
	return true;
}

bool CScoreWorker::MapVote(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
	auto *paMessages = pResult->m_Data.m_aaMessages;

	char aFuzzyMap[128];
	str_copy(aFuzzyMap, pData->m_aName, sizeof(aFuzzyMap));
	sqlstr::FuzzyString(aFuzzyMap, sizeof(aFuzzyMap));

	char aMapPrefix[128];
	str_copy(aMapPrefix, pData->m_aName, sizeof(aMapPrefix));
	str_append(aMapPrefix, "%");

	char aBuf[768];
	str_format(aBuf, sizeof(aBuf),
		"SELECT Map, Server "
		"FROM %s_maps "
		"WHERE Map LIKE %s "
		"ORDER BY "
		"  CASE WHEN LOWER(Map) = LOWER(?) THEN 0 ELSE 1 END, "
		"  CASE WHEN Map LIKE ? THEN 0 ELSE 1 END, "
		"  LENGTH(Map), Map "
		"LIMIT 1",
		pSqlServer->GetPrefix(), pSqlServer->CollateNocase());
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, aFuzzyMap);
	pSqlServer->BindString(2, pData->m_aName);
	pSqlServer->BindString(3, aMapPrefix);

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		pResult->SetVariant(CScorePlayerResult::MAP_VOTE);
		auto *pMapVote = &pResult->m_Data.m_MapVote;
		pSqlServer->GetString(1, pMapVote->m_aMap, sizeof(pMapVote->m_aMap));
		pSqlServer->GetString(2, pMapVote->m_aServer, sizeof(pMapVote->m_aServer));
		str_copy(pMapVote->m_aReason, "/map", sizeof(pMapVote->m_aReason));

		for(char *p = pMapVote->m_aServer; *p; p++) // lower case server
			*p = tolower(*p);
	}
	else
	{
		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_aName);
	}
	return true;
}

bool CScoreWorker::MapInfo(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());

	char aFuzzyMap[128];
	str_copy(aFuzzyMap, pData->m_aName, sizeof(aFuzzyMap));
	sqlstr::FuzzyString(aFuzzyMap, sizeof(aFuzzyMap));

	char aMapPrefix[128];
	str_copy(aMapPrefix, pData->m_aName, sizeof(aMapPrefix));
	str_append(aMapPrefix, "%");

	char aCurrentTimestamp[512];
	pSqlServer->ToUnixTimestamp("CURRENT_TIMESTAMP", aCurrentTimestamp, sizeof(aCurrentTimestamp));
	char aTimestamp[512];
	pSqlServer->ToUnixTimestamp("l.Timestamp", aTimestamp, sizeof(aTimestamp));

	char aMedianMapTime[2048];
	char aBuf[4096];
	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, "
		"  (%s) AS Median, "
		"  %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 LOWER(Map) = LOWER(?) 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->MedianMapTime(aMedianMapTime, sizeof(aMedianMapTime)),
		aTimestamp, aCurrentTimestamp, aTimestamp,
		pSqlServer->GetPrefix(), pSqlServer->GetPrefix(),
		pSqlServer->CollateNocase());
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aRequestingPlayer);
	pSqlServer->BindString(2, aFuzzyMap);
	pSqlServer->BindString(3, pData->m_aName);
	pSqlServer->BindString(4, aMapPrefix);

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		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);
		float Median = !pSqlServer->IsNull(8) ? pSqlServer->GetInt(8) : -1.0f;
		int Stamp = pSqlServer->GetInt(9);
		int Ago = pSqlServer->GetInt(10);
		float OwnTime = !pSqlServer->IsNull(11) ? pSqlServer->GetFloat(11) : -1.0f;

		char aAgoString[40] = "\0";
		char aReleasedString[60] = "\0";
		if(Stamp != 0)
		{
			sqlstr::AgoTimeToString(Ago, aAgoString, sizeof(aAgoString));
			str_format(aReleasedString, sizeof(aReleasedString), ", released %s ago", aAgoString);
		}

		char aMedianString[60] = "\0";
		if(Median > 0)
		{
			str_time((int64_t)Median * 100, TIME_HOURS, aBuf, sizeof(aBuf));
			str_format(aMedianString, sizeof(aMedianString), " in %s median", aBuf);
		}

		char aStars[20];
		switch(Stars)
		{
		case 0: str_copy(aStars, "✰✰✰✰✰", sizeof(aStars)); break;
		case 1: str_copy(aStars, "★✰✰✰✰", sizeof(aStars)); break;
		case 2: str_copy(aStars, "★★✰✰✰", sizeof(aStars)); break;
		case 3: str_copy(aStars, "★★★✰✰", sizeof(aStars)); break;
		case 4: str_copy(aStars, "★★★★✰", sizeof(aStars)); break;
		case 5: str_copy(aStars, "★★★★★", sizeof(aStars)); break;
		default: aStars[0] = '\0';
		}

		char aOwnFinishesString[40] = "\0";
		if(OwnTime > 0)
		{
			str_time_float(OwnTime, TIME_HOURS_CENTISECS, aBuf, sizeof(aBuf));
			str_format(aOwnFinishesString, sizeof(aOwnFinishesString),
				", your time: %s", aBuf);
		}

		str_format(pResult->m_Data.m_aaMessages[0], sizeof(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",
			aMedianString, aOwnFinishesString);
	}
	else
	{
		str_format(pResult->m_Data.m_aaMessages[0], sizeof(pResult->m_Data.m_aaMessages[0]),
			"No map like \"%s\" found.", pData->m_aName);
	}
	return true;
}

bool CScoreWorker::SaveScore(IDbConnection *pSqlServer, const ISqlData *pGameData, Write w, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlScoreData *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
	auto *paMessages = pResult->m_Data.m_aaMessages;

	char aBuf[1024];

	if(w == Write::NORMAL_SUCCEEDED)
	{
		str_format(aBuf, sizeof(aBuf),
			"DELETE FROM %s_race_backup WHERE GameId=? AND Name=? AND Timestamp=%s",
			pSqlServer->GetPrefix(), pSqlServer->InsertTimestampAsUtc());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aGameUuid);
		pSqlServer->BindString(2, pData->m_aName);
		pSqlServer->BindString(3, pData->m_aTimestamp);
		pSqlServer->Print();
		int NumDeleted;
		if(!pSqlServer->ExecuteUpdate(&NumDeleted, pError, ErrorSize))
		{
			return false;
		}
		if(NumDeleted == 0)
		{
			log_warn("sql", "Rank got moved out of backup database, will show up as duplicate rank in MySQL");
		}
		return true;
	}
	if(w == Write::NORMAL_FAILED)
	{
		int NumUpdated;
		// move to non-tmp table succeeded. delete from backup again
		str_format(aBuf, sizeof(aBuf),
			"INSERT INTO %s_race SELECT * FROM %s_race_backup WHERE GameId=? AND Name=? AND Timestamp=%s",
			pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pSqlServer->InsertTimestampAsUtc());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aGameUuid);
		pSqlServer->BindString(2, pData->m_aName);
		pSqlServer->BindString(3, pData->m_aTimestamp);
		pSqlServer->Print();
		if(!pSqlServer->ExecuteUpdate(&NumUpdated, pError, ErrorSize))
		{
			return false;
		}

		// move to non-tmp table succeeded. delete from backup again
		str_format(aBuf, sizeof(aBuf),
			"DELETE FROM %s_race_backup WHERE GameId=? AND Name=? AND Timestamp=%s",
			pSqlServer->GetPrefix(), pSqlServer->InsertTimestampAsUtc());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aGameUuid);
		pSqlServer->BindString(2, pData->m_aName);
		pSqlServer->BindString(3, pData->m_aTimestamp);
		pSqlServer->Print();
		if(!pSqlServer->ExecuteUpdate(&NumUpdated, pError, ErrorSize))
		{
			return false;
		}
		if(NumUpdated == 0)
		{
			log_warn("sql", "Rank got moved out of backup database, will show up as duplicate rank in MySQL");
		}
		return true;
	}

	if(w == Write::NORMAL)
	{
		str_format(aBuf, sizeof(aBuf),
			"SELECT COUNT(*) AS NumFinished FROM %s_race WHERE Map=? AND Name=? ORDER BY time ASC LIMIT 1",
			pSqlServer->GetPrefix());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aMap);
		pSqlServer->BindString(2, pData->m_aName);

		bool End;
		if(!pSqlServer->Step(&End, pError, ErrorSize))
		{
			return false;
		}
		int NumFinished = pSqlServer->GetInt(1);
		if(NumFinished == 0)
		{
			str_format(aBuf, sizeof(aBuf), "SELECT Points FROM %s_maps WHERE Map=?", pSqlServer->GetPrefix());
			if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
			{
				return false;
			}
			pSqlServer->BindString(1, pData->m_aMap);

			bool End2;
			if(!pSqlServer->Step(&End2, pError, ErrorSize))
			{
				return false;
			}
			if(!End2)
			{
				int Points = pSqlServer->GetInt(1);
				if(!pSqlServer->AddPoints(pData->m_aName, Points, pError, ErrorSize))
				{
					return false;
				}
				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),
		"%s INTO %s_race%s("
		"	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, "
		"	?, %s)",
		pSqlServer->InsertIgnore(), pSqlServer->GetPrefix(),
		w == Write::NORMAL ? "" : "_backup",
		pSqlServer->InsertTimestampAsUtc(), pData->m_Time,
		pData->m_aCurrentTimeCp[0], pData->m_aCurrentTimeCp[1], pData->m_aCurrentTimeCp[2],
		pData->m_aCurrentTimeCp[3], pData->m_aCurrentTimeCp[4], pData->m_aCurrentTimeCp[5],
		pData->m_aCurrentTimeCp[6], pData->m_aCurrentTimeCp[7], pData->m_aCurrentTimeCp[8],
		pData->m_aCurrentTimeCp[9], pData->m_aCurrentTimeCp[10], pData->m_aCurrentTimeCp[11],
		pData->m_aCurrentTimeCp[12], pData->m_aCurrentTimeCp[13], pData->m_aCurrentTimeCp[14],
		pData->m_aCurrentTimeCp[15], pData->m_aCurrentTimeCp[16], pData->m_aCurrentTimeCp[17],
		pData->m_aCurrentTimeCp[18], pData->m_aCurrentTimeCp[19], pData->m_aCurrentTimeCp[20],
		pData->m_aCurrentTimeCp[21], pData->m_aCurrentTimeCp[22], pData->m_aCurrentTimeCp[23],
		pData->m_aCurrentTimeCp[24], pSqlServer->False());
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aMap);
	pSqlServer->BindString(2, pData->m_aName);
	pSqlServer->BindString(3, pData->m_aTimestamp);
	pSqlServer->BindString(4, g_Config.m_SvSqlServerName);
	pSqlServer->BindString(5, pData->m_aGameUuid);
	pSqlServer->Print();
	int NumInserted;
	return pSqlServer->ExecuteUpdate(&NumInserted, pError, ErrorSize);
}

bool CScoreWorker::SaveTeamScore(IDbConnection *pSqlServer, const ISqlData *pGameData, Write w, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlTeamScoreData *>(pGameData);

	char aBuf[512];

	if(w == Write::NORMAL_SUCCEEDED)
	{
		str_format(aBuf, sizeof(aBuf),
			"DELETE FROM %s_teamrace_backup WHERE Id=?",
			pSqlServer->GetPrefix());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}

		// copy uuid, because mysql BindBlob doesn't support const buffers
		CUuid TeamrankId = pData->m_TeamrankUuid;
		pSqlServer->BindBlob(1, TeamrankId.m_aData, sizeof(TeamrankId.m_aData));
		pSqlServer->Print();
		int NumDeleted;
		if(!pSqlServer->ExecuteUpdate(&NumDeleted, pError, ErrorSize))
		{
			return false;
		}
		if(NumDeleted == 0)
		{
			log_warn("sql", "Teamrank got moved out of backup database, will show up as duplicate teamrank in MySQL");
		}
		return true;
	}
	if(w == Write::NORMAL_FAILED)
	{
		int NumInserted;
		CUuid TeamrankId = pData->m_TeamrankUuid;

		str_format(aBuf, sizeof(aBuf),
			"INSERT INTO %s_teamrace SELECT * FROM %s_teamrace_backup WHERE Id=?",
			pSqlServer->GetPrefix(), pSqlServer->GetPrefix());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindBlob(1, TeamrankId.m_aData, sizeof(TeamrankId.m_aData));
		pSqlServer->Print();
		if(!pSqlServer->ExecuteUpdate(&NumInserted, pError, ErrorSize))
		{
			return false;
		}

		str_format(aBuf, sizeof(aBuf),
			"DELETE FROM %s_teamrace_backup WHERE Id=?",
			pSqlServer->GetPrefix());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindBlob(1, TeamrankId.m_aData, sizeof(TeamrankId.m_aData));
		pSqlServer->Print();
		return pSqlServer->ExecuteUpdate(&NumInserted, pError, ErrorSize);
	}

	if(w == Write::NORMAL)
	{
		// get the names sorted in a tab separated string
		std::vector<std::string> vNames;
		vNames.reserve(pData->m_Size);
		for(unsigned int i = 0; i < pData->m_Size; i++)
			vNames.emplace_back(pData->m_aaNames[i]);

		std::sort(vNames.begin(), vNames.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 = %s"
			") as l INNER JOIN %s_teamrace AS r ON l.Id = r.Id "
			"ORDER BY l.Id, Name COLLATE %s",
			pSqlServer->GetPrefix(), pSqlServer->False(), pSqlServer->GetPrefix(), pSqlServer->BinaryCollate());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aMap);
		pSqlServer->BindString(2, pData->m_aaNames[0]);

		bool FoundTeam = false;
		float Time;
		CTeamrank Teamrank;
		bool End;
		if(!pSqlServer->Step(&End, pError, ErrorSize))
		{
			return false;
		}
		if(!End)
		{
			bool SearchTeamEnd = false;
			while(!SearchTeamEnd)
			{
				Time = pSqlServer->GetFloat(3);
				if(!Teamrank.NextSqlResult(pSqlServer, &SearchTeamEnd, pError, ErrorSize))
				{
					return false;
				}
				if(Teamrank.SamePlayers(&vNames))
				{
					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=%s, DDNet7=%s, GameId=? WHERE Id = ?",
					pSqlServer->GetPrefix(), pData->m_Time, pSqlServer->InsertTimestampAsUtc(), pSqlServer->False());
				if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
				{
					return false;
				}
				pSqlServer->BindString(1, pData->m_aTimestamp);
				pSqlServer->BindString(2, pData->m_aGameUuid);
				pSqlServer->BindBlob(3, Teamrank.m_TeamId.m_aData, sizeof(Teamrank.m_TeamId.m_aData));
				pSqlServer->Print();
				int NumUpdated;
				if(!pSqlServer->ExecuteUpdate(&NumUpdated, pError, ErrorSize))
				{
					return false;
				}
				// return error if we didn't update any rows
				return NumUpdated != 0;
			}
			return true;
		}
	}

	for(unsigned int i = 0; i < pData->m_Size; i++)
	{
		// if no entry found... create a new one
		str_format(aBuf, sizeof(aBuf),
			"%s INTO %s_teamrace%s(Map, Name, Timestamp, Time, Id, GameId, DDNet7) "
			"VALUES (?, ?, %s, %.2f, ?, ?, %s)",
			pSqlServer->InsertIgnore(), pSqlServer->GetPrefix(),
			w == Write::NORMAL ? "" : "_backup",
			pSqlServer->InsertTimestampAsUtc(), pData->m_Time, pSqlServer->False());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aMap);
		pSqlServer->BindString(2, pData->m_aaNames[i]);
		pSqlServer->BindString(3, pData->m_aTimestamp);
		// copy uuid, because mysql BindBlob doesn't support const buffers
		CUuid TeamrankId = pData->m_TeamrankUuid;
		pSqlServer->BindBlob(4, TeamrankId.m_aData, sizeof(TeamrankId.m_aData));
		pSqlServer->BindString(5, pData->m_aGameUuid);
		pSqlServer->Print();
		int NumInserted;
		if(!pSqlServer->ExecuteUpdate(&NumInserted, pError, ErrorSize))
		{
			return false;
		}
	}
	return true;
}

bool CScoreWorker::ShowRank(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());

	char aServerLike[16];
	str_format(aServerLike, sizeof(aServerLike), "%%%s%%", pData->m_aServer);

	// check sort method
	char aBuf[600];
	str_format(aBuf, sizeof(aBuf),
		"SELECT Ranking, Time, PercentRank "
		"FROM ("
		"  SELECT RANK() OVER w AS Ranking, PERCENT_RANK() OVER w as PercentRank, MIN(Time) AS Time, Name "
		"  FROM %s_race "
		"  WHERE Map = ? "
		"  AND Server LIKE ? "
		"  GROUP BY Name "
		"  WINDOW w AS (ORDER BY MIN(Time))"
		") as a "
		"WHERE Name = ?",
		pSqlServer->GetPrefix());

	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aMap);
	pSqlServer->BindString(2, aServerLike);
	pSqlServer->BindString(3, pData->m_aName);

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}

	char aRegionalRank[16];
	if(End)
	{
		str_copy(aRegionalRank, "unranked", sizeof(aRegionalRank));
	}
	else
	{
		str_format(aRegionalRank, sizeof(aRegionalRank), "rank %d", pSqlServer->GetInt(1));
	}

	const char *pAny = "%";

	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aMap);
	pSqlServer->BindString(2, pAny);
	pSqlServer->BindString(3, pData->m_aName);

	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}

	if(!End)
	{
		int Rank = pSqlServer->GetInt(1);
		float Time = pSqlServer->GetFloat(2);
		str_time_float(Time, TIME_HOURS_CENTISECS, aBuf, sizeof(aBuf));

		if(g_Config.m_SvHideScore)
		{
			str_format(pResult->m_Data.m_aaMessages[0], sizeof(pResult->m_Data.m_aaMessages[0]),
				"Your time: %s", aBuf);
		}
		else
		{
			pResult->m_MessageKind = CScorePlayerResult::ALL;
			// CEIL and FLOOR are not supported in SQLite
			int BetterThanPercent = std::floor(100.0f - 100.0f * pSqlServer->GetFloat(3));

			if(str_comp_nocase(pData->m_aRequestingPlayer, pData->m_aName) == 0)
			{
				str_format(pResult->m_Data.m_aaMessages[0], sizeof(pResult->m_Data.m_aaMessages[0]),
					"%s - %s - better than %d%%",
					pData->m_aName, aBuf, BetterThanPercent);
			}
			else
			{
				str_format(pResult->m_Data.m_aaMessages[0], sizeof(pResult->m_Data.m_aaMessages[0]),
					"%s - %s - better than %d%% - requested by %s",
					pData->m_aName, aBuf, BetterThanPercent, pData->m_aRequestingPlayer);
			}

			if(g_Config.m_SvRegionalRankings)
			{
				str_format(pResult->m_Data.m_aaMessages[1], sizeof(pResult->m_Data.m_aaMessages[1]),
					"Global rank %d - %s %s",
					Rank, pData->m_aServer, aRegionalRank);
			}
			else
			{
				str_format(pResult->m_Data.m_aaMessages[1], sizeof(pResult->m_Data.m_aaMessages[1]),
					"Global rank %d", Rank);
			}
		}
	}
	else
	{
		str_format(pResult->m_Data.m_aaMessages[0], sizeof(pResult->m_Data.m_aaMessages[0]),
			"%s is not ranked", pData->m_aName);
	}
	return true;
}

bool CScoreWorker::ShowTeamRank(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());

	// check sort method
	char aBuf[2400];

	str_format(aBuf, sizeof(aBuf),
		"SELECT l.Id, Name, Time, Ranking, PercentRank "
		"FROM (" // teamrank score board
		"  SELECT RANK() OVER w AS Ranking, PERCENT_RANK() OVER w AS PercentRank, Id "
		"  FROM %s_teamrace "
		"  WHERE Map = ? "
		"  GROUP BY ID "
		"  WINDOW w AS (ORDER BY Min(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());
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aMap);
	pSqlServer->BindString(2, pData->m_aMap);
	pSqlServer->BindString(3, pData->m_aName);

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		float Time = pSqlServer->GetFloat(3);
		str_time_float(Time, TIME_HOURS_CENTISECS, aBuf, sizeof(aBuf));
		int Rank = pSqlServer->GetInt(4);
		// CEIL and FLOOR are not supported in SQLite
		int BetterThanPercent = std::floor(100.0f - 100.0f * pSqlServer->GetFloat(5));
		CTeamrank Teamrank;
		if(!Teamrank.NextSqlResult(pSqlServer, &End, pError, ErrorSize))
		{
			return false;
		}

		char aFormattedNames[512] = "";
		for(unsigned int Name = 0; Name < Teamrank.m_NumNames; Name++)
		{
			str_append(aFormattedNames, Teamrank.m_aaNames[Name]);

			if(Name < Teamrank.m_NumNames - 2)
				str_append(aFormattedNames, ", ");
			else if(Name < Teamrank.m_NumNames - 1)
				str_append(aFormattedNames, " & ");
		}

		if(g_Config.m_SvHideScore)
		{
			str_format(pResult->m_Data.m_aaMessages[0], sizeof(pResult->m_Data.m_aaMessages[0]),
				"Your team time: %s, better than %d%%", aBuf, BetterThanPercent);
		}
		else
		{
			pResult->m_MessageKind = CScorePlayerResult::ALL;
			str_format(pResult->m_Data.m_aaMessages[0], sizeof(pResult->m_Data.m_aaMessages[0]),
				"%d. %s Team time: %s, better than %d%%, requested by %s",
				Rank, aFormattedNames, aBuf, BetterThanPercent, pData->m_aRequestingPlayer);
		}
	}
	else
	{
		str_format(pResult->m_Data.m_aaMessages[0], sizeof(pResult->m_Data.m_aaMessages[0]),
			"%s has no team ranks", pData->m_aName);
	}
	return true;
}

bool CScoreWorker::ShowTop(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());

	int LimitStart = maximum(absolute(pData->m_Offset) - 1, 0);
	const char *pOrder = pData->m_Offset >= 0 ? "ASC" : "DESC";
	const char *pAny = "%";

	// check sort method
	char aBuf[512];
	str_format(aBuf, sizeof(aBuf),
		"SELECT Name, Time, Ranking "
		"FROM ("
		"  SELECT RANK() OVER w AS Ranking, MIN(Time) AS Time, Name "
		"  FROM %s_race "
		"  WHERE Map = ? "
		"  AND Server LIKE ? "
		"  GROUP BY Name "
		"  WINDOW w AS (ORDER BY MIN(Time))"
		") as a "
		"ORDER BY Ranking %s "
		"LIMIT %d, ?",
		pSqlServer->GetPrefix(),
		pOrder,
		LimitStart);

	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aMap);
	pSqlServer->BindString(2, pAny);
	pSqlServer->BindInt(3, 5);

	// show top
	int Line = 0;
	str_copy(pResult->m_Data.m_aaMessages[Line], "------------ Global Top ------------", sizeof(pResult->m_Data.m_aaMessages[Line]));
	Line++;

	char aTime[32];
	bool End = false;

	while(pSqlServer->Step(&End, pError, ErrorSize) && !End)
	{
		char aName[MAX_NAME_LENGTH];
		pSqlServer->GetString(1, aName, sizeof(aName));
		float Time = pSqlServer->GetFloat(2);
		str_time_float(Time, TIME_HOURS_CENTISECS, aTime, sizeof(aTime));
		int Rank = pSqlServer->GetInt(3);
		str_format(pResult->m_Data.m_aaMessages[Line], sizeof(pResult->m_Data.m_aaMessages[Line]),
			"%d. %s Time: %s", Rank, aName, aTime);

		Line++;
	}

	if(!g_Config.m_SvRegionalRankings)
	{
		str_copy(pResult->m_Data.m_aaMessages[Line], "-----------------------------------------", sizeof(pResult->m_Data.m_aaMessages[Line]));
		return End;
	}

	char aServerLike[16];
	str_format(aServerLike, sizeof(aServerLike), "%%%s%%", pData->m_aServer);

	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aMap);
	pSqlServer->BindString(2, aServerLike);
	pSqlServer->BindInt(3, 3);

	str_format(pResult->m_Data.m_aaMessages[Line], sizeof(pResult->m_Data.m_aaMessages[Line]),
		"------------ %s Top ------------", pData->m_aServer);
	Line++;

	// show top
	while(pSqlServer->Step(&End, pError, ErrorSize) && !End)
	{
		char aName[MAX_NAME_LENGTH];
		pSqlServer->GetString(1, aName, sizeof(aName));
		float Time = pSqlServer->GetFloat(2);
		str_time_float(Time, TIME_HOURS_CENTISECS, aTime, sizeof(aTime));
		int Rank = pSqlServer->GetInt(3);
		str_format(pResult->m_Data.m_aaMessages[Line], sizeof(pResult->m_Data.m_aaMessages[Line]),
			"%d. %s Time: %s", Rank, aName, aTime);
		Line++;
	}

	return End;
}

bool CScoreWorker::ShowTeamTop5(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
	auto *paMessages = pResult->m_Data.m_aaMessages;

	int LimitStart = maximum(absolute(pData->m_Offset) - 1, 0);
	const char *pOrder = pData->m_Offset >= 0 ? "ASC" : "DESC";
	const char *pAny = "%";

	// check sort method
	char aBuf[1024];

	str_format(aBuf, sizeof(aBuf),
		"SELECT Name, Time, Ranking, TeamSize "
		"FROM ("
		"  SELECT TeamSize, Ranking, Id, Server "
		"  FROM (" // teamrank score board
		"    SELECT RANK() OVER w AS Ranking, COUNT(*) AS Teamsize, Id, Server "
		"    FROM ("
		"      SELECT tr.Map, tr.Time, tr.Id, ("
		"        SELECT rr.Server FROM %s_race AS rr "
		"        WHERE rr.Map = tr.Map AND rr.Name = tr.Name AND rr.Time = tr.Time "
		"        LIMIT 1"
		"      ) AS Server "
		"      FROM %s_teamrace AS tr "
		"      WHERE tr.Map = ? "
		"    ) AS ll "
		"    GROUP BY ID "
		"    WINDOW w AS (ORDER BY Min(Time))"
		"  ) as l1 "
		"  WHERE Server LIKE ? "
		"  ORDER BY Ranking %s "
		"  LIMIT %d, ?"
		") as l2 "
		"INNER JOIN %s_teamrace as r ON l2.Id = r.Id "
		"ORDER BY Ranking %s, r.Id, Name ASC",
		pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pOrder, LimitStart, pSqlServer->GetPrefix(), pOrder);
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aMap);
	pSqlServer->BindString(2, pAny);
	pSqlServer->BindInt(3, 5);

	int Line = 0;
	str_copy(paMessages[Line++], "------- Team Top 5 -------", sizeof(paMessages[Line]));

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		if(!CTeamrank::GetSqlTop5Team(pSqlServer, &End, pError, ErrorSize, paMessages, &Line, 5))
		{
			return false;
		}
	}

	if(!g_Config.m_SvRegionalRankings)
	{
		str_copy(paMessages[Line], "-------------------------------", sizeof(paMessages[Line]));
		return true;
	}

	char aServerLike[16];
	str_format(aServerLike, sizeof(aServerLike), "%%%s%%", pData->m_aServer);

	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aMap);
	pSqlServer->BindString(2, aServerLike);
	pSqlServer->BindInt(3, 3);

	str_format(pResult->m_Data.m_aaMessages[Line], sizeof(pResult->m_Data.m_aaMessages[Line]),
		"----- %s Team Top -----", pData->m_aServer);
	Line++;

	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		if(!CTeamrank::GetSqlTop5Team(pSqlServer, &End, pError, ErrorSize, paMessages, &Line, 3))
		{
			return false;
		}
	}
	return true;
}

bool CScoreWorker::ShowPlayerTeamTop5(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
	auto *paMessages = pResult->m_Data.m_aaMessages;

	int LimitStart = maximum(absolute(pData->m_Offset) - 1, 0);
	const char *pOrder = pData->m_Offset >= 0 ? "ASC" : "DESC";

	// check sort method
	char aBuf[2400];

	str_format(aBuf, sizeof(aBuf),
		"SELECT l.Id, Name, Time, Ranking "
		"FROM (" // teamrank score board
		"  SELECT RANK() OVER w AS Ranking, Id "
		"  FROM %s_teamrace "
		"  WHERE Map = ? "
		"  GROUP BY ID "
		"  WINDOW w AS (ORDER BY Min(Time))"
		") AS TeamRank INNER JOIN (" // select rank with Name in team
		"  SELECT ID "
		"  FROM %s_teamrace "
		"  WHERE Map = ? AND Name = ? "
		"  ORDER BY Time %s "
		"  LIMIT %d, 5 "
		") AS l ON TeamRank.Id = l.Id "
		"INNER JOIN %s_teamrace AS r ON l.Id = r.Id "
		"ORDER BY Time %s, l.Id, Name ASC",
		pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pOrder, LimitStart, pSqlServer->GetPrefix(), pOrder);
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aMap);
	pSqlServer->BindString(2, pData->m_aMap);
	pSqlServer->BindString(3, pData->m_aName);

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		// show teamtop5
		int Line = 0;
		str_copy(paMessages[Line++], "------- Team Top 5 -------", sizeof(paMessages[Line]));

		for(Line = 1; Line < 6; Line++) // print
		{
			float Time = pSqlServer->GetFloat(3);
			str_time_float(Time, TIME_HOURS_CENTISECS, aBuf, sizeof(aBuf));
			int Rank = pSqlServer->GetInt(4);
			CTeamrank Teamrank;
			bool Last;
			if(!Teamrank.NextSqlResult(pSqlServer, &Last, pError, ErrorSize))
			{
				return false;
			}

			char aFormattedNames[512] = "";
			for(unsigned int Name = 0; Name < Teamrank.m_NumNames; Name++)
			{
				str_append(aFormattedNames, Teamrank.m_aaNames[Name]);

				if(Name < Teamrank.m_NumNames - 2)
					str_append(aFormattedNames, ", ");
				else if(Name < Teamrank.m_NumNames - 1)
					str_append(aFormattedNames, " & ");
			}

			str_format(paMessages[Line], sizeof(paMessages[Line]), "%d. %s Team Time: %s",
				Rank, aFormattedNames, aBuf);
			if(Last)
			{
				Line++;
				break;
			}
		}
		str_copy(paMessages[Line], "---------------------------------", sizeof(paMessages[Line]));
	}
	else
	{
		if(pData->m_Offset == 0)
			str_format(paMessages[0], sizeof(paMessages[0]), "%s has no team ranks", pData->m_aName);
		else
			str_format(paMessages[0], sizeof(paMessages[0]), "%s has no team ranks in the specified range", pData->m_aName);
	}
	return true;
}

bool CScoreWorker::ShowTimes(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
	auto *paMessages = pResult->m_Data.m_aaMessages;

	int LimitStart = maximum(absolute(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_aName[0] != '\0') // last 5 times of a player
	{
		str_format(aBuf, sizeof(aBuf),
			"SELECT Time, (%s-%s) as Ago, %s as Stamp, Server "
			"FROM %s_race "
			"WHERE Map = ? AND Name = ? "
			"ORDER BY Timestamp %s "
			"LIMIT ?, 5",
			aCurrentTimestamp, aTimestamp, aTimestamp,
			pSqlServer->GetPrefix(), pOrder);
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aMap);
		pSqlServer->BindString(2, pData->m_aName);
		pSqlServer->BindInt(3, LimitStart);
	}
	else // last 5 times of server
	{
		str_format(aBuf, sizeof(aBuf),
			"SELECT Time, (%s-%s) as Ago, %s as Stamp, Server, Name "
			"FROM %s_race "
			"WHERE Map = ? "
			"ORDER BY Timestamp %s "
			"LIMIT ?, 5",
			aCurrentTimestamp, aTimestamp, aTimestamp,
			pSqlServer->GetPrefix(), pOrder);
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aMap);
		pSqlServer->BindInt(2, LimitStart);
	}

	// show top5
	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(End)
	{
		str_copy(paMessages[0], "There are no times in the specified range", sizeof(paMessages[0]));
		return true;
	}

	str_copy(paMessages[0], "------------- Last Times -------------", sizeof(paMessages[0]));
	int Line = 1;

	do
	{
		float Time = pSqlServer->GetFloat(1);
		str_time_float(Time, TIME_HOURS_CENTISECS, aBuf, sizeof(aBuf));
		int Ago = pSqlServer->GetInt(2);
		int Stamp = pSqlServer->GetInt(3);
		char aServer[5];
		pSqlServer->GetString(4, aServer, sizeof(aServer));
		char aServerFormatted[8] = "\0";
		if(str_comp(aServer, "UNK") != 0)
			str_format(aServerFormatted, sizeof(aServerFormatted), "[%s] ", aServer);

		char aAgoString[40] = "\0";
		sqlstr::AgoTimeToString(Ago, aAgoString, sizeof(aAgoString));

		if(pData->m_aName[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]),
					"%s%s, don't know how long ago", aServerFormatted, aBuf);
			else
				str_format(paMessages[Line], sizeof(paMessages[Line]),
					"%s%s ago, %s", aServerFormatted, aAgoString, aBuf);
		}
		else // last 5 times of the server
		{
			char aName[MAX_NAME_LENGTH];
			pSqlServer->GetString(5, 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%s, %s, don't know when", aServerFormatted, aName, aBuf);
			}
			else
			{
				str_format(paMessages[Line], sizeof(paMessages[Line]),
					"%s%s, %s ago, %s", aServerFormatted, aName, aAgoString, aBuf);
			}
		}
		Line++;
	} while(pSqlServer->Step(&End, pError, ErrorSize) && !End);
	if(!End)
	{
		return false;
	}
	str_copy(paMessages[Line], "-------------------------------------------", sizeof(paMessages[Line]));

	return true;
}

bool CScoreWorker::ShowPoints(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
	auto *paMessages = pResult->m_Data.m_aaMessages;

	char aBuf[512];
	str_format(aBuf, sizeof(aBuf),
		"SELECT ("
		"  SELECT COUNT(Name) + 1 FROM %s_points WHERE Points > ("
		"    SELECT Points FROM %s_points WHERE Name = ?"
		")) as Ranking, Points, Name "
		"FROM %s_points WHERE Name = ?",
		pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pSqlServer->GetPrefix());
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aName);
	pSqlServer->BindString(2, pData->m_aName);

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		int Rank = pSqlServer->GetInt(1);
		int Count = pSqlServer->GetInt(2);
		char aName[MAX_NAME_LENGTH];
		pSqlServer->GetString(3, aName, sizeof(aName));
		pResult->m_MessageKind = CScorePlayerResult::ALL;
		str_format(paMessages[0], sizeof(paMessages[0]),
			"%d. %s Points: %d, requested by %s",
			Rank, aName, Count, pData->m_aRequestingPlayer);
	}
	else
	{
		str_format(paMessages[0], sizeof(paMessages[0]),
			"%s has not collected any points so far", pData->m_aName);
	}
	return true;
}

bool CScoreWorker::ShowTopPoints(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
	auto *paMessages = pResult->m_Data.m_aaMessages;

	int LimitStart = maximum(pData->m_Offset - 1, 0);

	char aBuf[512];
	str_format(aBuf, sizeof(aBuf),
		"SELECT RANK() OVER (ORDER BY a.Points DESC) as Ranking, Points, Name "
		"FROM ("
		"  SELECT Points, Name "
		"  FROM %s_points "
		"  ORDER BY Points DESC LIMIT ?"
		") as a "
		"ORDER BY Ranking ASC, Name ASC LIMIT ?, 5",
		pSqlServer->GetPrefix());
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindInt(1, LimitStart + 5);
	pSqlServer->BindInt(2, LimitStart);

	// show top points
	str_copy(paMessages[0], "-------- Top Points --------", sizeof(paMessages[0]));

	bool End = false;
	int Line = 1;
	while(pSqlServer->Step(&End, pError, ErrorSize) && !End)
	{
		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++;
	}
	if(!End)
	{
		return false;
	}
	str_copy(paMessages[Line], "-------------------------------", sizeof(paMessages[Line]));

	return true;
}

bool CScoreWorker::RandomMap(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlRandomMapRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScoreRandomMapResult *>(pGameData->m_pResult.get());

	char aBuf[512];
	if(in_range(pData->m_MinStars, 0, 5) && in_range(pData->m_MaxStars, 0, 5))
	{
		str_format(aBuf, sizeof(aBuf),
			"SELECT Map FROM %s_maps "
			"WHERE Server = ? AND Map != ? AND Stars BETWEEN ? AND ? "
			"ORDER BY %s LIMIT 1",
			pSqlServer->GetPrefix(), pSqlServer->Random());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindInt(3, pData->m_MinStars);
		pSqlServer->BindInt(4, pData->m_MaxStars);
	}
	else
	{
		str_format(aBuf, sizeof(aBuf),
			"SELECT Map FROM %s_maps "
			"WHERE Server = ? AND Map != ? "
			"ORDER BY %s LIMIT 1",
			pSqlServer->GetPrefix(), pSqlServer->Random());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
	}
	pSqlServer->BindString(1, pData->m_aServerType);
	pSqlServer->BindString(2, pData->m_aCurrentMap);

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		pSqlServer->GetString(1, pResult->m_aMap, sizeof(pResult->m_aMap));
	}
	else
	{
		str_copy(pResult->m_aMessage, "No maps found on this server!", sizeof(pResult->m_aMessage));
	}
	return true;
}

bool CScoreWorker::RandomUnfinishedMap(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlRandomMapRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScoreRandomMapResult *>(pGameData->m_pResult.get());

	char aBuf[512];
	if(in_range(pData->m_MinStars, 0, 5) && in_range(pData->m_MaxStars, 0, 5))
	{
		str_format(aBuf, sizeof(aBuf),
			"SELECT Map "
			"FROM %s_maps "
			"WHERE Server = ? AND Map != ? AND Stars BETWEEN ? AND ? AND Map NOT IN ("
			"  SELECT Map "
			"  FROM %s_race "
			"  WHERE Name = ?"
			") ORDER BY %s "
			"LIMIT 1",
			pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pSqlServer->Random());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aServerType);
		pSqlServer->BindString(2, pData->m_aCurrentMap);
		pSqlServer->BindInt(3, pData->m_MinStars);
		pSqlServer->BindInt(4, pData->m_MaxStars);
		pSqlServer->BindString(5, pData->m_aRequestingPlayer);
	}
	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 %s "
			"LIMIT 1",
			pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pSqlServer->Random());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aServerType);
		pSqlServer->BindString(2, pData->m_aCurrentMap);
		pSqlServer->BindString(3, pData->m_aRequestingPlayer);
	}

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		pSqlServer->GetString(1, pResult->m_aMap, sizeof(pResult->m_aMap));
	}
	else
	{
		str_format(aBuf, sizeof(aBuf), "%s has no more unfinished maps on this server!", pData->m_aRequestingPlayer);
		str_copy(pResult->m_aMessage, aBuf, sizeof(pResult->m_aMessage));
	}
	return true;
}

bool CScoreWorker::SaveTeam(IDbConnection *pSqlServer, const ISqlData *pGameData, Write w, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlTeamSaveData *>(pGameData);
	auto *pResult = dynamic_cast<CScoreSaveResult *>(pGameData->m_pResult.get());

	if(w == Write::NORMAL_SUCCEEDED)
	{
		// write succeeded on mysql server. delete from sqlite again
		char aBuf[128] = {0};
		str_format(aBuf, sizeof(aBuf),
			"DELETE FROM %s_saves_backup WHERE Code = ?",
			pSqlServer->GetPrefix());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aGeneratedCode);
		bool End;
		return pSqlServer->Step(&End, pError, ErrorSize);
	}
	if(w == Write::NORMAL_FAILED)
	{
		char aBuf[256] = {0};
		bool End;
		// move to non-tmp table succeeded. delete from backup again
		str_format(aBuf, sizeof(aBuf),
			"INSERT INTO %s_saves SELECT * FROM %s_saves_backup WHERE Code = ?",
			pSqlServer->GetPrefix(), pSqlServer->GetPrefix());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aGeneratedCode);
		if(!pSqlServer->Step(&End, pError, ErrorSize))
		{
			return false;
		}

		// move to non-tmp table succeeded. delete from backup again
		str_format(aBuf, sizeof(aBuf),
			"DELETE FROM %s_saves_backup WHERE Code = ?",
			pSqlServer->GetPrefix());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pData->m_aGeneratedCode);
		return pSqlServer->Step(&End, pError, ErrorSize);
	}

	char aSaveId[UUID_MAXSTRSIZE];
	FormatUuid(pResult->m_SaveId, aSaveId, UUID_MAXSTRSIZE);

	char *pSaveState = pResult->m_SavedTeam.GetString();
	char aBuf[65536];

	dbg_msg("score/dbg", "code=%s failure=%d", pData->m_aCode, (int)w);
	bool UseGeneratedCode = pData->m_aCode[0] == '\0' || w != Write::NORMAL;

	str_copy(pResult->m_aGeneratedCode, pData->m_aGeneratedCode);
	str_copy(pResult->m_aCode, UseGeneratedCode ? "" : pData->m_aCode);

	bool Retry = false;
	// two tries, first use the user provided code, then the autogenerated
	do
	{
		Retry = false;
		char aCode[128] = {0};
		if(UseGeneratedCode)
			str_copy(aCode, pData->m_aGeneratedCode, sizeof(aCode));
		else
			str_copy(aCode, pData->m_aCode, sizeof(aCode));

		str_format(aBuf, sizeof(aBuf),
			"%s INTO %s_saves%s(Savegame, Map, Code, Timestamp, Server, SaveId, DDNet7) "
			"VALUES (?, ?, ?, CURRENT_TIMESTAMP, ?, ?, %s)",
			pSqlServer->InsertIgnore(), pSqlServer->GetPrefix(),
			w == Write::NORMAL ? "" : "_backup", pSqlServer->False());
		if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
		{
			return false;
		}
		pSqlServer->BindString(1, pSaveState);
		pSqlServer->BindString(2, pData->m_aMap);
		pSqlServer->BindString(3, aCode);
		pSqlServer->BindString(4, pData->m_aServer);
		pSqlServer->BindString(5, aSaveId);
		pSqlServer->Print();
		int NumInserted;
		if(!pSqlServer->ExecuteUpdate(&NumInserted, pError, ErrorSize))
		{
			return false;
		}
		if(NumInserted == 1)
		{
			pResult->m_Status = CScoreSaveResult::SAVE_SUCCESS;
			if(w != Write::NORMAL)
			{
				if(str_comp(pData->m_aServer, g_Config.m_SvSqlServerName) == 0)
				{
					pResult->m_aServer[0] = '\0';
				}
				pResult->m_Status = CScoreSaveResult::SAVE_FALLBACKFILE;
			}
		}
		else if(!UseGeneratedCode)
		{
			UseGeneratedCode = true;
			Retry = true;
		}
	} while(Retry);

	if(
		pResult->m_Status != CScoreSaveResult::SAVE_SUCCESS &&
		pResult->m_Status != CScoreSaveResult::SAVE_WARNING &&
		pResult->m_Status != CScoreSaveResult::SAVE_FALLBACKFILE)
	{
		dbg_msg("sql", "ERROR: This save-code already exists");
		pResult->m_Status = CScoreSaveResult::SAVE_FAILED;
		str_copy(pResult->m_aMessage, "This save-code already exists", sizeof(pResult->m_aMessage));
	}
	return true;
}

bool CScoreWorker::LoadTeam(IDbConnection *pSqlServer, const ISqlData *pGameData, Write w, char *pError, int ErrorSize)
{
	if(w == Write::NORMAL_SUCCEEDED || w == Write::BACKUP_FIRST)
		return true;
	const auto *pData = dynamic_cast<const CSqlTeamLoadRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScoreSaveResult *>(pGameData->m_pResult.get());
	pResult->m_Status = CScoreSaveResult::LOAD_FAILED;

	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, %s-%s AS Ago, SaveId "
		"FROM %s_saves "
		"where Code = ? AND Map = ? AND DDNet7 = %s",
		aCurrentTimestamp, aTimestamp,
		pSqlServer->GetPrefix(), pSqlServer->False());
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aCode);
	pSqlServer->BindString(2, pData->m_aMap);

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(End)
	{
		str_copy(pResult->m_aMessage, "No such savegame for this map", sizeof(pResult->m_aMessage));
		return true;
	}

	pResult->m_SaveId = UUID_NO_SAVE_ID;
	if(!pSqlServer->IsNull(3))
	{
		char aSaveId[UUID_MAXSTRSIZE];
		pSqlServer->GetString(3, aSaveId, sizeof(aSaveId));
		if(ParseUuid(&pResult->m_SaveId, aSaveId) || pResult->m_SaveId == UUID_NO_SAVE_ID)
		{
			str_copy(pResult->m_aMessage, "Unable to load savegame: SaveId corrupted", sizeof(pResult->m_aMessage));
			return true;
		}
	}

	char aSaveString[65536];
	pSqlServer->GetString(1, aSaveString, sizeof(aSaveString));
	int Num = pResult->m_SavedTeam.FromString(aSaveString);

	if(Num != 0)
	{
		str_copy(pResult->m_aMessage, "Unable to load savegame: data corrupted", sizeof(pResult->m_aMessage));
		return true;
	}

	bool Found = false;
	for(int i = 0; i < pResult->m_SavedTeam.GetMembersCount(); i++)
	{
		if(str_comp(pResult->m_SavedTeam.m_pSavedTees[i].GetName(), pData->m_aRequestingPlayer) == 0)
		{
			Found = true;
			break;
		}
	}
	if(!Found)
	{
		str_copy(pResult->m_aMessage, "This save exists, but you are not part of it. "
					      "Make sure you use the same name as you had when saving. "
					      "If you saved with an already used code, you get a new random save code, "
					      "check ddnet-saves.txt in config_directory.",
			sizeof(pResult->m_aMessage));
		return true;
	}

	int Since = pSqlServer->GetInt(2);
	if(Since < g_Config.m_SvSaveSwapGamesDelay)
	{
		str_format(pResult->m_aMessage, sizeof(pResult->m_aMessage),
			"You have to wait %d seconds until you can load this savegame",
			g_Config.m_SvSaveSwapGamesDelay - Since);
		return true;
	}

	bool CanLoad = pResult->m_SavedTeam.MatchPlayers(
		pData->m_aClientNames, pData->m_aClientId, pData->m_NumPlayer,
		pResult->m_aMessage, sizeof(pResult->m_aMessage));

	if(!CanLoad)
		return true;

	str_format(aBuf, sizeof(aBuf),
		"DELETE FROM %s_saves "
		"WHERE Code = ? AND Map = ? AND SaveId %s",
		pSqlServer->GetPrefix(),
		pResult->m_SaveId != UUID_NO_SAVE_ID ? "= ?" : "IS NULL");
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aCode);
	pSqlServer->BindString(2, pData->m_aMap);
	char aUuid[UUID_MAXSTRSIZE];
	if(pResult->m_SaveId != UUID_NO_SAVE_ID)
	{
		FormatUuid(pResult->m_SaveId, aUuid, sizeof(aUuid));
		pSqlServer->BindString(3, aUuid);
	}
	pSqlServer->Print();
	int NumDeleted;
	if(!pSqlServer->ExecuteUpdate(&NumDeleted, pError, ErrorSize))
	{
		return false;
	}

	if(NumDeleted != 1)
	{
		str_copy(pResult->m_aMessage, "Unable to load savegame: loaded on a different server", sizeof(pResult->m_aMessage));
		return true;
	}

	pResult->m_Status = CScoreSaveResult::LOAD_SUCCESS;
	str_copy(pResult->m_aMessage, "Loading successfully done", sizeof(pResult->m_aMessage));
	return true;
}

bool CScoreWorker::GetSaves(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
{
	const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
	auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
	auto *paMessages = pResult->m_Data.m_aaMessages;

	char aSaveLike[128] = "";
	str_append(aSaveLike, "%\n");
	sqlstr::EscapeLike(aSaveLike + str_length(aSaveLike),
		pData->m_aRequestingPlayer,
		sizeof(aSaveLike) - str_length(aSaveLike));
	str_append(aSaveLike, "\t%");

	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());
	if(!pSqlServer->PrepareStatement(aBuf, pError, ErrorSize))
	{
		return false;
	}
	pSqlServer->BindString(1, pData->m_aMap);
	pSqlServer->BindString(2, aSaveLike);

	bool End;
	if(!pSqlServer->Step(&End, pError, ErrorSize))
	{
		return false;
	}
	if(!End)
	{
		int NumSaves = pSqlServer->GetInt(1);
		char aLastSavedString[60] = "\0";
		if(!pSqlServer->IsNull(2))
		{
			int Ago = pSqlServer->GetInt(2);
			char aAgoString[40] = "\0";
			sqlstr::AgoTimeToString(Ago, aAgoString, sizeof(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_aRequestingPlayer,
			NumSaves, NumSaves == 1 ? "" : "s",
			pData->m_aMap, aLastSavedString);
	}
	return true;
}
