1#include "scoreworker.h"
2
3#include <base/log.h>
4#include <base/system.h>
5
6#include <engine/server/databases/connection.h>
7#include <engine/server/databases/connection_pool.h>
8#include <engine/server/sql_string_helpers.h>
9#include <engine/shared/config.h>
10
11#include <cmath>
12
13// "6b407e81-8b77-3e04-a207-8da17f37d000"
14// "save-no-save-id@ddnet.tw"
15static const CUuid UUID_NO_SAVE_ID =
16 {.m_aData: {0x6b, 0x40, 0x7e, 0x81, 0x8b, 0x77, 0x3e, 0x04,
17 0xa2, 0x07, 0x8d, 0xa1, 0x7f, 0x37, 0xd0, 0x00}};
18
19CScorePlayerResult::CScorePlayerResult()
20{
21 SetVariant(Variant::DIRECT);
22}
23
24void CScorePlayerResult::SetVariant(Variant v)
25{
26 m_MessageKind = v;
27 switch(v)
28 {
29 case DIRECT:
30 case ALL:
31 for(auto &aMessage : m_Data.m_aaMessages)
32 aMessage[0] = 0;
33 break;
34 case BROADCAST:
35 m_Data.m_aBroadcast[0] = 0;
36 break;
37 case MAP_VOTE:
38 m_Data.m_MapVote.m_aMap[0] = '\0';
39 m_Data.m_MapVote.m_aReason[0] = '\0';
40 m_Data.m_MapVote.m_aServer[0] = '\0';
41 break;
42 case PLAYER_INFO:
43 m_Data.m_Info.m_Birthday = 0;
44 m_Data.m_Info.m_Time.reset();
45 for(float &TimeCp : m_Data.m_Info.m_aTimeCp)
46 TimeCp = 0;
47 break;
48 case PLAYER_TIMECP:
49 m_Data.m_Info.m_aRequestedPlayer[0] = '\0';
50 m_Data.m_Info.m_Time.reset();
51 for(float &TimeCp : m_Data.m_Info.m_aTimeCp)
52 TimeCp = 0;
53 break;
54 }
55}
56
57CTeamrank::CTeamrank() :
58 m_NumNames(0)
59{
60 for(auto &aName : m_aaNames)
61 aName[0] = '\0';
62 mem_zero(block: &m_TeamId.m_aData, size: sizeof(m_TeamId));
63}
64
65bool CTeamrank::NextSqlResult(IDbConnection *pSqlServer, bool *pEnd, char *pError, int ErrorSize)
66{
67 pSqlServer->GetBlob(Col: 1, pBuffer: m_TeamId.m_aData, BufferSize: sizeof(m_TeamId.m_aData));
68 pSqlServer->GetString(Col: 2, pBuffer: m_aaNames[0], BufferSize: sizeof(m_aaNames[0]));
69 m_NumNames = 1;
70 bool End = false;
71 while(pSqlServer->Step(pEnd: &End, pError, ErrorSize) && !End)
72 {
73 CUuid TeamId;
74 pSqlServer->GetBlob(Col: 1, pBuffer: TeamId.m_aData, BufferSize: sizeof(TeamId.m_aData));
75 if(m_TeamId != TeamId)
76 {
77 *pEnd = false;
78 return true;
79 }
80 pSqlServer->GetString(Col: 2, pBuffer: m_aaNames[m_NumNames], BufferSize: sizeof(m_aaNames[m_NumNames]));
81 m_NumNames++;
82 }
83 if(!End)
84 {
85 return false;
86 }
87 *pEnd = true;
88 return true;
89}
90
91bool CTeamrank::SamePlayers(const std::vector<std::string> *pvSortedNames)
92{
93 if(pvSortedNames->size() != m_NumNames)
94 return false;
95 for(unsigned int i = 0; i < m_NumNames; i++)
96 {
97 if(str_comp(a: pvSortedNames->at(n: i).c_str(), b: m_aaNames[i]) != 0)
98 return false;
99 }
100 return true;
101}
102
103bool CTeamrank::GetSqlTop5Team(IDbConnection *pSqlServer, bool *pEnd, char *pError, int ErrorSize, char (*paMessages)[512], int *Line, int Count)
104{
105 char aTime[32];
106 int StartLine = *Line;
107 for(*Line = StartLine; *Line < StartLine + Count; (*Line)++)
108 {
109 bool Last = false;
110 float Time = pSqlServer->GetFloat(Col: 2);
111 str_time_float(secs: Time, format: TIME_HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
112 int Rank = pSqlServer->GetInt(Col: 3);
113 int TeamSize = pSqlServer->GetInt(Col: 4);
114
115 char aNames[2300] = {0};
116 for(int i = 0; i < TeamSize; i++)
117 {
118 char aName[MAX_NAME_LENGTH];
119 pSqlServer->GetString(Col: 1, pBuffer: aName, BufferSize: sizeof(aName));
120 str_append(dst&: aNames, src: aName);
121 if(i < TeamSize - 2)
122 str_append(dst&: aNames, src: ", ");
123 else if(i == TeamSize - 2)
124 str_append(dst&: aNames, src: " & ");
125 if(!pSqlServer->Step(pEnd: &Last, pError, ErrorSize))
126 {
127 return false;
128 }
129 if(Last)
130 {
131 break;
132 }
133 }
134 str_format(buffer: paMessages[*Line], buffer_size: sizeof(paMessages[*Line]), format: "%d. %s Team Time: %s",
135 Rank, aNames, aTime);
136 if(Last)
137 {
138 (*Line)++;
139 break;
140 }
141 }
142 return true;
143}
144
145bool CScoreWorker::LoadBestTime(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
146{
147 const auto *pData = dynamic_cast<const CSqlLoadBestTimeRequest *>(pGameData);
148 auto *pResult = dynamic_cast<CScoreLoadBestTimeResult *>(pGameData->m_pResult.get());
149
150 char aBuf[512];
151 // get the best time
152 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
153 format: "SELECT Time FROM %s_race WHERE Map=? ORDER BY `Time` ASC LIMIT 1",
154 pSqlServer->GetPrefix());
155 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
156 {
157 return false;
158 }
159 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
160
161 bool End;
162 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
163 {
164 return false;
165 }
166 if(!End)
167 {
168 pResult->m_CurrentRecord = pSqlServer->GetFloat(Col: 1);
169 }
170
171 return true;
172}
173
174// update stuff
175bool CScoreWorker::LoadPlayerData(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
176{
177 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
178 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
179 pResult->SetVariant(CScorePlayerResult::PLAYER_INFO);
180
181 char aBuf[1024];
182 // get best race time
183 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
184 format: "SELECT"
185 " (SELECT Time FROM %s_race WHERE Map = ? AND Name = ? ORDER BY Time ASC LIMIT 1) AS minTime, "
186 " cp1, cp2, cp3, cp4, cp5, cp6, cp7, cp8, cp9, cp10, cp11, cp12, cp13, cp14, "
187 " cp15, cp16, cp17, cp18, cp19, cp20, cp21, cp22, cp23, cp24, cp25, "
188 " (cp1 + cp2 + cp3 + cp4 + cp5 + cp6 + cp7 + cp8 + cp9 + cp10 + cp11 + cp12 + cp13 + cp14 + "
189 " cp15 + cp16 + cp17 + cp18 + cp19 + cp20 + cp21 + cp22 + cp23 + cp24 + cp25 > 0) AS hasCP, Time "
190 "FROM %s_race "
191 "WHERE Map = ? AND Name = ? "
192 "ORDER BY hasCP DESC, Time ASC "
193 "LIMIT 1",
194 pSqlServer->GetPrefix(), pSqlServer->GetPrefix());
195 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
196 {
197 return false;
198 }
199
200 const char *pPlayer = pData->m_aName[0] != '\0' ? pData->m_aName : pData->m_aRequestingPlayer;
201 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
202 pSqlServer->BindString(Idx: 2, pString: pData->m_aRequestingPlayer);
203 pSqlServer->BindString(Idx: 3, pString: pData->m_aMap);
204 pSqlServer->BindString(Idx: 4, pString: pPlayer);
205
206 bool End;
207 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
208 {
209 return false;
210 }
211 if(!End)
212 {
213 if(!pSqlServer->IsNull(Col: 1))
214 {
215 // get the best time
216 float Time = pSqlServer->GetFloat(Col: 1);
217 pResult->m_Data.m_Info.m_Time = Time;
218 }
219
220 for(int i = 0; i < NUM_CHECKPOINTS; i++)
221 {
222 pResult->m_Data.m_Info.m_aTimeCp[i] = pSqlServer->GetFloat(Col: i + 2);
223 }
224 }
225
226 // birthday check
227 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
228 format: "SELECT CURRENT_TIMESTAMP AS Current, MIN(Timestamp) AS Stamp "
229 "FROM %s_race "
230 "WHERE Name = ?",
231 pSqlServer->GetPrefix());
232 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
233 {
234 return false;
235 }
236 pSqlServer->BindString(Idx: 1, pString: pData->m_aRequestingPlayer);
237
238 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
239 {
240 return false;
241 }
242 if(!End && !pSqlServer->IsNull(Col: 2))
243 {
244 char aCurrent[TIMESTAMP_STR_LENGTH];
245 pSqlServer->GetString(Col: 1, pBuffer: aCurrent, BufferSize: sizeof(aCurrent));
246 char aStamp[TIMESTAMP_STR_LENGTH];
247 pSqlServer->GetString(Col: 2, pBuffer: aStamp, BufferSize: sizeof(aStamp));
248 int CurrentYear, CurrentMonth, CurrentDay;
249 int StampYear, StampMonth, StampDay;
250 if(sscanf(s: aCurrent, format: "%d-%d-%d", &CurrentYear, &CurrentMonth, &CurrentDay) == 3 && sscanf(s: aStamp, format: "%d-%d-%d", &StampYear, &StampMonth, &StampDay) == 3 && CurrentMonth == StampMonth && CurrentDay == StampDay)
251 pResult->m_Data.m_Info.m_Birthday = CurrentYear - StampYear;
252 }
253 return true;
254}
255
256bool CScoreWorker::LoadPlayerTimeCp(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
257{
258 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
259 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
260 auto *paMessages = pResult->m_Data.m_aaMessages;
261
262 char aBuf[1024];
263 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
264 format: "SELECT"
265 " Time, cp1, cp2, cp3, cp4, cp5, cp6, cp7, cp8, cp9, cp10, cp11, cp12, cp13, "
266 " cp14, cp15, cp16, cp17, cp18, cp19, cp20, cp21, cp22, cp23, cp24, cp25 "
267 "FROM %s_race "
268 "WHERE Map = ? AND Name = ? AND "
269 " (cp1 + cp2 + cp3 + cp4 + cp5 + cp6 + cp7 + cp8 + cp9 + cp10 + cp11 + cp12 + cp13 + cp14 + "
270 " cp15 + cp16 + cp17 + cp18 + cp19 + cp20 + cp21 + cp22 + cp23 + cp24 + cp25) > 0 "
271 "ORDER BY Time ASC "
272 "LIMIT 1",
273 pSqlServer->GetPrefix());
274 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
275 {
276 return false;
277 }
278
279 const char *pPlayer = pData->m_aName[0] != '\0' ? pData->m_aName : pData->m_aRequestingPlayer;
280 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
281 pSqlServer->BindString(Idx: 2, pString: pPlayer);
282
283 bool End;
284 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
285 {
286 return false;
287 }
288 if(!End)
289 {
290 pResult->SetVariant(CScorePlayerResult::PLAYER_TIMECP);
291 pResult->m_Data.m_Info.m_Time = pSqlServer->GetFloat(Col: 1);
292 for(int i = 0; i < NUM_CHECKPOINTS; i++)
293 {
294 pResult->m_Data.m_Info.m_aTimeCp[i] = pSqlServer->GetFloat(Col: i + 2);
295 }
296 str_copy(dst: pResult->m_Data.m_Info.m_aRequestedPlayer, src: pPlayer, dst_size: sizeof(pResult->m_Data.m_Info.m_aRequestedPlayer));
297 }
298 else
299 {
300 pResult->SetVariant(CScorePlayerResult::DIRECT);
301 str_format(buffer: paMessages[0], buffer_size: sizeof(paMessages[0]), format: "'%s' has no checkpoint times available", pPlayer);
302 }
303 return true;
304}
305
306bool CScoreWorker::MapVote(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
307{
308 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
309 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
310 auto *paMessages = pResult->m_Data.m_aaMessages;
311
312 char aFuzzyMap[128];
313 str_copy(dst: aFuzzyMap, src: pData->m_aName, dst_size: sizeof(aFuzzyMap));
314 sqlstr::FuzzyString(pString: aFuzzyMap, Size: sizeof(aFuzzyMap));
315
316 char aMapPrefix[128];
317 str_copy(dst: aMapPrefix, src: pData->m_aName, dst_size: sizeof(aMapPrefix));
318 str_append(dst&: aMapPrefix, src: "%");
319
320 char aBuf[768];
321 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
322 format: "SELECT Map, Server "
323 "FROM %s_maps "
324 "WHERE Map LIKE %s "
325 "ORDER BY "
326 " CASE WHEN LOWER(Map) = LOWER(?) THEN 0 ELSE 1 END, "
327 " CASE WHEN Map LIKE ? THEN 0 ELSE 1 END, "
328 " LENGTH(Map), Map "
329 "LIMIT 1",
330 pSqlServer->GetPrefix(), pSqlServer->CollateNocase());
331 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
332 {
333 return false;
334 }
335 pSqlServer->BindString(Idx: 1, pString: aFuzzyMap);
336 pSqlServer->BindString(Idx: 2, pString: pData->m_aName);
337 pSqlServer->BindString(Idx: 3, pString: aMapPrefix);
338
339 bool End;
340 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
341 {
342 return false;
343 }
344 if(!End)
345 {
346 pResult->SetVariant(CScorePlayerResult::MAP_VOTE);
347 auto *pMapVote = &pResult->m_Data.m_MapVote;
348 pSqlServer->GetString(Col: 1, pBuffer: pMapVote->m_aMap, BufferSize: sizeof(pMapVote->m_aMap));
349 pSqlServer->GetString(Col: 2, pBuffer: pMapVote->m_aServer, BufferSize: sizeof(pMapVote->m_aServer));
350 str_copy(dst: pMapVote->m_aReason, src: "/map", dst_size: sizeof(pMapVote->m_aReason));
351
352 for(char *p = pMapVote->m_aServer; *p; p++) // lower case server
353 *p = tolower(c: *p);
354 }
355 else
356 {
357 pResult->SetVariant(CScorePlayerResult::DIRECT);
358 str_format(buffer: paMessages[0], buffer_size: sizeof(paMessages[0]),
359 format: "No map like \"%s\" found. "
360 "Try adding a '%%' at the start if you don't know the first character. "
361 "Example: /map %%castle for \"Out of Castle\"",
362 pData->m_aName);
363 }
364 return true;
365}
366
367bool CScoreWorker::MapInfo(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
368{
369 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
370 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
371
372 char aFuzzyMap[128];
373 str_copy(dst: aFuzzyMap, src: pData->m_aName, dst_size: sizeof(aFuzzyMap));
374 sqlstr::FuzzyString(pString: aFuzzyMap, Size: sizeof(aFuzzyMap));
375
376 char aMapPrefix[128];
377 str_copy(dst: aMapPrefix, src: pData->m_aName, dst_size: sizeof(aMapPrefix));
378 str_append(dst&: aMapPrefix, src: "%");
379
380 char aCurrentTimestamp[512];
381 pSqlServer->ToUnixTimestamp(pTimestamp: "CURRENT_TIMESTAMP", aBuf: aCurrentTimestamp, BufferSize: sizeof(aCurrentTimestamp));
382 char aTimestamp[512];
383 pSqlServer->ToUnixTimestamp(pTimestamp: "l.Timestamp", aBuf: aTimestamp, BufferSize: sizeof(aTimestamp));
384
385 char aMedianMapTime[2048];
386 char aBuf[4096];
387 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
388 format: "SELECT l.Map, l.Server, Mapper, Points, Stars, "
389 " (SELECT COUNT(Name) FROM %s_race WHERE Map = l.Map) AS Finishes, "
390 " (SELECT COUNT(DISTINCT Name) FROM %s_race WHERE Map = l.Map) AS Finishers, "
391 " (%s) AS Median, "
392 " %s AS Stamp, "
393 " %s-%s AS Ago, "
394 " (SELECT MIN(Time) FROM %s_race WHERE Map = l.Map AND Name = ?) AS OwnTime "
395 "FROM ("
396 " SELECT * FROM %s_maps "
397 " WHERE Map LIKE %s "
398 " ORDER BY "
399 " CASE WHEN LOWER(Map) = LOWER(?) THEN 0 ELSE 1 END, "
400 " CASE WHEN Map LIKE ? THEN 0 ELSE 1 END, "
401 " LENGTH(Map), "
402 " Map "
403 " LIMIT 1"
404 ") as l",
405 pSqlServer->GetPrefix(), pSqlServer->GetPrefix(),
406 pSqlServer->MedianMapTime(pBuffer: aMedianMapTime, BufferSize: sizeof(aMedianMapTime)),
407 aTimestamp, aCurrentTimestamp, aTimestamp,
408 pSqlServer->GetPrefix(), pSqlServer->GetPrefix(),
409 pSqlServer->CollateNocase());
410 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
411 {
412 return false;
413 }
414 pSqlServer->BindString(Idx: 1, pString: pData->m_aRequestingPlayer);
415 pSqlServer->BindString(Idx: 2, pString: aFuzzyMap);
416 pSqlServer->BindString(Idx: 3, pString: pData->m_aName);
417 pSqlServer->BindString(Idx: 4, pString: aMapPrefix);
418
419 bool End;
420 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
421 {
422 return false;
423 }
424 if(!End)
425 {
426 char aMap[MAX_MAP_LENGTH];
427 pSqlServer->GetString(Col: 1, pBuffer: aMap, BufferSize: sizeof(aMap));
428 char aServer[32];
429 pSqlServer->GetString(Col: 2, pBuffer: aServer, BufferSize: sizeof(aServer));
430 char aMapper[128];
431 pSqlServer->GetString(Col: 3, pBuffer: aMapper, BufferSize: sizeof(aMapper));
432 int Points = pSqlServer->GetInt(Col: 4);
433 int Stars = pSqlServer->GetInt(Col: 5);
434 int Finishes = pSqlServer->GetInt(Col: 6);
435 int Finishers = pSqlServer->GetInt(Col: 7);
436 float Median = !pSqlServer->IsNull(Col: 8) ? pSqlServer->GetInt(Col: 8) : -1.0f;
437 int Stamp = pSqlServer->GetInt(Col: 9);
438 int Ago = pSqlServer->GetInt(Col: 10);
439 float OwnTime = !pSqlServer->IsNull(Col: 11) ? pSqlServer->GetFloat(Col: 11) : -1.0f;
440
441 char aAgoString[40] = "\0";
442 char aReleasedString[60] = "\0";
443 if(Stamp != 0)
444 {
445 sqlstr::AgoTimeToString(AgoTime: Ago, pAgoString: aAgoString, Size: sizeof(aAgoString));
446 str_format(buffer: aReleasedString, buffer_size: sizeof(aReleasedString), format: ", released %s ago", aAgoString);
447 }
448
449 char aMedianString[60] = "\0";
450 if(Median > 0)
451 {
452 str_time(centisecs: (int64_t)Median * 100, format: TIME_HOURS, buffer: aBuf, buffer_size: sizeof(aBuf));
453 str_format(buffer: aMedianString, buffer_size: sizeof(aMedianString), format: " in %s median", aBuf);
454 }
455
456 char aStars[20];
457 switch(Stars)
458 {
459 case 0: str_copy(dst: aStars, src: "✰✰✰✰✰", dst_size: sizeof(aStars)); break;
460 case 1: str_copy(dst: aStars, src: "★✰✰✰✰", dst_size: sizeof(aStars)); break;
461 case 2: str_copy(dst: aStars, src: "★★✰✰✰", dst_size: sizeof(aStars)); break;
462 case 3: str_copy(dst: aStars, src: "★★★✰✰", dst_size: sizeof(aStars)); break;
463 case 4: str_copy(dst: aStars, src: "★★★★✰", dst_size: sizeof(aStars)); break;
464 case 5: str_copy(dst: aStars, src: "★★★★★", dst_size: sizeof(aStars)); break;
465 default: aStars[0] = '\0';
466 }
467
468 char aOwnFinishesString[40] = "\0";
469 if(OwnTime > 0)
470 {
471 str_time_float(secs: OwnTime, format: TIME_HOURS_CENTISECS, buffer: aBuf, buffer_size: sizeof(aBuf));
472 str_format(buffer: aOwnFinishesString, buffer_size: sizeof(aOwnFinishesString),
473 format: ", your time: %s", aBuf);
474 }
475
476 str_format(buffer: pResult->m_Data.m_aaMessages[0], buffer_size: sizeof(pResult->m_Data.m_aaMessages[0]),
477 format: "\"%s\" by %s on %s, %s, %d %s%s, %d %s by %d %s%s%s",
478 aMap, aMapper, aServer, aStars,
479 Points, Points == 1 ? "point" : "points",
480 aReleasedString,
481 Finishes, Finishes == 1 ? "finish" : "finishes",
482 Finishers, Finishers == 1 ? "tee" : "tees",
483 aMedianString, aOwnFinishesString);
484 }
485 else
486 {
487 str_format(buffer: pResult->m_Data.m_aaMessages[0], buffer_size: sizeof(pResult->m_Data.m_aaMessages[0]),
488 format: "No map like \"%s\" found.", pData->m_aName);
489 }
490 return true;
491}
492
493bool CScoreWorker::SaveScore(IDbConnection *pSqlServer, const ISqlData *pGameData, Write w, char *pError, int ErrorSize)
494{
495 const auto *pData = dynamic_cast<const CSqlScoreData *>(pGameData);
496 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
497 auto *paMessages = pResult->m_Data.m_aaMessages;
498
499 char aBuf[1024];
500
501 if(w == Write::NORMAL_SUCCEEDED)
502 {
503 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
504 format: "DELETE FROM %s_race_backup WHERE GameId=? AND Name=? AND Timestamp=%s",
505 pSqlServer->GetPrefix(), pSqlServer->InsertTimestampAsUtc());
506 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
507 {
508 return false;
509 }
510 pSqlServer->BindString(Idx: 1, pString: pData->m_aGameUuid);
511 pSqlServer->BindString(Idx: 2, pString: pData->m_aName);
512 pSqlServer->BindString(Idx: 3, pString: pData->m_aTimestamp);
513 pSqlServer->Print();
514 int NumDeleted;
515 if(!pSqlServer->ExecuteUpdate(pNumUpdated: &NumDeleted, pError, ErrorSize))
516 {
517 return false;
518 }
519 if(NumDeleted == 0)
520 {
521 log_warn("sql", "Rank got moved out of backup database, will show up as duplicate rank in MySQL");
522 }
523 return true;
524 }
525 if(w == Write::NORMAL_FAILED)
526 {
527 int NumUpdated;
528 // move to non-tmp table succeeded. delete from backup again
529 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
530 format: "INSERT INTO %s_race SELECT * FROM %s_race_backup WHERE GameId=? AND Name=? AND Timestamp=%s",
531 pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pSqlServer->InsertTimestampAsUtc());
532 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
533 {
534 return false;
535 }
536 pSqlServer->BindString(Idx: 1, pString: pData->m_aGameUuid);
537 pSqlServer->BindString(Idx: 2, pString: pData->m_aName);
538 pSqlServer->BindString(Idx: 3, pString: pData->m_aTimestamp);
539 pSqlServer->Print();
540 if(!pSqlServer->ExecuteUpdate(pNumUpdated: &NumUpdated, pError, ErrorSize))
541 {
542 return false;
543 }
544
545 // move to non-tmp table succeeded. delete from backup again
546 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
547 format: "DELETE FROM %s_race_backup WHERE GameId=? AND Name=? AND Timestamp=%s",
548 pSqlServer->GetPrefix(), pSqlServer->InsertTimestampAsUtc());
549 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
550 {
551 return false;
552 }
553 pSqlServer->BindString(Idx: 1, pString: pData->m_aGameUuid);
554 pSqlServer->BindString(Idx: 2, pString: pData->m_aName);
555 pSqlServer->BindString(Idx: 3, pString: pData->m_aTimestamp);
556 pSqlServer->Print();
557 if(!pSqlServer->ExecuteUpdate(pNumUpdated: &NumUpdated, pError, ErrorSize))
558 {
559 return false;
560 }
561 if(NumUpdated == 0)
562 {
563 log_warn("sql", "Rank got moved out of backup database, will show up as duplicate rank in MySQL");
564 }
565 return true;
566 }
567
568 if(w == Write::NORMAL)
569 {
570 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
571 format: "SELECT COUNT(*) AS NumFinished FROM %s_race WHERE Map=? AND Name=? ORDER BY time ASC LIMIT 1",
572 pSqlServer->GetPrefix());
573 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
574 {
575 return false;
576 }
577 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
578 pSqlServer->BindString(Idx: 2, pString: pData->m_aName);
579
580 bool End;
581 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
582 {
583 return false;
584 }
585 int NumFinished = pSqlServer->GetInt(Col: 1);
586 if(NumFinished == 0)
587 {
588 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "SELECT Points FROM %s_maps WHERE Map=?", pSqlServer->GetPrefix());
589 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
590 {
591 return false;
592 }
593 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
594
595 bool End2;
596 if(!pSqlServer->Step(pEnd: &End2, pError, ErrorSize))
597 {
598 return false;
599 }
600 if(!End2)
601 {
602 int Points = pSqlServer->GetInt(Col: 1);
603 if(!pSqlServer->AddPoints(pPlayer: pData->m_aName, Points, pError, ErrorSize))
604 {
605 return false;
606 }
607 str_format(buffer: paMessages[0], buffer_size: sizeof(paMessages[0]),
608 format: "You earned %d point%s for finishing this map!",
609 Points, Points == 1 ? "" : "s");
610 }
611 }
612 }
613
614 // save score. Can't fail, because no UNIQUE/PRIMARY KEY constrain is defined.
615 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
616 format: "%s INTO %s_race%s("
617 " Map, Name, Timestamp, Time, Server, "
618 " cp1, cp2, cp3, cp4, cp5, cp6, cp7, cp8, cp9, cp10, cp11, cp12, cp13, "
619 " cp14, cp15, cp16, cp17, cp18, cp19, cp20, cp21, cp22, cp23, cp24, cp25, "
620 " GameId, DDNet7) "
621 "VALUES (?, ?, %s, %.2f, ?, "
622 " %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, "
623 " %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, "
624 " %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, "
625 " ?, %s)",
626 pSqlServer->InsertIgnore(), pSqlServer->GetPrefix(),
627 w == Write::NORMAL ? "" : "_backup",
628 pSqlServer->InsertTimestampAsUtc(), pData->m_Time,
629 pData->m_aCurrentTimeCp[0], pData->m_aCurrentTimeCp[1], pData->m_aCurrentTimeCp[2],
630 pData->m_aCurrentTimeCp[3], pData->m_aCurrentTimeCp[4], pData->m_aCurrentTimeCp[5],
631 pData->m_aCurrentTimeCp[6], pData->m_aCurrentTimeCp[7], pData->m_aCurrentTimeCp[8],
632 pData->m_aCurrentTimeCp[9], pData->m_aCurrentTimeCp[10], pData->m_aCurrentTimeCp[11],
633 pData->m_aCurrentTimeCp[12], pData->m_aCurrentTimeCp[13], pData->m_aCurrentTimeCp[14],
634 pData->m_aCurrentTimeCp[15], pData->m_aCurrentTimeCp[16], pData->m_aCurrentTimeCp[17],
635 pData->m_aCurrentTimeCp[18], pData->m_aCurrentTimeCp[19], pData->m_aCurrentTimeCp[20],
636 pData->m_aCurrentTimeCp[21], pData->m_aCurrentTimeCp[22], pData->m_aCurrentTimeCp[23],
637 pData->m_aCurrentTimeCp[24], pSqlServer->False());
638 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
639 {
640 return false;
641 }
642 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
643 pSqlServer->BindString(Idx: 2, pString: pData->m_aName);
644 pSqlServer->BindString(Idx: 3, pString: pData->m_aTimestamp);
645 pSqlServer->BindString(Idx: 4, pString: g_Config.m_SvSqlServerName);
646 pSqlServer->BindString(Idx: 5, pString: pData->m_aGameUuid);
647 pSqlServer->Print();
648 int NumInserted;
649 return pSqlServer->ExecuteUpdate(pNumUpdated: &NumInserted, pError, ErrorSize);
650}
651
652bool CScoreWorker::SaveTeamScore(IDbConnection *pSqlServer, const ISqlData *pGameData, Write w, char *pError, int ErrorSize)
653{
654 const auto *pData = dynamic_cast<const CSqlTeamScoreData *>(pGameData);
655
656 char aBuf[512];
657
658 if(w == Write::NORMAL_SUCCEEDED)
659 {
660 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
661 format: "DELETE FROM %s_teamrace_backup WHERE Id=?",
662 pSqlServer->GetPrefix());
663 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
664 {
665 return false;
666 }
667
668 // copy uuid, because mysql BindBlob doesn't support const buffers
669 CUuid TeamrankId = pData->m_TeamrankUuid;
670 pSqlServer->BindBlob(Idx: 1, pBlob: TeamrankId.m_aData, Size: sizeof(TeamrankId.m_aData));
671 pSqlServer->Print();
672 int NumDeleted;
673 if(!pSqlServer->ExecuteUpdate(pNumUpdated: &NumDeleted, pError, ErrorSize))
674 {
675 return false;
676 }
677 if(NumDeleted == 0)
678 {
679 log_warn("sql", "Teamrank got moved out of backup database, will show up as duplicate teamrank in MySQL");
680 }
681 return true;
682 }
683 if(w == Write::NORMAL_FAILED)
684 {
685 int NumInserted;
686 CUuid TeamrankId = pData->m_TeamrankUuid;
687
688 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
689 format: "INSERT INTO %s_teamrace SELECT * FROM %s_teamrace_backup WHERE Id=?",
690 pSqlServer->GetPrefix(), pSqlServer->GetPrefix());
691 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
692 {
693 return false;
694 }
695 pSqlServer->BindBlob(Idx: 1, pBlob: TeamrankId.m_aData, Size: sizeof(TeamrankId.m_aData));
696 pSqlServer->Print();
697 if(!pSqlServer->ExecuteUpdate(pNumUpdated: &NumInserted, pError, ErrorSize))
698 {
699 return false;
700 }
701
702 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
703 format: "DELETE FROM %s_teamrace_backup WHERE Id=?",
704 pSqlServer->GetPrefix());
705 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
706 {
707 return false;
708 }
709 pSqlServer->BindBlob(Idx: 1, pBlob: TeamrankId.m_aData, Size: sizeof(TeamrankId.m_aData));
710 pSqlServer->Print();
711 return pSqlServer->ExecuteUpdate(pNumUpdated: &NumInserted, pError, ErrorSize);
712 }
713
714 if(w == Write::NORMAL)
715 {
716 // get the names sorted in a tab separated string
717 std::vector<std::string> vNames;
718 vNames.reserve(n: pData->m_Size);
719 for(unsigned int i = 0; i < pData->m_Size; i++)
720 vNames.emplace_back(args: pData->m_aaNames[i]);
721
722 std::sort(first: vNames.begin(), last: vNames.end());
723 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
724 format: "SELECT l.Id, Name, Time "
725 "FROM (" // preselect teams with first name in team
726 " SELECT ID "
727 " FROM %s_teamrace "
728 " WHERE Map = ? AND Name = ? AND DDNet7 = %s"
729 ") as l INNER JOIN %s_teamrace AS r ON l.Id = r.Id "
730 "ORDER BY l.Id, Name COLLATE %s",
731 pSqlServer->GetPrefix(), pSqlServer->False(), pSqlServer->GetPrefix(), pSqlServer->BinaryCollate());
732 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
733 {
734 return false;
735 }
736 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
737 pSqlServer->BindString(Idx: 2, pString: pData->m_aaNames[0]);
738
739 bool FoundTeam = false;
740 float Time;
741 CTeamrank Teamrank;
742 bool End;
743 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
744 {
745 return false;
746 }
747 if(!End)
748 {
749 bool SearchTeamEnd = false;
750 while(!SearchTeamEnd)
751 {
752 Time = pSqlServer->GetFloat(Col: 3);
753 if(!Teamrank.NextSqlResult(pSqlServer, pEnd: &SearchTeamEnd, pError, ErrorSize))
754 {
755 return false;
756 }
757 if(Teamrank.SamePlayers(pvSortedNames: &vNames))
758 {
759 FoundTeam = true;
760 break;
761 }
762 }
763 }
764 if(FoundTeam)
765 {
766 dbg_msg(sys: "sql", fmt: "found team rank from same team (old time: %f, new time: %f)", Time, pData->m_Time);
767 if(pData->m_Time < Time)
768 {
769 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
770 format: "UPDATE %s_teamrace SET Time=%.2f, Timestamp=%s, DDNet7=%s, GameId=? WHERE Id = ?",
771 pSqlServer->GetPrefix(), pData->m_Time, pSqlServer->InsertTimestampAsUtc(), pSqlServer->False());
772 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
773 {
774 return false;
775 }
776 pSqlServer->BindString(Idx: 1, pString: pData->m_aTimestamp);
777 pSqlServer->BindString(Idx: 2, pString: pData->m_aGameUuid);
778 pSqlServer->BindBlob(Idx: 3, pBlob: Teamrank.m_TeamId.m_aData, Size: sizeof(Teamrank.m_TeamId.m_aData));
779 pSqlServer->Print();
780 int NumUpdated;
781 if(!pSqlServer->ExecuteUpdate(pNumUpdated: &NumUpdated, pError, ErrorSize))
782 {
783 return false;
784 }
785 // return error if we didn't update any rows
786 return NumUpdated != 0;
787 }
788 return true;
789 }
790 }
791
792 for(unsigned int i = 0; i < pData->m_Size; i++)
793 {
794 // if no entry found... create a new one
795 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
796 format: "%s INTO %s_teamrace%s(Map, Name, Timestamp, Time, Id, GameId, DDNet7) "
797 "VALUES (?, ?, %s, %.2f, ?, ?, %s)",
798 pSqlServer->InsertIgnore(), pSqlServer->GetPrefix(),
799 w == Write::NORMAL ? "" : "_backup",
800 pSqlServer->InsertTimestampAsUtc(), pData->m_Time, pSqlServer->False());
801 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
802 {
803 return false;
804 }
805 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
806 pSqlServer->BindString(Idx: 2, pString: pData->m_aaNames[i]);
807 pSqlServer->BindString(Idx: 3, pString: pData->m_aTimestamp);
808 // copy uuid, because mysql BindBlob doesn't support const buffers
809 CUuid TeamrankId = pData->m_TeamrankUuid;
810 pSqlServer->BindBlob(Idx: 4, pBlob: TeamrankId.m_aData, Size: sizeof(TeamrankId.m_aData));
811 pSqlServer->BindString(Idx: 5, pString: pData->m_aGameUuid);
812 pSqlServer->Print();
813 int NumInserted;
814 if(!pSqlServer->ExecuteUpdate(pNumUpdated: &NumInserted, pError, ErrorSize))
815 {
816 return false;
817 }
818 }
819 return true;
820}
821
822bool CScoreWorker::ShowRank(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
823{
824 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
825 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
826
827 char aServerLike[16];
828 str_format(buffer: aServerLike, buffer_size: sizeof(aServerLike), format: "%%%s%%", pData->m_aServer);
829
830 // check sort method
831 char aBuf[600];
832 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
833 format: "SELECT Ranking, Time, PercentRank "
834 "FROM ("
835 " SELECT RANK() OVER w AS Ranking, PERCENT_RANK() OVER w as PercentRank, MIN(Time) AS Time, Name "
836 " FROM %s_race "
837 " WHERE Map = ? "
838 " AND Server LIKE ? "
839 " GROUP BY Name "
840 " WINDOW w AS (ORDER BY MIN(Time))"
841 ") as a "
842 "WHERE Name = ?",
843 pSqlServer->GetPrefix());
844
845 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
846 {
847 return false;
848 }
849 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
850 pSqlServer->BindString(Idx: 2, pString: aServerLike);
851 pSqlServer->BindString(Idx: 3, pString: pData->m_aName);
852
853 bool End;
854 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
855 {
856 return false;
857 }
858
859 char aRegionalRank[16];
860 if(End)
861 {
862 str_copy(dst: aRegionalRank, src: "unranked", dst_size: sizeof(aRegionalRank));
863 }
864 else
865 {
866 str_format(buffer: aRegionalRank, buffer_size: sizeof(aRegionalRank), format: "rank %d", pSqlServer->GetInt(Col: 1));
867 }
868
869 const char *pAny = "%";
870
871 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
872 {
873 return false;
874 }
875 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
876 pSqlServer->BindString(Idx: 2, pString: pAny);
877 pSqlServer->BindString(Idx: 3, pString: pData->m_aName);
878
879 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
880 {
881 return false;
882 }
883
884 if(!End)
885 {
886 int Rank = pSqlServer->GetInt(Col: 1);
887 float Time = pSqlServer->GetFloat(Col: 2);
888 str_time_float(secs: Time, format: TIME_HOURS_CENTISECS, buffer: aBuf, buffer_size: sizeof(aBuf));
889
890 if(g_Config.m_SvHideScore)
891 {
892 str_format(buffer: pResult->m_Data.m_aaMessages[0], buffer_size: sizeof(pResult->m_Data.m_aaMessages[0]),
893 format: "Your time: %s", aBuf);
894 }
895 else
896 {
897 pResult->m_MessageKind = CScorePlayerResult::ALL;
898 // CEIL and FLOOR are not supported in SQLite
899 int BetterThanPercent = std::floor(x: 100.0f - 100.0f * pSqlServer->GetFloat(Col: 3));
900
901 if(str_comp_nocase(a: pData->m_aRequestingPlayer, b: pData->m_aName) == 0)
902 {
903 str_format(buffer: pResult->m_Data.m_aaMessages[0], buffer_size: sizeof(pResult->m_Data.m_aaMessages[0]),
904 format: "%s - %s - better than %d%%",
905 pData->m_aName, aBuf, BetterThanPercent);
906 }
907 else
908 {
909 str_format(buffer: pResult->m_Data.m_aaMessages[0], buffer_size: sizeof(pResult->m_Data.m_aaMessages[0]),
910 format: "%s - %s - better than %d%% - requested by %s",
911 pData->m_aName, aBuf, BetterThanPercent, pData->m_aRequestingPlayer);
912 }
913
914 if(g_Config.m_SvRegionalRankings)
915 {
916 str_format(buffer: pResult->m_Data.m_aaMessages[1], buffer_size: sizeof(pResult->m_Data.m_aaMessages[1]),
917 format: "Global rank %d - %s %s",
918 Rank, pData->m_aServer, aRegionalRank);
919 }
920 else
921 {
922 str_format(buffer: pResult->m_Data.m_aaMessages[1], buffer_size: sizeof(pResult->m_Data.m_aaMessages[1]),
923 format: "Global rank %d", Rank);
924 }
925 }
926 }
927 else
928 {
929 str_format(buffer: pResult->m_Data.m_aaMessages[0], buffer_size: sizeof(pResult->m_Data.m_aaMessages[0]),
930 format: "%s is not ranked", pData->m_aName);
931 }
932 return true;
933}
934
935bool CScoreWorker::ShowTeamRank(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
936{
937 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
938 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
939
940 // check sort method
941 char aBuf[2400];
942
943 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
944 format: "SELECT l.Id, Name, Time, Ranking, PercentRank "
945 "FROM (" // teamrank score board
946 " SELECT RANK() OVER w AS Ranking, PERCENT_RANK() OVER w AS PercentRank, Id "
947 " FROM %s_teamrace "
948 " WHERE Map = ? "
949 " GROUP BY ID "
950 " WINDOW w AS (ORDER BY Min(Time))"
951 ") AS TeamRank INNER JOIN (" // select rank with Name in team
952 " SELECT ID "
953 " FROM %s_teamrace "
954 " WHERE Map = ? AND Name = ? "
955 " ORDER BY Time "
956 " LIMIT 1"
957 ") AS l ON TeamRank.Id = l.Id "
958 "INNER JOIN %s_teamrace AS r ON l.Id = r.Id ",
959 pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pSqlServer->GetPrefix());
960 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
961 {
962 return false;
963 }
964 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
965 pSqlServer->BindString(Idx: 2, pString: pData->m_aMap);
966 pSqlServer->BindString(Idx: 3, pString: pData->m_aName);
967
968 bool End;
969 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
970 {
971 return false;
972 }
973 if(!End)
974 {
975 float Time = pSqlServer->GetFloat(Col: 3);
976 str_time_float(secs: Time, format: TIME_HOURS_CENTISECS, buffer: aBuf, buffer_size: sizeof(aBuf));
977 int Rank = pSqlServer->GetInt(Col: 4);
978 // CEIL and FLOOR are not supported in SQLite
979 int BetterThanPercent = std::floor(x: 100.0f - 100.0f * pSqlServer->GetFloat(Col: 5));
980 CTeamrank Teamrank;
981 if(!Teamrank.NextSqlResult(pSqlServer, pEnd: &End, pError, ErrorSize))
982 {
983 return false;
984 }
985
986 char aFormattedNames[512] = "";
987 for(unsigned int Name = 0; Name < Teamrank.m_NumNames; Name++)
988 {
989 str_append(dst&: aFormattedNames, src: Teamrank.m_aaNames[Name]);
990
991 if(Name < Teamrank.m_NumNames - 2)
992 str_append(dst&: aFormattedNames, src: ", ");
993 else if(Name < Teamrank.m_NumNames - 1)
994 str_append(dst&: aFormattedNames, src: " & ");
995 }
996
997 if(g_Config.m_SvHideScore)
998 {
999 str_format(buffer: pResult->m_Data.m_aaMessages[0], buffer_size: sizeof(pResult->m_Data.m_aaMessages[0]),
1000 format: "Your team time: %s, better than %d%%", aBuf, BetterThanPercent);
1001 }
1002 else
1003 {
1004 pResult->m_MessageKind = CScorePlayerResult::ALL;
1005 str_format(buffer: pResult->m_Data.m_aaMessages[0], buffer_size: sizeof(pResult->m_Data.m_aaMessages[0]),
1006 format: "%d. %s Team time: %s, better than %d%%, requested by %s",
1007 Rank, aFormattedNames, aBuf, BetterThanPercent, pData->m_aRequestingPlayer);
1008 }
1009 }
1010 else
1011 {
1012 str_format(buffer: pResult->m_Data.m_aaMessages[0], buffer_size: sizeof(pResult->m_Data.m_aaMessages[0]),
1013 format: "%s has no team ranks", pData->m_aName);
1014 }
1015 return true;
1016}
1017
1018bool CScoreWorker::ShowTop(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
1019{
1020 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
1021 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
1022
1023 int LimitStart = maximum(a: absolute(a: pData->m_Offset) - 1, b: 0);
1024 const char *pOrder = pData->m_Offset >= 0 ? "ASC" : "DESC";
1025 const char *pAny = "%";
1026
1027 // check sort method
1028 char aBuf[512];
1029 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1030 format: "SELECT Name, Time, Ranking "
1031 "FROM ("
1032 " SELECT RANK() OVER w AS Ranking, MIN(Time) AS Time, Name "
1033 " FROM %s_race "
1034 " WHERE Map = ? "
1035 " AND Server LIKE ? "
1036 " GROUP BY Name "
1037 " WINDOW w AS (ORDER BY MIN(Time))"
1038 ") as a "
1039 "ORDER BY Ranking %s "
1040 "LIMIT %d, ?",
1041 pSqlServer->GetPrefix(),
1042 pOrder,
1043 LimitStart);
1044
1045 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1046 {
1047 return false;
1048 }
1049 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
1050 pSqlServer->BindString(Idx: 2, pString: pAny);
1051 pSqlServer->BindInt(Idx: 3, Value: 5);
1052
1053 // show top
1054 int Line = 0;
1055 str_copy(dst: pResult->m_Data.m_aaMessages[Line], src: "------------ Global Top ------------", dst_size: sizeof(pResult->m_Data.m_aaMessages[Line]));
1056 Line++;
1057
1058 char aTime[32];
1059 bool End = false;
1060
1061 while(pSqlServer->Step(pEnd: &End, pError, ErrorSize) && !End)
1062 {
1063 char aName[MAX_NAME_LENGTH];
1064 pSqlServer->GetString(Col: 1, pBuffer: aName, BufferSize: sizeof(aName));
1065 float Time = pSqlServer->GetFloat(Col: 2);
1066 str_time_float(secs: Time, format: TIME_HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1067 int Rank = pSqlServer->GetInt(Col: 3);
1068 str_format(buffer: pResult->m_Data.m_aaMessages[Line], buffer_size: sizeof(pResult->m_Data.m_aaMessages[Line]),
1069 format: "%d. %s Time: %s", Rank, aName, aTime);
1070
1071 Line++;
1072 }
1073
1074 if(!g_Config.m_SvRegionalRankings)
1075 {
1076 str_copy(dst: pResult->m_Data.m_aaMessages[Line], src: "-----------------------------------------", dst_size: sizeof(pResult->m_Data.m_aaMessages[Line]));
1077 return End;
1078 }
1079
1080 char aServerLike[16];
1081 str_format(buffer: aServerLike, buffer_size: sizeof(aServerLike), format: "%%%s%%", pData->m_aServer);
1082
1083 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1084 {
1085 return false;
1086 }
1087 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
1088 pSqlServer->BindString(Idx: 2, pString: aServerLike);
1089 pSqlServer->BindInt(Idx: 3, Value: 3);
1090
1091 str_format(buffer: pResult->m_Data.m_aaMessages[Line], buffer_size: sizeof(pResult->m_Data.m_aaMessages[Line]),
1092 format: "------------ %s Top ------------", pData->m_aServer);
1093 Line++;
1094
1095 // show top
1096 while(pSqlServer->Step(pEnd: &End, pError, ErrorSize) && !End)
1097 {
1098 char aName[MAX_NAME_LENGTH];
1099 pSqlServer->GetString(Col: 1, pBuffer: aName, BufferSize: sizeof(aName));
1100 float Time = pSqlServer->GetFloat(Col: 2);
1101 str_time_float(secs: Time, format: TIME_HOURS_CENTISECS, buffer: aTime, buffer_size: sizeof(aTime));
1102 int Rank = pSqlServer->GetInt(Col: 3);
1103 str_format(buffer: pResult->m_Data.m_aaMessages[Line], buffer_size: sizeof(pResult->m_Data.m_aaMessages[Line]),
1104 format: "%d. %s Time: %s", Rank, aName, aTime);
1105 Line++;
1106 }
1107
1108 return End;
1109}
1110
1111bool CScoreWorker::ShowTeamTop5(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
1112{
1113 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
1114 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
1115 auto *paMessages = pResult->m_Data.m_aaMessages;
1116
1117 int LimitStart = maximum(a: absolute(a: pData->m_Offset) - 1, b: 0);
1118 const char *pOrder = pData->m_Offset >= 0 ? "ASC" : "DESC";
1119 const char *pAny = "%";
1120
1121 // check sort method
1122 char aBuf[1024];
1123
1124 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1125 format: "SELECT Name, Time, Ranking, TeamSize "
1126 "FROM ("
1127 " SELECT TeamSize, Ranking, Id, Server "
1128 " FROM (" // teamrank score board
1129 " SELECT RANK() OVER w AS Ranking, COUNT(*) AS Teamsize, Id, Server "
1130 " FROM ("
1131 " SELECT tr.Map, tr.Time, tr.Id, ("
1132 " SELECT rr.Server FROM %s_race AS rr "
1133 " WHERE rr.Map = tr.Map AND rr.Name = tr.Name AND rr.Time = tr.Time "
1134 " LIMIT 1"
1135 " ) AS Server "
1136 " FROM %s_teamrace AS tr "
1137 " WHERE tr.Map = ? "
1138 " ) AS ll "
1139 " GROUP BY ID "
1140 " WINDOW w AS (ORDER BY Min(Time))"
1141 " ) as l1 "
1142 " WHERE Server LIKE ? "
1143 " ORDER BY Ranking %s "
1144 " LIMIT %d, ?"
1145 ") as l2 "
1146 "INNER JOIN %s_teamrace as r ON l2.Id = r.Id "
1147 "ORDER BY Ranking %s, r.Id, Name ASC",
1148 pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pOrder, LimitStart, pSqlServer->GetPrefix(), pOrder);
1149 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1150 {
1151 return false;
1152 }
1153 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
1154 pSqlServer->BindString(Idx: 2, pString: pAny);
1155 pSqlServer->BindInt(Idx: 3, Value: 5);
1156
1157 int Line = 0;
1158 str_copy(dst: paMessages[Line++], src: "------- Team Top 5 -------", dst_size: sizeof(paMessages[Line]));
1159
1160 bool End;
1161 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
1162 {
1163 return false;
1164 }
1165 if(!End)
1166 {
1167 if(!CTeamrank::GetSqlTop5Team(pSqlServer, pEnd: &End, pError, ErrorSize, paMessages, Line: &Line, Count: 5))
1168 {
1169 return false;
1170 }
1171 }
1172
1173 if(!g_Config.m_SvRegionalRankings)
1174 {
1175 str_copy(dst: paMessages[Line], src: "-------------------------------", dst_size: sizeof(paMessages[Line]));
1176 return true;
1177 }
1178
1179 char aServerLike[16];
1180 str_format(buffer: aServerLike, buffer_size: sizeof(aServerLike), format: "%%%s%%", pData->m_aServer);
1181
1182 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1183 {
1184 return false;
1185 }
1186 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
1187 pSqlServer->BindString(Idx: 2, pString: aServerLike);
1188 pSqlServer->BindInt(Idx: 3, Value: 3);
1189
1190 str_format(buffer: pResult->m_Data.m_aaMessages[Line], buffer_size: sizeof(pResult->m_Data.m_aaMessages[Line]),
1191 format: "----- %s Team Top -----", pData->m_aServer);
1192 Line++;
1193
1194 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
1195 {
1196 return false;
1197 }
1198 if(!End)
1199 {
1200 if(!CTeamrank::GetSqlTop5Team(pSqlServer, pEnd: &End, pError, ErrorSize, paMessages, Line: &Line, Count: 3))
1201 {
1202 return false;
1203 }
1204 }
1205 return true;
1206}
1207
1208bool CScoreWorker::ShowPlayerTeamTop5(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
1209{
1210 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
1211 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
1212 auto *paMessages = pResult->m_Data.m_aaMessages;
1213
1214 int LimitStart = maximum(a: absolute(a: pData->m_Offset) - 1, b: 0);
1215 const char *pOrder = pData->m_Offset >= 0 ? "ASC" : "DESC";
1216
1217 // check sort method
1218 char aBuf[2400];
1219
1220 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1221 format: "SELECT l.Id, Name, Time, Ranking "
1222 "FROM (" // teamrank score board
1223 " SELECT RANK() OVER w AS Ranking, Id "
1224 " FROM %s_teamrace "
1225 " WHERE Map = ? "
1226 " GROUP BY ID "
1227 " WINDOW w AS (ORDER BY Min(Time))"
1228 ") AS TeamRank INNER JOIN (" // select rank with Name in team
1229 " SELECT ID "
1230 " FROM %s_teamrace "
1231 " WHERE Map = ? AND Name = ? "
1232 " ORDER BY Time %s "
1233 " LIMIT %d, 5 "
1234 ") AS l ON TeamRank.Id = l.Id "
1235 "INNER JOIN %s_teamrace AS r ON l.Id = r.Id "
1236 "ORDER BY Time %s, l.Id, Name ASC",
1237 pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pOrder, LimitStart, pSqlServer->GetPrefix(), pOrder);
1238 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1239 {
1240 return false;
1241 }
1242 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
1243 pSqlServer->BindString(Idx: 2, pString: pData->m_aMap);
1244 pSqlServer->BindString(Idx: 3, pString: pData->m_aName);
1245
1246 bool End;
1247 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
1248 {
1249 return false;
1250 }
1251 if(!End)
1252 {
1253 // show teamtop5
1254 int Line = 0;
1255 str_copy(dst: paMessages[Line++], src: "------- Team Top 5 -------", dst_size: sizeof(paMessages[Line]));
1256
1257 for(Line = 1; Line < 6; Line++) // print
1258 {
1259 float Time = pSqlServer->GetFloat(Col: 3);
1260 str_time_float(secs: Time, format: TIME_HOURS_CENTISECS, buffer: aBuf, buffer_size: sizeof(aBuf));
1261 int Rank = pSqlServer->GetInt(Col: 4);
1262 CTeamrank Teamrank;
1263 bool Last;
1264 if(!Teamrank.NextSqlResult(pSqlServer, pEnd: &Last, pError, ErrorSize))
1265 {
1266 return false;
1267 }
1268
1269 char aFormattedNames[512] = "";
1270 for(unsigned int Name = 0; Name < Teamrank.m_NumNames; Name++)
1271 {
1272 str_append(dst&: aFormattedNames, src: Teamrank.m_aaNames[Name]);
1273
1274 if(Name < Teamrank.m_NumNames - 2)
1275 str_append(dst&: aFormattedNames, src: ", ");
1276 else if(Name < Teamrank.m_NumNames - 1)
1277 str_append(dst&: aFormattedNames, src: " & ");
1278 }
1279
1280 str_format(buffer: paMessages[Line], buffer_size: sizeof(paMessages[Line]), format: "%d. %s Team Time: %s",
1281 Rank, aFormattedNames, aBuf);
1282 if(Last)
1283 {
1284 Line++;
1285 break;
1286 }
1287 }
1288 str_copy(dst: paMessages[Line], src: "---------------------------------", dst_size: sizeof(paMessages[Line]));
1289 }
1290 else
1291 {
1292 if(pData->m_Offset == 0)
1293 str_format(buffer: paMessages[0], buffer_size: sizeof(paMessages[0]), format: "%s has no team ranks", pData->m_aName);
1294 else
1295 str_format(buffer: paMessages[0], buffer_size: sizeof(paMessages[0]), format: "%s has no team ranks in the specified range", pData->m_aName);
1296 }
1297 return true;
1298}
1299
1300bool CScoreWorker::ShowTimes(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
1301{
1302 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
1303 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
1304 auto *paMessages = pResult->m_Data.m_aaMessages;
1305
1306 int LimitStart = maximum(a: absolute(a: pData->m_Offset) - 1, b: 0);
1307 const char *pOrder = pData->m_Offset >= 0 ? "DESC" : "ASC";
1308
1309 char aCurrentTimestamp[512];
1310 pSqlServer->ToUnixTimestamp(pTimestamp: "CURRENT_TIMESTAMP", aBuf: aCurrentTimestamp, BufferSize: sizeof(aCurrentTimestamp));
1311 char aTimestamp[512];
1312 pSqlServer->ToUnixTimestamp(pTimestamp: "Timestamp", aBuf: aTimestamp, BufferSize: sizeof(aTimestamp));
1313 char aBuf[512];
1314 if(pData->m_aName[0] != '\0') // last 5 times of a player
1315 {
1316 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1317 format: "SELECT Time, (%s-%s) as Ago, %s as Stamp, Server "
1318 "FROM %s_race "
1319 "WHERE Map = ? AND Name = ? "
1320 "ORDER BY Timestamp %s "
1321 "LIMIT ?, 5",
1322 aCurrentTimestamp, aTimestamp, aTimestamp,
1323 pSqlServer->GetPrefix(), pOrder);
1324 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1325 {
1326 return false;
1327 }
1328 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
1329 pSqlServer->BindString(Idx: 2, pString: pData->m_aName);
1330 pSqlServer->BindInt(Idx: 3, Value: LimitStart);
1331 }
1332 else // last 5 times of server
1333 {
1334 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1335 format: "SELECT Time, (%s-%s) as Ago, %s as Stamp, Server, Name "
1336 "FROM %s_race "
1337 "WHERE Map = ? "
1338 "ORDER BY Timestamp %s "
1339 "LIMIT ?, 5",
1340 aCurrentTimestamp, aTimestamp, aTimestamp,
1341 pSqlServer->GetPrefix(), pOrder);
1342 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1343 {
1344 return false;
1345 }
1346 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
1347 pSqlServer->BindInt(Idx: 2, Value: LimitStart);
1348 }
1349
1350 // show top5
1351 bool End;
1352 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
1353 {
1354 return false;
1355 }
1356 if(End)
1357 {
1358 str_copy(dst: paMessages[0], src: "There are no times in the specified range", dst_size: sizeof(paMessages[0]));
1359 return true;
1360 }
1361
1362 str_copy(dst: paMessages[0], src: "------------- Last Times -------------", dst_size: sizeof(paMessages[0]));
1363 int Line = 1;
1364
1365 do
1366 {
1367 float Time = pSqlServer->GetFloat(Col: 1);
1368 str_time_float(secs: Time, format: TIME_HOURS_CENTISECS, buffer: aBuf, buffer_size: sizeof(aBuf));
1369 int Ago = pSqlServer->GetInt(Col: 2);
1370 int Stamp = pSqlServer->GetInt(Col: 3);
1371 char aServer[5];
1372 pSqlServer->GetString(Col: 4, pBuffer: aServer, BufferSize: sizeof(aServer));
1373 char aServerFormatted[8] = "\0";
1374 if(str_comp(a: aServer, b: "UNK") != 0)
1375 str_format(buffer: aServerFormatted, buffer_size: sizeof(aServerFormatted), format: "[%s] ", aServer);
1376
1377 char aAgoString[40] = "\0";
1378 sqlstr::AgoTimeToString(AgoTime: Ago, pAgoString: aAgoString, Size: sizeof(aAgoString));
1379
1380 if(pData->m_aName[0] != '\0') // last 5 times of a player
1381 {
1382 if(Stamp == 0) // stamp is 00:00:00 cause it's an old entry from old times where there where no stamps yet
1383 str_format(buffer: paMessages[Line], buffer_size: sizeof(paMessages[Line]),
1384 format: "%s%s, don't know how long ago", aServerFormatted, aBuf);
1385 else
1386 str_format(buffer: paMessages[Line], buffer_size: sizeof(paMessages[Line]),
1387 format: "%s%s ago, %s", aServerFormatted, aAgoString, aBuf);
1388 }
1389 else // last 5 times of the server
1390 {
1391 char aName[MAX_NAME_LENGTH];
1392 pSqlServer->GetString(Col: 5, pBuffer: aName, BufferSize: sizeof(aName));
1393 if(Stamp == 0) // stamp is 00:00:00 cause it's an old entry from old times where there where no stamps yet
1394 {
1395 str_format(buffer: paMessages[Line], buffer_size: sizeof(paMessages[Line]),
1396 format: "%s%s, %s, don't know when", aServerFormatted, aName, aBuf);
1397 }
1398 else
1399 {
1400 str_format(buffer: paMessages[Line], buffer_size: sizeof(paMessages[Line]),
1401 format: "%s%s, %s ago, %s", aServerFormatted, aName, aAgoString, aBuf);
1402 }
1403 }
1404 Line++;
1405 } while(pSqlServer->Step(pEnd: &End, pError, ErrorSize) && !End);
1406 if(!End)
1407 {
1408 return false;
1409 }
1410 str_copy(dst: paMessages[Line], src: "-------------------------------------------", dst_size: sizeof(paMessages[Line]));
1411
1412 return true;
1413}
1414
1415bool CScoreWorker::ShowPoints(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
1416{
1417 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
1418 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
1419 auto *paMessages = pResult->m_Data.m_aaMessages;
1420
1421 char aBuf[512];
1422 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1423 format: "SELECT ("
1424 " SELECT COUNT(Name) + 1 FROM %s_points WHERE Points > ("
1425 " SELECT Points FROM %s_points WHERE Name = ?"
1426 ")) as Ranking, Points, Name "
1427 "FROM %s_points WHERE Name = ?",
1428 pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pSqlServer->GetPrefix());
1429 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1430 {
1431 return false;
1432 }
1433 pSqlServer->BindString(Idx: 1, pString: pData->m_aName);
1434 pSqlServer->BindString(Idx: 2, pString: pData->m_aName);
1435
1436 bool End;
1437 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
1438 {
1439 return false;
1440 }
1441 if(!End)
1442 {
1443 int Rank = pSqlServer->GetInt(Col: 1);
1444 int Count = pSqlServer->GetInt(Col: 2);
1445 char aName[MAX_NAME_LENGTH];
1446 pSqlServer->GetString(Col: 3, pBuffer: aName, BufferSize: sizeof(aName));
1447 pResult->m_MessageKind = CScorePlayerResult::ALL;
1448 str_format(buffer: paMessages[0], buffer_size: sizeof(paMessages[0]),
1449 format: "%d. %s Points: %d, requested by %s",
1450 Rank, aName, Count, pData->m_aRequestingPlayer);
1451 }
1452 else
1453 {
1454 str_format(buffer: paMessages[0], buffer_size: sizeof(paMessages[0]),
1455 format: "%s has not collected any points so far", pData->m_aName);
1456 }
1457 return true;
1458}
1459
1460bool CScoreWorker::ShowTopPoints(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
1461{
1462 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
1463 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
1464 auto *paMessages = pResult->m_Data.m_aaMessages;
1465
1466 int LimitStart = maximum(a: pData->m_Offset - 1, b: 0);
1467
1468 char aBuf[512];
1469 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1470 format: "SELECT RANK() OVER (ORDER BY a.Points DESC) as Ranking, Points, Name "
1471 "FROM ("
1472 " SELECT Points, Name "
1473 " FROM %s_points "
1474 " ORDER BY Points DESC LIMIT ?"
1475 ") as a "
1476 "ORDER BY Ranking ASC, Name ASC LIMIT ?, 5",
1477 pSqlServer->GetPrefix());
1478 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1479 {
1480 return false;
1481 }
1482 pSqlServer->BindInt(Idx: 1, Value: LimitStart + 5);
1483 pSqlServer->BindInt(Idx: 2, Value: LimitStart);
1484
1485 // show top points
1486 str_copy(dst: paMessages[0], src: "-------- Top Points --------", dst_size: sizeof(paMessages[0]));
1487
1488 bool End = false;
1489 int Line = 1;
1490 while(pSqlServer->Step(pEnd: &End, pError, ErrorSize) && !End)
1491 {
1492 int Rank = pSqlServer->GetInt(Col: 1);
1493 int Points = pSqlServer->GetInt(Col: 2);
1494 char aName[MAX_NAME_LENGTH];
1495 pSqlServer->GetString(Col: 3, pBuffer: aName, BufferSize: sizeof(aName));
1496 str_format(buffer: paMessages[Line], buffer_size: sizeof(paMessages[Line]),
1497 format: "%d. %s Points: %d", Rank, aName, Points);
1498 Line++;
1499 }
1500 if(!End)
1501 {
1502 return false;
1503 }
1504 str_copy(dst: paMessages[Line], src: "-------------------------------", dst_size: sizeof(paMessages[Line]));
1505
1506 return true;
1507}
1508
1509bool CScoreWorker::RandomMap(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
1510{
1511 const auto *pData = dynamic_cast<const CSqlRandomMapRequest *>(pGameData);
1512 auto *pResult = dynamic_cast<CScoreRandomMapResult *>(pGameData->m_pResult.get());
1513
1514 char aBuf[512];
1515 if(in_range(a: pData->m_MinStars, lower: 0, upper: 5) && in_range(a: pData->m_MaxStars, lower: 0, upper: 5))
1516 {
1517 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1518 format: "SELECT Map FROM %s_maps "
1519 "WHERE Server = ? AND Map != ? AND Stars BETWEEN ? AND ? "
1520 "ORDER BY %s LIMIT 1",
1521 pSqlServer->GetPrefix(), pSqlServer->Random());
1522 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1523 {
1524 return false;
1525 }
1526 pSqlServer->BindInt(Idx: 3, Value: pData->m_MinStars);
1527 pSqlServer->BindInt(Idx: 4, Value: pData->m_MaxStars);
1528 }
1529 else
1530 {
1531 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1532 format: "SELECT Map FROM %s_maps "
1533 "WHERE Server = ? AND Map != ? "
1534 "ORDER BY %s LIMIT 1",
1535 pSqlServer->GetPrefix(), pSqlServer->Random());
1536 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1537 {
1538 return false;
1539 }
1540 }
1541 pSqlServer->BindString(Idx: 1, pString: pData->m_aServerType);
1542 pSqlServer->BindString(Idx: 2, pString: pData->m_aCurrentMap);
1543
1544 bool End;
1545 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
1546 {
1547 return false;
1548 }
1549 if(!End)
1550 {
1551 pSqlServer->GetString(Col: 1, pBuffer: pResult->m_aMap, BufferSize: sizeof(pResult->m_aMap));
1552 }
1553 else
1554 {
1555 str_copy(dst: pResult->m_aMessage, src: "No maps found on this server!", dst_size: sizeof(pResult->m_aMessage));
1556 }
1557 return true;
1558}
1559
1560bool CScoreWorker::RandomUnfinishedMap(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
1561{
1562 const auto *pData = dynamic_cast<const CSqlRandomMapRequest *>(pGameData);
1563 auto *pResult = dynamic_cast<CScoreRandomMapResult *>(pGameData->m_pResult.get());
1564
1565 char aBuf[512];
1566 if(in_range(a: pData->m_MinStars, lower: 0, upper: 5) && in_range(a: pData->m_MaxStars, lower: 0, upper: 5))
1567 {
1568 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1569 format: "SELECT Map "
1570 "FROM %s_maps "
1571 "WHERE Server = ? AND Map != ? AND Stars BETWEEN ? AND ? AND Map NOT IN ("
1572 " SELECT Map "
1573 " FROM %s_race "
1574 " WHERE Name = ?"
1575 ") ORDER BY %s "
1576 "LIMIT 1",
1577 pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pSqlServer->Random());
1578 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1579 {
1580 return false;
1581 }
1582 pSqlServer->BindString(Idx: 1, pString: pData->m_aServerType);
1583 pSqlServer->BindString(Idx: 2, pString: pData->m_aCurrentMap);
1584 pSqlServer->BindInt(Idx: 3, Value: pData->m_MinStars);
1585 pSqlServer->BindInt(Idx: 4, Value: pData->m_MaxStars);
1586 pSqlServer->BindString(Idx: 5, pString: pData->m_aRequestingPlayer);
1587 }
1588 else
1589 {
1590 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1591 format: "SELECT Map "
1592 "FROM %s_maps AS maps "
1593 "WHERE Server = ? AND Map != ? AND Map NOT IN ("
1594 " SELECT Map "
1595 " FROM %s_race as race "
1596 " WHERE Name = ?"
1597 ") ORDER BY %s "
1598 "LIMIT 1",
1599 pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pSqlServer->Random());
1600 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1601 {
1602 return false;
1603 }
1604 pSqlServer->BindString(Idx: 1, pString: pData->m_aServerType);
1605 pSqlServer->BindString(Idx: 2, pString: pData->m_aCurrentMap);
1606 pSqlServer->BindString(Idx: 3, pString: pData->m_aRequestingPlayer);
1607 }
1608
1609 bool End;
1610 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
1611 {
1612 return false;
1613 }
1614 if(!End)
1615 {
1616 pSqlServer->GetString(Col: 1, pBuffer: pResult->m_aMap, BufferSize: sizeof(pResult->m_aMap));
1617 }
1618 else
1619 {
1620 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s has no more unfinished maps on this server!", pData->m_aRequestingPlayer);
1621 str_copy(dst: pResult->m_aMessage, src: aBuf, dst_size: sizeof(pResult->m_aMessage));
1622 }
1623 return true;
1624}
1625
1626bool CScoreWorker::SaveTeam(IDbConnection *pSqlServer, const ISqlData *pGameData, Write w, char *pError, int ErrorSize)
1627{
1628 const auto *pData = dynamic_cast<const CSqlTeamSaveData *>(pGameData);
1629 auto *pResult = dynamic_cast<CScoreSaveResult *>(pGameData->m_pResult.get());
1630
1631 if(w == Write::NORMAL_SUCCEEDED)
1632 {
1633 // write succeeded on mysql server. delete from sqlite again
1634 char aBuf[128] = {0};
1635 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1636 format: "DELETE FROM %s_saves_backup WHERE Code = ?",
1637 pSqlServer->GetPrefix());
1638 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1639 {
1640 return false;
1641 }
1642 pSqlServer->BindString(Idx: 1, pString: pData->m_aGeneratedCode);
1643 bool End;
1644 return pSqlServer->Step(pEnd: &End, pError, ErrorSize);
1645 }
1646 if(w == Write::NORMAL_FAILED)
1647 {
1648 char aBuf[256] = {0};
1649 bool End;
1650 // move to non-tmp table succeeded. delete from backup again
1651 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1652 format: "INSERT INTO %s_saves SELECT * FROM %s_saves_backup WHERE Code = ?",
1653 pSqlServer->GetPrefix(), pSqlServer->GetPrefix());
1654 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1655 {
1656 return false;
1657 }
1658 pSqlServer->BindString(Idx: 1, pString: pData->m_aGeneratedCode);
1659 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
1660 {
1661 return false;
1662 }
1663
1664 // move to non-tmp table succeeded. delete from backup again
1665 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1666 format: "DELETE FROM %s_saves_backup WHERE Code = ?",
1667 pSqlServer->GetPrefix());
1668 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1669 {
1670 return false;
1671 }
1672 pSqlServer->BindString(Idx: 1, pString: pData->m_aGeneratedCode);
1673 return pSqlServer->Step(pEnd: &End, pError, ErrorSize);
1674 }
1675
1676 char aSaveId[UUID_MAXSTRSIZE];
1677 FormatUuid(Uuid: pResult->m_SaveId, pBuffer: aSaveId, BufferLength: UUID_MAXSTRSIZE);
1678
1679 char *pSaveState = pResult->m_SavedTeam.GetString();
1680 char aBuf[65536];
1681
1682 dbg_msg(sys: "score/dbg", fmt: "code=%s failure=%d", pData->m_aCode, (int)w);
1683 bool UseGeneratedCode = pData->m_aCode[0] == '\0' || w != Write::NORMAL;
1684
1685 str_copy(dst&: pResult->m_aGeneratedCode, src: pData->m_aGeneratedCode);
1686 str_copy(dst&: pResult->m_aCode, src: UseGeneratedCode ? "" : pData->m_aCode);
1687
1688 bool Retry = false;
1689 // two tries, first use the user provided code, then the autogenerated
1690 do
1691 {
1692 Retry = false;
1693 char aCode[128] = {0};
1694 if(UseGeneratedCode)
1695 str_copy(dst: aCode, src: pData->m_aGeneratedCode, dst_size: sizeof(aCode));
1696 else
1697 str_copy(dst: aCode, src: pData->m_aCode, dst_size: sizeof(aCode));
1698
1699 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1700 format: "%s INTO %s_saves%s(Savegame, Map, Code, Timestamp, Server, SaveId, DDNet7) "
1701 "VALUES (?, ?, ?, CURRENT_TIMESTAMP, ?, ?, %s)",
1702 pSqlServer->InsertIgnore(), pSqlServer->GetPrefix(),
1703 w == Write::NORMAL ? "" : "_backup", pSqlServer->False());
1704 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1705 {
1706 return false;
1707 }
1708 pSqlServer->BindString(Idx: 1, pString: pSaveState);
1709 pSqlServer->BindString(Idx: 2, pString: pData->m_aMap);
1710 pSqlServer->BindString(Idx: 3, pString: aCode);
1711 pSqlServer->BindString(Idx: 4, pString: pData->m_aServer);
1712 pSqlServer->BindString(Idx: 5, pString: aSaveId);
1713 pSqlServer->Print();
1714 int NumInserted;
1715 if(!pSqlServer->ExecuteUpdate(pNumUpdated: &NumInserted, pError, ErrorSize))
1716 {
1717 return false;
1718 }
1719 if(NumInserted == 1)
1720 {
1721 pResult->m_Status = CScoreSaveResult::SAVE_SUCCESS;
1722 if(w != Write::NORMAL)
1723 {
1724 if(str_comp(a: pData->m_aServer, b: g_Config.m_SvSqlServerName) == 0)
1725 {
1726 pResult->m_aServer[0] = '\0';
1727 }
1728 pResult->m_Status = CScoreSaveResult::SAVE_FALLBACKFILE;
1729 }
1730 }
1731 else if(!UseGeneratedCode)
1732 {
1733 UseGeneratedCode = true;
1734 Retry = true;
1735 }
1736 } while(Retry);
1737
1738 if(
1739 pResult->m_Status != CScoreSaveResult::SAVE_SUCCESS &&
1740 pResult->m_Status != CScoreSaveResult::SAVE_WARNING &&
1741 pResult->m_Status != CScoreSaveResult::SAVE_FALLBACKFILE)
1742 {
1743 dbg_msg(sys: "sql", fmt: "ERROR: This save-code already exists");
1744 pResult->m_Status = CScoreSaveResult::SAVE_FAILED;
1745 str_copy(dst: pResult->m_aMessage, src: "This save-code already exists", dst_size: sizeof(pResult->m_aMessage));
1746 }
1747 return true;
1748}
1749
1750bool CScoreWorker::LoadTeam(IDbConnection *pSqlServer, const ISqlData *pGameData, Write w, char *pError, int ErrorSize)
1751{
1752 if(w == Write::NORMAL_SUCCEEDED || w == Write::BACKUP_FIRST)
1753 return true;
1754 const auto *pData = dynamic_cast<const CSqlTeamLoadRequest *>(pGameData);
1755 auto *pResult = dynamic_cast<CScoreSaveResult *>(pGameData->m_pResult.get());
1756 pResult->m_Status = CScoreSaveResult::LOAD_FAILED;
1757
1758 char aCurrentTimestamp[512];
1759 pSqlServer->ToUnixTimestamp(pTimestamp: "CURRENT_TIMESTAMP", aBuf: aCurrentTimestamp, BufferSize: sizeof(aCurrentTimestamp));
1760 char aTimestamp[512];
1761 pSqlServer->ToUnixTimestamp(pTimestamp: "Timestamp", aBuf: aTimestamp, BufferSize: sizeof(aTimestamp));
1762
1763 char aBuf[512];
1764 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1765 format: "SELECT Savegame, %s-%s AS Ago, SaveId "
1766 "FROM %s_saves "
1767 "where Code = ? AND Map = ? AND DDNet7 = %s",
1768 aCurrentTimestamp, aTimestamp,
1769 pSqlServer->GetPrefix(), pSqlServer->False());
1770 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1771 {
1772 return false;
1773 }
1774 pSqlServer->BindString(Idx: 1, pString: pData->m_aCode);
1775 pSqlServer->BindString(Idx: 2, pString: pData->m_aMap);
1776
1777 bool End;
1778 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
1779 {
1780 return false;
1781 }
1782 if(End)
1783 {
1784 str_copy(dst: pResult->m_aMessage, src: "No such savegame for this map", dst_size: sizeof(pResult->m_aMessage));
1785 return true;
1786 }
1787
1788 pResult->m_SaveId = UUID_NO_SAVE_ID;
1789 if(!pSqlServer->IsNull(Col: 3))
1790 {
1791 char aSaveId[UUID_MAXSTRSIZE];
1792 pSqlServer->GetString(Col: 3, pBuffer: aSaveId, BufferSize: sizeof(aSaveId));
1793 if(ParseUuid(pUuid: &pResult->m_SaveId, pBuffer: aSaveId) || pResult->m_SaveId == UUID_NO_SAVE_ID)
1794 {
1795 str_copy(dst: pResult->m_aMessage, src: "Unable to load savegame: SaveId corrupted", dst_size: sizeof(pResult->m_aMessage));
1796 return true;
1797 }
1798 }
1799
1800 char aSaveString[65536];
1801 pSqlServer->GetString(Col: 1, pBuffer: aSaveString, BufferSize: sizeof(aSaveString));
1802 int Num = pResult->m_SavedTeam.FromString(pString: aSaveString);
1803
1804 if(Num != 0)
1805 {
1806 str_copy(dst: pResult->m_aMessage, src: "Unable to load savegame: data corrupted", dst_size: sizeof(pResult->m_aMessage));
1807 return true;
1808 }
1809
1810 bool Found = false;
1811 for(int i = 0; i < pResult->m_SavedTeam.GetMembersCount(); i++)
1812 {
1813 if(str_comp(a: pResult->m_SavedTeam.m_pSavedTees[i].GetName(), b: pData->m_aRequestingPlayer) == 0)
1814 {
1815 Found = true;
1816 break;
1817 }
1818 }
1819 if(!Found)
1820 {
1821 str_copy(dst: pResult->m_aMessage, src: "This save exists, but you are not part of it. "
1822 "Make sure you use the same name as you had when saving. "
1823 "If you saved with an already used code, you get a new random save code, "
1824 "check ddnet-saves.txt in config_directory.",
1825 dst_size: sizeof(pResult->m_aMessage));
1826 return true;
1827 }
1828
1829 int Since = pSqlServer->GetInt(Col: 2);
1830 if(Since < g_Config.m_SvSaveSwapGamesDelay)
1831 {
1832 str_format(buffer: pResult->m_aMessage, buffer_size: sizeof(pResult->m_aMessage),
1833 format: "You have to wait %d seconds until you can load this savegame",
1834 g_Config.m_SvSaveSwapGamesDelay - Since);
1835 return true;
1836 }
1837
1838 bool CanLoad = pResult->m_SavedTeam.MatchPlayers(
1839 paNames: pData->m_aClientNames, pClientId: pData->m_aClientId, NumPlayer: pData->m_NumPlayer,
1840 pMessage: pResult->m_aMessage, MessageLen: sizeof(pResult->m_aMessage));
1841
1842 if(!CanLoad)
1843 return true;
1844
1845 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1846 format: "DELETE FROM %s_saves "
1847 "WHERE Code = ? AND Map = ? AND SaveId %s",
1848 pSqlServer->GetPrefix(),
1849 pResult->m_SaveId != UUID_NO_SAVE_ID ? "= ?" : "IS NULL");
1850 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1851 {
1852 return false;
1853 }
1854 pSqlServer->BindString(Idx: 1, pString: pData->m_aCode);
1855 pSqlServer->BindString(Idx: 2, pString: pData->m_aMap);
1856 char aUuid[UUID_MAXSTRSIZE];
1857 if(pResult->m_SaveId != UUID_NO_SAVE_ID)
1858 {
1859 FormatUuid(Uuid: pResult->m_SaveId, pBuffer: aUuid, BufferLength: sizeof(aUuid));
1860 pSqlServer->BindString(Idx: 3, pString: aUuid);
1861 }
1862 pSqlServer->Print();
1863 int NumDeleted;
1864 if(!pSqlServer->ExecuteUpdate(pNumUpdated: &NumDeleted, pError, ErrorSize))
1865 {
1866 return false;
1867 }
1868
1869 if(NumDeleted != 1)
1870 {
1871 str_copy(dst: pResult->m_aMessage, src: "Unable to load savegame: loaded on a different server", dst_size: sizeof(pResult->m_aMessage));
1872 return true;
1873 }
1874
1875 pResult->m_Status = CScoreSaveResult::LOAD_SUCCESS;
1876 str_copy(dst: pResult->m_aMessage, src: "Loading successfully done", dst_size: sizeof(pResult->m_aMessage));
1877 return true;
1878}
1879
1880bool CScoreWorker::GetSaves(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize)
1881{
1882 const auto *pData = dynamic_cast<const CSqlPlayerRequest *>(pGameData);
1883 auto *pResult = dynamic_cast<CScorePlayerResult *>(pGameData->m_pResult.get());
1884 auto *paMessages = pResult->m_Data.m_aaMessages;
1885
1886 char aSaveLike[128] = "";
1887 str_append(dst&: aSaveLike, src: "%\n");
1888 sqlstr::EscapeLike(pDst: aSaveLike + str_length(str: aSaveLike),
1889 pSrc: pData->m_aRequestingPlayer,
1890 DstSize: sizeof(aSaveLike) - str_length(str: aSaveLike));
1891 str_append(dst&: aSaveLike, src: "\t%");
1892
1893 char aCurrentTimestamp[512];
1894 pSqlServer->ToUnixTimestamp(pTimestamp: "CURRENT_TIMESTAMP", aBuf: aCurrentTimestamp, BufferSize: sizeof(aCurrentTimestamp));
1895 char aMaxTimestamp[512];
1896 pSqlServer->ToUnixTimestamp(pTimestamp: "MAX(Timestamp)", aBuf: aMaxTimestamp, BufferSize: sizeof(aMaxTimestamp));
1897
1898 char aBuf[512];
1899 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1900 format: "SELECT COUNT(*) AS NumSaves, %s-%s AS Ago "
1901 "FROM %s_saves "
1902 "WHERE Map = ? AND Savegame LIKE ?",
1903 aCurrentTimestamp, aMaxTimestamp,
1904 pSqlServer->GetPrefix());
1905 if(!pSqlServer->PrepareStatement(pStmt: aBuf, pError, ErrorSize))
1906 {
1907 return false;
1908 }
1909 pSqlServer->BindString(Idx: 1, pString: pData->m_aMap);
1910 pSqlServer->BindString(Idx: 2, pString: aSaveLike);
1911
1912 bool End;
1913 if(!pSqlServer->Step(pEnd: &End, pError, ErrorSize))
1914 {
1915 return false;
1916 }
1917 if(!End)
1918 {
1919 int NumSaves = pSqlServer->GetInt(Col: 1);
1920 char aLastSavedString[60] = "\0";
1921 if(!pSqlServer->IsNull(Col: 2))
1922 {
1923 int Ago = pSqlServer->GetInt(Col: 2);
1924 char aAgoString[40] = "\0";
1925 sqlstr::AgoTimeToString(AgoTime: Ago, pAgoString: aAgoString, Size: sizeof(aAgoString));
1926 str_format(buffer: aLastSavedString, buffer_size: sizeof(aLastSavedString), format: ", last saved %s ago", aAgoString);
1927 }
1928
1929 str_format(buffer: paMessages[0], buffer_size: sizeof(paMessages[0]),
1930 format: "%s has %d save%s on %s%s",
1931 pData->m_aRequestingPlayer,
1932 NumSaves, NumSaves == 1 ? "" : "s",
1933 pData->m_aMap, aLastSavedString);
1934 }
1935 return true;
1936}
1937