public interface DamageResolverManager {
    /**
     * Indicates whether the specified resolver is already registered.
     *
     * @param resolver Specified resolver
     * @return true if the resolver is registered, false otherwise
     *
     * @throws NullPointerException if resolver is null
     */
    boolean isRegistered(@Nonnull DamageResolver resolver);

    /**
     * Registers the specified resolver.
     *
     * @param resolver Specified resolver
     *
     * @throws NullPointerException if resolver is null
     */
    void register(@Nonnull DamageResolver resolver);

    /**
     * Registers the specified resolver indicating that certain resolvers must
     * be called before the specified one.
     *
     * @param resolver Specified resolver
     * @param before Classes of resolvers that must be called before this one
     *
     * @throws NullPointerException if resolver or before is null
     * @throws IllegalArgumentException if the constraints specified by the
     *                                  before classes cannot be solved
     */
    void register(@Nonnull DamageResolver resolver, @Nonnull Class<? extends DamageResolver>... before);

    /**
     * Registers the specified resolver indication that certain resolvers must
     * be called before the specified one.
     *
     * @param resolver Specified resolver
     * @param before Classes of resolvers that must be called before this one
     *
     * @throws NullPointerException if resolver or before is null
     * @throws IllegalArgumentException if the constraints specified by the
     *                                  before classes cannot be solved
     */
    void register(@Nonnull DamageResolver resolver, @Nonnull Collection<Class<? extends DamageResolver>> before);

    /**
     * Unregisters the specified resolver.
     *
     * @param resolver Specified resolver
     *
     * @throws NullPointerException if resolver is null
     */
    void unregister(@Nonnull DamageResolver resolver);

    /**
     * Gets the set of resolvers in the order
     *
     * @return Set of resolvers in the order they should be called
     */
    @Nonnull Set<DamageResolver> getResolvers();

    /**
     * Resolves the specified damage event using the set of registered
     * resolvers.
     *
     * If no resolver can resolve the specified event, this method will return
     * a generic {@link BukkitDamageInfo} instance.
     *
     * @param entity Entity that is being damaged
     * @param lifetime Lifetime of the entity up to this point
     * @param damageEvent Bukkit damage event to resolve
     * @return Resolved damage information (never null)
     *
     * @throws NullPointerException if entity, lifetime, or the event is null
     */
    @Nonnull DamageInfo resolve(@Nonnull LivingEntity entity, @Nonnull Lifetime lifetime, @Nonnull EntityDamageEvent damageEvent);
} public final class DamageResolvers {
    private DamageResolvers() { }

    public static @Nonnull DamageResolverManager getManager() {
        if(manager == null) {
            manager = new SimpleResolverManager();
        }
        return manager;
    }

    private static @Nullable DamageResolverManager manager = null;
} public interface TrackerManager {
    Set<Tracker> getTrackers();

    boolean hasTracker(@Nonnull Class<? extends Tracker> trackerClass);

    @Nullable <T extends Tracker> T getTracker(@Nonnull Class<T> trackerClass);

    @Nullable <T extends Tracker> T setTracker(@Nonnull Class<T> trackerClass, @Nullable T tracker);

    @Nullable <T extends Tracker> T clearTracker(@Nonnull Class<T> trackerClass);

    @Nullable <T extends Tracker> T clearTracker(@Nonnull Class<T> trackerClass, @Nonnull Class<? extends T> trackerImplClass);

    void clearTrackers(@Nonnull World world);
} public final class Trackers {
    private Trackers() { }

    public static @Nonnull TrackerManager getManager() {
        if(manager == null) {
            manager = new SimpleTrackerManager();
        }
        return manager;
    }

    /**
     * Utility method for {@link TrackerManager#getTracker}
     */
    public static <T extends Tracker> T getTracker(@Nonnull Class<T> trackerClass) {
        return getManager().getTracker(trackerClass);
    }

    private static TrackerManager manager;
} public class AnvilDamageResolver implements DamageResolver {
    private final AnvilTracker anvilTracker;

    public AnvilDamageResolver(AnvilTracker anvilTracker) {
        this.anvilTracker = anvilTracker;
    }

    public DamageInfo resolve(LivingEntity entity, Lifetime lifetime, EntityDamageEvent damageEvent) {
        if(damageEvent instanceof EntityDamageByEntityEvent) {
            EntityDamageByEntityEvent event = (EntityDamageByEntityEvent) damageEvent;

            if(event.getDamager() instanceof FallingBlock) {
                FallingBlock anvil = (FallingBlock) event.getDamager();
                OfflinePlayer offlineOwner = this.anvilTracker.getOwner(anvil);
                Player onlineOwner = null;

                if(offlineOwner != null) onlineOwner = offlineOwner.getPlayer();

                return new AnvilDamageInfo(anvil, onlineOwner, offlineOwner);
            }
        }

        return null;
    }

} public class BlockDamageResolver implements DamageResolver {
    public @Nullable DamageInfo resolve(@Nonnull LivingEntity entity, @Nonnull Lifetime lifetime, @Nonnull EntityDamageEvent damageEvent) {
        if(damageEvent instanceof EntityDamageByBlockEvent && damageEvent.getCause() == DamageCause.CONTACT) {
            EntityDamageByBlockEvent blockEvent = (EntityDamageByBlockEvent) damageEvent;

            return new SimpleBlockDamageInfo(blockEvent.getDamager().getState());
        }

        return null;
    }
} public class DispensedProjectileDamageResolver implements DamageResolver {
    private final ProjectileDistanceTracker projectileDistanceTracker;
    private final DispenserTracker dispenserTracker;

    public DispensedProjectileDamageResolver(ProjectileDistanceTracker projectileDistanceTracker, DispenserTracker dispenserTracker) {
        this.projectileDistanceTracker = projectileDistanceTracker;
        this.dispenserTracker = dispenserTracker;
    }

    public @Nullable DamageInfo resolve(@Nonnull LivingEntity entity, @Nonnull Lifetime lifetime, @Nonnull EntityDamageEvent damageEvent) {
        if(damageEvent instanceof EntityDamageByEntityEvent) {
            EntityDamageByEntityEvent event = (EntityDamageByEntityEvent) damageEvent;

            if(event.getDamager() instanceof Projectile) {
                Projectile projectile = (Projectile) event.getDamager();
                Location launchLocation = this.projectileDistanceTracker.getLaunchLocation(projectile);
                Double projectileDistance = null;
                OfflinePlayer dispenserOwner = dispenserTracker.getOwner(projectile);


                if(launchLocation != null) projectileDistance = event.getEntity().getLocation().distance(launchLocation);

                if(projectile.getShooter() instanceof LivingEntity) {
                    return new DispensedProjectileDamageInfo(projectile, (LivingEntity) projectile.getShooter(), projectileDistance, dispenserOwner);
                }
            }
        }
        return null;
    }
} public class FallDamageResolver implements DamageResolver {
    public @Nullable DamageInfo resolve(@Nonnull LivingEntity entity, @Nonnull Lifetime lifetime, @Nonnull EntityDamageEvent damageEvent) {
        if(damageEvent.getCause() == DamageCause.FALL) {
            float fallDistance = Math.max(0, entity.getFallDistance());

            return new SimpleFallDamageInfo(null, fallDistance);
        }

        return null;
    }
} public class GravityDamageResolver implements DamageResolver {
    public GravityDamageResolver(@Nonnull SimpleGravityKillTracker tracker) {
        Preconditions.checkNotNull(tracker, "tracker");
        this.tracker = tracker;
    }

    public @Nullable DamageInfo resolve(@Nonnull LivingEntity entity, @Nonnull Lifetime lifetime, @Nonnull EntityDamageEvent damageEvent) {
        if(!(entity instanceof Player)) return null;
        Player victim = (Player) entity;
        Fall fall = this.tracker.getCausingFall(victim, damageEvent.getCause());
        if(fall != null) {
            return new GravityDamageInfo(fall.attacker, fall.cause, fall.from);
        } else {
            return null;
        }
    }

    private final @Nonnull SimpleGravityKillTracker tracker;
} public class LavaDamageResolver implements DamageResolver {
    public @Nullable DamageInfo resolve(@Nonnull LivingEntity entity, @Nonnull Lifetime lifetime, @Nonnull EntityDamageEvent damageEvent) {
        if(damageEvent.getCause() == DamageCause.LAVA) {
            return new SimpleLavaDamageInfo(null);
        }

        return null;
    }
} public class MeleeDamageResolver implements DamageResolver {
    public @Nullable DamageInfo resolve(@Nonnull LivingEntity entity, @Nonnull Lifetime lifetime, @Nonnull EntityDamageEvent damageEvent) {
        if(damageEvent instanceof EntityDamageByEntityEvent && damageEvent.getCause() == DamageCause.ENTITY_ATTACK) {
            EntityDamageByEntityEvent entityEvent = (EntityDamageByEntityEvent) damageEvent;

            if(entityEvent.getDamager() instanceof LivingEntity) {
                LivingEntity attacker = (LivingEntity) entityEvent.getDamager();

                Material weaponMaterial;
                ItemStack held = attacker.getEquipment().getItemInHand();
                if(held != null) {
                    weaponMaterial = held.getType();
                } else {
                    weaponMaterial = Material.AIR;
                }

                return new SimpleMeleeDamageInfo(attacker, weaponMaterial);
            }
        }

        return null;
    }
} public class OwnedMobDamageResolver implements DamageResolver {
    private final OwnedMobTracker ownedMobTracker;

    public OwnedMobDamageResolver(OwnedMobTracker ownedMobTracker) {
        this.ownedMobTracker = ownedMobTracker;
    }

    public @Nullable DamageInfo resolve(@Nonnull LivingEntity entity, @Nonnull Lifetime lifetime, @Nonnull EntityDamageEvent damageEvent) {
        if(damageEvent instanceof EntityDamageByEntityEvent) {
            EntityDamageByEntityEvent event = (EntityDamageByEntityEvent) damageEvent;

            if(event.getDamager() instanceof Projectile) {
                if(((Projectile) event.getDamager()).getShooter() == null) return null;
                if(!(((Projectile) event.getDamager()).getShooter() instanceof Player) && ((Projectile) event.getDamager()).getShooter() instanceof LivingEntity) {
                    LivingEntity mob = (LivingEntity) ((Projectile) event.getDamager()).getShooter();
                    Player mobOwner = this.ownedMobTracker.getOwner(mob);

                    if(mobOwner != null) return new OwnedMobDamageInfo(mob, mobOwner, (Projectile) event.getDamager());
                }
            } else if(!(event.getDamager() instanceof Player) && event.getDamager() instanceof LivingEntity) {
                LivingEntity mob = (LivingEntity) event.getDamager();
                Player mobOwner = this.ownedMobTracker.getOwner(mob);

                if(mobOwner != null) return new OwnedMobDamageInfo(mob, mobOwner, null);
            }
        }
        return null;
    }
} public class ProjectileDamageResolver implements DamageResolver {
    private final ProjectileDistanceTracker projectileDistanceTracker;

    public ProjectileDamageResolver(ProjectileDistanceTracker projectileDistanceTracker) {
        this.projectileDistanceTracker = projectileDistanceTracker;
    }

    public @Nullable DamageInfo resolve(@Nonnull LivingEntity entity, @Nonnull Lifetime lifetime, @Nonnull EntityDamageEvent damageEvent) {
        if(damageEvent instanceof EntityDamageByEntityEvent) {
            EntityDamageByEntityEvent event = (EntityDamageByEntityEvent) damageEvent;

            if(event.getDamager() instanceof Projectile) {
                Projectile projectile = (Projectile) event.getDamager();
                Location launchLocation = this.projectileDistanceTracker.getLaunchLocation(projectile);
                Double projectileDistance = null;

                if(launchLocation != null) projectileDistance = event.getEntity().getLocation().distance(launchLocation);

                if(projectile.getShooter() instanceof LivingEntity) {
                    return new ProjectileDamageInfo(projectile, (LivingEntity) projectile.getShooter(), projectileDistance);
                }
            }
        }
        return null;
    }
} public class TNTDamageResolver implements DamageResolver {
    private final ExplosiveTracker explosiveTracker;
    private final DispenserTracker dispenserTracker;

    public TNTDamageResolver(ExplosiveTracker explosiveTracker, DispenserTracker dispenserTracker) {
        this.explosiveTracker = explosiveTracker;
        this.dispenserTracker = dispenserTracker;
    }

    public @Nullable DamageInfo resolve(@Nonnull LivingEntity entity, @Nonnull Lifetime lifetime, @Nonnull EntityDamageEvent damageEvent) {
        if(damageEvent instanceof EntityDamageByEntityEvent) {
            EntityDamageByEntityEvent event = (EntityDamageByEntityEvent) damageEvent;

            if(event.getDamager() instanceof TNTPrimed) {
                TNTPrimed tnt = (TNTPrimed) event.getDamager();
                Player owner = null;
                if(this.explosiveTracker.hasOwner(tnt)) {
                    owner = this.explosiveTracker.getOwner(tnt);
                } else if(this.dispenserTracker.hasOwner(tnt)) {
                    // getPlayer() is temporary to keep owner uniform until other trackers use OfflinePlayer
                    owner = this.dispenserTracker.getOwner(tnt).getPlayer();
                }

                return new TNTDamageInfo(tnt, owner);
            }
        }

        return null;
    }
} public class VoidDamageResolver implements DamageResolver {
    public @Nullable DamageInfo resolve(@Nonnull LivingEntity entity, @Nonnull Lifetime lifetime, @Nonnull EntityDamageEvent damageEvent) {
        if(damageEvent.getCause() == DamageCause.VOID) {
            return new SimpleVoidDamageInfo(null);
        }

        return null;
    }
} public class TickTimer {
    private final JavaPlugin plugin;
    private int taskId;
    private long ticks = 0;
    private boolean running;

    public TickTimer(JavaPlugin plugin) {
        this.plugin = plugin;
    }

    public void setTicks(long ticks) {
        this.ticks = ticks;
    }

    public long getTicks() {
        return this.ticks;
    }

    public boolean isRunning() {
        return this.running;
    }

    public void start() {
        if (this.running) {
            throw new RuntimeException("Timer already running");
        }

        this.taskId = this.plugin.getServer().getScheduler().scheduleSyncRepeatingTask(this.plugin, new Runnable() {
            public void run() {
                TickTimer.this.ticks++;
            }
        }, 1L, 1L);

        this.running = true;
    }

    public void stop() {
        if (!this.running) {
            throw new RuntimeException("Timer not running");
        }

        this.plugin.getServer().getScheduler().cancelTask(this.taskId);
    }
} public interface AnvilTracker extends Tracker {
    boolean hasOwner(@Nonnull FallingBlock entity);

    @Nullable OfflinePlayer getOwner(@Nonnull FallingBlock anvil);

    @Nullable OfflinePlayer setOwner(@Nonnull FallingBlock anvil, @Nullable OfflinePlayer offlinePlayer);

    boolean hasPlacer(@Nonnull Block block);

    @Nullable OfflinePlayer getPlacer(@Nonnull Block block);

    @Nullable OfflinePlayer setPlacer(@Nonnull Block block, @Nonnull OfflinePlayer offlinePlayer);

    @Nonnull OfflinePlayer clearPlacer(@Nullable Block block);
} public interface DispenserTracker extends Tracker {
    boolean hasOwner(@Nonnull Entity entity);

    @Nullable OfflinePlayer getOwner(@Nonnull Entity entity);

    @Nullable OfflinePlayer setOwner(@Nonnull Entity entity, @Nullable Player player);

    boolean hasPlacer(@Nonnull Block block);

    @Nullable OfflinePlayer getPlacer(@Nonnull Block block);

    @Nullable OfflinePlayer setPlacer(@Nonnull Block block, @Nullable Player player);

    @Nonnull OfflinePlayer clearPlacer(@Nonnull Block block);
} public interface ExplosiveTracker extends Tracker {
    boolean hasOwner(@Nonnull TNTPrimed entity);

    @Nullable Player getOwner(@Nonnull TNTPrimed entity);

    @Nullable Player setOwner(@Nonnull TNTPrimed entity, @Nullable Player player);

    boolean hasPlacer(@Nonnull Block block);

    @Nullable Player getPlacer(@Nonnull Block block);

    @Nullable Player setPlacer(@Nonnull Block block, @Nullable Player player);
} public interface OwnedMobTracker extends Tracker {
    boolean hasOwner(@Nonnull LivingEntity entity);

    @Nullable Player getOwner(@Nonnull LivingEntity entity);

    @Nullable Player setOwner(@Nonnull LivingEntity entity, @Nullable Player player);
} public interface ProjectileDistanceTracker extends Tracker {
    boolean hasLaunchLocation(@Nonnull Projectile entity);

    @Nullable Location getLaunchLocation(@Nonnull Projectile projectile);

    @Nullable Location setLaunchLocation(@Nonnull Projectile projectile, @Nullable Location location);
} public class SimpleAnvilTracker extends AbstractTracker implements AnvilTracker {
    HashMap<Block, OfflinePlayer> placedAnvils = Maps.newHashMap();
    HashMap<FallingBlock, OfflinePlayer> ownedAnvils = Maps.newHashMap();

    public boolean hasOwner(@Nonnull FallingBlock anvil) {
        Preconditions.checkNotNull(anvil, "anvil");

        return this.ownedAnvils.containsKey(anvil);
    }

    @Nullable
    public OfflinePlayer getOwner(@Nonnull FallingBlock anvil) {
        Preconditions.checkNotNull(anvil, "anvil");

        return this.ownedAnvils.get(anvil);
    }

    @Nullable
    public OfflinePlayer setOwner(@Nonnull FallingBlock anvil, @Nullable OfflinePlayer offlinePlayer) {
        Preconditions.checkNotNull(anvil, "anvil");

        if(offlinePlayer != null) {
            return this.ownedAnvils.put(anvil, offlinePlayer);
        } else {
            return this.ownedAnvils.remove(anvil);
        }
    }

    public boolean hasPlacer(@Nonnull Block block) {
        Preconditions.checkNotNull(block, "block");

        return this.placedAnvils.containsKey(block);
    }

    @Nullable
    public OfflinePlayer getPlacer(@Nonnull Block block) {
        Preconditions.checkNotNull(block, "block");

        return this.placedAnvils.get(block);
    }

    @Nullable
    public OfflinePlayer setPlacer(@Nonnull Block block, @Nonnull OfflinePlayer offlinePlayer) {
        Preconditions.checkNotNull(block, "block");
        Preconditions.checkNotNull(offlinePlayer, "player");

        return this.placedAnvils.put(block, offlinePlayer);
    }

    @Nonnull
    public OfflinePlayer clearPlacer(@Nullable Block block) {
        Preconditions.checkNotNull(block, "block");

        return this.placedAnvils.remove(block);
    }

    public void clear(@Nonnull World world) {
        // clear information about blocks in that world
        Iterator<Map.Entry<Block, OfflinePlayer>> blockIt = this.placedAnvils.entrySet().iterator();
        while(blockIt.hasNext()) {
            Block block = blockIt.next().getKey();
            if(block.getWorld().equals(world)) {
                blockIt.remove();
            }
        }

        // clear information about entitys in that world
        Iterator<Map.Entry<FallingBlock, OfflinePlayer>> entityIt = this.ownedAnvils.entrySet().iterator();
        while(entityIt.hasNext()) {
            Entity tnt = entityIt.next().getKey();
            if(tnt.getWorld().equals(world)) {
                entityIt.remove();
            }
        }
    }

} public class SimpleDispenserTracker extends AbstractTracker implements DispenserTracker {
    private final HashMap<Block, OfflinePlayer> placedDispensers = Maps.newHashMap();
    private final HashMap<Entity, OfflinePlayer> ownedEntitys = Maps.newHashMap();

    public boolean hasOwner(@Nonnull Entity entity) {
        Preconditions.checkNotNull(entity, "entity");

        return this.ownedEntitys.containsKey(entity);
    }

    public @Nullable OfflinePlayer getOwner(@Nonnull Entity entity) {
        Preconditions.checkNotNull(entity, "entity");

        return this.ownedEntitys.get(entity);
    }

    public @Nullable OfflinePlayer setOwner(@Nonnull Entity entity, @Nullable Player player) {
        Preconditions.checkNotNull(entity, "tnt entity");

        if(player != null) {
            return this.ownedEntitys.put(entity, player);
        } else {
            return this.ownedEntitys.remove(entity);
        }
    }

    public boolean hasPlacer(@Nonnull Block block) {
        Preconditions.checkNotNull(block, "block");

        return this.placedDispensers.containsKey(block);
    }

    public @Nullable OfflinePlayer getPlacer(@Nonnull Block block) {
        Preconditions.checkNotNull(block, "block");

        return this.placedDispensers.get(block);
    }

    public @Nullable OfflinePlayer setPlacer(@Nonnull Block block, @Nonnull Player player) {
        Preconditions.checkNotNull(block, "block");
        Preconditions.checkNotNull(player, "player");

        return this.placedDispensers.put(block, player.getPlayer());
    }

    public @Nonnull OfflinePlayer clearPlacer(@Nonnull Block block) {
        Preconditions.checkNotNull(block, "block");

        return this.placedDispensers.remove(block);
    }

    public void clear(@Nonnull World world) {
        // clear information about blocks in that world
        Iterator<Map.Entry<Block, OfflinePlayer>> blockIt = this.placedDispensers.entrySet().iterator();
        while(blockIt.hasNext()) {
            Block block = blockIt.next().getKey();
            if(block.getWorld().equals(world)) {
                blockIt.remove();
            }
        }

        // clear information about entitys in that world
        Iterator<Map.Entry<Entity, OfflinePlayer>> entityIt = this.ownedEntitys.entrySet().iterator();
        while(entityIt.hasNext()) {
            Entity tnt = entityIt.next().getKey();
            if(tnt.getWorld().equals(world)) {
                entityIt.remove();
            }
        }
    }
} public class SimpleExplosiveTracker extends AbstractTracker implements ExplosiveTracker {
    private final Map<Block, Player> placedBlocks = Maps.newHashMap();
    private final Map<TNTPrimed, Player> ownedTNTs = Maps.newHashMap();

    public boolean hasOwner(@Nonnull TNTPrimed entity) {
        Preconditions.checkNotNull(entity, "tnt entity");

        return this.ownedTNTs.containsKey(entity);
    }

    public @Nullable Player getOwner(@Nonnull TNTPrimed entity) {
        Preconditions.checkNotNull(entity, "tnt entity");

        return this.ownedTNTs.get(entity);
    }

    public @Nullable Player setOwner(@Nonnull TNTPrimed entity, @Nullable Player player) {
        Preconditions.checkNotNull(entity, "tnt entity");

        if(player != null) {
            return this.ownedTNTs.put(entity, player);
        } else {
            return this.ownedTNTs.remove(entity);
        }
    }

    public boolean hasPlacer(@Nonnull Block block) {
        Preconditions.checkNotNull(block, "block");

        return this.placedBlocks.containsKey(block);
    }

    public @Nullable Player getPlacer(@Nonnull Block block) {
        Preconditions.checkNotNull(block, "block");

        return this.placedBlocks.get(block);
    }

    public @Nullable Player setPlacer(@Nonnull Block block, @Nullable Player player) {
        Preconditions.checkNotNull(block, "block");

        if(player != null) {
            return this.placedBlocks.put(block, player);
        } else {
            return this.placedBlocks.remove(block);
        }
    }

    public void clear(@Nonnull World world) {
        Preconditions.checkNotNull(world, "world");

        // clear information about blocks in that world
        for(Iterator<Map.Entry<Block, Player>> it = this.placedBlocks.entrySet().iterator(); it.hasNext(); ) {
            Block block = it.next().getKey();
            if(block.getWorld().equals(world)) {
                it.remove();
            }
        }

        // clear information about entities in that world
        for(Iterator<Map.Entry<TNTPrimed, Player>> it = this.ownedTNTs.entrySet().iterator(); it.hasNext(); ) {
            TNTPrimed tnt = it.next().getKey();
            if(tnt.getWorld().equals(world)) {
                it.remove();
            }
        }
    }
} public class SimpleOwnedMobTracker extends AbstractTracker implements OwnedMobTracker {
    private final Map<LivingEntity, Player> ownedMobs = Maps.newHashMap();

    public boolean hasOwner(@Nonnull LivingEntity entity) {
        Preconditions.checkNotNull(entity, "entity");

        return this.ownedMobs.containsKey(entity);
    }

    public @Nullable Player getOwner(@Nonnull LivingEntity entity) {
        Preconditions.checkNotNull(entity, "entity");

        return this.ownedMobs.get(entity);
    }

    public @Nonnull Player setOwner(@Nonnull LivingEntity entity, @Nonnull Player player) {
        Preconditions.checkNotNull(entity, "entity");
        Preconditions.checkNotNull(player, "player");

        return this.ownedMobs.put(entity, player);
    }

    public @Nonnull Player clearOwner(@Nonnull LivingEntity entity) {
        Preconditions.checkNotNull(entity, "entity");

        return this.ownedMobs.remove(entity);
    }

    public void clear(@Nonnull World world) {
        Preconditions.checkNotNull(world, "world");

        // clear information about owned mobs in that world
        Iterator<Map.Entry<LivingEntity, Player>> mobIt = this.ownedMobs.entrySet().iterator();
        while(mobIt.hasNext()) {
            LivingEntity entity = mobIt.next().getKey();
            if(entity.getWorld().equals(world)) {
                entity.remove();
            }
        }
    }
} public class SimpleProjectileDistanceTracker extends AbstractTracker implements ProjectileDistanceTracker {
    private final HashMap<Projectile, Location> projectileLaunchLocations = Maps.newHashMap();

    public boolean hasLaunchLocation(@Nonnull Projectile projectile) {
        Preconditions.checkNotNull(projectile, "projectile entity");

        return this.projectileLaunchLocations.containsKey(projectile);
    }

    public @Nullable Location getLaunchLocation(@Nonnull Projectile projectile) {
        Preconditions.checkNotNull(projectile, "projectile entity");

        return this.projectileLaunchLocations.get(projectile);
    }

    public @Nullable Location setLaunchLocation(@Nonnull Projectile projectile, @Nullable Location location) {
        Preconditions.checkNotNull(projectile, "projectile entity");

        if(location != null) {
            return this.projectileLaunchLocations.put(projectile, location);
        } else {
            return this.projectileLaunchLocations.remove(projectile);
        }
    }

    public void clear(World world) {
        // clear information about projectile launch locations in that world
        Iterator<Map.Entry<Projectile, Location>> projectileIt = this.projectileLaunchLocations.entrySet().iterator();
        while(projectileIt.hasNext()) {
            Projectile projectile = projectileIt.next().getKey();
            if(projectile.getWorld().equals(world)) {
                projectileIt.remove();
            }
        }
    }
} public class SimpleGravityKillTracker extends AbstractTracker {
    private final HashMap<Location, BrokenBlock> brokenBlocks = new HashMap<Location, BrokenBlock>();
    private final HashMap<Player, Fall> falls = new HashMap<Player, Fall>();

    // A player must leave the ground within this many ticks of being attacked for
    // the fall to be caused by knockback from that attack
    public static final long MAX_KNOCKBACK_TIME = 20;

    // A player must leave the ground within this many ticks of a block being broken
    // under them for the fall to be caused by a spleef from that block
    public static final long MAX_SPLEEF_TIME = 20;

    // A player's fall is cancelled if they are on the ground continuously for more than this many ticks
    public static final long MAX_ON_GROUND_TIME = 10;

    // A player's fall is cancelled if they touch the ground more than this many times
    public static final long MAX_GROUND_TOUCHES = 2;

    // A player's fall is cancelled if they are in water for more than this many ticks
    public static final long MAX_SWIMMING_TIME = 20;

    // A player's fall is cancelled if they are climbing something for more than this many ticks
    public static final long MAX_CLIMBING_TIME = 10;

    private final TrackerPlugin plugin;
    private final TickTimer timer;

    public SimpleGravityKillTracker(TrackerPlugin plugin, TickTimer timer) {
        this.plugin = plugin;
        this.timer = timer;
    }

    public void clear(@Nonnull World world) {
        Iterator<Location> blocks = this.brokenBlocks.keySet().iterator();
        while(blocks.hasNext()) {
            Location loc = blocks.next();
            if(loc.getWorld() == world) {
                blocks.remove();
            }
        }

        Iterator<Player> players = this.falls.keySet().iterator();
        while(players.hasNext()) {
            Player player = players.next();
            if(player.getWorld() == world) {
                players.remove();
            }
        }
    }

    private boolean isSupported(final Fall fall) {
        return fall.isClimbing || fall.isSwimming || fall.victim.isOnGround();
    }

    private void cancelFall(Fall fall) {
        this.falls.remove(fall.victim);
    }

    private void checkFallCancelled(final Fall fall) {
        long now = this.timer.getTicks();
        if(this.falls.get(fall.victim) == fall) {
            if(fall.isFalling) {
                if (!fall.isInLava) {
                    if (fall.victim.isOnGround()) {
                        if(now - fall.onGroundTime > MAX_ON_GROUND_TIME) this.cancelFall(fall);
                        if(fall.groundTouchCount > MAX_GROUND_TOUCHES) this.cancelFall(fall);
                    }
                    if (fall.isSwimming && now - fall.swimmingTime > MAX_SWIMMING_TIME) this.cancelFall(fall);
                    if (fall.isClimbing && now - fall.climbingTime > MAX_CLIMBING_TIME) this.cancelFall(fall);
                }
            } else {
                if (fall.victim.isOnGround() && now - fall.attackTime > MAX_KNOCKBACK_TIME) this.cancelFall(fall);
                if (fall.isSwimming && now - fall.attackTime > MAX_KNOCKBACK_TIME) this.cancelFall(fall);
                if (fall.isClimbing && now - fall.attackTime > MAX_KNOCKBACK_TIME) this.cancelFall(fall);
            }
        }
    }

    private void scheduleCheckFallCancelled(final Fall fall, final long delay) {
        this.plugin.getServer().getScheduler().runTaskLater(this.plugin, new Runnable() {
            public void run() {
                SimpleGravityKillTracker.this.checkFallCancelled(fall);
            }
        }, delay + 1);
    }

    /**
     * Called whenever the player becomes "unsupported" to check if they were attacked recently enough for the attack
     * to be responsible for the fall
     */
    private void playerBecameAirborne(Fall fall) {
        if(!fall.isFalling && !this.isSupported(fall) && this.timer.getTicks() - fall.attackTime <= MAX_KNOCKBACK_TIME) {
            fall.isFalling = true;
        }
    }

    /**
     * Called when a player is damaged in a way that could initiate a Fall,
     * i.e. damage from another entity that causes knockback
     */
    public void playerAttacked(Player victim, Entity attacker) {
        if(this.falls.containsKey(victim)) {
            // A new fall can't be initiated if the victim is already falling
            return;
        }

        Location loc = victim.getLocation();
        boolean isInLava = PlayerBlockChecker.isSwimming(loc, Material.LAVA);
        boolean isClimbing = PlayerBlockChecker.isClimbing(loc);
        boolean isSwimming = PlayerBlockChecker.isSwimming(loc, Material.WATER);

        // Figure out the entity responsible for the attack and bail if it's not living
        Fall.Cause cause;
        if(attacker instanceof Projectile && ((Projectile) attacker).getShooter() instanceof LivingEntity) {
            attacker = (LivingEntity) ((Projectile) attacker).getShooter();
            cause = Fall.Cause.SHOOT;
        } else {
            cause = Fall.Cause.HIT;
        }

        if(!(attacker instanceof LivingEntity)) {
            return;
        }

        // Note the victim's situation when the attack happened
        Fall.From from = null;
        if(isClimbing) {
            from = Fall.From.LADDER;
        } else if(isSwimming) {
            from = Fall.From.WATER;
        } else {
            from = Fall.From.FLOOR;
        }

        Fall fall = new Fall((LivingEntity) attacker, cause, victim, from, this.timer.getTicks());
        this.falls.put(victim, fall);

        fall.isClimbing = isClimbing;
        fall.isSwimming = isSwimming;
        fall.isInLava = isInLava;

        // If the victim is already in the air, immediately confirm that they are falling.
        // Otherwise, the fall will be confirmed when they leave the ground, if it happens
        // within the time window.
        fall.isFalling = !this.isSupported(fall);

        if(!fall.isFalling) {
            this.scheduleCheckFallCancelled(fall, MAX_KNOCKBACK_TIME);
        }
    }

    /**
     * Called when a block is broken in a way that could initiate a spleef fall
     */
    public void blockBroken(Block block, Player breaker) {
        Material material = block.getType();
        if(!material.isSolid()) {
            return;
        }
        // Bukkit considers these "solid" for some reason
        switch(material) {
            case SIGN_POST:
            case WALL_SIGN:
            case WOOD_PLATE:
            case STONE_PLATE:
            case IRON_PLATE:
            case GOLD_PLATE:
                return;
        }

        final BrokenBlock brokenBlock = new BrokenBlock(block, breaker, this.timer.getTicks());
        final Location location = brokenBlock.block.getLocation();
        this.brokenBlocks.put(location, brokenBlock);

        this.plugin.getServer().getScheduler().runTaskLater(this.plugin, new Runnable() {
            public void run() {
                // Only remove the BrokenBlock if it's the same one we added. It may have been replaced since then.
                if (SimpleGravityKillTracker.this.brokenBlocks.get(location) == brokenBlock) {
                    SimpleGravityKillTracker.this.brokenBlocks.remove(location);
                }
            }
        }, MAX_SPLEEF_TIME + 1);
    }

    /**
     * Called when a player moves in a way that could affect their fall i.e. landing on a ladder or in liquid
     */
    public void playerMoved(Player player, Location to) {
        if(this.falls.containsKey(player)) {
            Fall fall = this.falls.get(player);

            boolean isClimbing = PlayerBlockChecker.isClimbing(to);
            boolean isSwimming = PlayerBlockChecker.isSwimming(to, Material.WATER);
            boolean isInLava = PlayerBlockChecker.isSwimming(to, Material.LAVA);
            boolean becameAirborne = false;

            if(isClimbing != fall.isClimbing) {
                if((fall.isClimbing = isClimbing)) {
                    // Player moved onto a ladder, cancel the fall if they are still on it after MAX_CLIMBING_TIME
                    fall.climbingTime = this.timer.getTicks();
                    this.scheduleCheckFallCancelled(fall, MAX_CLIMBING_TIME + 1);
                } else {
                    becameAirborne = true;
                }
            }

            if(isSwimming != fall.isSwimming) {
                if((fall.isSwimming = isSwimming)) {
                    // Player moved into water, cancel the fall if they are still in it after MAX_SWIMMING_TIME
                    fall.swimmingTime = this.timer.getTicks();
                    this.scheduleCheckFallCancelled(fall, MAX_SWIMMING_TIME + 1);
                } else {
                    becameAirborne = true;
                }
            }

            if(becameAirborne) {
                // Player moved out of water or off a ladder, check if it was caused by the attack
                this.playerBecameAirborne(fall);
            }

            if(isInLava != fall.isInLava) {
                if((fall.isInLava = isInLava)) {
                    fall.inLavaTime = this.timer.getTicks();
                } else {
                    // Because players continue to "fall" as long as they are in lava, moving out of lava
                    // can immediately end their fall
                    this.checkFallCancelled(fall);
                }
            }
        }
    }

    /**
     * Called when the player touches or leaves the ground
     */
    public void playerOnOrOffGround(Player player, boolean onGround) {
        Fall fall = this.falls.get(player);
        if(fall != null) {
            if(onGround) {
                // Falling player landed on the ground, cancel the fall if they are still there after MAX_ON_GROUND_TIME
                fall.onGroundTime = this.timer.getTicks();
                fall.groundTouchCount++;
                this.scheduleCheckFallCancelled(fall, MAX_ON_GROUND_TIME + 1);
            } else {
                // Falling player left the ground, check if it was caused by the attack
                this.playerBecameAirborne(fall);
            }

        } else if(!onGround) {
            // Player that is not currently falling left the ground, check if it was caused by a spleef
            BrokenBlock brokenBlock = BrokenBlock.lastBlockBrokenUnderPlayer(player, this.brokenBlocks);
            if(brokenBlock != null && this.timer.getTicks() - brokenBlock.time <= MAX_SPLEEF_TIME) {
                fall = new Fall(brokenBlock.breaker, Fall.Cause.SPLEEF, player, Fall.From.FLOOR, brokenBlock.time );

                Location loc = player.getLocation();
                fall.isClimbing = PlayerBlockChecker.isClimbing(loc);
                fall.isSwimming = PlayerBlockChecker.isSwimming(loc, Material.WATER);
                fall.isInLava = PlayerBlockChecker.isSwimming(loc, Material.LAVA);

                fall.isFalling = true;

                this.falls.put(player, fall);
            }
        }
    }

    /**
     * Called when anything happens to a player that should cancel their fall i.e. death, gamemode change, etc.
     */
    public void cancelFall(Player victim) {
        this.falls.remove(victim);
    }

    /**
     * Get the Fall that caused the given damage to the given player,
     * or null if the damage was not caused by a Fall.
     */
    public Fall getCausingFall(Player victim, EntityDamageEvent.DamageCause damageCause) {
        Fall fall = this.falls.get(victim);
        if(fall == null || !fall.isFalling) {
            return null;
        }

        // Do an extra check to see if the fall should be cancelled
        this.checkFallCancelled(fall);
        if(!this.falls.containsKey(victim)) {
            return null;
        }

        switch(damageCause) {
            case VOID:
            case FALL:
            case LAVA:
            case SUICIDE:
                return fall;

            case FIRE_TICK:
                return fall.isInLava ? fall : null;

            default:
                return null;
        }
    }
}
import javax.annotation.Nullable;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;
import tc.oc.tracker.DamageResolverManager;
import tc.oc.tracker.DamageResolvers;
import tc.oc.tracker.TrackerManager;
import tc.oc.tracker.Trackers;
import tc.oc.tracker.damage.resolvers.AnvilDamageResolver;
import tc.oc.tracker.damage.resolvers.BlockDamageResolver;
import tc.oc.tracker.damage.resolvers.DispensedProjectileDamageResolver;
import tc.oc.tracker.damage.resolvers.FallDamageResolver;
import tc.oc.tracker.damage.resolvers.GravityDamageResolver;
import tc.oc.tracker.damage.resolvers.LavaDamageResolver;
import tc.oc.tracker.damage.resolvers.MeleeDamageResolver;
import tc.oc.tracker.damage.resolvers.OwnedMobDamageResolver;
import tc.oc.tracker.damage.resolvers.ProjectileDamageResolver;
import tc.oc.tracker.damage.resolvers.TNTDamageResolver;
import tc.oc.tracker.damage.resolvers.VoidDamageResolver;
import tc.oc.tracker.timer.TickTimer;
import tc.oc.tracker.trackers.AnvilTracker;
import tc.oc.tracker.trackers.DispenserTracker;
import tc.oc.tracker.trackers.ExplosiveTracker;
import tc.oc.tracker.trackers.OwnedMobTracker;
import tc.oc.tracker.trackers.ProjectileDistanceTracker;
import tc.oc.tracker.trackers.base.SimpleAnvilTracker;
import tc.oc.tracker.trackers.base.SimpleDispenserTracker;
import tc.oc.tracker.trackers.base.SimpleExplosiveTracker;
import tc.oc.tracker.trackers.base.SimpleOwnedMobTracker;
import tc.oc.tracker.trackers.base.SimpleProjectileDistanceTracker;
import tc.oc.tracker.trackers.base.gravity.SimpleGravityKillTracker;
package tc.oc.tracker.plugin;




public class TrackerPlugin extends JavaPlugin {
    public @Nullable TickTimer tickTimer;

    @Override
    public void onDisable() {
        Trackers.getManager().clearTracker(ExplosiveTracker.class, SimpleExplosiveTracker.class);
    }

    @Override
    public void onEnable() {
        // basic operation listeners
        this.registerEvents(new LifetimeListener());
        this.registerEvents(new WorldListener(Trackers.getManager()));

        EntityDamageEventListener damageEventListener = new EntityDamageEventListener();
        damageEventListener.register(this);

        // initialize timer
        this.tickTimer = new TickTimer(this);
        this.tickTimer.start();

        // tracker setup
        TrackerManager tm = Trackers.getManager();

        ExplosiveTracker explosiveTracker = new SimpleExplosiveTracker();
        SimpleGravityKillTracker gravityKillTracker = new SimpleGravityKillTracker(this, this.tickTimer);

        explosiveTracker.enable();
        gravityKillTracker.enable();

        this.registerEvents(new ExplosiveListener(explosiveTracker));
        this.registerEvents(new GravityListener(this, gravityKillTracker));

        tm.setTracker(ExplosiveTracker.class, explosiveTracker);
        tm.setTracker(SimpleGravityKillTracker.class, gravityKillTracker);

        DispenserTracker dispenserTracker = new SimpleDispenserTracker();
        dispenserTracker.enable();

        this.registerEvents(new DispenserListener(dispenserTracker));
        tm.setTracker(DispenserTracker.class, dispenserTracker);

        ProjectileDistanceTracker projectileDistanceTracker = new SimpleProjectileDistanceTracker();
        projectileDistanceTracker.enable();

        this.registerEvents(new ProjectileDistanceListener(projectileDistanceTracker));
        tm.setTracker(ProjectileDistanceTracker.class, projectileDistanceTracker);

        OwnedMobTracker ownedMobTracker = new SimpleOwnedMobTracker();
        ownedMobTracker.enable();

        this.registerEvents(new OwnedMobListener(ownedMobTracker));
        tm.setTracker(OwnedMobTracker.class, ownedMobTracker);

        AnvilTracker anvilTracker = new SimpleAnvilTracker();
        anvilTracker.enable();

        this.registerEvents(new AnvilListener(anvilTracker));
        tm.setTracker(AnvilTracker.class, anvilTracker);

        // register damage resolvers
        DamageResolverManager drm = DamageResolvers.getManager();

        drm.register(new BlockDamageResolver());
        drm.register(new FallDamageResolver());
        drm.register(new LavaDamageResolver());