package tc.oc.pgm.restart; import java.time.Duration; import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; import com.google.common.eventbus.Subscribe; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TranslatableComponent; import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.server.ServerSuspendEvent; import tc.oc.api.docs.virtual.ServerDoc; import tc.oc.commons.core.chat.Component; import tc.oc.commons.core.logging.Loggers; import tc.oc.commons.core.plugin.PluginFacet; import tc.oc.commons.core.restart.CancelRestartEvent; import tc.oc.commons.core.restart.RequestRestartEvent; import tc.oc.commons.core.restart.RestartManager; import tc.oc.commons.core.util.Comparables; import tc.oc.pgm.countdowns.MatchCountdown; import tc.oc.pgm.events.ConfigLoadEvent; import tc.oc.pgm.events.MatchEndEvent; import tc.oc.pgm.events.MatchLoadEvent; import tc.oc.pgm.events.MatchUnloadEvent; import tc.oc.pgm.events.PlayerPartyChangeEvent; import tc.oc.pgm.match.Match; import tc.oc.pgm.match.MatchManager; /** * Listens for {@link RequestRestartEvent} and defers it until after the current match * and the restart countdown. Also listens for {@link CancelRestartEvent} and handles * it appropriately. * * Also keeps count of matches and requests a restart from {@link RestartManager} after * the configured limit. */ @Singleton public class RestartListener implements PluginFacet, Listener { private final Logger logger; private final RestartManager restartManager; private final MatchManager mm; private final Server server; private AutoRestartConfiguration config; private @Nullable Integer matchLimit; private @Nullable RequestRestartEvent.Deferral deferral; private boolean startingCountdown; @Inject RestartListener(Loggers loggers, RestartManager restartManager, MatchManager mm, Server server, AutoRestartConfiguration config) { this.restartManager = restartManager; this.logger = loggers.get(getClass()); this.mm = mm; this.server = server; this.config = config; this.matchLimit = config.matchLimit() > 0 ? config.matchLimit() : null; } @EventHandler public void configure(ConfigLoadEvent event) { this.config = new AutoRestartConfiguration(event.getConfig()); } private boolean isCountdownRunning(Match match) { return match.countdowns().getCountdown() instanceof RestartCountdown; } private void startCountdown(Match match, Duration duration) { try { startingCountdown = true; match.ensureNotRunning(); match.countdowns().start(new RestartCountdown(match), duration); } finally { startingCountdown = false; } } /** * Start a countdown of the given duration after cancelling any existing one */ private void forceCountdown(Match match, Duration duration) { match.countdowns().cancelAll(); logger.info("Starting countdown from " + duration); startCountdown(match, duration); } private void ensureCountdown(Match match) { if(!isCountdownRunning(match) && !startingCountdown) { logger.info("Starting countdown from " + config.time()); startCountdown(match, config.time()); } } private void cancelCountdown(Match match) { if(isCountdownRunning(match)) { logger.info("Cancelling countdown"); match.countdowns().cancelAll(); } } public boolean willRestart(Match match) { return startingCountdown || canResumeRestart(match); } private boolean canResumeRestart(Match match) { if(deferral == null) return false; final int priority = deferral.request().priority(); if(priority >= ServerDoc.Restart.Priority.HIGH) { // Restart immediately return true; } else if(priority >= ServerDoc.Restart.Priority.NORMAL) { // Restart after current match ends return match.canAbort(); } else { // Restart when server is empty return match.getPlayers().isEmpty(); } } private void checkCountdown(Match match) { if(deferral != null) { if(canResumeRestart(match)) { ensureCountdown(match); } else { cancelCountdown(match); } } } /** * When a restart is requested, let it restart immediately if the server if empty, * otherwise defer the restart and ensure that a countdown is running. */ @Subscribe public void onRequestRestart(RequestRestartEvent event) { if(!server.isSuspended()) { logger.info("Deferring restart"); deferral = event.defer(getClass().getName()); checkCountdown(mm.needCurrentMatch()); } } /** * When restart is cancelled, cancel any countdown and discard our deferral */ @Subscribe public void onCancelRestart(CancelRestartEvent event) { cancelCountdown(mm.needCurrentMatch()); deferral = null; } @EventHandler public void onSuspend(ServerSuspendEvent event) { if(deferral != null) { deferral.resume(); deferral = null; } } @EventHandler public void onMatchLoad(MatchLoadEvent event) { checkCountdown(event.getMatch()); } /** * When match ends, start a countdown if a restart is already requested, * otherwise check for the match limit and request a restart if needed. * This listens on LOW priority so that it takes priority over map cycling. */ @EventHandler(priority = EventPriority.LOW) public void onMatchEnd(MatchEndEvent event) { if(startingCountdown) return; if(deferral != null) { checkCountdown(event.getMatch()); } else if(matchLimit != null && event.getMatch().serialNumber() >= matchLimit) { restartManager.requestRestart("Reached match limit (" + event.getMatch().serialNumber() + " >= " + matchLimit + ")"); } } @EventHandler public void onMatchUnload(MatchUnloadEvent event) { if(deferral != null && deferral.request().priority() >= ServerDoc.Restart.Priority.NORMAL) { logger.info("Resuming restart because the match unloaded"); deferral.resume(); deferral = null; } } /** * If the match empties out while a restart is queued, end the match */ @EventHandler public void onPartyChange(PlayerPartyChangeEvent event) { checkCountdown(event.getMatch()); } /** * Request a restart from {@link RestartManager} and defer it with * a countdown of the given duration. */ public void queueRestart(Match match, Duration duration, String reason) { restartManager.requestRestart(reason); forceCountdown(match, duration); } /** * Set the match limit restart to the given number of matches from now, * or disable the limit if given null; * @return The number of matches from now when the server will restart */ public @Nullable Integer restartAfterMatches(Match match, @Nullable Integer matches) { matchLimit = matches == null ? null : match.serialNumber() + matches; return matchLimit == null ? null : matchLimit - match.serialNumber(); } public class RestartCountdown extends MatchCountdown { public RestartCountdown(Match match) { super(match); } @Override public BaseComponent barText(Player viewer) { if(Comparables.greaterThan(remaining, Duration.ZERO)) { return new Component(new TranslatableComponent("broadcast.serverRestart.message", secondsRemaining(ChatColor.DARK_RED)), ChatColor.AQUA); } else { return new Component(new TranslatableComponent("broadcast.serverRestart.kickMsg"), ChatColor.RED); } } @Override public void onEnd(Duration total) { super.onEnd(total); if(deferral != null) { logger.info("Resuming restart after countdown"); deferral.resume(); deferral = null; } } } }