diff --git a/Changelog.txt b/Changelog.txt
index 7d0717d25..1ee7a8ce7 100644
--- a/Changelog.txt
+++ b/Changelog.txt
@@ -99,6 +99,52 @@ Version 2.2.000
Parties got unnecessarily complex in my absence, I have removed many party features in order to simplify parties and bring them closer to my vision. I have also added new features which should improve parties where it matters.
About the removed party features, all the features I removed I consider poor quality features and I don't think they belong in mcMMO. Feel free to yell at me in discord if you disagree.
I don't know what genius decided to make parties public by default, when I found out that parties had been changed to such a system I could barely contain my disgust. Parties are back to being private, you get invited by a party leader or party officer. That is the only way to join a party.
+Version 2.1.165
+ The mcMMO system which tracks player placed blocks has had some major rewrites (thanks t00thpick1)
+ mcMMO will now be compatible with changes to world height (1.17 compatibility)
+ Added missing cooldown locale message 'Commands.Database.Cooldown'
+
+ NOTES:
+ t00thpick1 has taken time to rewrite our block meta tracking system to be more efficient, easier to maintain, and support upcoming features such as world height changes
+ This new system is compatible with the old one, it will convert old files to the new format as needed.
+ This update shouldn't break anything as the API is the same
+
+Version 2.1.164
+ mcMMO will now let players use vanilla blocks that have interactions (such as the vanilla Anvil) which are assigned as either Repair or Salvage blocks if a player is sneaking (see notes)
+ The Rarity known as Records has been renamed to Mythic
+ Fishing treasures have been moved from treasures.yml -> fishing_treasures.yml, copy over any custom entries you had from treasures.yml carefully as the config file has changed and you can't just copy paste your old entries without making a few edits
+ Added Enchanted Books to fishing_treasures.yml as Legendary rarity (can drop with any legal enchantment - see notes)
+ Added all Netherite gear to the Mythic rarity in fishing_treasures.yml
+ Added Name Tag to fishing_treasures.yml as Rare rarity
+ Added Netherite Scrap to fishing_treasures.yml as Legendary rarity
+ Added Nautilus Shell to fishing_treasures.yml as Legendary rarity
+ Music Disc rarity in fishing_tresures.yml has been broken up across tiers, they will be more common now.
+ Mythic rarity (formerly known as Records) now allows for Enchantments to be applied to drops (See Notes)
+ New exploit fix setting 'PreventPluginNPCInteraction' which defaults to true, when disabled it will allow combat interactions with "NPC" entities from plugins like Citizens
+ ExploitFix.PreventPluginNPCInteraction Added to experience.yml
+ Modified locale string 'Fishing.SubSkill.TreasureHunter.Stat.Extra' in existing locale files
+ You can now define a whitelist of enchants or a blacklist of enchants for an Enchanted_Book entries in fishing_treasures.yml, see notes for an example
+
+ NOTES:
+ Before reading, Fishing/Excavation are getting a complete loot table rewrite in the future, everything changed in this patch is meant as a temporary quality of life fix until the bigger better change in the future.
+ There's no real reason to allow for vanilla treasures anymore, so if you were using the vanilla treasure override I suggest turning it off.
+ You can't add Enchanted_Book to any treasures outside of Fishing's treasure drops right now, I'll fix it in an upcoming patch. Well you can add it, but it won't work.
+ The rarity formerly known as 'Records' was odd to me, if you got the best possible drop it was always going to be a Music Record drop (using the default mcMMO treasure list), and by default the Records tier had only music records. It was treated differently in the code as well, for example Records drops never had enchantments applied to them. So you could add say NETHERITE_ARMOR to them in your user config and it would never put enchantments on it, that seemed very odd to me.
+ As a response to this, I've renamed Records as Mythic, I've moved the records into varying tiers, you'll start getting them much earlier now. I've also added Netherite and Enchanted Books to the Mythic tier.
+ Enchanted Books have been added to Fishing loot, this is a basic hacky work around until the config update comes. Enchanted books can have any legal enchant and you can specify which Enchants a book can spawn with.
+ Also the Enchantment chance to be applied to the book is completely equal across all enchantments, it does not follow the same logic as applying enchantments to fished up gear.
+
+ Here is an example of using the whitelist or blacklist for an Enchanted_Book entry in fishing_treasures.yml
+ https://gist.github.com/nossr50/4e15b8ba6915b5a5f516eccfba2d7169
+ If you can't load this image, at the address of your treasure for example, at Fishing.Enchanted_Book.Enchantments_Blacklist: you define a list (which must follow yaml spec, google yaml linter) of enchants to disallow, likewise at Fishing.Enchanted_Book.Enchantments_Whitelist you can setup a whitelist, if neither is defined then the book can spawn with all possible enchants, if both are defined the whitelist is used instead of the blacklist
+ Take care when moving any fishing entries you may have defined in treasures.yml over to fishing_treasures.yml, the config file has had a few things changed (as noted in these notes).
+
+ When talking about NPCs in the below notes, I am referring to "Fake" Players used in plugins such as Citizens, not Villagers from Vanilla Minecraft or anything labeled NPC in another plugin which does not constitute a "Fake Player"
+ Historically mcMMO has checked an entity for being a Fake-Player-NPC and backed out of any interaction, this was originally done because of Fake-Player-NPCs that were meant to be invincible/etc and not give XP
+ However nowadays what a Fake-Player-NPC is used for is pretty loose, mcMMO only has definitions for some NPCs (such as from Citizens) it doesn't know about most Fake-Player-NPCs in most plugins unless they identify themselves in a similar way to the predefined parameters
+ Leave this new exploit fix setting on true unless you understand the implications
+
+ If you only ran mcMMO on your server you'd have no way to use Enchanted Books if you set the repair anvil to the vanilla anvil, so now you can sneak to open up its menu. By the way, mcMMO has the vanilla anvil as repair's default anvil (instead of iron block which it had been historically).
Version 2.1.163
Fixed the translate URL pointing to the wrong place (thanks chew)
Fixed a bug where FlatFile databases would always attempt a UUID conversion task every save operation (every 10 minutes) causing console spam
@@ -106,6 +152,7 @@ Version 2.1.163
COTW Summoned entities are now removed when the chunk they are in is unloaded (prevents some exploits)
NOTES:
+ Seems I skipped releasing 2.1.162, not a big deal though as you should be using this version instead!
I often test in SQL environments so I missed this bug, reminder to come bother me on discord if you find any annoying bugs!
Also work on T&C is going great lately, I feel great. Perhaps my depression is getting better!
diff --git a/pom.xml b/pom.xml
index 625f74d33..adb1be672 100755
--- a/pom.xml
+++ b/pom.xml
@@ -306,7 +306,25 @@
junit
junit-dep
- 4.10
+ 4.11
+ test
+
+
+ org.powermock
+ powermock-module-junit4
+ 2.0.7
+ test
+
+
+ org.powermock
+ powermock-api-mockito2
+ 2.0.7
+ test
+
+
+ org.mockito
+ mockito-core
+ 3.4.6
test
diff --git a/src/main/java/com/gmail/nossr50/commands/admin/DropTreasureCommand.java b/src/main/java/com/gmail/nossr50/commands/admin/DropTreasureCommand.java
new file mode 100644
index 000000000..5a60d9bee
--- /dev/null
+++ b/src/main/java/com/gmail/nossr50/commands/admin/DropTreasureCommand.java
@@ -0,0 +1,57 @@
+//package com.gmail.nossr50.commands.admin;
+//
+//import com.gmail.nossr50.config.treasure.FishingTreasureConfig;
+//import com.gmail.nossr50.datatypes.player.McMMOPlayer;
+//import com.gmail.nossr50.datatypes.treasure.FishingTreasure;
+//import com.gmail.nossr50.datatypes.treasure.Rarity;
+//import com.gmail.nossr50.mcMMO;
+//import com.gmail.nossr50.skills.fishing.FishingManager;
+//import com.gmail.nossr50.util.player.UserManager;
+//import org.bukkit.Location;
+//import org.bukkit.command.Command;
+//import org.bukkit.command.CommandExecutor;
+//import org.bukkit.command.CommandSender;
+//import org.bukkit.entity.Player;
+//import org.jetbrains.annotations.NotNull;
+//
+//public class DropTreasureCommand implements CommandExecutor {
+// @Override
+// public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
+// if(sender instanceof Player) {
+// if(!sender.isOp()) {
+// sender.sendMessage("This command is for Operators only");
+// return false;
+// }
+//
+// Player player = (Player) sender;
+// Location location = player.getLocation();
+// McMMOPlayer mmoPlayer = UserManager.getPlayer(player);
+//
+// if(mmoPlayer == null) {
+// //TODO: Localize
+// player.sendMessage("Your player data is not loaded yet");
+// return false;
+// }
+//
+// if(args.length == 0) {
+// mcMMO.p.getLogger().info(player.toString() +" is dropping all mcMMO treasures via admin command at location "+location.toString());
+// for(Rarity rarity : FishingTreasureConfig.getInstance().fishingRewards.keySet()) {
+// for(FishingTreasure fishingTreasure : FishingTreasureConfig.getInstance().fishingRewards.get(rarity)) {
+// FishingManager fishingManager = mmoPlayer.getFishingManager();
+// }
+// }
+// //TODO: impl
+// } else {
+// String targetTreasure = args[1];
+//
+// //Drop all treasures matching the name
+// //TODO: impl
+// }
+//
+// return true;
+// } else {
+// sender.sendMessage("No console support for this command");
+// return false;
+// }
+// }
+//}
diff --git a/src/main/java/com/gmail/nossr50/commands/skills/FishingCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/FishingCommand.java
index 314598f26..e2291d4c2 100644
--- a/src/main/java/com/gmail/nossr50/commands/skills/FishingCommand.java
+++ b/src/main/java/com/gmail/nossr50/commands/skills/FishingCommand.java
@@ -1,6 +1,7 @@
package com.gmail.nossr50.commands.skills;
-import com.gmail.nossr50.config.treasure.TreasureConfig;
+import com.gmail.nossr50.config.treasure.FishingTreasureConfig;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
import com.gmail.nossr50.datatypes.skills.SubSkillType;
import com.gmail.nossr50.datatypes.treasure.Rarity;
import com.gmail.nossr50.locale.LocaleLoader;
@@ -28,7 +29,7 @@ public class FishingCommand extends SkillCommand {
private String rareTreasure;
private String epicTreasure;
private String legendaryTreasure;
- private String recordTreasure;
+ private String mythicTreasure;
private String magicChance;
@@ -54,19 +55,19 @@ public class FishingCommand extends SkillCommand {
lootTier = fishingManager.getLootTier();
// Item drop rates
- commonTreasure = percent.format(TreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.COMMON) / 100.0);
- uncommonTreasure = percent.format(TreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.UNCOMMON) / 100.0);
- rareTreasure = percent.format(TreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.RARE) / 100.0);
- epicTreasure = percent.format(TreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.EPIC) / 100.0);
- legendaryTreasure = percent.format(TreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.LEGENDARY) / 100.0);
- recordTreasure = percent.format(TreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.RECORD) / 100.0);
+ commonTreasure = percent.format(FishingTreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.COMMON) / 100.0);
+ uncommonTreasure = percent.format(FishingTreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.UNCOMMON) / 100.0);
+ rareTreasure = percent.format(FishingTreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.RARE) / 100.0);
+ epicTreasure = percent.format(FishingTreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.EPIC) / 100.0);
+ legendaryTreasure = percent.format(FishingTreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.LEGENDARY) / 100.0);
+ mythicTreasure = percent.format(FishingTreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.MYTHIC) / 100.0);
// Magic hunter drop rates
double totalEnchantChance = 0;
for (Rarity rarity : Rarity.values()) {
- if (rarity != Rarity.RECORD) {
- totalEnchantChance += TreasureConfig.getInstance().getEnchantmentDropRate(lootTier, rarity);
+ if (rarity != Rarity.MYTHIC) {
+ totalEnchantChance += FishingTreasureConfig.getInstance().getEnchantmentDropRate(lootTier, rarity);
}
}
@@ -144,7 +145,7 @@ public class FishingCommand extends SkillCommand {
String.valueOf(rareTreasure),
String.valueOf(epicTreasure),
String.valueOf(legendaryTreasure),
- String.valueOf(recordTreasure)));
+ String.valueOf(mythicTreasure)));
}
return messages;
diff --git a/src/main/java/com/gmail/nossr50/config/Config.java b/src/main/java/com/gmail/nossr50/config/Config.java
index 6580565e7..c8c7cdb14 100644
--- a/src/main/java/com/gmail/nossr50/config/Config.java
+++ b/src/main/java/com/gmail/nossr50/config/Config.java
@@ -12,6 +12,7 @@ import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.configuration.ConfigurationSection;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
@@ -506,14 +507,17 @@ public class Config extends AutoUpdateConfigLoader {
public boolean getRepairAnvilMessagesEnabled() { return config.getBoolean("Skills.Repair.Anvil_Messages", true); }
public boolean getRepairAnvilPlaceSoundsEnabled() { return config.getBoolean("Skills.Repair.Anvil_Placed_Sounds", true); }
public boolean getRepairAnvilUseSoundsEnabled() { return config.getBoolean("Skills.Repair.Anvil_Use_Sounds", true); }
- public Material getRepairAnvilMaterial() { return Material.matchMaterial(config.getString("Skills.Repair.Anvil_Material", "IRON_BLOCK")); }
+ public @Nullable Material getRepairAnvilMaterial() { return Material.matchMaterial(config.getString("Skills.Repair.Anvil_Material", "IRON_BLOCK")); }
public boolean getRepairConfirmRequired() { return config.getBoolean("Skills.Repair.Confirm_Required", true); }
+ public boolean getAllowVanillaInventoryRepair() { return config.getBoolean("Skills.Repair.Allow_Vanilla_Anvil_Repair", false); }
+ public boolean getAllowVanillaAnvilRepair() { return config.getBoolean("Skills.Repair.Allow_Vanilla_Inventory_Repair", false); }
+ public boolean getAllowVanillaGrindstoneRepair() { return config.getBoolean("Skills.Repair.Allow_Vanilla_Grindstone_Repair", false); }
/* Salvage */
public boolean getSalvageAnvilMessagesEnabled() { return config.getBoolean("Skills.Salvage.Anvil_Messages", true); }
public boolean getSalvageAnvilPlaceSoundsEnabled() { return config.getBoolean("Skills.Salvage.Anvil_Placed_Sounds", true); }
public boolean getSalvageAnvilUseSoundsEnabled() { return config.getBoolean("Skills.Salvage.Anvil_Use_Sounds", true); }
- public Material getSalvageAnvilMaterial() { return Material.matchMaterial(config.getString("Skills.Salvage.Anvil_Material", "GOLD_BLOCK")); }
+ public @Nullable Material getSalvageAnvilMaterial() { return Material.matchMaterial(config.getString("Skills.Salvage.Anvil_Material", "GOLD_BLOCK")); }
public boolean getSalvageConfirmRequired() { return config.getBoolean("Skills.Salvage.Confirm_Required", true); }
/* Unarmed */
diff --git a/src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java b/src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java
index a35c2917c..0ff3b1648 100644
--- a/src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java
+++ b/src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java
@@ -154,6 +154,7 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
public boolean isPistonExploitPrevented() { return config.getBoolean("ExploitFix.Pistons", false); }
public boolean allowUnsafeEnchantments() { return config.getBoolean("ExploitFix.UnsafeEnchantments", false); }
public boolean isCOTWBreedingPrevented() { return config.getBoolean("ExploitFix.COTWBreeding", true); }
+ public boolean isNPCInteractionPrevented() { return config.getBoolean("ExploitFix.PreventPluginNPCInteraction", true); }
public boolean isFishingExploitingPrevented() { return config.getBoolean("ExploitFix.Fishing", true); }
public boolean isAcrobaticsExploitingPrevented() { return config.getBoolean("ExploitFix.Acrobatics", true); }
diff --git a/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java b/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java
new file mode 100755
index 000000000..41fa349ce
--- /dev/null
+++ b/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java
@@ -0,0 +1,383 @@
+package com.gmail.nossr50.config.treasure;
+
+import com.gmail.nossr50.config.ConfigLoader;
+import com.gmail.nossr50.datatypes.treasure.*;
+import com.gmail.nossr50.mcMMO;
+import com.gmail.nossr50.util.EnchantmentUtils;
+import org.bukkit.ChatColor;
+import org.bukkit.Material;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.entity.EntityType;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.inventory.meta.PotionMeta;
+import org.bukkit.potion.PotionData;
+import org.bukkit.potion.PotionType;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+
+public class FishingTreasureConfig extends ConfigLoader {
+
+ public static final String FILENAME = "fishing_treasures.yml";
+ private static FishingTreasureConfig instance;
+
+ public @NotNull HashMap> fishingRewards = new HashMap<>();
+ public @NotNull HashMap> fishingEnchantments = new HashMap<>();
+ public @NotNull HashMap> shakeMap = new HashMap<>();
+
+ private FishingTreasureConfig() {
+ super(FILENAME);
+ loadKeys();
+ validate();
+ }
+
+ public static FishingTreasureConfig getInstance() {
+ if (instance == null) {
+ instance = new FishingTreasureConfig();
+ }
+
+ return instance;
+ }
+
+ @Override
+ protected boolean validateKeys() {
+ // Validate all the settings!
+ List reason = new ArrayList<>();
+ for (String tier : config.getConfigurationSection("Enchantment_Drop_Rates").getKeys(false)) {
+ double totalEnchantDropRate = 0;
+ double totalItemDropRate = 0;
+
+ for (Rarity rarity : Rarity.values()) {
+ double enchantDropRate = config.getDouble("Enchantment_Drop_Rates." + tier + "." + rarity.toString());
+ double itemDropRate = config.getDouble("Item_Drop_Rates." + tier + "." + rarity.toString());
+
+ if ((enchantDropRate < 0.0 || enchantDropRate > 100.0)) {
+ reason.add("The enchant drop rate for " + tier + " items that are " + rarity.toString() + "should be between 0.0 and 100.0!");
+ }
+
+ if (itemDropRate < 0.0 || itemDropRate > 100.0) {
+ reason.add("The item drop rate for " + tier + " items that are " + rarity.toString() + "should be between 0.0 and 100.0!");
+ }
+
+ totalEnchantDropRate += enchantDropRate;
+ totalItemDropRate += itemDropRate;
+ }
+
+ if (totalEnchantDropRate < 0 || totalEnchantDropRate > 100.0) {
+ reason.add("The total enchant drop rate for " + tier + " should be between 0.0 and 100.0!");
+ }
+
+ if (totalItemDropRate < 0 || totalItemDropRate > 100.0) {
+ reason.add("The total item drop rate for " + tier + " should be between 0.0 and 100.0!");
+ }
+ }
+
+ return noErrorsInConfig(reason);
+ }
+
+ @Override
+ protected void loadKeys() {
+ if (config.getConfigurationSection("Treasures") != null) {
+ backup();
+ return;
+ }
+
+ loadTreasures("Fishing");
+ loadEnchantments();
+
+ for (EntityType entity : EntityType.values()) {
+ if (entity.isAlive()) {
+ loadTreasures("Shake." + entity.toString());
+ }
+ }
+ }
+
+ private void loadTreasures(@NotNull String type) {
+ boolean isFishing = type.equals("Fishing");
+ boolean isShake = type.contains("Shake");
+
+ ConfigurationSection treasureSection = config.getConfigurationSection(type);
+
+ if (treasureSection == null) {
+ return;
+ }
+
+ // Initialize fishing HashMap
+ for (Rarity rarity : Rarity.values()) {
+ if (!fishingRewards.containsKey(rarity)) {
+ fishingRewards.put(rarity, (new ArrayList<>()));
+ }
+ }
+
+ for (String treasureName : treasureSection.getKeys(false)) {
+ // Validate all the things!
+ List reason = new ArrayList<>();
+
+ String[] treasureInfo = treasureName.split("[|]");
+ String materialName = treasureInfo[0];
+
+ /*
+ * Material, Amount, and Data
+ */
+ Material material;
+
+ if (materialName.contains("INVENTORY")) {
+ // Use magic material BEDROCK to know that we're grabbing something from the inventory and not a normal treasure
+ addShakeTreasure(new ShakeTreasure(new ItemStack(Material.BEDROCK, 1, (byte) 0), 1, getInventoryStealDropChance(), getInventoryStealDropLevel()), EntityType.PLAYER);
+ continue;
+ } else {
+ material = Material.matchMaterial(materialName);
+ }
+
+ int amount = config.getInt(type + "." + treasureName + ".Amount");
+ short data = (treasureInfo.length == 2) ? Short.parseShort(treasureInfo[1]) : (short) config.getInt(type + "." + treasureName + ".Data");
+
+ if (material == null) {
+ reason.add("Cannot find matching item type in this version of MC, skipping - " + materialName);
+ continue;
+ }
+
+ if (amount <= 0) {
+ amount = 1;
+ }
+
+ if (material.isBlock() && (data > 127 || data < -128)) {
+ reason.add("Data of " + treasureName + " is invalid! " + data);
+ }
+
+ /*
+ * XP, Drop Chance, and Drop Level
+ */
+
+ int xp = config.getInt(type + "." + treasureName + ".XP");
+ double dropChance = config.getDouble(type + "." + treasureName + ".Drop_Chance");
+ int dropLevel = config.getInt(type + "." + treasureName + ".Drop_Level");
+
+ if (xp < 0) {
+ reason.add(treasureName + " has an invalid XP value: " + xp);
+ }
+
+ if (dropChance < 0.0D) {
+ reason.add(treasureName + " has an invalid Drop_Chance: " + dropChance);
+ }
+
+ if (dropLevel < 0) {
+ reason.add(treasureName + " has an invalid Drop_Level: " + dropLevel);
+ }
+
+ /*
+ * Specific Types
+ */
+ Rarity rarity = null;
+
+ if (isFishing) {
+ String rarityStr = config.getString(type + "." + treasureName + ".Rarity");
+
+ if(rarityStr != null) {
+ rarity = Rarity.getRarity(rarityStr);
+ } else {
+ mcMMO.p.getLogger().severe("Please edit your config and add a Rarity definition for - " + treasureName);
+ mcMMO.p.getLogger().severe("Skipping this treasure until rarity is defined - " + treasureName);
+ continue;
+ }
+ }
+
+ /*
+ * Itemstack
+ */
+ ItemStack item = null;
+
+
+ String customName = null;
+
+ if(hasCustomName(type, treasureName)) {
+ customName = config.getString(type + "." + treasureName + ".Custom_Name");
+ }
+
+ if (materialName.contains("POTION")) {
+ Material mat = Material.matchMaterial(materialName);
+ if (mat == null) {
+ reason.add("Potion format for " + FILENAME + " has changed");
+ } else {
+ item = new ItemStack(mat, amount, data);
+ PotionMeta itemMeta = (PotionMeta) item.getItemMeta();
+
+ if(itemMeta == null) {
+ mcMMO.p.getLogger().severe("Item meta when adding potion to fishing treasure was null, contact the mcMMO devs!");
+ continue;
+ }
+
+ PotionType potionType = null;
+ try {
+ potionType = PotionType.valueOf(config.getString(type + "." + treasureName + ".PotionData.PotionType", "WATER"));
+ } catch (IllegalArgumentException ex) {
+ reason.add("Invalid Potion_Type: " + config.getString(type + "." + treasureName + ".PotionData.PotionType", "WATER"));
+ }
+ boolean extended = config.getBoolean(type + "." + treasureName + ".PotionData.Extended", false);
+ boolean upgraded = config.getBoolean(type + "." + treasureName + ".PotionData.Upgraded", false);
+ itemMeta.setBasePotionData(new PotionData(potionType, extended, upgraded));
+
+ if (customName != null) {
+ itemMeta.setDisplayName(ChatColor.translateAlternateColorCodes('&', customName));
+ }
+
+ if (config.contains(type + "." + treasureName + ".Lore")) {
+ List lore = new ArrayList<>();
+ for (String s : config.getStringList(type + "." + treasureName + ".Lore")) {
+ lore.add(ChatColor.translateAlternateColorCodes('&', s));
+ }
+ itemMeta.setLore(lore);
+ }
+ item.setItemMeta(itemMeta);
+ }
+ } else if(material == Material.ENCHANTED_BOOK) {
+ //If any whitelisted enchants exist we use whitelist-based matching
+ item = new ItemStack(material, 1);
+ ItemMeta itemMeta = item.getItemMeta();
+
+ List allowedEnchantsList = config.getStringList(type + "." + treasureName + ".Enchantments_Whitelist");
+ List disallowedEnchantsList = config.getStringList(type + "." + treasureName + ".Enchantments_Blacklist");
+
+ Set blackListedEnchants = new HashSet<>();
+ Set whiteListedEnchants = new HashSet<>();
+
+ matchAndFillSet(disallowedEnchantsList, blackListedEnchants);
+ matchAndFillSet(allowedEnchantsList, whiteListedEnchants);
+
+ if (customName != null && itemMeta != null) {
+ itemMeta.setDisplayName(ChatColor.translateAlternateColorCodes('&', customName));
+ item.setItemMeta(itemMeta);
+ }
+
+ FishingTreasureBook fishingTreasureBook = new FishingTreasureBook(item, xp, blackListedEnchants, whiteListedEnchants);
+ addFishingTreasure(rarity, fishingTreasureBook);
+ //TODO: Add book support for shake
+ continue; //The code in this whole file is a disaster, ignore this hacky solution :P
+ } else {
+ item = new ItemStack(material, amount, data);
+
+ if (customName != null) {
+ ItemMeta itemMeta = item.getItemMeta();
+ itemMeta.setDisplayName(ChatColor.translateAlternateColorCodes('&', customName));
+ item.setItemMeta(itemMeta);
+ }
+
+ if (config.contains(type + "." + treasureName + ".Lore")) {
+ ItemMeta itemMeta = item.getItemMeta();
+ List lore = new ArrayList<>();
+ for (String s : config.getStringList(type + "." + treasureName + ".Lore")) {
+ lore.add(ChatColor.translateAlternateColorCodes('&', s));
+ }
+ itemMeta.setLore(lore);
+ item.setItemMeta(itemMeta);
+ }
+ }
+
+
+
+ if (noErrorsInConfig(reason)) {
+ if (isFishing) {
+ addFishingTreasure(rarity, new FishingTreasure(item, xp));
+ } else if (isShake) {
+ ShakeTreasure shakeTreasure = new ShakeTreasure(item, xp, dropChance, dropLevel);
+
+ EntityType entityType = EntityType.valueOf(type.substring(6));
+ addShakeTreasure(shakeTreasure, entityType);
+ }
+ }
+ }
+ }
+
+ private void addShakeTreasure(@NotNull ShakeTreasure shakeTreasure, @NotNull EntityType entityType) {
+ if (!shakeMap.containsKey(entityType))
+ shakeMap.put(entityType, new ArrayList<>());
+ shakeMap.get(entityType).add(shakeTreasure);
+ }
+
+ private void addFishingTreasure(@NotNull Rarity rarity, @NotNull FishingTreasure fishingTreasure) {
+ fishingRewards.get(rarity).add(fishingTreasure);
+ }
+
+ private boolean hasCustomName(@NotNull String type, @NotNull String treasureName) {
+ return config.contains(type + "." + treasureName + ".Custom_Name");
+ }
+
+ /**
+ * Matches enchantments on a list (user provided string) to known enchantments in the Spigot API
+ * Any matches are added to the passed set
+ * @param enchantListStr the users string list of enchantments
+ * @param permissiveList the permissive list of enchantments
+ */
+ private void matchAndFillSet(@NotNull List enchantListStr, @NotNull Set permissiveList) {
+ if(enchantListStr.isEmpty()) {
+ return;
+ }
+
+ for(String str : enchantListStr) {
+ boolean foundMatch = false;
+ for(Enchantment enchantment : Enchantment.values()) {
+ if(enchantment.getKey().getKey().equalsIgnoreCase(str)) {
+ permissiveList.add(enchantment);
+ foundMatch = true;
+ break;
+ }
+ }
+
+ if(!foundMatch) {
+ mcMMO.p.getLogger().info("[Fishing Treasure Init] Could not find any enchantments which matched the user defined enchantment named: "+str);
+ }
+ }
+ }
+
+ private void loadEnchantments() {
+ for (Rarity rarity : Rarity.values()) {
+ if (!fishingEnchantments.containsKey(rarity)) {
+ fishingEnchantments.put(rarity, (new ArrayList<>()));
+ }
+
+ ConfigurationSection enchantmentSection = config.getConfigurationSection("Enchantments_Rarity." + rarity.toString());
+
+ if (enchantmentSection == null) {
+ return;
+ }
+
+ for (String enchantmentName : enchantmentSection.getKeys(false)) {
+ int level = config.getInt("Enchantments_Rarity." + rarity.toString() + "." + enchantmentName);
+ Enchantment enchantment = EnchantmentUtils.getByName(enchantmentName);
+
+ if (enchantment == null) {
+ plugin.getLogger().warning("Skipping invalid enchantment in " + FILENAME + ": " + enchantmentName);
+ continue;
+ }
+
+ fishingEnchantments.get(rarity).add(new EnchantmentTreasure(enchantment, level));
+ }
+ }
+ }
+
+ public boolean getInventoryStealEnabled() {
+ return config.contains("Shake.PLAYER.INVENTORY");
+ }
+
+ public boolean getInventoryStealStacks() {
+ return config.getBoolean("Shake.PLAYER.INVENTORY.Whole_Stacks");
+ }
+
+ public double getInventoryStealDropChance() {
+ return config.getDouble("Shake.PLAYER.INVENTORY.Drop_Chance");
+ }
+
+ public int getInventoryStealDropLevel() {
+ return config.getInt("Shake.PLAYER.INVENTORY.Drop_Level");
+ }
+
+ public double getItemDropRate(int tier, @NotNull Rarity rarity) {
+ return config.getDouble("Item_Drop_Rates.Tier_" + tier + "." + rarity.toString());
+ }
+
+ public double getEnchantmentDropRate(int tier, @NotNull Rarity rarity) {
+ return config.getDouble("Enchantment_Drop_Rates.Tier_" + tier + "." + rarity.toString());
+ }
+}
diff --git a/src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java b/src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java
index be050d298..51e01307e 100755
--- a/src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java
+++ b/src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java
@@ -1,14 +1,13 @@
package com.gmail.nossr50.config.treasure;
import com.gmail.nossr50.config.ConfigLoader;
-import com.gmail.nossr50.datatypes.treasure.*;
-import com.gmail.nossr50.util.EnchantmentUtils;
+import com.gmail.nossr50.datatypes.treasure.ExcavationTreasure;
+import com.gmail.nossr50.datatypes.treasure.HylianTreasure;
import com.gmail.nossr50.util.text.StringUtils;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.configuration.ConfigurationSection;
-import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.EntityType;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
@@ -22,18 +21,14 @@ import java.util.List;
public class TreasureConfig extends ConfigLoader {
+ public static final String FILENAME = "treasures.yml";
private static TreasureConfig instance;
public HashMap> excavationMap = new HashMap<>();
-
- public HashMap> shakeMap = new HashMap<>();
public HashMap> hylianMap = new HashMap<>();
- public HashMap> fishingRewards = new HashMap<>();
- public HashMap> fishingEnchantments = new HashMap<>();
-
private TreasureConfig() {
- super("treasures.yml");
+ super(FILENAME);
loadKeys();
validate();
}
@@ -50,34 +45,6 @@ public class TreasureConfig extends ConfigLoader {
protected boolean validateKeys() {
// Validate all the settings!
List reason = new ArrayList<>();
- for (String tier : config.getConfigurationSection("Enchantment_Drop_Rates").getKeys(false)) {
- double totalEnchantDropRate = 0;
- double totalItemDropRate = 0;
-
- for (Rarity rarity : Rarity.values()) {
- double enchantDropRate = config.getDouble("Enchantment_Drop_Rates." + tier + "." + rarity.toString());
- double itemDropRate = config.getDouble("Item_Drop_Rates." + tier + "." + rarity.toString());
-
- if ((enchantDropRate < 0.0 || enchantDropRate > 100.0) && rarity != Rarity.RECORD) {
- reason.add("The enchant drop rate for " + tier + " items that are " + rarity.toString() + "should be between 0.0 and 100.0!");
- }
-
- if (itemDropRate < 0.0 || itemDropRate > 100.0) {
- reason.add("The item drop rate for " + tier + " items that are " + rarity.toString() + "should be between 0.0 and 100.0!");
- }
-
- totalEnchantDropRate += enchantDropRate;
- totalItemDropRate += itemDropRate;
- }
-
- if (totalEnchantDropRate < 0 || totalEnchantDropRate > 100.0) {
- reason.add("The total enchant drop rate for " + tier + " should be between 0.0 and 100.0!");
- }
-
- if (totalItemDropRate < 0 || totalItemDropRate > 100.0) {
- reason.add("The total item drop rate for " + tier + " should be between 0.0 and 100.0!");
- }
- }
return noErrorsInConfig(reason);
}
@@ -89,21 +56,11 @@ public class TreasureConfig extends ConfigLoader {
return;
}
- loadTreasures("Fishing");
loadTreasures("Excavation");
loadTreasures("Hylian_Luck");
- loadEnchantments();
-
- for (EntityType entity : EntityType.values()) {
- if (entity.isAlive()) {
- loadTreasures("Shake." + entity.toString());
- }
- }
}
private void loadTreasures(String type) {
- boolean isFishing = type.equals("Fishing");
- boolean isShake = type.contains("Shake");
boolean isExcavation = type.equals("Excavation");
boolean isHylian = type.equals("Hylian_Luck");
@@ -113,13 +70,6 @@ public class TreasureConfig extends ConfigLoader {
return;
}
- // Initialize fishing HashMap
- for (Rarity rarity : Rarity.values()) {
- if (!fishingRewards.containsKey(rarity)) {
- fishingRewards.put(rarity, (new ArrayList<>()));
- }
- }
-
for (String treasureName : treasureSection.getKeys(false)) {
// Validate all the things!
List reason = new ArrayList<>();
@@ -131,16 +81,7 @@ public class TreasureConfig extends ConfigLoader {
* Material, Amount, and Data
*/
Material material;
-
- if (materialName.contains("INVENTORY")) {
- // Use magic material BEDROCK to know that we're grabbing something from the inventory and not a normal treasure
- if (!shakeMap.containsKey(EntityType.PLAYER))
- shakeMap.put(EntityType.PLAYER, new ArrayList<>());
- shakeMap.get(EntityType.PLAYER).add(new ShakeTreasure(new ItemStack(Material.BEDROCK, 1, (byte) 0), 1, getInventoryStealDropChance(), getInventoryStealDropLevel()));
- continue;
- } else {
- material = Material.matchMaterial(materialName);
- }
+ material = Material.matchMaterial(materialName);
int amount = config.getInt(type + "." + treasureName + ".Amount");
short data = (treasureInfo.length == 2) ? Short.parseShort(treasureInfo[1]) : (short) config.getInt(type + "." + treasureName + ".Data");
@@ -177,19 +118,6 @@ public class TreasureConfig extends ConfigLoader {
reason.add(treasureName + " has an invalid Drop_Level: " + dropLevel);
}
- /*
- * Specific Types
- */
- Rarity rarity = null;
-
- if (isFishing) {
- rarity = Rarity.getRarity(config.getString(type + "." + treasureName + ".Rarity"));
-
- if (rarity == null) {
- reason.add("Invalid Rarity for item: " + treasureName);
- }
- }
-
/*
* Itemstack
*/
@@ -198,7 +126,7 @@ public class TreasureConfig extends ConfigLoader {
if (materialName.contains("POTION")) {
Material mat = Material.matchMaterial(materialName);
if (mat == null) {
- reason.add("Potion format for Treasures.yml has changed");
+ reason.add("Potion format for " + FILENAME + " has changed");
} else {
item = new ItemStack(mat, amount, data);
PotionMeta itemMeta = (PotionMeta) item.getItemMeta();
@@ -247,16 +175,7 @@ public class TreasureConfig extends ConfigLoader {
}
if (noErrorsInConfig(reason)) {
- if (isFishing) {
- fishingRewards.get(rarity).add(new FishingTreasure(item, xp));
- } else if (isShake) {
- ShakeTreasure shakeTreasure = new ShakeTreasure(item, xp, dropChance, dropLevel);
-
- EntityType entityType = EntityType.valueOf(type.substring(6));
- if (!shakeMap.containsKey(entityType))
- shakeMap.put(entityType, new ArrayList<>());
- shakeMap.get(entityType).add(shakeTreasure);
- } else if (isExcavation) {
+ if (isExcavation) {
ExcavationTreasure excavationTreasure = new ExcavationTreasure(item, xp, dropChance, dropLevel);
List dropList = config.getStringList(type + "." + treasureName + ".Drops_From");
@@ -308,58 +227,4 @@ public class TreasureConfig extends ConfigLoader {
hylianMap.put(dropper, new ArrayList<>());
hylianMap.get(dropper).add(treasure);
}
-
- private void loadEnchantments() {
- for (Rarity rarity : Rarity.values()) {
- if (rarity == Rarity.RECORD) {
- continue;
- }
-
- if (!fishingEnchantments.containsKey(rarity)) {
- fishingEnchantments.put(rarity, (new ArrayList<>()));
- }
-
- ConfigurationSection enchantmentSection = config.getConfigurationSection("Enchantments_Rarity." + rarity.toString());
-
- if (enchantmentSection == null) {
- return;
- }
-
- for (String enchantmentName : enchantmentSection.getKeys(false)) {
- int level = config.getInt("Enchantments_Rarity." + rarity.toString() + "." + enchantmentName);
- Enchantment enchantment = EnchantmentUtils.getByName(enchantmentName);
-
- if (enchantment == null) {
- plugin.getLogger().warning("Skipping invalid enchantment in treasures.yml: " + enchantmentName);
- continue;
- }
-
- fishingEnchantments.get(rarity).add(new EnchantmentTreasure(enchantment, level));
- }
- }
- }
-
- public boolean getInventoryStealEnabled() {
- return config.contains("Shake.PLAYER.INVENTORY");
- }
-
- public boolean getInventoryStealStacks() {
- return config.getBoolean("Shake.PLAYER.INVENTORY.Whole_Stacks");
- }
-
- public double getInventoryStealDropChance() {
- return config.getDouble("Shake.PLAYER.INVENTORY.Drop_Chance");
- }
-
- public int getInventoryStealDropLevel() {
- return config.getInt("Shake.PLAYER.INVENTORY.Drop_Level");
- }
-
- public double getItemDropRate(int tier, Rarity rarity) {
- return config.getDouble("Item_Drop_Rates.Tier_" + tier + "." + rarity.toString());
- }
-
- public double getEnchantmentDropRate(int tier, Rarity rarity) {
- return config.getDouble("Enchantment_Drop_Rates.Tier_" + tier + "." + rarity.toString());
- }
}
diff --git a/src/main/java/com/gmail/nossr50/datatypes/treasure/EnchantmentWrapper.java b/src/main/java/com/gmail/nossr50/datatypes/treasure/EnchantmentWrapper.java
new file mode 100644
index 000000000..c442b4179
--- /dev/null
+++ b/src/main/java/com/gmail/nossr50/datatypes/treasure/EnchantmentWrapper.java
@@ -0,0 +1,44 @@
+package com.gmail.nossr50.datatypes.treasure;
+
+import com.google.common.base.Objects;
+import org.bukkit.enchantments.Enchantment;
+import org.jetbrains.annotations.NotNull;
+
+public class EnchantmentWrapper {
+ private final @NotNull Enchantment enchantment;
+ private final int enchantmentLevel;
+
+ public EnchantmentWrapper(@NotNull Enchantment enchantment, int enchantmentLevel) {
+ this.enchantment = enchantment;
+ this.enchantmentLevel = enchantmentLevel;
+ }
+
+ public @NotNull Enchantment getEnchantment() {
+ return enchantment;
+ }
+
+ public int getEnchantmentLevel() {
+ return enchantmentLevel;
+ }
+
+ @Override
+ public String toString() {
+ return "EnchantmentWrapper{" +
+ "enchantment=" + enchantment +
+ ", enchantmentLevel=" + enchantmentLevel +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ EnchantmentWrapper that = (EnchantmentWrapper) o;
+ return enchantmentLevel == that.enchantmentLevel && Objects.equal(enchantment, that.enchantment);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(enchantment, enchantmentLevel);
+ }
+}
diff --git a/src/main/java/com/gmail/nossr50/datatypes/treasure/FishingTreasureBook.java b/src/main/java/com/gmail/nossr50/datatypes/treasure/FishingTreasureBook.java
new file mode 100644
index 000000000..8f89f7484
--- /dev/null
+++ b/src/main/java/com/gmail/nossr50/datatypes/treasure/FishingTreasureBook.java
@@ -0,0 +1,76 @@
+package com.gmail.nossr50.datatypes.treasure;
+
+import com.gmail.nossr50.mcMMO;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class FishingTreasureBook extends FishingTreasure {
+ private final @Nullable Set blackListedEnchantments;
+ private final @Nullable Set whiteListedEnchantments;
+ private final @NotNull List legalEnchantments; //TODO: Make immutable
+
+ public FishingTreasureBook(@NotNull ItemStack enchantedBook, int xp, @Nullable Set blackListedEnchantments, @Nullable Set whiteListedEnchantments) {
+ super(enchantedBook, xp);
+
+ this.blackListedEnchantments = blackListedEnchantments;
+ this.whiteListedEnchantments = whiteListedEnchantments;
+ this.legalEnchantments = new ArrayList<>();
+
+ initLegalEnchantments();
+ }
+
+ private void initLegalEnchantments() {
+ mcMMO.p.getLogger().info("Registering enchantments for Fishing Book...");
+
+ for(Enchantment enchantment : Enchantment.values()) {
+ if(isEnchantAllowed(enchantment)) {
+ addAllLegalEnchants(enchantment);
+ }
+ }
+ }
+
+ /**
+ * Get all the enchantments which can drop for this book
+ * This list can be empty, but should in practice never be empty...
+ *
+ * @return all the enchantments that can drop for this book
+ */
+ public @NotNull List getLegalEnchantments() {
+ return legalEnchantments;
+ }
+
+ private @Nullable Set getBlacklistedEnchantments() {
+ return blackListedEnchantments;
+ }
+
+ private @Nullable Set getWhitelistedEnchantments() {
+ return whiteListedEnchantments;
+ }
+
+ private void addAllLegalEnchants(@NotNull Enchantment enchantment) {
+ int legalEnchantCap = enchantment.getMaxLevel();
+
+ for(int i = 0; i < legalEnchantCap; i++) {
+ int enchantLevel = i+1;
+ EnchantmentWrapper enchantmentWrapper = new EnchantmentWrapper(enchantment, enchantLevel);
+ legalEnchantments.add(enchantmentWrapper);
+// mcMMO.p.getLogger().info("Fishing treasure book enchantment added: " + enchantmentWrapper);
+ }
+ }
+
+ private boolean isEnchantAllowed(@NotNull Enchantment enchantment) {
+ if(whiteListedEnchantments != null && !whiteListedEnchantments.isEmpty()) {
+ return whiteListedEnchantments.contains(enchantment);
+ } else if(blackListedEnchantments != null && !blackListedEnchantments.isEmpty()) {
+ return !blackListedEnchantments.contains(enchantment);
+ } else {
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/com/gmail/nossr50/datatypes/treasure/Rarity.java b/src/main/java/com/gmail/nossr50/datatypes/treasure/Rarity.java
index 40cddb737..4666b32b1 100644
--- a/src/main/java/com/gmail/nossr50/datatypes/treasure/Rarity.java
+++ b/src/main/java/com/gmail/nossr50/datatypes/treasure/Rarity.java
@@ -1,14 +1,21 @@
package com.gmail.nossr50.datatypes.treasure;
+import com.gmail.nossr50.mcMMO;
+import org.jetbrains.annotations.NotNull;
+
public enum Rarity {
- RECORD,
+ MYTHIC,
LEGENDARY,
EPIC,
RARE,
UNCOMMON,
COMMON;
- public static Rarity getRarity(String string) {
+ public static @NotNull Rarity getRarity(@NotNull String string) {
+ if(string.equalsIgnoreCase("Records")) {
+ mcMMO.p.getLogger().severe("Entries in fishing treasures have Records set as rarity, however Records was renamed to Mythic. Please update your treasures to read MYTHIC instead of RECORDS for rarity, or delete the config file to regenerate a new one.");
+ return Rarity.MYTHIC; //People that copy paste their configs will have Records interpretted as Mythic
+ }
try {
return valueOf(string);
}
diff --git a/src/main/java/com/gmail/nossr50/listeners/EntityListener.java b/src/main/java/com/gmail/nossr50/listeners/EntityListener.java
index 53f98625c..60610d13f 100644
--- a/src/main/java/com/gmail/nossr50/listeners/EntityListener.java
+++ b/src/main/java/com/gmail/nossr50/listeners/EntityListener.java
@@ -357,7 +357,8 @@ public class EntityListener implements Listener {
return;
}
- if (Misc.isNPCEntityExcludingVillagers(defender) || !defender.isValid() || !(defender instanceof LivingEntity)) {
+
+ if ((ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(defender)) || !defender.isValid() || !(defender instanceof LivingEntity)) {
return;
}
@@ -367,7 +368,7 @@ public class EntityListener implements Listener {
return;
}
- if (Misc.isNPCEntityExcludingVillagers(attacker)) {
+ if (ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(attacker)) {
return;
}
@@ -547,7 +548,7 @@ public class EntityListener implements Listener {
}
*/
- if (Misc.isNPCEntityExcludingVillagers(entity) || !entity.isValid() || !(entity instanceof LivingEntity)) {
+ if ((ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(entity)) || !entity.isValid() || !(entity instanceof LivingEntity)) {
return;
}
@@ -694,7 +695,7 @@ public class EntityListener implements Listener {
LivingEntity entity = event.getEntity();
- if (Misc.isNPCEntityExcludingVillagers(entity)) {
+ if (ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(entity)) {
return;
}
@@ -1002,7 +1003,7 @@ public class EntityListener implements Listener {
LivingEntity livingEntity = event.getEntity();
if (mcMMO.getUserManager().queryPlayer(player) == null
- || Misc.isNPCEntityExcludingVillagers(livingEntity)
+ || (ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(livingEntity))
|| persistentDataLayer.hasMobFlag(MobMetaFlagType.EGG_MOB, livingEntity)
|| persistentDataLayer.hasMobFlag(MobMetaFlagType.MOB_SPAWNER_MOB, livingEntity)) {
return;
diff --git a/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java
index cbd87f3ce..cfb114cd0 100644
--- a/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java
+++ b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java
@@ -420,7 +420,7 @@ public class PlayerListener implements Listener {
}
}
- fishingManager.handleFishing((Item) caught);
+ fishingManager.processFishing((Item) caught);
fishingManager.setFishingTarget();
}
return;
@@ -619,8 +619,8 @@ public class PlayerListener implements Listener {
if(clickedBlockType == Repair.anvilMaterial || clickedBlockType == Salvage.anvilMaterial) {
event.setUseItemInHand(Event.Result.ALLOW);
- if(mcMMO.getMaterialMapStore().isToolActivationBlackListed(clickedBlockType)) {
- event.setUseInteractedBlock(Event.Result.DENY);
+ if(!event.getPlayer().isSneaking() && mcMMO.getMaterialMapStore().isToolActivationBlackListed(clickedBlockType)) {
+ event.setUseInteractedBlock(Event.Result.DENY);
}
}
@@ -781,7 +781,7 @@ public class PlayerListener implements Listener {
public void onPlayerChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer();
- if (Misc.isNPCEntityExcludingVillagers(player) || !mcMMO.getUserManager().hasPlayerDataKey(player)) {
+ if ((ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(player)) || !UserManager.hasPlayerDataKey(player)) {
return;
}
diff --git a/src/main/java/com/gmail/nossr50/listeners/WorldListener.java b/src/main/java/com/gmail/nossr50/listeners/WorldListener.java
index 5fc386c86..650a0e0cc 100644
--- a/src/main/java/com/gmail/nossr50/listeners/WorldListener.java
+++ b/src/main/java/com/gmail/nossr50/listeners/WorldListener.java
@@ -42,28 +42,6 @@ public class WorldListener implements Listener {
}
}
- /**
- * Monitor WorldInit events.
- *
- * @param event The event to watch
- */
- @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
- public void onWorldInit(WorldInitEvent event) {
- /* WORLD BLACKLIST CHECK */
- if(WorldBlacklist.isWorldBlacklisted(event.getWorld()))
- return;
-
- World world = event.getWorld();
-
- if (!new File(world.getWorldFolder(), "mcmmo_data").exists() || plugin == null) {
- return;
- }
-
- plugin.getLogger().info("Converting block storage for " + world.getName() + " to a new format.");
-
- //new BlockStoreConversionMain(world).run();
- }
-
/**
* Monitor WorldUnload events.
*
diff --git a/src/main/java/com/gmail/nossr50/mcMMO.java b/src/main/java/com/gmail/nossr50/mcMMO.java
index 88bbef43d..5247cc72b 100644
--- a/src/main/java/com/gmail/nossr50/mcMMO.java
+++ b/src/main/java/com/gmail/nossr50/mcMMO.java
@@ -7,6 +7,7 @@ import com.gmail.nossr50.config.experience.ExperienceConfig;
import com.gmail.nossr50.config.skills.alchemy.PotionConfig;
import com.gmail.nossr50.config.skills.repair.RepairConfigManager;
import com.gmail.nossr50.config.skills.salvage.SalvageConfigManager;
+import com.gmail.nossr50.config.treasure.FishingTreasureConfig;
import com.gmail.nossr50.config.treasure.TreasureConfig;
import com.gmail.nossr50.database.DatabaseManager;
import com.gmail.nossr50.database.DatabaseManagerFactory;
@@ -34,8 +35,8 @@ import com.gmail.nossr50.skills.salvage.salvageables.Salvageable;
import com.gmail.nossr50.skills.salvage.salvageables.SalvageableManager;
import com.gmail.nossr50.skills.salvage.salvageables.SimpleSalvageableManager;
import com.gmail.nossr50.util.*;
-import com.gmail.nossr50.util.blockmeta.chunkmeta.ChunkManager;
-import com.gmail.nossr50.util.blockmeta.chunkmeta.ChunkManagerFactory;
+import com.gmail.nossr50.util.blockmeta.ChunkManager;
+import com.gmail.nossr50.util.blockmeta.ChunkManagerFactory;
import com.gmail.nossr50.util.commands.CommandRegistrationManager;
import com.gmail.nossr50.util.compat.CompatibilityManager;
import com.gmail.nossr50.util.experience.FormulaManager;
@@ -345,8 +346,8 @@ public class mcMMO extends JavaPlugin {
formulaManager.saveFormula();
holidayManager.saveAnniversaryFiles();
- placeStore.saveAll(); // Save our metadata
placeStore.cleanUp(); // Cleanup empty metadata stores
+ placeStore.closeAll();
}
catch (Exception e) { e.printStackTrace(); }
@@ -527,6 +528,7 @@ public class mcMMO extends JavaPlugin {
private void loadConfigFiles() {
// Force the loading of config files
TreasureConfig.getInstance();
+ FishingTreasureConfig.getInstance();
HiddenConfig.getInstance();
AdvancedConfig.getInstance();
PotionConfig.getInstance();
diff --git a/src/main/java/com/gmail/nossr50/skills/fishing/Fishing.java b/src/main/java/com/gmail/nossr50/skills/fishing/Fishing.java
index c5b7ef833..965b9c924 100644
--- a/src/main/java/com/gmail/nossr50/skills/fishing/Fishing.java
+++ b/src/main/java/com/gmail/nossr50/skills/fishing/Fishing.java
@@ -1,6 +1,6 @@
package com.gmail.nossr50.skills.fishing;
-import com.gmail.nossr50.config.treasure.TreasureConfig;
+import com.gmail.nossr50.config.treasure.FishingTreasureConfig;
import com.gmail.nossr50.datatypes.treasure.ShakeTreasure;
import com.gmail.nossr50.util.Misc;
import com.gmail.nossr50.util.adapter.BiomeAdapter;
@@ -31,8 +31,8 @@ public final class Fishing {
* @return possibleDrops List of ItemStack that can be dropped
*/
protected static List findPossibleDrops(LivingEntity target) {
- if (TreasureConfig.getInstance().shakeMap.containsKey(target.getType()))
- return TreasureConfig.getInstance().shakeMap.get(target.getType());
+ if (FishingTreasureConfig.getInstance().shakeMap.containsKey(target.getType()))
+ return FishingTreasureConfig.getInstance().shakeMap.get(target.getType());
return null;
}
diff --git a/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java b/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java
index 9c47ffdeb..076723292 100644
--- a/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java
+++ b/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java
@@ -4,15 +4,13 @@ import com.gmail.nossr50.api.ItemSpawnReason;
import com.gmail.nossr50.config.AdvancedConfig;
import com.gmail.nossr50.config.Config;
import com.gmail.nossr50.config.experience.ExperienceConfig;
-import com.gmail.nossr50.config.treasure.TreasureConfig;
+import com.gmail.nossr50.config.treasure.FishingTreasureConfig;
+import com.gmail.nossr50.datatypes.experience.XPGainReason;
import com.gmail.nossr50.datatypes.interactions.NotificationType;
import com.neetgames.mcmmo.player.OnlineMMOPlayer;
import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
import com.gmail.nossr50.datatypes.skills.SubSkillType;
-import com.gmail.nossr50.datatypes.treasure.EnchantmentTreasure;
-import com.gmail.nossr50.datatypes.treasure.FishingTreasure;
-import com.gmail.nossr50.datatypes.treasure.Rarity;
-import com.gmail.nossr50.datatypes.treasure.ShakeTreasure;
+import com.gmail.nossr50.datatypes.treasure.*;
import com.gmail.nossr50.events.skills.fishing.McMMOPlayerFishingTreasureEvent;
import com.gmail.nossr50.events.skills.fishing.McMMOPlayerShakeEvent;
import com.gmail.nossr50.locale.LocaleLoader;
@@ -39,10 +37,12 @@ import org.bukkit.entity.*;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
+import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.util.*;
@@ -369,9 +369,7 @@ public class FishingManager extends SkillManager {
return AdvancedConfig.getInstance().getFishingBoatReductionMaxWaitTicks();
}
-
- public boolean isMagicHunterEnabled()
- {
+ public boolean isMagicHunterEnabled() {
return RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.FISHING_MAGIC_HUNTER)
&& RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.FISHING_TREASURE_HUNTER)
&& Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.FISHING_TREASURE_HUNTER);
@@ -382,12 +380,14 @@ public class FishingManager extends SkillManager {
*
* @param fishingCatch The {@link Item} initially caught
*/
- public void handleFishing(Item fishingCatch) {
+ public void processFishing(@NotNull Item fishingCatch) {
this.fishingCatch = fishingCatch;
int fishXp = ExperienceConfig.getInstance().getXp(PrimarySkillType.FISHING, fishingCatch.getItemStack().getType());
int treasureXp = 0;
+ ItemStack treasureDrop = null;
Player player = getPlayer();
FishingTreasure treasure = null;
+ boolean fishingSucceeds = false;
if (Config.getInstance().getFishingDropsEnabled() && Permissions.isSubSkillEnabled(player, SubSkillType.FISHING_TREASURE_HUNTER)) {
treasure = getFishingTreasure();
@@ -395,49 +395,92 @@ public class FishingManager extends SkillManager {
}
if (treasure != null) {
- ItemStack treasureDrop = treasure.getDrop().clone(); // Not cloning is bad, m'kay?
- Map enchants = new HashMap<>();
+ if(treasure instanceof FishingTreasureBook) {
+ treasureDrop = createEnchantBook((FishingTreasureBook) treasure);
+ } else {
+ treasureDrop = treasure.getDrop().clone(); // Not cloning is bad, m'kay?
- if (isMagicHunterEnabled()
- && ItemUtils.isEnchantable(treasureDrop)) {
- enchants = handleMagicHunter(treasureDrop);
}
+ Map enchants = new HashMap<>();
+ McMMOPlayerFishingTreasureEvent event;
- McMMOPlayerFishingTreasureEvent event = EventUtils.callFishingTreasureEvent(player, treasureDrop, treasure.getXp(), enchants);
+ /*
+ * Books get some special treatment
+ */
+ if(treasure instanceof FishingTreasureBook) {
+ //Skip the magic hunter stuff
+ if(treasureDrop.getItemMeta() != null) {
+ enchants.putAll(treasureDrop.getItemMeta().getEnchants());
+ }
+
+ event = EventUtils.callFishingTreasureEvent(player, treasureDrop, treasure.getXp(), enchants);
+ } else {
+ if (isMagicHunterEnabled() && ItemUtils.isEnchantable(treasureDrop)) {
+ enchants = processMagicHunter(treasureDrop);
+ }
+
+ event = EventUtils.callFishingTreasureEvent(player, treasureDrop, treasure.getXp(), enchants);
+ }
if (!event.isCancelled()) {
treasureDrop = event.getTreasure();
treasureXp = event.getXp();
- }
- else {
+
+ // Drop the original catch at the feet of the player and set the treasure as the real catch
+ if (treasureDrop != null) {
+ fishingSucceeds = true;
+ boolean enchanted = false;
+
+ if(treasure instanceof FishingTreasureBook) {
+ enchanted = true;
+ } else if (!enchants.isEmpty()) {
+ treasureDrop.addUnsafeEnchantments(enchants);
+ enchanted = true;
+ }
+
+ if (enchanted) {
+ NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE, "Fishing.Ability.TH.MagicFound");
+ }
+
+ }
+ } else {
treasureDrop = null;
treasureXp = 0;
}
+ }
- // Drop the original catch at the feet of the player and set the treasure as the real catch
- if (treasureDrop != null) {
- boolean enchanted = false;
+ if(fishingSucceeds) {
+ fishingCatch.setItemStack(treasureDrop);
- if (!enchants.isEmpty()) {
- treasureDrop.addUnsafeEnchantments(enchants);
- enchanted = true;
- }
-
- if (enchanted) {
- NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE, "Fishing.Ability.TH.MagicFound");
- }
-
- if (Config.getInstance().getFishingExtraFish()) {
- Misc.spawnItem(player.getEyeLocation(), fishingCatch.getItemStack(), ItemSpawnReason.FISHING_EXTRA_FISH);
- }
-
- fishingCatch.setItemStack(treasureDrop);
+ if (Config.getInstance().getFishingExtraFish()) {
+ Misc.spawnItem(player.getEyeLocation(), fishingCatch.getItemStack(), ItemSpawnReason.FISHING_EXTRA_FISH);
}
}
applyXpGain(fishXp + treasureXp, XPGainReason.PVE);
}
+
+ private @NotNull ItemStack createEnchantBook(@NotNull FishingTreasureBook fishingTreasureBook) {
+ ItemStack itemStack = fishingTreasureBook.getDrop().clone();
+ EnchantmentWrapper enchantmentWrapper = getRandomEnchantment(fishingTreasureBook.getLegalEnchantments());
+ ItemMeta itemMeta = itemStack.getItemMeta();
+
+ if(itemMeta == null)
+ return itemStack;
+
+ itemMeta.addEnchant(enchantmentWrapper.getEnchantment(), enchantmentWrapper.getEnchantmentLevel(), ExperienceConfig.getInstance().allowUnsafeEnchantments());
+ itemStack.setItemMeta(itemMeta);
+ return itemStack;
+ }
+
+ private @NotNull EnchantmentWrapper getRandomEnchantment(@NotNull List enchantmentWrappers) {
+ Collections.shuffle(enchantmentWrappers, Misc.getRandom());
+
+ int randomIndex = Misc.getRandom().nextInt(enchantmentWrappers.size());
+ return enchantmentWrappers.get(randomIndex);
+ }
+
/**
* Handle the vanilla XP boost for Fishing
*
@@ -487,7 +530,7 @@ public class FishingManager extends SkillManager {
break;
case BEDROCK:
- if (TreasureConfig.getInstance().getInventoryStealEnabled()) {
+ if (FishingTreasureConfig.getInstance().getInventoryStealEnabled()) {
PlayerInventory inventory = targetPlayer.getInventory();
int length = inventory.getContents().length;
int slot = Misc.getRandom().nextInt(length);
@@ -497,7 +540,7 @@ public class FishingManager extends SkillManager {
break;
}
- if (TreasureConfig.getInstance().getInventoryStealStacks()) {
+ if (FishingTreasureConfig.getInstance().getInventoryStealStacks()) {
inventory.setItem(slot, null);
}
else {
@@ -547,7 +590,7 @@ public class FishingManager extends SkillManager {
*
* @return The {@link FishingTreasure} found, or null if no treasure was found.
*/
- private FishingTreasure getFishingTreasure() {
+ private @Nullable FishingTreasure getFishingTreasure() {
double diceRoll = Misc.getRandom().nextDouble() * 100;
int luck;
@@ -565,15 +608,11 @@ public class FishingManager extends SkillManager {
FishingTreasure treasure = null;
for (Rarity rarity : Rarity.values()) {
- double dropRate = TreasureConfig.getInstance().getItemDropRate(getLootTier(), rarity);
+ double dropRate = FishingTreasureConfig.getInstance().getItemDropRate(getLootTier(), rarity);
if (diceRoll <= dropRate) {
- /*if (rarity == Rarity.TRAP) {
- handleTraps();
- break;
- }*/
- List fishingTreasures = TreasureConfig.getInstance().fishingRewards.get(rarity);
+ List fishingTreasures = FishingTreasureConfig.getInstance().fishingRewards.get(rarity);
if (fishingTreasures.isEmpty()) {
return null;
@@ -611,21 +650,16 @@ public class FishingManager extends SkillManager {
* Process the Magic Hunter ability
*
* @param treasureDrop The {@link ItemStack} to enchant
- *
- * @return true if the item has been enchanted
*/
- private Map handleMagicHunter(ItemStack treasureDrop) {
+ private Map processMagicHunter(@NotNull ItemStack treasureDrop) {
Map enchants = new HashMap<>();
List fishingEnchantments = null;
double diceRoll = Misc.getRandom().nextDouble() * 100;
for (Rarity rarity : Rarity.values()) {
- if (rarity == Rarity.RECORD) {
- continue;
- }
- double dropRate = TreasureConfig.getInstance().getEnchantmentDropRate(getLootTier(), rarity);
+ double dropRate = FishingTreasureConfig.getInstance().getEnchantmentDropRate(getLootTier(), rarity);
if (diceRoll <= dropRate) {
// Make sure enchanted books always get some kind of enchantment. --hoorigan
@@ -633,7 +667,8 @@ public class FishingManager extends SkillManager {
diceRoll = dropRate + 1;
continue;
}
- fishingEnchantments = TreasureConfig.getInstance().fishingEnchantments.get(rarity);
+
+ fishingEnchantments = FishingTreasureConfig.getInstance().fishingEnchantments.get(rarity);
break;
}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java
new file mode 100644
index 000000000..dce7ab174
--- /dev/null
+++ b/src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java
@@ -0,0 +1,243 @@
+package com.gmail.nossr50.util.blockmeta;
+
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+
+import java.io.*;
+import java.util.BitSet;
+import java.util.UUID;
+
+public class BitSetChunkStore implements ChunkStore, Serializable {
+ private static final long serialVersionUID = -1L;
+ transient private boolean dirty = false;
+ // Bitset store conforms to a "bottom-up" bit ordering consisting of a stack of {worldHeight} Y planes, each Y plane consists of 16 Z rows of 16 X bits.
+ private BitSet store;
+ private static final int CURRENT_VERSION = 8;
+ private static final int MAGIC_NUMBER = 0xEA5EDEBB;
+ private int cx;
+ private int cz;
+ private int worldHeight;
+ private UUID worldUid;
+
+ public BitSetChunkStore(World world, int cx, int cz) {
+ this.cx = cx;
+ this.cz = cz;
+ this.worldUid = world.getUID();
+ this.worldHeight = world.getMaxHeight();
+ this.store = new BitSet(16 * 16 * worldHeight);
+ }
+
+ private BitSetChunkStore() {}
+
+ @Override
+ public boolean isDirty() {
+ return dirty;
+ }
+
+ @Override
+ public void setDirty(boolean dirty) {
+ this.dirty = dirty;
+ }
+
+ @Override
+ public int getChunkX() {
+ return cx;
+ }
+
+ @Override
+ public int getChunkZ() {
+ return cz;
+ }
+
+ @Override
+ public UUID getWorldId() {
+ return worldUid;
+ }
+
+ @Override
+ public boolean isTrue(int x, int y, int z) {
+ return store.get(coordToIndex(x, y, z));
+ }
+
+ @Override
+ public void setTrue(int x, int y, int z) {
+ set(x, y, z, true);
+ }
+
+ @Override
+ public void setFalse(int x, int y, int z) {
+ set(x, y, z, false);
+ }
+
+ @Override
+ public void set(int x, int y, int z, boolean value) {
+ store.set(coordToIndex(x, y, z), value);
+ dirty = true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return store.isEmpty();
+ }
+
+ private int coordToIndex(int x, int y, int z) {
+ if (x < 0 || x >= 16 || y < 0 || y >= worldHeight || z < 0 || z >= 16)
+ throw new IndexOutOfBoundsException();
+ return (z * 16 + x) + (256 * y);
+ }
+
+ private void fixWorldHeight() {
+ World world = Bukkit.getWorld(worldUid);
+
+ // Not sure how this case could come up, but might as well handle it gracefully. Loading a chunkstore for an unloaded world?
+ if (world == null)
+ return;
+
+ // Lop off any extra data if the world height has shrunk
+ int currentWorldHeight = world.getMaxHeight();
+ if (currentWorldHeight < worldHeight)
+ {
+ store.clear(coordToIndex(16, currentWorldHeight, 16), store.length());
+ worldHeight = currentWorldHeight;
+ dirty = true;
+ }
+ // If the world height has grown, update the worldHeight variable, but don't bother marking it dirty as unless something else changes we don't need to force a file write;
+ else if (currentWorldHeight > worldHeight)
+ worldHeight = currentWorldHeight;
+ }
+
+ @Deprecated
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ throw new UnsupportedOperationException("Serializable support should only be used for legacy deserialization");
+ }
+
+ @Deprecated
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.readInt(); // Magic number
+ in.readInt(); // Format version
+ long lsb = in.readLong();
+ long msb = in.readLong();
+ worldUid = new UUID(msb, lsb);
+ cx = in.readInt();
+ cz = in.readInt();
+
+ boolean[][][] oldStore = (boolean[][][]) in.readObject();
+ worldHeight = oldStore[0][0].length;
+ store = new BitSet(16 * 16 * worldHeight / 8);
+ for (int x = 0; x < 16; x++) {
+ for (int z = 0; z < 16; z++) {
+ for (int y = 0; y < worldHeight; y++) {
+ store.set(coordToIndex(x, y, z), oldStore[x][z][y]);
+ }
+ }
+ }
+ dirty = true;
+ fixWorldHeight();
+ }
+
+ private void serialize(DataOutputStream out) throws IOException {
+ out.writeInt(MAGIC_NUMBER);
+ out.writeInt(CURRENT_VERSION);
+
+ out.writeLong(worldUid.getLeastSignificantBits());
+ out.writeLong(worldUid.getMostSignificantBits());
+ out.writeInt(cx);
+ out.writeInt(cz);
+ out.writeInt(worldHeight);
+
+ // Store the byte array directly so we don't have the object type info overhead
+ byte[] storeData = store.toByteArray();
+ out.writeInt(storeData.length);
+ out.write(storeData);
+
+ dirty = false;
+ }
+
+ private static BitSetChunkStore deserialize(DataInputStream in) throws IOException {
+ int magic = in.readInt();
+ // Can be used to determine the format of the file
+ int fileVersionNumber = in.readInt();
+
+ if (magic != MAGIC_NUMBER || fileVersionNumber != CURRENT_VERSION)
+ throw new IOException();
+
+ BitSetChunkStore chunkStore = new BitSetChunkStore();
+
+ long lsb = in.readLong();
+ long msb = in.readLong();
+ chunkStore.worldUid = new UUID(msb, lsb);
+ chunkStore.cx = in.readInt();
+ chunkStore.cz = in.readInt();
+
+ chunkStore.worldHeight = in.readInt();
+ byte[] temp = new byte[in.readInt()];
+ in.readFully(temp);
+ chunkStore.store = BitSet.valueOf(temp);
+
+ chunkStore.fixWorldHeight();
+ return chunkStore;
+ }
+
+ public static class Serialization {
+
+ public static final short STREAM_MAGIC = (short)0xACDC;
+
+ public static ChunkStore readChunkStore(DataInputStream inputStream) throws IOException {
+ if (inputStream.markSupported())
+ inputStream.mark(2);
+ short magicNumber = inputStream.readShort();
+
+ if (magicNumber == ObjectStreamConstants.STREAM_MAGIC) // Java serializable, use legacy serialization
+ {
+ // "Un-read" the magic number for Serializables, they need it to still be in the stream
+ if (inputStream.markSupported())
+ inputStream.reset(); // Pretend we never read those bytes
+ else
+ {
+ // Creates a new stream with the two magic number bytes and then the rest of the original stream... Java is so dumb. I just wanted to look at two bytes.
+ PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream, 2);
+ pushbackInputStream.unread((magicNumber >>> 0) & 0xFF);
+ pushbackInputStream.unread((magicNumber >>> 8) & 0xFF);
+ inputStream = new DataInputStream(pushbackInputStream);
+ }
+ return new LegacyDeserializationInputStream(inputStream).readLegacyChunkStore();
+ }
+ else if (magicNumber == STREAM_MAGIC) // Pure bytes format
+ {
+ return BitSetChunkStore.deserialize(inputStream);
+ }
+ throw new IOException("Bad Data Format");
+ }
+
+ public static void writeChunkStore(DataOutputStream outputStream, ChunkStore chunkStore) throws IOException {
+ if (!(chunkStore instanceof BitSetChunkStore))
+ throw new InvalidClassException("ChunkStore must be instance of BitSetChunkStore");
+ outputStream.writeShort(STREAM_MAGIC);
+ ((BitSetChunkStore)chunkStore).serialize(outputStream);
+ }
+
+ // Handles loading the old serialized classes even though we have changed name/package
+ private static class LegacyDeserializationInputStream extends ObjectInputStream {
+ public LegacyDeserializationInputStream(InputStream in) throws IOException {
+ super(in);
+ enableResolveObject(true);
+ }
+
+ @Override
+ protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
+ ObjectStreamClass read = super.readClassDescriptor();
+ if (read.getName().contentEquals("com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore"))
+ return ObjectStreamClass.lookup(BitSetChunkStore.class);
+ return read;
+ }
+
+ public ChunkStore readLegacyChunkStore(){
+ try {
+ return (ChunkStore) readObject();
+ } catch (IOException | ClassNotFoundException e) {
+ return null;
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java
old mode 100755
new mode 100644
similarity index 61%
rename from src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManager.java
rename to src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java
index d64824a0e..5cb50ac7f
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManager.java
+++ b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java
@@ -1,59 +1,12 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
+package com.gmail.nossr50.util.blockmeta;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
-import org.bukkit.entity.Entity;
-
-import java.io.IOException;
public interface ChunkManager {
void closeAll();
- ChunkStore readChunkStore(World world, int x, int z) throws IOException;
-
- void writeChunkStore(World world, int x, int z, ChunkStore data);
-
- void closeChunkStore(World world, int x, int z);
-
- /**
- * Loads a specific chunklet
- *
- * @param cx Chunklet X coordinate that needs to be loaded
- * @param cy Chunklet Y coordinate that needs to be loaded
- * @param cz Chunklet Z coordinate that needs to be loaded
- * @param world World that the chunklet needs to be loaded in
- */
- void loadChunklet(int cx, int cy, int cz, World world);
-
- /**
- * Unload a specific chunklet
- *
- * @param cx Chunklet X coordinate that needs to be unloaded
- * @param cy Chunklet Y coordinate that needs to be unloaded
- * @param cz Chunklet Z coordinate that needs to be unloaded
- * @param world World that the chunklet needs to be unloaded from
- */
- void unloadChunklet(int cx, int cy, int cz, World world);
-
- /**
- * Load a given Chunk's Chunklet data
- *
- * @param cx Chunk X coordinate that is to be loaded
- * @param cz Chunk Z coordinate that is to be loaded
- * @param world World that the Chunk is in
- */
- void loadChunk(int cx, int cz, World world, Entity[] entities);
-
- /**
- * Unload a given Chunk's Chunklet data
- *
- * @param cx Chunk X coordinate that is to be unloaded
- * @param cz Chunk Z coordinate that is to be unloaded
- * @param world World that the Chunk is in
- */
- void unloadChunk(int cx, int cz, World world);
-
/**
* Saves a given Chunk's Chunklet data
*
@@ -63,17 +16,6 @@ public interface ChunkManager {
*/
void saveChunk(int cx, int cz, World world);
- boolean isChunkLoaded(int cx, int cz, World world);
-
- /**
- * Informs the ChunkletManager a chunk is loaded
- *
- * @param cx Chunk X coordinate that is loaded
- * @param cz Chunk Z coordinate that is loaded
- * @param world World that the chunk was loaded in
- */
- void chunkLoaded(int cx, int cz, World world);
-
/**
* Informs the ChunkletManager a chunk is unloaded
*
@@ -97,23 +39,11 @@ public interface ChunkManager {
*/
void unloadWorld(World world);
- /**
- * Load all ChunkletStores from all loaded chunks from this world into memory
- *
- * @param world World to load
- */
- void loadWorld(World world);
-
/**
* Save all ChunkletStores
*/
void saveAll();
- /**
- * Unload all ChunkletStores after saving them
- */
- void unloadAll();
-
/**
* Check to see if a given location is set to true
*
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManagerFactory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManagerFactory.java
old mode 100755
new mode 100644
similarity index 86%
rename from src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManagerFactory.java
rename to src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManagerFactory.java
index 2b4d90349..a290c5e2a
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManagerFactory.java
+++ b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManagerFactory.java
@@ -1,4 +1,4 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
+package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.config.HiddenConfig;
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java
old mode 100755
new mode 100644
similarity index 79%
rename from src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStore.java
rename to src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java
index 69b2acae1..eca783ccd
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStore.java
+++ b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java
@@ -1,13 +1,13 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
+package com.gmail.nossr50.util.blockmeta;
-import com.gmail.nossr50.util.blockmeta.ChunkletStore;
+import org.bukkit.World;
-import java.io.Serializable;
+import java.util.UUID;
/**
* A ChunkStore should be responsible for a 16x16xWorldHeight area of data
*/
-public interface ChunkStore extends Serializable {
+public interface ChunkStore {
/**
* Checks the chunk's save state
*
@@ -36,6 +36,8 @@ public interface ChunkStore extends Serializable {
*/
int getChunkZ();
+ UUID getWorldId();
+
/**
* Checks the value at the given coordinates
*
@@ -64,15 +66,18 @@ public interface ChunkStore extends Serializable {
*/
void setFalse(int x, int y, int z);
+ /**
+ * Set the value at the given coordinates
+ *
+ * @param x x coordinate in current chunklet
+ * @param y y coordinate in current chunklet
+ * @param z z coordinate in current chunklet
+ * @param value value to set
+ */
+ void set(int x, int y, int z, boolean value);
+
/**
* @return true if all values in the chunklet are false, false if otherwise
*/
boolean isEmpty();
-
- /**
- * Set all values in this ChunkletStore to the values from another provided ChunkletStore
- *
- * @param otherStore Another ChunkletStore that this one should copy all data from
- */
- void copyFrom(ChunkletStore otherStore);
}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManager.java
deleted file mode 100755
index feb54acd3..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManager.java
+++ /dev/null
@@ -1,151 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-import org.bukkit.World;
-import org.bukkit.block.Block;
-
-public interface ChunkletManager {
- /**
- * Loads a specific chunklet
- *
- * @param cx Chunklet X coordinate that needs to be loaded
- * @param cy Chunklet Y coordinate that needs to be loaded
- * @param cz Chunklet Z coordinate that needs to be loaded
- * @param world World that the chunklet needs to be loaded in
- */
- void loadChunklet(int cx, int cy, int cz, World world);
-
- /**
- * Unload a specific chunklet
- *
- * @param cx Chunklet X coordinate that needs to be unloaded
- * @param cy Chunklet Y coordinate that needs to be unloaded
- * @param cz Chunklet Z coordinate that needs to be unloaded
- * @param world World that the chunklet needs to be unloaded from
- */
- void unloadChunklet(int cx, int cy, int cz, World world);
-
- /**
- * Load a given Chunk's Chunklet data
- *
- * @param cx Chunk X coordinate that is to be loaded
- * @param cz Chunk Z coordinate that is to be loaded
- * @param world World that the Chunk is in
- */
- void loadChunk(int cx, int cz, World world);
-
- /**
- * Unload a given Chunk's Chunklet data
- *
- * @param cx Chunk X coordinate that is to be unloaded
- * @param cz Chunk Z coordinate that is to be unloaded
- * @param world World that the Chunk is in
- */
- void unloadChunk(int cx, int cz, World world);
-
- /**
- * Informs the ChunkletManager a chunk is loaded
- *
- * @param cx Chunk X coordinate that is loaded
- * @param cz Chunk Z coordinate that is loaded
- * @param world World that the chunk was loaded in
- */
- void chunkLoaded(int cx, int cz, World world);
-
- /**
- * Informs the ChunkletManager a chunk is unloaded
- *
- * @param cx Chunk X coordinate that is unloaded
- * @param cz Chunk Z coordinate that is unloaded
- * @param world World that the chunk was unloaded in
- */
- void chunkUnloaded(int cx, int cz, World world);
-
- /**
- * Save all ChunkletStores related to the given world
- *
- * @param world World to save
- */
- void saveWorld(World world);
-
- /**
- * Unload all ChunkletStores from memory related to the given world after saving them
- *
- * @param world World to unload
- */
- void unloadWorld(World world);
-
- /**
- * Load all ChunkletStores from all loaded chunks from this world into memory
- *
- * @param world World to load
- */
- void loadWorld(World world);
-
- /**
- * Save all ChunkletStores
- */
- void saveAll();
-
- /**
- * Unload all ChunkletStores after saving them
- */
- void unloadAll();
-
- /**
- * Check to see if a given location is set to true
- *
- * @param x X coordinate to check
- * @param y Y coordinate to check
- * @param z Z coordinate to check
- * @param world World to check in
- * @return true if the given location is set to true, false if otherwise
- */
- boolean isTrue(int x, int y, int z, World world);
-
- /**
- * Check to see if a given block location is set to true
- *
- * @param block Block location to check
- * @return true if the given block location is set to true, false if otherwise
- */
- boolean isTrue(Block block);
-
- /**
- * Set a given location to true, should create stores as necessary if the location does not exist
- *
- * @param x X coordinate to set
- * @param y Y coordinate to set
- * @param z Z coordinate to set
- * @param world World to set in
- */
- void setTrue(int x, int y, int z, World world);
-
- /**
- * Set a given block location to true, should create stores as necessary if the location does not exist
- *
- * @param block Block location to set
- */
- void setTrue(Block block);
-
- /**
- * Set a given location to false, should not create stores if one does not exist for the given location
- *
- * @param x X coordinate to set
- * @param y Y coordinate to set
- * @param z Z coordinate to set
- * @param world World to set in
- */
- void setFalse(int x, int y, int z, World world);
-
- /**
- * Set a given block location to false, should not create stores if one does not exist for the given location
- *
- * @param block Block location to set
- */
- void setFalse(Block block);
-
- /**
- * Delete any ChunkletStores that are empty
- */
- void cleanUp();
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManagerFactory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManagerFactory.java
deleted file mode 100755
index 39f8732d3..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManagerFactory.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-import com.gmail.nossr50.config.HiddenConfig;
-
-public class ChunkletManagerFactory {
- public static ChunkletManager getChunkletManager() {
- HiddenConfig hConfig = HiddenConfig.getInstance();
-
- if (hConfig.getChunkletsEnabled()) {
- return new HashChunkletManager();
- }
-
- return new NullChunkletManager();
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStore.java
deleted file mode 100755
index 9b1537782..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStore.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-import java.io.Serializable;
-
-/**
- * A ChunkletStore should be responsible for a 16x16x64 area of data
- */
-public interface ChunkletStore extends Serializable {
- /**
- * Checks the value at the given coordinates
- *
- * @param x x coordinate in current chunklet
- * @param y y coordinate in current chunklet
- * @param z z coordinate in current chunklet
- * @return true if the value is true at the given coordinates, false if otherwise
- */
- boolean isTrue(int x, int y, int z);
-
- /**
- * Set the value to true at the given coordinates
- *
- * @param x x coordinate in current chunklet
- * @param y y coordinate in current chunklet
- * @param z z coordinate in current chunklet
- */
- void setTrue(int x, int y, int z);
-
- /**
- * Set the value to false at the given coordinates
- *
- * @param x x coordinate in current chunklet
- * @param y y coordinate in current chunklet
- * @param z z coordinate in current chunklet
- */
- void setFalse(int x, int y, int z);
-
- /**
- * @return true if all values in the chunklet are false, false if otherwise
- */
- boolean isEmpty();
-
- /**
- * Set all values in this ChunkletStore to the values from another provided ChunkletStore
- *
- * @param otherStore Another ChunkletStore that this one should copy all data from
- */
- void copyFrom(ChunkletStore otherStore);
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStoreFactory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStoreFactory.java
deleted file mode 100755
index 1fb4a315a..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStoreFactory.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-public class ChunkletStoreFactory {
- protected static ChunkletStore getChunkletStore() {
- // TODO: Add in loading from config what type of store we want.
- return new PrimitiveExChunkletStore();
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java
new file mode 100644
index 000000000..888937872
--- /dev/null
+++ b/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java
@@ -0,0 +1,354 @@
+package com.gmail.nossr50.util.blockmeta;
+
+import com.gmail.nossr50.mcMMO;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockState;
+
+import java.io.*;
+import java.util.*;
+
+public class HashChunkManager implements ChunkManager {
+ private final HashMap regionMap = new HashMap<>(); // Tracks active regions
+ private final HashMap> chunkUsageMap = new HashMap<>(); // Tracks active chunks by region
+ private final HashMap chunkMap = new HashMap<>(); // Tracks active chunks
+
+ @Override
+ public synchronized void closeAll() {
+ // Save all dirty chunkstores
+ for (ChunkStore chunkStore : chunkMap.values())
+ {
+ if (!chunkStore.isDirty())
+ continue;
+ writeChunkStore(Bukkit.getWorld(chunkStore.getWorldId()), chunkStore);
+ }
+ // Clear in memory chunks
+ chunkMap.clear();
+ chunkUsageMap.clear();
+ // Close all region files
+ for (McMMOSimpleRegionFile rf : regionMap.values())
+ rf.close();
+ regionMap.clear();
+ }
+
+ private synchronized ChunkStore readChunkStore(World world, int cx, int cz) throws IOException {
+ McMMOSimpleRegionFile rf = getSimpleRegionFile(world, cx, cz, false);
+ if (rf == null)
+ return null; // If there is no region file, there can't be a chunk
+ try (DataInputStream in = rf.getInputStream(cx, cz)) { // Get input stream for chunk
+ if (in == null)
+ return null; // No chunk
+ return BitSetChunkStore.Serialization.readChunkStore(in); // Read in the chunkstore
+ }
+ }
+
+ private synchronized void writeChunkStore(World world, ChunkStore data) {
+ if (!data.isDirty())
+ return; // Don't save unchanged data
+ try {
+ McMMOSimpleRegionFile rf = getSimpleRegionFile(world, data.getChunkX(), data.getChunkZ(), true);
+ try (DataOutputStream out = rf.getOutputStream(data.getChunkX(), data.getChunkZ())) {
+ BitSetChunkStore.Serialization.writeChunkStore(out, data);
+ }
+ data.setDirty(false);
+ }
+ catch (IOException e) {
+ throw new RuntimeException("Unable to write chunk meta data for " + data.getChunkX() + ", " + data.getChunkZ(), e);
+ }
+ }
+
+ private synchronized McMMOSimpleRegionFile getSimpleRegionFile(World world, int cx, int cz, boolean createIfAbsent) {
+ CoordinateKey regionKey = toRegionKey(world.getUID(), cx, cz);
+
+ return regionMap.computeIfAbsent(regionKey, k -> {
+ File worldRegionsDirectory = new File(world.getWorldFolder(), "mcmmo_regions");
+ if (!createIfAbsent && !worldRegionsDirectory.isDirectory())
+ return null; // Don't create the directory on read-only operations
+ worldRegionsDirectory.mkdirs(); // Ensure directory exists
+ File regionFile = new File(worldRegionsDirectory, "mcmmo_" + regionKey.x + "_" + regionKey.z + "_.mcm");
+ if (!createIfAbsent && !regionFile.exists())
+ return null; // Don't create the file on read-only operations
+ return new McMMOSimpleRegionFile(regionFile, regionKey.x, regionKey.z);
+ });
+ }
+
+ private ChunkStore loadChunk(int cx, int cz, World world) {
+ try {
+ return readChunkStore(world, cx, cz);
+ }
+ catch (Exception ignored) {}
+
+ return null;
+ }
+
+ private void unloadChunk(int cx, int cz, World world) {
+ CoordinateKey chunkKey = toChunkKey(world.getUID(), cx, cz);
+ ChunkStore chunkStore = chunkMap.remove(chunkKey); // Remove from chunk map
+ if (chunkStore == null)
+ return;
+
+ if (chunkStore.isDirty())
+ writeChunkStore(world, chunkStore);
+
+ CoordinateKey regionKey = toRegionKey(world.getUID(), cx, cz);
+ HashSet chunkKeys = chunkUsageMap.get(regionKey);
+ chunkKeys.remove(chunkKey); // remove from region file in-use set
+ if (chunkKeys.isEmpty()) // If it was last chunk in region, close the region file and remove it from memory
+ {
+ chunkUsageMap.remove(regionKey);
+ regionMap.remove(regionKey).close();
+ }
+ }
+
+ @Override
+ public synchronized void saveChunk(int cx, int cz, World world) {
+ if (world == null)
+ return;
+
+ CoordinateKey chunkKey = toChunkKey(world.getUID(), cx, cz);
+
+ ChunkStore out = chunkMap.get(chunkKey);
+
+ if (out == null)
+ return;
+
+ if (!out.isDirty())
+ return;
+
+ writeChunkStore(world, out);
+ }
+
+ @Override
+ public synchronized void chunkUnloaded(int cx, int cz, World world) {
+ if (world == null)
+ return;
+
+ unloadChunk(cx, cz, world);
+ }
+
+ @Override
+ public synchronized void saveWorld(World world) {
+ if (world == null)
+ return;
+
+ UUID wID = world.getUID();
+
+ // Save all teh chunks
+ for (ChunkStore chunkStore : chunkMap.values()) {
+ if (!chunkStore.isDirty())
+ continue;
+ if (!wID.equals(chunkStore.getWorldId()))
+ continue;
+ try {
+ writeChunkStore(world, chunkStore);
+ }
+ catch (Exception ignore) { }
+ }
+ }
+
+ @Override
+ public synchronized void unloadWorld(World world) {
+ if (world == null)
+ return;
+
+ UUID wID = world.getUID();
+
+ // Save and remove all the chunks
+ List chunkKeys = new ArrayList<>(chunkMap.keySet());
+ for (CoordinateKey chunkKey : chunkKeys) {
+ if (!wID.equals(chunkKey.worldID))
+ continue;
+ ChunkStore chunkStore = chunkMap.remove(chunkKey);
+ if (!chunkStore.isDirty())
+ continue;
+ try {
+ writeChunkStore(world, chunkStore);
+ }
+ catch (Exception ignore) { }
+ }
+ // Clear all the region files
+ List regionKeys = new ArrayList<>(regionMap.keySet());
+ for (CoordinateKey regionKey : regionKeys) {
+ if (!wID.equals(regionKey.worldID))
+ continue;
+ regionMap.remove(regionKey).close();
+ chunkUsageMap.remove(regionKey);
+ }
+ }
+
+ @Override
+ public synchronized void saveAll() {
+ for (World world : mcMMO.p.getServer().getWorlds()) {
+ saveWorld(world);
+ }
+ }
+
+ @Override
+ public synchronized boolean isTrue(int x, int y, int z, World world) {
+ if (world == null)
+ return false;
+
+ CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z);
+
+ // Get chunk, load from file if necessary
+ // Get/Load/Create chunkstore
+ ChunkStore check = chunkMap.computeIfAbsent(chunkKey, k -> {
+ // Load from file
+ ChunkStore loaded = loadChunk(chunkKey.x, chunkKey.z, world);
+ if (loaded == null)
+ return null;
+ // Mark chunk in-use for region tracking
+ chunkUsageMap.computeIfAbsent(toRegionKey(chunkKey.worldID, chunkKey.x, chunkKey.z), j -> new HashSet<>()).add(chunkKey);
+ return loaded;
+ });
+
+ // No chunk, return false
+ if (check == null)
+ return false;
+
+ int ix = Math.abs(x) % 16;
+ int iz = Math.abs(z) % 16;
+
+ return check.isTrue(ix, y, iz);
+ }
+
+ @Override
+ public synchronized boolean isTrue(Block block) {
+ if (block == null)
+ return false;
+
+ return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
+ }
+
+ @Override
+ public synchronized boolean isTrue(BlockState blockState) {
+ if (blockState == null)
+ return false;
+
+ return isTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
+ }
+
+ @Override
+ public synchronized void setTrue(int x, int y, int z, World world) {
+ set(x, y, z, world, true);
+ }
+
+ @Override
+ public synchronized void setTrue(Block block) {
+ if (block == null)
+ return;
+
+ setTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
+ }
+
+ @Override
+ public synchronized void setTrue(BlockState blockState) {
+ if (blockState == null)
+ return;
+
+ setTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
+ }
+
+ @Override
+ public synchronized void setFalse(int x, int y, int z, World world) {
+ set(x, y, z, world, false);
+ }
+
+ @Override
+ public synchronized void setFalse(Block block) {
+ if (block == null)
+ return;
+
+ setFalse(block.getX(), block.getY(), block.getZ(), block.getWorld());
+ }
+
+ @Override
+ public synchronized void setFalse(BlockState blockState) {
+ if (blockState == null)
+ return;
+
+ setFalse(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
+ }
+
+ public synchronized void set(int x, int y, int z, World world, boolean value){
+ if (world == null)
+ return;
+
+ CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z);
+
+ // Get/Load/Create chunkstore
+ ChunkStore cStore = chunkMap.computeIfAbsent(chunkKey, k -> {
+ // Load from file
+ ChunkStore loaded = loadChunk(chunkKey.x, chunkKey.z, world);
+ if (loaded != null)
+ {
+ chunkUsageMap.computeIfAbsent(toRegionKey(chunkKey.worldID, chunkKey.x, chunkKey.z), j -> new HashSet<>()).add(chunkKey);
+ return loaded;
+ }
+ // If setting to false, no need to create an empty chunkstore
+ if (!value)
+ return null;
+ // Mark chunk in-use for region tracking
+ chunkUsageMap.computeIfAbsent(toRegionKey(chunkKey.worldID, chunkKey.x, chunkKey.z), j -> new HashSet<>()).add(chunkKey);
+ // Create a new chunkstore
+ return new BitSetChunkStore(world, chunkKey.x, chunkKey.z);
+ });
+
+ // Indicates setting false on empty chunkstore
+ if (cStore == null)
+ return;
+
+ // Get block offset (offset from chunk corner)
+ int ix = Math.abs(x) % 16;
+ int iz = Math.abs(z) % 16;
+
+ // Set chunk store value
+ cStore.set(ix, y, iz, value);
+ }
+
+ private CoordinateKey blockCoordinateToChunkKey(UUID worldUid, int x, int y, int z) {
+ return toChunkKey(worldUid, x >> 4, z >> 4);
+ }
+
+ private CoordinateKey toChunkKey(UUID worldUid, int cx, int cz){
+ return new CoordinateKey(worldUid, cx, cz);
+ }
+
+ private CoordinateKey toRegionKey(UUID worldUid, int cx, int cz) {
+ // Compute region index (32x32 chunk regions)
+ int rx = cx >> 5;
+ int rz = cz >> 5;
+ return new CoordinateKey(worldUid, rx, rz);
+ }
+
+ private static final class CoordinateKey {
+ public final UUID worldID;
+ public final int x;
+ public final int z;
+
+ private CoordinateKey(UUID worldID, int x, int z) {
+ this.worldID = worldID;
+ this.x = x;
+ this.z = z;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CoordinateKey coordinateKey = (CoordinateKey) o;
+ return x == coordinateKey.x &&
+ z == coordinateKey.z &&
+ worldID.equals(coordinateKey.worldID);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(worldID, x, z);
+ }
+ }
+
+ @Override
+ public synchronized void cleanUp() {}
+}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkletManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkletManager.java
deleted file mode 100755
index c2fc23faf..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkletManager.java
+++ /dev/null
@@ -1,410 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-import com.gmail.nossr50.mcMMO;
-import org.bukkit.World;
-import org.bukkit.block.Block;
-
-import java.io.*;
-import java.util.HashMap;
-
-public class HashChunkletManager implements ChunkletManager {
- public HashMap store = new HashMap<>();
-
- @Override
- public void loadChunklet(int cx, int cy, int cz, World world) {
- File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
- File cxDir = new File(dataDir, "" + cx);
- if (!cxDir.exists()) {
- return;
- }
- File czDir = new File(cxDir, "" + cz);
- if (!czDir.exists()) {
- return;
- }
- File yFile = new File(czDir, "" + cy);
- if (!yFile.exists()) {
- return;
- }
-
- ChunkletStore in = deserializeChunkletStore(yFile);
- if (in != null) {
- store.put(world.getName() + "," + cx + "," + cz + "," + cy, in);
- }
- }
-
- @Override
- public void unloadChunklet(int cx, int cy, int cz, World world) {
- File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
- if (store.containsKey(world.getName() + "," + cx + "," + cz + "," + cy)) {
- File cxDir = new File(dataDir, "" + cx);
- if (!cxDir.exists()) {
- cxDir.mkdir();
- }
- File czDir = new File(cxDir, "" + cz);
- if (!czDir.exists()) {
- czDir.mkdir();
- }
- File yFile = new File(czDir, "" + cy);
-
- ChunkletStore out = store.get(world.getName() + "," + cx + "," + cz + "," + cy);
- serializeChunkletStore(out, yFile);
- store.remove(world.getName() + "," + cx + "," + cz + "," + cy);
- }
- }
-
- @Override
- public void loadChunk(int cx, int cz, World world) {
- File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
- File cxDir = new File(dataDir, "" + cx);
- if (!cxDir.exists()) {
- return;
- }
- File czDir = new File(cxDir, "" + cz);
- if (!czDir.exists()) {
- return;
- }
-
- for (int y = 0; y < 4; y++) {
- File yFile = new File(czDir, "" + y);
- if (!yFile.exists()) {
- continue;
- }
-
- ChunkletStore in = deserializeChunkletStore(yFile);
- if (in != null) {
- store.put(world.getName() + "," + cx + "," + cz + "," + y, in);
- }
- }
- }
-
- @Override
- public void unloadChunk(int cx, int cz, World world) {
- File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
-
- for (int y = 0; y < 4; y++) {
- if (store.containsKey(world.getName() + "," + cx + "," + cz + "," + y)) {
- File cxDir = new File(dataDir, "" + cx);
- if (!cxDir.exists()) {
- cxDir.mkdir();
- }
- File czDir = new File(cxDir, "" + cz);
- if (!czDir.exists()) {
- czDir.mkdir();
- }
- File yFile = new File(czDir, "" + y);
-
- ChunkletStore out = store.get(world.getName() + "," + cx + "," + cz + "," + y);
- serializeChunkletStore(out, yFile);
- store.remove(world.getName() + "," + cx + "," + cz + "," + y);
- }
- }
- }
-
- @Override
- public void chunkLoaded(int cx, int cz, World world) {
- //loadChunk(cx, cz, world);
- }
-
- @Override
- public void chunkUnloaded(int cx, int cz, World world) {
- unloadChunk(cx, cx, world);
- }
-
- @Override
- public void saveWorld(World world) {
- String worldName = world.getName();
- File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
- if (!dataDir.exists()) {
- dataDir.mkdirs();
- }
-
- for (String key : store.keySet()) {
- String[] info = key.split(",");
- if (worldName.equals(info[0])) {
- File cxDir = new File(dataDir, "" + info[1]);
- if (!cxDir.exists()) {
- cxDir.mkdir();
- }
- File czDir = new File(cxDir, "" + info[2]);
- if (!czDir.exists()) {
- czDir.mkdir();
- }
-
- File yFile = new File(czDir, "" + info[3]);
- serializeChunkletStore(store.get(key), yFile);
- }
- }
- }
-
- @Override
- public void unloadWorld(World world) {
- saveWorld(world);
-
- String worldName = world.getName();
-
- for (String key : store.keySet()) {
- String tempWorldName = key.split(",")[0];
- if (tempWorldName.equals(worldName)) {
- store.remove(key);
- return;
- }
- }
- }
-
- @Override
- public void loadWorld(World world) {
- //for (Chunk chunk : world.getLoadedChunks()) {
- // this.chunkLoaded(chunk.getX(), chunk.getZ(), world);
- //}
- }
-
- @Override
- public void saveAll() {
- for (World world : mcMMO.p.getServer().getWorlds()) {
- saveWorld(world);
- }
- }
-
- @Override
- public void unloadAll() {
- saveAll();
- for (World world : mcMMO.p.getServer().getWorlds()) {
- unloadWorld(world);
- }
- }
-
- @Override
- public boolean isTrue(int x, int y, int z, World world) {
- int cx = x >> 4;
- int cz = z >> 4;
- int cy = y >> 6;
-
- String key = world.getName() + "," + cx + "," + cz + "," + cy;
-
- if (!store.containsKey(key)) {
- loadChunklet(cx, cy, cz, world);
- }
-
- if (!store.containsKey(key)) {
- return false;
- }
-
- ChunkletStore check = store.get(world.getName() + "," + cx + "," + cz + "," + cy);
- int ix = Math.abs(x) % 16;
- int iz = Math.abs(z) % 16;
- int iy = Math.abs(y) % 64;
-
- return check.isTrue(ix, iy, iz);
- }
-
- @Override
- public boolean isTrue(Block block) {
- return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
- }
-
- @Override
- public void setTrue(int x, int y, int z, World world) {
- int cx = x >> 4;
- int cz = z >> 4;
- int cy = y >> 6;
-
- int ix = Math.abs(x) % 16;
- int iz = Math.abs(z) % 16;
- int iy = Math.abs(y) % 64;
-
- String key = world.getName() + "," + cx + "," + cz + "," + cy;
-
- if (!store.containsKey(key)) {
- loadChunklet(cx, cy, cz, world);
- }
-
- ChunkletStore cStore = store.get(key);
-
- if (cStore == null) {
- cStore = ChunkletStoreFactory.getChunkletStore();
-
- store.put(world.getName() + "," + cx + "," + cz + "," + cy, cStore);
- }
-
- cStore.setTrue(ix, iy, iz);
- }
-
- @Override
- public void setTrue(Block block) {
- setTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
- }
-
- @Override
- public void setFalse(int x, int y, int z, World world) {
- int cx = x >> 4;
- int cz = z >> 4;
- int cy = y >> 6;
-
- int ix = Math.abs(x) % 16;
- int iz = Math.abs(z) % 16;
- int iy = Math.abs(y) % 64;
-
- String key = world.getName() + "," + cx + "," + cz + "," + cy;
-
- if (!store.containsKey(key)) {
- loadChunklet(cx, cy, cz, world);
- }
-
- ChunkletStore cStore = store.get(key);
-
- if (cStore == null) {
- return; // No need to make a store for something we will be setting to false
- }
-
- cStore.setFalse(ix, iy, iz);
- }
-
- @Override
- public void setFalse(Block block) {
- setFalse(block.getX(), block.getY(), block.getZ(), block.getWorld());
- }
-
- @Override
- public void cleanUp() {
- for (String key : store.keySet()) {
- if (store.get(key).isEmpty()) {
- String[] info = key.split(",");
- File dataDir = new File(mcMMO.p.getServer().getWorld(info[0]).getWorldFolder(), "mcmmo_data");
-
- File cxDir = new File(dataDir, "" + info[1]);
- if (!cxDir.exists()) {
- continue;
- }
- File czDir = new File(cxDir, "" + info[2]);
- if (!czDir.exists()) {
- continue;
- }
-
- File yFile = new File(czDir, "" + info[3]);
- yFile.delete();
-
- // Delete empty directories
- if (czDir.list().length == 0) {
- czDir.delete();
- }
- if (cxDir.list().length == 0) {
- cxDir.delete();
- }
- }
- }
- }
-
- /**
- * @param cStore ChunkletStore to save
- * @param location Where on the disk to put it
- */
- private void serializeChunkletStore(ChunkletStore cStore, File location) {
- FileOutputStream fileOut = null;
- ObjectOutputStream objOut = null;
-
- try {
- if (!location.exists()) {
- location.createNewFile();
- }
- fileOut = new FileOutputStream(location);
- objOut = new ObjectOutputStream(fileOut);
- objOut.writeObject(cStore);
- }
- catch (IOException ex) {
- ex.printStackTrace();
- }
- finally {
- if (objOut != null) {
- try {
- objOut.flush();
- objOut.close();
- }
- catch (IOException ex) {
- ex.printStackTrace();
- }
- }
-
- if (fileOut != null) {
- try {
- fileOut.close();
- }
- catch (IOException ex) {
- ex.printStackTrace();
- }
- }
- }
- }
-
- /**
- * @param location Where on the disk to read from
- * @return ChunkletStore from the specified location
- */
- private ChunkletStore deserializeChunkletStore(File location) {
- ChunkletStore storeIn = null;
- FileInputStream fileIn = null;
- ObjectInputStream objIn = null;
-
- try {
- fileIn = new FileInputStream(location);
- objIn = new ObjectInputStream(new BufferedInputStream(fileIn));
- storeIn = (ChunkletStore) objIn.readObject();
- }
- catch (IOException ex) {
- if (ex instanceof EOFException) {
- // EOF should only happen on Chunklets that somehow have been corrupted.
- //mcMMO.p.getLogger().severe("Chunklet data at " + location.toString() + " could not be read due to an EOFException, data in this area will be lost.");
- return ChunkletStoreFactory.getChunkletStore();
- }
- else if (ex instanceof StreamCorruptedException) {
- // StreamCorrupted happens when the Chunklet is no good.
- //mcMMO.p.getLogger().severe("Chunklet data at " + location.toString() + " is corrupted, data in this area will be lost.");
- return ChunkletStoreFactory.getChunkletStore();
- }
- else if (ex instanceof UTFDataFormatException) {
- // UTF happens when the Chunklet cannot be read or is corrupted
- //mcMMO.p.getLogger().severe("Chunklet data at " + location.toString() + " could not be read due to an UTFDataFormatException, data in this area will be lost.");
- return ChunkletStoreFactory.getChunkletStore();
- }
-
- ex.printStackTrace();
- }
- catch (ClassNotFoundException ex) {
- ex.printStackTrace();
- }
- finally {
- if (objIn != null) {
- try {
- objIn.close();
- }
- catch (IOException ex) {
- ex.printStackTrace();
- }
- }
-
- if (fileIn != null) {
- try {
- fileIn.close();
- }
- catch (IOException ex) {
- ex.printStackTrace();
- }
- }
- }
-
- // TODO: Make this less messy, as it is, it's kinda... depressing to do it like this.
- // Might also make a mess when we move to stacks, but at that point I think I will write a new Manager...
- // IMPORTANT! If ChunkletStoreFactory is going to be returning something other than PrimitiveEx we need to remove this, as it will be breaking time for old maps
-
- /*
- if (!(storeIn instanceof PrimitiveExChunkletStore)) {
- ChunkletStore tempStore = ChunkletStoreFactory.getChunkletStore();
- if (storeIn != null) {
- tempStore.copyFrom(storeIn);
- }
- storeIn = tempStore;
- }
- */
-
- return storeIn;
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java b/src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java
new file mode 100644
index 000000000..bef730ff4
--- /dev/null
+++ b/src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java
@@ -0,0 +1,257 @@
+/*
+ * This file is part of SpoutPlugin.
+ *
+ * Copyright (c) 2011-2012, SpoutDev
+ * SpoutPlugin is licensed under the GNU Lesser General Public License.
+ *
+ * SpoutPlugin is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * SpoutPlugin 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+package com.gmail.nossr50.util.blockmeta;
+
+import java.io.*;
+import java.util.BitSet;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * File format:
+ * bytes 0-4096 contain 1024 integer values representing the segment index of each chunk
+ * bytes 4096-8192 contain 1024 integer values representing the byte length of each chunk
+ * bytes 8192-8196 is the integer value of the segment exponent
+ * bytes 8196-12288 are reserved for future use
+ * bytes 12288+ contain the data segments, by default 1024 byte segments.
+ * Chunk data is compressed and stored in 1 or more segments as needed.
+ */
+public class McMMOSimpleRegionFile {
+ private static final int DEFAULT_SEGMENT_EXPONENT = 10; // TODO, analyze real world usage and determine if a smaller segment(512) is worth it or not. (need to know average chunkstore bytesize)
+ private static final int DEFAULT_SEGMENT_SIZE = (int)Math.pow(2, DEFAULT_SEGMENT_EXPONENT); // 1024
+ private static final int RESERVED_HEADER_BYTES = 12288; // This needs to be divisible by segment size
+ private static final int NUM_CHUNKS = 1024; // 32x32
+ private static final int SEEK_CHUNK_SEGMENT_INDICES = 0;
+ private static final int SEEK_CHUNK_BYTE_LENGTHS = 4096;
+ private static final int SEEK_FILE_INFO = 8192;
+ // Chunk info
+ private final int[] chunkSegmentIndex = new int[NUM_CHUNKS];
+ private final int[] chunkNumBytes = new int[NUM_CHUNKS];
+ private final int[] chunkNumSegments = new int[NUM_CHUNKS];
+
+ // Segments
+ private final BitSet segments = new BitSet(); // Used to denote which segments are in use or not
+
+ // Segment size/mask
+ private final int segmentExponent;
+ private final int segmentMask;
+
+ // File location
+ private final File parent;
+ // File access
+ private final RandomAccessFile file;
+
+ // Region index
+ private final int rx;
+ private final int rz;
+
+ public McMMOSimpleRegionFile(File f, int rx, int rz) {
+ this.rx = rx;
+ this.rz = rz;
+ this.parent = f;
+
+ try {
+ this.file = new RandomAccessFile(parent, "rw");
+
+ // New file, write out header bytes
+ if (file.length() < RESERVED_HEADER_BYTES) {
+ file.write(new byte[RESERVED_HEADER_BYTES]);
+ file.seek(SEEK_FILE_INFO);
+ file.writeInt(DEFAULT_SEGMENT_EXPONENT);
+ }
+
+ file.seek(SEEK_FILE_INFO);
+ this.segmentExponent = file.readInt();
+ this.segmentMask = (1 << segmentExponent) - 1;
+
+ // Mark reserved segments reserved
+ int reservedSegments = this.bytesToSegments(RESERVED_HEADER_BYTES);
+ segments.set(0, reservedSegments, true);
+
+ // Read chunk header data
+ file.seek(SEEK_CHUNK_SEGMENT_INDICES);
+ for (int i = 0; i < NUM_CHUNKS; i++)
+ chunkSegmentIndex[i] = file.readInt();
+
+ file.seek(SEEK_CHUNK_BYTE_LENGTHS);
+ for (int i = 0; i < NUM_CHUNKS; i++) {
+ chunkNumBytes[i] = file.readInt();
+ chunkNumSegments[i] = bytesToSegments(chunkNumBytes[i]);
+ markChunkSegments(i, true);
+ }
+
+ fixFileLength();
+ }
+ catch (IOException fnfe) {
+ throw new RuntimeException(fnfe);
+ }
+ }
+
+ public synchronized DataOutputStream getOutputStream(int x, int z) {
+ int index = getChunkIndex(x, z); // Get chunk index
+ return new DataOutputStream(new DeflaterOutputStream(new McMMOSimpleChunkBuffer(this, index)));
+ }
+
+ private static class McMMOSimpleChunkBuffer extends ByteArrayOutputStream {
+ final McMMOSimpleRegionFile rf;
+ final int index;
+
+ McMMOSimpleChunkBuffer(McMMOSimpleRegionFile rf, int index) {
+ super(DEFAULT_SEGMENT_SIZE);
+ this.rf = rf;
+ this.index = index;
+ }
+
+ @Override
+ public void close() throws IOException {
+ rf.write(index, buf, count);
+ }
+ }
+
+ private synchronized void write(int index, byte[] buffer, int size) throws IOException {
+ int oldSegmentIndex = chunkSegmentIndex[index]; // Get current segment index
+ markChunkSegments(index, false); // Clear our old segments
+ int newSegmentIndex = findContiguousSegments(oldSegmentIndex, size); // Find contiguous segments to save to
+ file.seek(newSegmentIndex << segmentExponent); // Seek to file location
+ file.write(buffer, 0, size); // Write data
+ // update in memory info
+ chunkSegmentIndex[index] = newSegmentIndex;
+ chunkNumBytes[index] = size;
+ chunkNumSegments[index] = bytesToSegments(size);
+ // Mark segments in use
+ markChunkSegments(index, true);
+ // Update header info
+ file.seek(SEEK_CHUNK_SEGMENT_INDICES + (4 * index));
+ file.writeInt(chunkSegmentIndex[index]);
+ file.seek(SEEK_CHUNK_BYTE_LENGTHS + (4 * index));
+ file.writeInt(chunkNumBytes[index]);
+ }
+
+ public synchronized DataInputStream getInputStream(int x, int z) throws IOException {
+ int index = getChunkIndex(x, z); // Get chunk index
+ int byteLength = chunkNumBytes[index]; // Get byte length of data
+
+ // No bytes
+ if (byteLength == 0)
+ return null;
+
+ byte[] data = new byte[byteLength];
+
+ file.seek(chunkSegmentIndex[index] << segmentExponent); // Seek to file location
+ file.readFully(data); // Read in the data
+ return new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data)));
+ }
+
+ public synchronized void close() {
+ try {
+ file.close();
+ segments.clear();
+ }
+ catch (IOException ioe) {
+ throw new RuntimeException("Unable to close file", ioe);
+ }
+ }
+
+ private synchronized void markChunkSegments(int index, boolean inUse) {
+ // No bytes used
+ if (chunkNumBytes[index] == 0)
+ return;
+
+ int start = chunkSegmentIndex[index];
+ int end = start + chunkNumSegments[index];
+
+ // If we are writing, assert we don't write over any in-use segments
+ if (inUse)
+ {
+ int nextSetBit = segments.nextSetBit(start);
+ if (nextSetBit != -1 && nextSetBit < end)
+ throw new IllegalStateException("Attempting to overwrite an in-use segment");
+ }
+
+ segments.set(start, end, inUse);
+ }
+
+ private synchronized void fixFileLength() throws IOException {
+ int fileLength = (int)file.length();
+ int extend = -fileLength & segmentMask; // how many bytes do we need to be divisible by segment size
+
+ // Go to end of file
+ file.seek(fileLength);
+ // Append bytes
+ file.write(new byte[extend], 0, extend);
+ }
+
+ private synchronized int findContiguousSegments(int hint, int size) {
+ if (size == 0)
+ return 0; // Zero byte data will not claim any chunks anyways
+
+ int segments = bytesToSegments(size); // Number of segments we need
+
+ // Check the hinted location (previous location of chunk) most of the time we can fit where we were.
+ boolean oldFree = true;
+ for (int i = hint; i < this.segments.size() && i < hint + segments; i++) {
+ if (this.segments.get(i)) {
+ oldFree = false;
+ break;
+ }
+ }
+
+ // We fit!
+ if (oldFree)
+ return hint;
+
+ // Find somewhere to put us
+ int start = 0;
+ int current = 0;
+
+ while (current < this.segments.size()) {
+ boolean segmentInUse = this.segments.get(current); // check if segment is in use
+ current++; // Move up a segment
+
+ // Move up start if the segment was in use
+ if (segmentInUse)
+ start = current;
+
+ // If we have enough segments now, return
+ if (current - start >= segments)
+ return start;
+ }
+
+ // Return the end of the segments (will expand to fit them)
+ return start;
+ }
+
+ private synchronized int bytesToSegments(int bytes) {
+ if (bytes <= 0)
+ return 1;
+
+ return ((bytes - 1) >> segmentExponent) + 1; // ((bytes - 1) / segmentSize) + 1
+ }
+
+ private synchronized int getChunkIndex(int x, int z) {
+ if (rx != (x >> 5) || rz != (z >> 5))
+ throw new IndexOutOfBoundsException();
+
+ x = x & 0x1F; // 5 bits (mod 32)
+ z = z & 0x1F; // 5 bits (mod 32)
+
+ return (x << 5) + z; // x in the upper 5 bits, z in the lower 5 bits
+ }
+}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/NullChunkManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java
old mode 100755
new mode 100644
similarity index 54%
rename from src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/NullChunkManager.java
rename to src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java
index 3081b0938..b777fa349
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/NullChunkManager.java
+++ b/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java
@@ -1,51 +1,17 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
+package com.gmail.nossr50.util.blockmeta;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
-import org.bukkit.entity.Entity;
-
-import java.io.IOException;
public class NullChunkManager implements ChunkManager {
@Override
public void closeAll() {}
- @Override
- public ChunkStore readChunkStore(World world, int x, int z) throws IOException {
- return null;
- }
-
- @Override
- public void writeChunkStore(World world, int x, int z, ChunkStore data) {}
-
- @Override
- public void closeChunkStore(World world, int x, int z) {}
-
- @Override
- public void loadChunklet(int cx, int cy, int cz, World world) {}
-
- @Override
- public void unloadChunklet(int cx, int cy, int cz, World world) {}
-
- @Override
- public void loadChunk(int cx, int cz, World world, Entity[] entities) {}
-
- @Override
- public void unloadChunk(int cx, int cz, World world) {}
-
@Override
public void saveChunk(int cx, int cz, World world) {}
- @Override
- public boolean isChunkLoaded(int cx, int cz, World world) {
- return true;
- }
-
- @Override
- public void chunkLoaded(int cx, int cz, World world) {}
-
@Override
public void chunkUnloaded(int cx, int cz, World world) {}
@@ -55,15 +21,9 @@ public class NullChunkManager implements ChunkManager {
@Override
public void unloadWorld(World world) {}
- @Override
- public void loadWorld(World world) {}
-
@Override
public void saveAll() {}
- @Override
- public void unloadAll() {}
-
@Override
public boolean isTrue(int x, int y, int z, World world) {
return false;
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkletManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkletManager.java
deleted file mode 100755
index 304ef8780..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkletManager.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-import org.bukkit.World;
-import org.bukkit.block.Block;
-
-/**
- * A ChunkletManager implementation that does nothing and returns false for all checks.
- *
- * Useful for turning off Chunklets without actually doing much work
- */
-public class NullChunkletManager implements ChunkletManager {
- @Override
- public void loadChunklet(int cx, int cy, int cz, World world) {
- }
-
- @Override
- public void unloadChunklet(int cx, int cy, int cz, World world) {
- }
-
- @Override
- public void loadChunk(int cx, int cz, World world) {
- }
-
- @Override
- public void unloadChunk(int cx, int cz, World world) {
- }
-
- @Override
- public void chunkLoaded(int cx, int cz, World world) {
- }
-
- @Override
- public void chunkUnloaded(int cx, int cz, World world) {
- }
-
- @Override
- public void saveWorld(World world) {
- }
-
- @Override
- public void unloadWorld(World world) {
- }
-
- @Override
- public void loadWorld(World world) {
- }
-
- @Override
- public void saveAll() {
- }
-
- @Override
- public void unloadAll() {
- }
-
- @Override
- public boolean isTrue(int x, int y, int z, World world) {
- return false;
- }
-
- @Override
- public boolean isTrue(Block block) {
- return false;
- }
-
- @Override
- public void setTrue(int x, int y, int z, World world) {
- }
-
- @Override
- public void setTrue(Block block) {
- }
-
- @Override
- public void setFalse(int x, int y, int z, World world) {
- }
-
- @Override
- public void setFalse(Block block) {
- }
-
- @Override
- public void cleanUp() {
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveChunkletStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveChunkletStore.java
deleted file mode 100755
index 8dfe3cb8d..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveChunkletStore.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-public class PrimitiveChunkletStore implements ChunkletStore {
- private static final long serialVersionUID = -3453078050608607478L;
-
- /** X, Z, Y */
- public boolean[][][] store = new boolean[16][16][64];
-
- @Override
- public boolean isTrue(int x, int y, int z) {
- return store[x][z][y];
- }
-
- @Override
- public void setTrue(int x, int y, int z) {
- store[x][z][y] = true;
- }
-
- @Override
- public void setFalse(int x, int y, int z) {
- store[x][z][y] = false;
- }
-
- @Override
- public boolean isEmpty() {
- for (int x = 0; x < 16; x++) {
- for (int z = 0; z < 16; z++) {
- for (int y = 0; y < 64; y++) {
- if (store[x][z][y]) {
- return false;
- }
- }
- }
- }
- return true;
- }
-
- @Override
- public void copyFrom(ChunkletStore otherStore) {
- for (int x = 0; x < 16; x++) {
- for (int z = 0; z < 16; z++) {
- for (int y = 0; y < 64; y++) {
- store[x][z][y] = otherStore.isTrue(x, y, z);
- }
- }
- }
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveExChunkletStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveExChunkletStore.java
deleted file mode 100755
index 187ad0dff..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveExChunkletStore.java
+++ /dev/null
@@ -1,180 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-import java.io.Externalizable;
-import java.io.IOException;
-import java.io.ObjectInput;
-import java.io.ObjectOutput;
-
-public class PrimitiveExChunkletStore implements ChunkletStore, Externalizable {
- private static final long serialVersionUID = 8603603827094383873L;
-
- /** X, Z, Y */
- public boolean[][][] store = new boolean[16][16][64];
-
- @Override
- public boolean isTrue(int x, int y, int z) {
- return store[x][z][y];
- }
-
- @Override
- public void setTrue(int x, int y, int z) {
- store[x][z][y] = true;
- }
-
- @Override
- public void setFalse(int x, int y, int z) {
- store[x][z][y] = false;
- }
-
- @Override
- public boolean isEmpty() {
- for (int x = 0; x < 16; x++) {
- for (int z = 0; z < 16; z++) {
- for (int y = 0; y < 64; y++) {
- if (store[x][z][y]) {
- return false;
- }
- }
- }
- }
- return true;
- }
-
- @Override
- public void copyFrom(ChunkletStore otherStore) {
- for (int x = 0; x < 16; x++) {
- for (int z = 0; z < 16; z++) {
- for (int y = 0; y < 64; y++) {
- store[x][z][y] = otherStore.isTrue(x, y, z);
- }
- }
- }
- }
-
- @Override
- public void writeExternal(ObjectOutput out) throws IOException {
- byte[] buffer = new byte[2304]; // 2304 is 16*16*9
- int bufferIndex = 0;
-
- for (int x = 0; x < 16; x++) {
- for (int z = 0; z < 16; z++) {
- for (int y = 0; y < 64; y++) {
- if (store[x][z][y]) {
- byte[] temp = constructColumn(x, z);
-
- for (int i = 0; i < 9; i++) {
- buffer[bufferIndex] = temp[i];
- bufferIndex++;
- }
-
- break;
- }
- }
- }
- }
-
- out.write(buffer, 0, bufferIndex);
- out.flush();
- }
-
- // For this we assume that store has been initialized to be all false by now
- @Override
- public void readExternal(ObjectInput in) throws IOException {
- byte[] temp = new byte[9];
-
- // Could probably reorganize this loop to print nasty things if it does not equal 9 or -1
- while (in.read(temp, 0, 9) == 9) {
- int x = addressByteX(temp[0]);
- int z = addressByteZ(temp[0]);
- boolean[] yColumn = new boolean[64];
-
- for (int i = 0; i < 8; i++) {
- for (int j = 0; j < 8; j++) {
- yColumn[j + (i * 8)] = (temp[i + 1] & (1 << j)) != 0;
- }
- }
-
- store[x][z] = yColumn;
- }
- }
-
- /*
- * The column: An array of 9 bytes which represent all y values for a given (x,z) Chunklet-coordinate
- *
- * The first byte is an address byte, this provides the x and z values.
- * The next 8 bytes are all y values from 0 to 63, with each byte containing 8 bits of true/false data
- *
- * Each of these 8 bytes address to a y value from right to left
- *
- * Examples:
- * 00000001 represents that the lowest y value in this byte is true, all others are off
- * 10000000 represents that the highest y value in this byte is true, all others are off
- * 10000001 represents that the lowest and highest y values in this byte are true, all others are off
- *
- * Full columns:
- * See comment on Address byte for information on how to use that byte
- *
- * Example:
- * ADDRESS_BYTE 10000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000
- * - x, z from ADDRESS_BYTE
- * - The next byte contains data from 0 to 7
- * - 1 is set in the highest bit position, this is 7 in y coordinate
- * - The next byte contains data from 8 to 15
- * - 1 is set in the lowest bit position, this is 8 in the y coordinate
- * Therefore, for this column: There are true values at (x, 7, z) and (x, 8, z)
- */
- private byte[] constructColumn(int x, int z) {
- byte[] column = new byte[9];
- int index = 1;
-
- column[0] = makeAddressByte(x, z);
-
- for (int i = 0; i < 8; i++) {
- byte yCompressed = 0x0;
- int subColumnIndex = 8 * i;
- int subColumnEnd = subColumnIndex + 8;
-
- for (int y = subColumnIndex; y < subColumnEnd; y++) {
- if (store[x][z][y]) {
- yCompressed |= 1 << (y % 8);
- }
- }
-
- column[index] = yCompressed;
- index++;
- }
-
- return column;
- }
-
- /*
- * The address byte: A single byte which contains x and z values which correspond to the x and z Chunklet-coordinates
- *
- * In Chunklet-coordinates, the only valid values are 0-15, so we can fit both into a single byte.
- *
- * The top 4 bits of the address byte are for the x value
- * The bottom 4 bits of the address byte are for the z value
- *
- * Examples:
- * An address byte with a value 00000001 would be split like so:
- * - x = 0000 = 0
- * - z = 0001 = 1
- * => Chunklet coordinates (0, 1)
- *
- * 01011111
- * - x = 0101 = 5
- * - z = 1111 = 15
- * => Chunklet coordinates (5, 15)
- */
- protected static byte makeAddressByte(int x, int z) {
- return (byte) ((x << 4) + z);
- }
-
- protected static int addressByteX(byte address) {
- return (address & 0xF0) >>> 4;
- }
-
- protected static int addressByteZ(byte address) {
- return address & 0x0F;
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStoreFactory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStoreFactory.java
deleted file mode 100755
index 53528ab66..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStoreFactory.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
-
-import org.bukkit.World;
-
-public class ChunkStoreFactory {
- protected static ChunkStore getChunkStore(World world, int x, int z) {
- // TODO: Add in loading from config what type of store we want.
- return new PrimitiveChunkStore(world, x, z);
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/HashChunkManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/HashChunkManager.java
deleted file mode 100755
index 05153816f..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/HashChunkManager.java
+++ /dev/null
@@ -1,447 +0,0 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
-
-import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.blockmeta.conversion.BlockStoreConversionZDirectory;
-import org.bukkit.World;
-import org.bukkit.block.Block;
-import org.bukkit.block.BlockState;
-import org.bukkit.entity.Entity;
-
-import java.io.*;
-import java.util.*;
-
-public class HashChunkManager implements ChunkManager {
- private final HashMap> regionFiles = new HashMap<>();
- public HashMap store = new HashMap<>();
- public ArrayList converters = new ArrayList<>();
- private final HashMap oldData = new HashMap<>();
-
- @Override
- public synchronized void closeAll() {
- for (UUID uid : regionFiles.keySet()) {
- HashMap worldRegions = regionFiles.get(uid);
- for (Iterator worldRegionIterator = worldRegions.values().iterator(); worldRegionIterator.hasNext(); ) {
- McMMOSimpleRegionFile rf = worldRegionIterator.next();
- if (rf != null) {
- rf.close();
- worldRegionIterator.remove();
- }
- }
- }
- regionFiles.clear();
- }
-
- @Override
- public synchronized ChunkStore readChunkStore(World world, int x, int z) throws IOException {
- McMMOSimpleRegionFile rf = getSimpleRegionFile(world, x, z);
- InputStream in = rf.getInputStream(x, z);
- if (in == null) {
- return null;
- }
- try (ObjectInputStream objectStream = new ObjectInputStream(in)) {
- Object o = objectStream.readObject();
- if (o instanceof ChunkStore) {
- return (ChunkStore) o;
- }
-
- throw new RuntimeException("Wrong class type read for chunk meta data for " + x + ", " + z);
- } catch (IOException | ClassNotFoundException e) {
- e.printStackTrace();
- // Assume the format changed
- return null;
- //throw new RuntimeException("Unable to process chunk meta data for " + x + ", " + z, e);
- }
- }
-
- @Override
- public synchronized void writeChunkStore(World world, int x, int z, ChunkStore data) {
- if (!data.isDirty()) {
- return;
- }
- try {
- McMMOSimpleRegionFile rf = getSimpleRegionFile(world, x, z);
- ObjectOutputStream objectStream = new ObjectOutputStream(rf.getOutputStream(x, z));
- objectStream.writeObject(data);
- objectStream.flush();
- objectStream.close();
- data.setDirty(false);
- }
- catch (IOException e) {
- throw new RuntimeException("Unable to write chunk meta data for " + x + ", " + z, e);
- }
- }
-
- @Override
- public synchronized void closeChunkStore(World world, int x, int z) {
- McMMOSimpleRegionFile rf = getSimpleRegionFile(world, x, z);
- if (rf != null) {
- rf.close();
- }
- }
-
- private synchronized McMMOSimpleRegionFile getSimpleRegionFile(World world, int x, int z) {
- File directory = new File(world.getWorldFolder(), "mcmmo_regions");
-
- directory.mkdirs();
-
- UUID key = world.getUID();
-
- HashMap worldRegions = regionFiles.computeIfAbsent(key, k -> new HashMap<>());
-
- int rx = x >> 5;
- int rz = z >> 5;
-
- long key2 = (((long) rx) << 32) | ((rz) & 0xFFFFFFFFL);
-
- McMMOSimpleRegionFile regionFile = worldRegions.get(key2);
-
- if (regionFile == null) {
- File file = new File(directory, "mcmmo_" + rx + "_" + rz + "_.mcm");
- regionFile = new McMMOSimpleRegionFile(file, rx, rz);
- worldRegions.put(key2, regionFile);
- }
-
- return regionFile;
- }
-
- @Override
- public synchronized void loadChunklet(int cx, int cy, int cz, World world) {
- loadChunk(cx, cz, world, null);
- }
-
- @Override
- public synchronized void unloadChunklet(int cx, int cy, int cz, World world) {
- unloadChunk(cx, cz, world);
- }
-
- @Override
- public synchronized void loadChunk(int cx, int cz, World world, Entity[] entities) {
- if (world == null || store.containsKey(world.getName() + "," + cx + "," + cz)) {
- return;
- }
-
- UUID key = world.getUID();
-
- if (!oldData.containsKey(key)) {
- oldData.put(key, (new File(world.getWorldFolder(), "mcmmo_data")).exists());
- }
- else if (oldData.get(key)) {
- if (convertChunk(new File(world.getWorldFolder(), "mcmmo_data"), cx, cz, world, true)) {
- return;
- }
- }
-
- ChunkStore chunkStore = null;
-
- try {
- chunkStore = readChunkStore(world, cx, cz);
- }
- catch (Exception e) { e.printStackTrace(); }
-
- if (chunkStore == null) {
- return;
- }
-
- store.put(world.getName() + "," + cx + "," + cz, chunkStore);
- }
-
- @Override
- public synchronized void unloadChunk(int cx, int cz, World world) {
- saveChunk(cx, cz, world);
-
- if (store.containsKey(world.getName() + "," + cx + "," + cz)) {
- store.remove(world.getName() + "," + cx + "," + cz);
-
- //closeChunkStore(world, cx, cz);
- }
- }
-
- @Override
- public synchronized void saveChunk(int cx, int cz, World world) {
- if (world == null) {
- return;
- }
-
- String key = world.getName() + "," + cx + "," + cz;
-
- if (store.containsKey(key)) {
- ChunkStore out = store.get(world.getName() + "," + cx + "," + cz);
-
- if (!out.isDirty()) {
- return;
- }
-
- writeChunkStore(world, cx, cz, out);
- }
- }
-
- @Override
- public synchronized boolean isChunkLoaded(int cx, int cz, World world) {
- if (world == null) {
- return false;
- }
-
- return store.containsKey(world.getName() + "," + cx + "," + cz);
- }
-
- @Override
- public synchronized void chunkLoaded(int cx, int cz, World world) {}
-
- @Override
- public synchronized void chunkUnloaded(int cx, int cz, World world) {
- if (world == null) {
- return;
- }
-
- unloadChunk(cx, cz, world);
- }
-
- @Override
- public synchronized void saveWorld(World world) {
- if (world == null) {
- return;
- }
-
- closeAll();
- String worldName = world.getName();
-
- List keys = new ArrayList<>(store.keySet());
- for (String key : keys) {
- String[] info = key.split(",");
- if (worldName.equals(info[0])) {
- try {
- saveChunk(Integer.parseInt(info[1]), Integer.parseInt(info[2]), world);
- }
- catch (Exception e) {
- // Ignore
- }
- }
- }
- }
-
- @Override
- public synchronized void unloadWorld(World world) {
- if (world == null) {
- return;
- }
-
- String worldName = world.getName();
-
- List keys = new ArrayList<>(store.keySet());
- for (String key : keys) {
- String[] info = key.split(",");
- if (worldName.equals(info[0])) {
- try {
- unloadChunk(Integer.parseInt(info[1]), Integer.parseInt(info[2]), world);
- }
- catch (Exception e) {
- // Ignore
- }
- }
- }
- closeAll();
- }
-
- @Override
- public synchronized void loadWorld(World world) {}
-
- @Override
- public synchronized void saveAll() {
- closeAll();
-
- for (World world : mcMMO.p.getServer().getWorlds()) {
- saveWorld(world);
- }
- }
-
- @Override
- public synchronized void unloadAll() {
- closeAll();
-
- for (World world : mcMMO.p.getServer().getWorlds()) {
- unloadWorld(world);
- }
- }
-
- @Override
- public synchronized boolean isTrue(int x, int y, int z, World world) {
- if (world == null) {
- return false;
- }
-
- int cx = x >> 4;
- int cz = z >> 4;
-
- String key = world.getName() + "," + cx + "," + cz;
-
- if (!store.containsKey(key)) {
- loadChunk(cx, cz, world, null);
- }
-
- if (!store.containsKey(key)) {
- return false;
- }
-
- ChunkStore check = store.get(key);
- int ix = Math.abs(x) % 16;
- int iz = Math.abs(z) % 16;
-
- return check.isTrue(ix, y, iz);
- }
-
- @Override
- public synchronized boolean isTrue(Block block) {
- if (block == null) {
- return false;
- }
-
- return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
- }
-
- @Override
- public synchronized boolean isTrue(BlockState blockState) {
- if (blockState == null) {
- return false;
- }
-
- return isTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
- }
-
- @Override
- public synchronized void setTrue(int x, int y, int z, World world) {
- if (world == null) {
- return;
- }
-
- int cx = x >> 4;
- int cz = z >> 4;
-
- int ix = Math.abs(x) % 16;
- int iz = Math.abs(z) % 16;
-
- String key = world.getName() + "," + cx + "," + cz;
-
- if (!store.containsKey(key)) {
- loadChunk(cx, cz, world, null);
- }
-
- ChunkStore cStore = store.get(key);
-
- if (cStore == null) {
- cStore = ChunkStoreFactory.getChunkStore(world, cx, cz);
- store.put(key, cStore);
- }
-
- cStore.setTrue(ix, y, iz);
- }
-
- @Override
- public synchronized void setTrue(Block block) {
- if (block == null) {
- return;
- }
-
- setTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
- }
-
- @Override
- public void setTrue(BlockState blockState) {
- if (blockState == null) {
- return;
- }
-
- setTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
- }
-
- @Override
- public synchronized void setFalse(int x, int y, int z, World world) {
- if (world == null) {
- return;
- }
-
- int cx = x >> 4;
- int cz = z >> 4;
-
- int ix = Math.abs(x) % 16;
- int iz = Math.abs(z) % 16;
-
- String key = world.getName() + "," + cx + "," + cz;
-
- if (!store.containsKey(key)) {
- loadChunk(cx, cz, world, null);
- }
-
- ChunkStore cStore = store.get(key);
-
- if (cStore == null) {
- return; // No need to make a store for something we will be setting to false
- }
-
- cStore.setFalse(ix, y, iz);
- }
-
- @Override
- public synchronized void setFalse(Block block) {
- if (block == null) {
- return;
- }
-
- setFalse(block.getX(), block.getY(), block.getZ(), block.getWorld());
- }
-
- @Override
- public synchronized void setFalse(BlockState blockState) {
- if (blockState == null) {
- return;
- }
-
- setFalse(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
- }
-
- @Override
- public synchronized void cleanUp() {}
-
- public synchronized void convertChunk(File dataDir, int cx, int cz, World world) {
- convertChunk(dataDir, cx, cz, world, false);
- }
-
- public synchronized boolean convertChunk(File dataDir, int cx, int cz, World world, boolean actually) {
- if (!actually || !dataDir.exists()) {
- return false;
- }
-
- File cxDir = new File(dataDir, "" + cx);
- if (!cxDir.exists()) {
- return false;
- }
-
- File czDir = new File(cxDir, "" + cz);
- if (!czDir.exists()) {
- return false;
- }
-
- boolean conversionSet = false;
-
- for (BlockStoreConversionZDirectory converter : this.converters) {
- if (converter == null) {
- continue;
- }
-
- if (converter.taskID >= 0) {
- continue;
- }
-
- converter.start(world, cxDir, czDir);
- conversionSet = true;
- break;
- }
-
- if (!conversionSet) {
- BlockStoreConversionZDirectory converter = new BlockStoreConversionZDirectory();
- converter.start(world, cxDir, czDir);
- converters.add(converter);
- }
-
- return true;
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleChunkBuffer.java b/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleChunkBuffer.java
deleted file mode 100644
index c2f158b95..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleChunkBuffer.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * This file is part of SpoutPlugin.
- *
- * Copyright (c) 2011-2012, SpoutDev
- * SpoutPlugin is licensed under the GNU Lesser General Public License.
- *
- * SpoutPlugin is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * SpoutPlugin 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 Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program. If not, see .
- */
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
-public class McMMOSimpleChunkBuffer extends ByteArrayOutputStream {
- final McMMOSimpleRegionFile rf;
- final int index;
-
- McMMOSimpleChunkBuffer(McMMOSimpleRegionFile rf, int index) {
- super(1024);
- this.rf = rf;
- this.index = index;
- }
-
- @Override
- public void close() throws IOException {
- rf.write(index, buf, count);
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleRegionFile.java b/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleRegionFile.java
deleted file mode 100644
index 2193417d8..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleRegionFile.java
+++ /dev/null
@@ -1,306 +0,0 @@
-/*
- * This file is part of SpoutPlugin.
- *
- * Copyright (c) 2011-2012, SpoutDev
- * SpoutPlugin is licensed under the GNU Lesser General Public License.
- *
- * SpoutPlugin is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * SpoutPlugin 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 Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program. If not, see .
- */
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
-
-import java.io.*;
-import java.util.ArrayList;
-import java.util.zip.DeflaterOutputStream;
-import java.util.zip.InflaterInputStream;
-
-public class McMMOSimpleRegionFile {
- private RandomAccessFile file;
- private final int[] dataStart = new int[1024];
- private final int[] dataActualLength = new int[1024];
- private final int[] dataLength = new int[1024];
- private final ArrayList inuse = new ArrayList<>();
- private int segmentSize;
- private int segmentMask;
- private final int rx;
- private final int rz;
- private final int defaultSegmentSize;
- private final File parent;
- @SuppressWarnings("unused")
- private long lastAccessTime = System.currentTimeMillis();
- @SuppressWarnings("unused")
- private static final long TIMEOUT_TIME = 300000; // 5 min
-
- public McMMOSimpleRegionFile(File f, int rx, int rz) {
- this(f, rx, rz, 10);
- }
-
- public McMMOSimpleRegionFile(File f, int rx, int rz, int defaultSegmentSize) {
- this.rx = rx;
- this.rz = rz;
- this.defaultSegmentSize = defaultSegmentSize;
- this.parent = f;
-
- lastAccessTime = System.currentTimeMillis();
- if (file == null) {
- try {
- this.file = new RandomAccessFile(parent, "rw");
-
- if (file.length() < 4096 * 3) {
- for (int i = 0; i < 1024 * 3; i++) {
- file.writeInt(0);
- }
- file.seek(4096 * 2);
- file.writeInt(defaultSegmentSize);
- }
-
- file.seek(4096 * 2);
-
- this.segmentSize = file.readInt();
- this.segmentMask = (1 << segmentSize) - 1;
-
- int reservedSegments = this.sizeToSegments(4096 * 3);
-
- for (int i = 0; i < reservedSegments; i++) {
- while (inuse.size() <= i) {
- inuse.add(false);
- }
- inuse.set(i, true);
- }
-
- file.seek(0);
-
- for (int i = 0; i < 1024; i++) {
- dataStart[i] = file.readInt();
- }
-
- for (int i = 0; i < 1024; i++) {
- dataActualLength[i] = file.readInt();
- dataLength[i] = sizeToSegments(dataActualLength[i]);
- setInUse(i, true);
- }
-
- extendFile();
- }
- catch (IOException fnfe) {
- throw new RuntimeException(fnfe);
- }
- }
- }
-
- public synchronized final RandomAccessFile getFile() {
- lastAccessTime = System.currentTimeMillis();
- if (file == null) {
- try {
- this.file = new RandomAccessFile(parent, "rw");
-
- if (file.length() < 4096 * 3) {
- for (int i = 0; i < 1024 * 3; i++) {
- file.writeInt(0);
- }
- file.seek(4096 * 2);
- file.writeInt(defaultSegmentSize);
- }
-
- file.seek(4096 * 2);
-
- this.segmentSize = file.readInt();
- this.segmentMask = (1 << segmentSize) - 1;
-
- int reservedSegments = this.sizeToSegments(4096 * 3);
-
- for (int i = 0; i < reservedSegments; i++) {
- while (inuse.size() <= i) {
- inuse.add(false);
- }
- inuse.set(i, true);
- }
-
- file.seek(0);
-
- for (int i = 0; i < 1024; i++) {
- dataStart[i] = file.readInt();
- }
-
- for (int i = 0; i < 1024; i++) {
- dataActualLength[i] = file.readInt();
- dataLength[i] = sizeToSegments(dataActualLength[i]);
- setInUse(i, true);
- }
-
- extendFile();
- }
- catch (IOException fnfe) {
- throw new RuntimeException(fnfe);
- }
- }
- return file;
- }
-
- public synchronized boolean testCloseTimeout() {
- /*
- if (System.currentTimeMillis() - TIMEOUT_TIME > lastAccessTime) {
- close();
- return true;
- }
- */
- return false;
- }
-
- public synchronized DataOutputStream getOutputStream(int x, int z) {
- int index = getChunkIndex(x, z);
- return new DataOutputStream(new DeflaterOutputStream(new McMMOSimpleChunkBuffer(this, index)));
- }
-
- public synchronized DataInputStream getInputStream(int x, int z) throws IOException {
- int index = getChunkIndex(x, z);
- int actualLength = dataActualLength[index];
-
- if (actualLength == 0) {
- return null;
- }
-
- byte[] data = new byte[actualLength];
-
- getFile().seek(dataStart[index] << segmentSize);
- getFile().readFully(data);
- return new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data)));
- }
-
- synchronized void write(int index, byte[] buffer, int size) throws IOException {
- int oldStart = setInUse(index, false);
- int start = findSpace(oldStart, size);
- getFile().seek(start << segmentSize);
- getFile().write(buffer, 0, size);
- dataStart[index] = start;
- dataActualLength[index] = size;
- dataLength[index] = sizeToSegments(size);
- setInUse(index, true);
- saveFAT();
- }
-
- public synchronized void close() {
- try {
- if (file != null) {
- file.seek(4096 * 2);
- file.close();
- }
-
- file = null;
- }
- catch (IOException ioe) {
- throw new RuntimeException("Unable to close file", ioe);
- }
- }
-
- private synchronized int setInUse(int index, boolean used) {
- if (dataActualLength[index] == 0) {
- return dataStart[index];
- }
-
- int start = dataStart[index];
- int end = start + dataLength[index];
-
- for (int i = start; i < end; i++) {
- while (i > inuse.size() - 1) {
- inuse.add(false);
- }
-
- Boolean old = inuse.set(i, used);
- if (old != null && old == used) {
- if (old) {
- throw new IllegalStateException("Attempting to overwrite an in-use segment");
- }
-
- throw new IllegalStateException("Attempting to delete empty segment");
- }
- }
-
- return dataStart[index];
- }
-
- private synchronized void extendFile() throws IOException {
- long extend = (-getFile().length()) & segmentMask;
-
- getFile().seek(getFile().length());
-
- while ((extend--) > 0) {
- getFile().write(0);
- }
- }
-
- private synchronized int findSpace(int oldStart, int size) {
- int segments = sizeToSegments(size);
-
- boolean oldFree = true;
- for (int i = oldStart; i < inuse.size() && i < oldStart + segments; i++) {
- if (inuse.get(i)) {
- oldFree = false;
- break;
- }
- }
-
- if (oldFree) {
- return oldStart;
- }
-
- int start = 0;
- int end = 0;
-
- while (end < inuse.size()) {
- if (inuse.get(end)) {
- end++;
- start = end;
- }
- else {
- end++;
- }
-
- if (end - start >= segments) {
- return start;
- }
- }
-
- return start;
- }
-
- private synchronized int sizeToSegments(int size) {
- if (size <= 0) {
- return 1;
- }
-
- return ((size - 1) >> segmentSize) + 1;
- }
-
- private synchronized Integer getChunkIndex(int x, int z) {
- if (rx != (x >> 5) || rz != (z >> 5)) {
- throw new RuntimeException(x + ", " + z + " not in region " + rx + ", " + rz);
- }
-
- x = x & 0x1F;
- z = z & 0x1F;
-
- return (x << 5) + z;
- }
-
- private synchronized void saveFAT() throws IOException {
- getFile().seek(0);
- for (int i = 0; i < 1024; i++) {
- getFile().writeInt(dataStart[i]);
- }
-
- for (int i = 0; i < 1024; i++) {
- getFile().writeInt(dataActualLength[i]);
- }
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/PrimitiveChunkStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/PrimitiveChunkStore.java
deleted file mode 100755
index d1866acab..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/PrimitiveChunkStore.java
+++ /dev/null
@@ -1,147 +0,0 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
-
-import com.gmail.nossr50.util.blockmeta.ChunkletStore;
-import org.bukkit.Bukkit;
-import org.bukkit.World;
-
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.util.UUID;
-
-public class PrimitiveChunkStore implements ChunkStore {
- private static final long serialVersionUID = -1L;
- transient private boolean dirty = false;
- /** X, Z, Y */
- public boolean[][][] store;
- private static final int CURRENT_VERSION = 7;
- private static final int MAGIC_NUMBER = 0xEA5EDEBB;
- private int cx;
- private int cz;
- private UUID worldUid;
-
- public PrimitiveChunkStore(World world, int cx, int cz) {
- this.cx = cx;
- this.cz = cz;
- this.worldUid = world.getUID();
- this.store = new boolean[16][16][world.getMaxHeight()];
- }
-
- @Override
- public boolean isDirty() {
- return dirty;
- }
-
- @Override
- public void setDirty(boolean dirty) {
- this.dirty = dirty;
- }
-
- @Override
- public int getChunkX() {
- return cx;
- }
-
- @Override
- public int getChunkZ() {
- return cz;
- }
-
- @Override
- public boolean isTrue(int x, int y, int z) {
- return store[x][z][y];
- }
-
- @Override
- public void setTrue(int x, int y, int z) {
- if (y >= store[0][0].length || y < 0)
- return;
- store[x][z][y] = true;
- dirty = true;
- }
-
- @Override
- public void setFalse(int x, int y, int z) {
- if (y >= store[0][0].length || y < 0)
- return;
- store[x][z][y] = false;
- dirty = true;
- }
-
- @Override
- public boolean isEmpty() {
- for (int x = 0; x < 16; x++) {
- for (int z = 0; z < 16; z++) {
- for (int y = 0; y < store[0][0].length; y++) {
- if (store[x][z][y]) {
- return false;
- }
- }
- }
- }
- return true;
- }
-
- @Override
- public void copyFrom(ChunkletStore otherStore) {
- for (int x = 0; x < 16; x++) {
- for (int z = 0; z < 16; z++) {
- for (int y = 0; y < store[0][0].length; y++) {
- store[x][z][y] = otherStore.isTrue(x, y, z);
- }
- }
- }
- dirty = true;
- }
-
- private void writeObject(ObjectOutputStream out) throws IOException {
- out.writeInt(MAGIC_NUMBER);
- out.writeInt(CURRENT_VERSION);
-
- out.writeLong(worldUid.getLeastSignificantBits());
- out.writeLong(worldUid.getMostSignificantBits());
- out.writeInt(cx);
- out.writeInt(cz);
- out.writeObject(store);
-
- dirty = false;
- }
-
- private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
- int magic = in.readInt();
- // Can be used to determine the format of the file
- int fileVersionNumber = in.readInt();
-
- if (magic != MAGIC_NUMBER) {
- fileVersionNumber = 0;
- }
-
- long lsb = in.readLong();
- long msb = in.readLong();
- worldUid = new UUID(msb, lsb);
- cx = in.readInt();
- cz = in.readInt();
-
- store = (boolean[][][]) in.readObject();
-
- if (fileVersionNumber < 5) {
- fixArray();
- dirty = true;
- }
- }
-
- private void fixArray() {
- boolean[][][] temp = this.store;
- this.store = new boolean[16][16][Bukkit.getWorld(worldUid).getMaxHeight()];
- for (int x = 0; x < 16; x++) {
- for (int z = 0; z < 16; z++) {
- for (int y = 0; y < store[0][0].length; y++) {
- try {
- store[x][z][y] = temp[x][y][z];
- }
- catch (Exception e) { e.printStackTrace(); }
- }
- }
- }
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionMain.java b/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionMain.java
deleted file mode 100755
index 9dcb20c2a..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionMain.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package com.gmail.nossr50.util.blockmeta.conversion;
-
-import com.gmail.nossr50.config.HiddenConfig;
-import com.gmail.nossr50.mcMMO;
-import org.bukkit.scheduler.BukkitScheduler;
-
-import java.io.File;
-
-public class BlockStoreConversionMain implements Runnable {
- private int taskID, i;
- private org.bukkit.World world;
- BukkitScheduler scheduler;
- File dataDir;
- File[] xDirs;
- BlockStoreConversionXDirectory[] converters;
-
- public BlockStoreConversionMain(org.bukkit.World world) {
- this.taskID = -1;
- this.world = world;
- this.scheduler = mcMMO.p.getServer().getScheduler();
- this.dataDir = new File(this.world.getWorldFolder(), "mcmmo_data");
- this.converters = new BlockStoreConversionXDirectory[HiddenConfig.getInstance().getConversionRate()];
- }
-
- public void start() {
- if (this.taskID >= 0) {
- return;
- }
-
- this.taskID = this.scheduler.runTaskLater(mcMMO.p, this, 1).getTaskId();
- }
-
- @Override
- public void run() {
- if (!this.dataDir.exists()) {
- softStop();
- return;
- }
-
- if (!this.dataDir.isDirectory()) {
- this.dataDir.delete();
- softStop();
- return;
- }
-
- if (this.dataDir.listFiles().length <= 0) {
- this.dataDir.delete();
- softStop();
- return;
- }
-
- this.xDirs = this.dataDir.listFiles();
-
- for (this.i = 0; (this.i < HiddenConfig.getInstance().getConversionRate()) && (this.i < this.xDirs.length); this.i++) {
- if (this.converters[this.i] == null) {
- this.converters[this.i] = new BlockStoreConversionXDirectory();
- }
-
- this.converters[this.i].start(this.world, this.xDirs[this.i]);
- }
-
- softStop();
- }
-
- public void stop() {
- if (this.taskID < 0) {
- return;
- }
-
- this.scheduler.cancelTask(this.taskID);
- this.taskID = -1;
- }
-
- public void softStop() {
- stop();
-
- if (this.dataDir.exists() && this.dataDir.isDirectory()) {
- start();
- return;
- }
-
- mcMMO.p.getLogger().info("Finished converting the storage for " + world.getName() + ".");
-
- this.dataDir = null;
- this.xDirs = null;
- this.world = null;
- this.scheduler = null;
- this.converters = null;
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionXDirectory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionXDirectory.java
deleted file mode 100755
index a64eec843..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionXDirectory.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package com.gmail.nossr50.util.blockmeta.conversion;
-
-import com.gmail.nossr50.config.HiddenConfig;
-import com.gmail.nossr50.mcMMO;
-import org.bukkit.scheduler.BukkitScheduler;
-
-import java.io.File;
-
-public class BlockStoreConversionXDirectory implements Runnable {
- private int taskID, i;
- private org.bukkit.World world;
- BukkitScheduler scheduler;
- File dataDir;
- File[] zDirs;
- BlockStoreConversionZDirectory[] converters;
-
- public BlockStoreConversionXDirectory() {
- this.taskID = -1;
- }
-
- public void start(org.bukkit.World world, File dataDir) {
- this.world = world;
- this.scheduler = mcMMO.p.getServer().getScheduler();
- this.converters = new BlockStoreConversionZDirectory[HiddenConfig.getInstance().getConversionRate()];
- this.dataDir = dataDir;
-
- if (this.taskID >= 0) {
- return;
- }
-
- this.taskID = this.scheduler.runTaskLater(mcMMO.p, this, 1).getTaskId();
- }
-
- @Override
- public void run() {
- if (!this.dataDir.exists()) {
- stop();
- return;
- }
-
- if (!this.dataDir.isDirectory()) {
- this.dataDir.delete();
- stop();
- return;
- }
-
- if (this.dataDir.listFiles().length <= 0) {
- this.dataDir.delete();
- stop();
- return;
- }
-
- this.zDirs = this.dataDir.listFiles();
-
- for (this.i = 0; (this.i < HiddenConfig.getInstance().getConversionRate()) && (this.i < this.zDirs.length); this.i++) {
- if (this.converters[this.i] == null) {
- this.converters[this.i] = new BlockStoreConversionZDirectory();
- }
-
- this.converters[this.i].start(this.world, this.dataDir, this.zDirs[this.i]);
- }
-
- stop();
- }
-
- public void stop() {
- if (this.taskID < 0) {
- return;
- }
-
- this.scheduler.cancelTask(this.taskID);
- this.taskID = -1;
-
- this.dataDir = null;
- this.zDirs = null;
- this.world = null;
- this.scheduler = null;
- this.converters = null;
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionZDirectory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionZDirectory.java
deleted file mode 100755
index 4a32a679c..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionZDirectory.java
+++ /dev/null
@@ -1,191 +0,0 @@
-package com.gmail.nossr50.util.blockmeta.conversion;
-
-import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.blockmeta.ChunkletStore;
-import com.gmail.nossr50.util.blockmeta.HashChunkletManager;
-import com.gmail.nossr50.util.blockmeta.PrimitiveChunkletStore;
-import com.gmail.nossr50.util.blockmeta.PrimitiveExChunkletStore;
-import com.gmail.nossr50.util.blockmeta.chunkmeta.HashChunkManager;
-import com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore;
-import org.bukkit.scheduler.BukkitScheduler;
-
-import java.io.File;
-
-public class BlockStoreConversionZDirectory implements Runnable {
- public int taskID, cx, cz, x, y, z, y2, xPos, zPos, cxPos, czPos;
- private String cxs, czs, chunkletName, chunkName;
- private org.bukkit.World world;
- private BukkitScheduler scheduler;
- private File xDir, dataDir;
- private HashChunkletManager manager;
- private HashChunkManager newManager;
- private ChunkletStore tempChunklet;
- private PrimitiveChunkletStore primitiveChunklet = null;
- private PrimitiveExChunkletStore primitiveExChunklet = null;
- private PrimitiveChunkStore currentChunk;
- private boolean[] oldArray, newArray;
-
- public BlockStoreConversionZDirectory() {
- this.taskID = -1;
- }
-
- public void start(org.bukkit.World world, File xDir, File dataDir) {
- this.world = world;
- this.scheduler = mcMMO.p.getServer().getScheduler();
- this.manager = new HashChunkletManager();
- this.newManager = (HashChunkManager) mcMMO.getPlaceStore();
- this.dataDir = dataDir;
- this.xDir = xDir;
-
- if (this.taskID >= 0) {
- return;
- }
-
- this.taskID = this.scheduler.runTaskLater(mcMMO.p, this, 1).getTaskId();
- }
-
- @Override
- public void run() {
- if (!this.dataDir.exists()) {
- stop();
- return;
- }
-
- if (!this.dataDir.isDirectory()) {
- this.dataDir.delete();
- stop();
- return;
- }
-
- if (this.dataDir.listFiles().length <= 0) {
- this.dataDir.delete();
- stop();
- return;
- }
-
- this.cxs = this.xDir.getName();
- this.czs = this.dataDir.getName();
- this.cx = 0;
- this.cz = 0;
-
- try {
- this.cx = Integer.parseInt(this.cxs);
- this.cz = Integer.parseInt(this.czs);
- }
- catch (Exception e) {
- this.dataDir.delete();
- stop();
- return;
- }
-
- this.manager.loadChunk(this.cx, this.cz, this.world);
-
- for (this.y = 0; this.y < (this.world.getMaxHeight() / 64); this.y++) {
- this.chunkletName = this.world.getName() + "," + this.cx + "," + this.cz + "," + this.y;
- this.tempChunklet = this.manager.store.get(this.chunkletName);
-
- if (this.tempChunklet instanceof PrimitiveChunkletStore) {
- this.primitiveChunklet = (PrimitiveChunkletStore) this.tempChunklet;
- }
- else if (this.tempChunklet instanceof PrimitiveExChunkletStore) {
- this.primitiveExChunklet = (PrimitiveExChunkletStore) this.tempChunklet;
- }
-
- if (this.tempChunklet == null) {
- continue;
- }
-
- this.chunkName = this.world.getName() + "," + this.cx + "," + this.cz;
- this.currentChunk = (PrimitiveChunkStore) this.newManager.store.get(this.chunkName);
-
- if (this.currentChunk != null) {
- this.xPos = this.cx * 16;
- this.zPos = this.cz * 16;
-
- for (this.x = 0; this.x < 16; this.x++) {
- for (this.z = 0; this.z < 16; this.z++) {
- this.cxPos = this.xPos + this.x;
- this.czPos = this.zPos + this.z;
-
- for (this.y2 = (64 * this.y); this.y2 < (64 * this.y + 64); this.y2++) {
- try {
- if (!this.manager.isTrue(this.cxPos, this.y2, this.czPos, this.world)) {
- continue;
- }
-
- this.newManager.setTrue(this.cxPos, this.y2, this.czPos, this.world);
- }
- catch (Exception e) { e.printStackTrace(); }
- }
- }
- }
-
- continue;
- }
-
- this.newManager.setTrue(this.cx * 16, 0, this.cz * 16, this.world);
- this.newManager.setFalse(this.cx * 16, 0, this.cz * 16, this.world);
- this.currentChunk = (PrimitiveChunkStore) this.newManager.store.get(this.chunkName);
-
- for (this.x = 0; this.x < 16; this.x++) {
- for (this.z = 0; this.z < 16; this.z++) {
- if (this.primitiveChunklet != null) {
- this.oldArray = this.primitiveChunklet.store[x][z];
- }
-
- if (this.primitiveExChunklet != null) {
- this.oldArray = this.primitiveExChunklet.store[x][z];
- }
- else {
- return;
- }
-
- this.newArray = this.currentChunk.store[x][z];
-
- if (this.oldArray.length < 64) {
- return;
- }
- else if (this.newArray.length < ((this.y * 64) + 64)) {
- return;
- }
-
- System.arraycopy(this.oldArray, 0, this.newArray, (this.y * 64), 64);
- }
- }
- }
-
- this.manager.unloadChunk(this.cx, this.cz, this.world);
- this.newManager.unloadChunk(this.cx, this.cz, this.world);
-
- for (File yFile : dataDir.listFiles()) {
- if (!yFile.exists()) {
- continue;
- }
-
- yFile.delete();
- }
-
- stop();
- }
-
- public void stop() {
- if (this.taskID < 0) {
- return;
- }
-
- this.scheduler.cancelTask(taskID);
- this.taskID = -1;
-
- this.cxs = null;
- this.czs = null;
- this.chunkletName = null;
- this.chunkName = null;
- this.manager = null;
- this.xDir = null;
- this.dataDir = null;
- this.tempChunklet = null;
- this.primitiveChunklet = null;
- this.primitiveExChunklet = null;
- this.currentChunk = null;
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java b/src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java
index fef50ae8a..4ea396995 100644
--- a/src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java
+++ b/src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java
@@ -149,6 +149,15 @@ public final class CommandRegistrationManager {
command.setExecutor(new McgodCommand());
}
+// private static void registerDropTreasureCommand() {
+// PluginCommand command = mcMMO.p.getCommand("mmodroptreasures");
+// command.setDescription(LocaleLoader.getString("Commands.Description.droptreasures"));
+// command.setPermission("mcmmo.commands.droptreasures");
+// command.setPermissionMessage(permissionsMessage);
+// command.setUsage(LocaleLoader.getString("Commands.Usage.0", "mcgod"));
+// command.setExecutor(new DropTreasureCommand());
+// }
+
private static void registerMmoInfoCommand() {
PluginCommand command = mcMMO.p.getCommand("mmoinfo");
command.setDescription(LocaleLoader.getString("Commands.Description.mmoinfo"));
diff --git a/src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java b/src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java
index 3ecce20b8..2eace2007 100644
--- a/src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java
+++ b/src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java
@@ -383,8 +383,10 @@ public final class CombatUtils {
EntityType entityType = painSource.getType();
if (target instanceof Player) {
- if (Misc.isNPCEntityExcludingVillagers(target)) {
- return;
+ if(ExperienceConfig.getInstance().isNPCInteractionPrevented()) {
+ if (Misc.isNPCEntityExcludingVillagers(target)) {
+ return;
+ }
}
Player player = (Player) target;
@@ -755,7 +757,7 @@ public final class CombatUtils {
break;
}
- if (Misc.isNPCEntityExcludingVillagers(entity) || !(entity instanceof LivingEntity) || !shouldBeAffected(attacker, entity)) {
+ if ((ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(entity)) || !(entity instanceof LivingEntity) || !shouldBeAffected(attacker, entity)) {
continue;
}
diff --git a/src/main/resources/experience.yml b/src/main/resources/experience.yml
index a2f66c4fa..7f627a70c 100644
--- a/src/main/resources/experience.yml
+++ b/src/main/resources/experience.yml
@@ -35,6 +35,10 @@ ExploitFix:
TreeFellerReducedXP: true
PistonCheating: true
SnowGolemExcavation: true
+ # This include NPCs from stuff like Citizens, this is not a setting for Vanilla Minecraft Villagers (Which can be considered NPCs)
+ # mcMMO normally doesn't process attacks against an Entity if it is an NPC from another plugin
+ # Of course, mcMMO doesn't know for sure whether or not something is an NPC, it checks a few known things, see our source code to see how
+ PreventPluginNPCInteraction: true
Experience_Bars:
# Turn this to false if you wanna disable XP bars
Enable: true
diff --git a/src/main/resources/fishing_treasures.yml b/src/main/resources/fishing_treasures.yml
new file mode 100644
index 000000000..d839da2fd
--- /dev/null
+++ b/src/main/resources/fishing_treasures.yml
@@ -0,0 +1,852 @@
+#
+# Settings for Fishing Treasures / Shake Treasures
+# Last updated on 12/28/2020
+###
+Fishing:
+ LEATHER_BOOTS:
+ Amount: 1
+ XP: 200
+ Rarity: COMMON
+ LEATHER_HELMET:
+ Amount: 1
+ XP: 200
+ Rarity: COMMON
+ LEATHER_LEGGINGS:
+ Amount: 1
+ XP: 200
+ Rarity: COMMON
+ LEATHER_CHESTPLATE:
+ Amount: 1
+ XP: 200
+ Rarity: COMMON
+ WOODEN_SWORD:
+ Amount: 1
+ XP: 200
+ Rarity: COMMON
+ WOODEN_SHOVEL:
+ Amount: 1
+ XP: 200
+ Rarity: COMMON
+ WOODEN_PICKAXE:
+ Amount: 1
+ XP: 200
+ Rarity: COMMON
+ WOODEN_AXE:
+ Amount: 1
+ XP: 200
+ Rarity: COMMON
+ WOODEN_HOE:
+ Amount: 1
+ XP: 200
+ Rarity: COMMON
+ LAPIS_LAZULI:
+ Amount: 20
+ XP: 200
+ Rarity: COMMON
+ STONE_SWORD:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ STONE_SHOVEL:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ STONE_PICKAXE:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ STONE_AXE:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ STONE_HOE:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ GOLDEN_SWORD:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ GOLDEN_SHOVEL:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ GOLDEN_PICKAXE:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ GOLDEN_AXE:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ GOLDEN_HOE:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ GOLDEN_BOOTS:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ GOLDEN_HELMET:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ GOLDEN_LEGGINGS:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ GOLDEN_CHESTPLATE:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ IRON_INGOT:
+ Amount: 5
+ XP: 200
+ Rarity: UNCOMMON
+ GOLD_INGOT:
+ Amount: 5
+ XP: 200
+ Rarity: UNCOMMON
+ IRON_SWORD:
+ Amount: 1
+ XP: 200
+ Rarity: RARE
+ IRON_SHOVEL:
+ Amount: 1
+ XP: 200
+ Rarity: RARE
+ IRON_PICKAXE:
+ Amount: 1
+ XP: 200
+ Rarity: RARE
+ IRON_AXE:
+ Amount: 1
+ XP: 200
+ Rarity: RARE
+ IRON_HOE:
+ Amount: 1
+ XP: 200
+ Rarity: RARE
+ BOW:
+ Amount: 1
+ XP: 200
+ Rarity: RARE
+ ENDER_PEARL:
+ Amount: 1
+ XP: 200
+ Rarity: RARE
+ BLAZE_ROD:
+ Amount: 1
+ XP: 200
+ Rarity: RARE
+ NAME_TAG:
+ Amount: 1
+ XP: 200
+ Rarity: RARE
+ IRON_BOOTS:
+ Amount: 1
+ XP: 200
+ Rarity: EPIC
+ IRON_HELMET:
+ Amount: 1
+ XP: 200
+ Rarity: EPIC
+ IRON_LEGGINGS:
+ Amount: 1
+ XP: 200
+ Rarity: EPIC
+ IRON_CHESTPLATE:
+ Amount: 1
+ XP: 200
+ Rarity: EPIC
+ GHAST_TEAR:
+ Amount: 1
+ XP: 200
+ Rarity: EPIC
+ DIAMOND:
+ Amount: 5
+ XP: 200
+ Rarity: EPIC
+ NAUTILUS_SHELL:
+ Amount: 1
+ XP: 200
+ Rarity: LEGENDARY
+ DIAMOND_SWORD:
+ Amount: 1
+ XP: 200
+ Rarity: LEGENDARY
+ DIAMOND_SHOVEL:
+ Amount: 1
+ XP: 200
+ Rarity: LEGENDARY
+ DIAMOND_PICKAXE:
+ Amount: 1
+ XP: 200
+ Rarity: LEGENDARY
+ DIAMOND_AXE:
+ Amount: 1
+ XP: 200
+ Rarity: LEGENDARY
+ DIAMOND_HOE:
+ Amount: 1
+ XP: 200
+ Rarity: LEGENDARY
+ DIAMOND_BOOTS:
+ Amount: 1
+ XP: 200
+ Rarity: LEGENDARY
+ DIAMOND_HELMET:
+ Amount: 1
+ XP: 200
+ Rarity: LEGENDARY
+ DIAMOND_LEGGINGS:
+ Amount: 1
+ XP: 200
+ Rarity: LEGENDARY
+ DIAMOND_CHESTPLATE:
+ Amount: 1
+ XP: 200
+ Rarity: LEGENDARY
+ MUSIC_DISC_BLOCKS:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ MUSIC_DISC_CHIRP:
+ Amount: 1
+ XP: 200
+ Rarity: UNCOMMON
+ MUSIC_DISC_FAR:
+ Amount: 1
+ XP: 200
+ Rarity: RARE
+ MUSIC_DISC_MALL:
+ Amount: 1
+ XP: 200
+ Rarity: RARE
+ MUSIC_DISC_MELLOHI:
+ Amount: 1
+ XP: 200
+ Rarity: RARE
+ MUSIC_DISC_STAL:
+ Amount: 1
+ XP: 200
+ Rarity: EPIC
+ MUSIC_DISC_STRAD:
+ Amount: 1
+ XP: 200
+ Rarity: EPIC
+ MUSIC_DISC_WARD:
+ Amount: 1
+ XP: 200
+ Rarity: EPIC
+ MUSIC_DISC_11:
+ Amount: 1
+ XP: 200
+ Rarity: LEGENDARY
+ MUSIC_DISC_WAIT:
+ Amount: 1
+ XP: 200
+ Rarity: LEGENDARY
+ MUSIC_DISC_13:
+ Amount: 1
+ XP: 200
+ Rarity: MYTHIC
+ NETHERITE_SWORD:
+ Amount: 1
+ XP: 200
+ Rarity: MYTHIC
+ NETHERITE_SHOVEL:
+ Amount: 1
+ XP: 200
+ Rarity: MYTHIC
+ NETHERITE_PICKAXE:
+ Amount: 1
+ XP: 200
+ Rarity: MYTHIC
+ NETHERITE_AXE:
+ Amount: 1
+ XP: 200
+ Rarity: MYTHIC
+ NETHERITE_HOE:
+ Amount: 1
+ XP: 200
+ Rarity: MYTHIC
+ NETHERITE_BOOTS:
+ Amount: 1
+ XP: 200
+ Rarity: MYTHIC
+ NETHERITE_HELMET:
+ Amount: 1
+ XP: 200
+ Rarity: MYTHIC
+ NETHERITE_LEGGINGS:
+ Amount: 1
+ XP: 200
+ Rarity: MYTHIC
+ NETHERITE_CHESTPLATE:
+ Amount: 1
+ XP: 200
+ Rarity: MYTHIC
+ ENCHANTED_BOOK:
+ Amount: 1
+ XP: 400
+ Rarity: LEGENDARY
+# Uncomment the below 3 lines to use the Whitelist, Alternatively rename it Enchantments_Blacklist to use the blacklist
+# NOTE: Enchantments_Whitelist and Enchantments_Blacklist only do anything for Enchanted_Books at the moment, you can't use it with any other treasure definition
+# Enchantments_Whitelist:
+# - Fortune
+# - Protection
+ NETHERITE_SCRAP:
+ Amount: 1
+ XP: 400
+ Rarity: LEGENDARY
+#
+# Fishing drop rates
+###
+Item_Drop_Rates:
+ Tier_1:
+ COMMON: 7.50
+ UNCOMMON: 1.25
+ RARE: 0.25
+ EPIC: 0.10
+ LEGENDARY: 0.01
+ MYTHIC: 0.01
+ Tier_2:
+ COMMON: 6.50
+ UNCOMMON: 1.75
+ RARE: 0.75
+ EPIC: 0.50
+ LEGENDARY: 0.05
+ MYTHIC: 0.01
+ Tier_3:
+ COMMON: 3.50
+ UNCOMMON: 2.75
+ RARE: 1.25
+ EPIC: 1.00
+ LEGENDARY: 0.10
+ MYTHIC: 0.01
+ Tier_4:
+ COMMON: 2.00
+ UNCOMMON: 3.50
+ RARE: 2.25
+ EPIC: 1.50
+ LEGENDARY: 1.00
+ MYTHIC: 0.01
+ Tier_5:
+ COMMON: 1.50
+ UNCOMMON: 3.75
+ RARE: 2.50
+ EPIC: 2.00
+ LEGENDARY: 1.00
+ MYTHIC: 0.01
+ Tier_6:
+ COMMON: 1.00
+ UNCOMMON: 3.25
+ RARE: 3.75
+ EPIC: 2.50
+ LEGENDARY: 1.50
+ MYTHIC: 0.05
+ Tier_7:
+ COMMON: 0.25
+ UNCOMMON: 2.75
+ RARE: 4.00
+ EPIC: 5.00
+ LEGENDARY: 2.50
+ MYTHIC: 0.10
+ Tier_8:
+ COMMON: 0.10
+ UNCOMMON: 1.50
+ RARE: 6.00
+ EPIC: 7.50
+ LEGENDARY: 5.00
+ MYTHIC: 0.25
+#
+# Fishing enchantment drop rates
+###
+Enchantments_Rarity:
+ COMMON:
+ EFFICIENCY: 1
+ UNBREAKING: 1
+ FORTUNE: 1
+ PROTECTION: 1
+ FIRE_PROTECTION: 1
+ FEATHER_FALLING: 1
+ BLAST_PROTECTION: 1
+ PROJECTILE_PROTECTION: 1
+ RESPIRATION: 1
+ THORNS: 1
+ SHARPNESS: 1
+ SMITE: 1
+ BANE_OF_ARTHROPODS: 1
+ POWER: 1
+ UNCOMMON:
+ EFFICIENCY: 2
+ PROTECTION: 2
+ FIRE_PROTECTION: 2
+ FEATHER_FALLING: 2
+ BLAST_PROTECTION: 2
+ PROJECTILE_PROTECTION: 2
+ SHARPNESS: 2
+ SMITE: 2
+ BANE_OF_ARTHROPODS: 2
+ KNOCKBACK: 1
+ LOOTING: 1
+ POWER: 2
+ PUNCH: 1
+ RARE:
+ EFFICIENCY: 3
+ UNBREAKING: 2
+ PROTECTION: 3
+ FIRE_PROTECTION: 3
+ FEATHER_FALLING: 3
+ BLAST_PROTECTION: 3
+ PROJECTILE_PROTECTION: 3
+ RESPIRATION: 2
+ SHARPNESS: 3
+ SMITE: 3
+ BANE_OF_ARTHROPODS: 3
+ FIRE_ASPECT: 1
+ LOOTING: 2
+ POWER: 3
+ EPIC:
+ EFFICIENCY: 4
+ FORTUNE: 2
+ AQUA_AFFINITY: 1
+ THORNS: 2
+ SHARPNESS: 4
+ SMITE: 4
+ BANE_OF_ARTHROPODS: 4
+ POWER: 4
+ FLAME: 1
+ LEGENDARY:
+ EFFICIENCY: 5
+ UNBREAKING: 3
+ FORTUNE: 3
+ PROTECTION: 4
+ FIRE_PROTECTION: 4
+ FEATHER_FALLING: 4
+ BLAST_PROTECTION: 4
+ PROJECTILE_PROTECTION: 4
+ RESPIRATION: 3
+ AQUA_AFFINITY: 1
+ THORNS: 3
+ SHARPNESS: 5
+ SMITE: 5
+ BANE_OF_ARTHROPODS: 5
+ KNOCKBACK: 2
+ FIRE_ASPECT: 2
+ LOOTING: 3
+ SILK_TOUCH: 1
+ POWER: 5
+ PUNCH: 2
+ INFINITY: 1
+ MYTHIC:
+ EFFICIENCY: 5
+ UNBREAKING: 3
+ FORTUNE: 3
+ PROTECTION: 4
+ FIRE_PROTECTION: 4
+ FEATHER_FALLING: 4
+ BLAST_PROTECTION: 4
+ PROJECTILE_PROTECTION: 4
+ RESPIRATION: 3
+ AQUA_AFFINITY: 1
+ THORNS: 3
+ SHARPNESS: 5
+ SMITE: 5
+ BANE_OF_ARTHROPODS: 5
+ KNOCKBACK: 2
+ FIRE_ASPECT: 2
+ LOOTING: 3
+ SILK_TOUCH: 1
+ POWER: 5
+ PUNCH: 2
+ INFINITY: 1
+Enchantment_Drop_Rates:
+ Tier_1:
+ COMMON: 5.00
+ UNCOMMON: 1.00
+ RARE: 0.10
+ EPIC: 0.01
+ LEGENDARY: 0.01
+ MYTHIC: 0.01
+ Tier_2:
+ COMMON: 7.50
+ UNCOMMON: 1.00
+ RARE: 0.10
+ EPIC: 0.01
+ LEGENDARY: 0.01
+ MYTHIC: 0.01
+ Tier_3:
+ COMMON: 7.50
+ UNCOMMON: 2.50
+ RARE: 0.25
+ EPIC: 0.10
+ LEGENDARY: 0.01
+ MYTHIC: 0.01
+ Tier_4:
+ COMMON: 10.0
+ UNCOMMON: 2.75
+ RARE: 0.50
+ EPIC: 0.10
+ LEGENDARY: 0.05
+ MYTHIC: 0.05
+ Tier_5:
+ COMMON: 10.0
+ UNCOMMON: 4.00
+ RARE: 0.75
+ EPIC: 0.25
+ LEGENDARY: 0.10
+ MYTHIC: 0.10
+ Tier_6:
+ COMMON: 9.50
+ UNCOMMON: 5.50
+ RARE: 1.75
+ EPIC: 0.50
+ LEGENDARY: 0.25
+ MYTHIC: 0.25
+ Tier_7:
+ COMMON: 8.50
+ UNCOMMON: 7.50
+ RARE: 2.75
+ EPIC: 0.75
+ LEGENDARY: 0.50
+ MYTHIC: 0.50
+ Tier_8:
+ COMMON: 7.50
+ UNCOMMON: 10.0
+ RARE: 5.25
+ EPIC: 1.50
+ LEGENDARY: 0.75
+ MYTHIC: 0.75
+#
+# Settings for Shake
+# If you are in retro mode, Drop_Level is multiplied by 10.
+###
+Shake:
+ BLAZE:
+ BLAZE_ROD:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 100.0
+ Drop_Level: 0
+ CAVE_SPIDER:
+ SPIDER_EYE:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 49.0
+ Drop_Level: 0
+ STRING:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 49.0
+ Drop_Level: 0
+ COBWEB:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 1.0
+ Drop_Level: 0
+ POTION|0|POISON:
+ PotionData:
+ PotionType: POISON
+ Amount: 1
+ XP: 0
+ Drop_Chance: 1.0
+ Drop_Level: 0
+ CHICKEN:
+ FEATHER:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 33.3
+ Drop_Level: 0
+ CHICKEN:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 33.3
+ Drop_Level: 0
+ EGG:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 33.3
+ Drop_Level: 0
+ COW:
+ MILK_BUCKET:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 2.0
+ Drop_Level: 0
+ LEATHER:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 49.0
+ Drop_Level: 0
+ BEEF:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 49.0
+ Drop_Level: 0
+ CREEPER:
+ CREEPER_HEAD:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 1.0
+ Drop_Level: 0
+ GUNPOWDER:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 99.0
+ Drop_Level: 0
+ ENDERMAN:
+ ENDER_PEARL:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 100.0
+ Drop_Level: 0
+ GHAST:
+ GUNPOWDER:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 50.0
+ Drop_Level: 0
+ GHAST_TEAR:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 50.0
+ Drop_Level: 0
+ HORSE:
+ LEATHER:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 99.0
+ Drop_Level: 0
+ SADDLE:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 1.0
+ Drop_Level: 0
+ IRON_GOLEM:
+ PUMPKIN:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 3.0
+ Drop_Level: 0
+ IRON_INGOT:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 12.0
+ Drop_Level: 0
+ POPPY:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 85.0
+ Drop_Level: 0
+ MAGMA_CUBE:
+ MAGMA_CREAM:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 100.0
+ Drop_Level: 0
+ MUSHROOM_COW:
+ MILK_BUCKET:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 5.0
+ Drop_Level: 0
+ MUSHROOM_STEW:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 5.0
+ Drop_Level: 0
+ LEATHER:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 30.0
+ Drop_Level: 0
+ BEEF:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 30.0
+ Drop_Level: 0
+ RED_MUSHROOM:
+ Amount: 2
+ XP: 0
+ Drop_Chance: 30.0
+ Drop_Level: 0
+ PIG:
+ PORKCHOP:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 100.0
+ Drop_Level: 0
+ PIG_ZOMBIE:
+ ROTTEN_FLESH:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 50.0
+ Drop_Level: 0
+ GOLD_NUGGET:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 50.0
+ Drop_Level: 0
+ PLAYER:
+ SKELETON_SKULL:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 0.0
+ Drop_Level: 0
+ INVENTORY:
+ Whole_Stacks: false
+ Drop_Chance: 0.0
+ Drop_Level: 0
+ SHEEP:
+ WHITE_WOOL:
+ Amount: 3
+ XP: 0
+ Drop_Chance: 100.0
+ Drop_Level: 0
+ SHULKER:
+ SHULKER_SHELL:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 25.0
+ Drop_Level: 0
+ PURPUR_BLOCK:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 75.0
+ Drop_Level: 0
+ SKELETON:
+ SKELETON_SKULL:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 2.0
+ Drop_Level: 0
+ BONE:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 49.0
+ Drop_Level: 0
+ ARROW:
+ Amount: 2
+ XP: 0
+ Drop_Chance: 49.0
+ Drop_Level: 0
+ SLIME:
+ SLIME_BALL:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 100.0
+ Drop_Level: 0
+ SPIDER:
+ SPIDER_EYE:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 50.0
+ Drop_Level: 0
+ STRING:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 50.0
+ Drop_Level: 0
+ SNOWMAN:
+ PUMPKIN:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 3.0
+ Drop_Level: 0
+ SNOWBALL:
+ Amount: 2
+ XP: 0
+ Drop_Chance: 97.0
+ Drop_Level: 0
+ SQUID:
+ INK_SAC:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 100.0
+ Drop_Level: 0
+ WITCH:
+ SPLASH_POTION|0|INSTANT_HEAL:
+ PotionData:
+ PotionType: INSTANT_HEAL
+ Amount: 1
+ XP: 0
+ Drop_Chance: 1.0
+ Drop_Level: 0
+ SPLASH_POTION|0|FIRE_RESISTANCE:
+ PotionData:
+ PotionType: FIRE_RESISTANCE
+ Amount: 1
+ XP: 0
+ Drop_Chance: 1.0
+ Drop_Level: 0
+ SPLASH_POTION|0|SPEED:
+ PotionData:
+ PotionType: SPEED
+ Amount: 1
+ XP: 0
+ Drop_Chance: 1.0
+ Drop_Level: 0
+ GLASS_BOTTLE:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 7.0
+ Drop_Level: 0
+ GLOWSTONE_DUST:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 15.0
+ Drop_Level: 0
+ GUNPOWDER:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 15.0
+ Drop_Level: 0
+ REDSTONE:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 15.0
+ Drop_Level: 0
+ SPIDER_EYE:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 15.0
+ Drop_Level: 0
+ STICK:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 15.0
+ Drop_Level: 0
+ SUGAR:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 15.0
+ Drop_Level: 0
+ WITHER_SKELETON:
+ WITHER_SKELETON_SKULL:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 2.0
+ Drop_Level: 0
+ BONE:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 49.0
+ Drop_Level: 0
+ COAL:
+ Amount: 2
+ XP: 0
+ Drop_Chance: 49.0
+ Drop_Level: 0
+ ZOMBIE:
+ ZOMBIE_HEAD:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 2.0
+ Drop_Level: 0
+ ROTTEN_FLESH:
+ Amount: 1
+ XP: 0
+ Drop_Chance: 98.0
+ Drop_Level: 0
\ No newline at end of file
diff --git a/src/main/resources/locale/locale_de.properties b/src/main/resources/locale/locale_de.properties
index 23e9fba05..d6525a129 100644
--- a/src/main/resources/locale/locale_de.properties
+++ b/src/main/resources/locale/locale_de.properties
@@ -380,7 +380,7 @@ Fishing.SubSkill.Shake.Stat = Rei\u00DFen Chance
Fishing.SubSkill.TreasureHunter.Description = Angle verschiedene Objekte
Fishing.SubSkill.TreasureHunter.Name = Schatz J\u00E4ger
Fishing.SubSkill.TreasureHunter.Stat = Schatz J\u00E4ger Rang: &a{0}&3/&a{1}
-Fishing.SubSkill.TreasureHunter.Stat.Extra = Drop Rate: &7\u00DCblich: &e{0} &aUn\u00FCblich: &e{1}\r\n&9Selten: &e{2} &dEpisch: &e{3} &6Legend\u00E4r: &e{4} &bSchallplatte: &e{5}
+Fishing.SubSkill.TreasureHunter.Stat.Extra = Drop Rate: &7\u00DCblich: &e{0} &aUn\u00FCblich: &e{1}\r\n&9Selten: &e{2} &dEpisch: &e{3} &6Legend\u00E4r: &e{4} &bMythic: &e{5}
Guides.Acrobatics.Section.0 = &3\u00DCber Akrobatik:\n&eAkrobatik ist die Kunst sich anmutig fortzubewegen.\n&eFall- und Kampfschaden werden reduziert\n\n&3XP GAIN:\n&eErfahrung sammelst du indem du in K\u00E4mpfen\n&eausweichst oder St\u00FCrze aus gro\u00DFen H\u00F6hen \u00FCberlebst.
Guides.Acrobatics.Section.1 = &3Wie funktioniert Abrollen?\n&eAb und zu rollst du beim Fallen ab und der Fallschaden wird\n&ereduziert. Wenn du den Schleichen Knopf w\u00E4hrend dem Fallen\n&eh\u00E4ltst, verdoppelt sich die Chance abzurollen.\n&eIn dem Fall rollst du anmutig ab.\n&eAnmutige Rollen sind wie normale Rollen, nur dass\n&esie \u00F6fter passieren und damit mehr Schutz vor St\u00FCrzen\n&eliefern.
diff --git a/src/main/resources/locale/locale_en_US.properties b/src/main/resources/locale/locale_en_US.properties
index dd25cfb15..48b4b0e95 100644
--- a/src/main/resources/locale/locale_en_US.properties
+++ b/src/main/resources/locale/locale_en_US.properties
@@ -247,7 +247,7 @@ Fishing.Ability.Locked.2=LOCKED UNTIL {0}+ SKILL (MASTER ANGLER)
Fishing.SubSkill.TreasureHunter.Name=Treasure Hunter
Fishing.SubSkill.TreasureHunter.Description=Fish up misc. objects
Fishing.SubSkill.TreasureHunter.Stat=Treasure Hunter Rank: &a{0}&3/&a{1}
-Fishing.SubSkill.TreasureHunter.Stat.Extra=Drop Rate: &7Common: &e{0} &aUncommon: &e{1}\n&9Rare: &e{2} &dEpic: &e{3} &6Legendary: &e{4} &bRecord: &e{5}
+Fishing.SubSkill.TreasureHunter.Stat.Extra=Drop Rate: &7Common: &e{0} &aUncommon: &e{1}\n&9Rare: &e{2} &dEpic: &e{3} &6Legendary: &e{4} &bMythic: &e{5}
Fishing.SubSkill.MagicHunter.Name=Magic Hunter
Fishing.SubSkill.MagicHunter.Description=Find Enchanted Items
Fishing.SubSkill.MagicHunter.Stat=Magic Hunter Chance
@@ -615,6 +615,7 @@ Commands.Cooldowns.Header=&6--= &amcMMO Ability Cooldowns&6 =--
Commands.Cooldowns.Row.N=\ &c{0}&f - &6{1} seconds left
Commands.Cooldowns.Row.Y=\ &b{0}&f - &2Ready!
Commands.Database.CooldownMS=You must wait {0} milliseconds before using this command again.
+Commands.Database.Cooldown=You must wait {0} seconds before using this command again.
Commands.Database.Processing=Your previous command is still being processed. Please wait.
Commands.Disabled=This command is disabled.
Commands.DoesNotExist= &cPlayer does not exist in the database!
diff --git a/src/main/resources/locale/locale_fr.properties b/src/main/resources/locale/locale_fr.properties
index 3ef73beeb..c773ad65c 100644
--- a/src/main/resources/locale/locale_fr.properties
+++ b/src/main/resources/locale/locale_fr.properties
@@ -240,7 +240,7 @@ Fishing.Ability.Locked.2=Bloqu\u00e9 jusqu\'\u00e0 {0}+ niveau(x) (Ma\u00eetre P
Fishing.SubSkill.TreasureHunter.Name=Chasseur de tr\u00e9sors
Fishing.SubSkill.TreasureHunter.Description=Remonte des objets inhabituels
Fishing.SubSkill.TreasureHunter.Stat=Grade de chasseur de tr\u00e9sor: &a{0}&3/&a{1}
-Fishing.SubSkill.TreasureHunter.Stat.Extra=Ratio de drop: &7Commun: &e{0} &aNon-commun: &e{1}\n&9Rare: &e{2} &dEpique: &e{3} &6Legendaire: &e{4} &bRecord: &e{5}
+Fishing.SubSkill.TreasureHunter.Stat.Extra=Ratio de drop: &7Commun: &e{0} &aNon-commun: &e{1}\n&9Rare: &e{2} &dEpique: &e{3} &6Legendaire: &e{4} &bMythic: &e{5}
Fishing.SubSkill.MagicHunter.Name=P\u00eache magique
Fishing.SubSkill.MagicHunter.Description=Remonte des objets magiques
Fishing.SubSkill.MagicHunter.Stat=Chance du chasseur de tr\u00e9sor
diff --git a/src/main/resources/locale/locale_hu_HU.properties b/src/main/resources/locale/locale_hu_HU.properties
index f3d8148f2..71f11514c 100644
--- a/src/main/resources/locale/locale_hu_HU.properties
+++ b/src/main/resources/locale/locale_hu_HU.properties
@@ -240,7 +240,7 @@ Fishing.Ability.Locked.2=LEZ\u00C1RVA {0}+ K\u00C9PESS\u00C9G SZINTIG (MESTER HO
Fishing.SubSkill.TreasureHunter.Name=Kincsvad\u00E1sz
Fishing.SubSkill.TreasureHunter.Description=Furcsa t\u00E1rgyak kihal\u00E1sz\u00E1sa
Fishing.SubSkill.TreasureHunter.Stat=Kincsvad\u00E1sz Szint: &a{0}&3/&a{1}
-Fishing.SubSkill.TreasureHunter.Stat.Extra=T\u00E1rgy Es\u00E9si Es\u00E9ly: &7\u00C1tlagos: &e{0} &aRendk\u00EDv\u00FCli: &e{1}\n&9Ritka: &e{2} &dEpikus: &e{3} &6Legend\u00E1s: &e{4} &bRekord: &e{5}
+Fishing.SubSkill.TreasureHunter.Stat.Extra=T\u00E1rgy Es\u00E9si Es\u00E9ly: &7\u00C1tlagos: &e{0} &aRendk\u00EDv\u00FCli: &e{1}\n&9Ritka: &e{2} &dEpikus: &e{3} &6Legend\u00E1s: &e{4} &bMythic: &e{5}
Fishing.SubSkill.MagicHunter.Name=M\u00E1gikus Vad\u00E1sz
Fishing.SubSkill.MagicHunter.Description=Elvar\u00E1zsolt T\u00E1rgyak Megtal\u00E1l\u00E1sa
Fishing.SubSkill.MagicHunter.Stat=Es\u00E9ly M\u00E1gikus Vad\u00E1szra
diff --git a/src/main/resources/locale/locale_it.properties b/src/main/resources/locale/locale_it.properties
index 10b7d5a97..c5f293733 100644
--- a/src/main/resources/locale/locale_it.properties
+++ b/src/main/resources/locale/locale_it.properties
@@ -247,7 +247,7 @@ Fishing.Ability.Locked.2=BLOCCATO FINO AD ABILIT\u00E0 {0}+ (PESCATORE PROVETTO)
Fishing.SubSkill.TreasureHunter.Name=Cacciatore di Tesori
Fishing.SubSkill.TreasureHunter.Description=Pesca oggetti vari
Fishing.SubSkill.TreasureHunter.Stat=Grado Cacciatore di Tesori: &a{0}&3/&a{1}
-Fishing.SubSkill.TreasureHunter.Stat.Extra=Tasso di Drop: &7Comune: &e{0} &aNon comune: &e{1}\n&9Raro: &e{2} &dEpico: &e{3} &6Leggendario: &e{4} &bRecord: &e{5}
+Fishing.SubSkill.TreasureHunter.Stat.Extra=Tasso di Drop: &7Comune: &e{0} &aNon comune: &e{1}\n&9Raro: &e{2} &dEpico: &e{3} &6Leggendario: &e{4} &bMythic: &e{5}
Fishing.SubSkill.MagicHunter.Name=Cacciatore di Magia
Fishing.SubSkill.MagicHunter.Description=Trova Oggetti Incantati
Fishing.SubSkill.MagicHunter.Stat=Possibilit\u00E0 Cacciatore di Magia
diff --git a/src/main/resources/locale/locale_ja_JP.properties b/src/main/resources/locale/locale_ja_JP.properties
index b3da37ccb..1064c29ac 100644
--- a/src/main/resources/locale/locale_ja_JP.properties
+++ b/src/main/resources/locale/locale_ja_JP.properties
@@ -230,7 +230,7 @@ Fishing.Ability.Locked.2=\u30ed\u30c3\u30af\u3055\u308c\u308b\u307e\u3067 {0}+ \
Fishing.SubSkill.TreasureHunter.Name=\u30c8\u30ec\u30b8\u30e3\u30fc\u30cf\u30f3\u30bf\u30fc
Fishing.SubSkill.TreasureHunter.Description=\u9b5a\u3084\u7269\u3092\u91e3\u308a\u4e0a\u3052\u308b\u3002
Fishing.SubSkill.TreasureHunter.Stat=\u30c8\u30ec\u30b8\u30e3\u30fc\u30cf\u30f3\u30bf\u30fc \u30e9\u30f3\u30af: &a{0}&3/&a{1}
-Fishing.SubSkill.TreasureHunter.Stat.Extra=\u30c9\u30ed\u30c3\u30d7\u7387: &7\u30b3\u30e2\u30f3: &e{0} &a\u30a2\u30f3\u30b3\u30e2\u30f3: &e{1}\n&9\u30ec\u30a2: &e{2} &d\u30a8\u30d4\u30c3\u30af: &e{3} &6\u30ec\u30b8\u30a7\u30f3\u30c0\u30ea\u30fc: &e{4} &b\u30ec\u30b3\u30fc\u30c9: &e{5}
+Fishing.SubSkill.TreasureHunter.Stat.Extra=\u30c9\u30ed\u30c3\u30d7\u7387: &7\u30b3\u30e2\u30f3: &e{0} &a\u30a2\u30f3\u30b3\u30e2\u30f3: &e{1}\n&9\u30ec\u30a2: &e{2} &d\u30a8\u30d4\u30c3\u30af: &e{3} &6\u30ec\u30b8\u30a7\u30f3\u30c0\u30ea\u30fc: &e{4} &bMythic: &e{5}
Fishing.SubSkill.MagicHunter.Name=\u30de\u30b8\u30c3\u30af\u30cf\u30f3\u30bf\u30fc
Fishing.SubSkill.MagicHunter.Description=\u30a8\u30f3\u30c1\u30e3\u30f3\u30c8\u3055\u308c\u305f\u30a2\u30a4\u30c6\u30e0\u3092\u898b\u3064\u3051\u308b\u3002
Fishing.SubSkill.MagicHunter.Stat=\u30de\u30b8\u30c3\u30af\u30cf\u30f3\u30bf\u30fc \u78ba\u7387
diff --git a/src/main/resources/locale/locale_lt_LT.properties b/src/main/resources/locale/locale_lt_LT.properties
index e6eaef4fc..80e26d8e4 100644
--- a/src/main/resources/locale/locale_lt_LT.properties
+++ b/src/main/resources/locale/locale_lt_LT.properties
@@ -240,7 +240,7 @@ Fishing.Ability.Locked.2=LOCKED UNTIL {0}+ SKILL (MASTER ANGLER)
Fishing.SubSkill.TreasureHunter.Name=Lobių ieškotojas
Fishing.SubSkill.TreasureHunter.Description=Fish up misc. objects
Fishing.SubSkill.TreasureHunter.Stat=Lobių ieškotojo rankas: &a{0}&3/&a{1}
-Fishing.SubSkill.TreasureHunter.Stat.Extra=Drop Rate: &7Common: &e{0} &aUncommon: &e{1}\n&9Rare: &e{2} &dEpic: &e{3} &6Legendary: &e{4} &bRecord: &e{5}
+Fishing.SubSkill.TreasureHunter.Stat.Extra=Drop Rate: &7Common: &e{0} &aUncommon: &e{1}\n&9Rare: &e{2} &dEpic: &e{3} &6Legendary: &e{4} &bMythic: &e{5}
Fishing.SubSkill.MagicHunter.Name=Magiškas žvejys
Fishing.SubSkill.MagicHunter.Description=Find Enchanted Items
Fishing.SubSkill.MagicHunter.Stat=Magic Hunter Chance
diff --git a/src/main/resources/locale/locale_ru.properties b/src/main/resources/locale/locale_ru.properties
index be1e8680b..9d36e5e2d 100644
--- a/src/main/resources/locale/locale_ru.properties
+++ b/src/main/resources/locale/locale_ru.properties
@@ -188,7 +188,7 @@ Fishing.Ability.FD=\u0420\u044b\u0431\u0430\u0446\u043a\u0430\u044f \u0414\u0438
Fishing.SubSkill.TreasureHunter.Name=\u041e\u0445\u043e\u0442\u043d\u0438\u043a \u0437\u0430 \u0421\u043e\u043a\u0440\u043e\u0432\u0438\u0449\u0430\u043c\u0438 (\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0435)
Fishing.SubSkill.TreasureHunter.Description=\u041b\u043e\u0432\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u043f\u0440\u0435\u0434\u043c\u0435\u0442\u043e\u0432
Fishing.SubSkill.TreasureHunter.Stat=\u0420\u0430\u043d\u0433 \u041e\u0445\u043e\u0442\u043d\u0438\u043a\u0430 \u0437\u0430 \u0421\u043e\u043a\u0440\u043e\u0432\u0438\u0449\u0430\u043c\u0438: &a{0}&3/&a{1}
-Fishing.SubSkill.TreasureHunter.Stat.Extra=\u0428\u0430\u043d\u0441 \u0434\u0440\u043e\u043f\u0430: &7\u041e\u0431\u044b\u0447\u043d\u044b\u0439: &e{0} &a\u041d\u0435\u043e\u0431\u044b\u0447\u043d\u044b\u0439: &e{1}\n&9\u0420\u0435\u0434\u043a\u0438\u0439: &e{2} &d\u042d\u043f\u0438\u0447\u0435\u0441\u043a\u0438\u0439: &e{3} &6\u041b\u0435\u0433\u0435\u043d\u0434\u0430\u0440\u043d\u044b\u0439: &e{4} &b\u041f\u043b\u0430\u0441\u0442\u0438\u043d\u043a\u0430: &e{5}
+Fishing.SubSkill.TreasureHunter.Stat.Extra=\u0428\u0430\u043d\u0441 \u0434\u0440\u043e\u043f\u0430: &7\u041e\u0431\u044b\u0447\u043d\u044b\u0439: &e{0} &a\u041d\u0435\u043e\u0431\u044b\u0447\u043d\u044b\u0439: &e{1}\n&9\u0420\u0435\u0434\u043a\u0438\u0439: &e{2} &d\u042d\u043f\u0438\u0447\u0435\u0441\u043a\u0438\u0439: &e{3} &6\u041b\u0435\u0433\u0435\u043d\u0434\u0430\u0440\u043d\u044b\u0439: &e{4} &bMythic: &e{5}
Fishing.SubSkill.MagicHunter.Name=\u041e\u0445\u043e\u0442\u043d\u0438\u043a \u0417\u0430 \u041c\u0430\u0433\u0438\u0435\u0439
Fishing.SubSkill.MagicHunter.Description=\u041d\u0430\u0445\u043e\u0434\u043a\u0430 \u0417\u0430\u0447\u0430\u0440\u043e\u0432\u0430\u043d\u044b\u0445 \u041f\u0440\u0435\u0434\u043c\u0435\u0442\u043e\u0432
Fishing.SubSkill.MagicHunter.Stat=\u041e\u0445\u043e\u0442\u043d\u0438\u043a \u0417\u0430 \u041c\u0430\u0433\u0438\u0435\u0439 \u0428\u0430\u043d\u0441
diff --git a/src/main/resources/locale/locale_zh_CN.properties b/src/main/resources/locale/locale_zh_CN.properties
index 5b2dcc042..7557b6cd3 100644
--- a/src/main/resources/locale/locale_zh_CN.properties
+++ b/src/main/resources/locale/locale_zh_CN.properties
@@ -240,7 +240,7 @@ Fishing.Ability.Locked.2=\u9501\u5b9a\u72b6\u6001,\u76f4\u5230 {0}+ \u6280\u80fd
Fishing.SubSkill.TreasureHunter.Name=\u6dd8\u91d1\u8005
Fishing.SubSkill.TreasureHunter.Description=\u9493\u51fa\u5404\u79cd\u5404\u6837\u7684\u7269\u54c1
Fishing.SubSkill.TreasureHunter.Stat=\u6dd8\u91d1\u8005\u7b49\u7ea7: &a{0}&3/&a{1}
-Fishing.SubSkill.TreasureHunter.Stat.Extra=\u6389\u843d\u7387: &7\u4e00\u822c: &e{0} &a\u666e\u901a: &e{1}\n&9\u7a00\u6709: &e{2} &d\u7f55\u89c1: &e{3} &6\u53f2\u8bd7: &e{4} &b\u4f20\u8bf4: &e{5}
+Fishing.SubSkill.TreasureHunter.Stat.Extra=\u6389\u843d\u7387: &7\u4e00\u822c: &e{0} &a\u666e\u901a: &e{1}\n&9\u7a00\u6709: &e{2} &d\u7f55\u89c1: &e{3} &6\u53f2\u8bd7: &e{4} &bMythic: &e{5}
Fishing.SubSkill.MagicHunter.Name=\u9b54\u6cd5\u730e\u4eba
Fishing.SubSkill.MagicHunter.Description=\u627e\u5230\u9644\u9b54\u7269\u54c1
Fishing.SubSkill.MagicHunter.Stat=\u9b54\u6cd5\u730e\u4eba\u51e0\u7387
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 6d148e62f..2d0cddc72 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -19,6 +19,9 @@ load: POSTWORLD
api-version: 1.13
commands:
+# mmodroptreasures:
+# description: An admin command used to spawn treasure drops
+# permission: mcmmo.commands.droptreasures
mmoxpbar:
aliases: xpbarsettings
description: Change XP bar settings
diff --git a/src/main/resources/treasures.yml b/src/main/resources/treasures.yml
index ab8acd47d..c6fd7ae43 100755
--- a/src/main/resources/treasures.yml
+++ b/src/main/resources/treasures.yml
@@ -1,447 +1,4 @@
#
-# Settings for Fishing
-# Last updated on ${project.version}-b${BUILD_NUMBER}
-###
-Fishing:
- LEATHER_BOOTS:
- Amount: 1
- XP: 200
- Rarity: COMMON
- LEATHER_HELMET:
- Amount: 1
- XP: 200
- Rarity: COMMON
- LEATHER_LEGGINGS:
- Amount: 1
- XP: 200
- Rarity: COMMON
- LEATHER_CHESTPLATE:
- Amount: 1
- XP: 200
- Rarity: COMMON
- WOODEN_SWORD:
- Amount: 1
- XP: 200
- Rarity: COMMON
- WOODEN_SHOVEL:
- Amount: 1
- XP: 200
- Rarity: COMMON
- WOODEN_PICKAXE:
- Amount: 1
- XP: 200
- Rarity: COMMON
- WOODEN_AXE:
- Amount: 1
- XP: 200
- Rarity: COMMON
- WOODEN_HOE:
- Amount: 1
- XP: 200
- Rarity: COMMON
- LAPIS_LAZULI:
- Amount: 20
- XP: 200
- Rarity: COMMON
- STONE_SWORD:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- STONE_SHOVEL:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- STONE_PICKAXE:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- STONE_AXE:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- STONE_HOE:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- GOLDEN_SWORD:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- GOLDEN_SHOVEL:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- GOLDEN_PICKAXE:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- GOLDEN_AXE:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- GOLDEN_HOE:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- GOLDEN_BOOTS:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- GOLDEN_HELMET:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- GOLDEN_LEGGINGS:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- GOLDEN_CHESTPLATE:
- Amount: 1
- XP: 200
- Rarity: UNCOMMON
- IRON_INGOT:
- Amount: 5
- XP: 200
- Rarity: UNCOMMON
- GOLD_INGOT:
- Amount: 5
- XP: 200
- Rarity: UNCOMMON
- IRON_SWORD:
- Amount: 1
- XP: 200
- Rarity: RARE
- IRON_SHOVEL:
- Amount: 1
- XP: 200
- Rarity: RARE
- IRON_PICKAXE:
- Amount: 1
- XP: 200
- Rarity: RARE
- IRON_AXE:
- Amount: 1
- XP: 200
- Rarity: RARE
- IRON_HOE:
- Amount: 1
- XP: 200
- Rarity: RARE
- BOW:
- Amount: 1
- XP: 200
- Rarity: RARE
- ENDER_PEARL:
- Amount: 1
- XP: 200
- Rarity: RARE
- BLAZE_ROD:
- Amount: 1
- XP: 200
- Rarity: RARE
- IRON_BOOTS:
- Amount: 1
- XP: 200
- Rarity: EPIC
- IRON_HELMET:
- Amount: 1
- XP: 200
- Rarity: EPIC
- IRON_LEGGINGS:
- Amount: 1
- XP: 200
- Rarity: EPIC
- IRON_CHESTPLATE:
- Amount: 1
- XP: 200
- Rarity: EPIC
- GHAST_TEAR:
- Amount: 1
- XP: 200
- Rarity: EPIC
- DIAMOND:
- Amount: 5
- XP: 200
- Rarity: EPIC
- DIAMOND_SWORD:
- Amount: 1
- XP: 200
- Rarity: LEGENDARY
- DIAMOND_SHOVEL:
- Amount: 1
- XP: 200
- Rarity: LEGENDARY
- DIAMOND_PICKAXE:
- Amount: 1
- XP: 200
- Rarity: LEGENDARY
- DIAMOND_AXE:
- Amount: 1
- XP: 200
- Rarity: LEGENDARY
- DIAMOND_HOE:
- Amount: 1
- XP: 200
- Rarity: LEGENDARY
- DIAMOND_BOOTS:
- Amount: 1
- XP: 200
- Rarity: LEGENDARY
- DIAMOND_HELMET:
- Amount: 1
- XP: 200
- Rarity: LEGENDARY
- DIAMOND_LEGGINGS:
- Amount: 1
- XP: 200
- Rarity: LEGENDARY
- DIAMOND_CHESTPLATE:
- Amount: 1
- XP: 200
- Rarity: LEGENDARY
- MUSIC_DISC_BLOCKS:
- Amount: 1
- XP: 200
- Rarity: RECORD
- MUSIC_DISC_CHIRP:
- Amount: 1
- XP: 200
- Rarity: RECORD
- MUSIC_DISC_FAR:
- Amount: 1
- XP: 200
- Rarity: RECORD
- MUSIC_DISC_MALL:
- Amount: 1
- XP: 200
- Rarity: RECORD
- MUSIC_DISC_MELLOHI:
- Amount: 1
- XP: 200
- Rarity: RECORD
- MUSIC_DISC_STAL:
- Amount: 1
- XP: 200
- Rarity: RECORD
- MUSIC_DISC_STRAD:
- Amount: 1
- XP: 200
- Rarity: RECORD
- MUSIC_DISC_WARD:
- Amount: 1
- XP: 200
- Rarity: RECORD
- MUSIC_DISC_11:
- Amount: 1
- XP: 200
- Rarity: RECORD
- MUSIC_DISC_WAIT:
- Amount: 1
- XP: 200
- Rarity: RECORD
- MUSIC_DISC_13:
- Amount: 1
- XP: 200
- Rarity: RECORD
-#
-# Fishing drop rates
-###
-Item_Drop_Rates:
- Tier_1:
- TRAP: 7.68
- COMMON: 7.50
- UNCOMMON: 1.25
- RARE: 0.25
- EPIC: 0.10
- LEGENDARY: 0.01
- RECORD: 0.01
- Tier_2:
- TRAP: 2.50
- COMMON: 6.50
- UNCOMMON: 1.75
- RARE: 0.75
- EPIC: 0.50
- LEGENDARY: 0.05
- RECORD: 0.01
- Tier_3:
- TRAP: 1.50
- COMMON: 3.50
- UNCOMMON: 2.75
- RARE: 1.25
- EPIC: 1.00
- LEGENDARY: 0.10
- RECORD: 0.01
- Tier_4:
- TRAP: 1.00
- COMMON: 2.00
- UNCOMMON: 3.50
- RARE: 2.25
- EPIC: 1.50
- LEGENDARY: 1.00
- RECORD: 0.01
- Tier_5:
- TRAP: 0.25
- COMMON: 1.50
- UNCOMMON: 3.75
- RARE: 2.50
- EPIC: 2.00
- LEGENDARY: 1.00
- RECORD: 0.01
- Tier_6:
- TRAP: 0.10
- COMMON: 1.00
- UNCOMMON: 3.25
- RARE: 3.75
- EPIC: 2.50
- LEGENDARY: 1.50
- RECORD: 0.05
- Tier_7:
- TRAP: 0.05
- COMMON: 0.25
- UNCOMMON: 2.75
- RARE: 4.00
- EPIC: 5.00
- LEGENDARY: 2.50
- RECORD: 0.10
- Tier_8:
- TRAP: 0.01
- COMMON: 0.10
- UNCOMMON: 1.50
- RARE: 6.00
- EPIC: 7.50
- LEGENDARY: 5.00
- RECORD: 0.25
-#
-# Fishing enchantment drop rates
-###
-Enchantments_Rarity:
- COMMON:
- EFFICIENCY: 1
- UNBREAKING: 1
- FORTUNE: 1
- PROTECTION: 1
- FIRE_PROTECTION: 1
- FEATHER_FALLING: 1
- BLAST_PROTECTION: 1
- PROJECTILE_PROTECTION: 1
- RESPIRATION: 1
- THORNS: 1
- SHARPNESS: 1
- SMITE: 1
- BANE_OF_ARTHROPODS: 1
- POWER: 1
- UNCOMMON:
- EFFICIENCY: 2
- PROTECTION: 2
- FIRE_PROTECTION: 2
- FEATHER_FALLING: 2
- BLAST_PROTECTION: 2
- PROJECTILE_PROTECTION: 2
- SHARPNESS: 2
- SMITE: 2
- BANE_OF_ARTHROPODS: 2
- KNOCKBACK: 1
- LOOTING: 1
- POWER: 2
- PUNCH: 1
- RARE:
- EFFICIENCY: 3
- UNBREAKING: 2
- PROTECTION: 3
- FIRE_PROTECTION: 3
- FEATHER_FALLING: 3
- BLAST_PROTECTION: 3
- PROJECTILE_PROTECTION: 3
- RESPIRATION: 2
- SHARPNESS: 3
- SMITE: 3
- BANE_OF_ARTHROPODS: 3
- FIRE_ASPECT: 1
- LOOTING: 2
- POWER: 3
- EPIC:
- EFFICIENCY: 4
- FORTUNE: 2
- AQUA_AFFINITY: 1
- THORNS: 2
- SHARPNESS: 4
- SMITE: 4
- BANE_OF_ARTHROPODS: 4
- POWER: 4
- FLAME: 1
- LEGENDARY:
- EFFICIENCY: 5
- UNBREAKING: 3
- FORTUNE: 3
- PROTECTION: 4
- FIRE_PROTECTION: 4
- FEATHER_FALLING: 4
- BLAST_PROTECTION: 4
- PROJECTILE_PROTECTION: 4
- RESPIRATION: 3
- AQUA_AFFINITY: 1
- THORNS: 3
- SHARPNESS: 5
- SMITE: 5
- BANE_OF_ARTHROPODS: 5
- KNOCKBACK: 2
- FIRE_ASPECT: 2
- LOOTING: 3
- SILK_TOUCH: 1
- POWER: 5
- PUNCH: 2
- INFINITY: 1
-
-Enchantment_Drop_Rates:
- Tier_1:
- COMMON: 5.00
- UNCOMMON: 1.00
- RARE: 0.10
- EPIC: 0.01
- LEGENDARY: 0.01
- Tier_2:
- COMMON: 7.50
- UNCOMMON: 1.00
- RARE: 0.10
- EPIC: 0.01
- LEGENDARY: 0.01
- Tier_3:
- COMMON: 7.50
- UNCOMMON: 2.50
- RARE: 0.25
- EPIC: 0.10
- LEGENDARY: 0.01
- Tier_4:
- COMMON: 10.0
- UNCOMMON: 2.75
- RARE: 0.50
- EPIC: 0.10
- LEGENDARY: 0.05
- Tier_5:
- COMMON: 10.0
- UNCOMMON: 4.00
- RARE: 0.75
- EPIC: 0.25
- LEGENDARY: 0.10
- Tier_6:
- COMMON: 9.50
- UNCOMMON: 5.50
- RARE: 1.75
- EPIC: 0.50
- LEGENDARY: 0.25
- Tier_7:
- COMMON: 8.50
- UNCOMMON: 7.50
- RARE: 2.75
- EPIC: 0.75
- LEGENDARY: 0.50
- Tier_8:
- COMMON: 7.50
- UNCOMMON: 10.0
- RARE: 5.25
- EPIC: 1.50
- LEGENDARY: 0.75
-#
# Settings for Excavation's Archaeology
# If you are in retro mode, Drop_Level is multiplied by 10.
###
@@ -634,335 +191,4 @@ Hylian_Luck:
XP: 0
Drop_Chance: 100.0
Drop_Level: 0
- Drops_From: [Pots]
-#
-# Settings for Shake
-# If you are in retro mode, Drop_Level is multiplied by 10.
-###
-Shake:
- BLAZE:
- BLAZE_ROD:
- Amount: 1
- XP: 0
- Drop_Chance: 100.0
- Drop_Level: 0
- CAVE_SPIDER:
- SPIDER_EYE:
- Amount: 1
- XP: 0
- Drop_Chance: 49.0
- Drop_Level: 0
- STRING:
- Amount: 1
- XP: 0
- Drop_Chance: 49.0
- Drop_Level: 0
- COBWEB:
- Amount: 1
- XP: 0
- Drop_Chance: 1.0
- Drop_Level: 0
- POTION|0|POISON:
- PotionData:
- PotionType: POISON
- Amount: 1
- XP: 0
- Drop_Chance: 1.0
- Drop_Level: 0
- CHICKEN:
- FEATHER:
- Amount: 1
- XP: 0
- Drop_Chance: 33.3
- Drop_Level: 0
- CHICKEN:
- Amount: 1
- XP: 0
- Drop_Chance: 33.3
- Drop_Level: 0
- EGG:
- Amount: 1
- XP: 0
- Drop_Chance: 33.3
- Drop_Level: 0
- COW:
- MILK_BUCKET:
- Amount: 1
- XP: 0
- Drop_Chance: 2.0
- Drop_Level: 0
- LEATHER:
- Amount: 1
- XP: 0
- Drop_Chance: 49.0
- Drop_Level: 0
- BEEF:
- Amount: 1
- XP: 0
- Drop_Chance: 49.0
- Drop_Level: 0
- CREEPER:
- CREEPER_HEAD:
- Amount: 1
- XP: 0
- Drop_Chance: 1.0
- Drop_Level: 0
- GUNPOWDER:
- Amount: 1
- XP: 0
- Drop_Chance: 99.0
- Drop_Level: 0
- ENDERMAN:
- ENDER_PEARL:
- Amount: 1
- XP: 0
- Drop_Chance: 100.0
- Drop_Level: 0
- GHAST:
- GUNPOWDER:
- Amount: 1
- XP: 0
- Drop_Chance: 50.0
- Drop_Level: 0
- GHAST_TEAR:
- Amount: 1
- XP: 0
- Drop_Chance: 50.0
- Drop_Level: 0
- HORSE:
- LEATHER:
- Amount: 1
- XP: 0
- Drop_Chance: 99.0
- Drop_Level: 0
- SADDLE:
- Amount: 1
- XP: 0
- Drop_Chance: 1.0
- Drop_Level: 0
- IRON_GOLEM:
- PUMPKIN:
- Amount: 1
- XP: 0
- Drop_Chance: 3.0
- Drop_Level: 0
- IRON_INGOT:
- Amount: 1
- XP: 0
- Drop_Chance: 12.0
- Drop_Level: 0
- POPPY:
- Amount: 1
- XP: 0
- Drop_Chance: 85.0
- Drop_Level: 0
- MAGMA_CUBE:
- MAGMA_CREAM:
- Amount: 1
- XP: 0
- Drop_Chance: 100.0
- Drop_Level: 0
- MUSHROOM_COW:
- MILK_BUCKET:
- Amount: 1
- XP: 0
- Drop_Chance: 5.0
- Drop_Level: 0
- MUSHROOM_STEW:
- Amount: 1
- XP: 0
- Drop_Chance: 5.0
- Drop_Level: 0
- LEATHER:
- Amount: 1
- XP: 0
- Drop_Chance: 30.0
- Drop_Level: 0
- BEEF:
- Amount: 1
- XP: 0
- Drop_Chance: 30.0
- Drop_Level: 0
- RED_MUSHROOM:
- Amount: 2
- XP: 0
- Drop_Chance: 30.0
- Drop_Level: 0
- PIG:
- PORKCHOP:
- Amount: 1
- XP: 0
- Drop_Chance: 100.0
- Drop_Level: 0
- PIG_ZOMBIE:
- ROTTEN_FLESH:
- Amount: 1
- XP: 0
- Drop_Chance: 50.0
- Drop_Level: 0
- GOLD_NUGGET:
- Amount: 1
- XP: 0
- Drop_Chance: 50.0
- Drop_Level: 0
- PLAYER:
- SKELETON_SKULL:
- Amount: 1
- XP: 0
- Drop_Chance: 0.0
- Drop_Level: 0
- INVENTORY:
- Whole_Stacks: false
- Drop_Chance: 0.0
- Drop_Level: 0
- SHEEP:
- WHITE_WOOL:
- Amount: 3
- XP: 0
- Drop_Chance: 100.0
- Drop_Level: 0
- SHULKER:
- SHULKER_SHELL:
- Amount: 1
- XP: 0
- Drop_Chance: 25.0
- Drop_Level: 0
- PURPUR_BLOCK:
- Amount: 1
- XP: 0
- Drop_Chance: 75.0
- Drop_Level: 0
- SKELETON:
- SKELETON_SKULL:
- Amount: 1
- XP: 0
- Drop_Chance: 2.0
- Drop_Level: 0
- BONE:
- Amount: 1
- XP: 0
- Drop_Chance: 49.0
- Drop_Level: 0
- ARROW:
- Amount: 2
- XP: 0
- Drop_Chance: 49.0
- Drop_Level: 0
- SLIME:
- SLIME_BALL:
- Amount: 1
- XP: 0
- Drop_Chance: 100.0
- Drop_Level: 0
- SPIDER:
- SPIDER_EYE:
- Amount: 1
- XP: 0
- Drop_Chance: 50.0
- Drop_Level: 0
- STRING:
- Amount: 1
- XP: 0
- Drop_Chance: 50.0
- Drop_Level: 0
- SNOWMAN:
- PUMPKIN:
- Amount: 1
- XP: 0
- Drop_Chance: 3.0
- Drop_Level: 0
- SNOWBALL:
- Amount: 2
- XP: 0
- Drop_Chance: 97.0
- Drop_Level: 0
- SQUID:
- INK_SAC:
- Amount: 1
- XP: 0
- Drop_Chance: 100.0
- Drop_Level: 0
- WITCH:
- SPLASH_POTION|0|INSTANT_HEAL:
- PotionData:
- PotionType: INSTANT_HEAL
- Amount: 1
- XP: 0
- Drop_Chance: 1.0
- Drop_Level: 0
- SPLASH_POTION|0|FIRE_RESISTANCE:
- PotionData:
- PotionType: FIRE_RESISTANCE
- Amount: 1
- XP: 0
- Drop_Chance: 1.0
- Drop_Level: 0
- SPLASH_POTION|0|SPEED:
- PotionData:
- PotionType: SPEED
- Amount: 1
- XP: 0
- Drop_Chance: 1.0
- Drop_Level: 0
- GLASS_BOTTLE:
- Amount: 1
- XP: 0
- Drop_Chance: 7.0
- Drop_Level: 0
- GLOWSTONE_DUST:
- Amount: 1
- XP: 0
- Drop_Chance: 15.0
- Drop_Level: 0
- GUNPOWDER:
- Amount: 1
- XP: 0
- Drop_Chance: 15.0
- Drop_Level: 0
- REDSTONE:
- Amount: 1
- XP: 0
- Drop_Chance: 15.0
- Drop_Level: 0
- SPIDER_EYE:
- Amount: 1
- XP: 0
- Drop_Chance: 15.0
- Drop_Level: 0
- STICK:
- Amount: 1
- XP: 0
- Drop_Chance: 15.0
- Drop_Level: 0
- SUGAR:
- Amount: 1
- XP: 0
- Drop_Chance: 15.0
- Drop_Level: 0
- WITHER_SKELETON:
- WITHER_SKELETON_SKULL:
- Amount: 1
- XP: 0
- Drop_Chance: 2.0
- Drop_Level: 0
- BONE:
- Amount: 1
- XP: 0
- Drop_Chance: 49.0
- Drop_Level: 0
- COAL:
- Amount: 2
- XP: 0
- Drop_Chance: 49.0
- Drop_Level: 0
- ZOMBIE:
- ZOMBIE_HEAD:
- Amount: 1
- XP: 0
- Drop_Chance: 2.0
- Drop_Level: 0
- ROTTEN_FLESH:
- Amount: 1
- XP: 0
- Drop_Chance: 98.0
- Drop_Level: 0
+ Drops_From: [Pots]
\ No newline at end of file
diff --git a/src/test/java/ChunkStoreTest.java b/src/test/java/ChunkStoreTest.java
new file mode 100644
index 000000000..f45b0e0c9
--- /dev/null
+++ b/src/test/java/ChunkStoreTest.java
@@ -0,0 +1,308 @@
+import com.gmail.nossr50.util.blockmeta.*;
+import com.google.common.io.Files;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.junit.*;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.*;
+import java.util.UUID;
+
+import static org.mockito.Mockito.mock;
+
+/**
+ * Could be a lot better. But some tests are better than none! Tests the major things, still kinda unit-testy. Verifies that the serialization isn't completely broken.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(Bukkit.class)
+public class ChunkStoreTest {
+ private static File tempDir;
+ @BeforeClass
+ public static void setUpClass() {
+ tempDir = Files.createTempDir();
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ recursiveDelete(tempDir);
+ }
+
+ private World mockWorld;
+ @Before
+ public void setUpMock(){
+ UUID worldUUID = UUID.randomUUID();
+ mockWorld = mock(World.class);
+ Mockito.when(mockWorld.getUID()).thenReturn(worldUUID);
+ Mockito.when(mockWorld.getMaxHeight()).thenReturn(256);
+ Mockito.when(mockWorld.getWorldFolder()).thenReturn(tempDir);
+ PowerMockito.mockStatic(Bukkit.class);
+ Mockito.when(Bukkit.getWorld(worldUUID)).thenReturn(mockWorld);
+ }
+
+ @Test
+ public void testSetValue() {
+ BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
+ original.setTrue(0, 0, 0);
+ Assert.assertTrue(original.isTrue(0, 0, 0));
+ original.setFalse(0, 0, 0);
+ Assert.assertFalse(original.isTrue(0, 0, 0));
+ }
+
+ @Test
+ public void testIsEmpty() {
+ BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
+ Assert.assertTrue(original.isEmpty());
+ original.setTrue(0, 0, 0);
+ original.setFalse(0, 0, 0);
+ Assert.assertTrue(original.isEmpty());
+ }
+
+ @Test
+ public void testRoundTrip() throws IOException {
+ BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
+ original.setTrue(14, 89, 12);
+ original.setTrue(14, 90, 12);
+ original.setTrue(13, 89, 12);
+ byte[] serializedBytes = serializeChunkstore(original);
+ ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
+ assertEqual(original, deserialized);
+ }
+
+ @Test
+ public void testChunkCoords() throws IOException {
+ for (int x = -96; x < 0; x++) {
+ int cx = x >> 4;
+ int ix = Math.abs(x) % 16;
+ System.out.print(cx + ":" + ix + " ");
+ }
+ }
+
+ @Test
+ public void testUpgrade() throws IOException {
+ LegacyChunkStore original = new LegacyChunkStore(mockWorld, 12, 32);
+ original.setTrue(14, 89, 12);
+ original.setTrue(14, 90, 12);
+ original.setTrue(13, 89, 12);
+ byte[] serializedBytes = serializeChunkstore(original);
+ ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
+ assertEqual(original, deserialized);
+ }
+
+ @Test
+ public void testSimpleRegionRoundtrip() throws IOException {
+ LegacyChunkStore original = new LegacyChunkStore(mockWorld, 12, 12);
+ original.setTrue(14, 89, 12);
+ original.setTrue(14, 90, 12);
+ original.setTrue(13, 89, 12);
+ File file = new File(tempDir, "SimpleRegionRoundTrip.region");
+ McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
+ try (DataOutputStream outputStream = region.getOutputStream(12, 12)){
+ outputStream.write(serializeChunkstore(original));
+ }
+ region.close();
+ region = new McMMOSimpleRegionFile(file, 0, 0);
+ try (DataInputStream is = region.getInputStream(original.getChunkX(), original.getChunkZ()))
+ {
+ Assert.assertNotNull(is);
+ ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(is);
+ assertEqual(original, deserialized);
+ }
+ region.close();
+ file.delete();
+ }
+
+ @Test
+ public void testSimpleRegionRejectsOutOfBounds() {
+ File file = new File(tempDir, "SimpleRegionRoundTrip.region");
+ McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
+ assertThrows(() -> region.getOutputStream(-1, 0), IndexOutOfBoundsException.class);
+ assertThrows(() -> region.getOutputStream(0, -1), IndexOutOfBoundsException.class);
+ assertThrows(() -> region.getOutputStream(32, 0), IndexOutOfBoundsException.class);
+ assertThrows(() -> region.getOutputStream(0, 32), IndexOutOfBoundsException.class);
+ region.close();
+ }
+
+ @Test
+ public void testChunkStoreRejectsOutOfBounds() {
+ ChunkStore chunkStore = new BitSetChunkStore(mockWorld, 0, 0);
+ assertThrows(() -> chunkStore.setTrue(-1, 0, 0), IndexOutOfBoundsException.class);
+ assertThrows(() -> chunkStore.setTrue(0, -1, 0), IndexOutOfBoundsException.class);
+ assertThrows(() -> chunkStore.setTrue(0, 0, -1), IndexOutOfBoundsException.class);
+ assertThrows(() -> chunkStore.setTrue(16, 0, 0), IndexOutOfBoundsException.class);
+ assertThrows(() -> chunkStore.setTrue(0, mockWorld.getMaxHeight(), 0), IndexOutOfBoundsException.class);
+ assertThrows(() -> chunkStore.setTrue(0, 0, 16), IndexOutOfBoundsException.class);
+ }
+
+ @Test
+ public void testRegressionChunkMirrorBug() {
+ ChunkManager chunkManager = new HashChunkManager();
+ chunkManager.setTrue(15,0,15, mockWorld);
+ chunkManager.setFalse(-15, 0, -15, mockWorld);
+ Assert.assertTrue(chunkManager.isTrue(15, 0, 15, mockWorld));
+ }
+
+ private interface Delegate {
+ void run();
+ }
+
+ private void assertThrows(Delegate delegate, Class> clazz) {
+ try {
+ delegate.run();
+ Assert.fail(); // We didn't throw
+ }
+ catch (Throwable t) {
+ Assert.assertTrue(t.getClass().equals(clazz));
+ }
+ }
+
+ private void assertEqual(ChunkStore expected, ChunkStore actual)
+ {
+ Assert.assertEquals(expected.getChunkX(), actual.getChunkX());
+ Assert.assertEquals(expected.getChunkZ(), actual.getChunkZ());
+ Assert.assertEquals(expected.getWorldId(), actual.getWorldId());
+ for (int y = 0; y < 256; y++)
+ for (int x = 0; x < 16; x++)
+ for (int z = 0; z < 16; z++)
+ Assert.assertTrue(expected.isTrue(x, y, z) == actual.isTrue(x, y, z));
+ }
+
+ private static void recursiveDelete(File directoryToBeDeleted) {
+ if (directoryToBeDeleted.isDirectory()) {
+ for (File file : directoryToBeDeleted.listFiles()) {
+ recursiveDelete(file);
+ }
+ }
+ directoryToBeDeleted.delete();
+ }
+
+ private static byte[] serializeChunkstore(ChunkStore chunkStore) throws IOException {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ if (chunkStore instanceof BitSetChunkStore)
+ BitSetChunkStore.Serialization.writeChunkStore(new DataOutputStream(byteArrayOutputStream), chunkStore);
+ else
+ new UnitTestObjectOutputStream(byteArrayOutputStream).writeObject(chunkStore); // Serializes the class as if it were the old PrimitiveChunkStore
+ return byteArrayOutputStream.toByteArray();
+ }
+
+
+ public static class LegacyChunkStore implements ChunkStore, Serializable {
+ private static final long serialVersionUID = -1L;
+ transient private boolean dirty = false;
+ public boolean[][][] store;
+ private static final int CURRENT_VERSION = 7;
+ private static final int MAGIC_NUMBER = 0xEA5EDEBB;
+ private int cx;
+ private int cz;
+ private UUID worldUid;
+
+ public LegacyChunkStore(World world, int cx, int cz) {
+ this.cx = cx;
+ this.cz = cz;
+ this.worldUid = world.getUID();
+ this.store = new boolean[16][16][world.getMaxHeight()];
+ }
+
+ @Override
+ public boolean isDirty() {
+ return dirty;
+ }
+
+ @Override
+ public void setDirty(boolean dirty) {
+ this.dirty = dirty;
+ }
+
+ @Override
+ public int getChunkX() {
+ return cx;
+ }
+
+ @Override
+ public int getChunkZ() {
+ return cz;
+ }
+
+ @Override
+ public UUID getWorldId() {
+ return worldUid;
+ }
+
+ @Override
+ public boolean isTrue(int x, int y, int z) {
+ return store[x][z][y];
+ }
+
+ @Override
+ public void setTrue(int x, int y, int z) {
+ if (y >= store[0][0].length || y < 0)
+ return;
+ store[x][z][y] = true;
+ dirty = true;
+ }
+
+ @Override
+ public void setFalse(int x, int y, int z) {
+ if (y >= store[0][0].length || y < 0)
+ return;
+ store[x][z][y] = false;
+ dirty = true;
+ }
+
+ @Override
+ public void set(int x, int y, int z, boolean value) {
+ if (y >= store[0][0].length || y < 0)
+ return;
+ store[x][z][y] = value;
+ dirty = true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ for (int x = 0; x < 16; x++) {
+ for (int z = 0; z < 16; z++) {
+ for (int y = 0; y < store[0][0].length; y++) {
+ if (store[x][z][y]) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.writeInt(MAGIC_NUMBER);
+ out.writeInt(CURRENT_VERSION);
+
+ out.writeLong(worldUid.getLeastSignificantBits());
+ out.writeLong(worldUid.getMostSignificantBits());
+ out.writeInt(cx);
+ out.writeInt(cz);
+ out.writeObject(store);
+
+ dirty = false;
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private static class UnitTestObjectOutputStream extends ObjectOutputStream {
+ public UnitTestObjectOutputStream(OutputStream outputStream) throws IOException {
+ super(outputStream);
+ }
+
+ @Override
+ public void writeUTF(String str) throws IOException {
+ // Pretend to be the old class
+ if (str.equals(LegacyChunkStore.class.getName()))
+ str = "com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore";
+ super.writeUTF(str);
+ }
+ }
+}