// 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);
    }
  }
 // sends a color-coded message to a player
 public static void sendMessage(Player player, ChatColor color, String message) {
   if (player == null) {
     GriefPrevention.AddLogEntry(removeColors(message));
   } else {
     player.sendMessage(color + message);
   }
 }
  public void parseMaterialListFromConfig(
      List<String> stringsToParse, MaterialCollection materialCollection) {
    materialCollection.clear();

    // for each string in the list
    for (int i = 0; i < stringsToParse.size(); i++) {
      // try to parse the string value into a material info
      MaterialInfo materialInfo = MaterialInfo.fromString(stringsToParse.get(i));

      // null value returned indicates an error parsing the string from the config file
      if (materialInfo == null) {
        // show error in log
        GriefPrevention.AddLogEntry(
            "ERROR: Unable to read a material entry from the config file.  Please update your config.yml.");

        // update string, which will go out to config file to help user find the error entry
        if (!stringsToParse.get(i).contains("can't")) {
          stringsToParse.set(
              i,
              stringsToParse.get(i)
                  + "     <-- can't understand this entry, see BukkitDev documentation");
        }
      }

      // otherwise store the valid entry in config data
      // but only if not currently in the list.
      else {
        materialCollection.add(materialInfo);
      }
    }
  }
  // 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;
            }
          }
        }
      }
    }
  }
  // initializes well...   everything
  public void onEnable() {
    instance = this;
    AddLogEntry("Grief Prevention enabled.");

    cmdHandler = new CommandHandler();
    // if the old data folder exists and the new one doesn't...
    File oldData = new File(dataStore.oldDataLayerFolderPath);
    File newData = new File(dataStore.dataLayerFolderPath);
    if (oldData.exists() && !newData.exists()) {
      // migrateData();
      AddLogEntry(
          "Found old GriefPrevention 7.7 or Earlier Data, but no 7.8 or later data. Attempting to copy to new folder.");
      AddLogEntry(
          "This will Copy your GriefPrevention 7.7 Data to the new GriefPrevention 7.8 and Later location.");
      AddLogEntry(
          "You will need to reconfigure your settings using the new World-based Configuration.");
      try {
        RecursiveCopyResult copied = recursiveCopy(oldData, newData, true);
        AddLogEntry(
            "Migration complete. Copied "
                + copied.FileCount
                + " Files in "
                + copied.DirCount
                + " Directories.");
      } catch (IOException exx) {
        AddLogEntry("Exception occured attempting to copy config data.");
        exx.printStackTrace();
      }
    }

    // MinecraftMMO = (mcMMO) Bukkit.getPluginManager().getPlugin("mcMMO");
    Debugger.Write(new File(DataStore.configFilePath).getAbsolutePath(), DebugLevel.Verbose);
    Debugger.Write(
        "File Exists:" + new File(DataStore.configFilePath).exists(), DebugLevel.Verbose);
    // load the config if it exists
    FileConfiguration config =
        YamlConfiguration.loadConfiguration(new File(DataStore.configFilePath));
    FileConfiguration outConfig = new YamlConfiguration();

    // read configuration settings (note defaults)

    // load player groups.
    // System.out.println("reading player groups...");
    this.config_player_groups = new PlayerGroups(config, "GriefPrevention.Groups");
    this.config_player_groups.Save(outConfig, "GriefPrevention.Groups");
    // optional database settings
    String databaseUrl = config.getString("GriefPrevention.Database.URL", "");
    String databaseUserName = config.getString("GriefPrevention.Database.UserName", "");
    String databasePassword = config.getString("GriefPrevention.Database.Password", "");
    // sea level
    String AcquiredLevel = config.getString("GriefPrevention.DebugLevel", "None");
    this.DebuggingLevel = Debugger.DebugLevel.valueOf(AcquiredLevel);
    config.set("GriefPrevention.DebugLevel", DebuggingLevel.name());
    this.debug = new Debugger(DebuggingLevel);
    outConfig.set("GriefPrevention.Database.URL", databaseUrl);
    outConfig.set("GriefPrevention.Database.UserName", databaseUserName);
    outConfig.set("GriefPrevention.Database.Password", databasePassword);

    this.config_economy_claimBlocksPurchaseCost =
        config.getDouble("GriefPrevention.Economy.ClaimBlocksPurchaseCost", 0);
    this.config_economy_claimBlocksSellValue =
        config.getDouble("GriefPrevention.Economy.ClaimBlocksSellValue", 0);
    this.config_claims_maxAccruedBlocks =
        config.getInt("GriefPrevention.Claims.MaxAccruedBlocks", 5000);
    outConfig.set("GriefPrevention.Claims.MaxAccruedBlocks", config_claims_maxAccruedBlocks);

    this.ModdedBlockRegexHelper =
        new RegExTestHelper(
            config,
            outConfig,
            "GriefPrevention.Mods.Containers",
            RegExTestHelper.DefaultContainers);
    this.AccessRegexPattern =
        new RegExTestHelper(
            config, outConfig, "GriefPrevention.Mods.Access", RegExTestHelper.DefaultAccess);
    this.OreBlockRegexHelper =
        new RegExTestHelper(
            config, outConfig, "GriefPrevention.Mods.Trash", RegExTestHelper.DefaultTrash);

    this.config_claims_initialBlocks = config.getInt("GriefPrevention.Claims.InitialBlocks", 100);
    this.config_mod_config_search =
        config.getBoolean("GriefPrevention.Mods.PerformConfigSearch", true);
    this.config_claims_deleteclaimswithunrecognizedowners =
        config.getBoolean("GriefPrevention.Claims.DeleteWithUnrecognizedOwner", false);
    this.config_autosubclaims =
        config.getBoolean("GriefPrevention.Claims.AutoSubClaimSwitch", false);
    outConfig.set("GriefPrevention.Claims.AutoSubClaimsSwitch", this.config_autosubclaims);
    outConfig.set(
        "GriefPrevention.Claims.DeleteWithUnrecognizedOwner",
        this.config_claims_deleteclaimswithunrecognizedowners);
    outConfig.set(
        "GriefPrevention.Economy.ClaimBlocksPurchaseCost",
        this.config_economy_claimBlocksPurchaseCost);
    outConfig.set(
        "GriefPrevention.Economy.ClaimBlocksSellValue", this.config_economy_claimBlocksSellValue);
    outConfig.set("GriefPrevention.Claims.InitialBlocks", config_claims_initialBlocks);
    outConfig.set("GriefPrevention.Mods.PerformConfigSearch", false);
    this.ModdedBlocks = new ModdedBlocksSearchResults();
    if (config_mod_config_search) {
      // Show message indicating what will happen. With mod_config_search enabled, we will search
      // for configs and find
      // IDs according to a regular expression, but for this entire session all World Configuration
      // loads will
      // change the configured values for modded containers and access blocks, so mention that.
      AddLogEntry("Performing Configuration Search.");
      AddLogEntry(
          "World Configurations Loaded during this session will have their current Container and Access IDs Overwritten!");

      this.ModdedBlocks = ModBlockHelper.ScanCfgs();

      // save these to _template.

    }
    Configuration = new ConfigData(config, outConfig);

    if (config_mod_config_search) { // if specified, to search, save the results to the template
                                    // file.
      // WorldConfig's will save the ModdedBlock Contents when they are created,
      // therefore we will set the template in this manner. Otherwise, this setting (ModdedBlock
      // search results)
      // will only be valid for this one server session.
      WorldConfig templatefile = WorldConfig.fromFile(Configuration.getTemplateFile());
      // we don't actually need to do anything with the variable, all the work was done in
      // fromFile() and the WorldConfig constructors.

    }

    // when datastore initializes, it loads player and claim data, and posts some stats to the log
    if (databaseUrl.length() > 0) {
      try {
        DatabaseDataStore databaseStore =
            new DatabaseDataStore(databaseUrl, databaseUserName, databasePassword);

        if (FlatFileDataStore.hasData()) {
          GriefPrevention.AddLogEntry(
              "There appears to be some data on the hard drive.  Migrating those data to the database...");
          FlatFileDataStore flatFileStore = new FlatFileDataStore();
          flatFileStore.migrateData(databaseStore);
          GriefPrevention.AddLogEntry(
              "Data migration process complete.  Reloading data from the database...");
          databaseStore.close();
          databaseStore = new DatabaseDataStore(databaseUrl, databaseUserName, databasePassword);
        }

        this.dataStore = databaseStore;
      } catch (Exception e) {
        GriefPrevention.AddLogEntry(
            "Because there was a problem with the database, GriefPrevention will not function properly.  Either update the database config settings resolve the issue, or delete those lines from your config.yml so that GriefPrevention can use the file system to store data.");
        return;
      }
    }

    // if not using the database because it's not configured or because there was a problem, use the
    // file system to store data
    // this is the preferred method, as it's simpler than the database scenario
    if (this.dataStore == null) {
      try {
        this.dataStore = new FlatFileDataStore();
      } catch (Exception e) {
        GriefPrevention.AddLogEntry("Unable to initialize the file system data store.  Details:");
        GriefPrevention.AddLogEntry(e.getMessage());
      }
    }

    // start the recurring cleanup event for entities in creative worlds, if enabled.

    // start recurring cleanup scan for unused claims belonging to inactive players
    // if the option is enabled.
    // look through all world configurations.
    boolean claimcleanupOn = false;
    boolean entitycleanupEnabled = false;

    if (entitycleanupEnabled) {
      EntityCleanupTask task = new EntityCleanupTask(0);
      this.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L);
    }

    // register for events
    if (!eventsRegistered) {
      eventsRegistered = true;
      PluginManager pluginManager = this.getServer().getPluginManager();

      // player events
      PlayerEventHandler playerEventHandler = new PlayerEventHandler(this.dataStore, this);
      pluginManager.registerEvents(playerEventHandler, this);

      // block events
      BlockEventHandler blockEventHandler = new BlockEventHandler(this.dataStore);
      pluginManager.registerEvents(blockEventHandler, this);

      // entity events
      EntityEventHandler entityEventHandler = new EntityEventHandler(this.dataStore);
      pluginManager.registerEvents(entityEventHandler, this);
    }

    // if economy is enabled
    if (this.config_economy_claimBlocksPurchaseCost > 0
        || this.config_economy_claimBlocksSellValue > 0) {
      // try to load Vault
      GriefPrevention.AddLogEntry("GriefPrevention requires Vault for economy integration.");
      GriefPrevention.AddLogEntry("Attempting to load Vault...");
      RegisteredServiceProvider<Economy> economyProvider =
          getServer()
              .getServicesManager()
              .getRegistration(net.milkbowl.vault.economy.Economy.class);
      GriefPrevention.AddLogEntry("Vault loaded successfully!");

      // ask Vault to hook into an economy plugin
      GriefPrevention.AddLogEntry("Looking for a Vault-compatible economy plugin...");
      if (economyProvider != null) {
        GriefPrevention.economy = economyProvider.getProvider();

        // on success, display success message
        if (GriefPrevention.economy != null) {
          GriefPrevention.AddLogEntry(
              "Hooked into economy: " + GriefPrevention.economy.getName() + ".");
          GriefPrevention.AddLogEntry("Ready to buy/sell claim blocks!");
        }

        // otherwise error message
        else {
          GriefPrevention.AddLogEntry(
              "ERROR: Vault was unable to find a supported economy plugin.  Either install a Vault-compatible economy plugin, or set both of the economy config variables to zero.");
        }
      }

      // another error case
      else {
        GriefPrevention.AddLogEntry(
            "ERROR: Vault was unable to find a supported economy plugin.  Either install a Vault-compatible economy plugin, or set both of the economy config variables to zero.");
      }
    }
    MetaHandler = new ClaimMetaHandler();
    try {
      // new File(DataStore.configFilePath).delete();
      outConfig.save(new File(DataStore.configFilePath).getAbsolutePath());
    } catch (IOException exx) {
      this.log.log(
          Level.SEVERE, "Failed to save primary configuration file:" + DataStore.configFilePath);
    }
    ww = new WorldWatcher();
    Bukkit.getPluginManager().registerEvents(ww, this);
    // go through all available worlds, and fire a "world load" event for them.
    for (World iterate : Bukkit.getWorlds()) {
      WorldLoadEvent wle = new WorldLoadEvent(iterate);
      ww.WorldLoad(wle);
    }

    Bukkit.getPluginManager().callEvent(new GPLoadEvent(this));
  }
 // adds a server log entry
 public static void AddLogEntry(String entry) {
   if (instance == null) return;
   instance.getLogger().log(Level.INFO, entry);
 }