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