diff --git a/Changelog.txt b/Changelog.txt index 1ee7a8ce7..c27d92678 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -99,15 +99,50 @@ Version 2.2.000 Parties got unnecessarily complex in my absence, I have removed many party features in order to simplify parties and bring them closer to my vision. I have also added new features which should improve parties where it matters. About the removed party features, all the features I removed I consider poor quality features and I don't think they belong in mcMMO. Feel free to yell at me in discord if you disagree. I don't know what genius decided to make parties public by default, when I found out that parties had been changed to such a system I could barely contain my disgust. Parties are back to being private, you get invited by a party leader or party officer. That is the only way to join a party. -Version 2.1.165 - The mcMMO system which tracks player placed blocks has had some major rewrites (thanks t00thpick1) - mcMMO will now be compatible with changes to world height (1.17 compatibility) - Added missing cooldown locale message 'Commands.Database.Cooldown' +Version 2.1.168 + Fixed an IndexOutOfBoundsException error when trying to access UserBlockTracker from an invalid range (thanks t00thpick1) + (API) UserBlockTracker is now the interface by which our block-tracker will be known (thanks t00thpick1) + +Version 2.1.167 + Fixed a serious dupe bug + Add McMMOPlayerPreXpGainEvent event for plugins to modify given exp (thanks electronicboy) + Add SkillActivationPerkEvent (thanks electronicboy) + + NOTE: + This bug was introduced in 2.1.165, update immediately if you are on 2.1.165 or 2.1.166 + +Version 2.1.166 + Fixed a small memory leak in the new COTW tracker + Potentially fixed a ConcurrentModificationException involving the TransientEntityTracker (report this error if you encounter it) + Music discs removed from the default fishing_treasures.yml + Optimized how mcMMO saves player data (should improve timings on servers with bad disk speeds and or bad connectivity to their SQL server instance) NOTES: + No one likes fishing up music discs, if you want this change it is recommended you delete fishing_treasures.yml and let it regenerate + (You won't have this file if you haven't updated in a while, if so you don't need to do anything) + If any of you encounter a ConcurrentModificationException error that mentions TransientEntityTracker in its stack trace after this update let me know, I have another fix in mind for this if this update didn't fix it. + +Version 2.1.165 + Fixed a bug where Enchanted Books dropped by mcMMO (in Fishing) did not function correctly + The mcMMO system which tracks player placed blocks has had some major rewrites (thanks t00thpick1) + Optimized our ChunkUnloadEvent, this should improve timings in this area + How mcMMO tracks COTW entities has been rewritten + When COTW summons are killed players are now informed (from anything other than the time expiring). + mcMMO will now be compatible with changes to world height (1.17 compatibility) + mcMMO will ignore EntityPickupItemEvents from "Fake-Player" NPCs if it recognizes them as such, this will prevent some compatibility issues with some plugins + SuperBreaker will now always activate if the target blocks fastest tool is a Pickaxe (used to require the block giving XP or being considered an ore) + Master Angler mentions that it works better with a boat in its hover tip + Added missing cooldown locale message 'Commands.Database.Cooldown' + Added new locale message 'Taming.Summon.COTW.Removed' + Updated locale entry 'Fishing.SubSkill.MasterAngler.Description' + + NOTES: + Books dropped before this fix will not be usable and should just be chucked in lava, the broken books have blue names, the working books have yellow names. 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 + This new system is compatible with the old one, it will convert old files to the new format as needed. You won't even know it is doing anything. + This update shouldn't break anything as the API is the same. + + Alright back to work on T&C unless some major bugs come out of this... 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) diff --git a/pom.xml b/pom.xml index adb1be672..c07f70a50 100755 --- a/pom.xml +++ b/pom.xml @@ -124,6 +124,10 @@ + + net.kyori.examination + com.gmail.nossr50.kyori.examination + net.kyori.adventure com.gmail.nossr50.mcmmo.kyori.adventure diff --git a/src/main/java/com/gmail/nossr50/events/experience/McMMOPlayerPreXpGainEvent.java b/src/main/java/com/gmail/nossr50/events/experience/McMMOPlayerPreXpGainEvent.java new file mode 100644 index 000000000..849d7ab48 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/events/experience/McMMOPlayerPreXpGainEvent.java @@ -0,0 +1,50 @@ +package com.gmail.nossr50.events.experience; + +import com.gmail.nossr50.datatypes.experience.XPGainReason; +import com.gmail.nossr50.datatypes.skills.PrimarySkillType; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called when a player gains XP in a skill + */ +public class McMMOPlayerPreXpGainEvent extends McMMOPlayerExperienceEvent { + private float xpGained; + + @Deprecated + public McMMOPlayerPreXpGainEvent(Player player, PrimarySkillType skill, float xpGained) { + super(player, skill, XPGainReason.UNKNOWN); + this.xpGained = xpGained; + } + + public McMMOPlayerPreXpGainEvent(Player player, PrimarySkillType skill, float xpGained, XPGainReason xpGainReason) { + super(player, skill, xpGainReason); + this.xpGained = xpGained; + } + + /** + * @return int amount of experience gained in this event + */ + public int getXpGained() { + return (int) xpGained; + } + + /** + * @param xpGained int amount of experience gained in this event + */ + public void setXpGained(int xpGained) { + this.xpGained = xpGained; + } + + private static final HandlerList handlers = new HandlerList(); + + @Override + public @NotNull HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/src/main/java/com/gmail/nossr50/events/skills/SkillActivationPerkEvent.java b/src/main/java/com/gmail/nossr50/events/skills/SkillActivationPerkEvent.java new file mode 100644 index 000000000..89c41dd6f --- /dev/null +++ b/src/main/java/com/gmail/nossr50/events/skills/SkillActivationPerkEvent.java @@ -0,0 +1,49 @@ +package com.gmail.nossr50.events.skills; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +public class SkillActivationPerkEvent extends Event { + + + + + private static final HandlerList handlers = new HandlerList(); + private final Player player; + private int ticks; + private final int maxTicks; + + public SkillActivationPerkEvent(Player player, int ticks, int maxTicks) { + + this.player = player; + this.ticks = ticks; + this.maxTicks = maxTicks; + } + + public Player getPlayer() { + return player; + } + + public int getTicks() { + return ticks; + } + + public void setTicks(int ticks) { + this.ticks = ticks; + } + + public int getMaxTicks() { + return maxTicks; + } + + @Override + public @NotNull HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/src/main/java/com/gmail/nossr50/listeners/BlockListener.java b/src/main/java/com/gmail/nossr50/listeners/BlockListener.java index efcc47af2..9c1b1aa9d 100644 --- a/src/main/java/com/gmail/nossr50/listeners/BlockListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/BlockListener.java @@ -252,12 +252,18 @@ public class BlockListener implements Listener { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockGrow(BlockGrowEvent event) { + Block block = event.getBlock(); + World world = block.getWorld(); + /* WORLD BLACKLIST CHECK */ - if(WorldBlacklist.isWorldBlacklisted(event.getBlock().getWorld())) + if(WorldBlacklist.isWorldBlacklisted(world)) return; - BlockState blockState = event.getBlock().getState(); - mcMMO.getPlaceStore().setFalse(blockState); + // Minecraft is dumb, the events still throw when a plant "grows" higher than the max block height. Even though no new block is created + if (block.getY() >= world.getMaxHeight()) + return; + + mcMMO.getPlaceStore().setFalse(block); } /** diff --git a/src/main/java/com/gmail/nossr50/listeners/ChunkListener.java b/src/main/java/com/gmail/nossr50/listeners/ChunkListener.java index 406a02436..761516deb 100644 --- a/src/main/java/com/gmail/nossr50/listeners/ChunkListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/ChunkListener.java @@ -1,30 +1,20 @@ package com.gmail.nossr50.listeners; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.util.compat.layers.persistentdata.MobMetaFlagType; -import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.world.ChunkUnloadEvent; +import java.util.List; + public class ChunkListener implements Listener { @EventHandler(ignoreCancelled = true) public void onChunkUnload(ChunkUnloadEvent event) { - for(Entity entity : event.getChunk().getEntities()) { - if(entity instanceof LivingEntity) { - LivingEntity livingEntity = (LivingEntity) entity; - if(mcMMO.getCompatibilityManager().getPersistentDataLayer().hasMobFlag(MobMetaFlagType.COTW_SUMMONED_MOB, livingEntity)) { - - //Remove from existence - if(livingEntity.isValid()) { - mcMMO.getCompatibilityManager().getPersistentDataLayer().removeMobFlags(livingEntity); - livingEntity.setHealth(0); - livingEntity.remove(); - } - } - } + List matchingEntities = mcMMO.getTransientEntityTracker().getAllTransientEntitiesInChunk(event.getChunk()); + for(LivingEntity livingEntity : matchingEntities) { + mcMMO.getTransientEntityTracker().removeSummon(livingEntity, null, false); } } } diff --git a/src/main/java/com/gmail/nossr50/listeners/EntityListener.java b/src/main/java/com/gmail/nossr50/listeners/EntityListener.java index 60610d13f..d4b4dbd18 100644 --- a/src/main/java/com/gmail/nossr50/listeners/EntityListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/EntityListener.java @@ -689,12 +689,17 @@ public class EntityListener implements Listener { */ @EventHandler(ignoreCancelled = true) public void onEntityDeath(EntityDeathEvent event) { - /* WORLD BLACKLIST CHECK */ - if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld())) - return; - LivingEntity entity = event.getEntity(); + if(mcMMO.getTransientEntityTracker().isTransientSummon(entity)) { + mcMMO.getTransientEntityTracker().removeSummon(entity, null, false); + } + + /* WORLD BLACKLIST CHECK */ + if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld())) { + return; + } + if (ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(entity)) { return; } diff --git a/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java index cfb114cd0..377341fde 100644 --- a/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java @@ -448,6 +448,10 @@ public class PlayerListener implements Listener { if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld())) return; + if(Misc.isNPCEntityExcludingVillagers(event.getEntity())) { + return; + } + if(event.getEntity() instanceof Player) { Player player = (Player) event.getEntity(); @@ -463,14 +467,13 @@ public class PlayerListener implements Listener { return; } + OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().queryPlayer(player); + //Profile not loaded - if(mcMMO.getUserManager().queryPlayer(player) == null) - { + if(mmoPlayer == null) { return; } - OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().queryPlayer(player); - Item drop = event.getItem(); ItemStack dropStack = drop.getItemStack(); @@ -525,18 +528,32 @@ public class PlayerListener implements Listener { return; } + McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player); + //Profile not loaded - OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().queryPlayer(player); + McMMOPlayer mmoPlayer = mcMMO.getUserManager().queryPlayer(player); if(mmoPlayer == null) { return; } - //TODO: There's an issue with using Async saves on player quit - //TODO: Basically there are conditions in which an async task does not execute fast enough to save the data if the server shutdown shortly after this task was scheduled - mcMMO.getUserManager().saveUserWithDelay(mmoPlayer.getMMOPlayerData(), false, 20); - - mcMMO.getUserManager().cleanupPlayer(mmoPlayer); //Handles cleaning up the player when their profile is no longer needed + mcMMO.getUserManager().saveUserImmediately(mmoPlayer, mcMMO.isServerShutdownExecuted()); + //Use a sync save if the server is shutting down to avoid race conditions + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + //TODO: Make sure this cleans up + mmoPlayer.logout(mcMMO.isServerShutdownExecuted()); } /** diff --git a/src/main/java/com/gmail/nossr50/mcMMO.java b/src/main/java/com/gmail/nossr50/mcMMO.java index 5247cc72b..2274864cb 100644 --- a/src/main/java/com/gmail/nossr50/mcMMO.java +++ b/src/main/java/com/gmail/nossr50/mcMMO.java @@ -91,6 +91,8 @@ public class mcMMO extends JavaPlugin { private static ChatManager chatManager; private static CommandManager commandManager; //ACF private static SkillRegister skillRegister; + private static TransientEntityTracker transientEntityTracker; + private static boolean serverShutdownExecuted = false; /* Adventure */ private static BukkitAudiences audiences; @@ -299,6 +301,9 @@ public class mcMMO extends JavaPlugin { chatManager = new ChatManager(this); commandManager = new CommandManager(this); + + transientEntityTracker = new TransientEntityTracker(); + setServerShutdown(false); //Reset flag, used to make decisions about async saves } public static PlayerLevelUtils getPlayerLevelUtils() { @@ -334,6 +339,10 @@ public class mcMMO extends JavaPlugin { */ @Override public void onDisable() { + setServerShutdown(true); + //TODO: Write code to catch unfinished async save tasks, for now we just hope they finish in time, which they should in most cases + mcMMO.p.getLogger().info("Server shutdown has been executed, saving and cleaning up data..."); + try { userManager.saveAllSync(); // Make sure to save player information if the server shuts down userManager.clearAll(); @@ -346,17 +355,11 @@ public class mcMMO extends JavaPlugin { formulaManager.saveFormula(); holidayManager.saveAnniversaryFiles(); - placeStore.cleanUp(); // Cleanup empty metadata stores placeStore.closeAll(); } catch (Exception e) { e.printStackTrace(); } - debug("Canceling all tasks..."); - getServer().getScheduler().cancelTasks(this); // This removes our tasks - debug("Unregister all events..."); - HandlerList.unregisterAll(this); // Cancel event registrations - if (Config.getInstance().getBackupsEnabled()) { // Remove other tasks BEFORE starting the Backup, or we just cancel it straight away. try { @@ -376,6 +379,11 @@ public class mcMMO extends JavaPlugin { } } + debug("Canceling all tasks..."); + getServer().getScheduler().cancelTasks(this); // This removes our tasks + debug("Unregister all events..."); + HandlerList.unregisterAll(this); // Cancel event registrations + databaseManager.onDisable(); debug("Was disabled."); // How informative! } @@ -704,5 +712,18 @@ public class mcMMO extends JavaPlugin { return commandManager; } + public static TransientEntityTracker getTransientEntityTracker() { + return transientEntityTracker; + } + + public static synchronized boolean isServerShutdownExecuted() { + return serverShutdownExecuted; + } + + private static synchronized void setServerShutdown(boolean bool) { + serverShutdownExecuted = bool; + } + + public @NotNull SkillRegister getSkillRegister() { return skillRegister; } } 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 076723292..a989695ab 100644 --- a/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java +++ b/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java @@ -37,7 +37,6 @@ 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; @@ -396,7 +395,7 @@ public class FishingManager extends SkillManager { if (treasure != null) { if(treasure instanceof FishingTreasureBook) { - treasureDrop = createEnchantBook((FishingTreasureBook) treasure); + treasureDrop = ItemUtils.createEnchantBook((FishingTreasureBook) treasure); } else { treasureDrop = treasure.getDrop().clone(); // Not cloning is bad, m'kay? @@ -450,37 +449,16 @@ public class FishingManager extends SkillManager { } if(fishingSucceeds) { - fishingCatch.setItemStack(treasureDrop); - if (Config.getInstance().getFishingExtraFish()) { Misc.spawnItem(player.getEyeLocation(), fishingCatch.getItemStack(), ItemSpawnReason.FISHING_EXTRA_FISH); } + + fishingCatch.setItemStack(treasureDrop); } applyXpGain(fishXp + treasureXp, XPGainReason.PVE); } - - private @NotNull ItemStack createEnchantBook(@NotNull FishingTreasureBook fishingTreasureBook) { - ItemStack itemStack = fishingTreasureBook.getDrop().clone(); - EnchantmentWrapper enchantmentWrapper = getRandomEnchantment(fishingTreasureBook.getLegalEnchantments()); - ItemMeta itemMeta = itemStack.getItemMeta(); - - if(itemMeta == null) - return itemStack; - - itemMeta.addEnchant(enchantmentWrapper.getEnchantment(), enchantmentWrapper.getEnchantmentLevel(), ExperienceConfig.getInstance().allowUnsafeEnchantments()); - itemStack.setItemMeta(itemMeta); - return itemStack; - } - - private @NotNull EnchantmentWrapper getRandomEnchantment(@NotNull List enchantmentWrappers) { - Collections.shuffle(enchantmentWrappers, Misc.getRandom()); - - int randomIndex = Misc.getRandom().nextInt(enchantmentWrappers.size()); - return enchantmentWrappers.get(randomIndex); - } - /** * Handle the vanilla XP boost for Fishing * diff --git a/src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java b/src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java index d6ea669f8..db97a0077 100644 --- a/src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java +++ b/src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java @@ -32,9 +32,7 @@ import org.bukkit.entity.*; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; public class TamingManager extends SkillManager { //TODO: Temporary static cache, will be changed in 2.2 @@ -42,8 +40,6 @@ public class TamingManager extends SkillManager { private static HashMap cotwSummonDataProperties; private long lastSummonTimeStamp; - private HashMap> playerSummonedEntities; - public TamingManager(@NotNull OnlineMMOPlayer mmoPlayer) { super(mmoPlayer, PrimarySkillType.TAMING); init(); @@ -55,20 +51,12 @@ public class TamingManager extends SkillManager { lastSummonTimeStamp = 0L; //Init per-player tracking of summoned entities - initPerPlayerSummonTracking(); + mcMMO.getTransientEntityTracker().initPlayer(mmoPlayer.getPlayer()); //Hacky stuff used as a band-aid initStaticCaches(); } - private void initPerPlayerSummonTracking() { - playerSummonedEntities = new HashMap<>(); - - for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) { - playerSummonedEntities.put(callOfTheWildType, new ArrayList<>()); - } - } - private void initStaticCaches() { //TODO: Temporary static cache, will be changed in 2.2 //This is shared between instances of TamingManager @@ -503,58 +491,12 @@ public class TamingManager extends SkillManager { return summoningItems.containsKey(itemStack.getType()); } - //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update - private int getAmountCurrentlySummoned(CallOfTheWildType callOfTheWildType) { - //The tracker is unreliable so validate its contents first - recalibrateTracker(); - - return playerSummonedEntities.get(callOfTheWildType).size(); + private int getAmountCurrentlySummoned(@NotNull CallOfTheWildType callOfTheWildType) { + return mcMMO.getTransientEntityTracker().getAmountCurrentlySummoned(getPlayer().getUniqueId(), callOfTheWildType); } - //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update private void addToTracker(@NotNull LivingEntity livingEntity, @NotNull CallOfTheWildType callOfTheWildType) { - TrackedTamingEntity trackedEntity = new TrackedTamingEntity(livingEntity, callOfTheWildType, this); - - playerSummonedEntities.get(callOfTheWildType).add(trackedEntity); - } - - //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update - public List getTrackedEntities(CallOfTheWildType callOfTheWildType) { - return playerSummonedEntities.get(callOfTheWildType); - } - - //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update - public void removeFromTracker(@NotNull TrackedTamingEntity trackedEntity) { - playerSummonedEntities.get(trackedEntity.getCallOfTheWildType()).remove(trackedEntity); - - NotificationManager.sendPlayerInformationChatOnly(getPlayer(), "Taming.Summon.COTW.TimeExpired", StringUtils.getPrettyEntityTypeString(trackedEntity.getLivingEntity().getType())); - } - - /** - * Builds a new tracked list by determining which tracked things are still valid - */ - //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update - private void recalibrateTracker() { - for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) { - ArrayList validEntities = getValidTrackedEntities(callOfTheWildType); - playerSummonedEntities.put(callOfTheWildType, validEntities); //Replace the old list with the new list - } - } - - //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update - private @NotNull ArrayList getValidTrackedEntities(@NotNull CallOfTheWildType callOfTheWildType) { - ArrayList validTrackedEntities = new ArrayList<>(); - - for(TrackedTamingEntity trackedTamingEntity : getTrackedEntities(callOfTheWildType)) { - LivingEntity livingEntity = trackedTamingEntity.getLivingEntity(); - - //Remove from existence - if(livingEntity != null && livingEntity.isValid()) { - validTrackedEntities.add(trackedTamingEntity); - } - } - - return validTrackedEntities; + mcMMO.getTransientEntityTracker().registerEntity(getPlayer().getUniqueId(), new TrackedTamingEntity(livingEntity, callOfTheWildType, getPlayer())); } /** @@ -563,20 +505,6 @@ public class TamingManager extends SkillManager { */ //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update public void cleanupAllSummons() { - for(List trackedTamingEntities : playerSummonedEntities.values()) { - for(TrackedTamingEntity trackedTamingEntity : trackedTamingEntities) { - LivingEntity livingEntity = trackedTamingEntity.getLivingEntity(); - - //Remove from existence - if(livingEntity != null && livingEntity.isValid()) { - mcMMO.getCompatibilityManager().getPersistentDataLayer().removeMobFlags(livingEntity); - livingEntity.setHealth(0); - livingEntity.remove(); - } - } - - //Clear the list - trackedTamingEntities.clear(); - } + mcMMO.getTransientEntityTracker().cleanupPlayer(getPlayer()); } } diff --git a/src/main/java/com/gmail/nossr50/skills/taming/TrackedTamingEntity.java b/src/main/java/com/gmail/nossr50/skills/taming/TrackedTamingEntity.java index aabcd44da..9116c4fdf 100644 --- a/src/main/java/com/gmail/nossr50/skills/taming/TrackedTamingEntity.java +++ b/src/main/java/com/gmail/nossr50/skills/taming/TrackedTamingEntity.java @@ -4,61 +4,40 @@ import com.gmail.nossr50.config.Config; import com.gmail.nossr50.datatypes.skills.subskills.taming.CallOfTheWildType; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.util.Misc; -import com.gmail.nossr50.util.skills.ParticleEffectUtils; -import org.bukkit.Location; -import org.bukkit.Sound; import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; - -import java.util.UUID; +import org.jetbrains.annotations.NotNull; public class TrackedTamingEntity extends BukkitRunnable { - private final LivingEntity livingEntity; - private final CallOfTheWildType callOfTheWildType; - private final UUID id; - private int length; - private final TamingManager tamingManagerRef; + private final @NotNull LivingEntity livingEntity; + private final @NotNull CallOfTheWildType callOfTheWildType; + private final @NotNull Player player; - protected TrackedTamingEntity(LivingEntity livingEntity, CallOfTheWildType callOfTheWildType, TamingManager tamingManagerRef) { - this.tamingManagerRef = tamingManagerRef; + protected TrackedTamingEntity(@NotNull LivingEntity livingEntity, @NotNull CallOfTheWildType callOfTheWildType, @NotNull Player player) { + this.player = player; this.callOfTheWildType = callOfTheWildType; this.livingEntity = livingEntity; - this.id = livingEntity.getUniqueId(); int tamingCOTWLength = Config.getInstance().getTamingCOTWLength(callOfTheWildType.getConfigEntityTypeEntry()); if (tamingCOTWLength > 0) { - this.length = tamingCOTWLength * Misc.TICK_CONVERSION_FACTOR; + int length = tamingCOTWLength * Misc.TICK_CONVERSION_FACTOR; this.runTaskLater(mcMMO.p, length); } } @Override public void run() { - if (livingEntity.isValid()) { - Location location = livingEntity.getLocation(); - location.getWorld().playSound(location, Sound.BLOCK_FIRE_EXTINGUISH, 0.8F, 0.8F); - ParticleEffectUtils.playCallOfTheWildEffect(livingEntity); - - if(tamingManagerRef != null) - tamingManagerRef.removeFromTracker(this); - - livingEntity.setHealth(0); - livingEntity.remove(); - } - + mcMMO.getTransientEntityTracker().removeSummon(this.getLivingEntity(), player, true); this.cancel(); } - public CallOfTheWildType getCallOfTheWildType() { + public @NotNull CallOfTheWildType getCallOfTheWildType() { return callOfTheWildType; } - public LivingEntity getLivingEntity() { + public @NotNull LivingEntity getLivingEntity() { return livingEntity; } - - public UUID getID() { - return id; - } } diff --git a/src/main/java/com/gmail/nossr50/util/BlockUtils.java b/src/main/java/com/gmail/nossr50/util/BlockUtils.java index 8c9a6f7eb..8c5dd11c1 100644 --- a/src/main/java/com/gmail/nossr50/util/BlockUtils.java +++ b/src/main/java/com/gmail/nossr50/util/BlockUtils.java @@ -182,11 +182,14 @@ public final class BlockUtils { * @return true if the block should affected by Super Breaker, false * otherwise */ - public static Boolean affectedBySuperBreaker(BlockState blockState) { + public static boolean affectedBySuperBreaker(BlockState blockState) { + if(mcMMO.getMaterialMapStore().isIntendedToolPickaxe(blockState.getType())) + return true; + if (ExperienceConfig.getInstance().doesBlockGiveSkillXP(PrimarySkillType.MINING, blockState.getBlockData())) return true; - return isOre(blockState) || mcMMO.getModManager().isCustomMiningBlock(blockState); + return mcMMO.getModManager().isCustomMiningBlock(blockState); } /** diff --git a/src/main/java/com/gmail/nossr50/util/ItemUtils.java b/src/main/java/com/gmail/nossr50/util/ItemUtils.java index 68a4da514..cbde856c1 100644 --- a/src/main/java/com/gmail/nossr50/util/ItemUtils.java +++ b/src/main/java/com/gmail/nossr50/util/ItemUtils.java @@ -2,7 +2,10 @@ package com.gmail.nossr50.util; import com.gmail.nossr50.config.AdvancedConfig; import com.gmail.nossr50.config.Config; +import com.gmail.nossr50.config.experience.ExperienceConfig; import com.gmail.nossr50.config.party.ItemWeightConfig; +import com.gmail.nossr50.datatypes.treasure.EnchantmentWrapper; +import com.gmail.nossr50.datatypes.treasure.FishingTreasureBook; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; import org.bukkit.ChatColor; @@ -12,9 +15,11 @@ import org.bukkit.entity.Player; import org.bukkit.inventory.FurnaceRecipe; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.Recipe; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; import org.bukkit.inventory.meta.ItemMeta; import org.jetbrains.annotations.NotNull; +import java.util.Collections; import java.util.List; import static org.bukkit.Material.AIR; @@ -581,4 +586,26 @@ public final class ItemUtils { public static boolean canBeSuperAbilityDigBoosted(@NotNull ItemStack itemStack) { return isShovel(itemStack) || isPickaxe(itemStack); } + + public static @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; + } + + EnchantmentStorageMeta enchantmentStorageMeta = (EnchantmentStorageMeta) itemMeta; + enchantmentStorageMeta.addStoredEnchant(enchantmentWrapper.getEnchantment(), enchantmentWrapper.getEnchantmentLevel(), ExperienceConfig.getInstance().allowUnsafeEnchantments()); + itemStack.setItemMeta(enchantmentStorageMeta); + return itemStack; + } + + public static @NotNull EnchantmentWrapper getRandomEnchantment(@NotNull List enchantmentWrappers) { + Collections.shuffle(enchantmentWrappers, Misc.getRandom()); + + int randomIndex = Misc.getRandom().nextInt(enchantmentWrappers.size()); + return enchantmentWrappers.get(randomIndex); + } } diff --git a/src/main/java/com/gmail/nossr50/util/MaterialMapStore.java b/src/main/java/com/gmail/nossr50/util/MaterialMapStore.java index e99514d36..320ff1519 100644 --- a/src/main/java/com/gmail/nossr50/util/MaterialMapStore.java +++ b/src/main/java/com/gmail/nossr50/util/MaterialMapStore.java @@ -55,6 +55,8 @@ public class MaterialMapStore { private final @NotNull HashSet enchantables; private final @NotNull HashSet ores; + private final @NotNull HashSet intendedToolPickAxe; + private final @NotNull HashSet intendedToolShovel; private final @NotNull HashMap tierValue; @@ -100,14 +102,16 @@ public class MaterialMapStore { enchantables = new HashSet<>(); ores = new HashSet<>(); + intendedToolPickAxe = new HashSet<>(); + intendedToolShovel = new HashSet<>(); tierValue = new HashMap<>(); fillVanillaMaterialRegisters(); } - private void fillVanillaMaterialRegisters() - { + private void fillVanillaMaterialRegisters() { + //The order matters fillAbilityBlackList(); fillToolBlackList(); fillMossyWhiteList(); @@ -122,6 +126,7 @@ public class MaterialMapStore { fillTools(); fillEnchantables(); fillOres(); + fillIntendedTools(); fillTierMap(); } @@ -206,6 +211,190 @@ public class MaterialMapStore { ores.add("gilded_blackstone"); } + private void fillIntendedTools() { + intendedToolPickAxe.addAll(ores); + + intendedToolPickAxe.add("ice"); + intendedToolPickAxe.add("packed_ice"); + intendedToolPickAxe.add("blue_ice"); + intendedToolPickAxe.add("frosted_ice"); + intendedToolPickAxe.add("anvil"); + intendedToolPickAxe.add("bell"); + intendedToolPickAxe.add("block_of_redstone"); + intendedToolPickAxe.add("brewing_stand"); + intendedToolPickAxe.add("cauldron"); + intendedToolPickAxe.add("chain"); + intendedToolPickAxe.add("hopper"); + intendedToolPickAxe.add("iron_bars"); + intendedToolPickAxe.add("iron_door"); + intendedToolPickAxe.add("iron_trapdoor"); + intendedToolPickAxe.add("lantern"); + intendedToolPickAxe.add("weighted_pressure_plates"); + intendedToolPickAxe.add("block_of_iron"); + intendedToolPickAxe.add("copper_blocks"); + intendedToolPickAxe.add("cut_copper"); + intendedToolPickAxe.add("cut_copper_slab"); + intendedToolPickAxe.add("cut_copper_stairs"); + intendedToolPickAxe.add("lapis_lazuli_block"); + intendedToolPickAxe.add("lightning_rod"); + intendedToolPickAxe.add("block_of_diamond"); + intendedToolPickAxe.add("block_of_emerald"); + intendedToolPickAxe.add("block_of_gold"); + intendedToolPickAxe.add("block_of_netherite"); + intendedToolPickAxe.add("piston"); + intendedToolPickAxe.add("sticky_piston"); + intendedToolPickAxe.add("conduit"); + intendedToolPickAxe.add("shulker_box"); + intendedToolPickAxe.add("element_constructor"); //be & ee + intendedToolPickAxe.add("compound_creator"); //be & ee + intendedToolPickAxe.add("material_reducer"); //be & ee + intendedToolPickAxe.add("activator_rail"); + intendedToolPickAxe.add("detector_rail"); + intendedToolPickAxe.add("powered_rail"); + intendedToolPickAxe.add("rail"); + intendedToolPickAxe.add("andesite"); + intendedToolPickAxe.add("basalt"); + intendedToolPickAxe.add("blackstone"); + intendedToolPickAxe.add("blast_furnace"); + intendedToolPickAxe.add("block_of_coal"); + intendedToolPickAxe.add("block_of_quartz"); + intendedToolPickAxe.add("bricks"); + intendedToolPickAxe.add("cobblestone"); + intendedToolPickAxe.add("cobblestone_wall"); + intendedToolPickAxe.add("concrete"); + intendedToolPickAxe.add("dark_prismarine"); + intendedToolPickAxe.add("diorite"); + intendedToolPickAxe.add("dispenser"); + intendedToolPickAxe.add("dripstone_block"); + intendedToolPickAxe.add("dropper"); + intendedToolPickAxe.add("enchantment_table"); + intendedToolPickAxe.add("end_stone"); + intendedToolPickAxe.add("ender_chest"); + intendedToolPickAxe.add("furnace"); + intendedToolPickAxe.add("glazed_terracotta"); + intendedToolPickAxe.add("granite"); + intendedToolPickAxe.add("grindstone"); + intendedToolPickAxe.add("heat_block"); //be & ee + intendedToolPickAxe.add("lodestone"); + intendedToolPickAxe.add("mossy_cobblestone"); + intendedToolPickAxe.add("nether_bricks"); + intendedToolPickAxe.add("nether_brick_fence"); + intendedToolPickAxe.add("nether_gold_ore"); + intendedToolPickAxe.add("nether_quartz_ore"); + intendedToolPickAxe.add("netherrack"); + intendedToolPickAxe.add("observer"); + intendedToolPickAxe.add("prismarine"); + intendedToolPickAxe.add("prismarine_bricks"); + intendedToolPickAxe.add("pointed_dripstone"); + intendedToolPickAxe.add("polished_andesite"); + intendedToolPickAxe.add("polished_blackstone"); + intendedToolPickAxe.add("polished_blackstone_bricks"); + intendedToolPickAxe.add("polished_diorite"); + intendedToolPickAxe.add("polished_granite"); + intendedToolPickAxe.add("red_sandstone"); + intendedToolPickAxe.add("sandstone"); + intendedToolPickAxe.add("smoker"); + intendedToolPickAxe.add("spawner"); + intendedToolPickAxe.add("stonecutter"); +// intendedToolPickAxe.add("slabs"); + intendedToolPickAxe.add("colored_terracotta"); +// intendedToolPickAxe.add("stairs"); + intendedToolPickAxe.add("smooth_stone"); + intendedToolPickAxe.add("stone"); + intendedToolPickAxe.add("stone_bricks"); + intendedToolPickAxe.add("stone_button"); + intendedToolPickAxe.add("stone_pressure_plate"); + intendedToolPickAxe.add("terracotta"); + intendedToolPickAxe.add("amethyst_bud"); + intendedToolPickAxe.add("amethyst_cluster"); + intendedToolPickAxe.add("block_of_amethyst"); + intendedToolPickAxe.add("budding_amethyst"); + intendedToolPickAxe.add("ancient_debris"); + intendedToolPickAxe.add("crying_obsidian"); + intendedToolPickAxe.add("glowing_obsidian"); //be + intendedToolPickAxe.add("obsidian"); + intendedToolPickAxe.add("respawn_anchor"); + + //slabs + intendedToolPickAxe.add("petrified_oak_slab"); + intendedToolPickAxe.add("stone_slab"); + intendedToolPickAxe.add("smooth_stone_slab"); + intendedToolPickAxe.add("cobblestone_slab"); + intendedToolPickAxe.add("mossy_cobblestone_slab"); + intendedToolPickAxe.add("stone_brick_slab"); + intendedToolPickAxe.add("mossy_stone_brick_slab"); + intendedToolPickAxe.add("andesite_slab"); + intendedToolPickAxe.add("polished_andesite_slab"); + intendedToolPickAxe.add("diorite_slab"); + intendedToolPickAxe.add("polished_diorite_slab"); + intendedToolPickAxe.add("granite_slab"); + intendedToolPickAxe.add("polished_granite_slab"); + intendedToolPickAxe.add("sandstone_slab"); + intendedToolPickAxe.add("cut_sandstone_slab"); + intendedToolPickAxe.add("smooth_sandstone_slab"); + intendedToolPickAxe.add("red_sandstone_slab"); + intendedToolPickAxe.add("cut_red_sandstone_slab"); + intendedToolPickAxe.add("smooth_red_sandstone_slab"); + intendedToolPickAxe.add("brick_slab"); + intendedToolPickAxe.add("prismarine_brick_slab"); + intendedToolPickAxe.add("dark_prismarine_slab"); + intendedToolPickAxe.add("nether_brick_slab"); + intendedToolPickAxe.add("red_netherbrick_slab"); + intendedToolPickAxe.add("quartz_slab"); + intendedToolPickAxe.add("smooth_quartz_slab"); + intendedToolPickAxe.add("purpur_slab"); + intendedToolPickAxe.add("end_stone_brick_slab"); + intendedToolPickAxe.add("blackstone_slab"); + intendedToolPickAxe.add("polished_blackstone_slab"); + intendedToolPickAxe.add("polished_blackstone_brick_slab"); + intendedToolPickAxe.add("lightly_weathered_cut_copper_slab"); + intendedToolPickAxe.add("semi_weathered_cut_copper_slab"); + intendedToolPickAxe.add("waxed_semi_weathered_cut_copper_slab"); + intendedToolPickAxe.add("weathered_cut_copper_slab"); + intendedToolPickAxe.add("waxed_cut_copper_slab"); + intendedToolPickAxe.add("waxed_lightly_weathered_cut_copper_slab"); + + //stairs (not all of these exist, just copied the above list and replaced slab with stairs) + intendedToolPickAxe.add("petrified_oak_stairs"); + intendedToolPickAxe.add("stone_stairs"); + intendedToolPickAxe.add("smooth_stone_stairs"); + intendedToolPickAxe.add("cobblestone_stairs"); + intendedToolPickAxe.add("mossy_cobblestone_stairs"); + intendedToolPickAxe.add("stone_brick_stairs"); + intendedToolPickAxe.add("mossy_stone_brick_stairs"); + intendedToolPickAxe.add("andesite_stairs"); + intendedToolPickAxe.add("polished_andesite_stairs"); + intendedToolPickAxe.add("diorite_stairs"); + intendedToolPickAxe.add("polished_diorite_stairs"); + intendedToolPickAxe.add("granite_stairs"); + intendedToolPickAxe.add("polished_granite_stairs"); + intendedToolPickAxe.add("sandstone_stairs"); + intendedToolPickAxe.add("cut_sandstone_stairs"); + intendedToolPickAxe.add("smooth_sandstone_stairs"); + intendedToolPickAxe.add("red_sandstone_stairs"); + intendedToolPickAxe.add("cut_red_sandstone_stairs"); + intendedToolPickAxe.add("smooth_red_sandstone_stairs"); + intendedToolPickAxe.add("brick_stairs"); + intendedToolPickAxe.add("prismarine_brick_stairs"); + intendedToolPickAxe.add("dark_prismarine_stairs"); + intendedToolPickAxe.add("nether_brick_stairs"); + intendedToolPickAxe.add("red_netherbrick_stairs"); + intendedToolPickAxe.add("quartz_stairs"); + intendedToolPickAxe.add("smooth_quartz_stairs"); + intendedToolPickAxe.add("purpur_stairs"); + intendedToolPickAxe.add("end_stone_brick_stairs"); + intendedToolPickAxe.add("blackstone_stairs"); + intendedToolPickAxe.add("polished_blackstone_stairs"); + intendedToolPickAxe.add("polished_blackstone_brick_stairs"); + intendedToolPickAxe.add("lightly_weathered_cut_copper_stairs"); + intendedToolPickAxe.add("semi_weathered_cut_copper_stairs"); + intendedToolPickAxe.add("waxed_semi_weathered_cut_copper_stairs"); + intendedToolPickAxe.add("weathered_cut_copper_stairs"); + intendedToolPickAxe.add("waxed_cut_copper_stairs"); + intendedToolPickAxe.add("waxed_lightly_weathered_cut_copper_stairs"); + + } + private void fillArmors() { fillLeatherArmorWhiteList(); fillIronArmorWhiteList(); @@ -1078,6 +1267,14 @@ public class MaterialMapStore { toolBlackList.add("respawn_anchor"); } + public boolean isIntendedToolPickaxe(Material material) { + return intendedToolPickAxe.contains(material.getKey().getKey()); + } + + public boolean isIntendedToolPickaxe(String string) { + return intendedToolPickAxe.contains(string); + } + public @NotNull HashSet getNetheriteArmor() { return netheriteArmor; } diff --git a/src/main/java/com/gmail/nossr50/util/TransientEntityTracker.java b/src/main/java/com/gmail/nossr50/util/TransientEntityTracker.java new file mode 100644 index 000000000..6e1b44938 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/util/TransientEntityTracker.java @@ -0,0 +1,316 @@ +package com.gmail.nossr50.util; + +import com.gmail.nossr50.datatypes.skills.subskills.taming.CallOfTheWildType; +import com.gmail.nossr50.mcMMO; +import com.gmail.nossr50.skills.taming.TrackedTamingEntity; +import com.gmail.nossr50.util.player.NotificationManager; +import com.gmail.nossr50.util.skills.ParticleEffectUtils; +import com.gmail.nossr50.util.text.StringUtils; +import com.google.common.collect.ImmutableSet; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Sound; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class TransientEntityTracker { + //These two are updated in step with each other + private final @NotNull HashMap>> perPlayerTransientEntityMap; + private final @NotNull HashSet chunkLookupCache; + + public TransientEntityTracker() { + perPlayerTransientEntityMap = new HashMap<>(); + chunkLookupCache = new HashSet<>(); + } + + public synchronized @NotNull HashSet getChunkLookupCache() { + return chunkLookupCache; + } + + public synchronized @NotNull HashMap>> getPerPlayerTransientEntityMap() { + return perPlayerTransientEntityMap; + } + + public synchronized void initPlayer(@NotNull Player player) { + if (!isPlayerRegistered(player.getUniqueId())) { + registerPlayer(player.getUniqueId()); + } + } + + /** + * Removes a player from the tracker + * + * @param playerUUID target player + */ + public synchronized void cleanupPlayer(@NotNull UUID playerUUID) { + cleanPlayer(null, playerUUID); + } + + /** + * Removes a player from the tracker + * + * @param player target player + */ + public synchronized void cleanupPlayer(@NotNull Player player) { + cleanPlayer(player, player.getUniqueId()); + } + + /** + * Removes a player from the tracker + * + * @param player target player + * @param playerUUID target player UUID + */ + private void cleanPlayer(@Nullable Player player, @NotNull UUID playerUUID) { + cleanupAllSummons(player, player.getUniqueId()); + removePlayerFromMap(playerUUID); + } + + private void removePlayerFromMap(@NotNull UUID playerUUID) { + getPerPlayerTransientEntityMap().remove(playerUUID); + } + + /** + * Checks if a player has already been registered + * Being registered constitutes having necessary values initialized in our per-player map + * + * @param playerUUID target player + * @return true if the player is registered + */ + private synchronized boolean isPlayerRegistered(@NotNull UUID playerUUID) { + return getPerPlayerTransientEntityMap().get(playerUUID) != null; + } + + /** + * Register a player to our tracker, which initializes the necessary values in our per-player map + * + * @param playerUUID player to register + */ + private synchronized void registerPlayer(@NotNull UUID playerUUID) { + getPerPlayerTransientEntityMap().put(playerUUID, new HashMap>()); + + for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) { + getPerPlayerTransientEntityMap().get(playerUUID).put(callOfTheWildType, new HashSet<>()); + } + } + + /** + * Get the tracked transient entities map for a specific player + * + * @param playerUUID the target uuid of the player + * @return the tracked entities map for the player, null if the player isn't registered + */ + public synchronized @Nullable HashMap> getPlayerTrackedEntityMap(@NotNull UUID playerUUID) { + return getPerPlayerTransientEntityMap().get(playerUUID); + } + + /** + * Registers an entity to a player + * This includes registration to our per-player map and our chunk lookup cache + * + * @param playerUUID target player's UUID + * @param trackedTamingEntity target entity + */ + public synchronized void registerEntity(@NotNull UUID playerUUID, @NotNull TrackedTamingEntity trackedTamingEntity) { + //Add to map entry + getTrackedEntities(playerUUID, trackedTamingEntity.getCallOfTheWildType()).add(trackedTamingEntity); + + //Add to cache for chunk lookups + addToChunkLookupCache(trackedTamingEntity); + } + + /** + * Checks if a living entity is a summon + * + * @param livingEntity target livinig entity + * @return true if target living entity is a summon + */ + public synchronized boolean isTransientSummon(@NotNull LivingEntity livingEntity) { + return getChunkLookupCache().contains(livingEntity); + } + + /** + * Get the tracked taming entities for a player + * If the player isn't registered this will return null + * + * @param playerUUID the target uuid of the player + * @param callOfTheWildType target type + * @return the set of tracked entities for the player, null if the player isn't registered, the set can be empty + */ + private synchronized @Nullable HashSet getTrackedEntities(@NotNull UUID playerUUID, @NotNull CallOfTheWildType callOfTheWildType) { + HashMap> playerEntityMap = getPlayerTrackedEntityMap(playerUUID); + + if(playerEntityMap == null) + return null; + + return playerEntityMap.get(callOfTheWildType); + } + + /** + * Adds an entity to our chunk lookup cache + * + * @param trackedTamingEntity target tracked taming entity + */ + private synchronized void addToChunkLookupCache(@NotNull TrackedTamingEntity trackedTamingEntity) { + getChunkLookupCache().add(trackedTamingEntity.getLivingEntity()); + } + + /** + * Removes an entity from our tracker + * This includes removal from our per-player map and our chunk lookup cache + * + * @param livingEntity target entity + */ + private void unregisterEntity(@NotNull LivingEntity livingEntity) { + chunkLookupCacheCleanup(livingEntity); + perPlayerTransientMapCleanup(livingEntity); + } + + /** + * Removes an entity from our chunk lookup cache + * + * @param livingEntity target entity + */ + private void chunkLookupCacheCleanup(@NotNull LivingEntity livingEntity) { + getChunkLookupCache().remove(livingEntity); + } + + /** + * Clean a living entity from our tracker + * Iterates over all players and their registered entities + * Doesn't do any kind of failure checking, if it doesn't find any player with a registered entity nothing bad happens or is reported + * However it should never happen like that, so maybe we could consider adding some failure to execute checking in the future + * + * @param livingEntity + */ + private void perPlayerTransientMapCleanup(@NotNull LivingEntity livingEntity) { + for(UUID uuid : getPerPlayerTransientEntityMap().keySet()) { + for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) { + + HashSet trackedEntities = getTrackedEntities(uuid, callOfTheWildType); + + if(trackedEntities == null) + continue; + + Iterator iterator = trackedEntities.iterator(); + while (iterator.hasNext()) { + if(iterator.next().getLivingEntity().equals(livingEntity)) { + iterator.remove(); + return; + } + } + } + } + } + + /** + * Get all transient entities that exist in a specific chunk + * + * @param chunk the chunk to match + * @return a list of transient entities that are located in the provided chunk + */ + public synchronized @NotNull List getAllTransientEntitiesInChunk(@NotNull Chunk chunk) { + ArrayList matchingEntities = new ArrayList<>(); + + for(LivingEntity livingEntity : getChunkLookupCache()) { + if(livingEntity.getLocation().getChunk().equals(chunk)) { + matchingEntities.add(livingEntity); + } + } + + return matchingEntities; + } + + /** + * Get the amount of a summon currently active for a player + * + * @param playerUUID target player + * @param callOfTheWildType summon type + * @return the amount of summons currently active for player of target type + */ + public synchronized int getAmountCurrentlySummoned(@NotNull UUID playerUUID, @NotNull CallOfTheWildType callOfTheWildType) { + HashSet trackedEntities = getTrackedEntities(playerUUID, callOfTheWildType); + + if(trackedEntities == null) + return 0; + + return trackedEntities.size(); + } + + /** + * Kills a summon and removes its metadata + * Then it removes it from the tracker / chunk lookup cache + * + * @param livingEntity entity to remove + * @param player associated player + */ + public synchronized void removeSummon(@NotNull LivingEntity livingEntity, @Nullable Player player, boolean timeExpired) { + //Kill the summon & remove it + if(livingEntity.isValid()) { + livingEntity.setHealth(0); //Should trigger entity death events + livingEntity.remove(); + + Location location = livingEntity.getLocation(); + + if (location.getWorld() != null) { + location.getWorld().playSound(location, Sound.BLOCK_FIRE_EXTINGUISH, 0.8F, 0.8F); + ParticleEffectUtils.playCallOfTheWildEffect(livingEntity); + } + + //Inform player of summon death + if(player != null && player.isOnline()) { + if(timeExpired) { + NotificationManager.sendPlayerInformationChatOnly(player, "Taming.Summon.COTW.TimeExpired", StringUtils.getPrettyEntityTypeString(livingEntity.getType())); + } else { + NotificationManager.sendPlayerInformationChatOnly(player, "Taming.Summon.COTW.Removed", StringUtils.getPrettyEntityTypeString(livingEntity.getType())); + } + } + } + + //Remove our metadata + mcMMO.getCompatibilityManager().getPersistentDataLayer().removeMobFlags(livingEntity); + + //Clean from trackers + unregisterEntity(livingEntity); + } + + /** + * Remove all tracked entities from existence if they currently exist + * Clear the tracked entity lists afterwards + * + * @deprecated use {@link #cleanupAllSummons(Player, UUID)} instead + */ + @Deprecated + private void cleanupAllSummons(@NotNull UUID playerUUID) { + cleanupAllSummons(Bukkit.getPlayer(playerUUID), playerUUID); + } + + /** + * Kills and cleans up all data related to all summoned entities for a player + * + * @param player used to send messages, can be null + * @param playerUUID used to grab associated data, cannot be null + */ + private void cleanupAllSummons(@Nullable Player player, @NotNull UUID playerUUID) { + for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) { + HashSet trackedEntities = getTrackedEntities(playerUUID, callOfTheWildType); + + if(trackedEntities == null) { + continue; + } + + ImmutableSet immutableSet = ImmutableSet.copyOf(trackedEntities); + + for(TrackedTamingEntity trackedTamingEntity : immutableSet) { + //Remove from existence + removeSummon(trackedTamingEntity.getLivingEntity(), player, false); + } + + } + } +} diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java index dce7ab174..272710aeb 100644 --- a/src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java +++ b/src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java @@ -2,32 +2,37 @@ package com.gmail.nossr50.util.blockmeta; import org.bukkit.Bukkit; import org.bukkit.World; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; 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; +public class BitSetChunkStore implements ChunkStore { 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 final int cx; + private final int cz; + private final int worldHeight; + private final @NotNull UUID worldUid; + // 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 final @NotNull BitSet store; + + private transient boolean dirty = false; + + public BitSetChunkStore(@NotNull World world, int cx, int cz) { + this(world.getUID(), world.getMaxHeight(), cx, cz); } - private BitSetChunkStore() {} + private BitSetChunkStore(@NotNull UUID worldUid, int worldHeight, int cx, int cz) { + this.cx = cx; + this.cz = cz; + this.worldUid = worldUid; + this.worldHeight = worldHeight; + this.store = new BitSet(16 * 16 * worldHeight); + } @Override public boolean isDirty() { @@ -50,7 +55,7 @@ public class BitSetChunkStore implements ChunkStore, Serializable { } @Override - public UUID getWorldId() { + public @NotNull UUID getWorldId() { return worldUid; } @@ -81,61 +86,27 @@ public class BitSetChunkStore implements ChunkStore, Serializable { } private int coordToIndex(int x, int y, int z) { + return coordToIndex(x, y, z, worldHeight); + } + + private static int coordToIndex(int x, int y, int z, int worldHeight) { if (x < 0 || x >= 16 || y < 0 || y >= worldHeight || z < 0 || z >= 16) - throw new IndexOutOfBoundsException(); + throw new IndexOutOfBoundsException(String.format("x: %d y: %d z: %d World Height: %d", x, y, z, worldHeight)); return (z * 16 + x) + (256 * y); } - private void fixWorldHeight() { + private static int getWorldHeight(@NotNull UUID worldUid, int storedWorldHeight) + { 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; + return storedWorldHeight; - // 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; + return world.getMaxHeight(); } - @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 { + private void serialize(@NotNull DataOutputStream out) throws IOException { out.writeInt(MAGIC_NUMBER); out.writeInt(CURRENT_VERSION); @@ -153,7 +124,7 @@ public class BitSetChunkStore implements ChunkStore, Serializable { dirty = false; } - private static BitSetChunkStore deserialize(DataInputStream in) throws IOException { + private static @NotNull BitSetChunkStore deserialize(@NotNull DataInputStream in) throws IOException { int magic = in.readInt(); // Can be used to determine the format of the file int fileVersionNumber = in.readInt(); @@ -161,28 +132,36 @@ public class BitSetChunkStore implements ChunkStore, Serializable { 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(); + UUID worldUid = new UUID(msb, lsb); + int cx = in.readInt(); + int cz = in.readInt(); - chunkStore.worldHeight = in.readInt(); + int worldHeight = in.readInt(); byte[] temp = new byte[in.readInt()]; in.readFully(temp); - chunkStore.store = BitSet.valueOf(temp); + BitSet stored = BitSet.valueOf(temp); + + int currentWorldHeight = getWorldHeight(worldUid, worldHeight); + + boolean worldHeightShrunk = currentWorldHeight < worldHeight; + // Lop off extra data if world height has shrunk + if (worldHeightShrunk) + stored.clear(coordToIndex(16, currentWorldHeight, 16, worldHeight), stored.length()); + + BitSetChunkStore chunkStore = new BitSetChunkStore(worldUid, currentWorldHeight, cx, cz); + chunkStore.store.or(stored); + chunkStore.dirty = worldHeightShrunk; // In the expanded case there is no reason to re-write it unless the data changes - chunkStore.fixWorldHeight(); return chunkStore; } public static class Serialization { - public static final short STREAM_MAGIC = (short)0xACDC; + public static final short STREAM_MAGIC = (short)0xACDC; // Rock on - public static ChunkStore readChunkStore(DataInputStream inputStream) throws IOException { + public static @Nullable ChunkStore readChunkStore(@NotNull DataInputStream inputStream) throws IOException { if (inputStream.markSupported()) inputStream.mark(2); short magicNumber = inputStream.readShort(); @@ -196,7 +175,7 @@ public class BitSetChunkStore implements ChunkStore, Serializable { { // 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) & 0xFF); pushbackInputStream.unread((magicNumber >>> 8) & 0xFF); inputStream = new DataInputStream(pushbackInputStream); } @@ -209,31 +188,85 @@ public class BitSetChunkStore implements ChunkStore, Serializable { throw new IOException("Bad Data Format"); } - public static void writeChunkStore(DataOutputStream outputStream, ChunkStore chunkStore) throws IOException { + public static void writeChunkStore(@NotNull DataOutputStream outputStream, @NotNull 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 + // Handles loading the old serialized class private static class LegacyDeserializationInputStream extends ObjectInputStream { - public LegacyDeserializationInputStream(InputStream in) throws IOException { + private static class LegacyChunkStoreDeserializer implements Serializable + { + private static final long serialVersionUID = -1L; + + private int cx; + private int cz; + private int worldHeight; + private UUID worldUid; + private boolean[][][] store; + + private LegacyChunkStoreDeserializer() {} + + @Deprecated + private void writeObject(@NotNull ObjectOutputStream out) throws IOException { + throw new UnsupportedOperationException("You goofed."); + } + + @Deprecated + private void readObject(@NotNull 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(); + + store = (boolean[][][]) in.readObject(); + worldHeight = store[0][0].length; + } + + public @NotNull BitSetChunkStore convert() + { + int currentWorldHeight = getWorldHeight(worldUid, worldHeight); + + BitSetChunkStore converted = new BitSetChunkStore(worldUid, currentWorldHeight, cx, cz); + + // Read old data into new chunkstore + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < worldHeight && y < currentWorldHeight; y++) { + converted.store.set(converted.coordToIndex(x, y, z), store[x][z][y]); + } + } + } + // Mark dirty so it will be re-written in new format on close + converted.dirty = true; + return converted; + } + } + + + public LegacyDeserializationInputStream(@NotNull InputStream in) throws IOException { super(in); enableResolveObject(true); } @Override - protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException { + protected @NotNull 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 ObjectStreamClass.lookup(LegacyChunkStoreDeserializer.class); return read; } - public ChunkStore readLegacyChunkStore(){ + public @Nullable ChunkStore readLegacyChunkStore(){ try { - return (ChunkStore) readObject(); + LegacyChunkStoreDeserializer deserializer = (LegacyChunkStoreDeserializer)readObject(); + return deserializer.convert(); } catch (IOException | ClassNotFoundException e) { return null; } diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java index 5cb50ac7f..a0b61ba6f 100644 --- a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java +++ b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java @@ -1,126 +1,10 @@ package com.gmail.nossr50.util.blockmeta; import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.block.BlockState; +import org.jetbrains.annotations.NotNull; -public interface ChunkManager { +public interface ChunkManager extends UserBlockTracker { void closeAll(); - - /** - * Saves a given Chunk's Chunklet data - * - * @param cx Chunk X coordinate that is to be saved - * @param cz Chunk Z coordinate that is to be saved - * @param world World that the Chunk is in - */ - void saveChunk(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); - - /** - * Save all ChunkletStores - */ - void saveAll(); - - /** - * 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); - - /** - * Check to see if a given BlockState location is set to true - * - * @param blockState BlockState to check - * @return true if the given BlockState location is set to true, false if otherwise - */ - boolean isTrue(BlockState blockState); - - /** - * 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 BlockState location to true, should create stores as necessary if the location does not exist - * - * @param blockState BlockState location to set - */ - void setTrue(BlockState blockState); - - /** - * 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); - - /** - * Set a given BlockState location to false, should not create stores if one does not exist for the given location - * - * @param blockState BlockState location to set - */ - void setFalse(BlockState blockState); - - /** - * Delete any ChunkletStores that are empty - */ - void cleanUp(); + void chunkUnloaded(int cx, int cz, @NotNull World world); + void unloadWorld(@NotNull World world); } diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManagerFactory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManagerFactory.java index a290c5e2a..e2c47662e 100644 --- a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManagerFactory.java +++ b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManagerFactory.java @@ -1,9 +1,10 @@ package com.gmail.nossr50.util.blockmeta; import com.gmail.nossr50.config.HiddenConfig; +import org.jetbrains.annotations.NotNull; public class ChunkManagerFactory { - public static ChunkManager getChunkManager() { + public static @NotNull ChunkManager getChunkManager() { HiddenConfig hConfig = HiddenConfig.getInstance(); if (hConfig.getChunkletsEnabled()) { diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java index eca783ccd..e21006628 100644 --- a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java +++ b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java @@ -1,6 +1,6 @@ package com.gmail.nossr50.util.blockmeta; -import org.bukkit.World; +import org.jetbrains.annotations.NotNull; import java.util.UUID; @@ -36,7 +36,7 @@ public interface ChunkStore { */ int getChunkZ(); - UUID getWorldId(); + @NotNull UUID getWorldId(); /** * Checks the value at the given coordinates diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java index 888937872..95397f89d 100644 --- a/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java +++ b/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java @@ -1,12 +1,16 @@ 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 org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.io.*; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; import java.util.*; public class HashChunkManager implements ChunkManager { @@ -21,7 +25,10 @@ public class HashChunkManager implements ChunkManager { { if (!chunkStore.isDirty()) continue; - writeChunkStore(Bukkit.getWorld(chunkStore.getWorldId()), chunkStore); + World world = Bukkit.getWorld(chunkStore.getWorldId()); + if (world == null) + continue; // Oh well + writeChunkStore(world, chunkStore); } // Clear in memory chunks chunkMap.clear(); @@ -32,7 +39,7 @@ public class HashChunkManager implements ChunkManager { regionMap.clear(); } - private synchronized ChunkStore readChunkStore(World world, int cx, int cz) throws IOException { + private synchronized @Nullable ChunkStore readChunkStore(@NotNull 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 @@ -43,7 +50,7 @@ public class HashChunkManager implements ChunkManager { } } - private synchronized void writeChunkStore(World world, ChunkStore data) { + private synchronized void writeChunkStore(@NotNull World world, @NotNull ChunkStore data) { if (!data.isDirty()) return; // Don't save unchanged data try { @@ -58,7 +65,7 @@ public class HashChunkManager implements ChunkManager { } } - private synchronized McMMOSimpleRegionFile getSimpleRegionFile(World world, int cx, int cz, boolean createIfAbsent) { + private synchronized @Nullable McMMOSimpleRegionFile getSimpleRegionFile(@NotNull World world, int cx, int cz, boolean createIfAbsent) { CoordinateKey regionKey = toRegionKey(world.getUID(), cx, cz); return regionMap.computeIfAbsent(regionKey, k -> { @@ -73,7 +80,7 @@ public class HashChunkManager implements ChunkManager { }); } - private ChunkStore loadChunk(int cx, int cz, World world) { + private @Nullable ChunkStore loadChunk(int cx, int cz, @NotNull World world) { try { return readChunkStore(world, cx, cz); } @@ -82,7 +89,7 @@ public class HashChunkManager implements ChunkManager { return null; } - private void unloadChunk(int cx, int cz, World world) { + private void unloadChunk(int cx, int cz, @NotNull World world) { CoordinateKey chunkKey = toChunkKey(world.getUID(), cx, cz); ChunkStore chunkStore = chunkMap.remove(chunkKey); // Remove from chunk map if (chunkStore == null) @@ -102,56 +109,12 @@ public class HashChunkManager implements ChunkManager { } @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; - + public synchronized void chunkUnloaded(int cx, int cz, @NotNull World world) { 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; - + public synchronized void unloadWorld(@NotNull World world) { UUID wID = world.getUID(); // Save and remove all the chunks @@ -177,18 +140,7 @@ public class HashChunkManager implements ChunkManager { } } - @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; - + private synchronized boolean isTrue(int x, int y, int z, @NotNull World world) { CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z); // Get chunk, load from file if necessary @@ -214,67 +166,36 @@ public class HashChunkManager implements ChunkManager { } @Override - public synchronized boolean isTrue(Block block) { - if (block == null) - return false; - + public synchronized boolean isTrue(@NotNull Block block) { return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld()); } @Override - public synchronized boolean isTrue(BlockState blockState) { - if (blockState == null) - return false; - + public synchronized boolean isTrue(@NotNull BlockState blockState) { 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); + public synchronized void setTrue(@NotNull Block block) { + set(block.getX(), block.getY(), block.getZ(), block.getWorld(), true); } @Override - public synchronized void setTrue(Block block) { - if (block == null) - return; - - setTrue(block.getX(), block.getY(), block.getZ(), block.getWorld()); + public synchronized void setTrue(@NotNull BlockState blockState) { + set(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld(), true); } @Override - public synchronized void setTrue(BlockState blockState) { - if (blockState == null) - return; - - setTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld()); + public synchronized void setFalse(@NotNull Block block) { + set(block.getX(), block.getY(), block.getZ(), block.getWorld(), false); } @Override - public synchronized void setFalse(int x, int y, int z, World world) { - set(x, y, z, world, false); + public synchronized void setFalse(@NotNull BlockState blockState) { + set(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld(), 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; - + private synchronized void set(int x, int y, int z, @NotNull World world, boolean value){ CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z); // Get/Load/Create chunkstore @@ -307,15 +228,15 @@ public class HashChunkManager implements ChunkManager { cStore.set(ix, y, iz, value); } - private CoordinateKey blockCoordinateToChunkKey(UUID worldUid, int x, int y, int z) { + private CoordinateKey blockCoordinateToChunkKey(@NotNull UUID worldUid, int x, int y, int z) { return toChunkKey(worldUid, x >> 4, z >> 4); } - private CoordinateKey toChunkKey(UUID worldUid, int cx, int cz){ + private CoordinateKey toChunkKey(@NotNull UUID worldUid, int cx, int cz){ return new CoordinateKey(worldUid, cx, cz); } - private CoordinateKey toRegionKey(UUID worldUid, int cx, int cz) { + private CoordinateKey toRegionKey(@NotNull UUID worldUid, int cx, int cz) { // Compute region index (32x32 chunk regions) int rx = cx >> 5; int rz = cz >> 5; @@ -323,11 +244,11 @@ public class HashChunkManager implements ChunkManager { } private static final class CoordinateKey { - public final UUID worldID; + public final @NotNull UUID worldID; public final int x; public final int z; - private CoordinateKey(UUID worldID, int x, int z) { + private CoordinateKey(@NotNull UUID worldID, int x, int z) { this.worldID = worldID; this.x = x; this.z = z; @@ -348,7 +269,4 @@ public class HashChunkManager implements ChunkManager { return Objects.hash(worldID, x, z); } } - - @Override - public synchronized void cleanUp() {} } diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java b/src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java index bef730ff4..3f61c9bef 100644 --- a/src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java +++ b/src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java @@ -19,6 +19,9 @@ */ package com.gmail.nossr50.util.blockmeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.io.*; import java.util.BitSet; import java.util.zip.DeflaterOutputStream; @@ -54,7 +57,7 @@ public class McMMOSimpleRegionFile { private final int segmentMask; // File location - private final File parent; + private final @NotNull File parent; // File access private final RandomAccessFile file; @@ -62,7 +65,7 @@ public class McMMOSimpleRegionFile { private final int rx; private final int rz; - public McMMOSimpleRegionFile(File f, int rx, int rz) { + public McMMOSimpleRegionFile(@NotNull File f, int rx, int rz) { this.rx = rx; this.rz = rz; this.parent = f; @@ -104,7 +107,7 @@ public class McMMOSimpleRegionFile { } } - public synchronized DataOutputStream getOutputStream(int x, int z) { + public synchronized @NotNull DataOutputStream getOutputStream(int x, int z) { int index = getChunkIndex(x, z); // Get chunk index return new DataOutputStream(new DeflaterOutputStream(new McMMOSimpleChunkBuffer(this, index))); } @@ -144,7 +147,7 @@ public class McMMOSimpleRegionFile { file.writeInt(chunkNumBytes[index]); } - public synchronized DataInputStream getInputStream(int x, int z) throws IOException { + public synchronized @Nullable 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 diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java index b777fa349..203376780 100644 --- a/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java +++ b/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java @@ -3,6 +3,7 @@ package com.gmail.nossr50.util.blockmeta; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockState; +import org.jetbrains.annotations.NotNull; public class NullChunkManager implements ChunkManager { @@ -10,53 +11,30 @@ public class NullChunkManager implements ChunkManager { public void closeAll() {} @Override - public void saveChunk(int cx, int cz, World world) {} + public void chunkUnloaded(int cx, int cz, @NotNull World world) {} @Override - public void chunkUnloaded(int cx, int cz, World world) {} + public void unloadWorld(@NotNull World world) {} @Override - public void saveWorld(World world) {} - - @Override - public void unloadWorld(World world) {} - - @Override - public void saveAll() {} - - @Override - public boolean isTrue(int x, int y, int z, World world) { + public boolean isTrue(@NotNull Block block) { return false; } @Override - public boolean isTrue(Block block) { + public boolean isTrue(@NotNull BlockState blockState) { return false; } @Override - public boolean isTrue(BlockState blockState) { - return false; - } + public void setTrue(@NotNull Block block) {} @Override - public void setTrue(int x, int y, int z, World world) {} + public void setTrue(@NotNull BlockState blockState) {} @Override - public void setTrue(Block block) {} + public void setFalse(@NotNull Block block) {} @Override - public void setTrue(BlockState blockState) {} - - @Override - public void setFalse(int x, int y, int z, World world) {} - - @Override - public void setFalse(Block block) {} - - @Override - public void setFalse(BlockState blockState) {} - - @Override - public void cleanUp() {} + public void setFalse(@NotNull BlockState blockState) {} } diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/UserBlockTracker.java b/src/main/java/com/gmail/nossr50/util/blockmeta/UserBlockTracker.java new file mode 100644 index 000000000..e5428b0c1 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/util/blockmeta/UserBlockTracker.java @@ -0,0 +1,56 @@ +package com.gmail.nossr50.util.blockmeta; + +import com.gmail.nossr50.mcMMO; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.jetbrains.annotations.NotNull; + +/** + * Contains blockstore methods that are safe for external plugins to access. + * An instance can be retrieved via {@link mcMMO#getPlaceStore() mcMMO.getPlaceStore()} + */ +public interface UserBlockTracker { + /** + * 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(@NotNull Block block); + + /** + * Check to see if a given BlockState location is set to true + * + * @param blockState BlockState to check + * @return true if the given BlockState location is set to true, false if otherwise + */ + boolean isTrue(@NotNull BlockState blockState); + + /** + * Set a given block location to true + * + * @param block Block location to set + */ + void setTrue(@NotNull Block block); + + /** + * Set a given BlockState location to true + * + * @param blockState BlockState location to set + */ + void setTrue(@NotNull BlockState blockState); + + /** + * Set a given block location to false + * + * @param block Block location to set + */ + void setFalse(@NotNull Block block); + + /** + * Set a given BlockState location to false + * + * @param blockState BlockState location to set + */ + void setFalse(@NotNull BlockState blockState); +} diff --git a/src/main/java/com/gmail/nossr50/util/skills/PerksUtils.java b/src/main/java/com/gmail/nossr50/util/skills/PerksUtils.java index 44d37df8b..09738f2ea 100644 --- a/src/main/java/com/gmail/nossr50/util/skills/PerksUtils.java +++ b/src/main/java/com/gmail/nossr50/util/skills/PerksUtils.java @@ -2,8 +2,12 @@ package com.gmail.nossr50.util.skills; import com.gmail.nossr50.config.experience.ExperienceConfig; import com.gmail.nossr50.datatypes.skills.PrimarySkillType; +import com.gmail.nossr50.events.skills.SkillActivationPerkEvent; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.util.Permissions; +import com.gmail.nossr50.util.player.UserManager; + +import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -43,7 +47,9 @@ public final class PerksUtils { ticks += 4; } - return ticks; + final SkillActivationPerkEvent skillActivationPerkEvent = new SkillActivationPerkEvent(player, ticks, maxTicks); + Bukkit.getPluginManager().callEvent(skillActivationPerkEvent); + return skillActivationPerkEvent.getTicks(); } public static float handleXpPerks(Player player, float xp, PrimarySkillType skill) { diff --git a/src/main/resources/fishing_treasures.yml b/src/main/resources/fishing_treasures.yml index d839da2fd..3b9f796dc 100644 --- a/src/main/resources/fishing_treasures.yml +++ b/src/main/resources/fishing_treasures.yml @@ -207,50 +207,6 @@ Fishing: 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 diff --git a/src/main/resources/locale/locale_cs_CZ.properties b/src/main/resources/locale/locale_cs_CZ.properties index b6be2e519..d56a7d4b7 100644 --- a/src/main/resources/locale/locale_cs_CZ.properties +++ b/src/main/resources/locale/locale_cs_CZ.properties @@ -90,7 +90,6 @@ Fishing.SubSkill.Shake.Description=Vyklepni p\u0159edm\u011bty z p\u0159\u00ed\u Fishing.SubSkill.FishermansDiet.Name=Ryb\u00e1\u0159\u016fv apetit Fishing.SubSkill.FishermansDiet.Description=Zlep\u0161uje dopl\u0148ov\u00e1n\u00ed hladu z naryba\u0159en\u00fdch j\u00eddel Fishing.SubSkill.MasterAngler.Name=Mistr Ryb\u00e1\u0159 -Fishing.SubSkill.MasterAngler.Description=Zvy\u0161uje \u0161anci zah\u00e1knut\u00ed ryby p\u0159i ryba\u0159en\u00ed Fishing.SubSkill.IceFishing.Name=Ryba\u0159en\u00ed v ledu Fishing.SubSkill.IceFishing.Description=Umo\u017e\u0148uje v\u00e1m ryba\u0159it v ledov\u00fdch prost\u0159ed\u00edch Fishing.Chance.Raining=&9 De\u0161\u0165ov\u00fd bonus diff --git a/src/main/resources/locale/locale_de.properties b/src/main/resources/locale/locale_de.properties index d6525a129..108e9cb63 100644 --- a/src/main/resources/locale/locale_de.properties +++ b/src/main/resources/locale/locale_de.properties @@ -372,7 +372,6 @@ Fishing.SubSkill.IceFishing.Stat = Eisangeln Fishing.SubSkill.MagicHunter.Description = Finde verzauberte Gegenst\u00E4nde Fishing.SubSkill.MagicHunter.Name = Zauber J\u00E4ger Fishing.SubSkill.MagicHunter.Stat = Zauber J\u00E4ger Chance -Fishing.SubSkill.MasterAngler.Description = Erh\u00F6ht die Chance des Anbei\u00DFens beim Angeln Fishing.SubSkill.MasterAngler.Name = Superangel Fishing.SubSkill.Shake.Description = Rei\u00DFe Gegenst\u00E4nde weg von Lebewesen und Spielern mit deiner Angel Fishing.SubSkill.Shake.Name = Rei\u00DFen diff --git a/src/main/resources/locale/locale_en_US.properties b/src/main/resources/locale/locale_en_US.properties index 48b4b0e95..43426e10a 100644 --- a/src/main/resources/locale/locale_en_US.properties +++ b/src/main/resources/locale/locale_en_US.properties @@ -258,7 +258,7 @@ Fishing.SubSkill.FishermansDiet.Name=Fisherman's Diet Fishing.SubSkill.FishermansDiet.Description=Improves hunger restored from fished foods Fishing.SubSkill.FishermansDiet.Stat=Fisherman's Diet:&a Rank {0} Fishing.SubSkill.MasterAngler.Name=Master Angler -Fishing.SubSkill.MasterAngler.Description=Fish are caught more frequently +Fishing.SubSkill.MasterAngler.Description=Fish are caught more frequently, works better when fishing from a boat. Fishing.SubSkill.MasterAngler.Stat=Fishing min wait time reduction: &a-{0} seconds Fishing.SubSkill.MasterAngler.Stat.Extra=Fishing max wait time reduction: &a-{0} seconds Fishing.SubSkill.IceFishing.Name=Ice Fishing @@ -493,6 +493,7 @@ Taming.Summon.COTW.Success.WithoutLifespan=&a(Call Of The Wild) &7You have summo Taming.Summon.COTW.Success.WithLifespan=&a(Call Of The Wild) &7You have summoned a &6{0}&7 and it has a duration of &6{1}&7 seconds. Taming.Summon.COTW.Limit=&a(Call Of The Wild) &7You can only have &c{0} &7summoned &7{1} pets at the same time. Taming.Summon.COTW.TimeExpired=&a(Call Of The Wild) &7Time is up, your &6{0}&7 departs. +Taming.Summon.COTW.Removed=&a(Call Of The Wild) &7Your summoned &6{0}&7 has vanished from this world. Taming.Summon.COTW.BreedingDisallowed=&a(Call Of The Wild) &cYou cannot breed a summoned animal. Taming.Summon.COTW.NeedMoreItems=&a(Call Of The Wild) &7You need &e{0}&7 more &3{1}&7(s) Taming.Summon.Name.Format=&6(COTW) &f{0}'s {1} diff --git a/src/main/resources/locale/locale_es.properties b/src/main/resources/locale/locale_es.properties index 6aa857cc3..cdce790bd 100644 --- a/src/main/resources/locale/locale_es.properties +++ b/src/main/resources/locale/locale_es.properties @@ -91,7 +91,6 @@ Fishing.SubSkill.Shake.Description=Sacudir los items fuera de los monstruos con Fishing.SubSkill.FishermansDiet.Name=Dieta del pescador Fishing.SubSkill.FishermansDiet.Description=Mejora el hambre restaurada a partir de alimentos pescados Fishing.SubSkill.MasterAngler.Name=Maestro pescador -Fishing.SubSkill.MasterAngler.Description=Aumenta la probabilidad de ser mordido mientras se pesca Fishing.SubSkill.IceFishing.Name=Pesca de hielo Fishing.SubSkill.IceFishing.Description=Te permite pescar en biomas de hielo Fishing.Chance.Raining=&9 Lluvia de Bonus diff --git a/src/main/resources/locale/locale_fr.properties b/src/main/resources/locale/locale_fr.properties index c773ad65c..c69bd5eb1 100644 --- a/src/main/resources/locale/locale_fr.properties +++ b/src/main/resources/locale/locale_fr.properties @@ -251,7 +251,6 @@ Fishing.SubSkill.FishermansDiet.Name=R\u00e9gime de fermier Fishing.SubSkill.FishermansDiet.Description=Am\u00e9liore la nutrition des produits de la ferme Fishing.SubSkill.FishermansDiet.Stat=R\u00e9gime de fermier:&a Grade {0} Fishing.SubSkill.MasterAngler.Name=Ma\u00eetre P\u00eacheur -Fishing.SubSkill.MasterAngler.Description=Augmente les chances que \u00e7a morde lors de la p\u00eache Fishing.SubSkill.IceFishing.Name=P\u00eache sur Glace Fishing.SubSkill.IceFishing.Description=Vous permet de p\u00eacher dans les biomes glac\u00e9s Fishing.SubSkill.IceFishing.Stat=P\u00eache sur Glace diff --git a/src/main/resources/locale/locale_hu_HU.properties b/src/main/resources/locale/locale_hu_HU.properties index 71f11514c..60776b3f7 100644 --- a/src/main/resources/locale/locale_hu_HU.properties +++ b/src/main/resources/locale/locale_hu_HU.properties @@ -251,7 +251,6 @@ Fishing.SubSkill.FishermansDiet.Name=Horg\u00E1szok Di\u00E9t\u00E1ja Fishing.SubSkill.FishermansDiet.Description=N\u00F6veli a kihal\u00E1szott \u00E9telek t\u00E1p\u00E9rt\u00E9k\u00E9t Fishing.SubSkill.FishermansDiet.Stat=Horg\u00E1szok Di\u00E9t\u00E1ja:&a Szint {0} Fishing.SubSkill.MasterAngler.Name=Mester Horg\u00E1sz -Fishing.SubSkill.MasterAngler.Description=N\u00F6veli a Kap\u00E1s es\u00E9ly\u00E9t horg\u00E1szat k\u00F6zben Fishing.SubSkill.IceFishing.Name=J\u00E9g Horg\u00E1szat Fishing.SubSkill.IceFishing.Description=Lehet\u0151v\u00E9 teszi sz\u00E1modra, hogy fagyos t\u00E1jakon is horg\u00E1szhass Fishing.SubSkill.IceFishing.Stat=J\u00E9g Horg\u00E1szat diff --git a/src/main/resources/locale/locale_it.properties b/src/main/resources/locale/locale_it.properties index c5f293733..d6df1ef1e 100644 --- a/src/main/resources/locale/locale_it.properties +++ b/src/main/resources/locale/locale_it.properties @@ -258,7 +258,6 @@ Fishing.SubSkill.FishermansDiet.Name=Dieta del Pescatore Fishing.SubSkill.FishermansDiet.Description=Aumenta la fame recuperata tramite cibi pescati Fishing.SubSkill.FishermansDiet.Stat=Dieta del Pescatore:&a Grado {0} Fishing.SubSkill.MasterAngler.Name=Pescatore Provetto -Fishing.SubSkill.MasterAngler.Description=Migliora la possibilit\u00E0 di ottenere un morso durante la pesca Fishing.SubSkill.IceFishing.Name=Pesca sul Ghiaccio Fishing.SubSkill.IceFishing.Description=Ti permette di pescare in biomi ghiacciati Fishing.SubSkill.IceFishing.Stat=Pesca sul Ghiaccio diff --git a/src/main/resources/locale/locale_ja_JP.properties b/src/main/resources/locale/locale_ja_JP.properties index 1064c29ac..f93cda4be 100644 --- a/src/main/resources/locale/locale_ja_JP.properties +++ b/src/main/resources/locale/locale_ja_JP.properties @@ -241,7 +241,6 @@ Fishing.SubSkill.FishermansDiet.Name=\u6f01\u5e2b\u306e\u98df\u4e8b Fishing.SubSkill.FishermansDiet.Description=\u9b5a\u4ecb\u985e\u304b\u3089\u56de\u5fa9\u3059\u308b\u6e80\u8179\u5ea6\u3092\u6539\u5584\u3059\u308b\u3002 Fishing.SubSkill.FishermansDiet.Stat=\u6f01\u5e2b\u306e\u98df\u4e8b:&a \u30e9\u30f3\u30af {0} Fishing.SubSkill.MasterAngler.Name=\u30de\u30b9\u30bf\u30fc\u30a2\u30f3\u30b0\u30e9\u30fc -Fishing.SubSkill.MasterAngler.Description=\u91e3\u308c\u308b\u78ba\u7387\u304c\u4e0a\u304c\u308a\u307e\u3059\u3002 Fishing.SubSkill.IceFishing.Name=\u7a74\u91e3\u308a Fishing.SubSkill.IceFishing.Description=\u5bd2\u3044\u30d0\u30a4\u30aa\u30fc\u30e0\u3067\u306e\u91e3\u308a\u304c\u3067\u304d\u308b\u3088\u3046\u306b\u306a\u308b\u3002 Fishing.SubSkill.IceFishing.Stat=\u7a74\u91e3\u308a diff --git a/src/main/resources/locale/locale_ko.properties b/src/main/resources/locale/locale_ko.properties index 6a5865391..e6f9e7c30 100644 --- a/src/main/resources/locale/locale_ko.properties +++ b/src/main/resources/locale/locale_ko.properties @@ -132,7 +132,6 @@ Fishing.SubSkill.Shake.Description=\uC544\uC774\uD15C\uC744 \uBAB9\uC774\uB098 \ Fishing.SubSkill.FishermansDiet.Name=\uC5B4\uBD80\uC758 \uB2E4\uC774\uC5B4\uD2B8 Fishing.SubSkill.FishermansDiet.Description=\uC5B4\uB958 \uC74C\uC2DD \uD5C8\uAE30 \uD68C\uBCF5 \uC99D\uAC00 Fishing.SubSkill.MasterAngler.Name=\uB09A\uC2DC\uAFBC \uC7A5\uC778 -Fishing.SubSkill.MasterAngler.Description=\uB09A\uC2DC\uC911 \uC785\uC9C8 \uD655\uB960 \uC99D\uAC00 Fishing.SubSkill.IceFishing.Name=\uC5BC\uC74C \uB09A\uC2DC Fishing.SubSkill.IceFishing.Description=\uC5BC\uC74C\uC774 \uB36E\uD600\uC788\uB294 \uD658\uACBD\uC5D0\uC11C \uB09A\uC2DC \uAC00\uB2A5 Fishing.Chance.Raining=&9 \uBE44 \uD2B9\uD61C diff --git a/src/main/resources/locale/locale_lt_LT.properties b/src/main/resources/locale/locale_lt_LT.properties index 80e26d8e4..f9d8bb9e4 100644 --- a/src/main/resources/locale/locale_lt_LT.properties +++ b/src/main/resources/locale/locale_lt_LT.properties @@ -251,7 +251,6 @@ Fishing.SubSkill.FishermansDiet.Name=Fisherman's Diet Fishing.SubSkill.FishermansDiet.Description=Improves hunger restored from fished foods Fishing.SubSkill.FishermansDiet.Stat=Fisherman's Diet:&a Rank {0} Fishing.SubSkill.MasterAngler.Name=Master Angler -Fishing.SubSkill.MasterAngler.Description=Improves chance of getting a bite while fishing Fishing.SubSkill.IceFishing.Name=Ice Fishing Fishing.SubSkill.IceFishing.Description=Allows you to fish in icy biomes Fishing.SubSkill.IceFishing.Stat=Ice Fishing diff --git a/src/main/resources/locale/locale_nl.properties b/src/main/resources/locale/locale_nl.properties index cd2ae6e67..19dea9c04 100644 --- a/src/main/resources/locale/locale_nl.properties +++ b/src/main/resources/locale/locale_nl.properties @@ -71,7 +71,6 @@ Fishing.SubSkill.Shake.Description=Schud items af van mobs w/ hengel Fishing.SubSkill.FishermansDiet.Name=Visserman\'s dieet Fishing.SubSkill.FishermansDiet.Description=Verbetert de honger hersteld vanaf geviste voedingsmiddelen Fishing.SubSkill.MasterAngler.Name=Meester Hengelaar -Fishing.SubSkill.MasterAngler.Description=Verbetert de kans op het bijten tijdens het vissen Fishing.SubSkill.IceFishing.Name=Ijs Vissen Fishing.SubSkill.IceFishing.Description=Stelt je in staat om te vissen in de ijzige biomen Fishing.Chance.Raining=&9 Regen Bonus diff --git a/src/main/resources/locale/locale_pl.properties b/src/main/resources/locale/locale_pl.properties index 960803ebf..cf4f89579 100644 --- a/src/main/resources/locale/locale_pl.properties +++ b/src/main/resources/locale/locale_pl.properties @@ -89,7 +89,6 @@ Fishing.SubSkill.Shake.Name=Potrz\u0105\u015bni\u0119cie (przeciwko jednostkom) Fishing.SubSkill.Shake.Description=Okradaj potwory z przedmiot\u00f3w u\u017cywaj\u0105c w\u0119dki. Fishing.SubSkill.FishermansDiet.Name=Dieta Rybaka Fishing.SubSkill.FishermansDiet.Description=Zwi\u0119ksza nasycenie posi\u0142k\u00f3w (ryby) -Fishing.SubSkill.MasterAngler.Description=Zwieksza szanse na zlapanie ryby na haczyk Fishing.SubSkill.IceFishing.Name=Lodowe lowienie ryb Fishing.SubSkill.IceFishing.Description=Pozwala na lowienie ryb w zimowych biomach Fishing.Chance.Raining=&9 Bonus od Deszczu diff --git a/src/main/resources/locale/locale_ru.properties b/src/main/resources/locale/locale_ru.properties index 9d36e5e2d..7510220b4 100644 --- a/src/main/resources/locale/locale_ru.properties +++ b/src/main/resources/locale/locale_ru.properties @@ -199,7 +199,6 @@ Fishing.SubSkill.FishermansDiet.Name=\u0420\u044b\u0431\u0430\u0446\u043a\u0430\ Fishing.SubSkill.FishermansDiet.Description=\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0443\u0442\u043e\u043b\u0435\u043d\u0438\u0435 \u0433\u043e\u043b\u043e\u0434\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0440\u044b\u0431\u0430\u0446\u043a\u043e\u0439 \u0435\u0434\u044b Fishing.SubSkill.FishermansDiet.Stat=\u0420\u044b\u0431\u0430\u0446\u043a\u0430\u044f \u0414\u0438\u0435\u0442\u0430:&a \u0420\u0430\u043d\u0433 {0} Fishing.SubSkill.MasterAngler.Name=\u041c\u0430\u0441\u0442\u0435\u0440 \u0420\u044b\u0431\u043e\u043b\u043e\u0432 -Fishing.SubSkill.MasterAngler.Description=\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0448\u0430\u043d\u0441 \u043f\u043e\u043a\u043b\u0435\u0432\u043a\u0438 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0440\u044b\u0431\u0430\u043b\u043a\u0438 Fishing.SubSkill.IceFishing.Name=\u041f\u043e\u0434\u043b\u0435\u0434\u043d\u0430\u044f \u0420\u044b\u0431\u0430\u043b\u043a\u0430 Fishing.SubSkill.IceFishing.Description=\u041f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u0430\u043c \u0440\u044b\u0431\u0430\u0447\u0438\u0442\u044c \u0432 \u0441\u043d\u0435\u0436\u043d\u044b\u0445 \u0431\u0438\u043e\u043c\u0430\u0445 Fishing.SubSkill.IceFishing.Stat=\u041f\u043e\u0434\u043b\u0435\u0434\u043d\u0430\u044f \u0420\u044b\u0431\u0430\u043b\u043a\u0430 diff --git a/src/main/resources/locale/locale_th_TH.properties b/src/main/resources/locale/locale_th_TH.properties index 1882de9eb..56695b922 100644 --- a/src/main/resources/locale/locale_th_TH.properties +++ b/src/main/resources/locale/locale_th_TH.properties @@ -90,7 +90,6 @@ Fishing.SubSkill.Shake.Description=\u0e40\u0e02\u0e22\u0e48\u0e32\u0e40\u0e2d\u0 Fishing.SubSkill.FishermansDiet.Name=Fisherman\'s Diet Fishing.SubSkill.FishermansDiet.Description=\u0e40\u0e1e\u0e34\u0e48\u0e21\u0e04\u0e27\u0e32\u0e21\u0e2d\u0e34\u0e48\u0e21\u0e08\u0e32\u0e01\u0e01\u0e32\u0e23\u0e01\u0e34\u0e19\u0e1b\u0e25\u0e32 Fishing.SubSkill.MasterAngler.Name=Master Angler -Fishing.SubSkill.MasterAngler.Description=\u0e40\u0e1e\u0e34\u0e48\u0e21\u0e42\u0e2d\u0e01\u0e32\u0e2a\u0e44\u0e14\u0e49\u0e23\u0e31\u0e1a\u0e1b\u0e25\u0e32\u0e21\u0e32\u0e01\u0e02\u0e36\u0e49\u0e19\u0e08\u0e32\u0e01\u0e01\u0e32\u0e23\u0e15\u0e01\u0e1b\u0e25\u0e32 Fishing.SubSkill.IceFishing.Name=Ice Fishing Fishing.SubSkill.IceFishing.Description=\u0e2d\u0e19\u0e38\u0e0d\u0e32\u0e15\u0e34\u0e43\u0e2b\u0e49\u0e15\u0e01\u0e1b\u0e25\u0e32\u0e43\u0e19\u0e19\u0e49\u0e33\u0e41\u0e02\u0e47\u0e07 Fishing.Chance.Raining=&9 Rain Bonus diff --git a/src/main/resources/locale/locale_zh_CN.properties b/src/main/resources/locale/locale_zh_CN.properties index 7557b6cd3..338737994 100644 --- a/src/main/resources/locale/locale_zh_CN.properties +++ b/src/main/resources/locale/locale_zh_CN.properties @@ -251,7 +251,6 @@ Fishing.SubSkill.FishermansDiet.Name=\u6e14\u592b\u7684\u98df\u8c31 Fishing.SubSkill.FishermansDiet.Description=\u63d0\u9ad8\u9c7c\u7c7b\u98df\u7269\u6062\u590d\u7684\u9971\u98df\u5ea6 Fishing.SubSkill.FishermansDiet.Stat=\u6e14\u592b\u7684\u98df\u8c31:&a \u7b49\u7ea7 {0} Fishing.SubSkill.MasterAngler.Name=\u9493\u9c7c\u5927\u5e08 -Fishing.SubSkill.MasterAngler.Description=\u63d0\u9ad8\u9493\u9c7c\u54ac\u94a9\u51e0\u7387 Fishing.SubSkill.IceFishing.Name=\u51b0\u9493 Fishing.SubSkill.IceFishing.Description=\u5141\u8bb8\u4f60\u5728\u51b0\u51b7\u7684\u73af\u5883\u4e0b\u9493\u9c7c Fishing.SubSkill.IceFishing.Stat=\u51b0\u9493 diff --git a/src/main/resources/locale/locale_zh_TW.properties b/src/main/resources/locale/locale_zh_TW.properties index f20ff761f..01a0c15ac 100644 --- a/src/main/resources/locale/locale_zh_TW.properties +++ b/src/main/resources/locale/locale_zh_TW.properties @@ -93,7 +93,6 @@ Fishing.SubSkill.Shake.Description=\u7528\u91e3\u7aff\u628a\u602a\u7269\u7684\u7 Fishing.SubSkill.FishermansDiet.Name=\u6f01\u4eba\u4fbf\u7576 Fishing.SubSkill.FishermansDiet.Description=\u98df\u7528\u9b5a\u98df\u54c1\u6642\u984d\u5916\u6062\u5fa9\u98fd\u98df\u5ea6 Fishing.SubSkill.MasterAngler.Name=\u5782\u91e3\u5927\u5e2b -Fishing.SubSkill.MasterAngler.Description=\u589e\u52a0\u5728\u91e3\u9b5a\u6642\u4e0a\u9264\u7684\u6a5f\u7387 Fishing.SubSkill.IceFishing.Name=\u51b0\u91e3 Fishing.SubSkill.IceFishing.Description=\u5141\u8a31\u4f60\u5728\u51b0\u5929\u96ea\u5730\u88e1\u91e3\u9b5a Fishing.Chance.Raining=&9 \u5927\u91cf\u734e\u52f5 diff --git a/src/test/java/ChunkStoreTest.java b/src/test/java/ChunkStoreTest.java index f45b0e0c9..2290ee103 100644 --- a/src/test/java/ChunkStoreTest.java +++ b/src/test/java/ChunkStoreTest.java @@ -2,6 +2,9 @@ import com.gmail.nossr50.util.blockmeta.*; import com.google.common.io.Files; import org.bukkit.Bukkit; import org.bukkit.World; +import org.bukkit.block.Block; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.*; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -140,16 +143,27 @@ public class ChunkStoreTest { @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)); + Block mockBlockA = mock(Block.class); + Mockito.when(mockBlockA.getX()).thenReturn(15); + Mockito.when(mockBlockA.getZ()).thenReturn(15); + Mockito.when(mockBlockA.getY()).thenReturn(0); + Mockito.when(mockBlockA.getWorld()).thenReturn(mockWorld); + Block mockBlockB = mock(Block.class); + Mockito.when(mockBlockB.getX()).thenReturn(-15); + Mockito.when(mockBlockB.getZ()).thenReturn(-15); + Mockito.when(mockBlockB.getY()).thenReturn(0); + Mockito.when(mockBlockB.getWorld()).thenReturn(mockWorld); + + chunkManager.setTrue(mockBlockA); + chunkManager.setFalse(mockBlockB); + Assert.assertTrue(chunkManager.isTrue(mockBlockA)); } private interface Delegate { void run(); } - private void assertThrows(Delegate delegate, Class clazz) { + private void assertThrows(@NotNull Delegate delegate, @NotNull Class clazz) { try { delegate.run(); Assert.fail(); // We didn't throw @@ -170,7 +184,7 @@ public class ChunkStoreTest { Assert.assertTrue(expected.isTrue(x, y, z) == actual.isTrue(x, y, z)); } - private static void recursiveDelete(File directoryToBeDeleted) { + private static void recursiveDelete(@NotNull File directoryToBeDeleted) { if (directoryToBeDeleted.isDirectory()) { for (File file : directoryToBeDeleted.listFiles()) { recursiveDelete(file); @@ -179,7 +193,7 @@ public class ChunkStoreTest { directoryToBeDeleted.delete(); } - private static byte[] serializeChunkstore(ChunkStore chunkStore) throws IOException { + private static byte[] serializeChunkstore(@NotNull ChunkStore chunkStore) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); if (chunkStore instanceof BitSetChunkStore) BitSetChunkStore.Serialization.writeChunkStore(new DataOutputStream(byteArrayOutputStream), chunkStore); @@ -188,18 +202,17 @@ public class ChunkStoreTest { 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; + private final int cx; + private final int cz; + private final @NotNull UUID worldUid; - public LegacyChunkStore(World world, int cx, int cz) { + public LegacyChunkStore(@NotNull World world, int cx, int cz) { this.cx = cx; this.cz = cz; this.worldUid = world.getUID(); @@ -227,7 +240,7 @@ public class ChunkStoreTest { } @Override - public UUID getWorldId() { + public @NotNull UUID getWorldId() { return worldUid; } @@ -274,7 +287,7 @@ public class ChunkStoreTest { return true; } - private void writeObject(ObjectOutputStream out) throws IOException { + private void writeObject(@NotNull ObjectOutputStream out) throws IOException { out.writeInt(MAGIC_NUMBER); out.writeInt(CURRENT_VERSION); @@ -287,18 +300,18 @@ public class ChunkStoreTest { dirty = false; } - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + private void readObject(@NotNull ObjectInputStream in) throws IOException, ClassNotFoundException { throw new UnsupportedOperationException(); } } private static class UnitTestObjectOutputStream extends ObjectOutputStream { - public UnitTestObjectOutputStream(OutputStream outputStream) throws IOException { + public UnitTestObjectOutputStream(@NotNull OutputStream outputStream) throws IOException { super(outputStream); } @Override - public void writeUTF(String str) throws IOException { + public void writeUTF(@NotNull String str) throws IOException { // Pretend to be the old class if (str.equals(LegacyChunkStore.class.getName())) str = "com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore";