Skip to content

Commit

Permalink
feat: max overtime and tiebreaker (verified working)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shigbeard committed Jan 10, 2025
1 parent c4fa909 commit 6a37c71
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 29 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.vscode
addons\sourcemod\plugins\
addons\sourcemod\plugins\*
71 changes: 56 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,75 @@
# [TF2] Progressive Ruleset Timer Plugins
The Progressive Ruleset ("Pro Ruleset" for short) incorporates this plugin that creates a dynamic win condition. 5CP round win limit gets reduced to the current highest score +1 when the match timer runs out. For example, if the score is 1 - 2 when the map timer runs out, the win limit will be set to 3 and round continues.
# Enhanced Match Timer
The Enhanced Match Timer, formerly "Progressive Ruleset Timer Plugins", is a SourceMod plugin that modifies the behavior of the match and round timer in competitive 5CP matches.

# Why Use This Plugin?
All current competitive rulesets for the 5CP gametype include a timer that sets a hard time limit on matches. Match timers reduce competitive integrity in the 5CP gametype. In its current form, the timer kills comebacks, causes anticlimactic endings, and encourages teams to run down the clock.
# Plugin History
Originally, this plugin set out to simply reduce the length of the round timer, and remove the match timer once said timer had expired while also reducing the score required to win.

Enhanced Match Timer reduces the length of matches without encouraging timer related strategies. It allows for exciting comebacks, makes every match end in a last capture, and discourages running down the clock.
The goal of that iteration of the plugin was to impove competitive integrity by eliminating timer based strategies such as parking the bus (that is, running down the clock once a team has a lead) and to make every match end in a last point capture.

However, the plugin failed to account for tournament structures where the overall match time has a significant impact on subsequent games played (such as LANs and One Night Cups), and additionally the plugin was susceptible to abuse by teams who would intentionally run up the score gap to a large number, and then park the bus into overtime to force the underdog team to make an impossible comeback over an unreasonably long period of time.

# How we've fixed this
First off, when I chose to work on this plugin, I wanted to make sure that the plugin followed a simple rule: **Any new features of the plugin should be opt-in, not opt-out**. This means that if a league admin does not update their configs, but does update this plugin, the behaviour of the plugin should not change.

With that said, we've implemented the following features:

## Win Difference Threshold
Enabled with `mp_winlimit_improved_threshold <score>`, this feature will prevent the plugin from activating if the score difference between the two teams is greater than the threshold. This can be a little confusing, so let me explain

When you take the score of the two teams (red and blue), and subtract the lower score from the higher score, you get the win difference. If this win difference is __*greater than*__ the threshold, the plugin will not start overtime, and the match will end as normal. This is handy for leagues that only wish to use the plugin as an automatic golden cap, or for leagues that wish to end a game early if it's clear that one team is significantly in the lead with next to no chance of being beaten.

In case that still isn't clear, here's a practical example:

Let's say we set the threshold to 1. Red team has scored 4 points, and Blue team has scored 1. At the end of the timer, the plugin will see that the win difference is 3, but we've set the threshold to 1. The game will now end.

Inversely, let's say the threshold is 1, Red team has scored 2 points, and Blue team has scored 1. The win difference is 1, and so is the threshold. The game will now go into overtime, and the winlimit will be reduced to 3, meaning that which ever team gets to 3 points in overtime will be declared the winner.

Lastly, if you simply want it to go into overtime no matter what, set the threshold to -1, and the plugin will ignore the win difference.

## Maximum Overtime Length
Enabled with `mp_timelimit_improved_timelimit <minutes>`, this feature will set a maximum time limit for the match timer in overtime. This is useful for leagues that wish to have a hard time limit on matches, but still want to use the plugin for its other features.

Once the match timer has expired, if the game were to go to overtime, the match timer will be increased by the value of `mp_timelimit_improved_timelimit <minutes>`. For instance, if the match timer is set to 25 minutes, and `mp_timelimit_improved_timelimit <minutes>` is set to 5 minutes, the match will go to overtime with a 30 minute timer (which TF2 will interpret as adding 5 minutes to the current timer).

This also adheres to the Win Difference Threshold, so if the win difference is greater than the threshold, the match will end as normal.

## Tiebreaker
Enabled with the Maximum Overtime Length and `mp_timelimit_improved_tiebreaker <0/1>`, this feature will resolve ties at the end of overtime. If both teams are tied for rounds, the winning team will be determined by the number of points captured (or, in simpler terms, whoever has captured mid). The winning team will receive 1 additional round point and the game will then be immediately ended. If the mid point is not captured, the game will end in a tie still, as it is not possible to determine a winner.

This feature is useful for leagues that wish to have a clear winner at the end of a match, and especially useeful for LANs.

# Why Use This Plugin instead of the Original?
As mentioned earlier, the original plugin had a number of flaws for tournament structures that weren't a traditional 5CP Scrim or Official, however the maintainers of that plugin do not seem to see these issues as a problem. At the bequest of several league admins, I have taken it upon myself to fix these issues and provide a more robust plugin that can be used in a wider variety of settings.

Naturally as the original plugin would conflict with this one, I've also chosen to add functionality to disable the original plugin should this one be running in the same environment. This is to prevent any conflicts that may arise from having two plugins that modify the same cvars or interact with the same timers.

# How to Install
You'll need SourceMod installed on your server before installing Enhanced Match Timer.

Place the addons folder into the tf directory.

# How to Use
Enhanced Match Timer creates a new cvar named "mp_timelimit_improved" which is by default 0. This means that the plugin by default does nothing. I recommend that you change this cvar only through ruleset related configs. I've provided a modified rgl_6s_5cp_scrim config that contains "mp_timelimit_improved 1" as an example.
# Commands
`end_match`: Ends the match immediately, can be used without RCON. Players should use this instead of re-executing, as it will not cause logs to be lost.

The plugin is only active on cp_ maps when mp_timelimit is above 0 and mp_timelimit_improved is set to 1. These conditions must be met before the match begins. If the plugin is active, you should see the phrase "Running Enhanced Match Timer..." in chat after the match starts. If you need to toggle the plugin, simply exec a config with mp_timelimit_improved changed before readying up.
# Options
`mp_timelimit_improved`: Enables the plugin's match timer related behaviors. 0 off (default), 1 on.

Additionally, the plugin will not activate if the score difference is greater than the threshold defined by mp_timelimit_improved_threshold. For instance, if this convar is set to 1, then the win difference between both teams must be 1 or lower in order to activate the plugin. If the timer expires with a win difference of 2 or more, the match would end as normal.
`mp_timelimit_improved_visibility`: Hides the match timer when a team reaches 4 rounds won. 0 off (default), 1 on.

# Options
mp_timelimit_improved: Enables the plugin's match timer related behaviors. 0 off (default), 1 on.
`mp_roundtime` / `round_time_override`: Changes the length (in seconds) of the round timer in 5CP and KOTH. -1 for default gametype behavior (default).

mp_timelimit_improved_visibility: Hides the match timer when a team reaches 4 rounds won. 0 off (default), 1 on.
`sm_improvedtimers_chat`: If 1 (default), prints timer related notifications to chat.

mp_roundtime / round_time_override: Changes the length (in seconds) of the round timer in 5CP and KOTH. -1 for default gametype behavior (default).
`mp_timelimit_improved_threshold`: The win difference threshold for activating the Enhanced Match Timer features. Anything above this number of rounds between the teams will end the match at the end of the timer. Set to -1 to disable (default)

sm_improvedtimers_chat: If 1 (default), prints timer related notifications to chat.
`mp_timelimit_improved_timelimit`: The time limit (in minutes) for the match timer in overtime. 0 for no time limit (default).

mp_timelimit_improved_threshold: The win difference threshold for activating the Enhanced Match Timer features. Anything above this number of rounds between the teams will end the match at the end of the timer. Set to -1 to disable (default)
`mp_timelimit_improved_tiebreaker`: If 1, the match will resolve ties at the end of overtime (if mp_timelimit_improved_timelimit is greater than 0). In the event that both teams are tied for rounds, the winning team will be determined by the number of points captured (or, in simpler terms, whoever has captured mid). The winning team will receive 1 additional round point and the game will then be immediately ended.

# Credits

- [Dewbsku](https://github.com/dewbsku) and [b4nny](https://github.com/b4nnyBot) - Original authors of the plugin

- [Ozfortress](https://ozfortress.com) - For their support, feedback, and testing of the plugin

- [CappingTV](https://twitch.tv/cappingtv) - For letting Summer Brawl 2025 be the guinnea pig for the tiebreaker feature.
56 changes: 42 additions & 14 deletions addons/sourcemod/scripting/enhanced_match_timer.sp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
#pragma newdecls required
#pragma semicolon 1

#define RED_TEAM 2
#define BLU_TEAM 3

bool doOnRestart = true;
char mapname[64];
int winlimit_original = -1;
Expand Down Expand Up @@ -46,7 +49,7 @@ public Plugin myinfo =
name = "Enhanced Match Timer (formerly Improved Match Timer)",
author = "Shigbeard (originally by Dooby Skoo)",
description = "TF2 round win limit gets reduced after the map timer runs out on 5CP, optionally after a defined threshold of round win difference.",
version = "1.4.0",
version = "1.4.1",
url = "https://github.com/Shigbeard"
};

Expand All @@ -61,6 +64,8 @@ public void OnPluginStart(){
mp_timelimit_improved_timelimit = CreateConVar("mp_timelimit_improved_timelimit","0","The timelimit to set the server to when the round timer runs out. Set to 0 to for infinite (default)", FCVAR_NONE, true, 0.0, false);
mp_timelimit_improved_tiebreaker = CreateConVar("mp_timelimit_improved_tiebreaker","0","Which method to use for handling a tie/draw in overtime. 0 - end game (default), 1 - grant final point to team with the most control points", FCVAR_NONE, true, 0.0, true, 1.0);

RegConsoleCmd("end_match", End_Match, "Ends the match immediately.", 0);

cvar_timelimit = FindConVar("mp_timelimit");
cvar_restartgame = FindConVar("mp_restartgame");
cvar_winlimit = FindConVar("mp_winlimit");
Expand All @@ -71,6 +76,27 @@ public void OnPluginStart(){

}

bool IsClientValid(int client){
return (client > 0 && client <= MaxClients && IsClientInGame(client));
}

public Action End_Match(int client, int flags)
{
char user[64];
if (IsClientValid(client))
{
GetClientName(client, user, sizeof(user));
PrintToChatAll("%s has ended the match.", user);
}
else
{
PrintToChatAll("The match has been ended.");
}
ServerCommand("mp_winlimit 1");
ServerCommand("mp_timelimit 1");
return Plugin_Handled;
}

public void OnConfigsExecuted()
{
// Detect if the original plugin is running, disable it if it is.
Expand Down Expand Up @@ -166,15 +192,15 @@ public Action WaitTime(Handle timer){
public Action CheckTieBreaker(Handle timer){
int timeleft;
GetMapTimeLeft(timeleft);
PrintToServer("Checking Tiebreaker %d", timeleft);
// PrintToServer("Checking Tiebreaker %d", timeleft);
bool tiebreaker_mode = GetConVarBool(mp_timelimit_improved_tiebreaker);
if(timeleft<=1){
if(tiebreaker_mode){
// First up - is a team in the lead?
int redScore = GetTeamScore(2);
int blueScore = GetTeamScore(3);
int redScore = GetTeamScore(RED_TEAM);
int blueScore = GetTeamScore(BLU_TEAM);
if (redScore != blueScore){
ServerCommand("mp_timelimit 1");
ServerCommand("mp_winlimit 1");
if(sm_improvedtimers_chat.BoolValue) PrintToChatAll("The game ends now!");
timer2 = INVALID_HANDLE;
return Plugin_Stop;
Expand All @@ -199,17 +225,19 @@ public Action CheckTieBreaker(Handle timer){
// SetTeamScore(2, redScore + 1);
AcceptEntityInput(MasterControlNode, "SetWinnerAndForceCaps 2");
if (sm_improvedtimers_chat.BoolValue) PrintToChatAll("Red team wins the tiebreaker!");
SetTeamScore(2, redScore + 1);
} else if(blueCaps > redCaps){
// Grant a round to blue
// SetTeamScore(3, blueScore + 1);
AcceptEntityInput(MasterControlNode, "SetWinnerAndForceCaps 3");
if (sm_improvedtimers_chat.BoolValue) PrintToChatAll("Blu team wins the tiebreaker!");
SetTeamScore(3, blueScore + 1);
}
else{
// No team has more control points than the other, end the game in a draw
if(sm_improvedtimers_chat.BoolValue) PrintToChatAll("The game ends in a draw!");
}
ServerCommand("mp_timelimit 1");
ServerCommand("mp_winlimit 1");
timer2 = INVALID_HANDLE;
return Plugin_Stop;
}
Expand All @@ -233,7 +261,7 @@ public Action CheckRoundTime(Handle timer){
// mp_timelimit_improved_threshold.IntValue;
if(timeleft<=1){
// If the Threshold is set, and the difference in wins is greater than the threshold, end the game
if(mp_timelimit_improved_threshold.IntValue > -1 && mp_timelimit_improved_threshold.IntValue < intAbs(GetTeamScore(2) - GetTeamScore(3))) {
if(mp_timelimit_improved_threshold.IntValue > -1 && mp_timelimit_improved_threshold.IntValue < intAbs(GetTeamScore(RED_TEAM) - GetTeamScore(BLU_TEAM))) {
// The threshold has been breached, and the timer is up. End the game.
if(sm_improvedtimers_chat.BoolValue) PrintToChatAll("Win Difference threshold has been met, this match is over.");
timer2 = INVALID_HANDLE;
Expand All @@ -250,8 +278,8 @@ public Action CheckRoundTime(Handle timer){
} else {
ServerCommand("mp_timelimit 0");
}
int newRoundLimit = GetTeamScore(3) + 1;
if(GetTeamScore(2)+1>GetTeamScore(3)+1) newRoundLimit = GetTeamScore(2)+1;
int newRoundLimit = GetTeamScore(BLU_TEAM) + 1;
if(GetTeamScore(RED_TEAM)+1>GetTeamScore(BLU_TEAM)+1) newRoundLimit = GetTeamScore(RED_TEAM)+1;
if(newRoundLimit>5) newRoundLimit = 5;
if(newRoundLimit<5){
ServerCommand("mp_winlimit %d", newRoundLimit);
Expand All @@ -260,9 +288,9 @@ public Action CheckRoundTime(Handle timer){
for(int client=1;client<=MAXPLAYERS;client++){
if(IsValidClient(client)){
if(GetClientTeam(client) == 2) PrintToChat(client, "Win %d more round%s to win the match!",
newRoundLimit-GetTeamScore(2), (newRoundLimit-GetTeamScore(2)!=1) ? "s":"");
newRoundLimit-GetTeamScore(RED_TEAM), (newRoundLimit-GetTeamScore(RED_TEAM)!=1) ? "s":"");
if(GetClientTeam(client) == 3) PrintToChat(client, "Win %d more round%s to win the match!",
newRoundLimit-GetTeamScore(3), (newRoundLimit-GetTeamScore(3)!=1) ? "s":"");
newRoundLimit-GetTeamScore(BLU_TEAM), (newRoundLimit-GetTeamScore(BLU_TEAM)!=1) ? "s":"");
}
}
if (mp_timelimit_improved_tiebreaker.IntValue==1){
Expand All @@ -275,14 +303,14 @@ public Action CheckRoundTime(Handle timer){
return Plugin_Stop;
}
}
if((GetTeamScore(2) >= 4 || GetTeamScore(3) >= 4) && mp_timelimit_improved_visibility.BoolValue){
if((GetTeamScore(RED_TEAM) >= 4 || GetTeamScore(BLU_TEAM) >= 4) && mp_timelimit_improved_visibility.BoolValue){
ServerCommand("mp_timelimit 0");
for(int client=1;client<=MAXPLAYERS;client++){
if(IsValidClient(client)){
if(GetClientTeam(client) == 2) PrintToChat(client, "Win %d more round%s to win the match!",
5-GetTeamScore(2), (5-GetTeamScore(2)!=1) ? "s":"");
5-GetTeamScore(RED_TEAM), (5-GetTeamScore(RED_TEAM)!=1) ? "s":"");
if(GetClientTeam(client) == 3) PrintToChat(client, "Win %d more round%s to win the match!",
5-GetTeamScore(3), (5-GetTeamScore(3)!=1) ? "s":"");
5-GetTeamScore(BLU_TEAM), (5-GetTeamScore(BLU_TEAM)!=1) ? "s":"");
}
}
timer2 = INVALID_HANDLE;
Expand Down

0 comments on commit 6a37c71

Please sign in to comment.