diff --git a/addons/sourcemod/scripting/include/shavit/sql-create-tables-and-migrations.sp b/addons/sourcemod/scripting/include/shavit/sql-create-tables-and-migrations.sp index 3b483d926..77fda5ee7 100644 --- a/addons/sourcemod/scripting/include/shavit/sql-create-tables-and-migrations.sp +++ b/addons/sourcemod/scripting/include/shavit/sql-create-tables-and-migrations.sp @@ -53,6 +53,7 @@ enum Migration_DeprecateExactTimeInt, Migration_AddPlayertimesAuthFK, Migration_FixSQLiteMapzonesROWID, + Migration_NormalizeStageTimes, MIGRATIONS_END }; @@ -87,6 +88,7 @@ char gS_MigrationNames[][] = { "DeprecateExactTimeInt", "AddPlayertimesAuthFK", "FixSQLiteMapzonesROWID", + "NormalizeStageTimes", }; static Database gH_SQL; @@ -126,6 +128,14 @@ public void SQL_CreateTables(Database hSQL, const char[] prefix, int driver) sOptionalINNODB = "ENGINE=INNODB"; } + // Enable foreign key constraints for SQLite (e.g. CASCADE DELETE) + // No-op within a transaction, so has to be its own query + if (driver == Driver_sqlite) + { + FormatEx(sQuery, sizeof(sQuery), "PRAGMA foreign_keys = ON"); + QueryLog(gH_SQL, SQL_PragmaFKSqlite_Callback, sQuery, 0, DBPrio_High); + } + // //// shavit-core // @@ -209,7 +219,7 @@ public void SQL_CreateTables(Database hSQL, const char[] prefix, int driver) AddQueryLog(trans, sQuery); FormatEx(sQuery, sizeof(sQuery), - "CREATE TABLE IF NOT EXISTS `%sstagetimeswr` (`style` TINYINT NOT NULL, `track` TINYINT NOT NULL DEFAULT 0, `map` VARCHAR(255) NOT NULL, `stage` TINYINT NOT NULL, `auth` INT NOT NULL, `time` FLOAT NOT NULL, PRIMARY KEY (`style`, `track`, `map`, `stage`)) %s;", + "CREATE TABLE IF NOT EXISTS `%sstagetimes` (`playertimes_id` INT NOT NULL, `stage` TINYINT NOT NULL, `time` FLOAT NOT NULL, PRIMARY KEY (`playertimes_id`, `stage`), FOREIGN KEY(`playertimes_id`) REFERENCES playertimes (`id`) ON UPDATE CASCADE ON DELETE CASCADE) %s;", gS_SQLPrefix, sOptionalINNODB); AddQueryLog(trans, sQuery); @@ -266,6 +276,14 @@ public void SQL_CreateTables(Database hSQL, const char[] prefix, int driver) hSQL.Execute(trans, Trans_CreateTables_Success, Trans_CreateTables_Error, 0, DBPrio_High); } +public void SQL_PragmaFKSqlite_Callback(Database db, DBResultSet results, const char[] error, any data) +{ + if (results == null) + { + SetFailState("Timer failed to enable foreign key constraints (SQLite). Reason: %s", error); + } +} + public void Trans_CreateTables_Error(Database db, any data, int numQueries, const char[] error, int failIndex, any[] queryData) { static char tablenames[][32] = { @@ -369,6 +387,7 @@ void ApplyMigration(int migration) case Migration_DeprecateExactTimeInt: ApplyMigration_DeprecateExactTimeInt(); case Migration_AddPlayertimesAuthFK: ApplyMigration_AddPlayertimesAuthFK(); case Migration_FixSQLiteMapzonesROWID: ApplyMigration_FixSQLiteMapzonesROWID(); + case Migration_NormalizeStageTimes: ApplyMigration_NormalizeStageTimes(); } } @@ -508,6 +527,76 @@ void ApplyMigration_NormalizeMapzonePoints() // TODO: test with sqlite lol QueryLog(gH_SQL, SQL_TableMigrationSingleQuery_Callback, sQuery, Migration_NormalizeMapzonePoints, DBPrio_High); } +void ApplyMigration_NormalizeStageTimes() +{ + char sQuery[192]; + // Check if stagetimeswr exists before migration (it does not on fresh installs) + if (gI_Driver == Driver_mysql) + { + FormatEx(sQuery, sizeof(sQuery), "SHOW TABLES LIKE '%sstagetimeswr';", gS_SQLPrefix); + } + else if (gI_Driver == Driver_sqlite) + { + FormatEx(sQuery, sizeof(sQuery), "SELECT 1 FROM sqlite_master WHERE type='table' AND name='%sstagetimeswr';", gS_SQLPrefix); + } + else // PostgreSQL unaffected + { + InsertMigration(Migration_NormalizeStageTimes); + return; + } + + QueryLog(gH_SQL, SQL_NormalizeStageTimes_Callback, sQuery, 0, DBPrio_High); +} + +public void SQL_NormalizeStageTimes_Callback(Database db, DBResultSet results, const char[] error, any data) +{ + if (results == null) + { + LogError("Timer error! Existence detection of %sstagetimeswr failed. Reason: %s", gS_SQLPrefix, error); + return; + } + + if (results.FetchRow()) //stagetimeswr exists + { + Transaction trans = new Transaction(); + char sQuery[512]; + + FormatEx(sQuery, sizeof(sQuery), + "INSERT INTO `%sstagetimes` (playertimes_id, stage, time) \ + SELECT \ + PT.id AS `playertimes_id`, \ + STWR.stage, \ + STWR.time \ + FROM `%splayertimes` AS `PT` \ + INNER JOIN `%sstagetimeswr` AS `STWR` ON PT.auth = STWR.auth \ + AND PT.map = STWR.map \ + AND PT.track = STWR.track \ + AND PT.style = STWR.style;", + gS_SQLPrefix, gS_SQLPrefix, gS_SQLPrefix + ); + AddQueryLog(trans, sQuery); + + FormatEx(sQuery, sizeof(sQuery), "DROP TABLE `%sstagetimeswr`;", gS_SQLPrefix); + AddQueryLog(trans, sQuery); + + gH_SQL.Execute(trans, Trans_NormalizeStageTimes_Success, Trans_NormalizeStageTimes_Error, 0, DBPrio_High); + } + else + { + InsertMigration(Migration_NormalizeStageTimes); + } +} + +public void Trans_NormalizeStageTimes_Success(Database db, any data, int numQueries, DBResultSet[] results, any[] queryData) +{ + InsertMigration(Migration_NormalizeStageTimes); +} + +public void Trans_NormalizeStageTimes_Error(Database db, any data, int numQueries, const char[] error, int failIndex, any[] queryData) +{ + LogError("Timer error! NormalizeStageTimes migration transaction failed. Reason: %s", error); +} + void ApplyMigration_AddMapzonesForm() { char sQuery[192]; diff --git a/addons/sourcemod/scripting/shavit-wr.sp b/addons/sourcemod/scripting/shavit-wr.sp index f1e4c2043..9546be93c 100644 --- a/addons/sourcemod/scripting/shavit-wr.sp +++ b/addons/sourcemod/scripting/shavit-wr.sp @@ -575,9 +575,10 @@ void UpdateWRCache(int client = -1) char sQuery[512]; FormatEx(sQuery, sizeof(sQuery), - "SELECT style, track, auth, stage, time FROM `%sstagetimeswr` WHERE map = '%s';", - gS_MySQLPrefix, gS_Map); - + "SELECT WR.style, WR.track, ST.stage, %s \ + FROM `%sstagetimes` AS `ST` INNER JOIN `%swrs` AS `WR` ON WR.id = ST.playertimes_id \ + WHERE WR.map = '%s';", gI_Driver == Driver_mysql ? "REPLACE(FORMAT(ST.time, 9), ',', '')" : "printf(\"%.9f\", ST.time)", + gS_MySQLPrefix, gS_MySQLPrefix, gS_Map); QueryLog(gH_SQL, SQL_UpdateWRStageTimes_Callback, sQuery); } @@ -604,9 +605,9 @@ public void SQL_UpdateWRStageTimes_Callback(Database db, DBResultSet results, co { int style = results.FetchInt(0); int track = results.FetchInt(1); - int stage = results.FetchInt(3); + int stage = results.FetchInt(2); - gA_StageWR[style][track][stage] = results.FetchFloat(4); + gA_StageWR[style][track][stage] = results.FetchFloat(3); } } @@ -2562,10 +2563,11 @@ public void Shavit_OnFinish(int client, int style, float time, int jumps, int st } bool bEveryone = (iOverwrite > 0); + bool bIsWR = (iOverwrite > 0 && (time < gF_WRTime[style][track] || gF_WRTime[style][track] == 0.0)); char sMessage[255]; char sMessage2[255]; - if(iOverwrite > 0 && (time < gF_WRTime[style][track] || gF_WRTime[style][track] == 0.0)) // WR? + if(bIsWR) { float fOldWR = gF_WRTime[style][track]; gF_WRTime[style][track] = time; @@ -2599,36 +2601,6 @@ public void Shavit_OnFinish(int client, int style, float time, int jumps, int st #if defined DEBUG Shavit_PrintToChat(client, "old: %.01f new: %.01f", fOldWR, time); #endif - - Transaction trans = new Transaction(); - char query[512]; - - FormatEx(query, sizeof(query), - "DELETE FROM `%sstagetimeswr` WHERE style = %d AND track = %d AND map = '%s';", - gS_MySQLPrefix, style, track, gS_Map - ); - - AddQueryLog(trans, query); - - for (int i = 0; i < MAX_STAGES; i++) - { - float fTime = gA_StageTimes[client][i]; - gA_StageWR[style][track][i] = fTime; - - if (fTime == 0.0) - { - continue; - } - - FormatEx(query, sizeof(query), - "INSERT INTO `%sstagetimeswr` (`style`, `track`, `map`, `auth`, `time`, `stage`) VALUES (%d, %d, '%s', %d, %f, %d);", - gS_MySQLPrefix, style, track, gS_Map, iSteamID, fTime, i - ); - - AddQueryLog(trans, query); - } - - gH_SQL.Execute(trans, Trans_ReplaceStageTimes_Success, Trans_ReplaceStageTimes_Error, 0, DBPrio_High); } int iRank = GetRankForTime(style, time, track); @@ -2668,8 +2640,36 @@ public void Shavit_OnFinish(int client, int style, float time, int jumps, int st { float fPoints = gB_Rankings ? Shavit_GuessPointsForTime(track, style, -1, time, gF_WRTime[style][track]) : 0.0; + Transaction trans = new Transaction(); char sQuery[1024]; + FormatEx(sQuery, sizeof(sQuery), "CREATE TEMPORARY TABLE IF NOT EXISTS `Insert_Stages` (`playertimes_id` INT, `stage` TINYINT NOT NULL, `time` FLOAT NOT NULL);"); + AddQueryLog(trans, sQuery); + + FormatEx(sQuery, sizeof(sQuery), "DELETE FROM `Insert_Stages`;"); + AddQueryLog(trans, sQuery); + + for (int i = 0; i < MAX_STAGES; i++) + { + float fTime = gA_StageTimes[client][i]; + + if(bIsWR) + { + gA_StageWR[style][track][i] = fTime; + } + + if (fTime == 0.0) + { + continue; + } + + FormatEx(sQuery, sizeof(sQuery), + "INSERT INTO `Insert_Stages` (`stage`, `time`) VALUES (%d, %.9f);", + i, fTime + ); + AddQueryLog(trans, sQuery); + } + if(iOverwrite == 1) // insert { FormatEx(sMessage, 255, "%s[%s]%s %T", @@ -2678,6 +2678,18 @@ public void Shavit_OnFinish(int client, int style, float time, int jumps, int st FormatEx(sQuery, sizeof(sQuery), "INSERT INTO %splayertimes (auth, map, time, jumps, date, style, strafes, sync, points, track, perfs) VALUES (%d, '%s', %.9f, %d, %d, %d, %d, %.2f, %f, %d, %.2f);", gS_MySQLPrefix, iSteamID, gS_Map, time, jumps, timestamp, style, strafes, sync, fPoints, track, perfs); + AddQueryLog(trans, sQuery); + + // Will affect 0 rows on linear/unstaged maps + // TODO: Needs PostgreSQL support + FormatEx(sQuery, sizeof(sQuery), "UPDATE `Insert_Stages` SET `playertimes_id` = %s;", gI_Driver == Driver_mysql ? "LAST_INSERT_ID()" : "last_insert_rowid()"); + AddQueryLog(trans, sQuery); + + FormatEx(sQuery, sizeof(sQuery), + "INSERT INTO `%sstagetimes` (playertimes_id, stage, time) SELECT playertimes_id, stage, time FROM `Insert_Stages`;", + gS_MySQLPrefix + ); + AddQueryLog(trans, sQuery); } else // update { @@ -2687,9 +2699,26 @@ public void Shavit_OnFinish(int client, int style, float time, int jumps, int st FormatEx(sQuery, sizeof(sQuery), "UPDATE %splayertimes SET time = %.9f, jumps = %d, date = %d, strafes = %d, sync = %.02f, points = %f, perfs = %.2f, completions = completions + 1 WHERE map = '%s' AND auth = %d AND style = %d AND track = %d;", gS_MySQLPrefix, time, jumps, timestamp, strafes, sync, fPoints, perfs, gS_Map, iSteamID, style, track); + AddQueryLog(trans, sQuery); + + // Will affect 0 rows on linear/unstaged maps + FormatEx(sQuery, sizeof(sQuery), "UPDATE `Insert_Stages` SET `playertimes_id` = (SELECT id FROM `%splayertimes` WHERE map = '%s' AND auth = %d AND style = %d AND track = %d);", + gS_MySQLPrefix, gS_Map, iSteamID, style, track); + AddQueryLog(trans, sQuery); + + // Delete all stage times first (safer than an UPDATE in unexpected cases where the new completion has fewer/no stage times, e.g. bad zoning) + FormatEx(sQuery, sizeof(sQuery), "DELETE FROM `%sstagetimes` WHERE playertimes_id IN (SELECT id FROM `%splayertimes` WHERE map = '%s' AND auth = %d AND style = %d AND track = %d);", + gS_MySQLPrefix, gS_MySQLPrefix, gS_Map, iSteamID, style, track); + AddQueryLog(trans, sQuery); + + FormatEx(sQuery, sizeof(sQuery), + "INSERT INTO `%sstagetimes` (playertimes_id, stage, time) SELECT playertimes_id, stage, time FROM `Insert_Stages`;", + gS_MySQLPrefix + ); + AddQueryLog(trans, sQuery); } - QueryLog(gH_SQL, SQL_OnFinish_Callback, sQuery, GetClientSerial(client), DBPrio_High); + gH_SQL.Execute(trans, Trans_OnFinishTimes_Success, Trans_OnFinishTimes_Error, GetClientSerial(client), DBPrio_High); Call_StartForward(gH_OnFinish_Post); Call_PushCell(client); @@ -2805,15 +2834,8 @@ public void SQL_OnIncrementCompletions_Callback(Database db, DBResultSet results } } -public void SQL_OnFinish_Callback(Database db, DBResultSet results, const char[] error, any data) +public void Trans_OnFinishTimes_Success(Database db, any data, int numQueries, DBResultSet[] results, any[] queryData) { - if(results == null) - { - LogError("Timer (WR OnFinish) SQL query failed. Reason: %s", error); - - return; - } - int client = GetClientFromSerial(data); if(client == 0) @@ -2824,14 +2846,9 @@ public void SQL_OnFinish_Callback(Database db, DBResultSet results, const char[] UpdateWRCache(client); } -public void Trans_ReplaceStageTimes_Success(Database db, any data, int numQueries, DBResultSet[] results, any[] queryData) +public void Trans_OnFinishTimes_Error(Database db, any data, int numQueries, const char[] error, int failIndex, any[] queryData) { - return; -} - -public void Trans_ReplaceStageTimes_Error(Database db, any data, int numQueries, const char[] error, int failIndex, any[] queryData) -{ - LogError("Timer (ReplaceStageTimes) SQL query failed %d/%d. Reason: %s", failIndex, numQueries, error); + LogError("Timer (WR OnFinish) SQL query failed %d/%d. Reason: %s", failIndex, numQueries, error); } void UpdateLeaderboards() @@ -2909,6 +2926,13 @@ public Action Shavit_OnStageMessage(int client, int stageNumber, char[] message, gA_StageTimes[client][stageNumber] = stageTime; + // Don't show ANY stage message if 0.0 + // (e.g. if stage zone intersects with start zone) + if (stageTime == 0.0) + { + return Plugin_Handled; + } + if (stageTimeWR == 0.0) { return Plugin_Continue; diff --git a/addons/sourcemod/scripting/shavit-zones.sp b/addons/sourcemod/scripting/shavit-zones.sp index b9c412a74..8820ff09a 100644 --- a/addons/sourcemod/scripting/shavit-zones.sp +++ b/addons/sourcemod/scripting/shavit-zones.sp @@ -4942,13 +4942,17 @@ public Action Shavit_OnStart(int client, int track) public void Shavit_OnRestart(int client, int track) { - gI_LastStage[client] = 0; - if (!IsPlayerAlive(client)) { return; } + // For stage zones that intersect with start zone + if (!Shavit_InsideZone(client, Zone_Start, -1)) + { + gI_LastStage[client] = 0; + } + int iIndex = GetZoneIndex(Zone_Start, track); if(gCV_TeleportToStart.BoolValue) @@ -5245,6 +5249,17 @@ public void StartTouchPost(int entity, int other) } } + case Zone_Start: + { + // Same logic as TouchPost Zone_Start: + // - reset last stage instantly for main start zone + // - only reset for bonus start zone if client's current track is a bonus + if (Shavit_GetClientTrack(other) != Track_Main || track == Track_Main) + { + gI_LastStage[other] = 0; + } + } + case Zone_End: { if (status == Timer_Running && Shavit_GetClientTrack(other) == track) @@ -5266,7 +5281,7 @@ public void StartTouchPost(int entity, int other) FormatSeconds(Shavit_GetClientTime(other), sTime, 32, true); char sMessage[255]; - FormatEx(sMessage, 255, "%T", "ZoneStageEnter", other, gS_ChatStrings.sText, gS_ChatStrings.sVariable2, num, gS_ChatStrings.sText, gS_ChatStrings.sVariable2, sTime, gS_ChatStrings.sText); + FormatEx(sMessage, 255, "%T", "ZoneStageEnter", other, gS_ChatStrings.sText, gS_ChatStrings.sVariable, num, gS_ChatStrings.sText, gS_ChatStrings.sVariable, sTime, gS_ChatStrings.sText); Action aResult = Plugin_Continue; Call_StartForward(gH_Forwards_StageMessage); diff --git a/addons/sourcemod/translations/shavit-zones.phrases.txt b/addons/sourcemod/translations/shavit-zones.phrases.txt index 5c8cdcfb6..aaae177c3 100644 --- a/addons/sourcemod/translations/shavit-zones.phrases.txt +++ b/addons/sourcemod/translations/shavit-zones.phrases.txt @@ -454,7 +454,7 @@ "ZoneStageEnter" { "#format" "{1:s},{2:s},{3:d},{4:s},{5:s},{6:s}{7:s}" - "en" "{1}Stage {2}{3}{4} @ {5}{6}{7} " + "en" "{1}You have reached stage {2}{3}{4} with a time of {5}{6}{7}." } "Zone_Start" {