// when a painting is placed...
  @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
  public void onPaintingPlace(PaintingPlaceEvent event) {
    // FEATURE: similar to above, placing a painting requires build permission in the claim

    // if the player doesn't have permission, don't allow the placement
    String noBuildReason =
        GriefPrevention.instance.allowBuild(event.getPlayer(), event.getPainting().getLocation());
    if (noBuildReason != null) {
      event.setCancelled(true);
      GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, noBuildReason);
      return;
    }

    // otherwise, apply entity-count limitations for creative worlds
    else if (GriefPrevention.instance.creativeRulesApply(event.getPainting().getLocation())) {
      PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName());
      Claim claim =
          this.dataStore.getClaimAt(event.getBlock().getLocation(), false, playerData.lastClaim);
      if (claim == null) return;

      String noEntitiesReason = claim.allowMoreEntities();
      if (noEntitiesReason != null) {
        GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, noEntitiesReason);
        event.setCancelled(true);
        return;
      }
    }
  }
  // when a painting is broken
  @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
  public void onPaintingBreak(PaintingBreakEvent event) {
    // FEATURE: claimed paintings are protected from breakage

    // only allow players to break paintings, not anything else (like water and explosions)
    if (!(event instanceof PaintingBreakByEntityEvent)) {
      event.setCancelled(true);
      return;
    }

    PaintingBreakByEntityEvent entityEvent = (PaintingBreakByEntityEvent) event;

    // who is removing it?
    Entity remover = entityEvent.getRemover();

    // again, making sure the breaker is a player
    if (!(remover instanceof Player)) {
      event.setCancelled(true);
      return;
    }

    // if the player doesn't have build permission, don't allow the breakage
    Player playerRemover = (Player) entityEvent.getRemover();
    String noBuildReason =
        GriefPrevention.instance.allowBuild(playerRemover, event.getPainting().getLocation());
    if (noBuildReason != null) {
      event.setCancelled(true);
      GriefPrevention.sendMessage(playerRemover, TextMode.Err, noBuildReason);
    }
  }
  // when a vehicle is damaged
  @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
  public void onVehicleDamage(VehicleDamageEvent event) {
    // all of this is anti theft code
    if (!GriefPrevention.instance.config_claims_preventTheft) return;

    // determine which player is attacking, if any
    Player attacker = null;
    Entity damageSource = event.getAttacker();
    if (damageSource instanceof Player) {
      attacker = (Player) damageSource;
    } else if (damageSource instanceof Arrow) {
      Arrow arrow = (Arrow) damageSource;
      if (arrow.getShooter() instanceof Player) {
        attacker = (Player) arrow.getShooter();
      }
    } else if (damageSource instanceof ThrownPotion) {
      ThrownPotion potion = (ThrownPotion) damageSource;
      if (potion.getShooter() instanceof Player) {
        attacker = (Player) potion.getShooter();
      }
    }

    // NOTE: vehicles can be pushed around.
    // so unless precautions are taken by the owner, a resourceful thief might find ways to steal
    // anyway
    Claim cachedClaim = null;
    PlayerData playerData = null;
    if (attacker != null) {
      playerData = this.dataStore.getPlayerData(attacker.getName());
      cachedClaim = playerData.lastClaim;
    }

    Claim claim = this.dataStore.getClaimAt(event.getVehicle().getLocation(), false, cachedClaim);

    // if it's claimed
    if (claim != null) {
      // if damaged by anything other than a player, cancel the event
      if (attacker == null) {
        event.setCancelled(true);
      }

      // otherwise the player damaging the entity must have permission
      else {
        String noContainersReason = claim.allowContainers(attacker);
        if (noContainersReason != null) {
          event.setCancelled(true);
          GriefPrevention.sendMessage(
              attacker, TextMode.Err, Messages.NoDamageClaimedEntity, claim.getOwnerName());
        }

        // cache claim for later
        if (playerData != null) {
          playerData.lastClaim = claim;
        }
      }
    }
  }
  @Override
  public void run() {
    // for each claim involved in this siege
    for (int i = 0; i < this.siegeData.claims.size(); i++) {
      // lock the doors
      Claim claim = this.siegeData.claims.get(i);
      claim.doorsOpen = false;

      // eject bad guys
      Player[] onlinePlayers = GriefPrevention.instance.getServer().getOnlinePlayers();
      for (int j = 0; j < onlinePlayers.length; j++) {
        Player player = onlinePlayers[j];
        if (claim.contains(player.getLocation(), false, false)
            && claim.allowAccess(player) != null) {
          GriefPrevention.sendMessage(
              player, TextMode.Err, "Looting time is up!  Ejected from the claim.");
          GriefPrevention.instance.ejectPlayer(player);
        }
      }
    }
  }
  // called when a player spawns, applies protection for that player if necessary
  public void checkPvpProtectionNeeded(Player player) {
    WorldConfig wc = GriefPrevention.instance.getWorldCfg(player.getWorld());
    // if pvp is disabled, do nothing
    if (!player.getWorld().getPVP()) return;

    // if player is in creative mode, do nothing
    if (player.getGameMode() == GameMode.CREATIVE) return;

    // if anti spawn camping feature is not enabled, do nothing
    if (!wc.getProtectFreshSpawns()) return;

    // if the player has the damage any player permission enabled, do nothing
    if (player.hasPermission("griefprevention.nopvpimmunity")) return;

    // check inventory for well, anything
    PlayerInventory inventory = player.getInventory();
    ItemStack[] armorStacks = inventory.getArmorContents();

    // check armor slots, stop if any items are found
    for (int i = 0; i < armorStacks.length; i++) {
      if (!(armorStacks[i] == null || armorStacks[i].getType() == Material.AIR)) return;
    }

    // check other slots, stop if any items are found
    ItemStack[] generalStacks = inventory.getContents();
    for (int i = 0; i < generalStacks.length; i++) {
      if (!(generalStacks[i] == null || generalStacks[i].getType() == Material.AIR)) return;
    }

    // otherwise, apply immunity
    PlayerData playerData = this.dataStore.getPlayerData(player.getName());
    playerData.pvpImmune = true;

    // inform the player
    GriefPrevention.sendMessage(player, TextMode.Success, Messages.PvPImmunityStart);
  }
  // when an entity is damaged
  @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
  public void onEntityDamage(EntityDamageEvent event) {
    // only actually interested in entities damaging entities (ignoring environmental damage)
    if (!(event instanceof EntityDamageByEntityEvent)) return;

    // monsters are never protected
    if (event.getEntity() instanceof Monster) return;

    EntityDamageByEntityEvent subEvent = (EntityDamageByEntityEvent) event;

    // determine which player is attacking, if any
    Player attacker = null;
    Entity damageSource = subEvent.getDamager();
    if (damageSource instanceof Player) {
      attacker = (Player) damageSource;
    } else if (damageSource instanceof Arrow) {
      Arrow arrow = (Arrow) damageSource;
      if (arrow.getShooter() instanceof Player) {
        attacker = (Player) arrow.getShooter();
      }
    } else if (damageSource instanceof ThrownPotion) {
      ThrownPotion potion = (ThrownPotion) damageSource;
      if (potion.getShooter() instanceof Player) {
        attacker = (Player) potion.getShooter();
      }
    }

    // if the attacker is a player and defender is a player (pvp combat)
    if (attacker != null && event.getEntity() instanceof Player) {
      // FEATURE: prevent pvp in the first minute after spawn, and prevent pvp when one or both
      // players have no inventory

      // doesn't apply when the attacker has the no pvp immunity permission
      // this rule is here to allow server owners to have a world with no spawn camp protection by
      // assigning permissions based on the player's world
      if (attacker.hasPermission("griefprevention.nopvpimmunity")) return;

      Player defender = (Player) (event.getEntity());

      PlayerData defenderData =
          this.dataStore.getPlayerData(((Player) event.getEntity()).getName());
      PlayerData attackerData = this.dataStore.getPlayerData(attacker.getName());

      // otherwise if protecting spawning players
      if (GriefPrevention.instance.config_pvp_protectFreshSpawns) {
        if (defenderData.pvpImmune) {
          event.setCancelled(true);
          GriefPrevention.sendMessage(attacker, TextMode.Err, Messages.ThatPlayerPvPImmune);
          return;
        }

        if (attackerData.pvpImmune) {
          event.setCancelled(true);
          GriefPrevention.sendMessage(attacker, TextMode.Err, Messages.CantFightWhileImmune);
          return;
        }
      }

      // FEATURE: prevent players who very recently participated in pvp combat from hiding inventory
      // to protect it from looting
      // FEATURE: prevent players who are in pvp combat from logging out to avoid being defeated

      long now = Calendar.getInstance().getTimeInMillis();
      defenderData.lastPvpTimestamp = now;
      defenderData.lastPvpPlayer = attacker.getName();
      attackerData.lastPvpTimestamp = now;
      attackerData.lastPvpPlayer = defender.getName();
    }

    // FEATURE: protect claimed animals, boats, minecarts
    // NOTE: animals can be lead with wheat, vehicles can be pushed around.
    // so unless precautions are taken by the owner, a resourceful thief might find ways to steal
    // anyway

    // if theft protection is enabled
    if (event instanceof EntityDamageByEntityEvent) {
      // if the entity is an non-monster creature (remember monsters disqualified above), or a
      // vehicle
      if ((subEvent.getEntity() instanceof Creature
          && GriefPrevention.instance.config_claims_protectCreatures)) {
        Claim cachedClaim = null;
        PlayerData playerData = null;
        if (attacker != null) {
          playerData = this.dataStore.getPlayerData(attacker.getName());
          cachedClaim = playerData.lastClaim;
        }

        Claim claim =
            this.dataStore.getClaimAt(event.getEntity().getLocation(), false, cachedClaim);

        // if it's claimed
        if (claim != null) {
          // if damaged by anything other than a player, cancel the event
          if (attacker == null) {
            event.setCancelled(true);
          }

          // otherwise the player damaging the entity must have permission
          else {
            String noContainersReason = claim.allowContainers(attacker);
            if (noContainersReason != null) {
              event.setCancelled(true);
              GriefPrevention.sendMessage(
                  attacker, TextMode.Err, Messages.NoDamageClaimedEntity, claim.getOwnerName());
            }

            // cache claim for later
            if (playerData != null) {
              playerData.lastClaim = claim;
            }
          }
        }
      }
    }
  }