package tc.oc.pgm.join; import java.util.LinkedHashSet; import java.util.Set; import javax.annotation.Nullable; import javax.inject.Inject; import com.google.common.eventbus.EventBus; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.TranslatableComponent; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import tc.oc.api.bukkit.users.BukkitUserStore; import tc.oc.api.bukkit.users.OnlinePlayers; import tc.oc.api.docs.Arena; import tc.oc.api.docs.Server; import tc.oc.api.docs.Ticket; import tc.oc.api.games.TicketStore; import tc.oc.api.model.ModelDispatcher; import tc.oc.api.model.ModelListener; import tc.oc.commons.bukkit.event.PlayerServerChangeEvent; import tc.oc.commons.bukkit.teleport.Teleporter; import tc.oc.commons.bukkit.ticket.TicketBooth; import tc.oc.commons.core.chat.Component; import tc.oc.commons.core.commands.CommandBinder; import tc.oc.commons.core.formatting.PeriodFormats; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.events.MatchPlayerAddEvent; import tc.oc.pgm.events.MatchPreCommitEvent; import tc.oc.pgm.match.Competitor; import tc.oc.pgm.match.MatchModule; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.MatchScope; import tc.oc.pgm.match.Party; import tc.oc.pgm.match.inject.MatchModuleFixtureManifest; import tc.oc.pgm.module.ModuleDescription; import tc.oc.pgm.teams.TeamMatchModule; import tc.oc.pgm.timelimit.TimeLimitMatchModule; @ModuleDescription(name = "Join") @ListenerScope(MatchScope.LOADED) public class JoinMatchModule extends MatchModule implements Listener, JoinHandler, ModelListener { public static class Manifest extends MatchModuleFixtureManifest<JoinMatchModule> { @Override protected void configure() { super.configure(); new CommandBinder(binder()) .register(JoinCommands.class); } } public static final String JOIN_PERMISSION = "pgm.join"; public static final String JOIN_FULL_PERMISSION = "pgm.join.full"; public static final String PRIORITY_KICK_PERMISSION = JOIN_FULL_PERMISSION; public static final String JOIN_OBSERVERS_PERMISSION = "pgm.join.choose.observing"; @Inject private JoinConfiguration config; @Inject private QueuedParticipants queuedParticipants; @Inject private Server localServer; @Inject private BukkitUserStore userStore; @Inject private OnlinePlayers onlinePlayers; @Inject private EventBus eventBus; @Inject private Teleporter teleporter; @Inject private TicketStore tickets; @Inject private TicketBooth ticketBooth; @Inject private ModelDispatcher modelDispatcher; private final Set<JoinHandler> handlers = new LinkedHashSet<>(); @Override public void load() { super.load(); eventBus.register(this); getMatch().addParty(queuedParticipants); ticketBooth.setPlayHandler(playHandler); modelDispatcher.subscribe(this); } @Override public void unload() { modelDispatcher.unsubscribe(this); ticketBooth.removePlayHandler(playHandler); eventBus.unregister(this); super.unload(); } public void registerHandler(JoinHandler handler) { handlers.add(handler); } public boolean canJoinFull(MatchPlayer joining) { return !config.capacity() || (config.overfill() && joining.getBukkit().hasPermission(JOIN_FULL_PERMISSION)); } public boolean canPriorityKick(MatchPlayer joining) { return config.priorityKick() && joining.getBukkit().hasPermission(PRIORITY_KICK_PERMISSION) && !getMatch().hasStarted(); } public boolean canJoinMid() { return config.midMatch(); } public boolean isRemoteJoin() { return localServer.game_id() != null; } @Override public JoinResult queryJoin(MatchPlayer joining, JoinRequest request) { // Player does not have permission to voluntarily join if(!joining.getBukkit().hasPermission(JOIN_PERMISSION)) { return JoinDenied.error("command.gameplay.join.joinDenied"); } // If mid-match join is disabled, player cannot join for the first time after the match has started if(!canJoinMid() && getMatch().isCommitted() && !getMatch().hasEverParticipated(joining.getPlayerId())) { return JoinDenied.friendly("command.gameplay.join.matchStarted"); } if(getMatch().isFinished()) { // This message should NOT look like an error, because remotely joining players will see it often. return JoinDenied.friendly("command.gameplay.join.matchFinished"); } JoinResult best = new JoinDenied(false, true, new TranslatableComponent("command.gameplay.join.notSupported")) { @Override public boolean isFallback() { return true; } }; for(JoinHandler handler : handlers) { final JoinResult result = handler.queryJoin(joining, request); if(result != null && result.compareTo(best) < 0) best = result; } return best; } @Override public boolean join(MatchPlayer joining, JoinRequest request, JoinResult result) { result.output().forEach(joining::sendMessage); if(result instanceof JoinQueued) { queueToJoin(joining); return true; } if(!result.isAllowed()) return true; for(JoinHandler handler : handlers) { if(handler.join(joining, request, result)) return true; } return false; } public boolean requestJoin(MatchPlayer joining, JoinRequest request) { final Player joiner = joining.getBukkit(); if(isRemoteJoin() && request.method() != JoinMethod.REMOTE && !isLocalParticipant(joining)) { ticketBooth.playLocalGame(joiner); return true; } else { final Arena arena = ticketBooth.localArena(); if(arena == null || !arena.equals(ticketBooth.currentArena(joiner))) { ticketBooth.leaveGame(joiner, false); } return join(joining, request, queryJoin(joining, request)); } } public boolean requestJoin(MatchPlayer joining, JoinMethod method, @Nullable Competitor competitor) { return requestJoin(joining, new JoinRequest(method, competitor)); } public boolean requestJoin(MatchPlayer joining, JoinMethod method) { return requestJoin(joining, method, null); } public boolean observe(MatchPlayer leaving) { final Party observers = getMatch().getDefaultParty(); leaving.sendMessage(new TranslatableComponent("team.join", observers.getComponentName())); return getMatch().setPlayerParty(leaving, observers); } public boolean requestObserve(MatchPlayer leaving) { if(cancelQueuedJoin(leaving)) return true; if(leaving.isObservingType()) { leaving.sendWarning(new TranslatableComponent("command.gameplay.leave.alreadyOnObservers"), false); return false; } if(isRemoteJoin()) { ticketBooth.leaveGame(leaving.getBukkit(), false); } if(!leaving.getBukkit().hasPermission(JOIN_OBSERVERS_PERMISSION)) { leaving.sendWarning(new TranslatableComponent("command.gameplay.leave.leaveDenied"), false); return false; } if(config.commitPlayers() && leaving.isCommitted()) { leaving.sendWarning(new TranslatableComponent("command.gameplay.leave.leaveDenied"), false); return false; } return observe(leaving); } public QueuedParticipants getQueuedParticipants() { return queuedParticipants; } public boolean isQueuedToJoin(MatchPlayer joining) { return joining.inParty(queuedParticipants); } public boolean queueToJoin(MatchPlayer joining) { boolean joined = getMatch().setPlayerParty(joining, queuedParticipants); if(joined) { joining.sendMessage(new TranslatableComponent("ffa.join")); } joining.sendMessage(new Component(new TranslatableComponent("team.join.deferred.request"), ChatColor.YELLOW)); // Always show this message if(getMatch().hasMatchModule(TeamMatchModule.class)) { // If they are joining a team, show them a scary warning about leaving the match joining.sendMessage( new Component(new TranslatableComponent( "team.join.forfeitWarning", new Component(new TranslatableComponent("team.join.forfeitWarning.emphasis.warning"), ChatColor.RED), new Component(new TranslatableComponent("team.join.forfeitWarning.emphasis.playUntilTheEnd"), ChatColor.RED), new Component(new TranslatableComponent("team.join.forfeitWarning.emphasis.doubleLoss"), ChatColor.RED), new Component(new TranslatableComponent("team.join.forfeitWarning.emphasis.suspension"), ChatColor.RED) ), ChatColor.DARK_RED) ); TimeLimitMatchModule tlmm = getMatch().getMatchModule(TimeLimitMatchModule.class); if(tlmm != null && tlmm.getTimeLimit() != null) { joining.sendMessage(new Component(new TranslatableComponent( "team.join.forfeitWarning.timeLimit", new Component(PeriodFormats.briefNaturalPrecise(tlmm.getTimeLimit().getDuration()), ChatColor.AQUA), new Component("/" + JoinCommands.OBSERVE_COMMAND, ChatColor.GOLD) ), ChatColor.DARK_RED, ChatColor.BOLD)); } else { joining.sendMessage(new Component(new TranslatableComponent( "team.join.forfeitWarning.noTimeLimit", new Component("/" + JoinCommands.OBSERVE_COMMAND, ChatColor.GOLD) ), ChatColor.DARK_RED, ChatColor.BOLD)); } } return joined; } public boolean cancelQueuedJoin(MatchPlayer joining) { if(!isQueuedToJoin(joining)) return false; if(getMatch().setPlayerParty(joining, getMatch().getDefaultParty())) { joining.sendMessage(new Component(new TranslatableComponent("team.join.deferred.cancel"), ChatColor.YELLOW)); return true; } else { return false; } } @Override public void queuedJoin(QueuedParticipants queue) { // Give all handlers a chance to bulk join for(JoinHandler handler : handlers) { if(queue.getPlayers().isEmpty()) break; handler.queuedJoin(queue); } // Send any leftover players to obs for(MatchPlayer joining : queue.getOrderedPlayers()) { getMatch().setPlayerParty(joining, getMatch().getDefaultParty()); } } @EventHandler(priority = EventPriority.MONITOR) public void onMatchCommit(MatchPreCommitEvent event) { queuedJoin(queuedParticipants); } @EventHandler public void onServerChange(PlayerServerChangeEvent event) { MatchPlayer player = getMatch().getPlayer(event.getPlayer()); if(config.commitPlayers() && player != null && player.isCommitted() && !getMatch().isFinished()) { event.setCancelled(true, new TranslatableComponent("engagement.committed")); } } private boolean isLocalParticipant(MatchPlayer player) { return isLocalParticipant(tickets.tryUser(player.getPlayerId())); } private boolean isLocalParticipant(@Nullable Ticket ticket) { return ticket != null && localServer._id().equals(ticket.server_id()); } @EventHandler public void onLogin(MatchPlayerAddEvent event) { if(config.commitPlayers() && getMatch().isCommitted() && !getMatch().isFinished()) { final Competitor competitor = getMatch().getLastCompetitor(event.getPlayerId()); if(competitor != null) { // Committed player is reconnecting getMatch().setPlayerParty(event.getPlayer(), competitor); return; } } if(isRemoteJoin() && isLocalParticipant(event.getPlayer())) { // Player has a remote ticket to play on this server requestJoin(event.getPlayer(), JoinMethod.REMOTE); } } @HandleModel public void ticketUpdated(@Nullable Ticket before, @Nullable Ticket after, Ticket latest) { if(isRemoteJoin()) match.player(latest.user()).ifPresent(player -> { final boolean isPlaying = isLocalParticipant(after); if(!player.isParticipatingType() && isPlaying) { requestJoin(player, JoinMethod.REMOTE); } else if(player.isParticipatingType() && !isPlaying) { observe(player); } }); }; private final TicketBooth.PlayHandler playHandler = player -> { final MatchPlayer mp = match.getPlayer(player); if(mp != null) { requestJoin(mp, JoinMethod.USER); return true; } return false; }; }