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