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