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