package tc.oc.pgm.spawns; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.logging.Level; import javax.annotation.Nullable; import javax.inject.Inject; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import org.bukkit.entity.Entity; import org.bukkit.event.EventException; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.player.PlayerInitialSpawnEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.jdom2.Element; import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent; import tc.oc.commons.core.random.RandomUtils; import tc.oc.commons.core.util.ThrowingConsumer; import tc.oc.pgm.events.CompetitorRemoveEvent; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.events.MatchBeginEvent; import tc.oc.pgm.events.MatchEndEvent; import tc.oc.pgm.events.MatchPlayerDeathEvent; import tc.oc.pgm.events.ObserverInteractEvent; import tc.oc.pgm.events.PlayerChangePartyEvent; import tc.oc.pgm.filters.Filter; import tc.oc.pgm.filters.query.IQuery; import tc.oc.pgm.kits.Kit; import tc.oc.pgm.match.Competitor; import tc.oc.pgm.match.Match; import tc.oc.pgm.match.MatchModule; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.MatchScope; import tc.oc.pgm.match.Repeatable; import tc.oc.pgm.spawns.states.Joining; import tc.oc.pgm.spawns.states.Observing; import tc.oc.pgm.spawns.states.State; import tc.oc.pgm.xml.InvalidXMLException; import static tc.oc.commons.core.exception.LambdaExceptionUtils.rethrowConsumer; import static tc.oc.commons.core.util.MapUtils.ifPresent; @ListenerScope(MatchScope.LOADED) public class SpawnMatchModule extends MatchModule implements Listener { private final SpawnModule module; private final Map<MatchPlayer, State> states = new HashMap<>(); private final ListMultimap<MatchPlayer, State> transitions = ArrayListMultimap.create(); private final Set<MatchPlayer> processing = new HashSet<>(); private final Map<Competitor, Spawn> uniqueSpawns = new HashMap<>(); private final Set<Spawn> failedSpawns = new HashSet<>(); @Inject private ObserverToolFactory observerToolFactory; public SpawnMatchModule(Match match, SpawnModule module) { super(match); this.module = module; } public RespawnOptions getRespawnOptions(IQuery query) { return module.respawnOptions.stream().filter(respawnOption -> respawnOption.filter.query(query).equals(Filter.QueryResponse.ALLOW)) .findFirst().orElseThrow(() -> new IllegalStateException("No respawn option could be used")); } public Spawn getDefaultSpawn() { return module.defaultSpawn; } public List<Spawn> getSpawns() { return module.spawns; } public List<Kit> getPlayerKits() { return module.playerKits; } public ObserverToolFactory getObserverToolFactory() { return observerToolFactory; } /** * Return all {@link Spawn}s that the given player is currently allowed to spawn at */ public List<Spawn> getSpawns(MatchPlayer player) { List<Spawn> result = Lists.newArrayList(); for(Spawn spawn : this.getSpawns()) { if(spawn.allows(player)) { result.add(spawn); } } return result; } /** * Return a randomly chosen {@link Spawn} that the given player is currently allowed * to spawn at, or null if none are available. If a team is given, assume the player * will have switched to that team by the time they spawn. */ public @Nullable Spawn chooseSpawn(MatchPlayer player) { Competitor competitor = player.getCompetitor(); if(player.isObserving()) { return getDefaultSpawn(); } else if(competitor != null && uniqueSpawns.containsKey(competitor)) { return uniqueSpawns.get(competitor); } else { List<Spawn> potential = getSpawns(player); potential.removeAll(uniqueSpawns.values()); if(!potential.isEmpty()) { Spawn spawn = RandomUtils.element(match.getRandom(), potential); if(spawn.attributes().exclusive) uniqueSpawns.put(competitor, spawn); return spawn; } else { return null; } } } @Repeatable(scope = MatchScope.LOADED) public void tick() { // Copy states so they can transition without concurrent modification ImmutableMap.copyOf(states).forEach((player, state) -> { state.tick(); processQueuedTransitions(player); }); } public void reportFailedSpawn(Spawn spawn, MatchPlayer player) { if(failedSpawns.add(spawn)) { Element elSpawn = getMatch().getModuleContext().features().definitionNode(spawn); InvalidXMLException ex = new InvalidXMLException("Failed to generate spawn location for " + player.getName(), elSpawn); getMatch().getMap().getLogger().log(Level.SEVERE, ex.getMessage(), ex); } } private void leaveState(MatchPlayer player) { final State state = states.get(player); if(state != null) { logger.fine(player.getName() + " leave " + state); state.leaveState(); states.remove(player); } } private void enterState(MatchPlayer player, State state) { logger.fine(player.getName() + " enter " + state); states.put(player, state); state.enterState(); } private void changeState(MatchPlayer player, @Nullable State state) { leaveState(player); if(state != null) { enterState(player, state); } } private boolean hasQueuedTransitions(MatchPlayer player) { return transitions.containsKey(player); } private void processQueuedTransitions(MatchPlayer player) { // Prevent nested processing of the same player if(processing.add(player)) { try { final List<State> queue = transitions.get(player); while(!queue.isEmpty()) { changeState(player, queue.remove(0)); } } finally { processing.remove(player); } } } public void transition(MatchPlayer player, @Nullable State newState) { logger.fine(player.getName() + " queue " + newState); transitions.put(player, newState); } private <X extends Throwable> void withState(@Nullable MatchPlayer player, ThrowingConsumer<State, X> consumer) throws X { if(player == null) return; ifPresent(states, player, rethrowConsumer(consumer::acceptThrows)); } private void withState(@Nullable Entity bukkit, BiConsumer<MatchPlayer, State> consumer) { final MatchPlayer player = match.getPlayer(bukkit); withState(player, state -> consumer.accept(player, state)); } private void dispatchEvent(@Nullable MatchPlayer player, Consumer<State> consumer) { withState(player, state -> { consumer.accept(state); processQueuedTransitions(player); }); } private void dispatchEvent(@Nullable Entity bukkit, BiConsumer<MatchPlayer, State> consumer) { withState(bukkit, (player, state) -> { consumer.accept(player, state); processQueuedTransitions(player); }); } // Events delegated to States @EventHandler(priority = EventPriority.MONITOR) public void onPartyChange(final PlayerChangePartyEvent event) throws EventException { final MatchPlayer player = event.getPlayer(); if(event.getOldParty() == null) { // Join match event.yield(); if(event.getNewParty().isParticipating()) { enterState(player, new Joining(player)); } else { enterState(player, new Observing(player, true, true)); } } else if(event.getNewParty() == null) { // Leave match leaveState(player); } else { // Party change during match withState(player, state -> { state.onEvent(event); if(hasQueuedTransitions(player)) { // If the party change caused a state transition, leave the old // state before the change, and enter the new state afterward. // The potential danger here is that the player has no spawn state // during the party change, while other events are firing. The // danger is minimized by listening at MONITOR priority. leaveState(player); event.yield(); processQueuedTransitions(player); } }); } } /** Must run before {@link tc.oc.pgm.tracker.trackers.DeathTracker#onPlayerDeath} */ @EventHandler(priority = EventPriority.LOW) public void onVanillaDeath(final PlayerDeathEvent event) { dispatchEvent(event.getEntity(), (player, state) -> state.onEvent(event)); } @EventHandler(priority = EventPriority.HIGH) public void onDeath(final MatchPlayerDeathEvent event) { dispatchEvent(event.getVictim(), state -> state.onEvent(event)); } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onInventoryClick(final InventoryClickEvent event) { dispatchEvent(event.getWhoClicked(), (player, state) -> state.onEvent(event)); } // Listen on HIGH so the picker can handle this first @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void onObserverInteract(final ObserverInteractEvent event) { dispatchEvent(event.getPlayer(), state -> state.onEvent(event)); } @EventHandler public void matchBegin(final MatchBeginEvent event) { // Copy states so they can transition without concurrent modification ImmutableMap.copyOf(states).forEach((player, state) -> { state.onEvent(event); processQueuedTransitions(player); }); } @EventHandler public void matchEnd(final MatchEndEvent event) { // Copy states so they can transition without concurrent modification ImmutableMap.copyOf(states).forEach((player, state) -> { // This event can be fired from inside a party change, so some players may have no party if(player.hasParty()) { state.onEvent(event); processQueuedTransitions(player); } }); } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void playerMove(final CoarsePlayerMoveEvent event) { dispatchEvent(event.getPlayer(), (player, state) -> state.onEvent(event)); } // Events handled for other reasons @EventHandler(priority = EventPriority.MONITOR) public void onInitialSpawn(final PlayerInitialSpawnEvent event) { // Make all joining players spawn in this match's world event.setSpawnLocation(match.getWorld().getSpawnLocation()); } @EventHandler(priority = EventPriority.LOW) public void playerJoin(final PlayerJoinEvent event) { // Add the player to the match if they spawn in this world if(match.getWorld().equals(event.getPlayer().getLocation().getWorld())) { event.getPlayer().setGliding(true); // Fixes client desync if player joins server while gliding match.addPlayer(event.getPlayer()); } } @EventHandler(priority = EventPriority.MONITOR) public void playerQuit(final PlayerQuitEvent event) { match.removePlayer(event.getPlayer()); } @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onCompetitorRemove(CompetitorRemoveEvent event) { // If a competitor is no longer valid, free up its provider Competitor competitor = event.getCompetitor(); if(uniqueSpawns.containsKey(competitor)) { Spawn spawn = uniqueSpawns.get(competitor); // Do not change if persistence is enabled if(!spawn.attributes().persistent) { uniqueSpawns.remove(competitor); } } } }