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 | |
17 | class IDbConnection; |
18 | |
19 | std::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 | |
28 | void 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 | |
48 | bool 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 | |
59 | void 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 | |
71 | CScore::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 | |
111 | void 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 | |
124 | void CScore::LoadPlayerData(int ClientId, const char *pName) |
125 | { |
126 | ExecPlayerThread(pFuncPtr: CScoreWorker::LoadPlayerData, pThreadName: "load player data" , ClientId, pName, Offset: 0); |
127 | } |
128 | |
129 | void CScore::LoadPlayerTimeCp(int ClientId, const char *pName) |
130 | { |
131 | ExecPlayerThread(pFuncPtr: CScoreWorker::LoadPlayerTimeCp, pThreadName: "load player timecp" , ClientId, pName, Offset: 0); |
132 | } |
133 | |
134 | void 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 | |
141 | void 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 | |
148 | void 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 | |
171 | void 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 | |
194 | void 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 | |
201 | void 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 | |
208 | void 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 | |
215 | void 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 | |
222 | void 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 | |
229 | void 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 | |
236 | void 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 | |
243 | void 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 | |
250 | void 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 | |
257 | void 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 | |
271 | void 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 | |
285 | void 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 | |
338 | void 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 | |
391 | void 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 | |