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 IOHANDLE File = GameServer()->Storage()->OpenFile(pFilename: "wordlist.txt", Flags: IOFLAG_READ | IOFLAG_SKIP_BOM, Type: IStorage::TYPE_ALL);
83 if(File)
84 {
85 CLineReader LineReader;
86 LineReader.Init(File);
87 char *pLine;
88 while((pLine = LineReader.Get()))
89 {
90 char aWord[32] = {0};
91 sscanf(s: pLine, format: "%*s %31s", aWord);
92 aWord[31] = 0;
93 m_vWordlist.emplace_back(args&: aWord);
94 }
95 io_close(io: File);
96 }
97 else
98 {
99 dbg_msg(sys: "sql", fmt: "failed to open wordlist, using fallback");
100 m_vWordlist.assign(first: std::begin(arr: g_aFallbackWordlist), last: std::end(arr: g_aFallbackWordlist));
101 }
102
103 if(m_vWordlist.size() < 1000)
104 {
105 dbg_msg(sys: "sql", fmt: "too few words in wordlist");
106 Server()->SetErrorShutdown("sql too few words in wordlist");
107 return;
108 }
109}
110
111void CScore::LoadBestTime()
112{
113 if(m_pGameServer->m_pController->m_pLoadBestTimeResult)
114 return; // already in progress
115
116 auto LoadBestTimeResult = std::make_shared<CScoreLoadBestTimeResult>();
117 m_pGameServer->m_pController->m_pLoadBestTimeResult = LoadBestTimeResult;
118
119 auto Tmp = std::make_unique<CSqlLoadBestTimeData>(args&: LoadBestTimeResult);
120 str_copy(dst: Tmp->m_aMap, src: g_Config.m_SvMap, dst_size: sizeof(Tmp->m_aMap));
121 m_pPool->Execute(pFunc: CScoreWorker::LoadBestTime, pSqlRequestData: std::move(Tmp), pName: "load best time");
122}
123
124void CScore::LoadPlayerData(int ClientId, const char *pName)
125{
126 ExecPlayerThread(pFuncPtr: CScoreWorker::LoadPlayerData, pThreadName: "load player data", ClientId, pName, Offset: 0);
127}
128
129void CScore::LoadPlayerTimeCp(int ClientId, const char *pName)
130{
131 ExecPlayerThread(pFuncPtr: CScoreWorker::LoadPlayerTimeCp, pThreadName: "load player timecp", ClientId, pName, Offset: 0);
132}
133
134void CScore::MapVote(int ClientId, const char *pMapName)
135{
136 if(RateLimitPlayer(ClientId))
137 return;
138 ExecPlayerThread(pFuncPtr: CScoreWorker::MapVote, pThreadName: "map vote", ClientId, pName: pMapName, Offset: 0);
139}
140
141void CScore::MapInfo(int ClientId, const char *pMapName)
142{
143 if(RateLimitPlayer(ClientId))
144 return;
145 ExecPlayerThread(pFuncPtr: CScoreWorker::MapInfo, pThreadName: "map info", ClientId, pName: pMapName, Offset: 0);
146}
147
148void CScore::SaveScore(int ClientId, float Time, const char *pTimestamp, const float aTimeCp[NUM_CHECKPOINTS], bool NotEligible)
149{
150 CConsole *pCon = (CConsole *)GameServer()->Console();
151 if(pCon->Cheated() || NotEligible)
152 return;
153
154 CPlayer *pCurPlayer = GameServer()->m_apPlayers[ClientId];
155 if(pCurPlayer->m_ScoreFinishResult != nullptr)
156 dbg_msg(sys: "sql", fmt: "WARNING: previous save score result didn't complete, overwriting it now");
157 pCurPlayer->m_ScoreFinishResult = std::make_shared<CScorePlayerResult>();
158 auto Tmp = std::make_unique<CSqlScoreData>(args&: pCurPlayer->m_ScoreFinishResult);
159 str_copy(dst: Tmp->m_aMap, src: g_Config.m_SvMap, dst_size: sizeof(Tmp->m_aMap));
160 FormatUuid(Uuid: GameServer()->GameUuid(), pBuffer: Tmp->m_aGameUuid, BufferLength: sizeof(Tmp->m_aGameUuid));
161 Tmp->m_ClientId = ClientId;
162 str_copy(dst: Tmp->m_aName, src: Server()->ClientName(ClientId), dst_size: sizeof(Tmp->m_aName));
163 Tmp->m_Time = Time;
164 str_copy(dst: Tmp->m_aTimestamp, src: pTimestamp, dst_size: sizeof(Tmp->m_aTimestamp));
165 for(int i = 0; i < NUM_CHECKPOINTS; i++)
166 Tmp->m_aCurrentTimeCp[i] = aTimeCp[i];
167
168 m_pPool->ExecuteWrite(pFunc: CScoreWorker::SaveScore, pSqlRequestData: std::move(Tmp), pName: "save score");
169}
170
171void CScore::SaveTeamScore(int *pClientIds, unsigned int Size, float Time, const char *pTimestamp)
172{
173 CConsole *pCon = (CConsole *)GameServer()->Console();
174 if(pCon->Cheated())
175 return;
176 for(unsigned int i = 0; i < Size; i++)
177 {
178 if(GameServer()->m_apPlayers[pClientIds[i]]->m_NotEligibleForFinish)
179 return;
180 }
181 auto Tmp = std::make_unique<CSqlTeamScoreData>();
182 for(unsigned int i = 0; i < Size; i++)
183 str_copy(dst: Tmp->m_aaNames[i], src: Server()->ClientName(ClientId: pClientIds[i]), dst_size: sizeof(Tmp->m_aaNames[i]));
184 Tmp->m_Size = Size;
185 Tmp->m_Time = Time;
186 str_copy(dst: Tmp->m_aTimestamp, src: pTimestamp, dst_size: sizeof(Tmp->m_aTimestamp));
187 FormatUuid(Uuid: GameServer()->GameUuid(), pBuffer: Tmp->m_aGameUuid, BufferLength: sizeof(Tmp->m_aGameUuid));
188 str_copy(dst: Tmp->m_aMap, src: g_Config.m_SvMap, dst_size: sizeof(Tmp->m_aMap));
189 Tmp->m_TeamrankUuid = RandomUuid();
190
191 m_pPool->ExecuteWrite(pFunc: CScoreWorker::SaveTeamScore, pSqlRequestData: std::move(Tmp), pName: "save team score");
192}
193
194void CScore::ShowRank(int ClientId, const char *pName)
195{
196 if(RateLimitPlayer(ClientId))
197 return;
198 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowRank, pThreadName: "show rank", ClientId, pName, Offset: 0);
199}
200
201void CScore::ShowTeamRank(int ClientId, const char *pName)
202{
203 if(RateLimitPlayer(ClientId))
204 return;
205 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowTeamRank, pThreadName: "show team rank", ClientId, pName, Offset: 0);
206}
207
208void CScore::ShowTop(int ClientId, int Offset)
209{
210 if(RateLimitPlayer(ClientId))
211 return;
212 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowTop, pThreadName: "show top5", ClientId, pName: "", Offset);
213}
214
215void CScore::ShowTeamTop5(int ClientId, int Offset)
216{
217 if(RateLimitPlayer(ClientId))
218 return;
219 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowTeamTop5, pThreadName: "show team top5", ClientId, pName: "", Offset);
220}
221
222void CScore::ShowPlayerTeamTop5(int ClientId, const char *pName, int Offset)
223{
224 if(RateLimitPlayer(ClientId))
225 return;
226 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowPlayerTeamTop5, pThreadName: "show team top5 player", ClientId, pName, Offset);
227}
228
229void CScore::ShowTimes(int ClientId, int Offset)
230{
231 if(RateLimitPlayer(ClientId))
232 return;
233 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowTimes, pThreadName: "show times", ClientId, pName: "", Offset);
234}
235
236void CScore::ShowTimes(int ClientId, const char *pName, int Offset)
237{
238 if(RateLimitPlayer(ClientId))
239 return;
240 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowTimes, pThreadName: "show times", ClientId, pName, Offset);
241}
242
243void CScore::ShowPoints(int ClientId, const char *pName)
244{
245 if(RateLimitPlayer(ClientId))
246 return;
247 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowPoints, pThreadName: "show points", ClientId, pName, Offset: 0);
248}
249
250void CScore::ShowTopPoints(int ClientId, int Offset)
251{
252 if(RateLimitPlayer(ClientId))
253 return;
254 ExecPlayerThread(pFuncPtr: CScoreWorker::ShowTopPoints, pThreadName: "show top points", ClientId, pName: "", Offset);
255}
256
257void CScore::RandomMap(int ClientId, int Stars)
258{
259 auto pResult = std::make_shared<CScoreRandomMapResult>(args&: ClientId);
260 GameServer()->m_SqlRandomMapResult = pResult;
261
262 auto Tmp = std::make_unique<CSqlRandomMapRequest>(args&: pResult);
263 Tmp->m_Stars = Stars;
264 str_copy(dst: Tmp->m_aCurrentMap, src: g_Config.m_SvMap, dst_size: sizeof(Tmp->m_aCurrentMap));
265 str_copy(dst: Tmp->m_aServerType, src: g_Config.m_SvServerType, dst_size: sizeof(Tmp->m_aServerType));
266 str_copy(dst: Tmp->m_aRequestingPlayer, src: GameServer()->Server()->ClientName(ClientId), dst_size: sizeof(Tmp->m_aRequestingPlayer));
267
268 m_pPool->Execute(pFunc: CScoreWorker::RandomMap, pSqlRequestData: std::move(Tmp), pName: "random map");
269}
270
271void CScore::RandomUnfinishedMap(int ClientId, int Stars)
272{
273 auto pResult = std::make_shared<CScoreRandomMapResult>(args&: ClientId);
274 GameServer()->m_SqlRandomMapResult = pResult;
275
276 auto Tmp = std::make_unique<CSqlRandomMapRequest>(args&: pResult);
277 Tmp->m_Stars = Stars;
278 str_copy(dst: Tmp->m_aCurrentMap, src: g_Config.m_SvMap, dst_size: sizeof(Tmp->m_aCurrentMap));
279 str_copy(dst: Tmp->m_aServerType, src: g_Config.m_SvServerType, dst_size: sizeof(Tmp->m_aServerType));
280 str_copy(dst: Tmp->m_aRequestingPlayer, src: GameServer()->Server()->ClientName(ClientId), dst_size: sizeof(Tmp->m_aRequestingPlayer));
281
282 m_pPool->Execute(pFunc: CScoreWorker::RandomUnfinishedMap, pSqlRequestData: std::move(Tmp), pName: "random unfinished map");
283}
284
285void CScore::SaveTeam(int ClientId, const char *pCode, const char *pServer)
286{
287 if(RateLimitPlayer(ClientId))
288 return;
289 auto *pController = GameServer()->m_pController;
290 int Team = pController->Teams().m_Core.Team(ClientId);
291 char aBuf[512];
292 if(pController->Teams().GetSaving(TeamId: Team))
293 {
294 GameServer()->SendChatTarget(To: ClientId, pText: "Team save already in progress");
295 return;
296 }
297 if(pController->Teams().IsPractice(Team))
298 {
299 GameServer()->SendChatTarget(To: ClientId, pText: "Team save disabled for teams in practice mode");
300 return;
301 }
302
303 auto SaveResult = std::make_shared<CScoreSaveResult>(args&: ClientId);
304 SaveResult->m_SaveId = RandomUuid();
305 int Result = SaveResult->m_SavedTeam.Save(pGameServer: GameServer(), Team);
306 if(CSaveTeam::HandleSaveError(Result, ClientId, pGameContext: GameServer()))
307 return;
308 pController->Teams().SetSaving(TeamId: Team, SaveResult);
309
310 auto Tmp = std::make_unique<CSqlTeamSave>(args&: SaveResult);
311 str_copy(dst: Tmp->m_aCode, src: pCode, dst_size: sizeof(Tmp->m_aCode));
312 str_copy(dst: Tmp->m_aMap, src: g_Config.m_SvMap, dst_size: sizeof(Tmp->m_aMap));
313 str_copy(dst: Tmp->m_aServer, src: pServer, dst_size: sizeof(Tmp->m_aServer));
314 str_copy(dst: Tmp->m_aClientName, src: this->Server()->ClientName(ClientId), dst_size: sizeof(Tmp->m_aClientName));
315 Tmp->m_aGeneratedCode[0] = '\0';
316 GeneratePassphrase(pBuf: Tmp->m_aGeneratedCode, BufSize: sizeof(Tmp->m_aGeneratedCode));
317
318 if(Tmp->m_aCode[0] == '\0')
319 {
320 str_format(buffer: aBuf,
321 buffer_size: sizeof(aBuf),
322 format: "Team save in progress. You'll be able to load with '/load %s'",
323 Tmp->m_aGeneratedCode);
324 }
325 else
326 {
327 str_format(buffer: aBuf,
328 buffer_size: sizeof(aBuf),
329 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",
330 Tmp->m_aCode,
331 Tmp->m_aGeneratedCode);
332 }
333 pController->Teams().KillSavedTeam(ClientId, Team);
334 GameServer()->SendChatTeam(Team, pText: aBuf);
335 m_pPool->ExecuteWrite(pFunc: CScoreWorker::SaveTeam, pSqlRequestData: std::move(Tmp), pName: "save team");
336}
337
338void CScore::LoadTeam(const char *pCode, int ClientId)
339{
340 if(RateLimitPlayer(ClientId))
341 return;
342 auto *pController = GameServer()->m_pController;
343 int Team = pController->Teams().m_Core.Team(ClientId);
344 if(pController->Teams().GetSaving(TeamId: Team))
345 {
346 GameServer()->SendChatTarget(To: ClientId, pText: "Team load already in progress");
347 return;
348 }
349 if(Team < TEAM_FLOCK || Team >= MAX_CLIENTS || (g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && Team == TEAM_FLOCK))
350 {
351 GameServer()->SendChatTarget(To: ClientId, pText: "You have to be in a team (from 1-63)");
352 return;
353 }
354 if(pController->Teams().GetTeamState(Team) != CGameTeams::TEAMSTATE_OPEN)
355 {
356 GameServer()->SendChatTarget(To: ClientId, pText: "Team can't be loaded while racing");
357 return;
358 }
359 if(pController->Teams().TeamFlock(Team))
360 {
361 GameServer()->SendChatTarget(To: ClientId, pText: "Team can't be loaded while in team 0 mode");
362 return;
363 }
364 if(pController->Teams().IsPractice(Team))
365 {
366 GameServer()->SendChatTarget(To: ClientId, pText: "Team can't be loaded while practice is enabled");
367 return;
368 }
369 auto SaveResult = std::make_shared<CScoreSaveResult>(args&: ClientId);
370 SaveResult->m_Status = CScoreSaveResult::LOAD_FAILED;
371 pController->Teams().SetSaving(TeamId: Team, SaveResult);
372 auto Tmp = std::make_unique<CSqlTeamLoad>(args&: SaveResult);
373 str_copy(dst: Tmp->m_aCode, src: pCode, dst_size: sizeof(Tmp->m_aCode));
374 str_copy(dst: Tmp->m_aMap, src: g_Config.m_SvMap, dst_size: sizeof(Tmp->m_aMap));
375 Tmp->m_ClientId = ClientId;
376 str_copy(dst: Tmp->m_aRequestingPlayer, src: Server()->ClientName(ClientId), dst_size: sizeof(Tmp->m_aRequestingPlayer));
377 Tmp->m_NumPlayer = 0;
378 for(int i = 0; i < MAX_CLIENTS; i++)
379 {
380 if(pController->Teams().m_Core.Team(ClientId: i) == Team)
381 {
382 // put all names at the beginning of the array
383 str_copy(dst: Tmp->m_aClientNames[Tmp->m_NumPlayer], src: Server()->ClientName(ClientId: i), dst_size: sizeof(Tmp->m_aClientNames[Tmp->m_NumPlayer]));
384 Tmp->m_aClientId[Tmp->m_NumPlayer] = i;
385 Tmp->m_NumPlayer++;
386 }
387 }
388 m_pPool->ExecuteWrite(pFunc: CScoreWorker::LoadTeam, pSqlRequestData: std::move(Tmp), pName: "load team");
389}
390
391void CScore::GetSaves(int ClientId)
392{
393 if(RateLimitPlayer(ClientId))
394 return;
395 ExecPlayerThread(pFuncPtr: CScoreWorker::GetSaves, pThreadName: "get saves", ClientId, pName: "", Offset: 0);
396}
397