/* * Copyright (C) 2004-2015 L2J Server * * This file is part of L2J Server. * * L2J Server is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * L2J Server is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.l2jserver.gameserver.model; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; import java.util.logging.Logger; import com.l2jserver.Config; import com.l2jserver.gameserver.model.actor.L2Character; import com.l2jserver.gameserver.model.actor.L2Summon; import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; import com.l2jserver.gameserver.model.effects.AbstractEffect; import com.l2jserver.gameserver.model.effects.EffectFlag; import com.l2jserver.gameserver.model.effects.L2EffectType; import com.l2jserver.gameserver.model.olympiad.OlympiadGameManager; import com.l2jserver.gameserver.model.olympiad.OlympiadGameTask; import com.l2jserver.gameserver.model.skills.AbnormalType; import com.l2jserver.gameserver.model.skills.BuffInfo; import com.l2jserver.gameserver.model.skills.EffectScope; import com.l2jserver.gameserver.model.skills.Skill; import com.l2jserver.gameserver.network.serverpackets.AbnormalStatusUpdate; import com.l2jserver.gameserver.network.serverpackets.ExAbnormalStatusUpdateFromTarget; import com.l2jserver.gameserver.network.serverpackets.ExOlympiadSpelledInfo; import com.l2jserver.gameserver.network.serverpackets.PartySpelled; import com.l2jserver.gameserver.network.serverpackets.ShortBuffStatusUpdate; /** * Effect lists.<br> * Holds all the buff infos that are affecting a creature.<br> * Manages the logic that controls whether a buff is added, remove, replaced or set inactive.<br> * Uses maps with skill ID as key and buff info DTO as value to avoid iterations.<br> * Uses Double-Checked Locking to avoid useless initialization and synchronization issues and overhead.<br> * Methods may resemble List interface, although it doesn't implement such interface. * @author Zoey76 */ public final class CharEffectList { private static final Logger _log = Logger.getLogger(CharEffectList.class.getName()); /** Map containing all effects from buffs for this effect list. */ private volatile ConcurrentHashMap<Integer, BuffInfo> _buffs; /** Map containing all triggered skills for this effect list. */ private volatile ConcurrentHashMap<Integer, BuffInfo> _triggered; /** Map containing all dances/songs for this effect list. */ private volatile ConcurrentHashMap<Integer, BuffInfo> _dances; /** Map containing all toggle for this effect list. */ private volatile ConcurrentHashMap<Integer, BuffInfo> _toggles; /** Map containing all debuffs for this effect list. */ private volatile ConcurrentHashMap<Integer, BuffInfo> _debuffs; /** They bypass most of the actions, they are not included in most operations. */ private volatile ConcurrentHashMap<Integer, BuffInfo> _passives; /** Map containing the all stacked effect in progress for each abnormal type. */ private volatile Map<AbnormalType, BuffInfo> _stackedEffects; /** Set containing all abnormal types that shouldn't be added to this creature effect list. */ private volatile Set<AbnormalType> _blockedBuffSlots = null; /** Short buff skill ID. */ private BuffInfo _shortBuff = null; /** If {@code true} this effect list has buffs removed on any action. */ private volatile boolean _hasBuffsRemovedOnAnyAction = false; /** If {@code true} this effect list has buffs removed on damage. */ private volatile boolean _hasBuffsRemovedOnDamage = false; /** If {@code true} this effect list has debuffs removed on damage. */ private volatile boolean _hasDebuffsRemovedOnDamage = false; /** Effect flags. */ private int _effectFlags; /** If {@code true} only party icons need to be updated. */ private boolean _partyOnly = false; /** The owner of this effect list. */ private final L2Character _owner; /** Hidden buffs count, prevents iterations. */ private final AtomicInteger _hiddenBuffs = new AtomicInteger(); /** * Constructor for effect list. * @param owner the creature that owns this effect list */ public CharEffectList(L2Character owner) { _owner = owner; } /** * Gets buff skills. * @return the buff skills */ public Map<Integer, BuffInfo> getBuffs() { if (_buffs == null) { synchronized (this) { if (_buffs == null) { _buffs = new ConcurrentHashMap<>(); } } } return _buffs; } /** * Gets triggered skill skills. * @return the triggered skill skills */ public Map<Integer, BuffInfo> getTriggered() { if (_triggered == null) { synchronized (this) { if (_triggered == null) { _triggered = new ConcurrentHashMap<>(); } } } return _triggered; } /** * Gets dance/song skills. * @return the dance/song skills */ public Map<Integer, BuffInfo> getDances() { if (_dances == null) { synchronized (this) { if (_dances == null) { _dances = new ConcurrentHashMap<>(); } } } return _dances; } /** * Gets toggle skills. * @return the toggle skills */ public Map<Integer, BuffInfo> getToggles() { if (_toggles == null) { synchronized (this) { if (_toggles == null) { _toggles = new ConcurrentHashMap<>(); } } } return _toggles; } /** * Gets debuff skills. * @return the debuff skills */ public Map<Integer, BuffInfo> getDebuffs() { if (_debuffs == null) { synchronized (this) { if (_debuffs == null) { _debuffs = new ConcurrentHashMap<>(); } } } return _debuffs; } /** * Gets passive skills. * @return the passive skills */ public Map<Integer, BuffInfo> getPassives() { if (_passives == null) { synchronized (this) { if (_passives == null) { _passives = new ConcurrentHashMap<>(); } } } return _passives; } /** * Gets all the effects on this effect list. * @return all the effects on this effect list */ public List<BuffInfo> getEffects() { if (isEmpty()) { return Collections.<BuffInfo> emptyList(); } final List<BuffInfo> buffs = new ArrayList<>(); if (hasBuffs()) { buffs.addAll(getBuffs().values()); } if (hasTriggered()) { buffs.addAll(getTriggered().values()); } if (hasDances()) { buffs.addAll(getDances().values()); } if (hasToggles()) { buffs.addAll(getToggles().values()); } if (hasDebuffs()) { buffs.addAll(getDebuffs().values()); } return buffs; } /** * Gets the effect list where the skill effects should be. * @param skill the skill * @return the effect list */ private Map<Integer, BuffInfo> getEffectList(Skill skill) { if (skill == null) { return null; } final Map<Integer, BuffInfo> effects; if (skill.isPassive()) { effects = getPassives(); } else if (skill.isDebuff()) { effects = getDebuffs(); } else if (skill.isTriggeredSkill()) { effects = getTriggered(); } else if (skill.isDance()) { effects = getDances(); } else if (skill.isToggle()) { effects = getToggles(); } else { effects = getBuffs(); } return effects; } /** * Gets the first effect for the given effect type.<br> * Prevents initialization.<br> * TODO: Remove this method after all the effect types gets replaced by abnormal skill types. * @param type the effect type * @return the first effect matching the given effect type */ public BuffInfo getFirstEffect(L2EffectType type) { if (hasBuffs()) { for (BuffInfo info : getBuffs().values()) { if (info != null) { for (AbstractEffect effect : info.getEffects()) { if ((effect != null) && (effect.getEffectType() == type)) { return info; } } } } } if (hasTriggered()) { for (BuffInfo info : getTriggered().values()) { if (info != null) { for (AbstractEffect effect : info.getEffects()) { if ((effect != null) && (effect.getEffectType() == type)) { return info; } } } } } if (hasDances()) { for (BuffInfo info : getDances().values()) { if (info != null) { for (AbstractEffect effect : info.getEffects()) { if ((effect != null) && (effect.getEffectType() == type)) { return info; } } } } } if (hasToggles()) { for (BuffInfo info : getToggles().values()) { if (info != null) { for (AbstractEffect effect : info.getEffects()) { if ((effect != null) && (effect.getEffectType() == type)) { return info; } } } } } if (hasDebuffs()) { for (BuffInfo info : getDebuffs().values()) { if (info != null) { for (AbstractEffect effect : info.getEffects()) { if ((effect != null) && (effect.getEffectType() == type)) { return info; } } } } } return null; } /** * Verifies if this effect list contains the given skill ID.<br> * Prevents initialization. * @param skillId the skill ID to verify * @return {@code true} if the skill ID is present in the effect list, {@code false} otherwise */ public boolean isAffectedBySkill(int skillId) { return (hasBuffs() && getBuffs().containsKey(skillId)) || (hasDebuffs() && getDebuffs().containsKey(skillId)) || (hasTriggered() && getTriggered().containsKey(skillId)) || (hasDances() && getDances().containsKey(skillId)) || (hasToggles() && getToggles().containsKey(skillId)) || (hasPassives() && getPassives().containsKey(skillId)); } /** * Gets the buff info by skill ID.<br> * Prevents initialization. * @param skillId the skill ID * @return the buff info */ public BuffInfo getBuffInfoBySkillId(int skillId) { BuffInfo info = null; if (hasBuffs() && getBuffs().containsKey(skillId)) { info = getBuffs().get(skillId); } else if (hasTriggered() && getTriggered().containsKey(skillId)) { info = getTriggered().get(skillId); } else if (hasDances() && getDances().containsKey(skillId)) { info = getDances().get(skillId); } else if (hasToggles() && getToggles().containsKey(skillId)) { info = getToggles().get(skillId); } else if (hasDebuffs() && getDebuffs().containsKey(skillId)) { info = getDebuffs().get(skillId); } else if (hasPassives() && getPassives().containsKey(skillId)) { info = getPassives().get(skillId); } return info; } /** * Gets a buff info by abnormal type.<br> * It's O(1) for every buff in this effect list. * @param type the abnormal skill type * @return the buff info if it's present, {@code null} otherwise */ public BuffInfo getBuffInfoByAbnormalType(AbnormalType type) { return (_stackedEffects != null) ? _stackedEffects.get(type) : null; } /** * Adds abnormal types to the blocked buff slot set. * @param blockedBuffSlots the blocked buff slot set to add */ public void addBlockedBuffSlots(Set<AbnormalType> blockedBuffSlots) { if (_blockedBuffSlots == null) { synchronized (this) { if (_blockedBuffSlots == null) { _blockedBuffSlots = new CopyOnWriteArraySet<>(); } } } _blockedBuffSlots.addAll(blockedBuffSlots); } /** * Removes abnormal types from the blocked buff slot set. * @param blockedBuffSlots the blocked buff slot set to remove * @return {@code true} if the blocked buff slots set has been modified, {@code false} otherwise */ public boolean removeBlockedBuffSlots(Set<AbnormalType> blockedBuffSlots) { if (_blockedBuffSlots != null) { return _blockedBuffSlots.removeAll(blockedBuffSlots); } return false; } /** * Gets all the blocked abnormal types for this creature effect list. * @return the current blocked buff slots set */ public Set<AbnormalType> getAllBlockedBuffSlots() { return _blockedBuffSlots; } /** * Gets the Short Buff info. * @return the short buff info */ public BuffInfo getShortBuff() { return _shortBuff; } /** * Sets the Short Buff data and sends an update if the effected is a player. * @param info the buff info */ public void shortBuffStatusUpdate(BuffInfo info) { if (_owner.isPlayer()) { _shortBuff = info; if (info == null) { _owner.sendPacket(ShortBuffStatusUpdate.RESET_SHORT_BUFF); } else { _owner.sendPacket(new ShortBuffStatusUpdate(info.getSkill().getId(), info.getSkill().getLevel(), info.getTime())); } } } /** * Checks if the given skill stacks with an existing one. * @param skill the skill to verify * @return {@code true} if this effect stacks with the given skill, {@code false} otherwise */ private boolean doesStack(Skill skill) { final AbnormalType type = skill.getAbnormalType(); if (type.isNone() || isEmpty()) { return false; } final Map<Integer, BuffInfo> effects = getEffectList(skill); if ((effects == null) || effects.isEmpty()) { return false; } for (BuffInfo info : effects.values()) { if ((info != null) && (info.getSkill().getAbnormalType() == type)) { return true; } } return false; } /** * Gets the buffs count without including the hidden buffs (after getting an Herb buff).<br> * Prevents initialization. * @return the number of buffs in this creature effect list */ public int getBuffCount() { return hasBuffs() ? getBuffs().size() - _hiddenBuffs.get() - (getShortBuff() != null ? 1 : 0) : 0; } /** * Gets the Songs/Dances count.<br> * Prevents initialization. * @return the number of Songs/Dances in this creature effect list */ public int getDanceCount() { return hasDances() ? getDances().size() : 0; } /** * Gets the triggered buffs count.<br> * Prevents initialization. * @return the number of triggered buffs in this creature effect list */ public int getTriggeredBuffCount() { return hasTriggered() ? getTriggered().size() : 0; } /** * Gets the hidden buff count. * @return the number of hidden buffs */ public int getHiddenBuffsCount() { return _hiddenBuffs.get(); } /** * Auxiliary method to stop all effects from a buff info and remove it from an effect list and stacked effects. * @param info the buff info */ protected void stopAndRemove(BuffInfo info) { stopAndRemove(true, info, getEffectList(info.getSkill())); } /** * Auxiliary method to stop all effects from a buff info and remove it from an effect list and stacked effects. * @param info the buff info * @param effects the effect list */ protected void stopAndRemove(BuffInfo info, Map<Integer, BuffInfo> effects) { stopAndRemove(true, info, effects); } /** * Auxiliary method to stop all effects from a buff info and remove it from an effect list and stacked effects. * @param removed {@code true} if the effect is removed, {@code false} otherwise * @param info the buff info * @param buffs the buff list */ private void stopAndRemove(boolean removed, BuffInfo info, Map<Integer, BuffInfo> buffs) { if (info == null) { return; } // Removes the buff from the given effect list. buffs.remove(info.getSkill().getId()); // Stop the buff effects. info.stopAllEffects(removed); // If it's a hidden buff that ends, then decrease hidden buff count. if (!info.isInUse()) { _hiddenBuffs.decrementAndGet(); } // Removes the buff from the stack. else if (_stackedEffects != null) { _stackedEffects.remove(info.getSkill().getAbnormalType()); } // If it's an herb that ends, check if there are hidden buffs. if (info.getSkill().isAbnormalInstant() && hasBuffs()) { for (BuffInfo buff : getBuffs().values()) { if ((buff != null) && (buff.getSkill().getAbnormalType() == info.getSkill().getAbnormalType()) && !buff.isInUse()) { // Sets the buff in use again. buff.setInUse(true); // Adds the stats. buff.addStats(); // Adds the buff to the stack. if (_stackedEffects != null) { _stackedEffects.put(buff.getSkill().getAbnormalType(), buff); } // If it's a hidden buff that gets activated, then decrease hidden buff count. _hiddenBuffs.decrementAndGet(); break; } } } if (!removed) { info.getSkill().applyEffectScope(EffectScope.END, info, true, false); } } /** * Exits all effects in this effect list.<br> * Stops all the effects, clear the effect lists and updates the effect flags and icons. */ public void stopAllEffects() { // Stop buffs. stopAllBuffs(false, true); // Stop dances and songs. stopAllDances(false); // Stop toggles. stopAllToggles(false); // Stop debuffs. stopAllDebuffs(false); if (_stackedEffects != null) { _stackedEffects.clear(); } // Update effect flags and icons. updateEffectList(true); } /** * Stops all effects in this effect list except those that last through death. */ public void stopAllEffectsExceptThoseThatLastThroughDeath() { boolean update = false; if (hasBuffs()) { getBuffs().values().stream().filter(info -> !info.getSkill().isStayAfterDeath()).forEach(info -> stopAndRemove(info, getBuffs())); update = true; } if (hasTriggered()) { getTriggered().values().stream().filter(info -> !info.getSkill().isStayAfterDeath()).forEach(info -> stopAndRemove(info, getTriggered())); update = true; } if (hasDebuffs()) { getDebuffs().values().stream().filter(info -> !info.getSkill().isStayAfterDeath()).forEach(info -> stopAndRemove(info, getDebuffs())); update = true; } if (hasDances()) { getDances().values().stream().filter(info -> !info.getSkill().isStayAfterDeath()).forEach(info -> stopAndRemove(info, getDances())); update = true; } if (hasToggles()) { getToggles().values().stream().filter(info -> !info.getSkill().isStayAfterDeath()).forEach(info -> stopAndRemove(info, getToggles())); update = true; } // Update effect flags and icons. updateEffectList(update); } /** * Stop all effects that doesn't stay on sub-class change. */ public void stopAllEffectsNotStayOnSubclassChange() { boolean update = false; if (hasBuffs()) { getBuffs().values().stream().filter(info -> !info.getSkill().isStayOnSubclassChange()).forEach(info -> stopAndRemove(info, getBuffs())); update = true; } if (hasTriggered()) { getTriggered().values().stream().filter(info -> !info.getSkill().isStayOnSubclassChange()).forEach(info -> stopAndRemove(info, getTriggered())); update = true; } if (hasDebuffs()) { getDebuffs().values().stream().filter(info -> !info.getSkill().isStayOnSubclassChange()).forEach(info -> stopAndRemove(info, getDebuffs())); update = true; } if (hasDances()) { getDances().values().stream().filter(info -> !info.getSkill().isStayOnSubclassChange()).forEach(info -> stopAndRemove(info, getDances())); update = true; } if (hasToggles()) { getToggles().values().stream().filter(info -> !info.getSkill().isStayOnSubclassChange()).forEach(info -> stopAndRemove(info, getToggles())); update = true; } // Update effect flags and icons. updateEffectList(update); } /** * Stops all the active buffs. * @param update set to true to update the effect flags and icons * @param triggered if {@code true} stops triggered skills buffs */ public void stopAllBuffs(boolean update, boolean triggered) { if (hasBuffs()) { getBuffs().forEach((k, info) -> stopAndRemove(info, getBuffs())); } if (triggered && hasTriggered()) { getTriggered().forEach((k, info) -> stopAndRemove(info, getTriggered())); } // Update effect flags and icons. updateEffectList(update); } /** * Stops all active toggle skills.<br> * Performs an update. */ public void stopAllToggles() { stopAllToggles(true); } /** * Stops all active toggle skills. * @param update set to true to update the effect flags and icons */ public void stopAllToggles(boolean update) { if (hasToggles()) { getToggles().forEach((k, info) -> stopAndRemove(info, getToggles())); // Update effect flags and icons. updateEffectList(update); } } /** * Stops all active dances/songs skills. * @param update set to true to update the effect flags and icons */ public void stopAllDances(boolean update) { if (hasDances()) { getDances().forEach((k, info) -> stopAndRemove(info, getDances())); // Update effect flags and icons. updateEffectList(update); } } /** * Stops all active dances/songs skills. * @param update set to true to update the effect flags and icons */ public void stopAllDebuffs(boolean update) { if (hasDebuffs()) { getDebuffs().forEach((k, info) -> stopAndRemove(info, getDebuffs())); // Update effect flags and icons. updateEffectList(update); } } /** * Exit all effects having a specified type.<br> * TODO: Remove after all effect types are replaced by abnormal skill types. * @param type the type of the effect to stop */ public void stopEffects(L2EffectType type) { boolean update = false; final Consumer<BuffInfo> action = info -> { if (info.getEffects().stream().anyMatch(effect -> (effect != null) && (effect.getEffectType() == type))) { stopAndRemove(info); } }; if (hasBuffs()) { getBuffs().values().stream().filter(Objects::nonNull).forEach(action); update = true; } if (hasTriggered()) { getTriggered().values().stream().filter(Objects::nonNull).forEach(action); update = true; } if (hasDances()) { getDances().values().stream().filter(Objects::nonNull).forEach(action); update = true; } if (hasToggles()) { getToggles().values().stream().filter(Objects::nonNull).forEach(action); update = true; } if (hasDebuffs()) { getDebuffs().values().stream().filter(Objects::nonNull).forEach(action); update = true; } // Update effect flags and icons. updateEffectList(update); } /** * Exits all effects created by a specific skill ID.<br> * Removes the effects from the effect list.<br> * Removes the stats from the creature.<br> * Updates the effect flags and icons.<br> * Presents two overloads:<br> * {@link #stopSkillEffects(boolean, Skill)}<br> * {@link #stopSkillEffects(boolean, AbnormalType)} * @param removed {@code true} if the effect is removed, {@code false} otherwise * @param skillId the skill ID */ public void stopSkillEffects(boolean removed, int skillId) { final BuffInfo info = getBuffInfoBySkillId(skillId); if (info != null) { stopSkillEffects(removed, info.getSkill()); } } /** * Exits all effects created by a specific skill.<br> * Removes the effects from the effect list.<br> * Removes the stats from the creature.<br> * Updates the effect flags and icons.<br> * Presents two overloads:<br> * {@link #stopSkillEffects(boolean, int)}<br> * {@link #stopSkillEffects(boolean, AbnormalType)} * @param removed {@code true} if the effect is removed, {@code false} otherwise * @param skill the skill */ public void stopSkillEffects(boolean removed, Skill skill) { if ((skill == null) || !isAffectedBySkill(skill.getId())) { return; } final Map<Integer, BuffInfo> effects = getEffectList(skill); if (effects != null) { remove(removed, effects.get(skill.getId())); } } /** * Exits all effects created by a specific skill abnormal type.<br> * It's O(1) for every effect in this effect list except passive effects.<br> * Presents two overloads:<br> * {@link #stopSkillEffects(boolean, int)}<br> * {@link #stopSkillEffects(boolean, Skill)} * @param removed {@code true} if the effect is removed, {@code false} otherwise * @param type the skill abnormal type * @return {@code true} if there was a buff info with the given abnormal type */ public boolean stopSkillEffects(boolean removed, AbnormalType type) { if (_stackedEffects != null) { final BuffInfo old = _stackedEffects.remove(type); if (old != null) { stopSkillEffects(removed, old.getSkill()); return true; } } return false; } /** * Exits all buffs effects of the skills with "removedOnAnyAction" set.<br> * Called on any action except movement (attack, cast). */ public void stopEffectsOnAction() { if (_hasBuffsRemovedOnAnyAction) { boolean update = false; if (hasBuffs()) { getBuffs().values().stream().filter(info -> info.getSkill().isRemovedOnAnyActionExceptMove()).forEach(info -> stopAndRemove(info, getBuffs())); update = true; } if (hasTriggered()) { getTriggered().values().stream().filter(info -> info.getSkill().isRemovedOnAnyActionExceptMove()).forEach(info -> stopAndRemove(info, getTriggered())); update = true; } if (hasDebuffs()) { getDebuffs().values().stream().filter(info -> info.getSkill().isRemovedOnAnyActionExceptMove()).forEach(info -> stopAndRemove(info, getDebuffs())); update = true; } if (hasDances()) { getDances().values().stream().filter(info -> info.getSkill().isRemovedOnAnyActionExceptMove()).forEach(info -> stopAndRemove(info, getDances())); update = true; } if (hasToggles()) { getToggles().values().stream().filter(info -> info.getSkill().isRemovedOnAnyActionExceptMove()).forEach(info -> stopAndRemove(info, getToggles())); update = true; } // Update effect flags and icons. updateEffectList(update); } } public void stopEffectsOnDamage(boolean awake) { if (awake) { boolean update = false; if (_hasBuffsRemovedOnDamage) { if (hasBuffs()) { getBuffs().values().stream().filter(Objects::nonNull).filter(info -> info.getSkill().isRemovedOnDamage()).forEach(info -> stopAndRemove(info, getBuffs())); update = true; } if (hasTriggered()) { getTriggered().values().stream().filter(Objects::nonNull).filter(info -> info.getSkill().isRemovedOnDamage()).forEach(info -> stopAndRemove(info, getTriggered())); update = true; } if (hasDances()) { getDances().values().stream().filter(Objects::nonNull).filter(info -> info.getSkill().isRemovedOnDamage()).forEach(info -> stopAndRemove(info, getDances())); update = true; } if (hasToggles()) { getToggles().values().stream().filter(Objects::nonNull).filter(info -> info.getSkill().isRemovedOnDamage()).forEach(info -> stopAndRemove(info, getToggles())); update = true; } } if (_hasDebuffsRemovedOnDamage) { if (hasDebuffs()) { getDebuffs().values().stream().filter(Objects::nonNull).filter(info -> info.getSkill().isRemovedOnDamage()).forEach(info -> stopAndRemove(info, getDebuffs())); update = true; } } // Update effect flags and icons. updateEffectList(update); } } /** * @param partyOnly */ public void updateEffectIcons(boolean partyOnly) { if (partyOnly) { _partyOnly = true; } // Update effect flags and icons. updateEffectList(true); } /** * Verify if this effect list is empty.<br> * Prevents initialization. * @return {@code true} if this effect list contains any skills */ public boolean isEmpty() { return !hasBuffs() && !hasTriggered() && !hasDances() && !hasDebuffs() && !hasToggles(); } /** * Verify if this effect list has buffs skills.<br> * Prevents initialization. * @return {@code true} if {@link #_buffs} is not {@code null} and is not empty */ public boolean hasBuffs() { return (_buffs != null) && !_buffs.isEmpty(); } /** * Verify if this effect list has triggered skills.<br> * Prevents initialization. * @return {@code true} if {@link #_triggered} is not {@code null} and is not empty */ public boolean hasTriggered() { return (_triggered != null) && !_triggered.isEmpty(); } /** * Verify if this effect list has dance/song skills.<br> * Prevents initialization. * @return {@code true} if {@link #_dances} is not {@code null} and is not empty */ public boolean hasDances() { return (_dances != null) && !_dances.isEmpty(); } /** * Verify if this effect list has toggle skills.<br> * Prevents initialization. * @return {@code true} if {@link #_toggles} is not {@code null} and is not empty */ public boolean hasToggles() { return (_toggles != null) && !_toggles.isEmpty(); } /** * Verify if this effect list has debuffs skills.<br> * Prevents initialization. * @return {@code true} if {@link #_debuffs} is not {@code null} and is not empty */ public boolean hasDebuffs() { return (_debuffs != null) && !_debuffs.isEmpty(); } /** * Verify if this effect list has passive skills.<br> * Prevents initialization. * @return {@code true} if {@link #_passives} is not {@code null} and is not empty */ public boolean hasPassives() { return (_passives != null) && !_passives.isEmpty(); } /** * Executes a procedure for all effects.<br> * Prevents initialization. * @param function the function to execute * @param dances if {@code true} dances/songs will be included */ public void forEach(Function<BuffInfo, Boolean> function, boolean dances) { boolean update = false; if (hasBuffs()) { for (BuffInfo info : getBuffs().values()) { update |= function.apply(info); } } if (hasTriggered()) { for (BuffInfo info : getTriggered().values()) { update |= function.apply(info); } } if (dances && hasDances()) { for (BuffInfo info : getDances().values()) { update |= function.apply(info); } } if (hasToggles()) { for (BuffInfo info : getToggles().values()) { update |= function.apply(info); } } if (hasDebuffs()) { for (BuffInfo info : getDebuffs().values()) { update |= function.apply(info); } } // Update effect flags and icons. updateEffectList(update); } /** * Removes a set of effects from this effect list. * @param removed {@code true} if the effect is removed, {@code false} otherwise * @param info the effects to remove */ public void remove(boolean removed, BuffInfo info) { if ((info == null) || !isAffectedBySkill(info.getSkill().getId())) { return; } // Remove the effect from creature effects. stopAndRemove(removed, info, getEffectList(info.getSkill())); // Update effect flags and icons. updateEffectList(true); } /** * Adds a set of effects to this effect list. * @param info the buff info */ public void add(BuffInfo info) { if (info == null) { return; } // Support for blocked buff slots. final Skill skill = info.getSkill(); if ((_blockedBuffSlots != null) && _blockedBuffSlots.contains(skill.getAbnormalType())) { return; } // Passive effects are treated specially if (skill.isPassive()) { // Passive effects don't need stack type! if (!skill.getAbnormalType().isNone()) { _log.warning("Passive " + skill + " with abnormal type: " + skill.getAbnormalType() + "!"); } // Check for passive skill conditions. if (!skill.checkCondition(info.getEffector(), info.getEffected(), false)) { return; } // Puts the effects in the list. final BuffInfo infoToRemove = getPassives().put(skill.getId(), info); if (infoToRemove != null) { // Removes the old stats from the creature if the skill was present. infoToRemove.setInUse(false); infoToRemove.removeStats(); } // Initialize effects. info.initializeEffects(); return; } // Prevent adding and initializing buffs/effects on dead creatures. if (info.getEffected().isDead()) { return; } // The old effect is removed using Map#remove(key) instead of Map#put(key, value) (that would be the wisest choice), // Because order matters and put method would insert in the same place it was before, instead of, at the end of the effect list // Where new buff should be placed if (skill.getAbnormalType().isNone()) { stopSkillEffects(false, skill); } // Verify stacked skills. else { if (_stackedEffects == null) { synchronized (this) { if (_stackedEffects == null) { _stackedEffects = new ConcurrentHashMap<>(); } } } if (_stackedEffects.containsKey(skill.getAbnormalType())) { BuffInfo stackedInfo = _stackedEffects.get(skill.getAbnormalType()); // Skills are only replaced if the incoming buff has greater or equal abnormal level. if ((stackedInfo != null) && (skill.getAbnormalLvl() >= stackedInfo.getSkill().getAbnormalLvl())) { // If it is an herb, set as not in use the lesser buff. // Effect will be present in the effect list. // Effects stats are removed and onActionTime() is not called. // But finish task continues to run, and ticks as well. if (skill.isAbnormalInstant()) { if (stackedInfo.getSkill().isAbnormalInstant()) { stopSkillEffects(false, skill.getAbnormalType()); stackedInfo = _stackedEffects.get(skill.getAbnormalType()); } if (stackedInfo != null) { stackedInfo.setInUse(false); // Remove stats stackedInfo.removeStats(); _hiddenBuffs.incrementAndGet(); } } // Remove buff that will stack with the abnormal type. else { if (stackedInfo.getSkill().isAbnormalInstant()) { stopSkillEffects(false, skill.getAbnormalType()); } stopSkillEffects(false, skill.getAbnormalType()); } } // If the new buff is a lesser buff, then don't add it. else { return; } } _stackedEffects.put(skill.getAbnormalType(), info); } // Select the map that holds the effects related to this skill. final Map<Integer, BuffInfo> effects = getEffectList(skill); // Remove first buff when buff list is full. if (!skill.isDebuff() && !skill.isToggle() && !skill.is7Signs() && !doesStack(skill)) { int buffsToRemove = -1; if (skill.isDance()) { buffsToRemove = getDanceCount() - Config.DANCES_MAX_AMOUNT; } else if (skill.isTriggeredSkill()) { buffsToRemove = getTriggeredBuffCount() - Config.TRIGGERED_BUFFS_MAX_AMOUNT; } else if (!skill.isHealingPotionSkill()) { buffsToRemove = getBuffCount() - _owner.getStat().getMaxBuffCount(); } for (BuffInfo bi : effects.values()) { if (buffsToRemove < 0) { break; } if (!bi.isInUse()) { continue; } stopAndRemove(bi, effects); buffsToRemove--; } } // After removing old buff (same ID) or stacked buff (same abnormal type), // Add the buff to the end of the effect list. effects.put(skill.getId(), info); // Initialize effects. info.initializeEffects(); // Update effect flags and icons. updateEffectList(true); } /** * Update effect icons.<br> * Prevents initialization. */ private void updateEffectIcons() { if (_owner == null) { return; } updateEffectFlags(); if (!_owner.isPlayable()) { return; } AbnormalStatusUpdate asu = null; PartySpelled ps = null; PartySpelled psSummon = null; ExOlympiadSpelledInfo os = null; boolean isSummon = false; if (_owner.isPlayer()) { if (_partyOnly) { _partyOnly = false; } else { asu = new AbnormalStatusUpdate(); } if (_owner.isInParty()) { ps = new PartySpelled(_owner); } if (_owner.getActingPlayer().isInOlympiadMode() && _owner.getActingPlayer().isOlympiadStart()) { os = new ExOlympiadSpelledInfo(_owner.getActingPlayer()); } } else if (_owner.isSummon()) { isSummon = true; ps = new PartySpelled(_owner); psSummon = new PartySpelled(_owner); } // Buffs. if (hasBuffs()) { for (BuffInfo info : getBuffs().values()) { if (info.getSkill().isHealingPotionSkill()) { shortBuffStatusUpdate(info); } else { addIcon(info, asu, ps, psSummon, os, isSummon); } } } // Triggered buffs. if (hasTriggered()) { for (BuffInfo info : getTriggered().values()) { addIcon(info, asu, ps, psSummon, os, isSummon); } } // Songs and dances. if (hasDances()) { for (BuffInfo info : getDances().values()) { addIcon(info, asu, ps, psSummon, os, isSummon); } } // Songs and dances. if (hasToggles()) { for (BuffInfo info : getToggles().values()) { addIcon(info, asu, ps, psSummon, os, isSummon); } } // Debuffs. if (hasDebuffs()) { for (BuffInfo info : getDebuffs().values()) { addIcon(info, asu, ps, psSummon, os, isSummon); } } if (asu != null) { _owner.sendPacket(asu); } if (ps != null) { if (_owner.isSummon()) { final L2PcInstance summonOwner = ((L2Summon) _owner).getOwner(); if (summonOwner != null) { if (summonOwner.isInParty()) { summonOwner.getParty().broadcastToPartyMembers(summonOwner, psSummon); // send to all member except summonOwner summonOwner.sendPacket(ps); // now send to summonOwner } else { summonOwner.sendPacket(ps); } } } else if (_owner.isPlayer() && _owner.isInParty()) { _owner.getParty().broadcastPacket(ps); } } if (os != null) { final OlympiadGameTask game = OlympiadGameManager.getInstance().getOlympiadTask(_owner.getActingPlayer().getOlympiadGameId()); if ((game != null) && game.isBattleStarted()) { game.getZone().broadcastPacketToObservers(os); } } } private void addIcon(BuffInfo info, AbnormalStatusUpdate asu, PartySpelled ps, PartySpelled psSummon, ExOlympiadSpelledInfo os, boolean isSummon) { // Avoid null and not in use buffs. if ((info == null) || !info.isInUse()) { return; } final Skill skill = info.getSkill(); if (asu != null) { asu.addSkill(info); } if ((ps != null) && (isSummon || !skill.isToggle())) { ps.addSkill(info); } if ((psSummon != null) && !skill.isToggle()) { psSummon.addSkill(info); } if (os != null) { os.addSkill(info); } } /** * Wrapper to update abnormal icons and effect flags. * @param update if {@code true} performs an update */ private void updateEffectList(boolean update) { if (update) { updateEffectIcons(); sendAbnormalStatusUpdateFromTarget(); computeEffectFlags(); } } private void sendAbnormalStatusUpdateFromTarget() { final ExAbnormalStatusUpdateFromTarget upd = new ExAbnormalStatusUpdateFromTarget(_owner); // Go through the StatusListener // Send the Server->Client packet StatusUpdate with current HP and MP // @formatter:off _owner.getStatus().getStatusListener().stream() .filter(Objects::nonNull) .filter(L2Object::isPlayer) .map(L2Character::getActingPlayer) .forEach(upd::sendTo); // @formatter:on if (_owner.isPlayer() && (_owner.getTarget() == _owner)) { _owner.sendPacket(upd); } } /** * Updates effect flags.<br> * TODO: Rework it to update in real time (add/remove/stop/activate/deactivate operations) and avoid iterations. */ private void updateEffectFlags() { if (hasBuffs()) { for (BuffInfo info : getBuffs().values()) { if (info == null) { continue; } if (info.getSkill().isRemovedOnAnyActionExceptMove()) { _hasBuffsRemovedOnAnyAction = true; } if (info.getSkill().isRemovedOnDamage()) { _hasBuffsRemovedOnDamage = true; } } } if (hasTriggered()) { for (BuffInfo info : getTriggered().values()) { if (info == null) { continue; } if (info.getSkill().isRemovedOnAnyActionExceptMove()) { _hasBuffsRemovedOnAnyAction = true; } if (info.getSkill().isRemovedOnDamage()) { _hasBuffsRemovedOnDamage = true; } } } if (hasToggles()) { for (BuffInfo info : getToggles().values()) { if (info == null) { continue; } if (info.getSkill().isRemovedOnAnyActionExceptMove()) { _hasBuffsRemovedOnAnyAction = true; } if (info.getSkill().isRemovedOnDamage()) { _hasBuffsRemovedOnDamage = true; } } } if (hasDebuffs()) { for (BuffInfo info : getDebuffs().values()) { if ((info != null) && info.getSkill().isRemovedOnDamage()) { _hasDebuffsRemovedOnDamage = true; } } } } /** * Recalculate effect bits flag.<br> * TODO: Rework to update in real time and avoid iterations. */ private void computeEffectFlags() { int flags = 0; if (hasBuffs()) { for (BuffInfo info : getBuffs().values()) { if (info != null) { for (AbstractEffect e : info.getEffects()) { flags |= e.getEffectFlags(); } } } } if (hasTriggered()) { for (BuffInfo info : getTriggered().values()) { if (info != null) { for (AbstractEffect e : info.getEffects()) { flags |= e.getEffectFlags(); } } } } if (hasDebuffs()) { for (BuffInfo info : getDebuffs().values()) { if (info != null) { for (AbstractEffect e : info.getEffects()) { flags |= e.getEffectFlags(); } } } } if (hasDances()) { for (BuffInfo info : getDances().values()) { if (info != null) { for (AbstractEffect e : info.getEffects()) { flags |= e.getEffectFlags(); } } } } if (hasToggles()) { for (BuffInfo info : getToggles().values()) { if (info != null) { for (AbstractEffect e : info.getEffects()) { flags |= e.getEffectFlags(); } } } } _effectFlags = flags; } /** * Check if target is affected with special buff * @param flag of special buff * @return boolean true if affected */ public boolean isAffected(EffectFlag flag) { return (_effectFlags & flag.getMask()) != 0; } }