Skip to content

Commit

Permalink
Implement forfeit after a certain amount of time
Browse files Browse the repository at this point in the history
  • Loading branch information
Pablete1234 committed Apr 2, 2021
1 parent 4312a28 commit 963426d
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 98 deletions.
13 changes: 7 additions & 6 deletions src/main/java/rip/bolt/ingame/commands/ForfeitCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public ForfeitCommands(RankedManager ranked) {
aliases = {"forfeit", "ff"},
desc = "Accept that you have no chance of winning")
public void forfeit(MatchPlayer sender, Match match) throws CommandException {
if (!AppData.allowForfeit())
if (!AppData.forfeitEnabled())
throw new CommandException(
ChatColor.RED + "The forfeit command is not enabled on this server.");

Expand All @@ -39,15 +39,16 @@ public void forfeit(MatchPlayer sender, Match match) throws CommandException {
ChatColor.RED + "Only match players are able to run this command.");

Competitor team = (Competitor) sender.getParty();
ForfeitManager.ForfeitCheck checker = forfeits.getChecker(team);
if (checker == null || !checker.isVotable())
if (!forfeits.mayForfeit(team))
throw new CommandException(
ChatColor.RED + "You may only run this command when your team has lost a player.");
ChatColor.YELLOW + "It's too early to forfeit this match, you can still win!");

if (checker.getVoted().contains(sender.getId()))
ForfeitManager.ForfeitPoll poll = forfeits.getForfeitPoll(team);

if (poll.getVoted().contains(sender.getId()))
throw new CommandException(ChatColor.RED + "You have already voted to forfeit this match.");

sender.sendMessage(text("You have voted to forfeit this match."));
checker.addVote(sender);
poll.addVote(sender);
}
}
10 changes: 7 additions & 3 deletions src/main/java/rip/bolt/ingame/config/AppData.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,15 @@ public static boolean allowRequeue() {
return Ingame.get().getConfig().getBoolean("allow-requeue", true);
}

public static boolean allowForfeit() {
return Ingame.get().getConfig().getBoolean("allow-forfeit", true);
public static boolean forfeitEnabled() {
return Ingame.get().getConfig().getBoolean("forfeit.enabled", true);
}

public static Duration forfeitAfter() {
return parseDuration(Ingame.get().getConfig().getString("forfeit.after", "300s"));
}

public static Duration matchStartDuration() {
return parseDuration(Ingame.get().getConfig().getString("match-start-duration", "300s"));
return parseDuration(Ingame.get().getConfig().getString("match-start-duration", "180s"));
}
}
150 changes: 68 additions & 82 deletions src/main/java/rip/bolt/ingame/ranked/ForfeitManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import static tc.oc.pgm.lib.net.kyori.adventure.text.Component.text;

import dev.pgm.events.Tournament;
import dev.pgm.events.team.TournamentPlayer;
import dev.pgm.events.team.TournamentTeamManager;
import java.time.Duration;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
Expand All @@ -15,12 +15,12 @@
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import rip.bolt.ingame.config.AppData;
import rip.bolt.ingame.utils.Messages;
import tc.oc.pgm.api.match.Match;
import tc.oc.pgm.api.match.MatchScope;
import tc.oc.pgm.api.party.Competitor;
import tc.oc.pgm.api.party.Party;
import tc.oc.pgm.api.party.VictoryCondition;
import tc.oc.pgm.api.player.MatchPlayer;
import tc.oc.pgm.lib.net.kyori.adventure.text.format.NamedTextColor;
Expand All @@ -30,113 +30,102 @@

public class ForfeitManager {

private final Map<Competitor, ForfeitCheck> checks = new HashMap<>();
private static final Duration FORFEIT_DURATION = AppData.forfeitAfter();

public ForfeitManager() {}
private final PlayerWatcher playerWatcher;

@Nullable
public ForfeitCheck getChecker(Competitor team) {
return checks.get(team);
}
private final Map<Competitor, LeaveAnnouncer> leaves = new HashMap<>();
private final Map<Competitor, ForfeitPoll> forfeit = new HashMap<>();

public void clearCheckers() {
checks.clear();
public ForfeitManager(PlayerWatcher playerWatcher) {
this.playerWatcher = playerWatcher;
}

public void startCountdown(Competitor team, PlayerWatcher.MatchParticipation participation) {
if (!AppData.allowForfeit()) return;
ForfeitCheck existingCheck = checks.get(team);

if (existingCheck != null) {
// Do not continue if existing check is in voting stage
if (existingCheck.isVotable()) {
existingCheck.check();
return;
}
public ForfeitPoll getForfeitPoll(Competitor team) {
return forfeit.computeIfAbsent(team, ForfeitPoll::new);
}

// Do nothing if new check is longer than currently running one
if (existingCheck.scheduledFuture.getDelay(TimeUnit.SECONDS)
< participation.absentDuration().getSeconds()) return;
public boolean mayForfeit(Competitor team) {
if (!AppData.forfeitEnabled()) return false;
if (team.getMatch().getDuration().compareTo(FORFEIT_DURATION) >= 0) return true;

existingCheck.stopTimer();
}
return getRegisteredPlayers(team)
.map(playerWatcher::getParticipation)
.filter(Objects::nonNull)
.anyMatch(PlayerWatcher.MatchParticipation::hasAbandoned);
}

checks.put(team, new ForfeitCheck(team, participation));
public void clearPolls() {
leaves.clear();
forfeit.clear();
}

public void stopCountdown(
PlayerWatcher.MatchParticipation participation,
Map<UUID, PlayerWatcher.MatchParticipation> players) {
checks.values().stream()
.filter(check -> check.participation.equals(participation) && !check.isVotable())
.peek(ForfeitCheck::stopTimer)
.findFirst()
.map(ForfeitCheck::getTeam)
.ifPresent(checks::remove);

// Check if another item needs starting
private Stream<UUID> getRegisteredPlayers(Competitor team) {
TournamentTeamManager teamManager = Tournament.get().getTeamManager();
teamManager
.tournamentTeamPlayer(participation.uuid)
.map(team -> team.getPlayers().stream())
return teamManager
.tournamentTeam(team)
.map(t -> t.getPlayers().stream())
.orElse(Stream.empty())
.map(tournamentPlayer -> players.get(tournamentPlayer.getUUID()))
.filter(Objects::nonNull)
.filter(PlayerWatcher.MatchParticipation::canStartCountdown)
.max(Comparator.comparingLong(p -> p.absentLength))
.ifPresent(p -> teamManager.playerTeam(p.uuid).ifPresent(t -> startCountdown(t, p)));
.map(TournamentPlayer::getUUID);
}

public static class ForfeitCheck {
public void updateCountdown(Party team) {
if (!AppData.forfeitEnabled() || !(team instanceof Competitor)) return;

leaves.computeIfAbsent((Competitor) team, LeaveAnnouncer::new).update();
}

public class LeaveAnnouncer {
private final Competitor team;
private final PlayerWatcher.MatchParticipation participation;
private final Set<UUID> voted = new HashSet<>();
private boolean hasCompleted;

private ScheduledFuture<?> scheduledFuture;
private boolean votable = false;

public ForfeitCheck(Competitor team, PlayerWatcher.MatchParticipation participation) {
public LeaveAnnouncer(Competitor team) {
this.team = team;
this.participation = participation;

startTimer();
}

public Competitor getTeam() {
return team;
private void broadcast() {
if (this.hasCompleted) return;
this.hasCompleted = true;
team.sendMessage(Messages.forfeit());
}

public Set<UUID> getVoted() {
return voted;
}
private void update() {
if (this.hasCompleted) return;

if (scheduledFuture != null) {
scheduledFuture.cancel(false);
scheduledFuture = null;
}

public boolean isVotable() {
return votable;
getRegisteredPlayers(team)
.map(playerWatcher::getParticipation)
.filter(PlayerWatcher.MatchParticipation::canStartCountdown)
.map(PlayerWatcher.MatchParticipation::absentDuration)
.max(Duration::compareTo)
.map(PlayerWatcher.ABSENT_MAX::minus)
.filter(duration -> !duration.isNegative())
.ifPresent(
duration ->
scheduledFuture =
team.getMatch()
.getExecutor(MatchScope.RUNNING)
.schedule(this::broadcast, duration.toMillis(), TimeUnit.MILLISECONDS));
}
}

private void startTimer() {
if (participation.absentDuration().compareTo(PlayerWatcher.ABSENT_MAX) > 0) {
broadcast();
} else {
public static class ForfeitPoll {

Duration length = PlayerWatcher.ABSENT_MAX.minus(participation.absentDuration());
scheduledFuture =
team.getMatch()
.getExecutor(MatchScope.RUNNING)
.schedule(this::broadcast, length.getSeconds(), TimeUnit.SECONDS);
}
}
private final Competitor team;
private final Set<UUID> voted = new HashSet<>();

private void stopTimer() {
if (scheduledFuture.isDone() || scheduledFuture.isCancelled()) return;
scheduledFuture.cancel(true);
public ForfeitPoll(Competitor team) {
this.team = team;
}

private void broadcast() {
votable = true;
team.sendMessage(Messages.forfeit());
check();
public Set<UUID> getVoted() {
return voted;
}

public void addVote(MatchPlayer player) {
Expand All @@ -149,10 +138,7 @@ public void check() {
}

private boolean hasPassed() {
// If all but one player has voted or all of the online players
long votes = voted.stream().filter(uuid -> team.getPlayer(uuid) != null).count();

return votes
return voted.stream().filter(uuid -> team.getPlayer(uuid) != null).count()
>= Math.min(
team instanceof Team ? ((Team) team).getMaxPlayers() - 1 : 0,
team.getPlayers().size());
Expand Down
20 changes: 14 additions & 6 deletions src/main/java/rip/bolt/ingame/ranked/PlayerWatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class PlayerWatcher implements Listener {

public PlayerWatcher(RankedManager rankedManager) {
this.rankedManager = rankedManager;
this.forfeitManager = new ForfeitManager();
this.forfeitManager = new ForfeitManager(this);
}

public ForfeitManager getForfeitManager() {
Expand All @@ -45,10 +45,14 @@ public ForfeitManager getForfeitManager() {

public void addPlayers(List<UUID> uuids) {
players.clear();
forfeitManager.clearCheckers();
forfeitManager.clearPolls();
uuids.forEach(uuid -> players.put(uuid, new MatchParticipation(uuid)));
}

public MatchParticipation getParticipation(UUID uuid) {
return players.get(uuid);
}

@EventHandler(priority = EventPriority.HIGH)
public void onPlayerLogin(final PlayerLoginEvent event) {
if (event.getResult() == PlayerLoginEvent.Result.KICK_FULL
Expand All @@ -69,7 +73,7 @@ public void onJoin(PlayerJoinMatchEvent event) {

MatchParticipation participation = players.get(player.getId());
participation.playerJoined();
forfeitManager.stopCountdown(participation, players);
forfeitManager.updateCountdown(event.getNewParty());
}

@EventHandler(priority = EventPriority.MONITOR)
Expand All @@ -85,7 +89,7 @@ public void onLeave(PlayerPartyChangeEvent event) {

MatchParticipation participation = players.get(player.getId());
participation.playerLeft();
forfeitManager.startCountdown((Competitor) event.getOldParty(), participation);
forfeitManager.updateCountdown(event.getOldParty());
}

@EventHandler(priority = EventPriority.LOW)
Expand All @@ -95,7 +99,7 @@ public void onMatchEnd(MatchFinishEvent event) {
if (event.getMatch().getDuration().compareTo(ABSENT_MAX) > 0) {
List<UUID> abandonedPlayers =
players.entrySet().stream()
.filter(player -> player.getValue().absentDuration().compareTo(ABSENT_MAX) > 0)
.filter(player -> player.getValue().hasAbandoned())
.map(Map.Entry::getKey)
.collect(Collectors.toList());

Expand All @@ -113,7 +117,7 @@ public void onMatchEnd(MatchFinishEvent event) {
@EventHandler(priority = EventPriority.MONITOR)
public void onMatchEndMonitor(MatchFinishEvent event) {
players.clear();
forfeitManager.clearCheckers();
forfeitManager.clearPolls();
}

@EventHandler(priority = EventPriority.HIGHEST)
Expand Down Expand Up @@ -184,6 +188,10 @@ public boolean canStartCountdown() {
return playerLeftAt != null;
}

public boolean hasAbandoned() {
return absentDuration().compareTo(ABSENT_MAX) > 0;
}

public Duration absentDuration() {
return Duration.ofMillis(
absentLength + (playerLeftAt == null ? 0 : System.currentTimeMillis() - playerLeftAt));
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ full-teams-required: true
allow-requeue: true

# enable the forfeit command and prompt
allow-forfeit: true
forfeit:
enabled: true
after: "300s"

# duration to countdown before starting match
match-start-duration: "300s"
Expand Down

0 comments on commit 963426d

Please sign in to comment.