Skip to content

Commit

Permalink
Add player forfeiting with timed start
Browse files Browse the repository at this point in the history
  • Loading branch information
Pugzy authored Mar 28, 2021
1 parent 147100f commit 4af4bdb
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 40 deletions.
3 changes: 3 additions & 0 deletions src/main/java/rip/bolt/ingame/Ingame.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import rip.bolt.ingame.api.APIManager;
import rip.bolt.ingame.commands.ForfeitCommands;
import rip.bolt.ingame.commands.RankedAdminCommands;
import rip.bolt.ingame.commands.RequeueCommands;
import rip.bolt.ingame.ranked.RankedManager;
Expand Down Expand Up @@ -46,6 +47,8 @@ public void onEnable() {
BasicBukkitCommandGraph g = new BasicBukkitCommandGraph(new CommandModule());
DispatcherNode node = g.getRootDispatcherNode();
node.registerCommands(new RequeueCommands());
node.registerCommands(new ForfeitCommands(rankedManager));

DispatcherNode subNode = node.registerNode("ingame");
subNode.registerCommands(new RankedAdminCommands(rankedManager));
new CommandExecutor(this, g).register();
Expand Down
53 changes: 53 additions & 0 deletions src/main/java/rip/bolt/ingame/commands/ForfeitCommands.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package rip.bolt.ingame.commands;

import static tc.oc.pgm.lib.net.kyori.adventure.text.Component.text;

import net.md_5.bungee.api.ChatColor;
import rip.bolt.ingame.config.AppData;
import rip.bolt.ingame.ranked.ForfeitManager;
import rip.bolt.ingame.ranked.RankedManager;
import tc.oc.pgm.api.match.Match;
import tc.oc.pgm.api.match.MatchPhase;
import tc.oc.pgm.api.party.Competitor;
import tc.oc.pgm.api.player.MatchPlayer;
import tc.oc.pgm.lib.app.ashcon.intake.Command;
import tc.oc.pgm.lib.app.ashcon.intake.CommandException;

public class ForfeitCommands {

private final RankedManager ranked;
private final ForfeitManager forfeits;

public ForfeitCommands(RankedManager ranked) {
this.ranked = ranked;
this.forfeits = this.ranked.getPlayerWatcher().getForfeitManager();
}

@Command(
aliases = {"forfeit", "ff"},
desc = "Accept that you have no chance of winning")
public void forfeit(MatchPlayer sender, Match match) throws CommandException {
if (!AppData.allowForfeit())
throw new CommandException(
ChatColor.RED + "The forfeit command is not enabled on this server.");

if (match.getPhase() != MatchPhase.RUNNING)
throw new CommandException(ChatColor.RED + "You may only run this command during a match.");

if (!(sender.getParty() instanceof Competitor))
throw new 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())
throw new CommandException(
ChatColor.RED + "You may only run this command when your team has lost a player.");

if (checker.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);
}
}
4 changes: 4 additions & 0 deletions src/main/java/rip/bolt/ingame/config/AppData.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ 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 Duration matchStartDuration() {
return parseDuration(Ingame.get().getConfig().getString("match-start-duration", "300s"));
}
Expand Down
178 changes: 178 additions & 0 deletions src/main/java/rip/bolt/ingame/ranked/ForfeitManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package rip.bolt.ingame.ranked;

import static tc.oc.pgm.lib.net.kyori.adventure.text.Component.text;

import dev.pgm.events.Tournament;
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;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
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.VictoryCondition;
import tc.oc.pgm.api.player.MatchPlayer;
import tc.oc.pgm.lib.net.kyori.adventure.text.format.NamedTextColor;
import tc.oc.pgm.result.CompetitorVictoryCondition;
import tc.oc.pgm.result.TieVictoryCondition;
import tc.oc.pgm.teams.Team;

public class ForfeitManager {

private final Map<Competitor, ForfeitCheck> checks = new HashMap<>();

public ForfeitManager() {}

@Nullable
public ForfeitCheck getChecker(Competitor team) {
return checks.get(team);
}

public void clearCheckers() {
checks.clear();
}

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;
}

// Do nothing if new check is longer than currently running one
if (existingCheck.scheduledFuture.getDelay(TimeUnit.SECONDS)
< participation.absentDuration().getSeconds()) return;

existingCheck.stopTimer();
}

checks.put(team, new ForfeitCheck(team, participation));
}

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
TournamentTeamManager teamManager = Tournament.get().getTeamManager();
teamManager
.tournamentTeamPlayer(participation.uuid)
.map(team -> team.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)));
}

public static class ForfeitCheck {

private final Competitor team;
private final PlayerWatcher.MatchParticipation participation;
private final Set<UUID> voted = new HashSet<>();

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

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

startTimer();
}

public Competitor getTeam() {
return team;
}

public Set<UUID> getVoted() {
return voted;
}

public boolean isVotable() {
return votable;
}

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

Duration length = PlayerWatcher.ABSENT_MAX.minus(participation.absentDuration());
scheduledFuture =
team.getMatch()
.getExecutor(MatchScope.RUNNING)
.schedule(this::broadcast, length.getSeconds(), TimeUnit.SECONDS);
}
}

private void stopTimer() {
if (scheduledFuture.isDone() || scheduledFuture.isCancelled()) return;
scheduledFuture.cancel(true);
}

private void broadcast() {
votable = true;
team.sendMessage(Messages.forfeit());
check();
}

public void addVote(MatchPlayer player) {
voted.add(player.getId());
check();
}

public void check() {
if (hasPassed()) endMatch();
}

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
>= Math.min(
team instanceof Team ? ((Team) team).getMaxPlayers() - 1 : 0,
team.getPlayers().size());
}

public void endMatch() {
Match match = team.getMatch();
match.sendMessage(
team.getName().append(text(" has voted to forfeit the match.", NamedTextColor.WHITE)));

// Create victory condition for other team or tie
VictoryCondition victoryCondition =
match.getCompetitors().stream()
.filter(competitor -> !competitor.equals(team))
.findFirst()
.<VictoryCondition>map(CompetitorVictoryCondition::new)
.orElseGet(TieVictoryCondition::new);

match.addVictoryCondition(victoryCondition);
match.finish(null);
}
}
}
Loading

0 comments on commit 4af4bdb

Please sign in to comment.