package tc.oc.pgm.damage; import java.util.Iterator; import java.util.List; import javax.annotation.Nullable; import javax.inject.Inject; import org.bukkit.entity.AreaEffectCloud; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.ExperienceOrb; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.entity.ThrownPotion; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.entity.AreaEffectCloudApplyEvent; import org.bukkit.event.entity.EntityCombustByBlockEvent; import org.bukkit.event.entity.EntityCombustByEntityEvent; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityTargetEvent; import org.bukkit.event.entity.PotionSplashEvent; import org.bukkit.event.vehicle.VehicleDamageEvent; import tc.oc.commons.bukkit.util.PotionClassification; import tc.oc.pgm.antigrief.AntiGrief; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.events.MatchPlayerDamageEvent; import tc.oc.pgm.filters.Filter; import tc.oc.pgm.filters.query.DamageQuery; import tc.oc.pgm.filters.query.IDamageQuery; import tc.oc.pgm.filters.query.IQuery; import tc.oc.pgm.match.MatchModule; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.MatchScope; import tc.oc.pgm.match.ParticipantState; import tc.oc.pgm.match.PlayerRelation; import tc.oc.pgm.tracker.EventResolver; import tc.oc.pgm.tracker.EntityResolver; import tc.oc.pgm.tracker.damage.DamageInfo; import tc.oc.pgm.tracker.damage.EntityInfo; import tc.oc.pgm.tracker.damage.ExplosionInfo; import tc.oc.pgm.tracker.damage.FallInfo; import tc.oc.pgm.tracker.damage.FireInfo; import tc.oc.pgm.tracker.damage.GenericDamageInfo; import tc.oc.pgm.tracker.damage.ProjectileInfo; import static com.google.common.base.Preconditions.checkNotNull; @ListenerScope(MatchScope.RUNNING) public class DamageMatchModule extends MatchModule implements Listener { @Inject private EventResolver damageResolver; @Inject private EntityResolver entityResolver; private final List<Filter> filters; DamageMatchModule(List<Filter> filters) { this.filters = filters; } /** * Are players allowed to inflict the given damage on themselves? * Custom rules can override this. */ public static boolean isAllowedSelfDamage(DamageInfo damageInfo) { // Disable self-damage with arrows if(damageInfo instanceof ProjectileInfo) { ProjectileInfo projectileInfo = (ProjectileInfo) damageInfo; if(projectileInfo.getProjectile() instanceof EntityInfo && ((EntityInfo) projectileInfo.getProjectile()).getEntityType() == EntityType.ARROW) { return false; } } return true; } /** * Is the given damage of a type that is always allowed against teammates, * even if friendly fire is disabled? Custom rules can override this. */ public static boolean isAllowedTeamDamage(DamageInfo damageInfo) { return damageInfo instanceof ExplosionInfo || damageInfo instanceof FireInfo || damageInfo instanceof FallInfo || damageInfo instanceof GenericDamageInfo; // This should never have an attacker anyway, but just in case } /** * Test if the given damage/attack is allowed by the default damage policies */ public Filter.QueryResponse queryDefaultRules(ParticipantState victim, DamageInfo damageInfo) { switch(PlayerRelation.get(victim, damageInfo.getAttacker())) { case SELF: if(!isAllowedSelfDamage(damageInfo)) { return Filter.QueryResponse.DENY; } case ALLY: if(!match.getMapInfo().friendlyFire && !isAllowedTeamDamage(damageInfo)) { return Filter.QueryResponse.DENY; } default: return Filter.QueryResponse.ABSTAIN; } } /** * Get a query for the given damage event */ public IDamageQuery getQuery(Event event, ParticipantState victim, DamageInfo damageInfo) { return DamageQuery.victimDefault(event, victim, damageInfo); } /** * Query the custom damage filters with the given damage event */ public Filter.QueryResponse queryRules(Event event, ParticipantState victim, DamageInfo damageInfo) { IQuery query = getQuery(event, victim, damageInfo); for(Filter filter : filters) { Filter.QueryResponse response = filter.query(query); if(response != Filter.QueryResponse.ABSTAIN) return response; } return Filter.QueryResponse.ABSTAIN; } /** * Query whether the given damage is both allowed and incentivized for the attacker. */ public Filter.QueryResponse queryHostile(Event event, ParticipantState victim, DamageInfo damageInfo) { switch(PlayerRelation.get(victim, damageInfo.getAttacker())) { case SELF: case ALLY: // Players don't want to hurt themselves or their teammates return Filter.QueryResponse.DENY; default: // They also don't want to waste time trying to inflict damage that will be filtered out return queryRules(event, victim, damageInfo); } } /** * Query whether the given damage is allowed or not. Both the custom damage filters and the friendly fire policy * are considered, the former having priority over the latter. */ public Filter.QueryResponse queryDamage(Event event, ParticipantState victim, DamageInfo damageInfo) { Filter.QueryResponse response = queryRules(event, victim, damageInfo); if(response != Filter.QueryResponse.ABSTAIN) return response; return queryDefaultRules(victim, damageInfo); } /** * Query the given damage event and cancel it if the result was denied. */ public Filter.QueryResponse processDamageEvent(Event event, ParticipantState victim, DamageInfo damageInfo) { Filter.QueryResponse response = queryDamage(checkNotNull(event), victim, damageInfo); if(response.isDenied() && event instanceof Cancellable) { ((Cancellable) event).setCancelled(true); } return response; } /** * Search the rider stack for a participant */ @Nullable MatchPlayer getVictim(Entity entity) { if(entity == null) return null; MatchPlayer victim = getMatch().getParticipant(entity); if(victim != null) { return victim; } else if(AntiGrief.VechicleProtect.enabled()) { return getVictim(entity.getPassenger()); } else { return null; } } @EventHandler(ignoreCancelled = true) public void onDamage(EntityDamageEvent event) { MatchPlayer victim = getVictim(event.getEntity()); if(victim == null) return; final DamageInfo info = damageResolver.resolveDamage(event); processDamageEvent(event, victim.getParticipantState(), info); if(!event.isCancelled()) { getMatch().callEvent(new MatchPlayerDamageEvent(event, victim, info)); } } @EventHandler(ignoreCancelled = true) public void onDamageVehicle(VehicleDamageEvent event) { MatchPlayer victim = getVictim(event.getVehicle()); if(victim == null) return; processDamageEvent(event, victim.getParticipantState(), damageResolver.resolveDamage(EntityDamageEvent.DamageCause.CUSTOM, event.getVehicle(), event.getAttacker())); } @EventHandler(ignoreCancelled = true) public void onIgnition(EntityCombustByEntityEvent event) { MatchPlayer victim = getVictim(event.getEntity()); if(victim == null) return; processDamageEvent(event, victim.getParticipantState(), damageResolver.resolveDamage(EntityDamageEvent.DamageCause.FIRE, event.getEntity(), event.getCombuster())); } @EventHandler(ignoreCancelled = true) public void onIgnition(EntityCombustByBlockEvent event) { MatchPlayer victim = getVictim(event.getEntity()); if(victim == null) return; processDamageEvent(event, victim.getParticipantState(), damageResolver.resolveDamage(EntityDamageEvent.DamageCause.FIRE, event.getEntity(), event.getCombuster())); } @EventHandler(ignoreCancelled = true) public void onPotionSplash(final PotionSplashEvent event) { final ThrownPotion potion = event.getPotion(); if(PotionClassification.classify(potion) != PotionClassification.HARMFUL) return; for(LivingEntity victim : event.getAffectedEntities()) { final ParticipantState victimState = getMatch().getParticipantState(victim); final DamageInfo damageInfo = damageResolver.resolveDamage(EntityDamageEvent.DamageCause.MAGIC, victim, potion); if(victimState != null && queryDamage(event, victimState, damageInfo).isDenied()) { event.setIntensity(victim, 0); } } } @EventHandler public void onPotionLinger(final AreaEffectCloudApplyEvent event) { final AreaEffectCloud cloud = event.getEntity(); if(PotionClassification.classify(cloud) != PotionClassification.HARMFUL) return; for(Iterator<LivingEntity> iterator = event.getAffectedEntities().iterator(); iterator.hasNext(); ) { final LivingEntity victim = iterator.next(); final ParticipantState victimState = getMatch().getParticipantState(victim); final DamageInfo damageInfo = damageResolver.resolveDamage(EntityDamageEvent.DamageCause.MAGIC, victim, cloud); if(victimState != null && queryDamage(event, victimState, damageInfo).isDenied()) { iterator.remove(); } } } @EventHandler(ignoreCancelled = true) public void onTarget(EntityTargetEvent event) { if(!(event.getEntity() instanceof ExperienceOrb)) { ParticipantState victimState = null; if(event.getTarget() instanceof Player) { // Don't target allies MatchPlayer victim = getVictim(event.getTarget()); if(victim == null) return; victimState = victim.getParticipantState(); } else if(event.getTarget() != null) { // Don't target other mobs owned by allies victimState = entityResolver.getOwner(event.getTarget()); } if(victimState == null) return; DamageInfo damageInfo = damageResolver.resolveDamage(EntityDamageEvent.DamageCause.ENTITY_ATTACK, event.getTarget(), event.getEntity()); if(queryHostile(event, victimState, damageInfo).isDenied()) { event.setCancelled(true); } } } }