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