673 lines
19 KiB
Java
673 lines
19 KiB
Java
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.MultiAudience;
|
|
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 MultiAudience, 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
|
|
*/
|
|
default void registerEventsAndRepeatables(Object thing) {
|
|
registerRepeatable(thing);
|
|
if(thing instanceof Listener) registerEvents((Listener) thing);
|
|
}
|
|
|
|
/**
|
|
* Unregister {@link Repeatable} methods on the given object, and also
|
|
* unregister it for events if it is a {@link Listener}.
|
|
*
|
|
* @see #unregisterEvents
|
|
* @see #unregisterRepeatable
|
|
*/
|
|
default void unregisterEventsAndRepeatables(Object thing) {
|
|
unregisterRepeatable(thing);
|
|
if(thing instanceof Listener) unregisterEvents((Listener) 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, boolean force);
|
|
|
|
/**
|
|
* 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));
|
|
}
|
|
}
|