Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ enum
Migration_DeprecateExactTimeInt,
Migration_AddPlayertimesAuthFK,
Migration_FixSQLiteMapzonesROWID,
Migration_NormalizeStageTimes,
MIGRATIONS_END
};

Expand Down Expand Up @@ -87,6 +88,7 @@ char gS_MigrationNames[][] = {
"DeprecateExactTimeInt",
"AddPlayertimesAuthFK",
"FixSQLiteMapzonesROWID",
"NormalizeStageTimes",
};

static Database gH_SQL;
Expand Down Expand Up @@ -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
//
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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] = {
Expand Down Expand Up @@ -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();
}
}

Expand Down Expand Up @@ -508,6 +527,75 @@ 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);
}

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];
Expand Down
128 changes: 76 additions & 52 deletions addons/sourcemod/scripting/shavit-wr.sp
Original file line number Diff line number Diff line change
Expand Up @@ -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, ST.time \
FROM `%sstagetimes` AS `ST` INNER JOIN `%swrs` AS `WR` ON WR.id = ST.playertimes_id \
WHERE WR.map = '%s';",
gS_MySQLPrefix, gS_MySQLPrefix, gS_Map);
QueryLog(gH_SQL, SQL_UpdateWRStageTimes_Callback, sQuery);
}

Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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, %f);",
i, fTime
);
AddQueryLog(trans, sQuery);
}

if(iOverwrite == 1) // insert
{
FormatEx(sMessage, 255, "%s[%s]%s %T",
Expand All @@ -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
{
Expand All @@ -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);
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion addons/sourcemod/scripting/shavit-zones.sp
Original file line number Diff line number Diff line change
Expand Up @@ -5266,7 +5266,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);
Expand Down Expand Up @@ -5426,10 +5426,12 @@ public void TouchPost(int entity, int other)
if (Shavit_GetTimerStatus(other) == Timer_Stopped || Shavit_GetClientTrack(other) != Track_Main)
{
Shavit_StartTimer(other, track);
gI_LastStage[other] = 0;
}
else if (track == Track_Main)
{
Shavit_StartTimer(other, Track_Main);
gI_LastStage[other] = 0;
}
}
case Zone_Respawn:
Expand Down
2 changes: 1 addition & 1 deletion addons/sourcemod/translations/shavit-zones.phrases.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
{
Expand Down