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 | 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 | |
107 | void 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 | |
120 | void CScore::LoadPlayerData(int ClientId, const char *pName) |
121 | { |
122 | ExecPlayerThread(pFuncPtr: CScoreWorker::LoadPlayerData, pThreadName: "load player data" , ClientId, pName, Offset: 0); |
123 | } |
124 | |
125 | void CScore::LoadPlayerTimeCp(int ClientId, const char *pName) |
126 | { |
127 | ExecPlayerThread(pFuncPtr: CScoreWorker::LoadPlayerTimeCp, pThreadName: "load player timecp" , ClientId, pName, Offset: 0); |
128 | } |
129 | |
130 | void 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 | |
137 | void 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 | |
144 | void 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 | |
169 | void 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 | |
195 | void 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 | |
202 | void 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 | |
209 | void 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 | |
216 | void 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 | |
223 | void 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 | |
230 | void 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 | |
237 | void 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 | |
244 | void 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 | |
251 | void 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 | |
258 | void 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 | |
272 | void 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 | |
286 | void 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 | |
339 | void 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 | |
392 | void 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 | |