Browse Source

Merge branch 'master' of github.com:mcMMO-Dev/mcMMO into tridentsxbows

nossr50 4 years ago
parent
commit
8a91791d2d
35 changed files with 393 additions and 307 deletions
  1. 1 0
      Changelog.txt
  2. 1 1
      src/main/java/com/gmail/nossr50/commands/experience/AddlevelsCommand.java
  3. 1 1
      src/main/java/com/gmail/nossr50/commands/experience/AddxpCommand.java
  4. 1 1
      src/main/java/com/gmail/nossr50/commands/experience/ExperienceCommand.java
  5. 1 1
      src/main/java/com/gmail/nossr50/commands/experience/MmoeditCommand.java
  6. 2 2
      src/main/java/com/gmail/nossr50/commands/experience/SkillresetCommand.java
  7. 1 1
      src/main/java/com/gmail/nossr50/commands/hardcore/HardcoreCommand.java
  8. 3 3
      src/main/java/com/gmail/nossr50/commands/skills/SkillCommand.java
  9. 2 2
      src/main/java/com/gmail/nossr50/commands/skills/SkillGuideCommand.java
  10. 0 1
      src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java
  11. 41 0
      src/main/java/com/gmail/nossr50/datatypes/BlockLocationHistory.java
  12. 0 57
      src/main/java/com/gmail/nossr50/datatypes/LimitedSizeList.java
  13. 5 1
      src/main/java/com/gmail/nossr50/datatypes/skills/PrimarySkillType.java
  14. 1 1
      src/main/java/com/gmail/nossr50/listeners/PlayerListener.java
  15. 0 4
      src/main/java/com/gmail/nossr50/listeners/WorldListener.java
  16. 1 1
      src/main/java/com/gmail/nossr50/runnables/commands/McrankCommandDisplayTask.java
  17. 2 2
      src/main/java/com/gmail/nossr50/runnables/commands/MctopCommandDisplayTask.java
  18. 4 3
      src/main/java/com/gmail/nossr50/skills/acrobatics/AcrobaticsManager.java
  19. 12 11
      src/main/java/com/gmail/nossr50/skills/axes/AxesManager.java
  20. 0 1
      src/main/java/com/gmail/nossr50/skills/herbalism/HerbalismManager.java
  21. 4 4
      src/main/java/com/gmail/nossr50/skills/swords/SwordsManager.java
  22. 5 4
      src/main/java/com/gmail/nossr50/skills/unarmed/UnarmedManager.java
  23. 24 12
      src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java
  24. 1 1
      src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java
  25. 2 0
      src/main/java/com/gmail/nossr50/util/random/RandomChanceExecution.java
  26. 42 64
      src/main/java/com/gmail/nossr50/util/random/RandomChanceSkill.java
  27. 4 4
      src/main/java/com/gmail/nossr50/util/random/RandomChanceSkillStatic.java
  28. 1 2
      src/main/java/com/gmail/nossr50/util/random/RandomChanceStatic.java
  29. 55 107
      src/main/java/com/gmail/nossr50/util/random/RandomChanceUtil.java
  30. 2 2
      src/main/java/com/gmail/nossr50/util/scoreboards/ScoreboardManager.java
  31. 0 1
      src/main/java/com/gmail/nossr50/util/skills/PerksUtils.java
  32. 17 0
      src/test/java/com/gmail/nossr50/TestUtil.java
  33. 37 0
      src/test/java/com/gmail/nossr50/datatypes/BlockLocationHistoryTest.java
  34. 4 12
      src/test/java/com/gmail/nossr50/util/blockmeta/ChunkStoreTest.java
  35. 116 0
      src/test/java/com/gmail/nossr50/util/random/RandomChanceTest.java

+ 1 - 0
Changelog.txt

@@ -102,6 +102,7 @@ Version 2.2.000
 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)
+    Optimized memory access for Acrobatics fall anti-exploit mechanics (thanks t00thpick1)
 
 Version 2.1.167
     Fixed a serious dupe bug

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/experience/AddlevelsCommand.java

@@ -45,6 +45,6 @@ public class AddlevelsCommand extends ExperienceCommand {
         if(isSilent)
             return;
 
-        player.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.1", value, rootSkill.getName()));
+        player.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.1", value, rootSkill.getLocalizedName()));
     }
 }

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/experience/AddxpCommand.java

@@ -46,6 +46,6 @@ public class AddxpCommand extends ExperienceCommand {
         if(isSilent)
             return;
 
-        player.sendMessage(LocaleLoader.getString("Commands.addxp.AwardSkill", value, rootSkill.getName()));
+        player.sendMessage(LocaleLoader.getString("Commands.addxp.AwardSkill", value, rootSkill.getLocalizedName()));
     }
 }

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/experience/ExperienceCommand.java

@@ -161,7 +161,7 @@ public abstract class ExperienceCommand implements TabExecutor {
             sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardAll.2", playerName));
         }
         else {
-            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", rootSkill.getName(), playerName));
+            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", rootSkill.getLocalizedName(), playerName));
         }
     }
 

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/experience/MmoeditCommand.java

@@ -51,6 +51,6 @@ public class MmoeditCommand extends ExperienceCommand {
         if(isSilent)
             return;
 
-        player.sendMessage(LocaleLoader.getString("Commands.mmoedit.Modified.1", rootSkill.getName(), value));
+        player.sendMessage(LocaleLoader.getString("Commands.mmoedit.Modified.1", rootSkill.getLocalizedName(), value));
     }
 }

+ 2 - 2
src/main/java/com/gmail/nossr50/commands/experience/SkillresetCommand.java

@@ -142,7 +142,7 @@ public class SkillresetCommand implements TabExecutor {
     }
 
     protected void handlePlayerMessageSkill(Player player, RootSkill rootSkill) {
-        player.sendMessage(LocaleLoader.getString("Commands.Reset.Single", rootSkill.getName()));
+        player.sendMessage(LocaleLoader.getString("Commands.Reset.Single", rootSkill.getLocalizedName()));
     }
 
     private boolean validateArguments(CommandSender sender, String skillName) {
@@ -154,7 +154,7 @@ public class SkillresetCommand implements TabExecutor {
             sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardAll.2", playerName));
         }
         else {
-            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", rootSkill.getName(), playerName));
+            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", rootSkill.getLocalizedName(), playerName));
         }
     }
 

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/hardcore/HardcoreCommand.java

@@ -59,6 +59,6 @@ public class HardcoreCommand extends HardcoreModeCommand {
             rootSkill.setHardcoreStatLossEnabled(enable);
         }
 
-        mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Hardcore.Mode." + (enable ? "Enabled" : "Disabled"), LocaleLoader.getString("Hardcore.DeathStatLoss.Name"), (rootSkill == null ? "all skills" : rootSkill.getName())));
+        mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Hardcore.Mode." + (enable ? "Enabled" : "Disabled"), LocaleLoader.getString("Hardcore.DeathStatLoss.Name"), (rootSkill == null ? "all skills" : rootSkill.getLocalizedName())));
     }
 }

+ 3 - 3
src/main/java/com/gmail/nossr50/commands/skills/SkillCommand.java

@@ -49,7 +49,7 @@ public abstract class SkillCommand implements TabExecutor {
     public SkillCommand(@NotNull RootSkill rootSkill) {
         this.rootSkill = CoreSkills.getSkill(primarySkillType);
         this.primarySkillType = primarySkillType;
-        skillName = rootSkill.getSkillName();
+        skillName = rootSkill.getLocalizedName();
         skillGuideCommand = new SkillGuideCommand(rootSkill);
     }
 
@@ -177,10 +177,10 @@ public abstract class SkillCommand implements TabExecutor {
             {
                 if(i+1 < parentList.size())
                 {
-                    parentMessage.append(LocaleLoader.getString("Effects.Child.ParentList", parentList.get(i).getSkillName(), mmoPlayer.getExperienceHandler().getSkillLevel(parentList.get(i))));
+                    parentMessage.append(LocaleLoader.getString("Effects.Child.ParentList", parentList.get(i).getLocalizedName(), mcMMOPlayer.getSkillLevel(parentList.get(i))));
                     parentMessage.append(ChatColor.GRAY).append(", ");
                 } else {
-                    parentMessage.append(LocaleLoader.getString("Effects.Child.ParentList", parentList.get(i).getSkillName(), mmoPlayer.getExperienceHandler().getSkillLevel(parentList.get(i))));
+                    parentMessage.append(LocaleLoader.getString("Effects.Child.ParentList", parentList.get(i).getLocalizedName(), mcMMOPlayer.getSkillLevel(parentList.get(i))));
                 }
             }
 

+ 2 - 2
src/main/java/com/gmail/nossr50/commands/skills/SkillGuideCommand.java

@@ -21,8 +21,8 @@ public class SkillGuideCommand implements CommandExecutor {
     private final String invalidPage = LocaleLoader.getString("Guides.Page.Invalid");
 
     public SkillGuideCommand(@NotNull RootSkill rootSkill) {
-        rootSkill = CoreSkills.getSkill(rootSkill);
-        header = LocaleLoader.getString("Guides.Header", rootSkill.getName());
+        this.rootSkill = rootSkill;
+        header = LocaleLoader.getString("Guides.Header", rootSkill.getLocalizedName());
         guide = getGuide(rootSkill);
     }
 

+ 0 - 1
src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java

@@ -8,7 +8,6 @@ import org.bukkit.ChatColor;
 import org.bukkit.Material;
 import org.bukkit.Tag;
 import org.bukkit.configuration.ConfigurationSection;
-import org.bukkit.entity.EntityType;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.meta.ItemMeta;
 import org.bukkit.inventory.meta.PotionMeta;

+ 41 - 0
src/main/java/com/gmail/nossr50/datatypes/BlockLocationHistory.java

@@ -0,0 +1,41 @@
+package com.gmail.nossr50.datatypes;
+
+import com.google.common.collect.HashMultiset;
+import org.bukkit.Location;
+
+import java.util.LinkedList;
+
+/**
+ * This class works with the assumption that you only pass in Block Locations.  If locations have differing pitch/yaw, the logic breaks
+ */
+public class BlockLocationHistory {
+    private final LinkedList<Location> limitedSizeOrderedList = new LinkedList<>();
+    private final HashMultiset<Location> lookup = HashMultiset.create();
+    private final int maxSize;
+
+    public BlockLocationHistory(int maxSize) {
+        this.maxSize = maxSize;
+    }
+
+    /**
+     * Adds a block location to the history.  If the history memory would exceed the max size, it will remove the least recently added block location
+     *
+     * @param newItem
+     */
+    public void add(Location newItem) {
+        limitedSizeOrderedList.addFirst(newItem);
+        lookup.add(newItem);
+        if (limitedSizeOrderedList.size() > maxSize)
+            lookup.remove(limitedSizeOrderedList.removeLast());
+    }
+
+    /**
+     * Returns true if the block location is in the recorded history
+     *
+     * @param targetLoc the block location to search for
+     * @return true if the block location is in the recorded history
+     */
+    public boolean contains(Location targetLoc) {
+        return lookup.contains(targetLoc);
+    }
+}

+ 0 - 57
src/main/java/com/gmail/nossr50/datatypes/LimitedSizeList.java

@@ -1,57 +0,0 @@
-package com.gmail.nossr50.datatypes;
-
-
-import org.bukkit.Location;
-
-public class LimitedSizeList {
-    public Location[] limitedSizeOrderedList;
-    private final int size;
-
-
-    public LimitedSizeList(int size)
-    {
-        this.size = size;
-        limitedSizeOrderedList = new Location[size];
-    }
-
-    /**
-     * Adds objects to our limited size ordered list
-     * New objects are added to the front
-     * @param newItem
-     */
-    public void add(Location newItem)
-    {
-        Location[] newList = new Location[size];
-
-        for(int i = 0; i < size-1; i++)
-        {
-            if(i != 0)
-                newList[i] = limitedSizeOrderedList[i-1];
-            else
-                newList[i] = newItem;
-        }
-
-        limitedSizeOrderedList = newList;
-    }
-
-    /**
-     * Returns true if the object is anywhere in our list
-     * @param targetLoc the object to check for
-     * @return true if the object is in our list
-     */
-    public boolean contains(Location targetLoc)
-    {
-        for(Location iter : limitedSizeOrderedList)
-        {
-            if(iter == null)
-                continue;
-
-            if(iter.getX() == targetLoc.getX()
-                    && iter.getY() == targetLoc.getY()
-                    && iter.getZ() == targetLoc.getZ())
-                return true;
-        }
-
-        return false;
-    }
-}

+ 5 - 1
src/main/java/com/gmail/nossr50/datatypes/skills/PrimarySkillType.java

@@ -234,10 +234,14 @@ public enum PrimarySkillType {
         return null;
     }
 
-    public String getName() {
+    public String getLocalizedName() {
         return StringUtils.getCapitalized(LocaleLoader.getString(StringUtils.getCapitalized(this.toString()) + ".SkillName"));
     }
 
+    public String getName() {
+        return StringUtils.getCapitalized(StringUtils.getCapitalized(this.toString()));
+    }
+
     public boolean getPermissions(Player player) {
         return Permissions.skillEnabled(player, this);
     }

+ 1 - 1
src/main/java/com/gmail/nossr50/listeners/PlayerListener.java

@@ -839,7 +839,7 @@ public class PlayerListener implements Listener {
             // Do these ACTUALLY have to be lower case to work properly?
             for (PrimarySkillType skill : PrimarySkillType.values()) {
                 String skillName = skill.toString().toLowerCase(Locale.ENGLISH);
-                String localizedName = skill.getName().toLowerCase(Locale.ENGLISH);
+                String localizedName = skill.getLocalizedName().toLowerCase(Locale.ENGLISH);
 
                 if (lowerCaseCommand.equals(localizedName)) {
                     event.setMessage(message.replace(command, skillName));

+ 0 - 4
src/main/java/com/gmail/nossr50/listeners/WorldListener.java

@@ -3,18 +3,14 @@ package com.gmail.nossr50.listeners;
 import com.gmail.nossr50.config.WorldBlacklist;
 import com.gmail.nossr50.mcMMO;
 import org.bukkit.Chunk;
-import org.bukkit.World;
 import org.bukkit.block.BlockState;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.EventPriority;
 import org.bukkit.event.Listener;
 import org.bukkit.event.world.ChunkUnloadEvent;
 import org.bukkit.event.world.StructureGrowEvent;
-import org.bukkit.event.world.WorldInitEvent;
 import org.bukkit.event.world.WorldUnloadEvent;
 
-import java.io.File;
-
 public class WorldListener implements Listener {
     private final mcMMO plugin;
 

+ 1 - 1
src/main/java/com/gmail/nossr50/runnables/commands/McrankCommandDisplayTask.java

@@ -53,7 +53,7 @@ public class McrankCommandDisplayTask extends BukkitRunnable {
 //            }
 
             rank = skills.get(skill);
-            sender.sendMessage(LocaleLoader.getString("Commands.mcrank.Skill", skill.getName(), (rank == null ? LocaleLoader.getString("Commands.mcrank.Unranked") : rank)));
+            sender.sendMessage(LocaleLoader.getString("Commands.mcrank.Skill", skill.getLocalizedName(), (rank == null ? LocaleLoader.getString("Commands.mcrank.Unranked") : rank)));
         }
 
         rank = skills.get(null);

+ 2 - 2
src/main/java/com/gmail/nossr50/runnables/commands/MctopCommandDisplayTask.java

@@ -63,10 +63,10 @@ public class MctopCommandDisplayTask extends BukkitRunnable {
         }
         else {
             if(sender instanceof Player) {
-                sender.sendMessage(LocaleLoader.getString("Commands.Skill.Leaderboard", rootSkill.getSkillName()));
+                sender.sendMessage(LocaleLoader.getString("Commands.Skill.Leaderboard", skill.getLocalizedName()));
             }
             else {
-                sender.sendMessage(ChatColor.stripColor(LocaleLoader.getString("Commands.Skill.Leaderboard", rootSkill.getSkillName())));
+                sender.sendMessage(ChatColor.stripColor(LocaleLoader.getString("Commands.Skill.Leaderboard", skill.getLocalizedName())));
             }
         }
 

+ 4 - 3
src/main/java/com/gmail/nossr50/skills/acrobatics/AcrobaticsManager.java

@@ -1,7 +1,8 @@
 package com.gmail.nossr50.skills.acrobatics;
 
 import com.gmail.nossr50.config.experience.ExperienceConfig;
-import com.gmail.nossr50.datatypes.LimitedSizeList;
+import com.gmail.nossr50.datatypes.BlockLocationHistory;
+import com.gmail.nossr50.datatypes.experience.XPGainReason;
 import com.gmail.nossr50.datatypes.interactions.NotificationType;
 import com.neetgames.mcmmo.player.OnlineMMOPlayer;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
@@ -27,13 +28,13 @@ public class AcrobaticsManager extends SkillManager {
 
     public AcrobaticsManager(OnlineMMOPlayer mmoPlayer) {
         super(mmoPlayer, PrimarySkillType.ACROBATICS);
-        fallLocationMap = new LimitedSizeList(50);
+        fallLocationMap = new BlockLocationHistory(50);
     }
 
     private long rollXPCooldown = 0;
     private final long rollXPInterval = (1000 * 3); //1 Minute
     private long rollXPIntervalLengthen = (1000 * 10); //10 Seconds
-    private final LimitedSizeList fallLocationMap;
+    private final BlockLocationHistory fallLocationMap;
 
     public boolean hasFallenInLocationBefore(Location location)
     {

+ 12 - 11
src/main/java/com/gmail/nossr50/skills/axes/AxesManager.java

@@ -17,6 +17,7 @@ import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.Player;
 import org.bukkit.event.entity.EntityDamageEvent.DamageModifier;
 import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.Map;
 
@@ -32,28 +33,28 @@ public class AxesManager extends SkillManager {
         return Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.AXES_AXE_MASTERY);
     }
 
-    public boolean canCriticalHit(LivingEntity target) {
+    public boolean canCriticalHit(@NotNull LivingEntity target) {
         if(!RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.AXES_CRITICAL_STRIKES))
             return false;
 
         return target.isValid() && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.AXES_CRITICAL_STRIKES);
     }
 
-    public boolean canImpact(LivingEntity target) {
+    public boolean canImpact(@NotNull LivingEntity target) {
         if(!RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.AXES_ARMOR_IMPACT))
             return false;
 
         return target.isValid() && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.AXES_ARMOR_IMPACT) && Axes.hasArmor(target);
     }
 
-    public boolean canGreaterImpact(LivingEntity target) {
+    public boolean canGreaterImpact(@NotNull LivingEntity target) {
         if(!RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.AXES_GREATER_IMPACT))
             return false;
 
         return target.isValid() && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.AXES_GREATER_IMPACT) && !Axes.hasArmor(target);
     }
 
-    public boolean canUseSkullSplitter(LivingEntity target) {
+    public boolean canUseSkullSplitter(@NotNull LivingEntity target) {
         if(!RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.AXES_SKULL_SPLITTER))
             return false;
 
@@ -68,7 +69,7 @@ public class AxesManager extends SkillManager {
      * Handle the effects of the Axe Mastery ability
      */
     public double axeMastery() {
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.ALWAYS_FIRES, SubSkillType.AXES_AXE_MASTERY, getPlayer(), mmoPlayer.getAttackStrength())) {
+        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.ALWAYS_FIRES, SubSkillType.AXES_AXE_MASTERY, getPlayer())) {
             return 0;
         }
 
@@ -82,7 +83,7 @@ public class AxesManager extends SkillManager {
      * @param damage The amount of damage initially dealt by the event
      */
     public double criticalHit(LivingEntity target, double damage) {
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.AXES_CRITICAL_STRIKES, getPlayer(), mmoPlayer.getAttackStrength())) {
+        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.AXES_CRITICAL_STRIKES, getPlayer())) {
             return 0;
         }
 
@@ -113,12 +114,12 @@ public class AxesManager extends SkillManager {
      *
      * @param target The {@link LivingEntity} being affected by Impact
      */
-    public void impactCheck(LivingEntity target) {
+    public void impactCheck(@NotNull LivingEntity target) {
         double durabilityDamage = getImpactDurabilityDamage();
 
         for (ItemStack armor : target.getEquipment().getArmorContents()) {
             if (armor != null && ItemUtils.isArmor(armor)) {
-                if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_STATIC_CHANCE, SubSkillType.AXES_ARMOR_IMPACT, getPlayer(), mmoPlayer.getAttackStrength())) {
+                if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_STATIC_CHANCE, SubSkillType.AXES_ARMOR_IMPACT, getPlayer())) {
                     SkillUtils.handleDurabilityChange(armor, durabilityDamage, 1);
                 }
             }
@@ -134,9 +135,9 @@ public class AxesManager extends SkillManager {
      *
      * @param target The {@link LivingEntity} being affected by the ability
      */
-    public double greaterImpact(LivingEntity target) {
+    public double greaterImpact(@NotNull LivingEntity target) {
         //static chance (3rd param)
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_STATIC_CHANCE, SubSkillType.AXES_GREATER_IMPACT, getPlayer(), mmoPlayer.getAttackStrength())) {
+        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_STATIC_CHANCE, SubSkillType.AXES_GREATER_IMPACT, getPlayer())) {
             return 0;
         }
 
@@ -166,7 +167,7 @@ public class AxesManager extends SkillManager {
      * @param target The {@link LivingEntity} being affected by the ability
      * @param damage The amount of damage initially dealt by the event
      */
-    public void skullSplitterCheck(LivingEntity target, double damage, Map<DamageModifier, Double> modifiers) {
+    public void skullSplitterCheck(@NotNull LivingEntity target, double damage, Map<DamageModifier, Double> modifiers) {
         CombatUtils.applyAbilityAoE(getPlayer(), target, damage / Axes.skullSplitterModifier, modifiers, skill);
     }
 }

+ 0 - 1
src/main/java/com/gmail/nossr50/skills/herbalism/HerbalismManager.java

@@ -738,7 +738,6 @@ public class HerbalismManager extends SkillManager {
             return false;
         }
 
-
         if (!playerInventory.containsAtLeast(seedStack, 1)) {
             return false;
         }

+ 4 - 4
src/main/java/com/gmail/nossr50/skills/swords/SwordsManager.java

@@ -62,7 +62,7 @@ public class SwordsManager extends SkillManager {
      */
     public void ruptureCheck(@NotNull LivingEntity target) throws IllegalStateException {
         if(BleedTimerTask.isBleedOperationAllowed()) {
-            if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.SWORDS_RUPTURE, getPlayer(), this.mmoPlayer.getAttackStrength())) {
+            if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.SWORDS_RUPTURE, getPlayer())) {
 
                 if (target instanceof Player) {
                     Player defender = (Player) target;
@@ -98,7 +98,7 @@ public class SwordsManager extends SkillManager {
         return 0;
     }
 
-    public int getToolTier(ItemStack itemStack)
+    public int getToolTier(@NotNull ItemStack itemStack)
     {
         if(ItemUtils.isNetheriteTool(itemStack))
             return 5;
@@ -128,7 +128,7 @@ public class SwordsManager extends SkillManager {
      * @param attacker The {@link LivingEntity} being affected by the ability
      * @param damage The amount of damage initially dealt by the event
      */
-    public void counterAttackChecks(LivingEntity attacker, double damage) {
+    public void counterAttackChecks(@NotNull LivingEntity attacker, double damage) {
         if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.SWORDS_COUNTER_ATTACK, getPlayer())) {
             CombatUtils.dealDamage(attacker, damage / Swords.counterAttackModifier, getPlayer());
 
@@ -146,7 +146,7 @@ public class SwordsManager extends SkillManager {
      * @param target The {@link LivingEntity} being affected by the ability
      * @param damage The amount of damage initially dealt by the event
      */
-    public void serratedStrikes(LivingEntity target, double damage, Map<DamageModifier, Double> modifiers) {
+    public void serratedStrikes(@NotNull LivingEntity target, double damage, Map<DamageModifier, Double> modifiers) {
         CombatUtils.applyAbilityAoE(getPlayer(), target, damage / Swords.serratedStrikesModifier, modifiers, skill);
     }
 }

+ 5 - 4
src/main/java/com/gmail/nossr50/skills/unarmed/UnarmedManager.java

@@ -24,6 +24,7 @@ import org.bukkit.entity.Item;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.Player;
 import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
 
 public class UnarmedManager extends SkillManager {
 
@@ -69,7 +70,7 @@ public class UnarmedManager extends SkillManager {
         return Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.UNARMED_BLOCK_CRACKER);
     }
 
-    public boolean blockCrackerCheck(BlockState blockState) {
+    public boolean blockCrackerCheck(@NotNull BlockState blockState) {
         if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.ALWAYS_FIRES, SubSkillType.UNARMED_BLOCK_CRACKER, getPlayer())) {
             return false;
         }
@@ -100,8 +101,8 @@ public class UnarmedManager extends SkillManager {
      *
      * @param defender The defending player
      */
-    public void disarmCheck(Player defender) {
-        if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.UNARMED_DISARM, getPlayer(), mmoPlayer.getAttackStrength()) && !hasIronGrip(defender)) {
+    public void disarmCheck(@NotNull Player defender) {
+        if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.UNARMED_DISARM, getPlayer()) && !hasIronGrip(defender)) {
             if (EventUtils.callDisarmEvent(defender).isCancelled()) {
                 return;
             }
@@ -178,7 +179,7 @@ public class UnarmedManager extends SkillManager {
      * @param defender The defending player
      * @return true if the defender was not disarmed, false otherwise
      */
-    private boolean hasIronGrip(Player defender) {
+    private boolean hasIronGrip(@NotNull Player defender) {
         if (!Misc.isNPCEntityExcludingVillagers(defender)
                 && Permissions.isSubSkillEnabled(defender, SubSkillType.UNARMED_IRON_GRIP)
                 && RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.UNARMED_IRON_GRIP, defender)) {

+ 24 - 12
src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java

@@ -40,7 +40,7 @@ public class HashChunkManager implements ChunkManager {
     }
 
     private synchronized @Nullable ChunkStore readChunkStore(@NotNull World world, int cx, int cz) throws IOException {
-        McMMOSimpleRegionFile rf = getSimpleRegionFile(world, cx, cz, false);
+        McMMOSimpleRegionFile rf = getReadableSimpleRegionFile(world, cx, cz);
         if (rf == null)
             return null; // If there is no region file, there can't be a chunk
         try (DataInputStream in = rf.getInputStream(cx, cz)) { // Get input stream for chunk
@@ -54,7 +54,7 @@ public class HashChunkManager implements ChunkManager {
         if (!data.isDirty())
             return; // Don't save unchanged data
         try {
-            McMMOSimpleRegionFile rf = getSimpleRegionFile(world, data.getChunkX(), data.getChunkZ(), true);
+            McMMOSimpleRegionFile rf = getWriteableSimpleRegionFile(world, data.getChunkX(), data.getChunkZ());
             try (DataOutputStream out = rf.getOutputStream(data.getChunkX(), data.getChunkZ())) {
                 BitSetChunkStore.Serialization.writeChunkStore(out, data);
             }
@@ -65,21 +65,33 @@ public class HashChunkManager implements ChunkManager {
         }
     }
 
-    private synchronized @Nullable McMMOSimpleRegionFile getSimpleRegionFile(@NotNull World world, int cx, int cz, boolean createIfAbsent) {
+    private synchronized @NotNull McMMOSimpleRegionFile getWriteableSimpleRegionFile(@NotNull World world, int cx, int cz) {
         CoordinateKey regionKey = toRegionKey(world.getUID(), cx, cz);
 
         return regionMap.computeIfAbsent(regionKey, k -> {
-            File worldRegionsDirectory = new File(world.getWorldFolder(), "mcmmo_regions");
-            if (!createIfAbsent && !worldRegionsDirectory.isDirectory())
-                return null; // Don't create the directory on read-only operations
-            worldRegionsDirectory.mkdirs(); // Ensure directory exists
-            File regionFile = new File(worldRegionsDirectory, "mcmmo_" + regionKey.x + "_" + regionKey.z + "_.mcm");
-            if (!createIfAbsent && !regionFile.exists())
+            File regionFile = getRegionFile(world, regionKey);
+            regionFile.getParentFile().mkdirs();
+            return new McMMOSimpleRegionFile(regionFile, regionKey.x, regionKey.z);
+        });
+    }
+
+    private synchronized @Nullable McMMOSimpleRegionFile getReadableSimpleRegionFile(@NotNull World world, int cx, int cz) {
+        CoordinateKey regionKey = toRegionKey(world.getUID(), cx, cz);
+
+        return regionMap.computeIfAbsent(regionKey, k -> {
+            File regionFile = getRegionFile(world, regionKey);
+            if (!regionFile.exists())
                 return null; // Don't create the file on read-only operations
             return new McMMOSimpleRegionFile(regionFile, regionKey.x, regionKey.z);
         });
     }
 
+    private @NotNull File getRegionFile(@NotNull World world, @NotNull CoordinateKey regionKey) {
+        if (world.getUID() != regionKey.worldID)
+            throw new IllegalArgumentException();
+        return new File(new File(world.getWorldFolder(), "mcmmo_regions"), "mcmmo_" + regionKey.x + "_" + regionKey.z + "_.mcm");
+    }
+
     private @Nullable ChunkStore loadChunk(int cx, int cz, @NotNull World world) {
         try {
             return readChunkStore(world, cx, cz);
@@ -228,15 +240,15 @@ public class HashChunkManager implements ChunkManager {
         cStore.set(ix, y, iz, value);
     }
 
-    private CoordinateKey blockCoordinateToChunkKey(@NotNull UUID worldUid, int x, int y, int z) {
+    private @NotNull CoordinateKey blockCoordinateToChunkKey(@NotNull UUID worldUid, int x, int y, int z) {
         return toChunkKey(worldUid, x >> 4, z >> 4);
     }
 
-    private CoordinateKey toChunkKey(@NotNull UUID worldUid, int cx, int cz){
+    private @NotNull CoordinateKey toChunkKey(@NotNull UUID worldUid, int cx, int cz){
         return new CoordinateKey(worldUid, cx, cz);
     }
 
-    private CoordinateKey toRegionKey(@NotNull UUID worldUid, int cx, int cz) {
+    private @NotNull CoordinateKey toRegionKey(@NotNull UUID worldUid, int cx, int cz) {
         // Compute region index (32x32 chunk regions)
         int rx = cx >> 5;
         int rz = cz >> 5;

+ 1 - 1
src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java

@@ -37,7 +37,7 @@ public final class CommandRegistrationManager {
     private static void registerSkillCommands() {
         for (PrimarySkillType skill : PrimarySkillType.values()) {
             String commandName = skill.toString().toLowerCase(Locale.ENGLISH);
-            String localizedName = skill.getName().toLowerCase(Locale.ENGLISH);
+            String localizedName = skill.getLocalizedName().toLowerCase(Locale.ENGLISH);
 
             PluginCommand command;
 

+ 2 - 0
src/main/java/com/gmail/nossr50/util/random/RandomChanceExecution.java

@@ -3,6 +3,7 @@ package com.gmail.nossr50.util.random;
 public interface RandomChanceExecution {
     /**
      * Gets the XPos used in the formula for success
+     *
      * @return value of x for our success probability graph
      */
     double getXPos();
@@ -10,6 +11,7 @@ public interface RandomChanceExecution {
     /**
      * The maximum odds for this RandomChanceExecution
      * For example, if this value is 10, then 10% odds would be the maximum and would be achieved only when xPos equaled the LinearCurvePeak
+     *
      * @return maximum probability odds from 0.00 (no chance of ever happened) to 100.0 (probability can be guaranteed)
      */
     double getProbabilityCap();

+ 42 - 64
src/main/java/com/gmail/nossr50/util/random/RandomChanceSkill.java

@@ -1,132 +1,114 @@
 package com.gmail.nossr50.util.random;
 
-import com.gmail.nossr50.config.AdvancedConfig;
-import com.neetgames.mcmmo.player.OnlineMMOPlayer;
-import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.util.Permissions;
+import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 public class RandomChanceSkill implements RandomChanceExecution {
-
-    protected final PrimarySkillType primarySkillType;
-    protected final SubSkillType subSkillType;
     protected final double probabilityCap;
     protected final boolean isLucky;
     protected int skillLevel;
-    protected double resultModifier;
+    protected final double resultModifier;
+    protected final double maximumBonusLevelCap;
 
-    public RandomChanceSkill(Player player, SubSkillType subSkillType, double resultModifier)
-    {
-        this.primarySkillType = subSkillType.getParentSkill();
-        this.subSkillType = subSkillType;
+    public RandomChanceSkill(@Nullable Player player, @NotNull SubSkillType subSkillType, double resultModifier) {
         this.probabilityCap = RandomChanceUtil.LINEAR_CURVE_VAR;
 
-        final OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().getPlayer(player);
-        if (player != null && mmoPlayer != null) {
-            this.skillLevel = mmoPlayer.getSkillLevel(primarySkillType);
+        final McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
+        if (player != null && mcMMOPlayer != null) {
+            this.skillLevel = mcMMOPlayer.getSkillLevel(subSkillType.getParentSkill());
         } else {
             this.skillLevel = 0;
         }
 
-        if(player != null)
-            isLucky = Permissions.lucky(player, primarySkillType);
+        if (player != null)
+            isLucky = Permissions.lucky(player, subSkillType.getParentSkill());
         else
             isLucky = false;
 
         this.resultModifier = resultModifier;
+        this.maximumBonusLevelCap = RandomChanceUtil.getMaxBonusLevelCap(subSkillType);
     }
 
-    public RandomChanceSkill(Player player, SubSkillType subSkillType)
-    {
-        this.primarySkillType = subSkillType.getParentSkill();
-        this.subSkillType = subSkillType;
+    public RandomChanceSkill(@Nullable Player player, @NotNull SubSkillType subSkillType) {
         this.probabilityCap = RandomChanceUtil.LINEAR_CURVE_VAR;
 
-        final OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().getPlayer(player);
-        if (player != null && mmoPlayer != null) {
-            this.skillLevel = mmoPlayer.getSkillLevel(primarySkillType);
+        final McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
+        if (player != null && mcMMOPlayer != null) {
+            this.skillLevel = mcMMOPlayer.getSkillLevel(subSkillType.getParentSkill());
         } else {
             this.skillLevel = 0;
         }
 
-        if(player != null)
-            isLucky = Permissions.lucky(player, primarySkillType);
+        if (player != null)
+            isLucky = Permissions.lucky(player, subSkillType.getParentSkill());
         else
             isLucky = false;
 
         this.resultModifier = 1.0D;
+        this.maximumBonusLevelCap = RandomChanceUtil.getMaxBonusLevelCap(subSkillType);
     }
 
-    public RandomChanceSkill(Player player, SubSkillType subSkillType, boolean hasCap)
-    {
-        if(hasCap)
-            this.probabilityCap = AdvancedConfig.getInstance().getMaximumProbability(subSkillType);
+    public RandomChanceSkill(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean hasCap) {
+        if (hasCap)
+            this.probabilityCap = RandomChanceUtil.getMaximumProbability(subSkillType);
         else
             this.probabilityCap = RandomChanceUtil.LINEAR_CURVE_VAR;
 
-        this.primarySkillType = subSkillType.getParentSkill();
-        this.subSkillType = subSkillType;
-
-        final OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().getPlayer(player);
-        if (player != null && mmoPlayer != null) {
-            this.skillLevel = mmoPlayer.getSkillLevel(primarySkillType);
+        final McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
+        if (player != null && mcMMOPlayer != null) {
+            this.skillLevel = mcMMOPlayer.getSkillLevel(subSkillType.getParentSkill());
         } else {
             this.skillLevel = 0;
         }
 
-        if(player != null)
-            isLucky = Permissions.lucky(player, primarySkillType);
+        if (player != null)
+            isLucky = Permissions.lucky(player, subSkillType.getParentSkill());
         else
             isLucky = false;
 
         this.resultModifier = 1.0D;
+        this.maximumBonusLevelCap = RandomChanceUtil.getMaxBonusLevelCap(subSkillType);
     }
 
-    public RandomChanceSkill(Player player, SubSkillType subSkillType, boolean hasCap, double resultModifier)
-    {
-        if(hasCap)
-            this.probabilityCap = AdvancedConfig.getInstance().getMaximumProbability(subSkillType);
+    public RandomChanceSkill(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean hasCap, double resultModifier) {
+        if (hasCap)
+            this.probabilityCap = RandomChanceUtil.getMaximumProbability(subSkillType);
         else
             this.probabilityCap = RandomChanceUtil.LINEAR_CURVE_VAR;
 
-        this.primarySkillType = subSkillType.getParentSkill();
-        this.subSkillType = subSkillType;
-
-        final OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().getPlayer(player);
-        if (player != null && mmoPlayer != null) {
-            this.skillLevel = mmoPlayer.getSkillLevel(primarySkillType);
+        final McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
+        if (player != null && mcMMOPlayer != null) {
+            this.skillLevel = mcMMOPlayer.getSkillLevel(subSkillType.getParentSkill());
         } else {
             this.skillLevel = 0;
         }
 
-        if(player != null)
-            isLucky = Permissions.lucky(player, primarySkillType);
+        if (player != null)
+            isLucky = Permissions.lucky(player, subSkillType.getParentSkill());
         else
             isLucky = false;
 
         this.resultModifier = resultModifier;
-    }
-
-    /**
-     * The subskill corresponding to this RandomChanceSkill
-     * @return this subskill
-     */
-    public SubSkillType getSubSkill() {
-        return subSkillType;
+        this.maximumBonusLevelCap = RandomChanceUtil.getMaxBonusLevelCap(subSkillType);
     }
 
     /**
      * Gets the skill level of the player who owns this RandomChanceSkill
+     *
      * @return the current skill level relating to this RandomChanceSkill
      */
-    public int getSkillLevel()
-    {
+    public int getSkillLevel() {
         return skillLevel;
     }
 
     /**
      * Modify the skill level used for this skill's RNG calculations
+     *
      * @param newSkillLevel new skill level
      */
     public void setSkillLevel(int newSkillLevel) {
@@ -141,7 +123,7 @@ public class RandomChanceSkill implements RandomChanceExecution {
      * @return the maximum bonus from skill level for this skill
      */
     public double getMaximumBonusLevelCap() {
-        return AdvancedConfig.getInstance().getMaxBonusLevel(subSkillType);
+        return maximumBonusLevelCap;
     }
 
     /**
@@ -172,8 +154,4 @@ public class RandomChanceSkill implements RandomChanceExecution {
     public double getResultModifier() {
         return resultModifier;
     }
-
-    public void setResultModifier(double resultModifier) {
-        this.resultModifier = resultModifier;
-    }
 }

+ 4 - 4
src/main/java/com/gmail/nossr50/util/random/RandomChanceSkillStatic.java

@@ -2,19 +2,19 @@ package com.gmail.nossr50.util.random;
 
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 public class RandomChanceSkillStatic extends RandomChanceSkill {
     private final double xPos;
 
-    public RandomChanceSkillStatic(double xPos, Player player, SubSkillType subSkillType)
-    {
+    public RandomChanceSkillStatic(double xPos, @Nullable Player player, @NotNull SubSkillType subSkillType) {
         super(player, subSkillType);
 
         this.xPos = xPos;
     }
 
-    public RandomChanceSkillStatic(double xPos, Player player, SubSkillType subSkillType, double resultModifier)
-    {
+    public RandomChanceSkillStatic(double xPos, @Nullable Player player, @NotNull SubSkillType subSkillType, double resultModifier) {
         super(player, subSkillType, resultModifier);
 
         this.xPos = xPos;

+ 1 - 2
src/main/java/com/gmail/nossr50/util/random/RandomChanceStatic.java

@@ -5,8 +5,7 @@ public class RandomChanceStatic implements RandomChanceExecution {
     private final double probabilityCap;
     private final boolean isLucky;
 
-    public RandomChanceStatic(double xPos, boolean isLucky)
-    {
+    public RandomChanceStatic(double xPos, boolean isLucky) {
         this.xPos = xPos;
         this.probabilityCap = xPos;
         this.isLucky = isLucky;

+ 55 - 107
src/main/java/com/gmail/nossr50/util/random/RandomChanceUtil.java

@@ -1,10 +1,8 @@
 package com.gmail.nossr50.util.random;
 
 import com.gmail.nossr50.config.AdvancedConfig;
-import com.neetgames.mcmmo.player.OnlineMMOPlayer;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
-import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill;
 import com.gmail.nossr50.events.skills.secondaryabilities.SubSkillEvent;
 import com.gmail.nossr50.events.skills.secondaryabilities.SubSkillRandomCheckEvent;
 import com.gmail.nossr50.util.EventUtils;
@@ -12,15 +10,16 @@ import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.skills.SkillActivationType;
 import org.bukkit.entity.Player;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.text.DecimalFormat;
 import java.util.concurrent.ThreadLocalRandom;
 
-public class RandomChanceUtil
-{
-    public static final DecimalFormat percent = new DecimalFormat("##0.00%");
+public class RandomChanceUtil {
+    public static final @NotNull DecimalFormat percent = new DecimalFormat("##0.00%");
     //public static final DecimalFormat decimal = new DecimalFormat("##0.00");
     public static final double LINEAR_CURVE_VAR = 100.0D;
+    public static final double LUCKY_MODIFIER = 1.333D;
 
     /**
      * This method is the final step in determining if a Sub-Skill / Secondary Skill in mcMMO successfully activates either from chance or otherwise
@@ -28,14 +27,12 @@ public class RandomChanceUtil
      * non-RNG skills just fire the cancellable event and succeed if they go uncancelled
      *
      * @param skillActivationType this value represents what kind of activation procedures this sub-skill uses
-     * @param subSkillType The identifier for this specific sub-skill
-     * @param player The owner of this sub-skill
+     * @param subSkillType        The identifier for this specific sub-skill
+     * @param player              The owner of this sub-skill
      * @return returns true if all conditions are met and the event is not cancelled
      */
-    public static boolean isActivationSuccessful(SkillActivationType skillActivationType, SubSkillType subSkillType, Player player)
-    {
-        switch(skillActivationType)
-        {
+    public static boolean isActivationSuccessful(@NotNull SkillActivationType skillActivationType, @NotNull SubSkillType subSkillType, @Nullable Player player) {
+        switch (skillActivationType) {
             case RANDOM_LINEAR_100_SCALE_WITH_CAP:
                 return checkRandomChanceExecutionSuccess(player, subSkillType, true);
             case RANDOM_STATIC_CHANCE:
@@ -48,36 +45,8 @@ public class RandomChanceUtil
         }
     }
 
-    /**
-     * This method is the final step in determining if a Sub-Skill / Secondary Skill in mcMMO successfully activates either from chance or otherwise
-     * Random skills check for success based on numbers and then fire a cancellable event, if that event is not cancelled they succeed
-     * non-RNG skills just fire the cancellable event and succeed if they go uncancelled
-     *
-     * @param skillActivationType this value represents what kind of activation procedures this sub-skill uses
-     * @param subSkillType The identifier for this specific sub-skill
-     * @param player The owner of this sub-skill
-     * @return returns true if all conditions are met and the event is not cancelled
-     */
-    public static boolean isActivationSuccessful(SkillActivationType skillActivationType, SubSkillType subSkillType, Player player, double resultModifier)
-    {
-        switch(skillActivationType)
-        {
-            case RANDOM_LINEAR_100_SCALE_WITH_CAP:
-                return checkRandomChanceExecutionSuccess(player, subSkillType, true);
-            case RANDOM_STATIC_CHANCE:
-                return checkRandomStaticChanceExecutionSuccess(player, subSkillType, resultModifier);
-            case ALWAYS_FIRES:
-                SubSkillEvent event = EventUtils.callSubSkillEvent(player, subSkillType);
-                return !event.isCancelled();
-            default:
-                return false;
-        }
-    }
-
-    public static double getActivationChance(SkillActivationType skillActivationType, SubSkillType subSkillType, Player player)
-    {
-        switch(skillActivationType)
-        {
+    public static double getActivationChance(@NotNull SkillActivationType skillActivationType, @NotNull SubSkillType subSkillType, @Nullable Player player) {
+        switch (skillActivationType) {
             case RANDOM_LINEAR_100_SCALE_WITH_CAP:
                 return getRandomChanceExecutionSuccess(player, subSkillType, true);
             case RANDOM_STATIC_CHANCE:
@@ -89,10 +58,10 @@ public class RandomChanceUtil
 
     /**
      * Checks whether or not the random chance succeeds
+     *
      * @return true if the random chance succeeds
      */
-    public static boolean checkRandomChanceExecutionSuccess(Player player, PrimarySkillType primarySkillType, double chance)
-    {
+    public static boolean checkRandomChanceExecutionSuccess(@NotNull Player player, @NotNull PrimarySkillType primarySkillType, double chance) {
         //Check the odds
         chance *= 100;
 
@@ -115,11 +84,11 @@ public class RandomChanceUtil
 
     /**
      * Used for stuff like Excavation, Fishing, etc...
+     *
      * @param randomChance
      * @return
      */
-    public static boolean checkRandomChanceExecutionSuccess(RandomChanceSkillStatic randomChance, double resultModifier)
-    {
+    public static boolean checkRandomChanceExecutionSuccess(@NotNull RandomChanceSkillStatic randomChance, double resultModifier) {
         double chanceOfSuccess = calculateChanceOfSuccess(randomChance);
 
         //Check the odds
@@ -128,16 +97,15 @@ public class RandomChanceUtil
 
     /**
      * Used for stuff like Excavation, Fishing, etc...
+     *
      * @param randomChance
      * @return
      */
-    public static boolean checkRandomChanceExecutionSuccess(RandomChanceSkillStatic randomChance)
-    {
+    public static boolean checkRandomChanceExecutionSuccess(@NotNull RandomChanceSkillStatic randomChance) {
         return checkRandomChanceExecutionSuccess(randomChance, 1.0F);
     }
 
-    public static boolean checkRandomChanceExecutionSuccess(RandomChanceSkill randomChance)
-    {
+    public static boolean checkRandomChanceExecutionSuccess(@NotNull RandomChanceSkill randomChance) {
         double chanceOfSuccess = calculateChanceOfSuccess(randomChance);
 
         //Check the odds
@@ -153,14 +121,15 @@ public class RandomChanceUtil
 
     /**
      * Gets the Static Chance for something to activate
+     *
      * @param randomChance
      * @return
      */
-    public static double getRandomChanceExecutionChance(RandomChanceExecution randomChance) {
+    public static double getRandomChanceExecutionChance(@NotNull RandomChanceExecution randomChance) {
         return getChanceOfSuccess(randomChance.getXPos(), randomChance.getProbabilityCap(), LINEAR_CURVE_VAR);
     }
 
-    public static double getRandomChanceExecutionChance(RandomChanceStatic randomChance) {
+    public static double getRandomChanceExecutionChance(@NotNull RandomChanceStatic randomChance) {
         double chanceOfSuccess = getChanceOfSuccess(randomChance.getXPos(), randomChance.getProbabilityCap(), LINEAR_CURVE_VAR);
 
         chanceOfSuccess = addLuck(randomChance.isLucky(), chanceOfSuccess);
@@ -173,7 +142,7 @@ public class RandomChanceUtil
         return chanceOfSuccess;
     }*/
 
-    private static double calculateChanceOfSuccess(RandomChanceSkill randomChance) {
+    public static double calculateChanceOfSuccess(@NotNull RandomChanceSkill randomChance) {
         double skillLevel = randomChance.getSkillLevel();
         double maximumProbability = randomChance.getProbabilityCap();
         double maximumBonusLevel = randomChance.getMaximumBonusLevelCap();
@@ -194,7 +163,7 @@ public class RandomChanceUtil
         return chanceOfSuccess;
     }
 
-    private static double calculateChanceOfSuccess(RandomChanceSkillStatic randomChance) {
+    public static double calculateChanceOfSuccess(@NotNull RandomChanceSkillStatic randomChance) {
         double chanceOfSuccess = getChanceOfSuccess(randomChance.getXPos(), 100, 100);
 
         //Add Luck
@@ -209,27 +178,23 @@ public class RandomChanceUtil
      *
      * @return the chance of success from 0-100 (100 = guaranteed)
      */
-    private static int getChanceOfSuccess(double skillLevel, double maxProbability, double maxLevel)
-    {
+    private static int getChanceOfSuccess(double skillLevel, double maxProbability, double maxLevel) {
         //return (int) (x / (y / LINEAR_CURVE_VAR));
-        return (int) (maxProbability * (skillLevel/maxLevel));
+        return (int) (maxProbability * (skillLevel / maxLevel));
         // max probability * (weight/maxlevel) = chance of success
     }
 
-    private static int getChanceOfSuccess(double x, double y)
-    {
+    private static int getChanceOfSuccess(double x, double y) {
         return (int) (x / (y / LINEAR_CURVE_VAR));
         // max probability * (weight/maxlevel) = chance of success
     }
 
-    public static double getRandomChanceExecutionSuccess(Player player, SubSkillType subSkillType, boolean hasCap)
-    {
+    public static double getRandomChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean hasCap) {
         RandomChanceSkill rcs = new RandomChanceSkill(player, subSkillType, hasCap);
         return calculateChanceOfSuccess(rcs);
     }
 
-    public static double getRandomStaticChanceExecutionSuccess(Player player, SubSkillType subSkillType)
-    {
+    public static double getRandomStaticChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType) {
         try {
             return getRandomChanceExecutionChance(new RandomChanceSkillStatic(getStaticRandomChance(subSkillType), player, subSkillType));
         } catch (InvalidStaticChance invalidStaticChance) {
@@ -240,29 +205,24 @@ public class RandomChanceUtil
         return 0.1337; //Puts on shades
     }
 
-    public static boolean checkRandomChanceExecutionSuccess(Player player, SubSkillType subSkillType, boolean hasCap)
-    {
+    public static boolean checkRandomChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean hasCap) {
         return checkRandomChanceExecutionSuccess(new RandomChanceSkill(player, subSkillType, hasCap));
     }
 
-    public static boolean checkRandomChanceExecutionSuccess(Player player, SubSkillType subSkillType)
-    {
+    public static boolean checkRandomChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType) {
         return checkRandomChanceExecutionSuccess(new RandomChanceSkill(player, subSkillType));
     }
 
-    public static boolean checkRandomChanceExecutionSuccess(Player player, SubSkillType subSkillType, boolean hasCap, double resultModifier)
-    {
+    public static boolean checkRandomChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean hasCap, double resultModifier) {
         return checkRandomChanceExecutionSuccess(new RandomChanceSkill(player, subSkillType, hasCap, resultModifier));
     }
 
-    public static boolean checkRandomChanceExecutionSuccess(Player player, SubSkillType subSkillType, double resultModifier)
-    {
+    public static boolean checkRandomChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType, double resultModifier) {
         return checkRandomChanceExecutionSuccess(new RandomChanceSkill(player, subSkillType, resultModifier));
     }
 
 
-    public static boolean checkRandomStaticChanceExecutionSuccess(Player player, SubSkillType subSkillType, double resultModifier)
-    {
+    public static boolean checkRandomStaticChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType) {
         try {
             return checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(getStaticRandomChance(subSkillType), player, subSkillType));
         } catch (InvalidStaticChance invalidStaticChance) {
@@ -273,21 +233,15 @@ public class RandomChanceUtil
         return false;
     }
 
-    public static boolean checkRandomStaticChanceExecutionSuccess(Player player, SubSkillType subSkillType)
-    {
-        return checkRandomStaticChanceExecutionSuccess(player, subSkillType, 1.0F);
-    }
-
     /**
      * Grabs static activation rolls for Secondary Abilities
+     *
      * @param subSkillType The secondary ability to grab properties of
-     * @throws InvalidStaticChance if the skill has no defined static chance this exception will be thrown and you should know you're a naughty boy
      * @return The static activation roll involved in the RNG calculation
+     * @throws InvalidStaticChance if the skill has no defined static chance this exception will be thrown and you should know you're a naughty boy
      */
-    public static double getStaticRandomChance(SubSkillType subSkillType) throws InvalidStaticChance
-    {
-        switch(subSkillType)
-        {
+    public static double getStaticRandomChance(@NotNull SubSkillType subSkillType) throws InvalidStaticChance {
+        switch (subSkillType) {
             case AXES_ARMOR_IMPACT:
                 return AdvancedConfig.getInstance().getImpactChance();
             case AXES_GREATER_IMPACT:
@@ -299,24 +253,12 @@ public class RandomChanceUtil
         }
     }
 
-    public static boolean sendSkillEvent(Player player, SubSkillType subSkillType, double activationChance)
-    {
+    public static boolean sendSkillEvent(Player player, SubSkillType subSkillType, double activationChance) {
         SubSkillRandomCheckEvent event = new SubSkillRandomCheckEvent(player, subSkillType, activationChance);
         return !event.isCancelled();
     }
 
-    /*public static boolean treasureDropSuccessful(Player player, double dropChance, int activationChance) {
-        SubSkillRandomCheckEvent event = new SubSkillRandomCheckEvent(player, SubSkillType.EXCAVATION_ARCHAEOLOGY, dropChance / activationChance);
-        mcMMO.p.getServer().getPluginManager().callEvent(event);
-        return (event.getChance() * activationChance) > (Misc.getRandom().nextDouble() * activationChance) && !event.isCancelled();
-    }*/
-
-    public static boolean isActivationSuccessful(SkillActivationType skillActivationType, AbstractSubSkill abstractSubSkill, Player player)
-    {
-        return isActivationSuccessful(skillActivationType, abstractSubSkill.getSubSkillType(), player);
-    }
-
-    public static String[] calculateAbilityDisplayValues(@NotNull SkillActivationType skillActivationType, @NotNull Player player, @NotNull SubSkillType subSkillType) {
+    public static String @NotNull [] calculateAbilityDisplayValues(@NotNull SkillActivationType skillActivationType, @NotNull Player player, @NotNull SubSkillType subSkillType) {
         double successChance = getActivationChance(skillActivationType, subSkillType, player);
         String[] displayValues = new String[2];
 
@@ -328,7 +270,7 @@ public class RandomChanceUtil
         return displayValues;
     }
 
-    public static String[] calculateAbilityDisplayValuesStatic(@NotNull OnlineMMOPlayer mmoPlayer, PrimarySkillType primarySkillType, double chance) {
+    public static String @NotNull [] calculateAbilityDisplayValuesStatic(@NotNull Player player, @NotNull PrimarySkillType primarySkillType, double chance) {
         RandomChanceStatic rcs = new RandomChanceStatic(chance, false);
         double successChance = getRandomChanceExecutionChance(rcs);
 
@@ -337,7 +279,7 @@ public class RandomChanceUtil
 
         String[] displayValues = new String[2];
 
-        boolean isLucky = Permissions.lucky(Misc.adaptPlayer(mmoPlayer), primarySkillType);
+        boolean isLucky = Permissions.lucky(player, primarySkillType);
 
         displayValues[0] = percent.format(Math.min(successChance, 100.0D) / 100.0D);
         displayValues[1] = isLucky ? percent.format(Math.min(successChance_lucky, 100.0D) / 100.0D) : null;
@@ -345,7 +287,7 @@ public class RandomChanceUtil
         return displayValues;
     }
 
-    public static String[] calculateAbilityDisplayValuesCustom(SkillActivationType skillActivationType, Player player, SubSkillType subSkillType, double multiplier) {
+    public static String @NotNull [] calculateAbilityDisplayValuesCustom(@NotNull SkillActivationType skillActivationType, @NotNull Player player, @NotNull SubSkillType subSkillType, double multiplier) {
         double successChance = getActivationChance(skillActivationType, subSkillType, player);
         successChance *= multiplier; //Currently only used for graceful roll
         String[] displayValues = new String[2];
@@ -360,19 +302,25 @@ public class RandomChanceUtil
         return displayValues;
     }
 
-    public static double addLuck(Player player, PrimarySkillType primarySkillType, double chance)
-    {
-        if(Permissions.lucky(player, primarySkillType))
-            return chance * 1.333D;
+    public static double addLuck(@NotNull Player player, @NotNull PrimarySkillType primarySkillType, double chance) {
+        if (Permissions.lucky(player, primarySkillType))
+            return chance * LUCKY_MODIFIER;
         else
             return chance;
     }
 
-    public static double addLuck(boolean isLucky, double chance)
-    {
-        if(isLucky)
-            return chance * 1.333D;
+    public static double addLuck(boolean isLucky, double chance) {
+        if (isLucky)
+            return chance * LUCKY_MODIFIER;
         else
             return chance;
     }
+
+    public static double getMaximumProbability(@NotNull SubSkillType subSkillType) {
+        return AdvancedConfig.getInstance().getMaximumProbability(subSkillType);
+    }
+
+    public static double getMaxBonusLevelCap(@NotNull SubSkillType subSkillType) {
+        return AdvancedConfig.getInstance().getMaxBonusLevel(subSkillType);
+    }
 }

+ 2 - 2
src/main/java/com/gmail/nossr50/util/scoreboards/ScoreboardManager.java

@@ -94,7 +94,7 @@ public class ScoreboardManager {
             int i = 0;
             for (PrimarySkillType type : PrimarySkillType.values()) {
                 // Include child skills
-                skillLabelBuilder.put(type, getShortenedName(colors.get(i) + type.getName(), false));
+                skillLabelBuilder.put(type, getShortenedName(colors.get(i) + type.getLocalizedName(), false));
 
                 if (type.getSuperAbilityType() != null) {
                     abilityLabelBuilder.put(type.getSuperAbilityType(), getShortenedName(colors.get(i) + type.getSuperAbilityType().getLocalizedName()));
@@ -116,7 +116,7 @@ public class ScoreboardManager {
         else {
             for (PrimarySkillType type : PrimarySkillType.values()) {
                 // Include child skills
-                skillLabelBuilder.put(type, getShortenedName(ChatColor.GREEN + type.getName()));
+                skillLabelBuilder.put(type, getShortenedName(ChatColor.GREEN + type.getLocalizedName()));
 
                 if (type.getSuperAbilityType() != null) {
                     abilityLabelBuilder.put(type.getSuperAbilityType(), formatAbility(type.getSuperAbilityType().getLocalizedName()));

+ 0 - 1
src/main/java/com/gmail/nossr50/util/skills/PerksUtils.java

@@ -6,7 +6,6 @@ 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;

+ 17 - 0
src/test/java/com/gmail/nossr50/TestUtil.java

@@ -0,0 +1,17 @@
+package com.gmail.nossr50;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+//TODO: Move generic test stuff here
+public class TestUtil {
+    public static void recursiveDelete(@NotNull File directoryToBeDeleted) {
+        if (directoryToBeDeleted.isDirectory()) {
+            for (File file : directoryToBeDeleted.listFiles()) {
+                recursiveDelete(file);
+            }
+        }
+        directoryToBeDeleted.delete();
+    }
+}

+ 37 - 0
src/test/java/com/gmail/nossr50/datatypes/BlockLocationHistoryTest.java

@@ -0,0 +1,37 @@
+package com.gmail.nossr50.datatypes;
+
+import org.bukkit.Location;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class BlockLocationHistoryTest {
+    @Test
+    public void testRemovesOldestElement() {
+        BlockLocationHistory history = new BlockLocationHistory(2);
+        Location locationA = new Location(null, 0, 1, 2);
+        Location locationB = new Location(null, 1, 2, 3);
+        Location locationC = new Location(null, 2, 3, 4);
+
+        history.add(locationA);
+        history.add(locationB);
+        history.add(locationC);
+        Assert.assertFalse(history.contains(locationA));
+        Assert.assertTrue(history.contains(locationB));
+        Assert.assertTrue(history.contains(locationC));
+    }
+
+    @Test
+    public void testSupportsDuplicateElement() {
+        BlockLocationHistory history = new BlockLocationHistory(2);
+        Location locationA = new Location(null, 0, 1, 2);
+        Location locationB = new Location(null, 1, 2, 3);
+
+        history.add(locationA);
+        history.add(locationA);
+        history.add(locationB);
+        Assert.assertTrue(history.contains(locationA));
+        Assert.assertTrue(history.contains(locationB));
+        history.add(locationB);
+        Assert.assertFalse(history.contains(locationA));
+    }
+}

+ 4 - 12
src/test/java/ChunkStoreTest.java → src/test/java/com/gmail/nossr50/util/blockmeta/ChunkStoreTest.java

@@ -1,10 +1,11 @@
-import com.gmail.nossr50.util.blockmeta.*;
+package com.gmail.nossr50.util.blockmeta;
+
+import com.gmail.nossr50.TestUtil;
 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;
@@ -31,7 +32,7 @@ public class ChunkStoreTest {
 
     @AfterClass
     public static void tearDownClass() {
-        recursiveDelete(tempDir);
+        TestUtil.recursiveDelete(tempDir);
     }
 
     private World mockWorld;
@@ -184,15 +185,6 @@ public class ChunkStoreTest {
                     Assert.assertTrue(expected.isTrue(x, y, z) == actual.isTrue(x, y, z));
     }
 
-    private static void recursiveDelete(@NotNull File directoryToBeDeleted) {
-        if (directoryToBeDeleted.isDirectory()) {
-            for (File file : directoryToBeDeleted.listFiles()) {
-                recursiveDelete(file);
-            }
-        }
-        directoryToBeDeleted.delete();
-    }
-
     private static byte[] serializeChunkstore(@NotNull ChunkStore chunkStore) throws IOException {
         ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
         if (chunkStore instanceof BitSetChunkStore)

+ 116 - 0
src/test/java/com/gmail/nossr50/util/random/RandomChanceTest.java

@@ -0,0 +1,116 @@
+package com.gmail.nossr50.util.random;
+
+import com.gmail.nossr50.datatypes.player.McMMOPlayer;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+import com.gmail.nossr50.datatypes.skills.SubSkillType;
+import com.gmail.nossr50.util.Permissions;
+import com.gmail.nossr50.util.player.UserManager;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import static org.mockito.Mockito.mock;
+
+//TODO: Rewrite the entire com.gmail.nossr50.util.random package, it was written in haste and it disgusts me
+//TODO: Add more tests for the other types of random dice rolls
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({RandomChanceUtil.class, UserManager.class})
+public class RandomChanceTest {
+
+    private Player luckyPlayer;
+    private McMMOPlayer mmoPlayerLucky;
+
+    private Player normalPlayer;
+    private McMMOPlayer mmoPlayerNormal;
+
+    private SubSkillType subSkillType;
+    private PrimarySkillType primarySkillType;
+
+    private final String testASCIIHeader = "---- mcMMO Tests ----";
+
+    @Before
+    public void setUpMock() {
+        primarySkillType = PrimarySkillType.HERBALISM;
+        subSkillType = SubSkillType.HERBALISM_GREEN_THUMB;
+
+        //TODO: Likely needs to be changed per skill if more tests were added
+        PowerMockito.stub(PowerMockito.method(RandomChanceUtil.class, "getMaximumProbability", subSkillType.getClass())).toReturn(100D);
+        PowerMockito.stub(PowerMockito.method(RandomChanceUtil.class, "getMaxBonusLevelCap", subSkillType.getClass())).toReturn(1000D);
+
+        normalPlayer = mock(Player.class);
+        luckyPlayer = mock(Player.class);
+
+        mmoPlayerNormal = mock(McMMOPlayer.class);
+        mmoPlayerLucky = mock(McMMOPlayer.class);
+
+        PowerMockito.mockStatic(UserManager.class);
+        Mockito.when(UserManager.getPlayer(normalPlayer)).thenReturn(mmoPlayerNormal);
+        Mockito.when(UserManager.getPlayer(luckyPlayer)).thenReturn(mmoPlayerLucky);
+
+        Mockito.when(mmoPlayerNormal.getPlayer()).thenReturn(normalPlayer);
+        Mockito.when(mmoPlayerLucky.getPlayer()).thenReturn(luckyPlayer);
+
+        //Lucky player has the lucky permission
+        //Normal player doesn't have any lucky permission
+        Mockito.when(Permissions.lucky(luckyPlayer, primarySkillType)).thenReturn(true);
+        Mockito.when(Permissions.lucky(normalPlayer, primarySkillType)).thenReturn(false);
+
+        Mockito.when(mmoPlayerNormal.getSkillLevel(primarySkillType)).thenReturn(800);
+        Mockito.when(mmoPlayerLucky.getSkillLevel(primarySkillType)).thenReturn(800);
+    }
+
+    @Test
+    public void testLuckyChance() {
+        System.out.println(testASCIIHeader);
+        System.out.println("Testing success odds to fall within expected values...");
+        assertEquals(80D, getSuccessChance(mmoPlayerNormal),0D);
+        assertEquals(80D * RandomChanceUtil.LUCKY_MODIFIER, getSuccessChance(mmoPlayerLucky),0D);
+    }
+
+    @Test
+    public void testNeverFailsSuccessLuckyPlayer() {
+        System.out.println(testASCIIHeader);
+        System.out.println("Test - Lucky Player with 80% base success should never fail (10,000 iterations)");
+        for(int x = 0; x < 10000; x++) {
+            Assert.assertTrue(RandomChanceUtil.checkRandomChanceExecutionSuccess(luckyPlayer, SubSkillType.HERBALISM_GREEN_THUMB, true));
+            if(x == 10000-1)
+                System.out.println("They never failed!");
+        }
+    }
+
+    @Test
+    public void testFailsAboutExpected() {
+        System.out.println(testASCIIHeader);
+        System.out.println("Test - Player with 800 skill should fail about 20% of the time (100,000 iterations)");
+        double ratioDivisor = 1000; //1000 because we run the test 100,000 times
+        double expectedFailRate = 20D;
+
+        double win = 0, loss = 0;
+        for(int x = 0; x < 100000; x++) {
+            if(RandomChanceUtil.checkRandomChanceExecutionSuccess(normalPlayer, SubSkillType.HERBALISM_GREEN_THUMB, true)) {
+                win++;
+            } else {
+                loss++;
+            }
+        }
+
+        double lossRatio = (loss / ratioDivisor);
+        Assert.assertEquals(lossRatio, expectedFailRate, 1D);
+    }
+
+    private double getSuccessChance(@NotNull McMMOPlayer mmoPlayer) {
+        RandomChanceSkill randomChanceSkill = new RandomChanceSkill(mmoPlayer.getPlayer(), subSkillType, true);
+        return RandomChanceUtil.calculateChanceOfSuccess(randomChanceSkill);
+    }
+
+    private void assertEquals(double expected, double actual, double delta) {
+        Assert.assertEquals(expected, actual, delta);
+    }
+}