package tc.oc.pgm.match; import java.net.URL; import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.UUID; import java.util.logging.Logger; import java.util.stream.Stream; import javax.annotation.Nullable; import com.google.common.collect.BiMap; import com.google.common.collect.Iterables; import com.google.common.collect.Range; import com.google.common.collect.SetMultimap; import net.md_5.bungee.api.chat.BaseComponent; import org.bukkit.Server; import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.event.Event; import org.bukkit.event.Listener; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; import java.time.Duration; import java.time.Instant; import tc.oc.api.docs.PlayerId; import tc.oc.api.docs.UserId; import tc.oc.commons.core.chat.Audience; import tc.oc.commons.core.inject.InjectionScopable; import tc.oc.commons.core.random.Entropy; import tc.oc.commons.core.util.ArrayUtils; import tc.oc.commons.core.util.PunchClock; import tc.oc.commons.core.util.Streams; import tc.oc.pgm.countdowns.SingleCountdownContext; import tc.oc.pgm.features.Feature; import tc.oc.pgm.features.FeatureDefinitionContext; import tc.oc.pgm.features.FeatureFactory; import tc.oc.pgm.features.MatchFeatureContext; import tc.oc.pgm.filters.Filterable; import tc.oc.pgm.filters.query.IMatchQuery; import tc.oc.pgm.map.MapInfo; import tc.oc.pgm.map.MapModuleContext; import tc.oc.pgm.map.PGMMap; import tc.oc.pgm.match.inject.MatchBinders; import tc.oc.pgm.match.inject.MatchScoped; import tc.oc.pgm.module.ModuleLoadException; import tc.oc.pgm.time.TickClock; import tc.oc.pgm.time.TickTime; public interface Match extends Audience, IMatchQuery, Filterable<IMatchQuery>, MatchPlayerFinder, InjectionScopable<MatchScoped> { /** * Unique ID for this match */ String getId(); /** * Readable identifier for this match */ default String getSlug() { return createSlug(serialNumber()); } static String createSlug(int serialNumber) { return "match-" + serialNumber; } /** * A unique serial number assigned to this match. * * This will be roughly the number of matches that have loaded since server startup. */ int serialNumber(); /** * URL of the match info page */ URL getUrl(); @Deprecated // use your own logger Logger getLogger(); @Deprecated // @Inject me PGMMap getMap(); @Deprecated // @Inject me default MapInfo getMapInfo() { return getMap().getInfo(); } @Deprecated // @Inject me Plugin getPlugin(); @Deprecated // @Inject me World getWorld(); @Deprecated // @Inject me Server getServer(); @Deprecated // @Inject me PluginManager getPluginManager(); @Override default Match getMatch() { return this; } boolean isMainThread(); @Deprecated // @Inject me MatchFeatureContext features(); @Override default <T extends Feature<?>> T feature(FeatureFactory<T> factory) { return features().get(factory); } @Override default Optional<? extends Filterable<? super IMatchQuery>> filterableParent() { return Optional.empty(); } @Override default Stream<? extends Filterable<? extends IMatchQuery>> filterableChildren() { return parties(); } @Override default <R extends Filterable<?>> Stream<? extends R> filterableDescendants(Class<R> type) { Stream<R> result = Stream.of(); if(type.isAssignableFrom(Match.class)) { result = Stream.concat(result, Stream.of((R) this)); } if(Party.class.isAssignableFrom(type)) { result = Stream.concat(result, Streams.instancesOf(parties(), type)); } if(type.isAssignableFrom(MatchPlayer.class)) { result = Stream.concat(result, (Stream<? extends R>) players()); } return result; } @Deprecated // @Inject me MatchScheduler getScheduler(MatchScope scope); @Deprecated // @Inject me TickClock getClock(); @Deprecated // @Inject TickClock default Instant getInstantNow() { return getClock().now().instant; } /** * Return a {@link Entropy} that changes state between ticks, * and remains in a constant state for the duration of each tick. */ Entropy entropyForTick(); @Deprecated // use Entropy Random getRandom(); @Deprecated // Does nothing special, just use EventBus void callEvent(Event event); /** * Register an event {@link Listener} scoped to this match. * * The listener will only receive events from this match, * and it will be automatically unregistered when the match unloads. * * The {@link MatchScope} associated with the listener determines when * event handlers are called. An exception will be thrown if no scope * can be derived. * * @see MatchBinders#matchListener the preferred way to do this */ void registerEvents(Listener listener); /** * Unregister a {@link Listener} that was previously passed to {@link #registerEvents}. */ void unregisterEvents(Listener listener); /** * Register any {@link Repeatable} methods found on the given object * to be called during this match. * * The {@link MatchScope} associated with the object/method determines when * it is called. An exception will be thrown if no scope can be derived. */ void registerRepeatable(Object object); /** * Unregister an object that was previously passed to {@link #registerRepeatable}. */ void unregisterRepeatable(Object object); /** * Register {@link Repeatable} methods on the given object, and also * register it for events if it is a {@link Listener}. * * @see #registerEvents * @see #registerRepeatable */ void registerEventsAndRepeatables(Object thing); /** * Return the {@link MapModuleContext} that was used to load this match. * * This may not be the current context in the {@link PGMMap} object, if * it has just reloaded, for example (that's why it needs to be cached). */ @Deprecated // @Inject me MapModuleContext getModuleContext(); @Deprecated // @Inject me FeatureDefinitionContext featureDefinitions(); @Deprecated // @Inject me @Nullable <T extends MatchModule> T getMatchModule(Class<T> matchModuleClass); @Deprecated // @Inject me boolean hasMatchModule(Class<? extends MatchModule> matchModuleClass); @Deprecated // @Inject me <T extends MatchModule> T needMatchModule(Class<T> matchModuleClass); @Deprecated // @Inject me SingleCountdownContext countdowns(); /** * True if this Match is loaded. This is only set true after the entire loading * process is complete i.e. all modules are loaded, events are called etc. Likewise, * it is set false before the unloading process starts. It is safe to call this method * from any thread. */ boolean isLoaded(); /** * The time that this match started loading (it may not have finished yet). * This is never null, as it is set immediately on match construction. */ Instant getLoadTime(); /** * True if this match has completely unloaded. */ boolean isUnloaded(); /** * The time that this match started unloading (it may not have finished yet), * or null if the match has not started unloading yet. */ @Nullable Instant getUnloadTime(); /** * Load the match */ void load() throws ModuleLoadException; /** * Unload the match */ void unload(); /** * The time that this match last transitioned into the given state, * or null if the match has never been in that state. * * @see #matchState */ @Nullable Instant getStateChangeTime(MatchState state); /** * Is this match currently in the given state? * * @see #matchState */ default boolean inState(MatchState state) { return matchState() == state; } /** * Is this match currently in the given scope? */ default boolean inScope(MatchScope scope) { switch(scope) { case LOADED: return !isUnloaded(); // This scope includes (un)loading case RUNNING: return isRunning(); default: throw new IllegalStateException(); } } default boolean isStarting() { return inState(MatchState.Starting); } default boolean isRunning() { return inState(MatchState.Running); } default boolean isFinished() { return inState(MatchState.Finished); } default boolean hasStarted() { return inState(MatchState.Running) || inState(MatchState.Finished); } default boolean canTransitionTo(MatchState state) { return matchState().canTransitionTo(state); } default boolean canBeIn(MatchState state) { return inState(state) || canTransitionTo(state); } /** * Is this match in a state where it can be unloaded, without interrupting anything important? */ default boolean canAbort() { // Don't allow restart while match is running or starting, unless it's empty. switch(matchState()) { case Idle: case Finished: return true; default: return getParticipatingPlayers().isEmpty(); } } /** * Transition into the given state * * @throws IllegalStateException if the transition is invalid */ void transitionTo(MatchState newState); default void ensureState(MatchState state) { if(!inState(state)) { transitionTo(state); } } default void end() { transitionTo(MatchState.Finished); } default void ensureNotRunning() { if(isRunning()) { transitionTo(MatchState.Finished); } } /** * If the match has not started yet, returns null. * If the match is running, return the current time. * If the match is finished, return the time that it finished. */ default @Nullable Instant getEndTime() { if(isFinished()) { return getStateChangeTime(MatchState.Finished); } else if(this.hasStarted()) { return getClock().now().instant; } else { return null; } } /** * If the match has not started, throws {@link IllegalStateException} * If the match is running, return the time since it started. * If the match is finished, return the total time it ran for. */ default Duration getLength() { Instant startTime = getStateChangeTime(MatchState.Running); if(startTime == null) { throw new IllegalStateException("match has not started yet"); } return Duration.between(startTime, getEndTime()); } /** * Get the duration of the match, or zero if the match has not started */ @Override default Duration runningTime() { Instant startTime = getStateChangeTime(MatchState.Running); if(startTime == null) { return Duration.ZERO; } return Duration.between(startTime, getEndTime()); } /** * The range of player counts this match can support. * * This is initially zero, and is updated by some modules that load. * * @see #setPlayerLimits */ Range<Integer> getPlayerLimits(); default int getMaxPlayers() { return getPlayerLimits().upperEndpoint(); } void setPlayerLimits(Range<Integer> limits); /** * All players currently in this match */ @Override default Stream<MatchPlayer> players() { return getPlayers().stream(); } /** * All players currently in this match */ default Set<MatchPlayer> getPlayers() { return playersByEntity().values(); } /** * All players currently in this match, by their Bukkit entity */ BiMap<Player, MatchPlayer> playersByEntity(); /** * All players currently in this match, by party type */ SetMultimap<Party.Type, MatchPlayer> playersByType(); default Set<MatchPlayer> getPlayers(Party.Type type) { return playersByType().get(type); } default Set<MatchPlayer> getObservingPlayers() { return playersByType().get(Party.Type.Observing); } default Set<MatchPlayer> getParticipatingPlayers() { return playersByType().get(Party.Type.Participating); } /** * Players who have been in a participating {@link Party} after match commitment. * * @see #commit */ Set<PlayerId> getPastParticipants(); default boolean hasEverParticipated(PlayerId playerId) { return getPastParticipants().contains(playerId); } /** * The {@link PunchClock} that tracks the cumulative participation times * of all players who have ever participated in this match. */ PunchClock<PlayerId> getParticipationClock(); /** * Find a {@link MatchUserContext} for the player with the given {@link UUID}. * * This can be used to retrieve {@link MatchUserFacet}s. */ Optional<MatchUserContext> userContext(UUID uuid); @Override default Optional<MatchPlayer> player(UserId userId) { return MatchPlayerFinder.super.player(userId); } @Override default Optional<MatchPlayer> participant(UserId userId) { return MatchPlayerFinder.super.participant(userId); } @Override default Stream<MatchPlayer> participants() { return getParticipatingPlayers().stream(); } @Override default Stream<MatchPlayer> observers() { return getObservingPlayers().stream(); } @Override default @Nullable MatchPlayer getPlayer(@Nullable Player bukkit) { return bukkit == null ? null : playersByEntity().get(bukkit); } /** * Add the given {@link Player} to this match, if they are not already in it. * * The player will be added to the default {@link Party} * and teleported to the match {@link World}. */ void addPlayer(Player bukkit); default void addAllPlayers(Stream<Player> bukkits) { bukkits.forEach(this::addPlayer); } /** * Remove the given player from this match. * * All match-related state will be torn down, but the player * will not be removed from the {@link World}. * * @throws IllegalArgumentException if the given player is not in this match */ void removePlayer(MatchPlayer player); /** * Remove the given {@link Player} from this match, if they are currently in it. * * If the player is the only member of an automatic {@link Party}, then that * party is also removed from the match (see {@link Party#isAutomatic()}). * * @see #removePlayer */ default void removePlayer(Player bukkit) { final MatchPlayer player = getPlayer(bukkit); if(player != null) { removePlayer(player); } } default void removeAllPlayers() { while(getPlayers().size() > 0) { removePlayer(getPlayers().iterator().next()); } } /** * Return all {@link Party}s currently in the match */ Set<Party> getParties(); /** * Return all {@link Competitor}s currently in the match */ Set<Competitor> getCompetitors(); default Stream<Party> parties() { return getParties().stream(); } @Override default Stream<Competitor> competitors() { return getCompetitors().stream(); } /** * Return all {@link Competitor}s that the given player has ever been * a member of, after the match was committed. This will always be empty * before the match is committed. * * @see #commit */ Set<Competitor> getPastCompetitors(PlayerId playerId); /** * Return the most recent {@link Competitor} that the given player * has been a member of, since the match was committed. Returns null * if the player has never competed in this match, or the match has * not been committed yet. * * @see #commit */ default @Nullable Competitor getLastCompetitor(PlayerId playerId) { return Iterables.getLast(getPastCompetitors(playerId), null); } /** * Is the given {@link Party} currently in this match? */ boolean hasParty(Party party); /** * Add the given {@link Party} to this match. * * The party must be empty of players, and not already in this match. */ void addParty(Party party); /** * Remove the given {@link Party} from this match. * * The party must be empty of players, and currently in this match. */ void removeParty(Party party); /** * Return the default {@link Party} for this match. * * This is the party that new players are added to automatically. */ Party getDefaultParty(); /** * Add the given {@link MatchPlayer} to the given {@link Party}, after removing * them from any current party they are in. If the party is not currently in * this match, and it is an automatic party, then the party is also added to the * match (see {@link Party#isAutomatic}). * * This is the ONLY way that external code can change a player's party. * Any other methods that appear to do so are meant for internal use only. */ boolean setPlayerParty(MatchPlayer player, Party newParty); /** * Commit the match, if it is not already committed. Commitment is a boolean * state that starts false and becomes true at some point before or at match start. * The transition only happens once per match, and is irreversible, even if the start * countdown is cancelled. * * The commitment event is when teams are chosen/balanced (depending on settings), * and also when players become committed to playing the match, if that is enabled. * If mid-match join is disallowed, this is also when that restriction becomes effective. * * Commitment happens automatically at match start, if this method has not been * called before then. */ void commit(); /** * Has this match been committed yet? * * @see #commit */ default boolean isCommitted() { return getCommitTime() != null; } /** * The time that this match was committed, or null if it has not been committed yet. * * @see #commit */ @Nullable TickTime getCommitTime(); default void sendMessageExcept(BaseComponent message, MatchPlayer... except) { players().filter(player -> !ArrayUtils.contains(except, player)) .forEach(player -> player.sendMessage(message)); } default void sendMessageExcept(BaseComponent message, MatchPlayerState... except) { players().filter(player -> Stream.of(except).noneMatch(ex -> ex.isPlayer(player))) .forEach(player -> player.sendMessage(message)); } }