package tc.oc.pgm.match; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.logging.Logger; import java.util.stream.Stream; import javax.annotation.Nullable; import javax.inject.Inject; import me.anxuiz.settings.Setting; import me.anxuiz.settings.SettingManager; import me.anxuiz.settings.bukkit.PlayerSettings; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.BaseComponent; import org.bukkit.EntityLocation; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.event.Listener; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.PlayerInventory; import org.bukkit.potion.PotionEffect; import java.time.Duration; import java.time.Instant; import tc.oc.api.bukkit.friends.OnlineFriends; import tc.oc.api.bukkit.users.BukkitUserStore; import tc.oc.api.docs.PlayerId; import tc.oc.api.docs.Server; import tc.oc.api.docs.User; import tc.oc.commons.bukkit.attribute.AttributeUtils; import tc.oc.commons.bukkit.chat.BukkitSound; import tc.oc.commons.bukkit.chat.ComponentRenderers; import tc.oc.commons.bukkit.chat.NameStyle; import tc.oc.commons.bukkit.chat.Named; import tc.oc.commons.bukkit.chat.PlayerComponent; import tc.oc.commons.bukkit.nick.Identity; import tc.oc.commons.bukkit.nick.IdentityProvider; import tc.oc.commons.bukkit.settings.SettingManagerProvider; import tc.oc.commons.bukkit.util.PlayerStates; import tc.oc.commons.core.chat.Audience; import tc.oc.commons.core.chat.Component; import tc.oc.commons.core.chat.Sound; import tc.oc.commons.core.logging.Loggers; import tc.oc.commons.core.util.Optionals; import tc.oc.pgm.events.PlayerResetEvent; import tc.oc.pgm.filters.Filterable; import tc.oc.pgm.filters.query.IPlayerQuery; import tc.oc.pgm.kits.WalkSpeedKit; import tc.oc.pgm.settings.ObserverSetting; import tc.oc.pgm.settings.Settings; import static com.google.common.base.Preconditions.checkNotNull; /** * MatchPlayer represents a player who is part of a match. Note that the * MatchPlayer object should only exist as long as the corresponding Match * instance exists. * * MatchPlayer stores all information that is necessary for the core plugin. */ public class MatchPlayer extends MatchFacetContext<MatchPlayerFacet> implements InventoryHolder, Audience, Named, IPlayerQuery, Listener, Filterable<IPlayerQuery> { private static final Sound ERROR_SOUND = new BukkitSound(org.bukkit.Sound.BLOCK_NOTE_BASS, 1f, 0.75f); private static final Duration ERROR_SOUND_COOLDOWN = Duration.ofSeconds(10); private static final Duration SPARK_SOUND_COOLDOWN = Duration.ofSeconds(1); @Inject private BukkitUserStore userStore; @Inject private IdentityProvider identityProvider; @Inject private Server localServer; @Inject private OnlineFriends friendMap; @Inject private PlayerStates playerStates; @Inject MatchUserContext userContext; // per-player stuff @Inject private Player bukkit; @Inject private PlayerId playerId; // per-match stuff @Inject private Match match; private Logger logger; private SettingManager settings; @Inject private void init(Loggers loggers, SettingManagerProvider settingManagerProvider, Player player) { this.logger = loggers.get(match.getLogger(), getClass(), getName()); this.settings = settingManagerProvider.getManager(player); } protected @Nullable Party party; protected Instant lastErrorSoundTime; protected Instant lastSparkSoundTime; protected boolean spawned; protected boolean dead; protected boolean visible; // Save these so toString works after bukkit is nulled private String name; private UUID uuid; @Inject void saveNameAndId(Player bukkit) { name = bukkit.getName(); uuid = bukkit.getUniqueId(); } @Override public String toString() { return MatchPlayer.class.getSimpleName() + "{name=" + getName() + " uuid=" + getUniqueId() + "}"; } @Override public boolean equals(Object that) { return this == that || (that instanceof MatchPlayer && this.match.equals(((MatchPlayer) that).match) && this.getUniqueId().equals(((MatchPlayer) that).getUniqueId())); } @Override public int hashCode() { return Objects.hash(match, getUniqueId()); } public String getName() { return name; } public UUID getUniqueId() { return uuid; } @Override public void enableAllThrows() throws Exception { super.enableAllThrows(); match.registerRepeatable(this); } @Override public void disableAllThrows() throws Exception { match.unregisterRepeatable(this); // Clear these so anything that tries to use this object // out of scope will cause a noisy error. match = null; bukkit = null; super.disableAllThrows(); } /** * Call this to check if a player is still online. * * You can't call getBukkit().isOnline(), it will throw an IllegalStateException */ public boolean isOnline() { return bukkit != null && bukkit.isOnline(); } public String getName(CommandSender viewer) { return getBukkit().getName(viewer); } public String getName(MatchPlayer viewer) { return getName(viewer.getBukkit()); } public String getDisplayName() { return getBukkit().getDisplayName(); } public String getDisplayName(CommandSender viewer) { return getBukkit().getDisplayName(viewer); } public String getDisplayName(MatchPlayer viewer) { return getDisplayName(viewer.getBukkit()); } public PlayerInventory getInventory() { return getBukkit().getInventory(); } @Override public World getWorld() { return match.getWorld(); } public Match getMatch() { return match; } public Player getBukkit() { if(bukkit == null) { throw new IllegalStateException("Tried to access unloaded " + this); } return bukkit; } public PlayerId getPlayerId() { return playerId; } public User getDocument() { return userStore.getUser(getBukkit()); // Must use the real Player, not this } public MatchUserContext getUserContext() { return userContext; } /** * Reverse-chronological list of match commitments within the last 24 hours */ public List<Instant> recentMatchCommitments() { final List<Instant> instants = getDocument().recent_match_joins_by_family_id().get(localServer.family()); return instants == null ? Collections.emptyList() : instants; } public boolean hasParty() { return party != null; } /** * The player's current party. */ public Party getParty() { if(!hasParty()) { throw new IllegalStateException(getName() + " has no party"); } return party; } public boolean inParty(Party party) { return party.equals(this.party); } public Optional<Party> partyMaybe() { return Optional.ofNullable(party); } /** * Called ONLY by {@link Match}. The match is not necessarily * in a consistent state when this method is called, so it should do nothing * except update internal data structures. Any other reactions to changing * parties should be implemented with an event handler. */ protected @Nullable Party setPartyInternal(@Nullable Party newParty) { final Party oldParty = party; if(!Objects.equals(oldParty, newParty)) { party = newParty; playerStates.setParticipating(getBukkit(), competitor().isPresent()); } return oldParty; } public @Nullable Competitor getCompetitor() { return party instanceof Competitor ? (Competitor) party : null; } public Optional<Competitor> competitor() { return party instanceof Competitor ? Optional.of((Competitor) party) : Optional.empty(); } @Override public MatchPlayerState playerState() { final Identity identity = identityProvider.currentIdentity(getBukkit()); final Party party = getParty(); if(party instanceof Competitor) { return new ParticipantState(match, identity, getUniqueId(), (Competitor) party, getEntityLocation()); } else { return new MatchPlayerState(match, identity, getUniqueId(), party, getEntityLocation()); } } @Override public Optional<ParticipantState> participantState() { return Optionals.cast(playerState(), ParticipantState.class); } public @Nullable ParticipantState getParticipantState() { return participantState().orElse(null); } @Override public Optional<? extends Filterable<? super IPlayerQuery>> filterableParent() { return Optional.of(getParty()); } @Override public Stream<? extends Filterable<? extends IPlayerQuery>> filterableChildren() { return Stream.of(); } @Override public Optional<MatchPlayer> onlinePlayer() { return Optional.of(this); } @Override public EntityLocation getEntityLocation() { return getBukkit().getEntityLocation(); } /** * Called when the player is committed to the match. * See {@link Match#commit()} for details. */ protected void commit() { } /** * Get the cumulative time the player has participated in this match */ public Duration getCumulativeParticipationTime() { return match.getParticipationClock().getCumulativePresence(getPlayerId()); } /** * Get the cumulative percentage of the match running time in which the player has participated */ public double getCumulativeParticipationPercent() { return match.getParticipationClock().getCumulativePresencePercent(getPlayerId()); } public boolean isParticipatingType() { return party != null && party.isParticipatingType(); } public boolean isParticipating() { return party != null && party.isParticipating(); } public boolean isObservingType() { return party != null && party.isObservingType(); } public boolean isObserving() { return isObservingType() && !isSpawned(); } public boolean isCommitted() { return isParticipatingType() && match.isCommitted(); } public boolean canInteract() { return this.isParticipating() && !this.isDead(); } public void setSpawned(boolean spawned) { if(this.spawned != spawned) { this.spawned = spawned; if(spawned) setDead(false); } } public boolean isSpawned() { return spawned; } public boolean isDead() { return dead; } public void setDead(boolean dead) { if(this.dead != dead) { this.dead = dead; playerStates.setDead(getBukkit(), dead); if(dead) setSpawned(false); } } public boolean isVisible() { return visible; } public void setVisible(boolean visible) { this.visible = visible; } public void refreshInteraction() { final Player bukkit = getBukkit(); boolean interact = canInteract(); if(!interact) bukkit.leaveVehicle(); bukkit.setAffectsSpawning(interact); bukkit.setCollidesWithEntities(interact); } public void clearInventory() { final Player bukkit = getBukkit(); bukkit.getInventory().clear(); } public void reset() { final Player bukkit = getBukkit(); bukkit.closeInventory(); clearInventory(); bukkit.setExhaustion(0); bukkit.setFallDistance(0); bukkit.setFireTicks(0); bukkit.setFoodLevel(20); // full bukkit.setMaxHealth(20); bukkit.setHealth(bukkit.getMaxHealth()); bukkit.setAbsorption(0); bukkit.setLevel(0); bukkit.setExp(0); // clear xp bukkit.setSaturation(5); // default bukkit.setFastNaturalRegeneration(false); bukkit.setSlowNaturalRegeneration(true); bukkit.setAllowFlight(false); bukkit.setFlying(false); bukkit.setSneaking(false); bukkit.setSprinting(false); bukkit.setFlySpeed(0.1f); bukkit.setKnockbackReduction(0); bukkit.setWalkSpeed(WalkSpeedKit.BUKKIT_DEFAULT); AttributeUtils.removeAllModifiers(bukkit); resetPotions(); // we only reset bed spawn here so people don't have to see annoying messages when they respawn bukkit.setBedSpawnLocation(null); match.callEvent(new PlayerResetEvent(this)); } public void resetPotions() { final Player bukkit = getBukkit(); for(PotionEffect effect : bukkit.getActivePotionEffects()) { if(effect.getType() != null) { bukkit.removePotionEffect(effect.getType()); } } } public boolean canSee(MatchPlayer that) { // Dead players are never visible if(!that.isVisible()) return false; // Spawned players are always visible if(that.isSpawned()) return true; // Spawned/dead players can only see other spawned players if(isSpawned() || isDead()) return false; // If both players are observing, decide based on the viewer's setting switch(settings.getValue(ObserverSetting.get(), ObserverSetting.Options.class)) { case NONE: return false; case FRIENDS: return friendMap.areFriends(getBukkit(), that.getBukkit()); default: return true; } } public void refreshVisibility() { final Player bukkit = getBukkit(); bukkit.showInvisibles(isObserving()); for(MatchPlayer other : match.getPlayers()) { if(canSee(other)) { bukkit.showPlayer(other.getBukkit()); } else { bukkit.hidePlayer(other.getBukkit()); } if(other.canSee(this)) { other.getBukkit().showPlayer(bukkit); } else { other.getBukkit().hidePlayer(bukkit); } } } public ChatColor getColor() { return party == null ? ChatColor.AQUA : party.getColor(); } public String getColoredName() { return getColor() + getName(); } public String getColoredName(CommandSender viewer) { return getColor() + getName(viewer); } public String getColoredName(MatchPlayer viewer) { return getColor() + getName(viewer.getBukkit()); } public BaseComponent getComponentName() { return getStyledName(NameStyle.COLOR); } @Override public PlayerComponent getStyledName(NameStyle style) { return new PlayerComponent(identityProvider.currentIdentity(getBukkit()), style); } @Override public void sendMessage(String message) { getBukkit().sendMessage(message); } @Override public void sendMessage(BaseComponent message) { getBukkit().sendMessage(ComponentRenderers.render(checkNotNull(message), getBukkit())); } @Override public void sendHotbarMessage(BaseComponent message) { getBukkit().sendMessage(ChatMessageType.ACTION_BAR, ComponentRenderers.render(message, getBukkit())); } @Override public void showTitle(BaseComponent title, BaseComponent subtitle, int inTicks, int stayTicks, int outTicks) { title = title == null ? new Component("") : ComponentRenderers.render(title, getBukkit()); subtitle = subtitle == null ? new Component("") : ComponentRenderers.render(subtitle, getBukkit()); getBukkit().showTitle(title, subtitle, inTicks, stayTicks, outTicks); } @Override public void hideTitle() { getBukkit().hideTitle(); } @Override public void sendWarning(String message, boolean audible) { getBukkit().sendMessage(ChatColor.YELLOW + " \u26a0 " + ChatColor.RED + message); // The character is '⚠' if(audible) playWarningSound(); } @Override public void sendWarning(BaseComponent message, boolean audible) { sendMessage(new Component(net.md_5.bungee.api.ChatColor.RED).extra(new Component(" \u26a0 ", net.md_5.bungee.api.ChatColor.YELLOW), message)); if(audible) playWarningSound(); } @Override public void playSound(Sound sound) { this.playSound(sound, getBukkit().getLocation()); } public void sendMessage(List<String> lines) { for(String line : lines) { this.sendMessage(line); } } public void sendWarning(String message) { this.sendWarning(message, false); } public void sendWarning(BaseComponent message) { this.sendWarning(message, false); } public boolean playWarningSound() { Instant now = Instant.now(); if(this.lastErrorSoundTime == null || this.lastErrorSoundTime.isBefore(now.minus(ERROR_SOUND_COOLDOWN))) { this.lastErrorSoundTime = now; this.playSound(ERROR_SOUND); return true; } return false; } public void playSound(String sound, Location location, float volume, float pitch) { if(this.getToggle(Settings.SOUNDS)) { getBukkit().playSound(location, sound, volume, pitch); } } public void playSound(Sound sound, Location location) { if(this.getToggle(Settings.SOUNDS)) { this.playSound(sound.name(), location, sound.volume(), sound.pitch()); } } public void playSound(org.bukkit.Sound sound, Location location, float volume, float pitch) { if(this.getToggle(Settings.SOUNDS)) { getBukkit().playSound(location, sound, volume, pitch); } } public void playSound(org.bukkit.Sound sound, float volume, float pitch) { this.playSound(sound, getBukkit().getLocation(), volume, pitch); } public void playSound(org.bukkit.Sound sound, Location location) { this.playSound(sound, location, 1, 1); } public void playSound(org.bukkit.Sound sound) { this.playSound(sound, getBukkit().getLocation(), Float.POSITIVE_INFINITY, 1); // +Inf volume means non-local } public void playSparks() { if(this.lastSparkSoundTime == null || this.lastSparkSoundTime.isBefore(Instant.now().minus(SPARK_SOUND_COOLDOWN))) { this.playSound(org.bukkit.Sound.ENTITY_FIREWORK_BLAST, 1, 1); this.playSound(org.bukkit.Sound.ENTITY_FIREWORK_TWINKLE, 1, 1); } } public boolean getToggle(Setting setting) { return PlayerSettings.getManager(getBukkit()).getValue(setting, Boolean.class); } public void nextTick(MatchScope scope, Runnable task) { match.getScheduler(scope).createTask(() -> { if(isOnline()) { task.run(); } }); } public void nextTick(Runnable task) { nextTick(MatchScope.LOADED, task); } }