1#include "score.h"
2
3#include "player.h"
4#include "save.h"
5#include "scoreworker.h"
6
7#include <base/system.h>
8
9#include <engine/server.h>
10#include <engine/server/databases/connection_pool.h>
11#include <engine/shared/config.h>
12#include <engine/shared/console.h>
13#include <engine/shared/linereader.h>
14#include <engine/storage.h>
15
16#include <generated/wordlist.h>
17
18#include <game/server/gamecontext.h>
19#include <game/server/gamemodes/ddnet.h>
20#include <game/team_state.h>
21
22#include <memory>
23
24class IDbConnection;
25
26std::shared_ptr<CScorePlayerResult> CScore::NewSqlPlayerResult(int ClientId)
27{
28 CPlayer *pCurPlayer = GameServer()->m_apPlayers[ClientId];
29 if(pCurPlayer->m_ScoreQueryResult != nullptr) // TODO: send player a message: "too many requests"
30 return nullptr;
31 pCurPlayer->m_ScoreQueryResult = std::make_shared<CScorePlayerResult>();
32 return pCurPlayer->m_ScoreQueryResult;
33}
34
35void CScore::ExecPlayerThread(
36 bool (*pFuncPtr)(IDbConnection *, const ISqlData *, char *pError, int ErrorSize),
37 const char *pThreadName,
38 int ClientId,
39 const char *pName,
40 int Offset)
41{
42 auto pResult = NewSqlPlayerResult(ClientId);
43 if(pResult == nullptr)
44 return;
45 auto Tmp = std::make_unique<CSqlPlayerRequest>(args&: pResult);
46 str_copy(dst: Tmp->m_aName, src: pName, dst_size: sizeof(Tmp->m_aName));
47 str_copy(dst: Tmp->m_aMap, src: Server()->GetMapName(), dst_size: sizeof(Tmp->m_aMap));
48 str_copy(dst: Tmp->m_aServer, src: g_Config.m_SvSqlServerName, dst_size: sizeof(Tmp->m_aServer));
49 str_copy(dst: Tmp->m_aRequestingPlayer, src: Server()->ClientName(ClientId), dst_size: sizeof(Tmp->m_aRequestingPlayer));
50 Tmp->m_Offset = Offset;
51
52 m_pPool->Execute(pFunc: pFuncPtr, pSqlRequestData: std::move(Tmp), pName: pThreadName);
53}
54
55bool CScore::RateLimitPlayer(int ClientId)
56{
57 CPlayer *pPlayer = GameServer()->m_apPlayers[ClientId];
58 if(pPlayer == nullptr)
59 return true;
60 if(pPlayer->m_LastSqlQuery + (int64_t)g_Config.m_SvSqlQueriesDelay * Server()->TickSpeed() >= Server()->Tick())
61 return true;
62 pPlayer->m_LastSqlQuery = Server()->Tick();
63 return false;
64}
65
66void CScore::GeneratePassphrase(char *pBuf, int BufSize)
67{
68 for(int i = 0; i < 3; i++)
69 {
70 if(i != 0)
71 str_append(dst: pBuf, src: " ", dst_size: BufSize);
72 // TODO: decide if the slight bias towards lower numbers is ok
73 int Rand = m_Prng.RandomBits() % m_vWordlist.size();
74 str_append(dst: pBuf, src: m_vWordlist[Rand].c_str(), dst_size: BufSize);
75 }
76}
77
78CScore::CScore(CGameContext *pGameServer, CDbConnectionPool *pPool) :
79 m_pPool(pPool),
80 m_pGameServer(pGameServer),
81 m_pServer(pGameServer->Server())
82{
83 LoadBestTime();
84
85 uint64_t aSeed[2];
86 secure_random_fill(bytes: aSeed, length: sizeof(aSeed));
87 m_Prng.Seed(aSeed);
88
89 CLineReader LineReader;
90 if(LineReader.OpenFile(File: GameServer()->Storage()->OpenFile(pFilename: "wordlist.txt", Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL)))
91 {
92 while(const char *pLine = LineReader.Get())
93 {
94 char aWord[32] = {0};
95 sscanf(s: pLine, format: "%*s %31s", aWord);
96 aWord[31] = 0;
97 m_vWordlist.emplace_back(args&: aWord);
98 }
99 }
100 else
101 {
102 dbg_msg(sys: "sql", fmt: "failed to open wordlist, using fallback");
103 m_vWordlist.assign(first: std::begin(arr: g_aFallbackWordlist), last: std::end(arr: g_aFallbackWordlist));
104 }
105
106 if(m_vWordlist.size() < 1000)
107 {
108 dbg_msg(sys: "sql", fmt: "too few words in wordlist");
109 Server()->SetErrorShutdown("sql too few words in wordlist");
110 return;
111 }
112}
113
114void CScore::LoadBestTime()
115{
116 if(m_pGameServer->m_pController->m_pLoadBestTimeResult)
117 return; // already in progress
118
119 auto LoadBestTimeResult = std::make_shared<CScoreLoadBestTimeResult>();
120 m_pGameServer->m_pController->m_pLoadBestTimeResult = LoadBestTimeResult;
121
122 auto Tmp = std::make_unique<CSqlLoadBestTimeRequest>(args&: LoadBestTimeResult);
123 str_copy(dst: Tmp->m_aMap, src: Server()->GetMapName(), dst_size: sizeof(Tmp->m_aMap));
124 m_pPool->Execute(pFunc: CScoreWorker::LoadBestTime, pSqlRequestData: std::move(Tmp), pName: "load best time");
125}
126
127void CScore::LoadMapInfo()
128{
129 if(m_pGameServer->m_pLoadMapInfoResult)
130 return; // already in progress
131
132 auto pResult = std::make_shared<CScorePlayerResult>();
133 m_pGameServer->m_pLoadMapInfoResult = pResult;
134
135 auto Tmp = std::make_unique<CSqlPlayerRequest>(args&: pResult);
136 str_copy(dst: Tmp->m_aName, src: Server()->GetMapName(), dst_size: sizeof(Tmp->m_aName));
137 Tmp->m_aRequestingPlayer[0] = '\0'; // no player, so no "your time" in result
138 m_pPool->Execute(pFunc: CScoreWorker::MapInfo, pSqlRequestData: std::move(Tmp), pName: "load map info");
139}
140
141void CScore::LoadPlayerData(int ClientId, const char *pName)
142{
143 ExecPlayerThread(pFuncPtr: CScoreWorker::LoadPlayerData, pThreadName: "load player data", ClientId, pName, Offset: 0);
144}
145
146void CScore::LoadPlayerTimeCp(int ClientId, const char *pName)
147{
148 ExecPlayerThread(pFuncPtr: CScoreWorker::LoadPlayerTimeCp, pThreadName: "load player timecp", ClientId, pName, Offset: 0);
149}
150
151void CScore::MapVote(int ClientId, const char *pMapName)
152{
153 if(RateLimitPlayer(ClientId))
154 return;
155 ExecPlayerThread(pFuncPtr: CScoreWorker::MapVote, pThreadName: "map vote", ClientId, pName: pMapName, Offset: 0);
156}
157
158void CScore::MapInfo(int ClientId, const char *pMapName)
159{
160 if(RateLimitPlayer(ClientId))
161 return;
162 ExecPlayerThread(pFuncPtr: CScoreWorker::MapInfo, pThreadName: "map info", ClientId, pName: pMapName, Offset: 0);
163}
164
165void CScore::SaveScore(int ClientId, int TimeTicks, const char *pTimestamp, const float aTimeCp[NUM_CHECKPOINTS], bool NotEligible)
166{
167 CConsole *pCon = (CConsole *)GameServer()->Console();
168 if(pCon->Cheated() || NotEligible)
169 return;
170
171 GameServer()->TeehistorianRecordPlayerFinish(ClientId, TimeTicks);
172
173 CPlayer *pCurPlayer = GameServer()->m_apPlayers[ClientId];
174 if(pCurPlayer->m_ScoreFinishResult != nullptr)
175 dbg_msg(sys: "sql", fmt: "WARNING: previous save score result didn't complete, overwriting it now");
176 pCurPlayer->m_ScoreFinishResult = std::make_shared<CScorePlayerResult>();
177 auto Tmp = std::make_unique<CSqlScoreData>(args&: pCurPlayer->m_ScoreFinishResult);
178 str_copy(dst: Tmp->m_aMap, src: Server()->GetMapName(), dst_size: sizeof(Tmp->m_aMap));
179 FormatUuid(Uuid: GameServer()->GameUuid(), pBuffer: Tmp->m_aGameUuid, BufferLength: sizeof(Tmp->m_aGameUuid));
180 Tmp->m_ClientId = ClientId;
181 str_copy(dst: Tmp->m_aName, src: Server()->ClientName(ClientId), dst_size: sizeof(Tmp->m_aName));
182 Tmp->m_Time = (float)(TimeTicks) / (float)Server()->TickSpeed();
183 str_copy(dst: Tmp->m_aTimestamp, src: pTimestamp, dst_size: sizeof(Tmp->m_aTimestamp));
184 for(int i = 0; i < NUM_CHECKPOINTS; i++)
185 Tmp->m_aCurrentTimeCp[i] = aTimeCp[i];
186
187 m_pPool->ExecuteWrite(pFunc: CScoreWorker::SaveScore, pSqlRequestData: std::move(Tmp), pName: "save score");
188}
189
190void CScore::SaveTeamScore(int Team, int *pClientIds, unsigned int Size, int TimeTicks, const char *pTimestamp)
191{
192 CConsole *pCon = (CConsole *)GameServer()->Console();
193 if(pCon->Cheated())
194 return;
195 for(unsigned int i = 0; i < Size; i++)
196 {
197 if(GameServer()->m_apPlayers[pClientIds[i]]->m_NotEligibleForFinish)
198 return;
199 }
200
201 GameServer()->TeehistorianRecordTeamFinish(TeamId: Team, TimeTicks);
202
203 auto Tmp = std::make_unique<CSqlTeamScoreData>();
204 for(unsigned int i = 0; i < Size; i++)
205 str_copy(dst: Tmp->m_aaNames[i], src: Server()->ClientName(ClientId: pClientIds[i]), dst_size: sizeof(Tmp->m_aaNames[i]));
206 Tmp->m_Size = Size;
207 Tmp->m_Time = (float)TimeTicks / (float)Server()->TickSpeed();
208 str_copy(dst: Tmp->m_aTimestamp, src: pTimestamp, dst_size: sizeof(Tmp->m_aTimestamp));
209 FormatUuid(Uuid: GameServer()->GameUuid(), pBuffer: Tmp->m_aGameUuid, BufferLength: sizeof(Tmp->m_aGameUuid));
210 str_copy(dst: Tmp->m_aMap, src: Server()->GetMapName(), dst_size: sizeof(Tmp->m_aMap));
211 Tmp->m_TeamrankUuid = RandomUuid();
212
213 m_pPool->ExecuteWrite(pFunc: CScoreWorker::SaveTeamScore, pSqlRequestData: std::move(Tmp), pName: "save team score");
214}
215
216void CScore::ShowRank(int ClientId, const char *pName)
217{
218 if(RateLimitPlayer(ClientId))
219 return;
220 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowRank, pThreadName: "show rank", ClientId, pName, Offset: 0);
221}
222
223void CScore::ShowTeamRank(int ClientId, const char *pName)
224{
225 if(RateLimitPlayer(ClientId))
226 return;
227 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowTeamRank, pThreadName: "show team rank", ClientId, pName, Offset: 0);
228}
229
230void CScore::ShowTop(int ClientId, int Offset)
231{
232 if(RateLimitPlayer(ClientId))
233 return;
234 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowTop, pThreadName: "show top5", ClientId, pName: "", Offset);
235}
236
237void CScore::ShowTeamTop5(int ClientId, int Offset)
238{
239 if(RateLimitPlayer(ClientId))
240 return;
241 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowTeamTop5, pThreadName: "show team top5", ClientId, pName: "", Offset);
242}
243
244void CScore::ShowPlayerTeamTop5(int ClientId, const char *pName, int Offset)
245{
246 if(RateLimitPlayer(ClientId))
247 return;
248 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowPlayerTeamTop5, pThreadName: "show team top5 player", ClientId, pName, Offset);
249}
250
251void CScore::ShowTimes(int ClientId, int Offset)
252{
253 if(RateLimitPlayer(ClientId))
254 return;
255 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowTimes, pThreadName: "show times", ClientId, pName: "", Offset);
256}
257
258void CScore::ShowTimes(int ClientId, const char *pName, int Offset)
259{
260 if(RateLimitPlayer(ClientId))
261 return;
262 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowTimes, pThreadName: "show times", ClientId, pName, Offset);
263}
264
265void CScore::ShowPoints(int ClientId, const char *pName)
266{
267 if(RateLimitPlayer(ClientId))
268 return;
269 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowPoints, pThreadName: "show points", ClientId, pName, Offset: 0);
270}
271
272void CScore::ShowTopPoints(int ClientId, int Offset)
273{
274 if(RateLimitPlayer(ClientId))
275 return;
276 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowTopPoints, pThreadName: "show top points", ClientId, pName: "", Offset);
277}
278
279void CScore::RandomMap(int ClientId, int MinStars, int MaxStars)
280{
281 auto pResult = std::make_shared<CScoreRandomMapResult>(args&: ClientId);
282 GameServer()->m_SqlRandomMapResult = pResult;
283
284 auto Tmp = std::make_unique<CSqlRandomMapRequest>(args&: pResult);
285 Tmp->m_MinStars = MinStars;
286 Tmp->m_MaxStars = MaxStars;
287 str_copy(dst: Tmp->m_aCurrentMap, src: Server()->GetMapName(), dst_size: sizeof(Tmp->m_aCurrentMap));
288 str_copy(dst: Tmp->m_aServerType, src: g_Config.m_SvServerType, dst_size: sizeof(Tmp->m_aServerType));
289 str_copy(dst: Tmp->m_aRequestingPlayer, src: ClientId == -1 ? "nameless tee" : GameServer()->Server()->ClientName(ClientId), dst_size: sizeof(Tmp->m_aRequestingPlayer));
290
291 m_pPool->Execute(pFunc: CScoreWorker::RandomMap, pSqlRequestData: std::move(Tmp), pName: "random map");
292}
293
294void CScore::RandomUnfinishedMap(int ClientId, int MinStars, int MaxStars)
295{
296 auto pResult = std::make_shared<CScoreRandomMapResult>(args&: ClientId);
297 GameServer()->m_SqlRandomMapResult = pResult;
298
299 auto Tmp = std::make_unique<CSqlRandomMapRequest>(args&: pResult);
300 Tmp->m_MinStars = MinStars;
301 Tmp->m_MaxStars = MaxStars;
302 str_copy(dst: Tmp->m_aCurrentMap, src: Server()->GetMapName(), dst_size: sizeof(Tmp->m_aCurrentMap));
303 str_copy(dst: Tmp->m_aServerType, src: g_Config.m_SvServerType, dst_size: sizeof(Tmp->m_aServerType));
304 str_copy(dst: Tmp->m_aRequestingPlayer, src: ClientId == -1 ? "nameless tee" : GameServer()->Server()->ClientName(ClientId), dst_size: sizeof(Tmp->m_aRequestingPlayer));
305
306 m_pPool->Execute(pFunc: CScoreWorker::RandomUnfinishedMap, pSqlRequestData: std::move(Tmp), pName: "random unfinished map");
307}
308
309void CScore::SaveTeam(int ClientId, const char *pCode, const char *pServer)
310{
311 if(RateLimitPlayer(ClientId))
312 return;
313 auto *pController = GameServer()->m_pController;
314 int Team = pController->Teams().m_Core.Team(ClientId);
315 if(pController->Teams().GetSaving(TeamId: Team))
316 {
317 GameServer()->SendChatTarget(To: ClientId, pText: "Team save already in progress");
318 return;
319 }
320 if(pController->Teams().IsPractice(Team))
321 {
322 GameServer()->SendChatTarget(To: ClientId, pText: "Team save disabled for teams in practice mode");
323 return;
324 }
325
326 auto SaveResult = std::make_shared<CScoreSaveResult>(args&: ClientId, args: Server()->ClientName(ClientId), args&: pServer);
327 SaveResult->m_SaveId = RandomUuid();
328 ESaveResult Result = SaveResult->m_SavedTeam.Save(pGameServer: GameServer(), Team);
329 if(CSaveTeam::HandleSaveError(Result, ClientId, pGameContext: GameServer()))
330 return;
331 pController->Teams().SetSaving(TeamId: Team, SaveResult);
332
333 auto Tmp = std::make_unique<CSqlTeamSaveData>(args&: SaveResult);
334 str_copy(dst: Tmp->m_aCode, src: pCode, dst_size: sizeof(Tmp->m_aCode));
335 str_copy(dst: Tmp->m_aMap, src: Server()->GetMapName(), dst_size: sizeof(Tmp->m_aMap));
336 str_copy(dst: Tmp->m_aServer, src: pServer, dst_size: sizeof(Tmp->m_aServer));
337 str_copy(dst: Tmp->m_aClientName, src: this->Server()->ClientName(ClientId), dst_size: sizeof(Tmp->m_aClientName));
338 Tmp->m_aGeneratedCode[0] = '\0';
339 GeneratePassphrase(pBuf: Tmp->m_aGeneratedCode, BufSize: sizeof(Tmp->m_aGeneratedCode));
340
341 pController->Teams().KillCharacterOrTeam(ClientId, Team);
342
343 GameServer()->SendSaveCode(
344 Team,
345 TeamSize: SaveResult->m_SavedTeam.GetMembersCount(),
346 State: SAVESTATE_PENDING,
347 pError: "",
348 pSaveRequester: SaveResult->m_aRequestingPlayer,
349 pServerName: Tmp->m_aServer,
350 pGeneratedCode: Tmp->m_aGeneratedCode,
351 pCode: Tmp->m_aCode);
352
353 m_pPool->ExecuteWrite(pFunc: CScoreWorker::SaveTeam, pSqlRequestData: std::move(Tmp), pName: "save team");
354}
355
356void CScore::LoadTeam(const char *pCode, int ClientId)
357{
358 if(RateLimitPlayer(ClientId))
359 return;
360 auto *pController = GameServer()->m_pController;
361 int Team = pController->Teams().m_Core.Team(ClientId);
362 if(pController->Teams().GetSaving(TeamId: Team))
363 {
364 GameServer()->SendChatTarget(To: ClientId, pText: "Team load already in progress");
365 return;
366 }
367 if(!pController->Teams().IsValidTeamNumber(Team) || (g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && Team == TEAM_FLOCK))
368 {
369 GameServer()->SendChatTarget(To: ClientId, pText: "You have to be in a team (from 1-63)");
370 return;
371 }
372 if(pController->Teams().GetTeamState(Team) != ETeamState::OPEN)
373 {
374 GameServer()->SendChatTarget(To: ClientId, pText: "Team can't be loaded while racing");
375 return;
376 }
377 if(pController->Teams().TeamFlock(Team))
378 {
379 GameServer()->SendChatTarget(To: ClientId, pText: "Team can't be loaded while in team 0 mode");
380 return;
381 }
382 if(pController->Teams().IsPractice(Team))
383 {
384 GameServer()->SendChatTarget(To: ClientId, pText: "Team can't be loaded while practice is enabled");
385 return;
386 }
387 auto SaveResult = std::make_shared<CScoreSaveResult>(args&: ClientId, args: Server()->ClientName(ClientId), args&: g_Config.m_SvSqlServerName);
388 SaveResult->m_Status = CScoreSaveResult::LOAD_FAILED;
389 pController->Teams().SetSaving(TeamId: Team, SaveResult);
390 auto Tmp = std::make_unique<CSqlTeamLoadRequest>(args&: SaveResult);
391 str_copy(dst: Tmp->m_aCode, src: pCode, dst_size: sizeof(Tmp->m_aCode));
392 str_copy(dst: Tmp->m_aMap, src: Server()->GetMapName(), dst_size: sizeof(Tmp->m_aMap));
393 str_copy(dst: Tmp->m_aRequestingPlayer, src: Server()->ClientName(ClientId), dst_size: sizeof(Tmp->m_aRequestingPlayer));
394 Tmp->m_NumPlayer = 0;
395 for(int i = 0; i < MAX_CLIENTS; i++)
396 {
397 if(pController->Teams().m_Core.Team(ClientId: i) == Team)
398 {
399 // put all names at the beginning of the array
400 str_copy(dst: Tmp->m_aClientNames[Tmp->m_NumPlayer], src: Server()->ClientName(ClientId: i), dst_size: sizeof(Tmp->m_aClientNames[Tmp->m_NumPlayer]));
401 Tmp->m_aClientId[Tmp->m_NumPlayer] = i;
402 Tmp->m_NumPlayer++;
403 }
404 }
405 m_pPool->ExecuteWrite(pFunc: CScoreWorker::LoadTeam, pSqlRequestData: std::move(Tmp), pName: "load team");
406}
407
408void CScore::GetSaves(int ClientId)
409{
410 if(RateLimitPlayer(ClientId))
411 return;
412 ExecPlayerThread(pFuncPtr: CScoreWorker::GetSaves, pThreadName: "get saves", ClientId, pName: "", Offset: 0);
413}
414