package tc.oc.pgm.cycle;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
import javax.inject.Inject;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
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.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import tc.oc.api.bukkit.users.OnlinePlayers;
import tc.oc.api.docs.Game;
import tc.oc.api.docs.Server;
import tc.oc.api.games.GameStore;
import tc.oc.api.games.TicketService;
import tc.oc.api.message.types.CycleRequest;
import tc.oc.api.servers.ServerStore;
import tc.oc.commons.bukkit.chat.Audiences;
import tc.oc.commons.bukkit.format.GameFormatter;
import tc.oc.commons.bukkit.teleport.PlayerServerChanger;
import tc.oc.commons.core.chat.Audience;
import tc.oc.commons.core.chat.BlankComponent;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.commands.CommandBinder;
import tc.oc.commons.core.util.Comparables;
import tc.oc.pgm.countdowns.MatchCountdown;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.MatchEndEvent;
import tc.oc.pgm.events.PlayerPartyChangeEvent;
import tc.oc.pgm.map.PGMMap;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchExecutor;
import tc.oc.pgm.match.MatchManager;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.match.Repeatable;
import tc.oc.pgm.match.inject.MatchModuleFixtureManifest;
import tc.oc.pgm.module.ModuleDescription;
import tc.oc.pgm.restart.RestartListener;
import tc.oc.time.Time;
@ModuleDescription(name = "Cycle")
@ListenerScope(MatchScope.LOADED)
public class CycleMatchModule extends MatchModule implements Listener {
public static class Manifest extends MatchModuleFixtureManifest<CycleMatchModule> {
@Override protected void configure() {
super.configure();
new CommandBinder(binder())
.register(CycleCommands.class);
}
}
@Inject private MatchManager mm;
@Inject private RestartListener restartListener;
@Inject private TicketService ticketService;
@Inject private Server localServer;
@Inject private PlayerServerChanger serverChanger;
@Inject private CycleConfig config;
@Inject private OnlinePlayers onlinePlayers;
@Inject private GameStore games;
@Inject private ServerStore servers;
@Inject private Audiences audiences;
@Inject private GameFormatter gameFormatter;
@Inject private MatchExecutor matchExecutor;
private boolean autoCycle = true;
public CycleConfig getConfig() {
return config;
}
public boolean isCycling() {
return match.countdowns().getCountdown() instanceof CycleCountdown;
}
public void cycleNow() {
cycleNow(null);
}
public void cycleNow(PGMMap map) {
startCountdown(Duration.ZERO, map);
}
public void startCountdown(@Nullable Duration duration) {
if(duration == null) duration = config.countdown();
match.ensureNotRunning();
if(Duration.ZERO.equals(duration)) {
requestCycle(false);
} else {
getMatch().countdowns().start(new CycleCountdown(mm, getMatch()), duration);
}
}
public void startCountdown(@Nullable Duration duration, PGMMap nextMap) {
mm.setNextMap(nextMap);
startCountdown(duration);
}
private void requestCycle(boolean retryRotation) {
if(localServer.game_id() != null) {
final PGMMap nextMap = mm.getNextMap();
matchExecutor.callback(
ticketService.requestCycle(
new CycleRequest() {
@Override public String server_id() {
return localServer._id();
}
@Override public String map_id() {
return nextMap.getDocument()._id();
}
@Override public int min_players() {
return nextMap.getContext().playerLimits().lowerEndpoint();
}
@Override public int max_players() {
return nextMap.getContext().playerLimits().upperEndpoint();
}
}
),
(match0, reply) -> {
final List<ListenableFuture<?>> quitFutures = new ArrayList<>();
if(localServer.game_id() != null) {
for(Map.Entry<UUID, String> entry : reply.destinations().entrySet()) {
final String serverId = entry.getValue();
if(localServer._id().equals(serverId)) continue;
final Player player = onlinePlayers.find(entry.getKey());
if(player == null) continue;;
final Audience audience = audiences.get(player);
final Game game = games.byId(localServer.game_id());
if(serverId == null) {
quitFutures.add(serverChanger.sendPlayerToLobby(player, true));
} else {
audience.sendMessage(gameFormatter.rejoining(game));
quitFutures.add(serverChanger.sendPlayerToServer(player, servers.byId(serverId), true));
}
}
}
if(quitFutures.isEmpty()) {
doCycle(retryRotation);
} else {
// Cycle after all requeued players are gone
matchExecutor.callback(
Futures.allAsList(quitFutures),
(match1, list) -> doCycle(retryRotation)
);
}
}
);
} else {
doCycle(retryRotation);
}
}
private void doCycle(boolean retryRotation) {
mm.cycleToNext(getMatch(), retryRotation, false);
}
@EventHandler
public void onPartyChange(PlayerPartyChangeEvent event) {
if(match.isRunning() && match.getParticipatingPlayers().isEmpty()) {
checkEmptyServerCycle();
}
}
@EventHandler
public void onMatchEnd(MatchEndEvent event) {
checkMatchEndCycle();
}
@Repeatable(scope = MatchScope.LOADED, interval = @Time(seconds = 1))
public void periodicCheck() {
// Check auto-cycle conditions periodically, so we don't get stuck when something fails
checkMatchEndCycle();
checkEmptyServerCycle();
}
private boolean canAutoCycle() {
return autoCycle &&
!isCycling() &&
!restartListener.willRestart(match);
}
private void checkMatchEndCycle() {
if(canAutoCycle() && match.isFinished()) {
final CycleConfig.Auto autoConfig = config.matchEnd();
if(autoConfig.enabled()) {
startCountdown(autoConfig.countdown());
}
}
}
private void checkEmptyServerCycle() {
if(canAutoCycle() && match.isRunning() && match.getParticipatingPlayers().isEmpty()) {
final CycleConfig.Auto autoConfig = config.matchEmpty();
if(autoConfig.enabled()) {
logger.info("Cycling due to empty match");
startCountdown(autoConfig.countdown());
}
}
}
public class CycleCountdown extends MatchCountdown {
protected final MatchManager mm;
public CycleCountdown(MatchManager mm, Match match) {
super(match);
this.mm = mm;
}
@Override
public BaseComponent barText(Player viewer) {
PGMMap nextMap = mm.getNextMap();
if(nextMap == null || !nextMap.isLoaded()) return BlankComponent.INSTANCE;
BaseComponent mapName = new Component(nextMap.getInfo().name, ChatColor.AQUA);
if(Comparables.greaterThan(remaining, Duration.ZERO)) {
return new Component(new TranslatableComponent("countdown.cycle.message", mapName, secondsRemaining(ChatColor.DARK_RED)), ChatColor.DARK_AQUA);
} else {
return new Component(new TranslatableComponent("countdown.cycle.complete", mapName), ChatColor.DARK_AQUA);
}
}
@Override
public void onCancel(Duration remaining, Duration total, boolean manual) {
super.onCancel(remaining, total, manual);
if(manual && match.isFinished()) {
CycleMatchModule.this.autoCycle = false;
}
}
@Override
public void onEnd(Duration total) {
super.onEnd(total);
requestCycle(true);
}
}
}