From 1046007b7aa99a92cac7466307f18141313ba4f6 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Wed, 16 Dec 2020 16:41:24 -0800 Subject: [PATCH 01/22] 2.1.163 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e6c2c9896..def6dc78b 100755 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.gmail.nossr50.mcMMO mcMMO - 2.1.163-SNAPSHOT + 2.1.163 mcMMO https://github.com/mcMMO-Dev/mcMMO From 4d386f2e619442bdf432d48967f2b9afe33475b0 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Wed, 16 Dec 2020 16:43:08 -0800 Subject: [PATCH 02/22] update changelog --- Changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.txt b/Changelog.txt index f2a7d129e..27c11d51a 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -5,6 +5,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! From e8d51f42f38834a139a4ba0207482c3c08ce84c5 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Mon, 28 Dec 2020 12:44:21 -0800 Subject: [PATCH 03/22] dev mode --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index def6dc78b..19ed2dbfa 100755 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.gmail.nossr50.mcMMO mcMMO - 2.1.163 + 2.1.164-SNAPSHOT mcMMO https://github.com/mcMMO-Dev/mcMMO From a4ef322fa526d81ec42749cd16a21d46665083c0 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Mon, 28 Dec 2020 12:58:03 -0800 Subject: [PATCH 04/22] Add ExploitFix.PreventPluginNPCInteraction to experience.yml Read the changelog for why --- Changelog.txt | 8 ++++++++ .../nossr50/config/experience/ExperienceConfig.java | 1 + .../com/gmail/nossr50/listeners/EntityListener.java | 11 ++++++----- .../com/gmail/nossr50/listeners/PlayerListener.java | 2 +- src/main/java/com/gmail/nossr50/util/EventUtils.java | 1 + .../com/gmail/nossr50/util/skills/CombatUtils.java | 8 +++++--- src/main/resources/experience.yml | 4 ++++ 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Changelog.txt b/Changelog.txt index 27c11d51a..9d689ced8 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,11 @@ +Version 2.1.164 + New exploit fix setting, when disabled it will allow combat interactions with "NPC" entities from plugins like Citizens + ExploitFix.PreventPluginNPCInteraction Added to experience.yml + + NOTES: + Historically mcMMO has checked an entity for being a NPC (not a Villager) and backed out of any interaction, this was originally done because of NPCs that were meant to be invincible/etc and not give XP + However nowadays what an NPC is used for is pretty loose, mcMMO only has definitions for some NPCs (such as from Citizens) it doesn't know about most NPCs in most plugins unless they identify themselves in a similar way to the predefined parameters + 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 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 bd4e92ad6..2d84ea0c7 100644 --- a/src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java +++ b/src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java @@ -152,6 +152,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/listeners/EntityListener.java b/src/main/java/com/gmail/nossr50/listeners/EntityListener.java index 804c3c73a..73f9b064b 100644 --- a/src/main/java/com/gmail/nossr50/listeners/EntityListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/EntityListener.java @@ -312,7 +312,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; } @@ -322,7 +323,7 @@ public class EntityListener implements Listener { return; } - if (Misc.isNPCEntityExcludingVillagers(attacker)) { + if (ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(attacker)) { return; } @@ -502,7 +503,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; } @@ -649,7 +650,7 @@ public class EntityListener implements Listener { LivingEntity entity = event.getEntity(); - if (Misc.isNPCEntityExcludingVillagers(entity)) { + if (ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(entity)) { return; } @@ -957,7 +958,7 @@ public class EntityListener implements Listener { LivingEntity livingEntity = event.getEntity(); if (!UserManager.hasPlayerDataKey(player) - || 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 c0fb6da54..d7f727278 100644 --- a/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java @@ -895,7 +895,7 @@ public class PlayerListener implements Listener { public void onPlayerChat(AsyncPlayerChatEvent event) { Player player = event.getPlayer(); - if (Misc.isNPCEntityExcludingVillagers(player) || !UserManager.hasPlayerDataKey(player)) { + if ((ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(player)) || !UserManager.hasPlayerDataKey(player)) { return; } diff --git a/src/main/java/com/gmail/nossr50/util/EventUtils.java b/src/main/java/com/gmail/nossr50/util/EventUtils.java index 52a2f989e..a290ab0dd 100644 --- a/src/main/java/com/gmail/nossr50/util/EventUtils.java +++ b/src/main/java/com/gmail/nossr50/util/EventUtils.java @@ -1,6 +1,7 @@ package com.gmail.nossr50.util; import com.gmail.nossr50.config.Config; +import com.gmail.nossr50.config.experience.ExperienceConfig; import com.gmail.nossr50.datatypes.experience.XPGainReason; import com.gmail.nossr50.datatypes.experience.XPGainSource; import com.gmail.nossr50.datatypes.party.Party; 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 616f0e562..1aaf6395f 100644 --- a/src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java +++ b/src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java @@ -321,8 +321,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; @@ -692,7 +694,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 fd0644d93..4cbbfd080 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 From 31134b38de0efbbc2169f7bf7742116c8951d35e Mon Sep 17 00:00:00 2001 From: nossr50 Date: Mon, 28 Dec 2020 13:00:47 -0800 Subject: [PATCH 05/22] updated changelog --- Changelog.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Changelog.txt b/Changelog.txt index 9d689ced8..ff8d8429e 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -3,8 +3,10 @@ Version 2.1.164 ExploitFix.PreventPluginNPCInteraction Added to experience.yml NOTES: - Historically mcMMO has checked an entity for being a NPC (not a Villager) and backed out of any interaction, this was originally done because of NPCs that were meant to be invincible/etc and not give XP - However nowadays what an NPC is used for is pretty loose, mcMMO only has definitions for some NPCs (such as from Citizens) it doesn't know about most NPCs in most plugins unless they identify themselves in a similar way to the predefined parameters + 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 Version 2.1.163 Fixed the translate URL pointing to the wrong place (thanks chew) From 652a9519c1952bd3c78541a0195de39f7a5ce6ac Mon Sep 17 00:00:00 2001 From: nossr50 Date: Mon, 28 Dec 2020 16:42:00 -0800 Subject: [PATCH 06/22] Move fishing treasues to new file fishing_treasures.yml, replace Records rarity with Mythic, and allow for Enchanted_Book in the treasures list with new optional whitelist/blacklist parameters read the changelog for information about this --- Changelog.txt | 16 +- .../commands/admin/DropTreasureCommand.java | 57 ++ .../commands/skills/FishingCommand.java | 8 +- .../treasure/FishingTreasureConfig.java | 368 ++++++++ .../config/treasure/TreasureConfig.java | 102 +-- .../nossr50/datatypes/player/McMMOPlayer.java | 1 - .../treasure/EnchantmentWrapper.java | 44 + .../treasure/FishingTreasureBook.java | 76 ++ .../nossr50/datatypes/treasure/Rarity.java | 6 +- src/main/java/com/gmail/nossr50/mcMMO.java | 2 + .../gmail/nossr50/skills/fishing/Fishing.java | 6 +- .../skills/fishing/FishingManager.java | 127 ++- .../com/gmail/nossr50/util/EventUtils.java | 1 - .../commands/CommandRegistrationManager.java | 9 + src/main/resources/fishing_treasures.yml | 828 ++++++++++++++++++ .../resources/locale/locale_de.properties | 2 +- .../resources/locale/locale_en_US.properties | 2 +- .../resources/locale/locale_fr.properties | 2 +- .../resources/locale/locale_hu_HU.properties | 2 +- .../resources/locale/locale_it.properties | 2 +- .../resources/locale/locale_ja_JP.properties | 2 +- .../resources/locale/locale_lt_LT.properties | 2 +- .../resources/locale/locale_ru.properties | 2 +- .../resources/locale/locale_zh_CN.properties | 2 +- src/main/resources/plugin.yml | 3 + src/main/resources/treasures.yml | 776 +--------------- 26 files changed, 1512 insertions(+), 936 deletions(-) create mode 100644 src/main/java/com/gmail/nossr50/commands/admin/DropTreasureCommand.java create mode 100755 src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java create mode 100644 src/main/java/com/gmail/nossr50/datatypes/treasure/EnchantmentWrapper.java create mode 100644 src/main/java/com/gmail/nossr50/datatypes/treasure/FishingTreasureBook.java create mode 100644 src/main/resources/fishing_treasures.yml diff --git a/Changelog.txt b/Changelog.txt index ff8d8429e..29d9eeb91 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,12 +1,26 @@ Version 2.1.164 - New exploit fix setting, when disabled it will allow combat interactions with "NPC" entities from plugins like Citizens + The Rarity known as Records has been renamed to Mythic + Fishing treasures have been moved from treasures.yml -> fishing_treasures.yml, you'll have to copy over your changes and be aware that Records rarity is now Mythic + Mythic rarity (formerly known as Records) now allows for Enchantments to be applied to drops (See Notes) + Added all Netherite gear to the Mythic tier in fishing_treasures.yml + Added Enchanted Books to fishing loot tables + 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 entry in fishing_treasures.yml, see notes for an example NOTES: + The rarity known as Records was odd to me, if you got the best possible drop it was always going to be a Records drop, 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. 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 + 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 + Version 2.1.163 Fixed the translate URL pointing to the wrong place (thanks chew) 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 0f34e84b1..b6f1aef42 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/FishingCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/FishingCommand.java @@ -29,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; @@ -60,13 +60,13 @@ public class FishingCommand extends SkillCommand { 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); + mythicTreasure = percent.format(TreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.MYTHIC) / 100.0); // Magic hunter drop rates double totalEnchantChance = 0; for (Rarity rarity : Rarity.values()) { - if (rarity != Rarity.RECORD) { + if (rarity != Rarity.MYTHIC) { totalEnchantChance += TreasureConfig.getInstance().getEnchantmentDropRate(lootTier, rarity); } } @@ -145,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/treasure/FishingTreasureConfig.java b/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java new file mode 100755 index 000000000..f7f0a9ec2 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java @@ -0,0 +1,368 @@ +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(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 + 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); + } + + 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("Invalid material: " + materialName); + } + + if (amount <= 0) { + amount = 1; + } + + if (material != null && 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) { + rarity = Rarity.getRarity(config.getString(type + "." + treasureName + ".Rarity")); + + if (rarity == null) { + reason.add("Invalid Rarity for item: " + treasureName); + } + } + + /* + * 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 Treasures.yml has changed"); + } else { + item = new ItemStack(mat, amount, data); + PotionMeta itemMeta = (PotionMeta) item.getItemMeta(); + + 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 != null) { + 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); + //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) { + 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); + } + } + } + } + + 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(List enchantListStr, 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, 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/config/treasure/TreasureConfig.java b/src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java index be050d298..a91968acf 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,14 @@ 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.datatypes.treasure.Rarity; 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 +22,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(); } @@ -51,29 +47,18 @@ public class TreasureConfig extends ConfigLoader { // 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!"); } @@ -92,7 +77,6 @@ public class TreasureConfig extends ConfigLoader { loadTreasures("Fishing"); loadTreasures("Excavation"); loadTreasures("Hylian_Luck"); - loadEnchantments(); for (EntityType entity : EntityType.values()) { if (entity.isAlive()) { @@ -102,8 +86,6 @@ public class TreasureConfig extends ConfigLoader { } 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 +95,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 +106,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 +143,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 +151,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 +200,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"); @@ -309,36 +253,6 @@ public class TreasureConfig extends ConfigLoader { 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"); } diff --git a/src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java b/src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java index 7ae9861a9..4d94f0d4b 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java +++ b/src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java @@ -56,7 +56,6 @@ import com.gmail.nossr50.util.sounds.SoundManager; import com.gmail.nossr50.util.sounds.SoundType; import net.kyori.adventure.identity.Identified; import net.kyori.adventure.identity.Identity; -import org.apache.commons.lang.Validate; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.block.Block; 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..4e65cd9c7 --- /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..ff76b670d 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,16 @@ package com.gmail.nossr50.datatypes.treasure; +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) { try { return valueOf(string); } diff --git a/src/main/java/com/gmail/nossr50/mcMMO.java b/src/main/java/com/gmail/nossr50/mcMMO.java index 15314c693..b98ad9f9f 100644 --- a/src/main/java/com/gmail/nossr50/mcMMO.java +++ b/src/main/java/com/gmail/nossr50/mcMMO.java @@ -11,6 +11,7 @@ import com.gmail.nossr50.config.mods.ToolConfigManager; 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; @@ -517,6 +518,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 9d76097b6..59789c29d 100644 --- a/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java +++ b/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java @@ -4,16 +4,14 @@ 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.FishingTreasureConfig; import com.gmail.nossr50.config.treasure.TreasureConfig; import com.gmail.nossr50.datatypes.experience.XPGainReason; import com.gmail.nossr50.datatypes.interactions.NotificationType; import com.gmail.nossr50.datatypes.player.McMMOPlayer; 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; @@ -40,10 +38,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.*; @@ -370,9 +370,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); @@ -383,12 +381,14 @@ public class FishingManager extends SkillManager { * * @param fishingCatch The {@link Item} initially caught */ - public void handleFishing(Item fishingCatch) { + public void handleFishing(@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(); @@ -396,49 +396,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+1); + } + /** * Handle the vanilla XP boost for Fishing * @@ -548,7 +591,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; @@ -569,12 +612,8 @@ public class FishingManager extends SkillManager { double dropRate = TreasureConfig.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; @@ -612,19 +651,14 @@ 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); @@ -634,7 +668,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/EventUtils.java b/src/main/java/com/gmail/nossr50/util/EventUtils.java index a290ab0dd..52a2f989e 100644 --- a/src/main/java/com/gmail/nossr50/util/EventUtils.java +++ b/src/main/java/com/gmail/nossr50/util/EventUtils.java @@ -1,7 +1,6 @@ package com.gmail.nossr50.util; import com.gmail.nossr50.config.Config; -import com.gmail.nossr50.config.experience.ExperienceConfig; import com.gmail.nossr50.datatypes.experience.XPGainReason; import com.gmail.nossr50.datatypes.experience.XPGainSource; import com.gmail.nossr50.datatypes.party.Party; 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 e6b50d469..215c160d8 100644 --- a/src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java +++ b/src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java @@ -142,6 +142,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/resources/fishing_treasures.yml b/src/main/resources/fishing_treasures.yml new file mode 100644 index 000000000..36948f73b --- /dev/null +++ b/src/main/resources/fishing_treasures.yml @@ -0,0 +1,828 @@ +# +# 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 + 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: 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: MYTHIC + Enchantments_Whitelist: + - Fortune + - Protection + NETHERITE_SCRAP: + Amount: 1 + XP: 400 + Rarity: MYTHIC +# +# 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 + MYTHIC: 0.01 + Tier_2: + TRAP: 2.50 + COMMON: 6.50 + UNCOMMON: 1.75 + RARE: 0.75 + EPIC: 0.50 + LEGENDARY: 0.05 + MYTHIC: 0.01 + Tier_3: + TRAP: 1.50 + COMMON: 3.50 + UNCOMMON: 2.75 + RARE: 1.25 + EPIC: 1.00 + LEGENDARY: 0.10 + MYTHIC: 0.01 + Tier_4: + TRAP: 1.00 + COMMON: 2.00 + UNCOMMON: 3.50 + RARE: 2.25 + EPIC: 1.50 + LEGENDARY: 1.00 + MYTHIC: 0.01 + Tier_5: + TRAP: 0.25 + COMMON: 1.50 + UNCOMMON: 3.75 + RARE: 2.50 + EPIC: 2.00 + LEGENDARY: 1.00 + MYTHIC: 0.01 + Tier_6: + TRAP: 0.10 + COMMON: 1.00 + UNCOMMON: 3.25 + RARE: 3.75 + EPIC: 2.50 + LEGENDARY: 1.50 + MYTHIC: 0.05 + Tier_7: + TRAP: 0.05 + COMMON: 0.25 + UNCOMMON: 2.75 + RARE: 4.00 + EPIC: 5.00 + LEGENDARY: 2.50 + MYTHIC: 0.10 + Tier_8: + TRAP: 0.01 + 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 +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 585d9a5f9..e60f89a86 100644 --- a/src/main/resources/locale/locale_en_US.properties +++ b/src/main/resources/locale/locale_en_US.properties @@ -241,7 +241,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 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 9bbf76aa9..f3dcec5c1 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 077a628c8..24de2fb86 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. ### @@ -630,335 +187,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 From 14a6e5c6034257ecf026c3a752adbaee725c0c51 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Mon, 28 Dec 2020 16:45:16 -0800 Subject: [PATCH 07/22] Comment out whitelist in fishing_treasures.yml --- src/main/resources/fishing_treasures.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/resources/fishing_treasures.yml b/src/main/resources/fishing_treasures.yml index 36948f73b..9bdca543a 100644 --- a/src/main/resources/fishing_treasures.yml +++ b/src/main/resources/fishing_treasures.yml @@ -283,9 +283,10 @@ Fishing: Amount: 1 XP: 400 Rarity: MYTHIC - Enchantments_Whitelist: - - Fortune - - Protection +# Uncomment the below 3 lines to use the Whitelist, Alternatively rename it Enchantments_Blacklist to use the blacklist +# Enchantments_Whitelist: +# - Fortune +# - Protection NETHERITE_SCRAP: Amount: 1 XP: 400 From 31e0f1e4da52c8b80532f036e58524a6c0510faf Mon Sep 17 00:00:00 2001 From: nossr50 Date: Mon, 28 Dec 2020 16:47:33 -0800 Subject: [PATCH 08/22] less verbose enchantment book log --- .../gmail/nossr50/datatypes/treasure/FishingTreasureBook.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/gmail/nossr50/datatypes/treasure/FishingTreasureBook.java b/src/main/java/com/gmail/nossr50/datatypes/treasure/FishingTreasureBook.java index 4e65cd9c7..8f89f7484 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/treasure/FishingTreasureBook.java +++ b/src/main/java/com/gmail/nossr50/datatypes/treasure/FishingTreasureBook.java @@ -60,7 +60,7 @@ public class FishingTreasureBook extends FishingTreasure { int enchantLevel = i+1; EnchantmentWrapper enchantmentWrapper = new EnchantmentWrapper(enchantment, enchantLevel); legalEnchantments.add(enchantmentWrapper); - mcMMO.p.getLogger().info("Fishing treasure book enchantment added: " + enchantmentWrapper); +// mcMMO.p.getLogger().info("Fishing treasure book enchantment added: " + enchantmentWrapper); } } From 8b62c0b693e9871008e38c310a70cd3ffbcbf6a1 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Mon, 28 Dec 2020 16:58:57 -0800 Subject: [PATCH 09/22] Better message for items not found in current version of MC from Fishing Config --- Changelog.txt | 15 ++++++++------- .../config/treasure/FishingTreasureConfig.java | 3 ++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Changelog.txt b/Changelog.txt index 29d9eeb91..8b93415d7 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -3,24 +3,25 @@ Version 2.1.164 Fishing treasures have been moved from treasures.yml -> fishing_treasures.yml, you'll have to copy over your changes and be aware that Records rarity is now Mythic Mythic rarity (formerly known as Records) now allows for Enchantments to be applied to drops (See Notes) Added all Netherite gear to the Mythic tier in fishing_treasures.yml - Added Enchanted Books to fishing loot tables + Added Enchanted Books to fishing_treasures.yml as Mythic rarity (can drop with any legal enchantment - 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 entry in fishing_treasures.yml, see notes for an example + 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: - The rarity known as Records was odd to me, if you got the best possible drop it was always going to be a Records drop, 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. + The rarity 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. - 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 + 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 + 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 Version 2.1.163 Fixed the translate URL pointing to the wrong place (thanks chew) diff --git a/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java b/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java index f7f0a9ec2..d71ef7247 100755 --- a/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java +++ b/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java @@ -137,7 +137,8 @@ public class FishingTreasureConfig extends ConfigLoader { short data = (treasureInfo.length == 2) ? Short.parseShort(treasureInfo[1]) : (short) config.getInt(type + "." + treasureName + ".Data"); if (material == null) { - reason.add("Invalid material: " + materialName); + reason.add("Cannot find matching item type in this version of MC, skipping - " + materialName); + continue; } if (amount <= 0) { From 2162c81b21e7665946ab50bab1e903f3c8158449 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Mon, 28 Dec 2020 17:00:50 -0800 Subject: [PATCH 10/22] Add semi-important note to changelog --- Changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.txt b/Changelog.txt index 8b93415d7..000604dcf 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -13,6 +13,7 @@ Version 2.1.164 The rarity 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. + 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 From ac31a3dc0eb9c0ee9d1ad2b7135f0cefd13a8aca Mon Sep 17 00:00:00 2001 From: nossr50 Date: Mon, 28 Dec 2020 17:37:14 -0800 Subject: [PATCH 11/22] Fixing start up errors when loading treasures.yml --- .../config/treasure/TreasureConfig.java | 34 ------------------- .../skills/fishing/FishingManager.java | 4 +-- 2 files changed, 2 insertions(+), 36 deletions(-) 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 a91968acf..a4dbd6b03 100755 --- a/src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java +++ b/src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java @@ -46,23 +46,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 totalItemDropRate = 0; - - for (Rarity rarity : Rarity.values()) { - double itemDropRate = config.getDouble("Item_Drop_Rates." + tier + "." + rarity.toString()); - - 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!"); - } - - totalItemDropRate += itemDropRate; - } - - 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); } @@ -74,7 +57,6 @@ public class TreasureConfig extends ConfigLoader { return; } - loadTreasures("Fishing"); loadTreasures("Excavation"); loadTreasures("Hylian_Luck"); @@ -253,22 +235,6 @@ public class TreasureConfig extends ConfigLoader { hylianMap.get(dropper).add(treasure); } - 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()); } 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 59789c29d..f2bba2bd5 100644 --- a/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java +++ b/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java @@ -531,7 +531,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); @@ -541,7 +541,7 @@ public class FishingManager extends SkillManager { break; } - if (TreasureConfig.getInstance().getInventoryStealStacks()) { + if (FishingTreasureConfig.getInstance().getInventoryStealStacks()) { inventory.setItem(slot, null); } else { From bdee0278a5517f6d8c762f1668837b259c4f25f8 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Tue, 29 Dec 2020 12:03:57 -0800 Subject: [PATCH 12/22] Fix Fishing using values from the wrong config --- .../nossr50/commands/skills/FishingCommand.java | 16 ++++++++-------- .../nossr50/config/treasure/TreasureConfig.java | 9 --------- .../nossr50/skills/fishing/FishingManager.java | 5 ++--- 3 files changed, 10 insertions(+), 20 deletions(-) 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 b6f1aef42..f22f8399e 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,6 @@ 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; @@ -55,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); - mythicTreasure = percent.format(TreasureConfig.getInstance().getItemDropRate(lootTier, Rarity.MYTHIC) / 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.MYTHIC) { - totalEnchantChance += TreasureConfig.getInstance().getEnchantmentDropRate(lootTier, rarity); + totalEnchantChance += FishingTreasureConfig.getInstance().getEnchantmentDropRate(lootTier, rarity); } } 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 a4dbd6b03..eac722de1 100755 --- a/src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java +++ b/src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java @@ -3,7 +3,6 @@ package com.gmail.nossr50.config.treasure; import com.gmail.nossr50.config.ConfigLoader; import com.gmail.nossr50.datatypes.treasure.ExcavationTreasure; import com.gmail.nossr50.datatypes.treasure.HylianTreasure; -import com.gmail.nossr50.datatypes.treasure.Rarity; import com.gmail.nossr50.util.text.StringUtils; import org.bukkit.ChatColor; import org.bukkit.Material; @@ -234,12 +233,4 @@ public class TreasureConfig extends ConfigLoader { hylianMap.put(dropper, new ArrayList<>()); hylianMap.get(dropper).add(treasure); } - - 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/skills/fishing/FishingManager.java b/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java index f2bba2bd5..1211966c3 100644 --- a/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java +++ b/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java @@ -5,7 +5,6 @@ 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.FishingTreasureConfig; -import com.gmail.nossr50.config.treasure.TreasureConfig; import com.gmail.nossr50.datatypes.experience.XPGainReason; import com.gmail.nossr50.datatypes.interactions.NotificationType; import com.gmail.nossr50.datatypes.player.McMMOPlayer; @@ -609,7 +608,7 @@ 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) { @@ -660,7 +659,7 @@ public class FishingManager extends SkillManager { for (Rarity rarity : Rarity.values()) { - 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 From 0f4455d5a8d3841a4d5b1591e4fb156dd3b7d5ac Mon Sep 17 00:00:00 2001 From: nossr50 Date: Tue, 29 Dec 2020 12:12:03 -0800 Subject: [PATCH 13/22] remove unused settings from fishing treasures and add enchantment section for mythic --- src/main/resources/fishing_treasures.yml | 30 +++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/resources/fishing_treasures.yml b/src/main/resources/fishing_treasures.yml index 9bdca543a..0994aeb79 100644 --- a/src/main/resources/fishing_treasures.yml +++ b/src/main/resources/fishing_treasures.yml @@ -296,7 +296,6 @@ Fishing: ### Item_Drop_Rates: Tier_1: - TRAP: 7.68 COMMON: 7.50 UNCOMMON: 1.25 RARE: 0.25 @@ -304,7 +303,6 @@ Item_Drop_Rates: LEGENDARY: 0.01 MYTHIC: 0.01 Tier_2: - TRAP: 2.50 COMMON: 6.50 UNCOMMON: 1.75 RARE: 0.75 @@ -312,7 +310,6 @@ Item_Drop_Rates: LEGENDARY: 0.05 MYTHIC: 0.01 Tier_3: - TRAP: 1.50 COMMON: 3.50 UNCOMMON: 2.75 RARE: 1.25 @@ -320,7 +317,6 @@ Item_Drop_Rates: LEGENDARY: 0.10 MYTHIC: 0.01 Tier_4: - TRAP: 1.00 COMMON: 2.00 UNCOMMON: 3.50 RARE: 2.25 @@ -328,7 +324,6 @@ Item_Drop_Rates: LEGENDARY: 1.00 MYTHIC: 0.01 Tier_5: - TRAP: 0.25 COMMON: 1.50 UNCOMMON: 3.75 RARE: 2.50 @@ -336,7 +331,6 @@ Item_Drop_Rates: LEGENDARY: 1.00 MYTHIC: 0.01 Tier_6: - TRAP: 0.10 COMMON: 1.00 UNCOMMON: 3.25 RARE: 3.75 @@ -344,7 +338,6 @@ Item_Drop_Rates: LEGENDARY: 1.50 MYTHIC: 0.05 Tier_7: - TRAP: 0.05 COMMON: 0.25 UNCOMMON: 2.75 RARE: 4.00 @@ -352,7 +345,6 @@ Item_Drop_Rates: LEGENDARY: 2.50 MYTHIC: 0.10 Tier_8: - TRAP: 0.01 COMMON: 0.10 UNCOMMON: 1.50 RARE: 6.00 @@ -439,6 +431,28 @@ Enchantments_Rarity: 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 From d9f98b1aa9b2ff97fd840ba51b92e9f84a771b81 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Tue, 29 Dec 2020 12:39:06 -0800 Subject: [PATCH 14/22] Fix a bug where books weren't registered + tweaked how treasures were loaded --- .../treasure/FishingTreasureConfig.java | 119 ++++++++++-------- .../nossr50/datatypes/treasure/Rarity.java | 5 + .../nossr50/listeners/PlayerListener.java | 2 +- .../skills/fishing/FishingManager.java | 2 +- 4 files changed, 74 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java b/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java index d71ef7247..69d835541 100755 --- a/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java +++ b/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java @@ -15,6 +15,7 @@ import org.bukkit.inventory.meta.PotionMeta; import org.bukkit.potion.PotionData; import org.bukkit.potion.PotionType; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.*; @@ -94,7 +95,7 @@ public class FishingTreasureConfig extends ConfigLoader { } } - private void loadTreasures(String type) { + private void loadTreasures(@NotNull String type) { boolean isFishing = type.equals("Fishing"); boolean isShake = type.contains("Shake"); @@ -125,9 +126,7 @@ public class FishingTreasureConfig extends ConfigLoader { 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())); + addShakeTreasure(new ShakeTreasure(new ItemStack(Material.BEDROCK, 1, (byte) 0), 1, getInventoryStealDropChance(), getInventoryStealDropLevel()), EntityType.PLAYER); continue; } else { material = Material.matchMaterial(materialName); @@ -145,7 +144,7 @@ public class FishingTreasureConfig extends ConfigLoader { amount = 1; } - if (material != null && material.isBlock() && (data > 127 || data < -128)) { + if (material.isBlock() && (data > 127 || data < -128)) { reason.add("Data of " + treasureName + " is invalid! " + data); } @@ -175,10 +174,14 @@ public class FishingTreasureConfig extends ConfigLoader { Rarity rarity = null; if (isFishing) { - rarity = Rarity.getRarity(config.getString(type + "." + treasureName + ".Rarity")); + String rarityStr = config.getString(type + "." + treasureName + ".Rarity"); - if (rarity == null) { - reason.add("Invalid Rarity for item: " + treasureName); + 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; } } @@ -202,6 +205,11 @@ public class FishingTreasureConfig extends ConfigLoader { 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")); @@ -225,67 +233,74 @@ public class FishingTreasureConfig extends ConfigLoader { } item.setItemMeta(itemMeta); } - } else if (material != null) { - if(material == Material.ENCHANTED_BOOK) { - //If any whitelisted enchants exist we use whitelist-based matching - item = new ItemStack(material, 1); - ItemMeta itemMeta = item.getItemMeta(); + } 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"); + List allowedEnchantsList = config.getStringList(type + "." + treasureName + ".Enchantments_Whitelist"); + List disallowedEnchantsList = config.getStringList(type + "." + treasureName + ".Enchantments_Blacklist"); - Set blackListedEnchants = new HashSet<>(); - Set whiteListedEnchants = new HashSet<>(); + Set blackListedEnchants = new HashSet<>(); + Set whiteListedEnchants = new HashSet<>(); - matchAndFillSet(disallowedEnchantsList, blackListedEnchants); - matchAndFillSet(allowedEnchantsList, whiteListedEnchants); + 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); - //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 (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) { - fishingRewards.get(rarity).add(new FishingTreasure(item, xp)); + addFishingTreasure(rarity, 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); + 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"); } @@ -296,7 +311,7 @@ public class FishingTreasureConfig extends ConfigLoader { * @param enchantListStr the users string list of enchantments * @param permissiveList the permissive list of enchantments */ - private void matchAndFillSet(List enchantListStr, Set permissiveList) { + private void matchAndFillSet(@NotNull List enchantListStr, @NotNull Set permissiveList) { if(enchantListStr.isEmpty()) { return; } @@ -359,11 +374,11 @@ public class FishingTreasureConfig extends ConfigLoader { return config.getInt("Shake.PLAYER.INVENTORY.Drop_Level"); } - public double getItemDropRate(int tier, Rarity rarity) { + public double getItemDropRate(int tier, @NotNull Rarity rarity) { return config.getDouble("Item_Drop_Rates.Tier_" + tier + "." + rarity.toString()); } - public double getEnchantmentDropRate(int tier, Rarity rarity) { + 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/datatypes/treasure/Rarity.java b/src/main/java/com/gmail/nossr50/datatypes/treasure/Rarity.java index ff76b670d..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,5 +1,6 @@ package com.gmail.nossr50.datatypes.treasure; +import com.gmail.nossr50.mcMMO; import org.jetbrains.annotations.NotNull; public enum Rarity { @@ -11,6 +12,10 @@ public enum Rarity { COMMON; 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/PlayerListener.java b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java index d7f727278..e2981c32c 100644 --- a/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java @@ -419,7 +419,7 @@ public class PlayerListener implements Listener { } } - fishingManager.handleFishing((Item) caught); + fishingManager.processFishing((Item) caught); fishingManager.setFishingTarget(); } return; 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 1211966c3..52c4163ab 100644 --- a/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java +++ b/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java @@ -380,7 +380,7 @@ public class FishingManager extends SkillManager { * * @param fishingCatch The {@link Item} initially caught */ - public void handleFishing(@NotNull Item fishingCatch) { + public void processFishing(@NotNull Item fishingCatch) { this.fishingCatch = fishingCatch; int fishXp = ExperienceConfig.getInstance().getXp(PrimarySkillType.FISHING, fishingCatch.getItemStack().getType()); int treasureXp = 0; From 3c9c8556dde9ec6fefafd889ce182b43490c60a8 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Tue, 29 Dec 2020 12:58:39 -0800 Subject: [PATCH 15/22] Allow vanilla block interaction with sneak if the block is set to be a mcMMO repair/salvage anvil --- Changelog.txt | 2 ++ src/main/java/com/gmail/nossr50/listeners/PlayerListener.java | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Changelog.txt b/Changelog.txt index 000604dcf..c70b7c4f9 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,4 +1,5 @@ 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, you'll have to copy over your changes and be aware that Records rarity is now Mythic Mythic rarity (formerly known as Records) now allows for Enchantments to be applied to drops (See Notes) @@ -10,6 +11,7 @@ Version 2.1.164 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: + 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. The rarity 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. diff --git a/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java index e2981c32c..1339a8850 100644 --- a/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java @@ -616,8 +616,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); } } From bab13f32e75d54cf192134f5bb97ce0c203a7348 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Tue, 29 Dec 2020 13:15:15 -0800 Subject: [PATCH 16/22] update notes --- Changelog.txt | 2 +- src/main/java/com/gmail/nossr50/config/Config.java | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Changelog.txt b/Changelog.txt index c70b7c4f9..6058013d5 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -11,7 +11,7 @@ Version 2.1.164 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: - 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. + 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). The rarity 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. diff --git a/src/main/java/com/gmail/nossr50/config/Config.java b/src/main/java/com/gmail/nossr50/config/Config.java index d1368dc9b..9747d3acb 100644 --- a/src/main/java/com/gmail/nossr50/config/Config.java +++ b/src/main/java/com/gmail/nossr50/config/Config.java @@ -9,6 +9,7 @@ import com.gmail.nossr50.util.text.StringUtils; import org.bukkit.Material; import org.bukkit.block.data.BlockData; import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -509,14 +510,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 */ From 8ff345af38ec712c5b4f01e4017af2a82a93ddb9 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Tue, 29 Dec 2020 13:20:10 -0800 Subject: [PATCH 17/22] fix array out of bounds --- .../java/com/gmail/nossr50/skills/fishing/FishingManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 52c4163ab..3e0ec850e 100644 --- a/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java +++ b/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java @@ -478,7 +478,7 @@ public class FishingManager extends SkillManager { Collections.shuffle(enchantmentWrappers, Misc.getRandom()); int randomIndex = Misc.getRandom().nextInt(enchantmentWrappers.size()); - return enchantmentWrappers.get(randomIndex+1); + return enchantmentWrappers.get(randomIndex); } /** From dd3d3244158735bb9d7abad9728d8c388987e7e8 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Tue, 29 Dec 2020 13:38:03 -0800 Subject: [PATCH 18/22] update notes and default fishing treasures --- Changelog.txt | 19 +++++++++++++------ src/main/resources/fishing_treasures.yml | 13 +++++++++++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Changelog.txt b/Changelog.txt index 6058013d5..85d2b10ae 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,31 +1,38 @@ 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, you'll have to copy over your changes and be aware that Records rarity is now 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) - Added all Netherite gear to the Mythic tier in fishing_treasures.yml - Added Enchanted Books to fishing_treasures.yml as Mythic rarity (can drop with any legal enchantment - 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: - 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). - The rarity 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. + 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. + 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. + 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 diff --git a/src/main/resources/fishing_treasures.yml b/src/main/resources/fishing_treasures.yml index 0994aeb79..d839da2fd 100644 --- a/src/main/resources/fishing_treasures.yml +++ b/src/main/resources/fishing_treasures.yml @@ -139,6 +139,10 @@ Fishing: Amount: 1 XP: 200 Rarity: RARE + NAME_TAG: + Amount: 1 + XP: 200 + Rarity: RARE IRON_BOOTS: Amount: 1 XP: 200 @@ -163,6 +167,10 @@ Fishing: Amount: 5 XP: 200 Rarity: EPIC + NAUTILUS_SHELL: + Amount: 1 + XP: 200 + Rarity: LEGENDARY DIAMOND_SWORD: Amount: 1 XP: 200 @@ -282,15 +290,16 @@ Fishing: ENCHANTED_BOOK: Amount: 1 XP: 400 - Rarity: MYTHIC + 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: MYTHIC + Rarity: LEGENDARY # # Fishing drop rates ### From 7802d54ebd7d27cf766b65c2c629a1120ac1692c Mon Sep 17 00:00:00 2001 From: nossr50 Date: Tue, 29 Dec 2020 13:47:40 -0800 Subject: [PATCH 19/22] 2.1.164 Closes #4358 #4342 #3812 #3643 #3540 --- Changelog.txt | 1 + pom.xml | 2 +- .../nossr50/config/treasure/FishingTreasureConfig.java | 3 +-- .../com/gmail/nossr50/config/treasure/TreasureConfig.java | 6 ------ 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Changelog.txt b/Changelog.txt index 85d2b10ae..99717caaf 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -16,6 +16,7 @@ Version 2.1.164 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. diff --git a/pom.xml b/pom.xml index 19ed2dbfa..e353ab913 100755 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.gmail.nossr50.mcMMO mcMMO - 2.1.164-SNAPSHOT + 2.1.164 mcMMO https://github.com/mcMMO-Dev/mcMMO diff --git a/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java b/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java index 69d835541..41fa349ce 100755 --- a/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java +++ b/src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java @@ -15,7 +15,6 @@ import org.bukkit.inventory.meta.PotionMeta; import org.bukkit.potion.PotionData; import org.bukkit.potion.PotionType; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.*; @@ -200,7 +199,7 @@ public class FishingTreasureConfig 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(); 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 eac722de1..51e01307e 100755 --- a/src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java +++ b/src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java @@ -58,12 +58,6 @@ public class TreasureConfig extends ConfigLoader { loadTreasures("Excavation"); loadTreasures("Hylian_Luck"); - - for (EntityType entity : EntityType.values()) { - if (entity.isAlive()) { - loadTreasures("Shake." + entity.toString()); - } - } } private void loadTreasures(String type) { From 01f31e76f57e8be8bbaa867bf6396bbaf752f792 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Wed, 30 Dec 2020 15:20:24 -0800 Subject: [PATCH 20/22] new chunkstore Co-authored-by: t00thpick1 --- Changelog.txt | 7 + pom.xml | 22 +- .../nossr50/listeners/WorldListener.java | 22 - src/main/java/com/gmail/nossr50/mcMMO.java | 6 +- .../util/blockmeta/BitSetChunkStore.java | 243 ++++++++++ .../{chunkmeta => }/ChunkManager.java | 72 +-- .../{chunkmeta => }/ChunkManagerFactory.java | 2 +- .../blockmeta/{chunkmeta => }/ChunkStore.java | 27 +- .../util/blockmeta/ChunkletManager.java | 151 ------ .../blockmeta/ChunkletManagerFactory.java | 15 - .../nossr50/util/blockmeta/ChunkletStore.java | 48 -- .../util/blockmeta/ChunkletStoreFactory.java | 8 - .../util/blockmeta/HashChunkManager.java | 354 ++++++++++++++ .../util/blockmeta/HashChunkletManager.java | 410 ---------------- .../util/blockmeta/McMMOSimpleRegionFile.java | 257 ++++++++++ .../{chunkmeta => }/NullChunkManager.java | 42 +- .../util/blockmeta/NullChunkletManager.java | 85 ---- .../blockmeta/PrimitiveChunkletStore.java | 48 -- .../blockmeta/PrimitiveExChunkletStore.java | 180 ------- .../chunkmeta/ChunkStoreFactory.java | 10 - .../blockmeta/chunkmeta/HashChunkManager.java | 447 ------------------ .../chunkmeta/McMMOSimpleChunkBuffer.java | 39 -- .../chunkmeta/McMMOSimpleRegionFile.java | 306 ------------ .../chunkmeta/PrimitiveChunkStore.java | 147 ------ .../conversion/BlockStoreConversionMain.java | 90 ---- .../BlockStoreConversionXDirectory.java | 80 ---- .../BlockStoreConversionZDirectory.java | 191 -------- src/test/java/ChunkStoreTest.java | 308 ++++++++++++ 28 files changed, 1211 insertions(+), 2406 deletions(-) create mode 100644 src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java rename src/main/java/com/gmail/nossr50/util/blockmeta/{chunkmeta => }/ChunkManager.java (61%) mode change 100755 => 100644 rename src/main/java/com/gmail/nossr50/util/blockmeta/{chunkmeta => }/ChunkManagerFactory.java (86%) mode change 100755 => 100644 rename src/main/java/com/gmail/nossr50/util/blockmeta/{chunkmeta => }/ChunkStore.java (79%) mode change 100755 => 100644 delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManager.java delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManagerFactory.java delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStore.java delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStoreFactory.java create mode 100644 src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkletManager.java create mode 100644 src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java rename src/main/java/com/gmail/nossr50/util/blockmeta/{chunkmeta => }/NullChunkManager.java (54%) mode change 100755 => 100644 delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkletManager.java delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveChunkletStore.java delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveExChunkletStore.java delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStoreFactory.java delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/HashChunkManager.java delete mode 100644 src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleChunkBuffer.java delete mode 100644 src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleRegionFile.java delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/PrimitiveChunkStore.java delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionMain.java delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionXDirectory.java delete mode 100755 src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionZDirectory.java create mode 100644 src/test/java/ChunkStoreTest.java diff --git a/Changelog.txt b/Changelog.txt index 99717caaf..64cce7473 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,10 @@ +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) + + 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 + 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 diff --git a/pom.xml b/pom.xml index e353ab913..755d4490f 100755 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.gmail.nossr50.mcMMO mcMMO - 2.1.164 + 2.1.165-SNAPSHOT mcMMO https://github.com/mcMMO-Dev/mcMMO @@ -279,7 +279,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/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 b98ad9f9f..529f70fb5 100644 --- a/src/main/java/com/gmail/nossr50/mcMMO.java +++ b/src/main/java/com/gmail/nossr50/mcMMO.java @@ -38,8 +38,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; @@ -336,8 +336,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(); } 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/test/java/ChunkStoreTest.java b/src/test/java/ChunkStoreTest.java new file mode 100644 index 000000000..9d4e4995d --- /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 alot 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); + } + } +} From d0d05a33f8561cacbedbfc31099cfd22e317f7d2 Mon Sep 17 00:00:00 2001 From: nossr50 Date: Wed, 30 Dec 2020 15:30:46 -0800 Subject: [PATCH 21/22] update changelog --- Changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.txt b/Changelog.txt index 64cce7473..db1394a97 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -4,6 +4,8 @@ Version 2.1.165 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) From 006a7bf2772bb6ed16a471b1e0f9117119d5dc6d Mon Sep 17 00:00:00 2001 From: nossr50 Date: Wed, 30 Dec 2020 15:41:14 -0800 Subject: [PATCH 22/22] Add back missing cooldown locale message Fixes #4361 --- Changelog.txt | 1 + src/main/resources/locale/locale_en_US.properties | 1 + src/test/java/ChunkStoreTest.java | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Changelog.txt b/Changelog.txt index db1394a97..278a9f4d3 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,6 +1,7 @@ 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 diff --git a/src/main/resources/locale/locale_en_US.properties b/src/main/resources/locale/locale_en_US.properties index e60f89a86..939132271 100644 --- a/src/main/resources/locale/locale_en_US.properties +++ b/src/main/resources/locale/locale_en_US.properties @@ -589,6 +589,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/test/java/ChunkStoreTest.java b/src/test/java/ChunkStoreTest.java index 9d4e4995d..f45b0e0c9 100644 --- a/src/test/java/ChunkStoreTest.java +++ b/src/test/java/ChunkStoreTest.java @@ -15,7 +15,7 @@ import java.util.UUID; import static org.mockito.Mockito.mock; /** - * Could be alot better. But some tests are better than none! Tests the major things, still kinda unit-testy. Verifies that the serialization isn't completely broken. + * 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)