· 8 years ago · Jan 16, 2018, 10:24 PM
1/*
2 * shavit's Timer - Rankings
3 * by: shavit
4 *
5 * This file is part of shavit's Timer.
6 *
7 * This program is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, version 3.0, as published by the
9 * Free Software Foundation.
10 *
11 * This program is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14 * details.
15 *
16 * You should have received a copy of the GNU General Public License along with
17 * this program. If not, see <http://www.gnu.org/licenses/>.
18 *
19*/
20
21// Design idea:
22// Rank 1 per map/style/track gets ((points per tier * tier) * 1.5) + ((amount of records * (tier / 10.0) * 0.25)) + (rank 1 time in seconds / 15.0) points.
23// Records below rank 1 get points% relative to their time in comparison to rank 1 and a final multiplier of 0.85% to promote rank 1 hunting.
24//
25// Bonus track gets a 0.25* final mutliplier for points and is treated as tier 1.
26//
27// Points for all styles are combined to promote competitive and fair gameplay.
28// A player that gets good times at all styles should be ranked high.
29//
30// Total player points are weighted in the following way: (descending sort of points)
31// points[0] * 0.975^0 + points[1] * 0.975^1 + points[2] * 0.975^2 + ... + points[n] * 0.975^(n-1)
32//
33// The ranking leaderboard will be calculated upon: map start.
34// Points are calculated per-player upon: connection/map.
35// Points are calculated per-map upon: map start, map end, tier changes.
36// Rankings leaderboard is re-calculated once per map change.
37// A command will be supplied to recalculate all of the above.
38//
39// Heavily inspired by pp (performance points) from osu!, written by Tom94. https://github.com/ppy/osu-performance
40
41#include <sourcemod>
42
43#undef REQUIRE_PLUGIN
44#include <shavit>
45
46#pragma newdecls required
47#pragma semicolon 1
48
49// uncomment when done
50// #define DEBUG
51
52char gS_MySQLPrefix[32];
53Database gH_SQL = null;
54
55bool gB_Stats = false;
56bool gB_Late = false;
57
58int gI_Tier = 1; // No floating numbers for tiers, sorry.
59
60char gS_Map[160];
61
62int gI_ValidMaps = 0;
63ArrayList gA_ValidMaps = null;
64StringMap gA_MapTiers = null;
65
66ConVar gCV_PointsPerTier = null;
67float gF_PointsPerTier = 50.0;
68
69int gI_Rank[MAXPLAYERS+1];
70float gF_Points[MAXPLAYERS+1];
71int gI_Progress[MAXPLAYERS+1];
72
73int gI_RankedPlayers = 0;
74Menu gH_Top100Menu = null;
75
76Handle gH_Forwards_OnTierAssigned = null;
77
78// Timer settings.
79char gS_ChatStrings[CHATSETTINGS_SIZE][128];
80char gS_StyleNames[STYLE_LIMIT][64];
81char gS_TrackNames[TRACKS_SIZE][32];
82
83any gA_StyleSettings[STYLE_LIMIT][STYLESETTINGS_SIZE];
84int gI_Styles = 0;
85int gI_RankedStyles = 0;
86
87public Plugin myinfo =
88{
89 name = "[shavit] Rankings",
90 author = "shavit",
91 description = "A fair and competitive ranking system for shavit's bhoptimer.",
92 version = SHAVIT_VERSION,
93 url = "https://github.com/shavitush/bhoptimer"
94}
95
96public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
97{
98 CreateNative("Shavit_GetMapTier", Native_GetMapTier);
99 CreateNative("Shavit_GetMapTiers", Native_GetMapTiers);
100 CreateNative("Shavit_GetPoints", Native_GetPoints);
101 CreateNative("Shavit_GetRank", Native_GetRank);
102 CreateNative("Shavit_GetRankedPlayers", Native_GetRankedPlayers);
103
104 RegPluginLibrary("shavit-rankings");
105
106 gB_Late = late;
107
108 return APLRes_Success;
109}
110
111public void OnAllPluginsLoaded()
112{
113 if(!LibraryExists("shavit-wr"))
114 {
115 SetFailState("shavit-wr is required for the plugin to work.");
116 }
117
118 if(gH_SQL == null)
119 {
120 Shavit_OnDatabaseLoaded();
121 }
122
123 for(int i = 0; i < TRACKS_SIZE; i++)
124 {
125 GetTrackName(LANG_SERVER, i, gS_TrackNames[i], 32);
126 }
127}
128
129public void OnPluginStart()
130{
131 gH_Forwards_OnTierAssigned = CreateGlobalForward("Shavit_OnTierAssigned", ET_Event, Param_String, Param_Cell);
132
133 RegConsoleCmd("sm_tier", Command_Tier, "Prints the map's tier to chat.");
134 RegConsoleCmd("sm_maptier", Command_Tier, "Prints the map's tier to chat. (sm_tier alias)");
135
136 RegConsoleCmd("sm_rank", Command_Rank, "Show your or someone else's rank. Usage: sm_rank [name]");
137 RegConsoleCmd("sm_top", Command_Top, "Show the top 100 players."); // The rewrite of rankings will not have the ability to show over 100 entries. Dynamic fetching can be exploited and overload the database.
138
139 RegAdminCmd("sm_settier", Command_SetTier, ADMFLAG_RCON, "Change the map's tier. Usage: sm_settier <tier>");
140 RegAdminCmd("sm_setmaptier", Command_SetTier, ADMFLAG_RCON, "Prints the map's tier to chat. Usage: sm_setmaptier <tier> (sm_settier alias)");
141
142 RegAdminCmd("sm_recalcmap", Command_RecalcMap, ADMFLAG_RCON, "Recalculate the current map's records' points.");
143
144 RegAdminCmd("sm_recalcall", Command_RecalcAll, ADMFLAG_ROOT, "Recalculate the points for every map on the server. Run this after you change the ranking multiplier for a style or after you install the plugin.");
145
146 gCV_PointsPerTier = CreateConVar("shavit_rankings_pointspertier", "50.0", "Base points to use for per-tier scaling.\nRead the design idea to see how it works: https://github.com/shavitush/bhoptimer/issues/465", 0, true, 1.0);
147 gCV_PointsPerTier.AddChangeHook(OnConVarChanged);
148
149 AutoExecConfig();
150
151 LoadTranslations("common.phrases");
152 LoadTranslations("shavit-common.phrases");
153 LoadTranslations("shavit-rankings.phrases");
154
155 // tier cache
156 gA_ValidMaps = new ArrayList(128);
157 gA_MapTiers = new StringMap();
158
159 SQL_SetPrefix();
160
161 if(gB_Late)
162 {
163 Shavit_OnChatConfigLoaded();
164 }
165}
166
167public void Shavit_OnChatConfigLoaded()
168{
169 for(int i = 0; i < CHATSETTINGS_SIZE; i++)
170 {
171 Shavit_GetChatStrings(i, gS_ChatStrings[i], 128);
172 }
173}
174
175public void Shavit_OnStyleConfigLoaded(int styles)
176{
177 if(styles == -1)
178 {
179 gI_Styles = Shavit_GetStyleCount();
180 }
181
182 gI_RankedStyles = 0;
183
184 for(int i = 0; i < gI_Styles; i++)
185 {
186 Shavit_GetStyleSettings(i, gA_StyleSettings[i]);
187 Shavit_GetStyleStrings(i, sStyleName, gS_StyleNames[i], 64);
188
189 if(!gA_StyleSettings[i][bUnranked])
190 {
191 gI_RankedStyles++;
192 }
193 }
194}
195
196public void OnLibraryAdded(const char[] name)
197{
198 if(StrEqual(name, "shavit-stats"))
199 {
200 gB_Stats = true;
201 }
202}
203
204public void OnLibraryRemoved(const char[] name)
205{
206 if(StrEqual(name, "shavit-stats"))
207 {
208 gB_Stats = false;
209 }
210}
211
212public void Shavit_OnDatabaseLoaded()
213{
214 gH_SQL = Shavit_GetDatabase();
215 SetSQLInfo();
216}
217
218public Action CheckForSQLInfo(Handle Timer)
219{
220 return SetSQLInfo();
221}
222
223Action SetSQLInfo()
224{
225 if(gH_SQL == null)
226 {
227 gH_SQL = Shavit_GetDatabase();
228
229 CreateTimer(0.5, CheckForSQLInfo);
230 }
231
232 else
233 {
234 SQL_DBConnect();
235
236 return Plugin_Stop;
237 }
238
239 return Plugin_Continue;
240}
241
242void SQL_SetPrefix()
243{
244 char[] sFile = new char[PLATFORM_MAX_PATH];
245 BuildPath(Path_SM, sFile, PLATFORM_MAX_PATH, "configs/shavit-prefix.txt");
246
247 File fFile = OpenFile(sFile, "r");
248
249 if(fFile == null)
250 {
251 SetFailState("Cannot open \"configs/shavit-prefix.txt\". Make sure this file exists and that the server has read permissions to it.");
252 }
253
254 char[] sLine = new char[PLATFORM_MAX_PATH*2];
255
256 while(fFile.ReadLine(sLine, PLATFORM_MAX_PATH*2))
257 {
258 TrimString(sLine);
259 strcopy(gS_MySQLPrefix, 32, sLine);
260
261 break;
262 }
263
264 delete fFile;
265}
266
267void SQL_DBConnect()
268{
269 if(gH_SQL != null)
270 {
271 char[] sDriver = new char[8];
272 gH_SQL.Driver.GetIdentifier(sDriver, 8);
273
274 if(!StrEqual(sDriver, "mysql", false))
275 {
276 SetFailState("MySQL is the only supported database engine for shavit-rankings.");
277 }
278
279 char[] sQuery = new char[256];
280 FormatEx(sQuery, 256, "CREATE TABLE IF NOT EXISTS `%smaptiers` (`map` CHAR(128), `tier` INT NOT NULL DEFAULT 1, PRIMARY KEY (`map`)) ENGINE=INNODB;", gS_MySQLPrefix);
281
282 gH_SQL.Query(SQL_CreateTable_Callback, sQuery, 0);
283 }
284}
285
286public void SQL_CreateTable_Callback(Database db, DBResultSet results, const char[] error, any data)
287{
288 if(results == null)
289 {
290 LogError("Timer (rankings) error! Map tiers table creation failed. Reason: %s", error);
291
292 return;
293 }
294
295 #if defined DEBUG
296 PrintToServer("DEBUG: 0 (SQL_CreateTable_Callback)");
297 #endif
298
299 if(gI_Styles == 0)
300 {
301 Shavit_OnStyleConfigLoaded(-1);
302 }
303
304 SQL_LockDatabase(gH_SQL);
305 SQL_FastQuery(gH_SQL, "DELIMITER ;;");
306 SQL_FastQuery(gH_SQL, "DROP PROCEDURE IF EXISTS UpdateAllPoints;;"); // old (and very slow) deprecated method
307 SQL_FastQuery(gH_SQL, "DROP FUNCTION IF EXISTS GetWeightedPoints;;"); // this is here, just in case we ever choose to modify or optimize the calculation
308
309 char[] sQuery = new char[1024];
310 FormatEx(sQuery, 1024,
311 "CREATE FUNCTION GetWeightedPoints(authid CHAR(32)) " ...
312 "RETURNS FLOAT " ...
313 "BEGIN " ...
314 "DECLARE p FLOAT; " ...
315 "DECLARE total FLOAT DEFAULT 0.0; " ...
316 "DECLARE mult FLOAT DEFAULT 1.0; " ...
317 "DECLARE done INT DEFAULT 0; " ...
318 "DECLARE cur CURSOR FOR SELECT points FROM %splayertimes WHERE auth = authid AND points > 0.0 ORDER BY points DESC; " ...
319 "DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; " ...
320 "OPEN cur; " ...
321 "iter: LOOP " ...
322 "FETCH cur INTO p; " ...
323 "IF done THEN " ...
324 "LEAVE iter; " ...
325 "END IF; " ...
326 "SET total = total + (p * mult); " ...
327 "SET mult = mult * 0.975; " ...
328 "END LOOP; " ...
329 "CLOSE cur; " ...
330 "RETURN total; " ...
331 "END;;", gS_MySQLPrefix);
332
333 #if defined DEBUG
334 LogError("%s", sQuery);
335 #endif
336
337 bool bSuccess = true;
338
339 if(!SQL_FastQuery(gH_SQL, sQuery))
340 {
341 char[] sError = new char[255];
342 SQL_GetError(gH_SQL, sError, 255);
343 LogError("Timer (rankings, create GetWeightedPoints function) error! Reason: %s", sError);
344
345 bSuccess = false;
346 }
347
348 SQL_FastQuery(gH_SQL, "DELIMITER ;");
349 SQL_UnlockDatabase(gH_SQL);
350
351 if(!bSuccess)
352 {
353 return;
354 }
355
356 OnMapStart();
357
358 if(gB_Late)
359 {
360 for(int i = 1; i <= MaxClients; i++)
361 {
362 OnClientConnected(i);
363 }
364 }
365}
366
367public void OnConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
368{
369 gF_PointsPerTier = gCV_PointsPerTier.FloatValue;
370}
371
372public void OnClientConnected(int client)
373{
374 gI_Rank[client] = 0;
375 gF_Points[client] = 0.0;
376}
377
378public void OnClientPostAdminCheck(int client)
379{
380 if(!IsFakeClient(client))
381 {
382 UpdatePlayerRank(client);
383 }
384}
385
386public void OnMapStart()
387{
388 if(gH_SQL == null)
389 {
390 return;
391 }
392
393 #if defined DEBUG
394 PrintToServer("DEBUG: 1 (OnMapStart)");
395 #endif
396
397 UpdateRankedPlayers();
398
399 GetCurrentMap(gS_Map, 160);
400 GetMapDisplayName(gS_Map, gS_Map, 160);
401
402 // Default tier.
403 // I won't repeat the same mistake blacky has done with tier 3 being default..
404 gI_Tier = 1;
405
406 char[] sDriver = new char[8];
407 gH_SQL.Driver.GetIdentifier(sDriver, 8);
408
409 if(!StrEqual(sDriver, "mysql", false))
410 {
411 SetFailState("Rankings will only support MySQL for the moment. Sorry.");
412 }
413
414 char[] sQuery = new char[256];
415 FormatEx(sQuery, 256, "SELECT tier FROM %smaptiers WHERE map = '%s';", gS_MySQLPrefix, gS_Map);
416 gH_SQL.Query(SQL_GetMapTier_Callback, sQuery, 0, DBPrio_Low);
417}
418
419public void SQL_GetMapTier_Callback(Database db, DBResultSet results, const char[] error, any data)
420{
421 if(results == null)
422 {
423 LogError("Timer (rankings, get map tier) error! Reason: %s", error);
424
425 return;
426 }
427
428 #if defined DEBUG
429 PrintToServer("DEBUG: 2 (SQL_GetMapTier_Callback)");
430 #endif
431
432 if(results.RowCount > 0 && results.FetchRow())
433 {
434 gI_Tier = results.FetchInt(0);
435
436 #if defined DEBUG
437 PrintToServer("DEBUG: 3 (tier: %d) (SQL_GetMapTier_Callback)", gI_Tier);
438 #endif
439
440 RecalculateAll(gS_Map, gI_Tier);
441 UpdateAllPoints();
442
443 #if defined DEBUG
444 PrintToServer("DEBUG: 4 (SQL_GetMapTier_Callback)");
445 #endif
446
447 char[] sQuery = new char[256];
448 FormatEx(sQuery, 256, "SELECT map, tier FROM %smaptiers;", gS_MySQLPrefix, gS_Map);
449 gH_SQL.Query(SQL_FillTierCache_Callback, sQuery, 0, DBPrio_High);
450 }
451
452 else
453 {
454 char[] sQuery = new char[256];
455 FormatEx(sQuery, 256, "REPLACE INTO %smaptiers (map, tier) VALUES ('%s', %d);", gS_MySQLPrefix, gS_Map, gI_Tier);
456 gH_SQL.Query(SQL_SetMapTier_Callback, sQuery, gI_Tier, DBPrio_High);
457 }
458}
459
460public void SQL_FillTierCache_Callback(Database db, DBResultSet results, const char[] error, any data)
461{
462 if(results == null)
463 {
464 LogError("Timer (rankings, fill tier cache) error! Reason: %s", error);
465
466 return;
467 }
468
469 gA_ValidMaps.Clear();
470 gA_MapTiers.Clear();
471
472 while(results.FetchRow())
473 {
474 char[] sMap = new char[160];
475 results.FetchString(0, sMap, 160);
476
477 int tier = results.FetchInt(1);
478
479 gA_MapTiers.SetValue(sMap, tier);
480 gA_ValidMaps.PushString(sMap);
481
482 Call_StartForward(gH_Forwards_OnTierAssigned);
483 Call_PushString(sMap);
484 Call_PushCell(tier);
485 Call_Finish();
486 }
487
488 gI_ValidMaps = gA_ValidMaps.Length;
489 SortADTArray(gA_ValidMaps, Sort_Ascending, Sort_String);
490}
491
492void GuessBestMapName(const char[] input, char[] output, int size)
493{
494 if(gA_ValidMaps.FindString(input) != -1)
495 {
496 strcopy(output, size, input);
497
498 return;
499 }
500
501 char[] sCache = new char[128];
502
503 for(int i = 0; i < gI_ValidMaps; i++)
504 {
505 gA_ValidMaps.GetString(i, sCache, 128);
506
507 if(StrContains(sCache, input) != -1)
508 {
509 strcopy(output, size, sCache);
510
511 return;
512 }
513 }
514}
515
516public void OnMapEnd()
517{
518 RecalculateAll(gS_Map, gI_Tier);
519}
520
521public Action Command_Tier(int client, int args)
522{
523 int tier = gI_Tier;
524
525 char[] sMap = new char[128];
526 strcopy(sMap, 128, gS_Map);
527
528 if(args > 0)
529 {
530 GetCmdArgString(sMap, 128);
531 GuessBestMapName(sMap, sMap, 128);
532
533 if(!gA_MapTiers.GetValue(sMap, tier))
534 {
535 strcopy(sMap, 128, gS_Map);
536 }
537 }
538
539 Shavit_PrintToChat(client, "%T", "CurrentTier", client, gS_ChatStrings[sMessageVariable], sMap, gS_ChatStrings[sMessageText], gS_ChatStrings[sMessageVariable2], tier, gS_ChatStrings[sMessageText]);
540
541 return Plugin_Handled;
542}
543
544public Action Command_Rank(int client, int args)
545{
546 int target = client;
547
548 if(args > 0)
549 {
550 char[] sArgs = new char[MAX_TARGET_LENGTH];
551 GetCmdArgString(sArgs, MAX_TARGET_LENGTH);
552
553 target = FindTarget(client, sArgs, true, false);
554
555 if(target == -1)
556 {
557 return Plugin_Handled;
558 }
559 }
560
561 if(gF_Points[target] == 0.0)
562 {
563 Shavit_PrintToChat(client, "%T", "Unranked", client, gS_ChatStrings[sMessageVariable2], target, gS_ChatStrings[sMessageText]);
564
565 return Plugin_Handled;
566 }
567
568 Shavit_PrintToChat(client, "%T", "Rank", client, gS_ChatStrings[sMessageVariable2], target, gS_ChatStrings[sMessageText],
569 gS_ChatStrings[sMessageVariable], (gI_Rank[target] > gI_RankedPlayers)? gI_RankedPlayers:gI_Rank[target], gS_ChatStrings[sMessageText],
570 gI_RankedPlayers,
571 gS_ChatStrings[sMessageVariable], gF_Points[target], gS_ChatStrings[sMessageText]);
572
573 return Plugin_Handled;
574}
575
576public Action Command_Top(int client, int args)
577{
578 gH_Top100Menu.SetTitle("%T (%d)\n ", "Top100", client, gI_RankedPlayers);
579 gH_Top100Menu.Display(client, 60);
580
581 return Plugin_Handled;
582}
583
584public int MenuHandler_Top(Menu menu, MenuAction action, int param1, int param2)
585{
586 if(action == MenuAction_Select)
587 {
588 char[] sInfo = new char[32];
589 menu.GetItem(param2, sInfo, 32);
590
591 if(gB_Stats && !StrEqual(sInfo, "-1"))
592 {
593 Shavit_OpenStatsMenu(param1, sInfo);
594 }
595 }
596
597 return 0;
598}
599
600public Action Command_SetTier(int client, int args)
601{
602 char[] sArg = new char[8];
603 GetCmdArg(1, sArg, 8);
604
605 int tier = StringToInt(sArg);
606
607 if(args == 0 || tier < 1 || tier > 10)
608 {
609 ReplyToCommand(client, "%T", "ArgumentsMissing", client, "sm_settier <tier> (1-10)");
610
611 return Plugin_Handled;
612 }
613
614 gI_Tier = tier;
615 gA_MapTiers.SetValue(gS_Map, tier);
616
617 Call_StartForward(gH_Forwards_OnTierAssigned);
618 Call_PushString(gS_Map);
619 Call_PushCell(tier);
620 Call_Finish();
621
622 Shavit_PrintToChat(client, "%T", "SetTier", client, gS_ChatStrings[sMessageVariable2], tier, gS_ChatStrings[sMessageText]);
623
624 char[] sQuery = new char[256];
625 FormatEx(sQuery, 256, "REPLACE INTO %smaptiers (map, tier) VALUES ('%s', %d);", gS_MySQLPrefix, gS_Map, tier);
626
627 gH_SQL.Query(SQL_SetMapTier_Callback, sQuery, tier, DBPrio_Low);
628
629 return Plugin_Handled;
630}
631
632public void SQL_SetMapTier_Callback(Database db, DBResultSet results, const char[] error, any data)
633{
634 if(results == null)
635 {
636 LogError("Timer (rankings, set map tier) error! Reason: %s", error);
637
638 return;
639 }
640
641 RecalculateAll(gS_Map, data);
642}
643
644public Action Command_RecalcMap(int client, int args)
645{
646 RecalculateAll(gS_Map, gI_Tier);
647 UpdateAllPoints();
648
649 ReplyToCommand(client, "Done.");
650
651 return Plugin_Handled;
652}
653
654public Action Command_RecalcAll(int client, int args)
655{
656 ReplyToCommand(client, "Check your console for information.\nDatabase related queries might not work until this is done.");
657 ReplyToCommand(client, "- [0.0%%] Started recalculating points for all maps.");
658
659 gI_Progress[client] = 0;
660
661 int serial = (client == 0)? 0:GetClientSerial(client);
662 int size = gA_ValidMaps.Length;
663
664 for(int i = 0; i < size; i++)
665 {
666 char[] sMap = new char[160];
667 gA_ValidMaps.GetString(i, sMap, 160);
668
669 int tier = 1;
670 gA_MapTiers.GetValue(sMap, tier);
671
672 DataPack dataPack;
673 dataPack.WriteCell(serial);
674 dataPack.WriteString(sMap);
675 dataPack.WriteCell(tier);
676
677 CreateDataTimer(i*0.5, RecalcTimer, dataPack);
678 //RecalculateAll(sMap, tier, serial, true);
679
680 #if defined DEBUG
681 PrintToConsole(client, "size: %d | %d | %s", size, i, sMap);
682 #endif
683 }
684
685 return Plugin_Handled;
686}
687
688public Action RecalcTimer(Handle timer, Handle pack)
689{
690 /* Set to the beginning and unpack it */
691 ResetPack(pack);
692
693 int serial = ReadPackCell(pack);
694
695 char sMap[160];
696 ReadPackString(pack, sMap, sizeof(sMap));
697
698 int tier = ReadPackCell(pack);
699
700 RecalculateAll(sMap, tier, serial, true);
701}
702
703void RecalculateAll(const char[] map, const int tier, int serial = 0, bool print = false)
704{
705 #if defined DEBUG
706 LogError("DEBUG: 5 (RecalculateAll)");
707 #endif
708
709 for(int i = 0; i < TRACKS_SIZE; i++)
710 {
711 for(int j = 0; j < gI_Styles; j++)
712 {
713 if(gA_StyleSettings[j][bUnranked])
714 {
715 continue;
716 }
717
718 RecalculateMap(map, i, j, tier, serial, print);
719 }
720 }
721}
722
723public void Shavit_OnFinish_Post(int client, int style, float time, int jumps, int strafes, float sync, int rank, int overwrite, int track)
724{
725 RecalculateMap(gS_Map, track, style, gI_Tier);
726}
727
728void RecalculateMap(const char[] map, const int track, const int style, const int tier, int serial = -1, bool print = false)
729{
730 #if defined DEBUG
731 PrintToServer("Recalculating points. (%s, %d, %d, %d)", map, track, style, tier);
732 #endif
733
734 char[] sQuery = new char[2048];
735 FormatEx(sQuery, 2048, "UPDATE %splayertimes t LEFT JOIN " ...
736 "(SELECT MIN(time) mintime, MAP, track, style FROM %splayertimes GROUP BY MAP, track, style) minjoin " ...
737 "ON t.time = minjoin.mintime AND t.MAP = minjoin.MAP AND t.track = minjoin.track AND t.style = minjoin.style " ...
738 "JOIN (SELECT ((%.01f * %d) * 1.5) points) best " ...
739 "JOIN (SELECT (COUNT(*) * (%d / 10.0)) points, MAP, track, style FROM %splayertimes GROUP BY MAP, track, style) additive " ...
740 "ON t.MAP = additive.MAP AND t.track = additive.track AND t.style = additive.style " ...
741 "JOIN (SELECT MIN(time) lowest, (MIN(time) / 15.0) points, MAP, track, style FROM %splayertimes GROUP BY MAP, track, style) FINAL " ...
742 "ON t.MAP = FINAL.MAP AND t.track = FINAL.track AND t.style = FINAL.style JOIN (SELECT (%.03f) style, (%.03f) track) multipliers " ...
743
744 "SET t.points = (CASE " ...
745 "WHEN minjoin.mintime IS NOT NULL THEN (((best.points + additive.points + FINAL.points) * multipliers.style) * multipliers.track) " ...
746 "ELSE (((((best.points + additive.points + FINAL.points) * multipliers.style) * multipliers.track) * (FINAL.lowest / t.time)) * 0.85) " ...
747 "END) " ...
748
749 "WHERE t.MAP = '%s' " ...
750 "AND t.track = %d " ...
751 "AND t.style = %d;",
752 gS_MySQLPrefix, gS_MySQLPrefix,
753 gF_PointsPerTier, (track == Track_Main)? tier:1, (track == Track_Main)? tier:1,
754 gS_MySQLPrefix, gS_MySQLPrefix,
755 gA_StyleSettings[style][fRankingMultiplier], (track == Track_Main)? 1.0:0.25,
756 map, track, style);
757
758 DataPack pack = new DataPack();
759 pack.WriteCell(serial);
760 pack.WriteCell(strlen(map));
761 pack.WriteString(map);
762 pack.WriteCell(track);
763 pack.WriteCell(style);
764 pack.WriteCell(print);
765
766 gH_SQL.Query(SQL_Recalculate_Callback, sQuery, pack, DBPrio_High);
767
768 #if defined DEBUG
769 PrintToServer("Sent query.");
770 #endif
771}
772
773public void SQL_Recalculate_Callback(Database db, DBResultSet results, const char[] error, DataPack data)
774{
775 data.Reset();
776 int serial = data.ReadCell();
777 int size = data.ReadCell();
778
779 char[] sMap = new char[size + 1];
780 ReadPackString(data, sMap, size + 1);
781
782 int track = data.ReadCell();
783 int style = data.ReadCell();
784 bool print = view_as<bool>(data.ReadCell());
785 delete data;
786
787 if(results == null)
788 {
789 LogError("Timer (rankings, recalculate map points) error! Reason: %s", error);
790
791 return;
792 }
793
794 if(print && serial != -1)
795 {
796 int client = (serial == 0)? 0:GetClientFromSerial(serial);
797
798 if(serial != 0 && client == 0)
799 {
800 return;
801 }
802
803 int max = ((gA_ValidMaps.Length * TRACKS_SIZE) * gI_RankedStyles);
804 float current = ((float(++gI_Progress[client]) / max) * 100.0);
805
806 PrintToConsole(client, "- [%.01f%%] Recalculated \"%s\" (%s | %s).", current, sMap, gS_TrackNames[track], gS_StyleNames[style]);
807 }
808
809 #if defined DEBUG
810 PrintToServer("Recalculated.");
811 #endif
812}
813
814void UpdateAllPoints()
815{
816 #if defined DEBUG
817 LogError("DEBUG: 6 (UpdateAllPoints)");
818 #endif
819
820 char[] sQuery = new char[128];
821 FormatEx(sQuery, 128, "UPDATE %susers SET points = GetWeightedPoints(auth);", gS_MySQLPrefix);
822 gH_SQL.Query(SQL_UpdateAllPoints_Callback, sQuery);
823}
824
825public void SQL_UpdateAllPoints_Callback(Database db, DBResultSet results, const char[] error, any data)
826{
827 if(results == null)
828 {
829 LogError("Timer (rankings, update all points) error! Reason: %s", error);
830
831 return;
832 }
833}
834
835void UpdatePlayerRank(int client)
836{
837 gI_Rank[client] = 0;
838 gF_Points[client] = 0.0;
839
840 char[] sAuthID = new char[32];
841
842 if(GetClientAuthId(client, AuthId_Steam3, sAuthID, 32))
843 {
844 // if there's any issue with this query,
845 // add "ORDER BY points DESC " before "LIMIT 1"
846 char[] sQuery = new char[512];
847 FormatEx(sQuery, 512, "SELECT COUNT(*) rank, p.points FROM %susers u JOIN (SELECT points FROM %susers WHERE auth = '%s' LIMIT 1) p WHERE u.points >= p.points LIMIT 1;",
848 gS_MySQLPrefix, gS_MySQLPrefix, sAuthID);
849
850 gH_SQL.Query(SQL_UpdatePlayerRank_Callback, sQuery, GetClientSerial(client), DBPrio_Low);
851 }
852}
853
854public void SQL_UpdatePlayerRank_Callback(Database db, DBResultSet results, const char[] error, any data)
855{
856 if(results == null)
857 {
858 LogError("Timer (rankings, update player rank) error! Reason: %s", error);
859
860 return;
861 }
862
863 int client = GetClientFromSerial(data);
864
865 if(client == 0)
866 {
867 return;
868 }
869
870 if(results.FetchRow())
871 {
872 gI_Rank[client] = results.FetchInt(0);
873 gF_Points[client] = results.FetchFloat(1);
874 }
875}
876
877void UpdateRankedPlayers()
878{
879 char[] sQuery = new char[512];
880 FormatEx(sQuery, 512, "SELECT COUNT(*) count FROM %susers WHERE points > 0.0;", gS_MySQLPrefix);
881 gH_SQL.Query(SQL_UpdateRankedPlayers_Callback, sQuery, 0, DBPrio_High);
882}
883
884public void SQL_UpdateRankedPlayers_Callback(Database db, DBResultSet results, const char[] error, any data)
885{
886 if(results == null)
887 {
888 LogError("Timer (rankings, update ranked players) error! Reason: %s", error);
889
890 return;
891 }
892
893 if(results.FetchRow())
894 {
895 gI_RankedPlayers = results.FetchInt(0);
896
897 UpdateTop100();
898 }
899}
900
901void UpdateTop100()
902{
903 char[] sQuery = new char[512];
904 FormatEx(sQuery, 512, "SELECT auth, name, FORMAT(points, 2) FROM %susers WHERE points > 0.0 ORDER BY points DESC LIMIT 100;", gS_MySQLPrefix);
905 gH_SQL.Query(SQL_UpdateTop100_Callback, sQuery, 0, DBPrio_Low);
906}
907
908public void SQL_UpdateTop100_Callback(Database db, DBResultSet results, const char[] error, any data)
909{
910 if(results == null)
911 {
912 LogError("Timer (rankings, update top 100) error! Reason: %s", error);
913
914 return;
915 }
916
917 if(gH_Top100Menu != null)
918 {
919 delete gH_Top100Menu;
920 }
921
922 gH_Top100Menu = new Menu(MenuHandler_Top);
923
924 int row = 0;
925
926 while(results.FetchRow())
927 {
928 if(row > 100)
929 {
930 break;
931 }
932
933 char[] sAuthID = new char[32];
934 results.FetchString(0, sAuthID, 32);
935
936 char[] sName = new char[MAX_NAME_LENGTH];
937 results.FetchString(1, sName, MAX_NAME_LENGTH);
938
939 char[] sPoints = new char[16];
940 results.FetchString(2, sPoints, 16);
941
942 char[] sDisplay = new char[96];
943 FormatEx(sDisplay, 96, "#%d - %s (%s)", (++row), sName, sPoints);
944 gH_Top100Menu.AddItem(sAuthID, sDisplay);
945 }
946
947 if(gH_Top100Menu.ItemCount == 0)
948 {
949 char[] sDisplay = new char[64];
950 FormatEx(sDisplay, 64, "%t", "NoRankedPlayers");
951 gH_Top100Menu.AddItem("-1", sDisplay);
952 }
953
954 gH_Top100Menu.ExitButton = true;
955}
956
957void GetTrackName(int client, int track, char[] output, int size)
958{
959 if(track < 0 || track >= TRACKS_SIZE)
960 {
961 FormatEx(output, size, "%T", "Track_Unknown", client);
962
963 return;
964 }
965
966 static char sTrack[16];
967 FormatEx(sTrack, 16, "Track_%d", track);
968 FormatEx(output, size, "%T", sTrack, client);
969}
970
971public int Native_GetMapTier(Handle handler, int numParams)
972{
973 int tier = 0;
974
975 char[] sMap = new char[128];
976 GetNativeString(1, sMap, 128);
977
978 if(!gA_MapTiers.GetValue(sMap, tier))
979 {
980 return 0;
981 }
982
983 return tier;
984}
985
986public int Native_GetMapTiers(Handle handler, int numParams)
987{
988 return view_as<int>(CloneHandle(gA_MapTiers, handler));
989}
990
991public int Native_GetPoints(Handle handler, int numParams)
992{
993 return view_as<int>(gF_Points[GetNativeCell(1)]);
994}
995
996public int Native_GetRank(Handle handler, int numParams)
997{
998 return gI_Rank[GetNativeCell(1)];
999}
1000
1001public int Native_GetRankedPlayers(Handle handler, int numParams)
1002{
1003 return gI_RankedPlayers;
1004}