package com.supaham.commons.bukkit.potion; import com.google.common.base.Preconditions; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import com.supaham.commons.Pausable; import com.supaham.commons.bukkit.TickerTask; import com.supaham.commons.bukkit.modules.CommonModule; import com.supaham.commons.bukkit.modules.ModuleContainer; import com.supaham.commons.state.State; import com.supaham.commons.utils.TimeUtils; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.UUID; import javax.annotation.Nonnull; /** * Represents a pottion effect manager for {@link LivingEntity} instances. This class * utilizes the following {@link Potion} fields to implement usage and functionality: * <ul> * <li>{@link Potion#getReapplyTicks()}</li> * <li>{@link Potion#isHidingDuration()}</li> * <li>{@link Potion#isDeathPersistent()}</li> * <li>{@link Potion#isSessionPersistent()}</li> * </ul> * <p /> * <b>NOTE: ONLY SUPPORTS PLAYERS AT THE MOMENT.</b> * * @see #apply(Potion, LivingEntity) * @since 0.2 */ public class PotionEffectManager extends CommonModule { private final TickerTask expiryTask; private final Listener listener = new PlayerListener(); private final Table<UUID, Integer, PotionData> entityEffects = HashBasedTable.create(); /** * Constructs a new {@link PotionEffectManager}. * * @param container module container to own this module. * * @see #apply(Potion, LivingEntity) */ public PotionEffectManager(@Nonnull ModuleContainer container) { super(container); this.expiryTask = new ExpiryTask(0, 1); registerTask(this.expiryTask); registerListener(this.listener); } @Override public boolean setState(@Nonnull State state) throws UnsupportedOperationException { State old = this.state; boolean change = super.setState(state); if (change) { switch (state) { case PAUSED: for (PotionData data : this.entityEffects.values()) { data.pause(); } break; case ACTIVE: if (old == State.PAUSED) { // Only resume if it was previously paused for (PotionData data : this.entityEffects.values()) { data.resume(); } } break; } } return change; } /** * Clears a specific effect registered to this manager for a {@link LivingEntity}. * * @param entity entity to clear effect from in this manager * @param type type of the effect to remove * * @return whether the entity was located and cleared * * @see #clear(UUID, PotionEffectType) */ public boolean clear(@Nonnull LivingEntity entity, PotionEffectType type) { return clear(entity.getUniqueId(), type); } /** * Clears a specific effect registered to this manager for a {@link LivingEntity}. * * @param entity entity to clear effect from in this manager * @param potionId potion id of the effect to remove * * @return whether the entity was located and cleared * * @see #clear(UUID, int) */ public boolean clear(@Nonnull LivingEntity entity, int potionId) { return clear(entity.getUniqueId(), potionId); } /** * Clears a specific effect registered to this manager for a {@link UUID}. If the entity of the * given UUID is active and loaded, the registered effect is removed from them. * * @param uuid uuid to clear effect from in this manager * @param type type of the effect to remove * * @return whether the entity was located and cleared * * @see #clear(UUID, int) */ public boolean clear(@Nonnull UUID uuid, PotionEffectType type) { return clear(uuid, type.getId()); } /** * Clears a specific effect registered to this manager for a {@link UUID}. If the entity of the * given UUID is active and loaded, the registered effect is removed from them. * * @param uuid uuid to clear effect from in this manager * @param potionId potion id of the effect to remove * * @return whether the entity was located and cleared */ public boolean clear(@Nonnull UUID uuid, int potionId) { PotionData data = this.entityEffects.remove(uuid, potionId); if (data == null) { return false; } LivingEntity entity = getEntityByUUID(uuid); if (entity == null) { return false; } entity.removePotionEffect(data.type); return true; } /** * Clears all registered registered potion effects from this manager. * * @see #clearAll(UUID) */ public void clearAll() { for (UUID uuid : this.entityEffects.rowKeySet()) { clearAll(uuid); } } /** * Clears all effects registered to this manager for a {@link UUID}. If the entity of the given * UUID is active and loaded, the registered effects are removed from them. * * @param uuid uuid to clear from this manager * * @return whether the entity was located and cleared */ public boolean clearAll(@Nonnull UUID uuid) { Preconditions.checkNotNull(uuid, "uuid cannot be null."); LivingEntity entity = getEntityByUUID(uuid); if (entity == null) { this.entityEffects.row(uuid).clear(); return false; } Iterator<PotionData> it = this.entityEffects.row(uuid).values().iterator(); while (it.hasNext()) { PotionData next = it.next(); entity.removePotionEffect(next.type); it.remove(); } return true; } /** * Applies a {@link Potion} to a {@link LivingEntity} through this manager. The given * {@link Potion} instance is cloned to prevent instability. * <p /> * This is the main method that puts all the gears together, without this method usage, the * manager is useless. Existing effects applied by other means may be overwritten by this call. * * @param potion potion to apply to the {@code entity} * @param entity entity to apply the {@code potion} to */ public void apply(@Nonnull Potion potion, @Nonnull LivingEntity entity) { Preconditions.checkNotNull(potion, "potion cannot be null."); Preconditions.checkNotNull(entity, "entity cannot be null."); // Remove old effect PotionData old = this.entityEffects.remove(entity.getUniqueId(), potion.getPotionId()); if (old != null) { entity.removePotionEffect(old.type); } potion = new Potion(potion); PotionData data = new PotionData(potion); if (this.state == State.PAUSED) { data.pause(); } data.apply(entity, true); // always force the first application this.entityEffects.put(entity.getUniqueId(), potion.getPotionId(), data); } /** * Handles a {@link Player} leaving the game. This clears any registered effects to the manager * as well as the effects applied to the player by this manager. * * @param player player to handle */ public void handleSessionQuit(Player player) { Iterator<PotionData> it = this.entityEffects.row(player.getUniqueId()).values().iterator(); while (it.hasNext()) { // Remove potion effects that are not death persistent. PotionData data = it.next(); player.removePotionEffect(data.type); // remove all effects if (!data.potion.isSessionPersistent()) { it.remove(); } } } /** * Handles the death of an entity by {@link UUID}. This merely clears any registered effects as * the potions themselves are already removed on death. * * @param uuid uuid of the entity to handle */ public void handleDeath(UUID uuid) { Iterator<PotionData> it = this.entityEffects.row(uuid).values() .iterator(); while (it.hasNext()) { // Remove potion effects that are not death persistent. PotionData data = it.next(); if (!data.potion.isDeathPersistent()) { it.remove(); } } } /** * Handles the effect expiration of an entity by {@link UUID}. If an effect has expired, this * will clear the effect registration from the manager as well as the effects applied to the * player by this manager. Otherwise, it will attempt to apply the effect, assuming the entity is * not null, using {@link PotionData#apply(LivingEntity)}. * * @param uuid uuid of the entity to handle */ public void handleExpire(UUID uuid) { LivingEntity entity = getEntityByUUID(uuid); Iterator<PotionData> it = this.entityEffects.row(uuid).values().iterator(); while (it.hasNext()) { PotionData data = it.next(); if (data.isDone()) { it.remove(); if (entity != null) { entity.removePotionEffect(data.type); } } else { data.apply(entity); } } } /** * Gets a {@link PotionData} by {@link PotionEffectType} for a {@link LivingEntity}. * * @param entity entity to get data for * @param type type of potion to get data from * * @return potion data, nullable * * @see #getPotionData(LivingEntity, int) */ public PotionData getPotionData(@Nonnull LivingEntity entity, @Nonnull PotionEffectType type) { Preconditions.checkNotNull(entity, "entity cannot be null."); Preconditions.checkNotNull(type, "potion type cannot be null."); return getPotionData(entity, type.getId()); } /** * Gets a {@link PotionData} by {@link PotionEffectType} for a {@link UUID}. * * @param uuid uuid to get data for * @param type type of potion to get data from * * @return potion data, nullable * * @see #getPotionData(UUID, int) */ public PotionData getPotionData(@Nonnull UUID uuid, @Nonnull PotionEffectType type) { Preconditions.checkNotNull(uuid, "uuid cannot be null."); Preconditions.checkNotNull(type, "potion type cannot be null."); return getPotionData(uuid, type.getId()); } /** * Gets a {@link PotionData} by integral potion id for a {@link LivingEntity}. * * @param entity entity to get data for * @param potionId potion to get data from * * @return potion data, nullable * * @see #getPotionData(UUID, int) */ public PotionData getPotionData(@Nonnull LivingEntity entity, int potionId) { Preconditions.checkNotNull(entity, "entity cannot be null."); Preconditions.checkArgument(potionId >= 0, "potion id cannot be smaller than 0."); return getPotionData(entity.getUniqueId(), potionId); } /** * Gets a {@link PotionData} by integral potion id for a {@link UUID}. * * @param uuid uuid to get data for * @param potionId potion to get data from * * @return potion data, nullable */ public PotionData getPotionData(@Nonnull UUID uuid, int potionId) { Preconditions.checkNotNull(uuid, "uuid cannot be null."); Preconditions.checkArgument(potionId >= 0, "potion id cannot be smaller than 0."); return this.entityEffects.get(uuid, potionId); } /** * Gets a {@link Map} of {@link PotionEffectType} and {@link PotionData} for a * {@link LivingEntity}. * * @param entity entity to get potion data from * * @return unmodifiable map of potion type and potion data * * @see #getPotionDataBukkit(UUID) */ public Map<PotionEffectType, PotionData> getPotionDataBukkit(@Nonnull LivingEntity entity) { Preconditions.checkNotNull(entity, "entity cannot be null."); return getPotionDataBukkit(entity.getUniqueId()); } /** * Gets a {@link Map} of {@link PotionEffectType} and {@link PotionData} for a * {@link UUID}. * * @param uuid uuid to get data for * * @return unmodifiable map of potion type and potion data */ public Map<PotionEffectType, PotionData> getPotionDataBukkit(@Nonnull UUID uuid) { Preconditions.checkNotNull(uuid, "uuid cannot be null."); Map<PotionEffectType, PotionData> map = new HashMap<>(); for (Map.Entry<Integer, PotionData> entry : getPotionData(uuid).entrySet()) { map.put(PotionEffectType.getById(entry.getKey()), entry.getValue()); } return Collections.unmodifiableMap(map); } /** * Gets a {@link Map} of {@link PotionEffectType} and {@link PotionData} for a * {@link LivingEntity}. * * @param entity entity to get data for * * @return unmodifiable map of potion type and potion data * * @see #getPotionData(UUID) */ public Map<Integer, PotionData> getPotionData(@Nonnull LivingEntity entity) { Preconditions.checkNotNull(entity, "entity cannot be null."); return getPotionData(entity.getUniqueId()); } /** * Gets a {@link Map} of {@link PotionEffectType} and {@link PotionData} for a * {@link UUID}. * * @param uuid uuid to get data for * * @return unmodifiable map of potion type and potion data */ public Map<Integer, PotionData> getPotionData(@Nonnull UUID uuid) { Preconditions.checkNotNull(uuid, "uuid cannot be null."); return Collections.unmodifiableMap(this.entityEffects.row(uuid)); } private LivingEntity getEntityByUUID(@Nonnull UUID uuid) { Preconditions.checkNotNull(uuid, "uuid cannot be null."); return this.plugin.getServer().getPlayer(uuid); } public static final class PotionData implements Pausable { private final Potion potion; private final PotionEffectType type; private long expires = -1; // infinite by default private long firstApply = -1; private long lastApply = -1; // don't reapply the potion by default. private long pausedAt = -1; private PotionData(Potion potion) { Preconditions.checkArgument(potion.getPotionId() >= 0, "potion id cannot be smaller than 0."); Preconditions.checkArgument(potion.getAmplifier() >= 0, "amplifier cannot be smaller than 0."); Preconditions.checkArgument(potion.getDuration() >= 0, "duration cannot be smaller than 0."); this.potion = potion; this.type = PotionEffectType.getById(this.potion.getPotionId()); if (this.potion.getReapplyTicks() > 0) { this.lastApply = 0; // allow reapplications of this effect } if (potion.getDuration() != Integer.MAX_VALUE) { this.expires = System.currentTimeMillis() + ((long) potion.getDuration() * 50); } } @Override public boolean pause() { this.pausedAt = System.currentTimeMillis(); return true; } @Override public boolean resume() { this.expires += this.pausedAt - this.firstApply; // time difference between first and pause this.pausedAt = -1; return true; } @Override public boolean isPaused() { return this.pausedAt > -1; } public boolean isDone() { return this.expires > -1 && System.currentTimeMillis() - this.expires >= 0; } // TODO should these methods be public? private void apply(LivingEntity entity) { apply(entity, false); } private void apply(LivingEntity entity, boolean force) { if (entity == null) { return; } Potion pot = this.potion; if (this.firstApply < 0) { this.firstApply = System.currentTimeMillis(); } if (force || (this.lastApply > -1 && TimeUtils.elapsed(this.lastApply, pot.getReapplyTicks() * 50))) { entity.addPotionEffect(getPotionEffect(), true); if (this.lastApply > -1) { this.lastApply = System.currentTimeMillis(); } } else { // attempt to add by default in case it got removed somehow. entity.addPotionEffect(getPotionEffect(), false); } } private PotionEffect getPotionEffect() { int duration = this.potion.isHidingDuration() ? Integer.MAX_VALUE : ((int) ((this.expires - System.currentTimeMillis()) / 50)); try { return new PotionEffect(type, duration, this.potion.getAmplifier(), this.potion.isAmbient(), this.potion.isSpawningParticles()); } catch (NoSuchMethodError e) { // 1.7 return new PotionEffect(type, duration, this.potion.getAmplifier(), this.potion.isAmbient()); } } } private final class ExpiryTask extends TickerTask { public ExpiryTask(long delay, long interval) { super(PotionEffectManager.this.plugin, delay, interval); } @Override public void run() { Iterator<UUID> it = PotionEffectManager.this.entityEffects.rowKeySet().iterator(); while (it.hasNext()) { // iterator in case removals during the handle process handleExpire(it.next()); } } } private final class PlayerListener implements Listener { @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { handleSessionQuit(event.getPlayer()); } @EventHandler public void onPlayerDeath(PlayerDeathEvent event) { handleDeath(event.getEntity().getUniqueId()); } } }