Bläddra i källkod

mcMMO 2.2.000-RC1 candidate for release

nossr50 1 år sedan
förälder
incheckning
81ac48904c
100 ändrade filer med 1500 tillägg och 919 borttagningar
  1. 58 12
      Changelog.txt
  2. 8 1
      pom.xml
  3. 2 4
      src/main/java/com/gmail/nossr50/api/ExperienceAPI.java
  4. 0 2
      src/main/java/com/gmail/nossr50/api/PartyAPI.java
  5. 9 0
      src/main/java/com/gmail/nossr50/api/exceptions/ValueOutOfBoundsException.java
  6. 2 1
      src/main/java/com/gmail/nossr50/chat/mailer/AdminChatMailer.java
  7. 3 1
      src/main/java/com/gmail/nossr50/chat/message/PartyChatMessage.java
  8. 32 1
      src/main/java/com/gmail/nossr50/commands/CommandManager.java
  9. 0 1
      src/main/java/com/gmail/nossr50/commands/chat/PartyChatCommand.java
  10. 0 1
      src/main/java/com/gmail/nossr50/commands/party/PartyAcceptCommand.java
  11. 0 1
      src/main/java/com/gmail/nossr50/commands/party/PartyChangeOwnerCommand.java
  12. 0 1
      src/main/java/com/gmail/nossr50/commands/party/PartyCreateCommand.java
  13. 0 1
      src/main/java/com/gmail/nossr50/commands/party/PartyDisbandCommand.java
  14. 0 1
      src/main/java/com/gmail/nossr50/commands/party/PartyInfoCommand.java
  15. 0 1
      src/main/java/com/gmail/nossr50/commands/party/PartyInviteCommand.java
  16. 0 1
      src/main/java/com/gmail/nossr50/commands/party/PartyJoinCommand.java
  17. 0 1
      src/main/java/com/gmail/nossr50/commands/party/PartyKickCommand.java
  18. 0 1
      src/main/java/com/gmail/nossr50/commands/party/PartyQuitCommand.java
  19. 0 1
      src/main/java/com/gmail/nossr50/commands/party/PartyRenameCommand.java
  20. 0 1
      src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceAcceptCommand.java
  21. 0 1
      src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceCommand.java
  22. 0 1
      src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceDisbandCommand.java
  23. 0 1
      src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceInviteCommand.java
  24. 0 1
      src/main/java/com/gmail/nossr50/commands/party/teleport/PtpCommand.java
  25. 6 25
      src/main/java/com/gmail/nossr50/commands/skills/AcrobaticsCommand.java
  26. 2 2
      src/main/java/com/gmail/nossr50/commands/skills/AlchemyCommand.java
  27. 8 7
      src/main/java/com/gmail/nossr50/commands/skills/ArcheryCommand.java
  28. 7 7
      src/main/java/com/gmail/nossr50/commands/skills/AxesCommand.java
  29. 77 0
      src/main/java/com/gmail/nossr50/commands/skills/CrossbowsCommand.java
  30. 2 2
      src/main/java/com/gmail/nossr50/commands/skills/ExcavationCommand.java
  31. 11 8
      src/main/java/com/gmail/nossr50/commands/skills/FishingCommand.java
  32. 26 11
      src/main/java/com/gmail/nossr50/commands/skills/HerbalismCommand.java
  33. 20 4
      src/main/java/com/gmail/nossr50/commands/skills/MiningCommand.java
  34. 40 0
      src/main/java/com/gmail/nossr50/commands/skills/PowerLevelCommand.java
  35. 5 5
      src/main/java/com/gmail/nossr50/commands/skills/RepairCommand.java
  36. 3 2
      src/main/java/com/gmail/nossr50/commands/skills/SalvageCommand.java
  37. 4 19
      src/main/java/com/gmail/nossr50/commands/skills/SkillCommand.java
  38. 5 5
      src/main/java/com/gmail/nossr50/commands/skills/SmeltingCommand.java
  39. 7 6
      src/main/java/com/gmail/nossr50/commands/skills/SwordsCommand.java
  40. 10 10
      src/main/java/com/gmail/nossr50/commands/skills/TamingCommand.java
  41. 62 0
      src/main/java/com/gmail/nossr50/commands/skills/TridentsCommand.java
  42. 9 9
      src/main/java/com/gmail/nossr50/commands/skills/UnarmedCommand.java
  43. 24 13
      src/main/java/com/gmail/nossr50/commands/skills/WoodcuttingCommand.java
  44. 9 0
      src/main/java/com/gmail/nossr50/config/AdvancedConfig.java
  45. 6 0
      src/main/java/com/gmail/nossr50/config/ChatConfig.java
  46. 2 0
      src/main/java/com/gmail/nossr50/config/GeneralConfig.java
  47. 53 72
      src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java
  48. 3 2
      src/main/java/com/gmail/nossr50/database/DatabaseManagerFactory.java
  49. 7 0
      src/main/java/com/gmail/nossr50/database/FlatFileDataProcessor.java
  50. 55 29
      src/main/java/com/gmail/nossr50/database/FlatFileDatabaseManager.java
  51. 230 127
      src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java
  52. 7 0
      src/main/java/com/gmail/nossr50/database/flatfile/FlatFileDataUtil.java
  53. 0 1
      src/main/java/com/gmail/nossr50/datatypes/party/Party.java
  54. 19 15
      src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java
  55. 12 5
      src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java
  56. 2 0
      src/main/java/com/gmail/nossr50/datatypes/skills/PrimarySkillType.java
  57. 14 5
      src/main/java/com/gmail/nossr50/datatypes/skills/SubSkillType.java
  58. 45 47
      src/main/java/com/gmail/nossr50/datatypes/skills/SuperAbilityType.java
  59. 8 1
      src/main/java/com/gmail/nossr50/datatypes/skills/ToolType.java
  60. 31 23
      src/main/java/com/gmail/nossr50/datatypes/skills/subskills/acrobatics/Roll.java
  61. 38 4
      src/main/java/com/gmail/nossr50/datatypes/treasure/Treasure.java
  62. 3 3
      src/main/java/com/gmail/nossr50/events/skills/McMMOPlayerSkillEvent.java
  63. 41 47
      src/main/java/com/gmail/nossr50/events/skills/secondaryabilities/SubSkillRandomCheckEvent.java
  64. 1 1
      src/main/java/com/gmail/nossr50/listeners/BlockListener.java
  65. 0 40
      src/main/java/com/gmail/nossr50/listeners/CommandListener.java
  66. 38 52
      src/main/java/com/gmail/nossr50/listeners/EntityListener.java
  67. 3 0
      src/main/java/com/gmail/nossr50/listeners/PlayerListener.java
  68. 1 4
      src/main/java/com/gmail/nossr50/mcMMO.java
  69. 0 3
      src/main/java/com/gmail/nossr50/party/PartyManager.java
  70. 5 0
      src/main/java/com/gmail/nossr50/placeholders/PapiExpansion.java
  71. 0 1
      src/main/java/com/gmail/nossr50/runnables/SaveTimerTask.java
  72. 0 1
      src/main/java/com/gmail/nossr50/runnables/items/TeleportationWarmup.java
  73. 0 1
      src/main/java/com/gmail/nossr50/runnables/party/PartyAutoKickTask.java
  74. 1 3
      src/main/java/com/gmail/nossr50/runnables/skills/AbilityCooldownTask.java
  75. 0 1
      src/main/java/com/gmail/nossr50/runnables/skills/RuptureTask.java
  76. 3 3
      src/main/java/com/gmail/nossr50/skills/acrobatics/AcrobaticsManager.java
  77. 6 7
      src/main/java/com/gmail/nossr50/skills/archery/ArcheryManager.java
  78. 11 8
      src/main/java/com/gmail/nossr50/skills/axes/AxesManager.java
  79. 0 64
      src/main/java/com/gmail/nossr50/skills/child/ChildConfig.java
  80. 0 54
      src/main/java/com/gmail/nossr50/skills/child/FamilyTree.java
  81. 41 0
      src/main/java/com/gmail/nossr50/skills/crossbows/Crossbows.java
  82. 108 0
      src/main/java/com/gmail/nossr50/skills/crossbows/CrossbowsManager.java
  83. 32 13
      src/main/java/com/gmail/nossr50/skills/excavation/ExcavationManager.java
  84. 8 6
      src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java
  85. 6 8
      src/main/java/com/gmail/nossr50/skills/herbalism/HerbalismManager.java
  86. 32 4
      src/main/java/com/gmail/nossr50/skills/mining/MiningManager.java
  87. 4 6
      src/main/java/com/gmail/nossr50/skills/repair/RepairManager.java
  88. 4 5
      src/main/java/com/gmail/nossr50/skills/salvage/SalvageManager.java
  89. 2 3
      src/main/java/com/gmail/nossr50/skills/smelting/SmeltingManager.java
  90. 5 4
      src/main/java/com/gmail/nossr50/skills/swords/SwordsManager.java
  91. 8 21
      src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java
  92. 35 0
      src/main/java/com/gmail/nossr50/skills/tridents/TridentsManager.java
  93. 8 9
      src/main/java/com/gmail/nossr50/skills/unarmed/UnarmedManager.java
  94. 46 17
      src/main/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingManager.java
  95. 18 19
      src/main/java/com/gmail/nossr50/util/BlockUtils.java
  96. 2 3
      src/main/java/com/gmail/nossr50/util/EventUtils.java
  97. 12 0
      src/main/java/com/gmail/nossr50/util/ItemUtils.java
  98. 8 4
      src/main/java/com/gmail/nossr50/util/MaterialMapStore.java
  99. 3 0
      src/main/java/com/gmail/nossr50/util/MetadataConstants.java
  100. 22 0
      src/main/java/com/gmail/nossr50/util/Misc.java

+ 58 - 12
Changelog.txt

@@ -1,8 +1,54 @@
-Version 2.1.232
-    Added new setting to experience.yml 'ExploitFix.PreventArmorStandInteraction' can be turned to false to enable mcMMO abilities on armor stands again
+Version 2.2.000
+    General
+    Added Crossbows Skill, this skill is a WIP and feedback on discord is appreciated
+    Added Tridents Skill, this skill is a WIP and feedback on discord is appreciated
+    Added the "endgame" triple drop subskill 'Mother Lode' to Mining
+    Added the "endgame" triple drop subskill 'Clean Cuts' to Woodcutting
+    Added the "endgame" triple drop subskill 'Verdant Bounty' to Herbalism
+    Added /mmopower command which simply shows your power level (aliases /mmopowerlevel /powerlevel)
+
+    Config
+    Added 'Send_To_Console' settings to chat.yml to toggle sending party or admin chat messages to console
+    Replaced 'Experience_Formula.Modifier' in experience.yml with 'Experience_Formula.Skill_Multiplier' which is easier to understand and less prone to divide by zero bugs
+    child.yml config is gone now, feel free to delete it
+    Added ExploitFix.PreventArmorStandInteraction to experience.yml to prevent players from triggering mcMMO abilities off armor stands
+
+    Tweaks
+    Tree Feller now drops 90% less non-wood block rewards (leaves/etc) on average from Knock on Wood.
+    Treasure drop rate from Shake, Fishing, Hylian, and Excavation now benefit from the Luck perk
+    Updated advanced.yml with entries for the new skills
+
+    Permission nodes
+    Added 'mcmmo.commands.mmopower' permission node for the new /mmopower command
+    Added 'mcmmo.commands.crossbows' permission node
+    Added 'mcmmo.ability.crossbows.crossbowslimitbreak' permission node
+    Added 'mcmmo.ability.crossbows.trickshot' permission node
+    Added 'mcmmo.ability.herbalism.verdantbounty' permission node
+    Added 'mcmmo.ability.mining.motherlode' permission node
+    Added 'mcmmo.ability.woodcutting.cleancuts' permission node
+
+    Locale
+    Added locale entries for motherlode, cleancuts, and verdant bounty
+
+    Codebase
+    Major rewrite for how random chance was handled in the code
+    Many skills with RNG elements now send out a SubSkillEvent (which can be used to modify probability or cancel the results), some skills without RNG still send out this event when activated, this event is cancellable so it can be used to make a skill fail
+    A lot of new unit tests were added to help keep mcMMO stable as part of this update, of course, more could always be added.
 
 
     NOTES:
     NOTES:
-    The new setting simply prevents mcMMO abilities from activating when attacking an armor stand, this prevents some exploits
+    One feature of this update is to provide a endgame benefits to some skills that you can grind for a long time, ideally for a long while. I will likely expand upon this idea in future updates.
+    A few skills have these endgame oriented subskills, these new subskills provide a small benefit at first that grows and scales up to level 10,000 (or 1,000 for Standard mode which no one uses) and does not have ranks (other than the initial rank to unlock it).
+    These endgame sub skills unlock at level 1000 for users with default mcMMO settings, or 100 for those using the optional Standard scaling.
+    You can tweak the benefits of these skills in advanced.yml, the default settings are meant to be a good starting point.
+
+    Crossbows and Tridents are WIP skills, I would like feedback on discord about them.
+
+    More info on the new Triple Drop skills (Mother Lode, Clean Cuts, Verdant Bounty):
+    Currently these start at about 5%  chance and can reach a maximum 50% chance if a player acquired 10,000 skill, you can adjust this in advanced.yml
+    These skills respect double drop settings from config.yml just like the corresponding Double Drop skills do, if a double drop is disabled for an item, then its disabled for triple drops too.
+    I added a new Power Level Command, for now this just shows you your current power level. If I ever add features based on power level, this command will likely display output related to those features.
+
+    Regarding Maces, I will likely add that as a WIP skill when the next Minecraft update drops.
 
 
 Version 2.1.231
 Version 2.1.231
     Fixed a bug preventing parties from being made without passwords (Thanks Momshroom)
     Fixed a bug preventing parties from being made without passwords (Thanks Momshroom)
@@ -1337,7 +1383,7 @@ Version 2.1.128
     Fixed a bug where certain types of ore did not receive bonuses from Blast Mining
     Fixed a bug where certain types of ore did not receive bonuses from Blast Mining
     Fixed a few locale errors with commands
     Fixed a few locale errors with commands
     (API) Added ExperienceAPI::addCombatXP for adding combat XP to players, signature may change so its deprecated for now
     (API) Added ExperienceAPI::addCombatXP for adding combat XP to players, signature may change so its deprecated for now
-    mcMMO now logs whether or not its using FlatFile or SQL database on load
+    mcMMO now logs whether its using FlatFile or SQL database on load
     (1.16) Strider added to combat experience with a value of 1.2
     (1.16) Strider added to combat experience with a value of 1.2
 
 
     NOTES: A more thorough look at Unarmed balance will happen in the future, the intention of this nerf is to make Unarmed less rewarding until it is leveled quite a bit.
     NOTES: A more thorough look at Unarmed balance will happen in the future, the intention of this nerf is to make Unarmed less rewarding until it is leveled quite a bit.
@@ -1357,7 +1403,7 @@ Version 2.1.127
 Version 2.1.126
 Version 2.1.126
     mcMMO now relies on NMS for some of its features, if NMS cannot properly be wired up when initializing mcMMO behaviours relying on NMS will either be partially supported or disabled
     mcMMO now relies on NMS for some of its features, if NMS cannot properly be wired up when initializing mcMMO behaviours relying on NMS will either be partially supported or disabled
     mcMMO now has a compatibility mode, any features that require specific versions of Minecraft for full functionality will be disabled if your server is not running a compatible version, mcMMO will still function in compatibility mode, but either the feature will be modified or disabled depending on the version of the server software
     mcMMO now has a compatibility mode, any features that require specific versions of Minecraft for full functionality will be disabled if your server is not running a compatible version, mcMMO will still function in compatibility mode, but either the feature will be modified or disabled depending on the version of the server software
-    New command /mmocompat - Shows information about whether or not mcMMO is fully functional or if some features are disabled due to the server software not being fully supported. Can be used by players or console.
+    New command /mmocompat - Shows information about whether mcMMO is fully functional or if some features are disabled due to the server software not being fully supported. Can be used by players or console.
     New command /mmoxpbar (alias /xpbarsettings) - Players can choose to always show XP bars or to never show XP bars on a per skill basis
     New command /mmoxpbar (alias /xpbarsettings) - Players can choose to always show XP bars or to never show XP bars on a per skill basis
     XPBars now last for 3 seconds before hiding instead of 2 seconds
     XPBars now last for 3 seconds before hiding instead of 2 seconds
     Fixed an exploit involving fishing rods
     Fixed an exploit involving fishing rods
@@ -1993,7 +2039,7 @@ Version 2.1.68
     Fixed a bug where consuming food in the off hand did not trigger the Diet abilities
     Fixed a bug where consuming food in the off hand did not trigger the Diet abilities
 
 
 Version 2.1.67
 Version 2.1.67
-    The XP bar now reflects whether or not the player is receiving the early game boost
+    The XP bar now reflects whether the player is receiving the early game boost
     Players who are receiving an early game boost will be shown "Learning a skill..." as the title of the XP bar while gaining XP
     Players who are receiving an early game boost will be shown "Learning a skill..." as the title of the XP bar while gaining XP
     New locale string 'XPBar.Template.EarlyGameBoost'
     New locale string 'XPBar.Template.EarlyGameBoost'
 
 
@@ -2042,7 +2088,7 @@ Version 2.1.63
 Version 2.1.62
 Version 2.1.62
     Added a new admin notification system, sensitive commands will print chat messages to "admins" (players with either Operator status or admin chat permission)
     Added a new admin notification system, sensitive commands will print chat messages to "admins" (players with either Operator status or admin chat permission)
     Added a setting to disable the new admin notifications to config.yml 'General.AdminNotifications' (this will be more configurable in 2.2)
     Added a setting to disable the new admin notifications to config.yml 'General.AdminNotifications' (this will be more configurable in 2.2)
-    OPs and players with the admin chat permission will now see details about XP rate event commands regardless of whether or not the XP rate event messages are enabled
+    OPs and players with the admin chat permission will now see details about XP rate event commands regardless of whether the XP rate event messages are enabled
     Updated hu_HU locale (thanks andris155)
     Updated hu_HU locale (thanks andris155)
     Added XP for mining Magma_Block (default 30 XP - Update your config, see notes)
     Added XP for mining Magma_Block (default 30 XP - Update your config, see notes)
     Diamond tools & armor in the repair config now have a minimum level of 0 (Update your config, temporary hotfix, 2.2 addresses this issue, see notes)
     Diamond tools & armor in the repair config now have a minimum level of 0 (Update your config, temporary hotfix, 2.2 addresses this issue, see notes)
@@ -2050,9 +2096,9 @@ Version 2.1.62
     New locale string - 'Server.ConsoleName' the name of the server console, this will be used in place of player names when sending admin notifications out if the command was used from console
     New locale string - 'Server.ConsoleName' the name of the server console, this will be used in place of player names when sending admin notifications out if the command was used from console
     New locale string - 'Notifications.Admin.Format.Others' style formatting + prefix for admin notifications used in the other new strings below
     New locale string - 'Notifications.Admin.Format.Others' style formatting + prefix for admin notifications used in the other new strings below
     New locale string - 'Notifications.Admin.Format.Self' style formatting + prefix for admin command confirmations sent to the user who executed the command
     New locale string - 'Notifications.Admin.Format.Self' style formatting + prefix for admin command confirmations sent to the user who executed the command
-    New locale string - 'Notifications.Admin.XPRate.Start.Self' sent to the user who modifies the XP rate regardless of whether or not messages for the event are enabled
+    New locale string - 'Notifications.Admin.XPRate.Start.Self' sent to the user who modifies the XP rate regardless of whether messages for the event are enabled
     New locale string - 'Notifications.Admin.XPRate.Start.Others' details of who started an XP rate event are sent to players who have Operator status or admin chat permission when the command to start or modify XP of an event has been issued
     New locale string - 'Notifications.Admin.XPRate.Start.Others' details of who started an XP rate event are sent to players who have Operator status or admin chat permission when the command to start or modify XP of an event has been issued
-    New locale string - 'Notifications.Admin.XPRate.End.Self' sent to the user who ended the XP rate event regardless of whether or not messages for the event are enabled
+    New locale string - 'Notifications.Admin.XPRate.End.Self' sent to the user who ended the XP rate event regardless of whether messages for the event are enabled
     New locale string - 'Notifications.Admin.XPRate.End.Others' details of who ended an XP rate event are sent to players who have Operator status or admin chat permission when the command to end the event has been issued
     New locale string - 'Notifications.Admin.XPRate.End.Others' details of who ended an XP rate event are sent to players who have Operator status or admin chat permission when the command to end the event has been issued
 
 
     NOTES:
     NOTES:
@@ -2323,7 +2369,7 @@ Version 2.1.26
 
 
     Notes:
     Notes:
     The new Limit Break subskills are intended to make Prot IV players less tanky and for you to feel more powerful for having high skill level.
     The new Limit Break subskills are intended to make Prot IV players less tanky and for you to feel more powerful for having high skill level.
-    Limit Break has 10 ranks, each rank gives 1 extra RAW damage, this is damage before reductions from armor and enchantments. The net result is you deal about 50% more damage with an end game skill compared to before.
+    Limit Break has 10 ranks, each rank gives 1 extra RAW damage, this is damage before reductions from armor and enchantments. The net result is you deal about 50% more damage with an endgame skill compared to before.
     With these new changes, most skills can 2 shot normal diamond armor, and it takes about 5 hits to kill someone in Prot IV Diamond Armor.
     With these new changes, most skills can 2 shot normal diamond armor, and it takes about 5 hits to kill someone in Prot IV Diamond Armor.
     I'm not sure everyone will like these changes, the net result is players are a lot easier to kill now, whereas before you could take quite a beating before getting killed.
     I'm not sure everyone will like these changes, the net result is players are a lot easier to kill now, whereas before you could take quite a beating before getting killed.
     I collected several sets of data before making these changes, including damage to player with and without prot 4 diamond armor, damage to those players with and without enchanted weapons, damage with and without leveling your skills, and combinations of the previously mentioned things.
     I collected several sets of data before making these changes, including damage to player with and without prot 4 diamond armor, damage to those players with and without enchanted weapons, damage with and without leveling your skills, and combinations of the previously mentioned things.
@@ -4024,7 +4070,7 @@ Removed performance debugging
 Removed some useless settings from the config file
 Removed some useless settings from the config file
 
 
 Version 1.0.34
 Version 1.0.34
-Fixed the PVP setting determining whether or not you would hurt yourself from AoE Abilities
+Fixed the PVP setting determining whether you would hurt yourself from AoE Abilities
 Added Dutch (nl) language support
 Added Dutch (nl) language support
 Super Breaker now gives the correct XP as determined by config.yml
 Super Breaker now gives the correct XP as determined by config.yml
 Sand Stone XP is now configurable and no longer shares the 'stone' node
 Sand Stone XP is now configurable and no longer shares the 'stone' node
@@ -4034,7 +4080,7 @@ Version 1.0.33
 Fixed the toggle for the Excavation drop 'Cocoa Beans'
 Fixed the toggle for the Excavation drop 'Cocoa Beans'
 Fixed bug where Unarmed users could disarm without being bare handed
 Fixed bug where Unarmed users could disarm without being bare handed
 Cocoa Beans now have an XP modifier in config.yml
 Cocoa Beans now have an XP modifier in config.yml
-You can now toggle whether or not Mobspawners will give XP (in config.yml)
+You can now toggle whether Mobspawners will give XP (in config.yml)
 MySQL version now makes requests to the MySQL server less frequently (should help performance)
 MySQL version now makes requests to the MySQL server less frequently (should help performance)
 Fixed bug with Skull Splitter hitting the user
 Fixed bug with Skull Splitter hitting the user
 
 

+ 8 - 1
pom.xml

@@ -2,7 +2,7 @@
     <modelVersion>4.0.0</modelVersion>
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.gmail.nossr50.mcMMO</groupId>
     <groupId>com.gmail.nossr50.mcMMO</groupId>
     <artifactId>mcMMO</artifactId>
     <artifactId>mcMMO</artifactId>
-    <version>2.1.232-SNAPSHOT</version>
+    <version>2.2.000-RC1</version>
     <name>mcMMO</name>
     <name>mcMMO</name>
     <url>https://github.com/mcMMO-Dev/mcMMO</url>
     <url>https://github.com/mcMMO-Dev/mcMMO</url>
     <scm>
     <scm>
@@ -258,6 +258,13 @@
         <!-- ... -->
         <!-- ... -->
     </repositories>
     </repositories>
     <dependencies>
     <dependencies>
+        <!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <version>2.2.224</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
         <dependency>
             <groupId>me.clip</groupId>
             <groupId>me.clip</groupId>
             <artifactId>placeholderapi</artifactId>
             <artifactId>placeholderapi</artifactId>

+ 2 - 4
src/main/java/com/gmail/nossr50/api/ExperienceAPI.java

@@ -9,7 +9,6 @@ import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.PlayerProfile;
 import com.gmail.nossr50.datatypes.player.PlayerProfile;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.skills.child.FamilyTree;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.SkillTools;
 import com.gmail.nossr50.util.skills.SkillTools;
@@ -20,7 +19,6 @@ import org.bukkit.entity.Player;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
-import java.util.Set;
 import java.util.UUID;
 import java.util.UUID;
 
 
 public final class ExperienceAPI {
 public final class ExperienceAPI {
@@ -706,7 +704,7 @@ public final class ExperienceAPI {
         PrimarySkillType skill = getSkillType(skillType);
         PrimarySkillType skill = getSkillType(skillType);
 
 
         if (SkillTools.isChildSkill(skill)) {
         if (SkillTools.isChildSkill(skill)) {
-            Set<PrimarySkillType> parentSkills = FamilyTree.getParents(skill);
+            var parentSkills = mcMMO.p.getSkillTools().getChildSkillParents(skill);
 
 
             for (PrimarySkillType parentSkill : parentSkills) {
             for (PrimarySkillType parentSkill : parentSkills) {
                 profile.addLevels(parentSkill, (levels / parentSkills.size()));
                 profile.addLevels(parentSkill, (levels / parentSkills.size()));
@@ -737,7 +735,7 @@ public final class ExperienceAPI {
         PrimarySkillType skill = getSkillType(skillType);
         PrimarySkillType skill = getSkillType(skillType);
 
 
         if (SkillTools.isChildSkill(skill)) {
         if (SkillTools.isChildSkill(skill)) {
-            Set<PrimarySkillType> parentSkills = FamilyTree.getParents(skill);
+            var parentSkills = mcMMO.p.getSkillTools().getChildSkillParents(skill);
 
 
             for (PrimarySkillType parentSkill : parentSkills) {
             for (PrimarySkillType parentSkill : parentSkills) {
                 profile.addLevels(parentSkill, (levels / parentSkills.size()));
                 profile.addLevels(parentSkill, (levels / parentSkills.size()));

+ 0 - 2
src/main/java/com/gmail/nossr50/api/PartyAPI.java

@@ -5,10 +5,8 @@ import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.PartyLeader;
 import com.gmail.nossr50.datatypes.party.PartyLeader;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
-import jdk.jfr.Experimental;
 import org.bukkit.OfflinePlayer;
 import org.bukkit.OfflinePlayer;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
 import org.jetbrains.annotations.Nullable;
 import org.jetbrains.annotations.Nullable;

+ 9 - 0
src/main/java/com/gmail/nossr50/api/exceptions/ValueOutOfBoundsException.java

@@ -0,0 +1,9 @@
+package com.gmail.nossr50.api.exceptions;
+
+import org.jetbrains.annotations.NotNull;
+
+public class ValueOutOfBoundsException extends RuntimeException {
+    public ValueOutOfBoundsException(@NotNull String message) {
+        super(message);
+    }
+}

+ 2 - 1
src/main/java/com/gmail/nossr50/chat/mailer/AdminChatMailer.java

@@ -3,6 +3,7 @@ package com.gmail.nossr50.chat.mailer;
 import com.gmail.nossr50.chat.author.Author;
 import com.gmail.nossr50.chat.author.Author;
 import com.gmail.nossr50.chat.message.AdminChatMessage;
 import com.gmail.nossr50.chat.message.AdminChatMessage;
 import com.gmail.nossr50.chat.message.ChatMessage;
 import com.gmail.nossr50.chat.message.ChatMessage;
+import com.gmail.nossr50.config.ChatConfig;
 import com.gmail.nossr50.datatypes.chat.ChatChannel;
 import com.gmail.nossr50.datatypes.chat.ChatChannel;
 import com.gmail.nossr50.events.chat.McMMOAdminChatEvent;
 import com.gmail.nossr50.events.chat.McMMOAdminChatEvent;
 import com.gmail.nossr50.events.chat.McMMOChatEvent;
 import com.gmail.nossr50.events.chat.McMMOChatEvent;
@@ -44,7 +45,7 @@ public class AdminChatMailer extends AbstractChatMailer {
     public @NotNull Predicate<CommandSender> predicate() {
     public @NotNull Predicate<CommandSender> predicate() {
         return (commandSender) -> commandSender.isOp()
         return (commandSender) -> commandSender.isOp()
                 || commandSender.hasPermission(MCMMO_CHAT_ADMINCHAT_PERMISSION)
                 || commandSender.hasPermission(MCMMO_CHAT_ADMINCHAT_PERMISSION)
-                || commandSender instanceof ConsoleCommandSender;
+                || (ChatConfig.getInstance().isConsoleIncludedInAudience(ChatChannel.ADMIN) && commandSender instanceof ConsoleCommandSender);
     }
     }
 
 
     /**
     /**

+ 3 - 1
src/main/java/com/gmail/nossr50/chat/message/PartyChatMessage.java

@@ -1,6 +1,7 @@
 package com.gmail.nossr50.chat.message;
 package com.gmail.nossr50.chat.message;
 
 
 import com.gmail.nossr50.chat.author.Author;
 import com.gmail.nossr50.chat.author.Author;
+import com.gmail.nossr50.config.ChatConfig;
 import com.gmail.nossr50.datatypes.chat.ChatChannel;
 import com.gmail.nossr50.datatypes.chat.ChatChannel;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
@@ -51,7 +52,8 @@ public class PartyChatMessage extends AbstractChatMessage {
         messagePartyChatSpies(spyMessage);
         messagePartyChatSpies(spyMessage);
 
 
         //Console message
         //Console message
-        mcMMO.p.getChatManager().sendConsoleMessage(author, spyMessage);
+        if(ChatConfig.getInstance().isConsoleIncludedInAudience(ChatChannel.PARTY))
+            mcMMO.p.getChatManager().sendConsoleMessage(author, spyMessage);
     }
     }
 
 
     /**
     /**

+ 32 - 1
src/main/java/com/gmail/nossr50/commands/CommandManager.java

@@ -5,6 +5,7 @@ import co.aikar.commands.BukkitCommandManager;
 import co.aikar.commands.ConditionFailedException;
 import co.aikar.commands.ConditionFailedException;
 import com.gmail.nossr50.commands.chat.AdminChatCommand;
 import com.gmail.nossr50.commands.chat.AdminChatCommand;
 import com.gmail.nossr50.commands.chat.PartyChatCommand;
 import com.gmail.nossr50.commands.chat.PartyChatCommand;
+import com.gmail.nossr50.commands.skills.PowerLevelCommand;
 import com.gmail.nossr50.config.ChatConfig;
 import com.gmail.nossr50.config.ChatConfig;
 import com.gmail.nossr50.datatypes.chat.ChatChannel;
 import com.gmail.nossr50.datatypes.chat.ChatChannel;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
@@ -20,9 +21,14 @@ import org.jetbrains.annotations.NotNull;
  * For now this class will only handle ACF converted commands, all other commands will be handled elsewhere
  * For now this class will only handle ACF converted commands, all other commands will be handled elsewhere
  */
  */
 public class CommandManager {
 public class CommandManager {
+    public static final @NotNull String MMO_DATA_LOADED = "mmoDataLoaded";
+
+    //CHAT
     public static final @NotNull String ADMIN_CONDITION = "adminCondition";
     public static final @NotNull String ADMIN_CONDITION = "adminCondition";
     public static final @NotNull String PARTY_CONDITION = "partyCondition";
     public static final @NotNull String PARTY_CONDITION = "partyCondition";
-    public static final @NotNull String MMO_DATA_LOADED = "mmoDataLoaded";
+
+    //SKILLS
+    public static final @NotNull String POWER_LEVEL_CONDITION = "powerLevelCondition";
 
 
     private final @NotNull mcMMO pluginRef;
     private final @NotNull mcMMO pluginRef;
     private final @NotNull BukkitCommandManager bukkitCommandManager;
     private final @NotNull BukkitCommandManager bukkitCommandManager;
@@ -36,9 +42,16 @@ public class CommandManager {
     }
     }
 
 
     private void registerCommands() {
     private void registerCommands() {
+        registerSkillCommands(); //TODO: Implement other skills not just power level
         registerChatCommands();
         registerChatCommands();
     }
     }
 
 
+    private void registerSkillCommands() {
+        if(mcMMO.p.getGeneralConfig().isMasterySystemEnabled()) {
+            bukkitCommandManager.registerCommand(new PowerLevelCommand(pluginRef));
+        }
+    }
+
     /**
     /**
      * Registers chat commands if the chat system is enabled
      * Registers chat commands if the chat system is enabled
      */
      */
@@ -54,6 +67,23 @@ public class CommandManager {
     }
     }
 
 
     public void registerConditions() {
     public void registerConditions() {
+        registerChatCommandConditions(); //Chat Commands
+        registerSkillConditions();
+    }
+
+    private void registerSkillConditions() {
+        bukkitCommandManager.getCommandConditions().addCondition(POWER_LEVEL_CONDITION, (context) -> {
+            BukkitCommandIssuer issuer = context.getIssuer();
+
+            if(issuer.getIssuer() instanceof Player) {
+                validateLoadedData(issuer.getPlayer());
+            } else {
+                throw new ConditionFailedException(LocaleLoader.getString("Commands.NoConsole"));
+            }
+        });
+    }
+
+    private void registerChatCommandConditions() {
         // Method or Class based - Can only be used on methods
         // Method or Class based - Can only be used on methods
         bukkitCommandManager.getCommandConditions().addCondition(ADMIN_CONDITION, (context) -> {
         bukkitCommandManager.getCommandConditions().addCondition(ADMIN_CONDITION, (context) -> {
             BukkitCommandIssuer issuer = context.getIssuer();
             BukkitCommandIssuer issuer = context.getIssuer();
@@ -78,6 +108,7 @@ public class CommandManager {
             if(bukkitCommandIssuer.getIssuer() instanceof Player) {
             if(bukkitCommandIssuer.getIssuer() instanceof Player) {
                 validateLoadedData(bukkitCommandIssuer.getPlayer());
                 validateLoadedData(bukkitCommandIssuer.getPlayer());
                 validatePlayerParty(bukkitCommandIssuer.getPlayer());
                 validatePlayerParty(bukkitCommandIssuer.getPlayer());
+                //TODO: Is there even a point in validating permission? look into this later
                 validatePermission("mcmmo.chat.partychat", bukkitCommandIssuer.getPlayer());
                 validatePermission("mcmmo.chat.partychat", bukkitCommandIssuer.getPlayer());
             }
             }
         });
         });

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/chat/PartyChatCommand.java

@@ -11,7 +11,6 @@ import com.gmail.nossr50.datatypes.chat.ChatChannel;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.StringUtils;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/PartyAcceptCommand.java

@@ -3,7 +3,6 @@ package com.gmail.nossr50.commands.party;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandExecutor;
 import org.bukkit.command.CommandExecutor;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/PartyChangeOwnerCommand.java

@@ -3,7 +3,6 @@ package com.gmail.nossr50.commands.party;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.OfflinePlayer;
 import org.bukkit.OfflinePlayer;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/PartyCreateCommand.java

@@ -3,7 +3,6 @@ package com.gmail.nossr50.commands.party;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandExecutor;
 import org.bukkit.command.CommandExecutor;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/PartyDisbandCommand.java

@@ -5,7 +5,6 @@ import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.events.party.McMMOPartyChangeEvent.EventReason;
 import com.gmail.nossr50.events.party.McMMOPartyChangeEvent.EventReason;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandExecutor;
 import org.bukkit.command.CommandExecutor;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/PartyInfoCommand.java

@@ -6,7 +6,6 @@ import com.gmail.nossr50.datatypes.party.ShareMode;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.ChatColor;
 import org.bukkit.ChatColor;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/PartyInviteCommand.java

@@ -4,7 +4,6 @@ import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/PartyJoinCommand.java

@@ -4,7 +4,6 @@ import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/PartyKickCommand.java

@@ -4,7 +4,6 @@ import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.events.party.McMMOPartyChangeEvent.EventReason;
 import com.gmail.nossr50.events.party.McMMOPartyChangeEvent.EventReason;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.OfflinePlayer;
 import org.bukkit.OfflinePlayer;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/PartyQuitCommand.java

@@ -5,7 +5,6 @@ import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.events.party.McMMOPartyChangeEvent.EventReason;
 import com.gmail.nossr50.events.party.McMMOPartyChangeEvent.EventReason;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandExecutor;
 import org.bukkit.command.CommandExecutor;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/PartyRenameCommand.java

@@ -5,7 +5,6 @@ import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.events.party.McMMOPartyChangeEvent.EventReason;
 import com.gmail.nossr50.events.party.McMMOPartyChangeEvent.EventReason;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandExecutor;
 import org.bukkit.command.CommandExecutor;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceAcceptCommand.java

@@ -3,7 +3,6 @@ package com.gmail.nossr50.commands.party.alliance;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandExecutor;
 import org.bukkit.command.CommandExecutor;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceCommand.java

@@ -5,7 +5,6 @@ import com.gmail.nossr50.datatypes.party.PartyFeature;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceDisbandCommand.java

@@ -4,7 +4,6 @@ import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandExecutor;
 import org.bukkit.command.CommandExecutor;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceInviteCommand.java

@@ -4,7 +4,6 @@ import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;

+ 0 - 1
src/main/java/com/gmail/nossr50/commands/party/teleport/PtpCommand.java

@@ -7,7 +7,6 @@ import com.gmail.nossr50.datatypes.party.PartyTeleportRecord;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.runnables.items.TeleportationWarmup;
 import com.gmail.nossr50.runnables.items.TeleportationWarmup;
 import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Misc;

+ 6 - 25
src/main/java/com/gmail/nossr50/commands/skills/AcrobaticsCommand.java

@@ -5,9 +5,8 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill;
 import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill;
 import com.gmail.nossr50.listeners.InteractionManager;
 import com.gmail.nossr50.listeners.InteractionManager;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
-import com.gmail.nossr50.util.random.RandomChanceSkill;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
-import com.gmail.nossr50.util.skills.SkillActivationType;
+import com.gmail.nossr50.util.Permissions;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.Component;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
@@ -30,7 +29,7 @@ public class AcrobaticsCommand extends SkillCommand {
     protected void dataCalculations(Player player, float skillValue) {
     protected void dataCalculations(Player player, float skillValue) {
         // ACROBATICS_DODGE
         // ACROBATICS_DODGE
         if (canDodge) {
         if (canDodge) {
-            String[] dodgeStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.ACROBATICS_DODGE);
+            String[] dodgeStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.ACROBATICS_DODGE);
             dodgeChance = dodgeStrings[0];
             dodgeChance = dodgeStrings[0];
             dodgeChanceLucky = dodgeStrings[1];
             dodgeChanceLucky = dodgeStrings[1];
         }
         }
@@ -38,8 +37,8 @@ public class AcrobaticsCommand extends SkillCommand {
 
 
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
-        canDodge = canUseSubskill(player, SubSkillType.ACROBATICS_DODGE);
-        canRoll = canUseSubskill(player, SubSkillType.ACROBATICS_ROLL);
+        canDodge = Permissions.canUseSubSkill(player, SubSkillType.ACROBATICS_DODGE);
+        canRoll = Permissions.canUseSubSkill(player, SubSkillType.ACROBATICS_ROLL);
     }
     }
 
 
     @Override
     @Override
@@ -57,25 +56,7 @@ public class AcrobaticsCommand extends SkillCommand {
 
 
             if(abstractSubSkill != null)
             if(abstractSubSkill != null)
             {
             {
-                double rollChance, graceChance;
-
-                //Chance to roll at half
-                RandomChanceSkill roll_rcs  = new RandomChanceSkill(player, SubSkillType.ACROBATICS_ROLL);
-
-                //Chance to graceful roll
-                RandomChanceSkill grace_rcs = new RandomChanceSkill(player, SubSkillType.ACROBATICS_ROLL);
-                grace_rcs.setSkillLevel(grace_rcs.getSkillLevel() * 2); //Double Odds
-
-                //Chance Stat Calculations
-                rollChance       = RandomChanceUtil.getRandomChanceExecutionChance(roll_rcs);
-                graceChance      = RandomChanceUtil.getRandomChanceExecutionChance(grace_rcs);
-                //damageThreshold  = mcMMO.p.getAdvancedConfig().getRollDamageThreshold();
-
-                String[] rollStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.ACROBATICS_ROLL);
-
-                //Format
-                double rollChanceLucky  = rollChance * 1.333D;
-                double graceChanceLucky = graceChance * 1.333D;
+                String[] rollStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.ACROBATICS_ROLL);
 
 
                 messages.add(getStatMessage(SubSkillType.ACROBATICS_ROLL, rollStrings[0])
                 messages.add(getStatMessage(SubSkillType.ACROBATICS_ROLL, rollStrings[0])
                         + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", rollStrings[1]) : ""));
                         + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", rollStrings[1]) : ""));

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

@@ -68,8 +68,8 @@ public class AlchemyCommand extends SkillCommand {
 
 
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
-        canCatalysis = canUseSubskill(player, SubSkillType.ALCHEMY_CATALYSIS);
-        canConcoctions = canUseSubskill(player, SubSkillType.ALCHEMY_CONCOCTIONS);
+        canCatalysis = Permissions.canUseSubSkill(player, SubSkillType.ALCHEMY_CATALYSIS);
+        canConcoctions = Permissions.canUseSubSkill(player, SubSkillType.ALCHEMY_CONCOCTIONS);
     }
     }
 
 
     @Override
     @Override

+ 8 - 7
src/main/java/com/gmail/nossr50/commands/skills/ArcheryCommand.java

@@ -4,8 +4,9 @@ import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.skills.archery.Archery;
 import com.gmail.nossr50.skills.archery.Archery;
+import com.gmail.nossr50.util.Permissions;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.CombatUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.Component;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
@@ -32,14 +33,14 @@ public class ArcheryCommand extends SkillCommand {
     protected void dataCalculations(Player player, float skillValue) {
     protected void dataCalculations(Player player, float skillValue) {
         // ARCHERY_ARROW_RETRIEVAL
         // ARCHERY_ARROW_RETRIEVAL
         if (canRetrieve) {
         if (canRetrieve) {
-            String[] retrieveStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.ARCHERY_ARROW_RETRIEVAL);
+            String[] retrieveStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.ARCHERY_ARROW_RETRIEVAL);
             retrieveChance = retrieveStrings[0];
             retrieveChance = retrieveStrings[0];
             retrieveChanceLucky = retrieveStrings[1];
             retrieveChanceLucky = retrieveStrings[1];
         }
         }
         
         
         // ARCHERY_DAZE
         // ARCHERY_DAZE
         if (canDaze) {
         if (canDaze) {
-            String[] dazeStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.ARCHERY_DAZE);
+            String[] dazeStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.ARCHERY_DAZE);
             dazeChance = dazeStrings[0];
             dazeChance = dazeStrings[0];
             dazeChanceLucky = dazeStrings[1];
             dazeChanceLucky = dazeStrings[1];
         }
         }
@@ -52,9 +53,9 @@ public class ArcheryCommand extends SkillCommand {
 
 
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
-        canSkillShot = canUseSubskill(player, SubSkillType.ARCHERY_SKILL_SHOT);
-        canDaze = canUseSubskill(player, SubSkillType.ARCHERY_DAZE);
-        canRetrieve = canUseSubskill(player, SubSkillType.ARCHERY_ARROW_RETRIEVAL);
+        canSkillShot = Permissions.canUseSubSkill(player, SubSkillType.ARCHERY_SKILL_SHOT);
+        canDaze = Permissions.canUseSubSkill(player, SubSkillType.ARCHERY_DAZE);
+        canRetrieve = Permissions.canUseSubSkill(player, SubSkillType.ARCHERY_ARROW_RETRIEVAL);
     }
     }
 
 
     @Override
     @Override
@@ -75,7 +76,7 @@ public class ArcheryCommand extends SkillCommand {
             messages.add(getStatMessage(SubSkillType.ARCHERY_SKILL_SHOT, skillShotBonus));
             messages.add(getStatMessage(SubSkillType.ARCHERY_SKILL_SHOT, skillShotBonus));
         }
         }
 
 
-        if(canUseSubskill(player, SubSkillType.ARCHERY_ARCHERY_LIMIT_BREAK)) {
+        if(Permissions.canUseSubSkill(player, SubSkillType.ARCHERY_ARCHERY_LIMIT_BREAK)) {
             messages.add(getStatMessage(SubSkillType.ARCHERY_ARCHERY_LIMIT_BREAK,
             messages.add(getStatMessage(SubSkillType.ARCHERY_ARCHERY_LIMIT_BREAK,
                 String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, SubSkillType.ARCHERY_ARCHERY_LIMIT_BREAK, 1000))));
                 String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, SubSkillType.ARCHERY_ARCHERY_LIMIT_BREAK, 1000))));
         }
         }

+ 7 - 7
src/main/java/com/gmail/nossr50/commands/skills/AxesCommand.java

@@ -6,9 +6,9 @@ import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.skills.axes.Axes;
 import com.gmail.nossr50.skills.axes.Axes;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.Component;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
@@ -48,7 +48,7 @@ public class AxesCommand extends SkillCommand {
         
         
         // CRITICAL HIT
         // CRITICAL HIT
         if (canCritical) {
         if (canCritical) {
-            String[] criticalHitStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.AXES_CRITICAL_STRIKES);
+            String[] criticalHitStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.AXES_CRITICAL_STRIKES);
             critChance = criticalHitStrings[0];
             critChance = criticalHitStrings[0];
             critChanceLucky = criticalHitStrings[1];
             critChanceLucky = criticalHitStrings[1];
         }
         }
@@ -64,10 +64,10 @@ public class AxesCommand extends SkillCommand {
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
         canSkullSplitter = Permissions.skullSplitter(player) && RankUtils.hasUnlockedSubskill(player, SubSkillType.AXES_SKULL_SPLITTER);
         canSkullSplitter = Permissions.skullSplitter(player) && RankUtils.hasUnlockedSubskill(player, SubSkillType.AXES_SKULL_SPLITTER);
-        canCritical = canUseSubskill(player, SubSkillType.AXES_CRITICAL_STRIKES);
-        canAxeMastery = canUseSubskill(player, SubSkillType.AXES_AXE_MASTERY);
-        canImpact = canUseSubskill(player, SubSkillType.AXES_ARMOR_IMPACT);
-        canGreaterImpact = canUseSubskill(player, SubSkillType.AXES_GREATER_IMPACT);
+        canCritical = Permissions.canUseSubSkill(player, SubSkillType.AXES_CRITICAL_STRIKES);
+        canAxeMastery = Permissions.canUseSubSkill(player, SubSkillType.AXES_AXE_MASTERY);
+        canImpact = Permissions.canUseSubSkill(player, SubSkillType.AXES_ARMOR_IMPACT);
+        canGreaterImpact = Permissions.canUseSubSkill(player, SubSkillType.AXES_GREATER_IMPACT);
     }
     }
 
 
     @Override
     @Override
@@ -96,7 +96,7 @@ public class AxesCommand extends SkillCommand {
                     + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", skullSplitterLengthEndurance) : ""));
                     + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", skullSplitterLengthEndurance) : ""));
         }
         }
 
 
-        if(canUseSubskill(player, SubSkillType.AXES_AXES_LIMIT_BREAK)) {
+        if(Permissions.canUseSubSkill(player, SubSkillType.AXES_AXES_LIMIT_BREAK)) {
             messages.add(getStatMessage(SubSkillType.AXES_AXES_LIMIT_BREAK,
             messages.add(getStatMessage(SubSkillType.AXES_AXES_LIMIT_BREAK,
                     String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, SubSkillType.AXES_AXES_LIMIT_BREAK, 1000))));
                     String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, SubSkillType.AXES_AXES_LIMIT_BREAK, 1000))));
         }
         }

+ 77 - 0
src/main/java/com/gmail/nossr50/commands/skills/CrossbowsCommand.java

@@ -0,0 +1,77 @@
+package com.gmail.nossr50.commands.skills;
+
+import com.gmail.nossr50.datatypes.player.McMMOPlayer;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+import com.gmail.nossr50.util.Permissions;
+import com.gmail.nossr50.util.player.UserManager;
+import com.gmail.nossr50.util.skills.CombatUtils;
+import com.gmail.nossr50.util.skills.RankUtils;
+import com.gmail.nossr50.util.text.TextComponentFactory;
+import net.kyori.adventure.text.Component;
+import org.bukkit.ChatColor;
+import org.bukkit.entity.Player;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.gmail.nossr50.datatypes.skills.SubSkillType.*;
+
+public class CrossbowsCommand extends SkillCommand {
+    private boolean canTrickShot;
+    private boolean canPoweredShot;
+
+    public CrossbowsCommand() {
+        super(PrimarySkillType.CROSSBOWS);
+    }
+
+    @Override
+    protected void dataCalculations(Player player, float skillValue) {
+        // TODO: Implement data calculations
+    }
+
+    @Override
+    protected void permissionsCheck(Player player) {
+        canTrickShot = RankUtils.hasUnlockedSubskill(player, CROSSBOWS_TRICK_SHOT)
+                && Permissions.trickShot(player);
+
+        canPoweredShot = RankUtils.hasUnlockedSubskill(player, CROSSBOWS_POWERED_SHOT)
+                && Permissions.poweredShot(player);
+    }
+
+    @Override
+    protected List<String> statsDisplay(Player player, float skillValue, boolean hasEndurance, boolean isLucky) {
+        List<String> messages = new ArrayList<>();
+
+        McMMOPlayer mmoPlayer = UserManager.getPlayer(player);
+        if (mmoPlayer == null) {
+            return messages;
+        }
+
+        if (canPoweredShot) {
+            messages.add(getStatMessage(ARCHERY_SKILL_SHOT, percent.format(mmoPlayer.getCrossbowsManager().getDamageBonusPercent(player))));
+        }
+
+        if (canTrickShot) {
+            messages.add(getStatMessage(CROSSBOWS_TRICK_SHOT,
+                    String.valueOf(mmoPlayer.getCrossbowsManager().getTrickShotMaxBounceCount())));
+        }
+
+        if(Permissions.canUseSubSkill(player, CROSSBOWS_CROSSBOWS_LIMIT_BREAK)) {
+            messages.add(getStatMessage(CROSSBOWS_CROSSBOWS_LIMIT_BREAK,
+                    String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, CROSSBOWS_CROSSBOWS_LIMIT_BREAK, 1000))));
+        }
+
+        messages.add(ChatColor.GRAY + "The Crossbows skill is a work in progress and is still being developed, feedback would be appreciated in the mcMMO discord server.");
+
+        return messages;
+    }
+
+    @Override
+    protected List<Component> getTextComponents(Player player) {
+        List<Component> textComponents = new ArrayList<>();
+
+        TextComponentFactory.getSubSkillTextComponents(player, textComponents, PrimarySkillType.CROSSBOWS);
+
+        return textComponents;
+    }
+}

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

@@ -38,7 +38,7 @@ public class ExcavationCommand extends SkillCommand {
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
         canGigaDrill = Permissions.gigaDrillBreaker(player) && RankUtils.hasUnlockedSubskill(player, SubSkillType.EXCAVATION_GIGA_DRILL_BREAKER);
         canGigaDrill = Permissions.gigaDrillBreaker(player) && RankUtils.hasUnlockedSubskill(player, SubSkillType.EXCAVATION_GIGA_DRILL_BREAKER);
-        canTreasureHunt = canUseSubskill(player, SubSkillType.EXCAVATION_ARCHAEOLOGY);
+        canTreasureHunt = Permissions.canUseSubSkill(player, SubSkillType.EXCAVATION_ARCHAEOLOGY);
     }
     }
 
 
     @Override
     @Override
@@ -54,7 +54,7 @@ public class ExcavationCommand extends SkillCommand {
             //messages.add(LocaleLoader.getString("Excavation.Effect.Length", gigaDrillBreakerLength) + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", gigaDrillBreakerLengthEndurance) : ""));
             //messages.add(LocaleLoader.getString("Excavation.Effect.Length", gigaDrillBreakerLength) + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", gigaDrillBreakerLengthEndurance) : ""));
         }
         }
 
 
-        if(canUseSubskill(player, SubSkillType.EXCAVATION_ARCHAEOLOGY)) {
+        if(Permissions.canUseSubSkill(player, SubSkillType.EXCAVATION_ARCHAEOLOGY)) {
             messages.add(getStatMessage(false, false, SubSkillType.EXCAVATION_ARCHAEOLOGY,
             messages.add(getStatMessage(false, false, SubSkillType.EXCAVATION_ARCHAEOLOGY,
                     percent.format(excavationManager.getArchaelogyExperienceOrbChance() / 100.0D)));
                     percent.format(excavationManager.getArchaelogyExperienceOrbChance() / 100.0D)));
             messages.add(getStatMessage(true, false, SubSkillType.EXCAVATION_ARCHAEOLOGY,
             messages.add(getStatMessage(true, false, SubSkillType.EXCAVATION_ARCHAEOLOGY,

+ 11 - 8
src/main/java/com/gmail/nossr50/commands/skills/FishingCommand.java

@@ -7,8 +7,10 @@ import com.gmail.nossr50.datatypes.treasure.Rarity;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.fishing.FishingManager;
 import com.gmail.nossr50.skills.fishing.FishingManager;
+import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.Probability;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
@@ -79,7 +81,8 @@ public class FishingCommand extends SkillCommand {
 
 
         // FISHING_SHAKE
         // FISHING_SHAKE
         if (canShake) {
         if (canShake) {
-            String[] shakeStrings = RandomChanceUtil.calculateAbilityDisplayValuesStatic(player, PrimarySkillType.FISHING, fishingManager.getShakeChance());
+            Probability shakeProbability = Probability.ofPercent(fishingManager.getShakeChance());
+            String[] shakeStrings = ProbabilityUtil.getRNGDisplayValues(shakeProbability);
             shakeChance = shakeStrings[0];
             shakeChance = shakeStrings[0];
             shakeChanceLucky = shakeStrings[1];
             shakeChanceLucky = shakeStrings[1];
         }
         }
@@ -98,12 +101,12 @@ public class FishingCommand extends SkillCommand {
 
 
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
-        canTreasureHunt = canUseSubskill(player, SubSkillType.FISHING_TREASURE_HUNTER);
-        canMagicHunt = canUseSubskill(player, SubSkillType.FISHING_MAGIC_HUNTER) && canUseSubskill(player, SubSkillType.FISHING_TREASURE_HUNTER);
-        canShake = canUseSubskill(player, SubSkillType.FISHING_SHAKE);
-        canFishermansDiet = canUseSubskill(player, SubSkillType.FISHING_FISHERMANS_DIET);
-        canMasterAngler = mcMMO.getCompatibilityManager().getMasterAnglerCompatibilityLayer() != null && canUseSubskill(player, SubSkillType.FISHING_MASTER_ANGLER);
-        canIceFish = canUseSubskill(player, SubSkillType.FISHING_ICE_FISHING);
+        canTreasureHunt = Permissions.canUseSubSkill(player, SubSkillType.FISHING_TREASURE_HUNTER);
+        canMagicHunt = Permissions.canUseSubSkill(player, SubSkillType.FISHING_MAGIC_HUNTER) && Permissions.canUseSubSkill(player, SubSkillType.FISHING_TREASURE_HUNTER);
+        canShake = Permissions.canUseSubSkill(player, SubSkillType.FISHING_SHAKE);
+        canFishermansDiet = Permissions.canUseSubSkill(player, SubSkillType.FISHING_FISHERMANS_DIET);
+        canMasterAngler = mcMMO.getCompatibilityManager().getMasterAnglerCompatibilityLayer() != null && Permissions.canUseSubSkill(player, SubSkillType.FISHING_MASTER_ANGLER);
+        canIceFish = Permissions.canUseSubSkill(player, SubSkillType.FISHING_ICE_FISHING);
     }
     }
 
 
     @Override
     @Override

+ 26 - 11
src/main/java/com/gmail/nossr50/commands/skills/HerbalismCommand.java

@@ -5,8 +5,8 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.Component;
 import org.bukkit.Material;
 import org.bukkit.Material;
@@ -24,6 +24,8 @@ public class HerbalismCommand extends SkillCommand {
     private int farmersDietRank;
     private int farmersDietRank;
     private String doubleDropChance;
     private String doubleDropChance;
     private String doubleDropChanceLucky;
     private String doubleDropChanceLucky;
+    private String tripleDropChance;
+    private String tripleDropChanceLucky;
     private String hylianLuckChance;
     private String hylianLuckChance;
     private String hylianLuckChanceLucky;
     private String hylianLuckChanceLucky;
     private String shroomThumbChance;
     private String shroomThumbChance;
@@ -35,6 +37,7 @@ public class HerbalismCommand extends SkillCommand {
     private boolean canGreenThumbBlocks;
     private boolean canGreenThumbBlocks;
     private boolean canFarmersDiet;
     private boolean canFarmersDiet;
     private boolean canDoubleDrop;
     private boolean canDoubleDrop;
+    private boolean canTripleDrop;
     private boolean canShroomThumb;
     private boolean canShroomThumb;
 
 
     public HerbalismCommand() {
     public HerbalismCommand() {
@@ -46,10 +49,16 @@ public class HerbalismCommand extends SkillCommand {
         
         
         // DOUBLE DROPS
         // DOUBLE DROPS
         if (canDoubleDrop) {
         if (canDoubleDrop) {
-            String[] doubleDropStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.HERBALISM_DOUBLE_DROPS);
+            String[] doubleDropStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.HERBALISM_DOUBLE_DROPS);
             doubleDropChance = doubleDropStrings[0];
             doubleDropChance = doubleDropStrings[0];
             doubleDropChanceLucky = doubleDropStrings[1];
             doubleDropChanceLucky = doubleDropStrings[1];
         }
         }
+
+        if (canTripleDrop) {
+            String[] tripleDropStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.HERBALISM_VERDANT_BOUNTY);
+            tripleDropChance = tripleDropStrings[0];
+            tripleDropChanceLucky = tripleDropStrings[1];
+        }
         
         
         // FARMERS DIET
         // FARMERS DIET
         if (canFarmersDiet) {
         if (canFarmersDiet) {
@@ -67,21 +76,21 @@ public class HerbalismCommand extends SkillCommand {
         if (canGreenThumbBlocks || canGreenThumbPlants) {
         if (canGreenThumbBlocks || canGreenThumbPlants) {
             greenThumbStage = RankUtils.getRank(player, SubSkillType.HERBALISM_GREEN_THUMB);
             greenThumbStage = RankUtils.getRank(player, SubSkillType.HERBALISM_GREEN_THUMB);
 
 
-            String[] greenThumbStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.HERBALISM_GREEN_THUMB);
+            String[] greenThumbStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.HERBALISM_GREEN_THUMB);
             greenThumbChance = greenThumbStrings[0];
             greenThumbChance = greenThumbStrings[0];
             greenThumbChanceLucky = greenThumbStrings[1];
             greenThumbChanceLucky = greenThumbStrings[1];
         }
         }
 
 
         // HYLIAN LUCK
         // HYLIAN LUCK
         if (hasHylianLuck) {
         if (hasHylianLuck) {
-            String[] hylianLuckStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.HERBALISM_HYLIAN_LUCK);
+            String[] hylianLuckStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.HERBALISM_HYLIAN_LUCK);
             hylianLuckChance = hylianLuckStrings[0];
             hylianLuckChance = hylianLuckStrings[0];
             hylianLuckChanceLucky = hylianLuckStrings[1];
             hylianLuckChanceLucky = hylianLuckStrings[1];
         }
         }
 
 
         // SHROOM THUMB
         // SHROOM THUMB
         if (canShroomThumb) {
         if (canShroomThumb) {
-            String[] shroomThumbStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.HERBALISM_SHROOM_THUMB);
+            String[] shroomThumbStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.HERBALISM_SHROOM_THUMB);
             shroomThumbChance = shroomThumbStrings[0];
             shroomThumbChance = shroomThumbStrings[0];
             shroomThumbChanceLucky = shroomThumbStrings[1];
             shroomThumbChanceLucky = shroomThumbStrings[1];
         }
         }
@@ -89,13 +98,14 @@ public class HerbalismCommand extends SkillCommand {
 
 
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
-        hasHylianLuck = canUseSubskill(player, SubSkillType.HERBALISM_HYLIAN_LUCK);
+        hasHylianLuck = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_HYLIAN_LUCK);
         canGreenTerra = Permissions.greenTerra(player);
         canGreenTerra = Permissions.greenTerra(player);
         canGreenThumbPlants = RankUtils.hasUnlockedSubskill(player, SubSkillType.HERBALISM_GREEN_THUMB) && (Permissions.greenThumbPlant(player, Material.WHEAT) || Permissions.greenThumbPlant(player, Material.CARROT) || Permissions.greenThumbPlant(player, Material.POTATO) || Permissions.greenThumbPlant(player, Material.BEETROOTS) || Permissions.greenThumbPlant(player, Material.NETHER_WART) || Permissions.greenThumbPlant(player, Material.COCOA));
         canGreenThumbPlants = RankUtils.hasUnlockedSubskill(player, SubSkillType.HERBALISM_GREEN_THUMB) && (Permissions.greenThumbPlant(player, Material.WHEAT) || Permissions.greenThumbPlant(player, Material.CARROT) || Permissions.greenThumbPlant(player, Material.POTATO) || Permissions.greenThumbPlant(player, Material.BEETROOTS) || Permissions.greenThumbPlant(player, Material.NETHER_WART) || Permissions.greenThumbPlant(player, Material.COCOA));
         canGreenThumbBlocks = RankUtils.hasUnlockedSubskill(player, SubSkillType.HERBALISM_GREEN_THUMB) && (Permissions.greenThumbBlock(player, Material.DIRT) || Permissions.greenThumbBlock(player, Material.COBBLESTONE) || Permissions.greenThumbBlock(player, Material.COBBLESTONE_WALL) || Permissions.greenThumbBlock(player, Material.STONE_BRICKS));
         canGreenThumbBlocks = RankUtils.hasUnlockedSubskill(player, SubSkillType.HERBALISM_GREEN_THUMB) && (Permissions.greenThumbBlock(player, Material.DIRT) || Permissions.greenThumbBlock(player, Material.COBBLESTONE) || Permissions.greenThumbBlock(player, Material.COBBLESTONE_WALL) || Permissions.greenThumbBlock(player, Material.STONE_BRICKS));
-        canFarmersDiet = canUseSubskill(player, SubSkillType.HERBALISM_FARMERS_DIET);
-        canDoubleDrop = canUseSubskill(player, SubSkillType.HERBALISM_DOUBLE_DROPS) && !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill);
-        canShroomThumb = canUseSubskill(player, SubSkillType.HERBALISM_SHROOM_THUMB);
+        canFarmersDiet = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_FARMERS_DIET);
+        canDoubleDrop = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_DOUBLE_DROPS) && !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill);
+        canTripleDrop = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_VERDANT_BOUNTY) && !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill);
+        canShroomThumb = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_SHROOM_THUMB);
     }
     }
 
 
     @Override
     @Override
@@ -106,11 +116,16 @@ public class HerbalismCommand extends SkillCommand {
             messages.add(getStatMessage(SubSkillType.HERBALISM_DOUBLE_DROPS, doubleDropChance)
             messages.add(getStatMessage(SubSkillType.HERBALISM_DOUBLE_DROPS, doubleDropChance)
                     + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", doubleDropChanceLucky) : ""));
                     + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", doubleDropChanceLucky) : ""));
         }
         }
-        
+
+        if (canTripleDrop) {
+            messages.add(getStatMessage(SubSkillType.HERBALISM_VERDANT_BOUNTY, tripleDropChance)
+                    + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", tripleDropChanceLucky) : ""));
+        }
+
         if (canFarmersDiet) {
         if (canFarmersDiet) {
             messages.add(getStatMessage(false, true, SubSkillType.HERBALISM_FARMERS_DIET, String.valueOf(farmersDietRank)));
             messages.add(getStatMessage(false, true, SubSkillType.HERBALISM_FARMERS_DIET, String.valueOf(farmersDietRank)));
         }
         }
-        
+
         if (canGreenTerra) {
         if (canGreenTerra) {
             messages.add(getStatMessage(SubSkillType.HERBALISM_GREEN_TERRA, greenTerraLength)
             messages.add(getStatMessage(SubSkillType.HERBALISM_GREEN_TERRA, greenTerraLength)
                     + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", greenTerraLengthEndurance) : ""));
                     + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", greenTerraLengthEndurance) : ""));

+ 20 - 4
src/main/java/com/gmail/nossr50/commands/skills/MiningCommand.java

@@ -6,8 +6,8 @@ import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.skills.mining.MiningManager;
 import com.gmail.nossr50.skills.mining.MiningManager;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.Component;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
@@ -18,6 +18,8 @@ import java.util.List;
 public class MiningCommand extends SkillCommand {
 public class MiningCommand extends SkillCommand {
     private String doubleDropChance;
     private String doubleDropChance;
     private String doubleDropChanceLucky;
     private String doubleDropChanceLucky;
+    private String tripleDropChance;
+    private String tripleDropChanceLucky;
     private String superBreakerLength;
     private String superBreakerLength;
     private String superBreakerLengthEndurance;
     private String superBreakerLengthEndurance;
 
 
@@ -30,6 +32,7 @@ public class MiningCommand extends SkillCommand {
 
 
     private boolean canSuperBreaker;
     private boolean canSuperBreaker;
     private boolean canDoubleDrop;
     private boolean canDoubleDrop;
+    private boolean canTripleDrop;
     private boolean canBlast;
     private boolean canBlast;
     private boolean canBiggerBombs;
     private boolean canBiggerBombs;
     private boolean canDemoExpert;
     private boolean canDemoExpert;
@@ -51,10 +54,17 @@ public class MiningCommand extends SkillCommand {
             blastDamageDecrease = percent.format(miningManager.getBlastDamageModifier() / 100.0D);
             blastDamageDecrease = percent.format(miningManager.getBlastDamageModifier() / 100.0D);
             blastRadiusIncrease = miningManager.getBlastRadiusModifier();
             blastRadiusIncrease = miningManager.getBlastRadiusModifier();
         }
         }
+
+        // Mastery TRIPLE DROPS
+        if (canTripleDrop) {
+            String[] masteryTripleDropStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.MINING_MOTHER_LODE);
+            tripleDropChance = masteryTripleDropStrings[0];
+            tripleDropChanceLucky = masteryTripleDropStrings[1];
+        }
         
         
         // DOUBLE DROPS
         // DOUBLE DROPS
         if (canDoubleDrop) {
         if (canDoubleDrop) {
-            String[] doubleDropStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.MINING_DOUBLE_DROPS);
+            String[] doubleDropStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.MINING_DOUBLE_DROPS);
             doubleDropChance = doubleDropStrings[0];
             doubleDropChance = doubleDropStrings[0];
             doubleDropChanceLucky = doubleDropStrings[1];
             doubleDropChanceLucky = doubleDropStrings[1];
         }
         }
@@ -72,7 +82,8 @@ public class MiningCommand extends SkillCommand {
         canBiggerBombs = RankUtils.hasUnlockedSubskill(player, SubSkillType.MINING_BIGGER_BOMBS) && Permissions.biggerBombs(player);
         canBiggerBombs = RankUtils.hasUnlockedSubskill(player, SubSkillType.MINING_BIGGER_BOMBS) && Permissions.biggerBombs(player);
         canBlast = RankUtils.hasUnlockedSubskill(player, SubSkillType.MINING_BLAST_MINING) && Permissions.remoteDetonation(player);
         canBlast = RankUtils.hasUnlockedSubskill(player, SubSkillType.MINING_BLAST_MINING) && Permissions.remoteDetonation(player);
         canDemoExpert = RankUtils.hasUnlockedSubskill(player, SubSkillType.MINING_DEMOLITIONS_EXPERTISE) && Permissions.demolitionsExpertise(player);
         canDemoExpert = RankUtils.hasUnlockedSubskill(player, SubSkillType.MINING_DEMOLITIONS_EXPERTISE) && Permissions.demolitionsExpertise(player);
-        canDoubleDrop = canUseSubskill(player, SubSkillType.MINING_DOUBLE_DROPS);
+        canDoubleDrop = Permissions.canUseSubSkill(player, SubSkillType.MINING_DOUBLE_DROPS);
+        canTripleDrop = Permissions.canUseSubSkill(player, SubSkillType.MINING_MOTHER_LODE);
         canSuperBreaker = RankUtils.hasUnlockedSubskill(player, SubSkillType.MINING_SUPER_BREAKER) && Permissions.superBreaker(player);
         canSuperBreaker = RankUtils.hasUnlockedSubskill(player, SubSkillType.MINING_SUPER_BREAKER) && Permissions.superBreaker(player);
     }
     }
 
 
@@ -94,13 +105,18 @@ public class MiningCommand extends SkillCommand {
             messages.add(getStatMessage(SubSkillType.MINING_DEMOLITIONS_EXPERTISE, blastDamageDecrease));
             messages.add(getStatMessage(SubSkillType.MINING_DEMOLITIONS_EXPERTISE, blastDamageDecrease));
             //messages.add(LocaleLoader.getString("Mining.Effect.Decrease", blastDamageDecrease));
             //messages.add(LocaleLoader.getString("Mining.Effect.Decrease", blastDamageDecrease));
         }
         }
-        
+
         if (canDoubleDrop) {
         if (canDoubleDrop) {
             messages.add(getStatMessage(SubSkillType.MINING_DOUBLE_DROPS, doubleDropChance)
             messages.add(getStatMessage(SubSkillType.MINING_DOUBLE_DROPS, doubleDropChance)
                     + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", doubleDropChanceLucky) : ""));
                     + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", doubleDropChanceLucky) : ""));
             //messages.add(LocaleLoader.getString("Mining.Effect.DropChance", doubleDropChance) + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", doubleDropChanceLucky) : ""));
             //messages.add(LocaleLoader.getString("Mining.Effect.DropChance", doubleDropChance) + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", doubleDropChanceLucky) : ""));
         }
         }
 
 
+        if(canTripleDrop) {
+            messages.add(getStatMessage(SubSkillType.MINING_MOTHER_LODE, tripleDropChance)
+                    + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", tripleDropChanceLucky) : ""));
+        }
+
         if (canSuperBreaker) {
         if (canSuperBreaker) {
             messages.add(getStatMessage(SubSkillType.MINING_SUPER_BREAKER, superBreakerLength)
             messages.add(getStatMessage(SubSkillType.MINING_SUPER_BREAKER, superBreakerLength)
                     + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", superBreakerLengthEndurance) : ""));
                     + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", superBreakerLengthEndurance) : ""));

+ 40 - 0
src/main/java/com/gmail/nossr50/commands/skills/PowerLevelCommand.java

@@ -0,0 +1,40 @@
+package com.gmail.nossr50.commands.skills;
+
+import co.aikar.commands.BaseCommand;
+import co.aikar.commands.BukkitCommandIssuer;
+import co.aikar.commands.annotation.CommandAlias;
+import co.aikar.commands.annotation.CommandPermission;
+import co.aikar.commands.annotation.Conditions;
+import co.aikar.commands.annotation.Default;
+import com.gmail.nossr50.commands.CommandManager;
+import com.gmail.nossr50.datatypes.player.McMMOPlayer;
+import com.gmail.nossr50.mcMMO;
+import com.gmail.nossr50.util.player.UserManager;
+import org.bukkit.ChatColor;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+@CommandPermission("mcmmo.commands.mmopower")
+@CommandAlias("mmopower|mmopowerlevel|powerlevel")
+public class PowerLevelCommand extends BaseCommand {
+    private final @NotNull mcMMO pluginRef;
+
+    public PowerLevelCommand(@NotNull mcMMO pluginRef) {
+        this.pluginRef = pluginRef;
+    }
+
+    @Default
+    @Conditions(CommandManager.POWER_LEVEL_CONDITION)
+    public void processCommand(String[] args) {
+        BukkitCommandIssuer bukkitCommandIssuer = (BukkitCommandIssuer) getCurrentCommandIssuer();
+        Player player = bukkitCommandIssuer.getPlayer();
+        McMMOPlayer mmoPlayer = UserManager.getPlayer(player); //Should never be null at this point because its caught in an ACF validation
+        if (mmoPlayer == null) {
+            return;
+        }
+
+        int powerLevel = mmoPlayer.getPowerLevel();
+
+        mmoPlayer.getPlayer().sendMessage(ChatColor.DARK_AQUA + "Your " + ChatColor.GOLD + "[mcMMO]" + ChatColor.DARK_AQUA + " power level is: " + ChatColor.GREEN + powerLevel);
+    }
+}

+ 5 - 5
src/main/java/com/gmail/nossr50/commands/skills/RepairCommand.java

@@ -11,8 +11,8 @@ import com.gmail.nossr50.skills.repair.RepairManager;
 import com.gmail.nossr50.skills.repair.repairables.Repairable;
 import com.gmail.nossr50.skills.repair.repairables.Repairable;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.Component;
 import org.bukkit.Material;
 import org.bukkit.Material;
@@ -68,7 +68,7 @@ public class RepairCommand extends SkillCommand {
 
 
         // SUPER REPAIR
         // SUPER REPAIR
         if (canSuperRepair) {
         if (canSuperRepair) {
-            String[] superRepairStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.REPAIR_SUPER_REPAIR);
+            String[] superRepairStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.REPAIR_SUPER_REPAIR);
             superRepairChance = superRepairStrings[0];
             superRepairChance = superRepairStrings[0];
             superRepairChanceLucky = superRepairStrings[1];
             superRepairChanceLucky = superRepairStrings[1];
         }
         }
@@ -76,9 +76,9 @@ public class RepairCommand extends SkillCommand {
 
 
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
-        canSuperRepair = canUseSubskill(player, SubSkillType.REPAIR_SUPER_REPAIR);
-        canMasterRepair = canUseSubskill(player, SubSkillType.REPAIR_REPAIR_MASTERY);
-        canArcaneForge = canUseSubskill(player, SubSkillType.REPAIR_ARCANE_FORGING);
+        canSuperRepair = Permissions.canUseSubSkill(player, SubSkillType.REPAIR_SUPER_REPAIR);
+        canMasterRepair = Permissions.canUseSubSkill(player, SubSkillType.REPAIR_REPAIR_MASTERY);
+        canArcaneForge = Permissions.canUseSubSkill(player, SubSkillType.REPAIR_ARCANE_FORGING);
         canRepairDiamond = Permissions.repairMaterialType(player, MaterialType.DIAMOND);
         canRepairDiamond = Permissions.repairMaterialType(player, MaterialType.DIAMOND);
         canRepairGold = Permissions.repairMaterialType(player, MaterialType.GOLD);
         canRepairGold = Permissions.repairMaterialType(player, MaterialType.GOLD);
         canRepairIron = Permissions.repairMaterialType(player, MaterialType.IRON);
         canRepairIron = Permissions.repairMaterialType(player, MaterialType.IRON);

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

@@ -5,6 +5,7 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.skills.salvage.Salvage;
 import com.gmail.nossr50.skills.salvage.Salvage;
 import com.gmail.nossr50.skills.salvage.SalvageManager;
 import com.gmail.nossr50.skills.salvage.SalvageManager;
+import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
@@ -30,8 +31,8 @@ public class SalvageCommand extends SkillCommand {
 
 
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
-        canScrapCollector = canUseSubskill(player, SubSkillType.SALVAGE_SCRAP_COLLECTOR);
-        canArcaneSalvage = canUseSubskill(player, SubSkillType.SALVAGE_ARCANE_SALVAGE);
+        canScrapCollector = Permissions.canUseSubSkill(player, SubSkillType.SALVAGE_SCRAP_COLLECTOR);
+        canArcaneSalvage = Permissions.canUseSubSkill(player, SubSkillType.SALVAGE_ARCANE_SALVAGE);
     }
     }
 
 
     @Override
     @Override

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

@@ -5,16 +5,12 @@ import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.skills.child.FamilyTree;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
 import com.gmail.nossr50.util.scoreboards.ScoreboardManager;
 import com.gmail.nossr50.util.scoreboards.ScoreboardManager;
 import com.gmail.nossr50.util.skills.PerksUtils;
 import com.gmail.nossr50.util.skills.PerksUtils;
-import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.skills.SkillTools;
 import com.gmail.nossr50.util.skills.SkillTools;
 import com.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
@@ -32,7 +28,6 @@ import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
-import java.util.Set;
 
 
 public abstract class SkillCommand implements TabExecutor {
 public abstract class SkillCommand implements TabExecutor {
     protected PrimarySkillType skill;
     protected PrimarySkillType skill;
@@ -173,7 +168,7 @@ public abstract class SkillCommand implements TabExecutor {
              */
              */
 
 
 
 
-            Set<PrimarySkillType> parents = FamilyTree.getParents(skill);
+            var parents = mcMMO.p.getSkillTools().getChildSkillParents(skill);
 
 
             //TODO: Add JSON here
             //TODO: Add JSON here
             /*player.sendMessage(parent.getName() + " - " + LocaleLoader.getString("Effects.Level.Overhaul", mcMMOPlayer.getSkillLevel(parent), mcMMOPlayer.getSkillXpLevel(parent), mcMMOPlayer.getXpToLevel(parent)))*/
             /*player.sendMessage(parent.getName() + " - " + LocaleLoader.getString("Effects.Level.Overhaul", mcMMOPlayer.getSkillLevel(parent), mcMMOPlayer.getSkillXpLevel(parent), mcMMOPlayer.getXpToLevel(parent)))*/
@@ -232,9 +227,9 @@ public abstract class SkillCommand implements TabExecutor {
         return Math.min((int) skillValue, maxLevel) / rankChangeLevel;
         return Math.min((int) skillValue, maxLevel) / rankChangeLevel;
     }
     }
 
 
-    protected String[] getAbilityDisplayValues(SkillActivationType skillActivationType, Player player, SubSkillType subSkill) {
-        return RandomChanceUtil.calculateAbilityDisplayValues(skillActivationType, player, subSkill);
-    }
+//    protected String[] getAbilityDisplayValues(SkillActivationType skillActivationType, Player player, SubSkillType subSkill) {
+//        return RandomChanceUtil.calculateAbilityDisplayValues(skillActivationType, player, subSkill);
+//    }
 
 
     protected String[] calculateLengthDisplayValues(Player player, float skillValue) {
     protected String[] calculateLengthDisplayValues(Player player, float skillValue) {
         int maxLength = mcMMO.p.getSkillTools().getSuperAbilityMaxLength(mcMMO.p.getSkillTools().getSuperAbility(skill));
         int maxLength = mcMMO.p.getSkillTools().getSuperAbilityMaxLength(mcMMO.p.getSkillTools().getSuperAbility(skill));
@@ -297,14 +292,4 @@ public abstract class SkillCommand implements TabExecutor {
 
 
     protected abstract List<Component> getTextComponents(Player player);
     protected abstract List<Component> getTextComponents(Player player);
 
 
-    /**
-     * Checks if a player can use a skill
-     * @param player target player
-     * @param subSkillType target subskill
-     * @return true if the player has permission and has the skill unlocked
-     */
-    protected boolean canUseSubskill(Player player, SubSkillType subSkillType)
-    {
-        return Permissions.isSubSkillEnabled(player, subSkillType) && RankUtils.hasUnlockedSubskill(player, subSkillType);
-    }
 }
 }

+ 5 - 5
src/main/java/com/gmail/nossr50/commands/skills/SmeltingCommand.java

@@ -5,8 +5,8 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.Component;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
@@ -39,14 +39,14 @@ public class SmeltingCommand extends SkillCommand {
 
 
         // FLUX MINING
         // FLUX MINING
         /*if (canFluxMine) {
         /*if (canFluxMine) {
-            String[] fluxMiningStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.SMELTING_FLUX_MINING);
+            String[] fluxMiningStrings = getRNGDisplayValues(player, SubSkillType.SMELTING_FLUX_MINING);
             str_fluxMiningChance = fluxMiningStrings[0];
             str_fluxMiningChance = fluxMiningStrings[0];
             str_fluxMiningChanceLucky = fluxMiningStrings[1];
             str_fluxMiningChanceLucky = fluxMiningStrings[1];
         }*/
         }*/
         
         
         // SECOND SMELT
         // SECOND SMELT
         if (canSecondSmelt) {
         if (canSecondSmelt) {
-            String[] secondSmeltStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.SMELTING_SECOND_SMELT);
+            String[] secondSmeltStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.SMELTING_SECOND_SMELT);
             str_secondSmeltChance = secondSmeltStrings[0];
             str_secondSmeltChance = secondSmeltStrings[0];
             str_secondSmeltChanceLucky = secondSmeltStrings[1];
             str_secondSmeltChanceLucky = secondSmeltStrings[1];
         }
         }
@@ -54,8 +54,8 @@ public class SmeltingCommand extends SkillCommand {
 
 
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
-        canFuelEfficiency = canUseSubskill(player, SubSkillType.SMELTING_FUEL_EFFICIENCY);
-        canSecondSmelt = canUseSubskill(player, SubSkillType.SMELTING_SECOND_SMELT);
+        canFuelEfficiency = Permissions.canUseSubSkill(player, SubSkillType.SMELTING_FUEL_EFFICIENCY);
+        canSecondSmelt = Permissions.canUseSubSkill(player, SubSkillType.SMELTING_SECOND_SMELT);
         //canFluxMine = canUseSubskill(player, SubSkillType.SMELTING_FLUX_MINING);
         //canFluxMine = canUseSubskill(player, SubSkillType.SMELTING_FLUX_MINING);
         canUnderstandTheArt = Permissions.vanillaXpBoost(player, skill) && RankUtils.hasUnlockedSubskill(player, SubSkillType.SMELTING_UNDERSTANDING_THE_ART);
         canUnderstandTheArt = Permissions.vanillaXpBoost(player, skill) && RankUtils.hasUnlockedSubskill(player, SubSkillType.SMELTING_UNDERSTANDING_THE_ART);
     }
     }

+ 7 - 6
src/main/java/com/gmail/nossr50/commands/skills/SwordsCommand.java

@@ -6,9 +6,10 @@ import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
+import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.Component;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
@@ -38,7 +39,7 @@ public class SwordsCommand extends SkillCommand {
     protected void dataCalculations(Player player, float skillValue) {
     protected void dataCalculations(Player player, float skillValue) {
         // SWORDS_COUNTER_ATTACK
         // SWORDS_COUNTER_ATTACK
         if (canCounter) {
         if (canCounter) {
-            String[] counterStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.SWORDS_COUNTER_ATTACK);
+            String[] counterStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.SWORDS_COUNTER_ATTACK);
             counterChance = counterStrings[0];
             counterChance = counterStrings[0];
             counterChanceLucky = counterStrings[1];
             counterChanceLucky = counterStrings[1];
         }
         }
@@ -69,8 +70,8 @@ public class SwordsCommand extends SkillCommand {
 
 
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
-        canRupture = canUseSubskill(player, SubSkillType.SWORDS_RUPTURE);
-        canCounter = canUseSubskill(player, SubSkillType.SWORDS_COUNTER_ATTACK);
+        canRupture = SkillUtils.canUseSubskill(player, SubSkillType.SWORDS_RUPTURE);
+        canCounter = SkillUtils.canUseSubskill(player, SubSkillType.SWORDS_COUNTER_ATTACK);
         canSerratedStrike = RankUtils.hasUnlockedSubskill(player, SubSkillType.SWORDS_SERRATED_STRIKES) && Permissions.serratedStrikes(player);
         canSerratedStrike = RankUtils.hasUnlockedSubskill(player, SubSkillType.SWORDS_SERRATED_STRIKES) && Permissions.serratedStrikes(player);
     }
     }
 
 
@@ -101,13 +102,13 @@ public class SwordsCommand extends SkillCommand {
                     + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", serratedStrikesLengthEndurance) : ""));
                     + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", serratedStrikesLengthEndurance) : ""));
         }
         }
 
 
-        if(canUseSubskill(player, SubSkillType.SWORDS_STAB))
+        if(SkillUtils.canUseSubskill(player, SubSkillType.SWORDS_STAB))
         {
         {
             messages.add(getStatMessage(SubSkillType.SWORDS_STAB,
             messages.add(getStatMessage(SubSkillType.SWORDS_STAB,
                     String.valueOf(UserManager.getPlayer(player).getSwordsManager().getStabDamage())));
                     String.valueOf(UserManager.getPlayer(player).getSwordsManager().getStabDamage())));
         }
         }
 
 
-        if(canUseSubskill(player, SubSkillType.SWORDS_SWORDS_LIMIT_BREAK)) {
+        if(SkillUtils.canUseSubskill(player, SubSkillType.SWORDS_SWORDS_LIMIT_BREAK)) {
             messages.add(getStatMessage(SubSkillType.SWORDS_SWORDS_LIMIT_BREAK,
             messages.add(getStatMessage(SubSkillType.SWORDS_SWORDS_LIMIT_BREAK,
                     String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, SubSkillType.SWORDS_SWORDS_LIMIT_BREAK, 1000))));
                     String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, SubSkillType.SWORDS_SWORDS_LIMIT_BREAK, 1000))));
         }
         }

+ 10 - 10
src/main/java/com/gmail/nossr50/commands/skills/TamingCommand.java

@@ -5,7 +5,7 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.skills.taming.Taming;
 import com.gmail.nossr50.skills.taming.Taming;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
-import com.gmail.nossr50.util.skills.SkillActivationType;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.Component;
 import org.bukkit.entity.EntityType;
 import org.bukkit.entity.EntityType;
@@ -35,7 +35,7 @@ public class TamingCommand extends SkillCommand {
     @Override
     @Override
     protected void dataCalculations(Player player, float skillValue) {
     protected void dataCalculations(Player player, float skillValue) {
         if (canGore) {
         if (canGore) {
-            String[] goreStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.TAMING_GORE);
+            String[] goreStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.TAMING_GORE);
             goreChance = goreStrings[0];
             goreChance = goreStrings[0];
             goreChanceLucky = goreStrings[1];
             goreChanceLucky = goreStrings[1];
         }
         }
@@ -43,15 +43,15 @@ public class TamingCommand extends SkillCommand {
 
 
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
-        canBeastLore = canUseSubskill(player, SubSkillType.TAMING_BEAST_LORE);
+        canBeastLore = Permissions.canUseSubSkill(player, SubSkillType.TAMING_BEAST_LORE);
         canCallWild = Permissions.callOfTheWild(player, EntityType.HORSE) || Permissions.callOfTheWild(player, EntityType.WOLF) || Permissions.callOfTheWild(player, EntityType.OCELOT);
         canCallWild = Permissions.callOfTheWild(player, EntityType.HORSE) || Permissions.callOfTheWild(player, EntityType.WOLF) || Permissions.callOfTheWild(player, EntityType.OCELOT);
-        canEnvironmentallyAware = canUseSubskill(player, SubSkillType.TAMING_ENVIRONMENTALLY_AWARE);
-        canFastFood = canUseSubskill(player, SubSkillType.TAMING_FAST_FOOD_SERVICE);
-        canGore = canUseSubskill(player, SubSkillType.TAMING_GORE);
-        canSharpenedClaws = canUseSubskill(player, SubSkillType.TAMING_SHARPENED_CLAWS);
-        canShockProof = canUseSubskill(player, SubSkillType.TAMING_SHOCK_PROOF);
-        canThickFur = canUseSubskill(player, SubSkillType.TAMING_THICK_FUR);
-        canHolyHound = canUseSubskill(player, SubSkillType.TAMING_HOLY_HOUND);
+        canEnvironmentallyAware = Permissions.canUseSubSkill(player, SubSkillType.TAMING_ENVIRONMENTALLY_AWARE);
+        canFastFood = Permissions.canUseSubSkill(player, SubSkillType.TAMING_FAST_FOOD_SERVICE);
+        canGore = Permissions.canUseSubSkill(player, SubSkillType.TAMING_GORE);
+        canSharpenedClaws = Permissions.canUseSubSkill(player, SubSkillType.TAMING_SHARPENED_CLAWS);
+        canShockProof = Permissions.canUseSubSkill(player, SubSkillType.TAMING_SHOCK_PROOF);
+        canThickFur = Permissions.canUseSubSkill(player, SubSkillType.TAMING_THICK_FUR);
+        canHolyHound = Permissions.canUseSubSkill(player, SubSkillType.TAMING_HOLY_HOUND);
     }
     }
 
 
     @Override
     @Override

+ 62 - 0
src/main/java/com/gmail/nossr50/commands/skills/TridentsCommand.java

@@ -0,0 +1,62 @@
+package com.gmail.nossr50.commands.skills;
+
+import com.gmail.nossr50.datatypes.player.McMMOPlayer;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+import com.gmail.nossr50.util.player.UserManager;
+import com.gmail.nossr50.util.skills.CombatUtils;
+import com.gmail.nossr50.util.skills.SkillUtils;
+import com.gmail.nossr50.util.text.TextComponentFactory;
+import net.kyori.adventure.text.Component;
+import org.bukkit.ChatColor;
+import org.bukkit.entity.Player;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.gmail.nossr50.datatypes.skills.SubSkillType.TRIDENTS_IMPALE;
+import static com.gmail.nossr50.datatypes.skills.SubSkillType.TRIDENTS_TRIDENTS_LIMIT_BREAK;
+
+public class TridentsCommand extends SkillCommand {
+
+
+    public TridentsCommand() {
+        super(PrimarySkillType.TRIDENTS);
+    }
+
+    @Override
+    protected void dataCalculations(Player player, float skillValue) {}
+
+    @Override
+    protected void permissionsCheck(Player player) {}
+
+    @Override
+    protected List<String> statsDisplay(Player player, float skillValue, boolean hasEndurance, boolean isLucky) {
+        List<String> messages = new ArrayList<>();
+        McMMOPlayer mmoPlayer = UserManager.getPlayer(player);
+        if (mmoPlayer == null) {
+            return messages;
+        }
+
+        if(SkillUtils.canUseSubskill(player, TRIDENTS_TRIDENTS_LIMIT_BREAK)) {
+            messages.add(getStatMessage(TRIDENTS_TRIDENTS_LIMIT_BREAK,
+                    String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, TRIDENTS_TRIDENTS_LIMIT_BREAK, 1000))));
+        }
+
+        if(SkillUtils.canUseSubskill(player, TRIDENTS_IMPALE)) {
+            messages.add(getStatMessage(TRIDENTS_IMPALE,
+                    String.valueOf(mmoPlayer.getTridentsManager().impaleDamageBonus())));
+        }
+
+        messages.add(ChatColor.GRAY + "The Tridents skill is a work in progress and is still being developed, feedback would be appreciated in the mcMMO discord server.");
+        return messages;
+    }
+
+    @Override
+    protected List<Component> getTextComponents(Player player) {
+        List<Component> textComponents = new ArrayList<>();
+
+        TextComponentFactory.getSubSkillTextComponents(player, textComponents, PrimarySkillType.TRIDENTS);
+
+        return textComponents;
+    }
+}

+ 9 - 9
src/main/java/com/gmail/nossr50/commands/skills/UnarmedCommand.java

@@ -5,9 +5,9 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.Component;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
@@ -40,7 +40,7 @@ public class UnarmedCommand extends SkillCommand {
     protected void dataCalculations(Player player, float skillValue) {
     protected void dataCalculations(Player player, float skillValue) {
         // UNARMED_ARROW_DEFLECT
         // UNARMED_ARROW_DEFLECT
         if (canDeflect) {
         if (canDeflect) {
-            String[] deflectStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.UNARMED_ARROW_DEFLECT);
+            String[] deflectStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.UNARMED_ARROW_DEFLECT);
             deflectChance = deflectStrings[0];
             deflectChance = deflectStrings[0];
             deflectChanceLucky = deflectStrings[1];
             deflectChanceLucky = deflectStrings[1];
         }
         }
@@ -54,7 +54,7 @@ public class UnarmedCommand extends SkillCommand {
 
 
         // UNARMED_DISARM
         // UNARMED_DISARM
         if (canDisarm) {
         if (canDisarm) {
-            String[] disarmStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.UNARMED_DISARM);
+            String[] disarmStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.UNARMED_DISARM);
             disarmChance = disarmStrings[0];
             disarmChance = disarmStrings[0];
             disarmChanceLucky = disarmStrings[1];
             disarmChanceLucky = disarmStrings[1];
         }
         }
@@ -66,7 +66,7 @@ public class UnarmedCommand extends SkillCommand {
 
 
         // IRON GRIP
         // IRON GRIP
         if (canIronGrip) {
         if (canIronGrip) {
-            String[] ironGripStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.UNARMED_IRON_GRIP);
+            String[] ironGripStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.UNARMED_IRON_GRIP);
             ironGripChance = ironGripStrings[0];
             ironGripChance = ironGripStrings[0];
             ironGripChanceLucky = ironGripStrings[1];
             ironGripChanceLucky = ironGripStrings[1];
         }
         }
@@ -75,10 +75,10 @@ public class UnarmedCommand extends SkillCommand {
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
         canBerserk = RankUtils.hasUnlockedSubskill(player, SubSkillType.UNARMED_BERSERK) && Permissions.berserk(player);
         canBerserk = RankUtils.hasUnlockedSubskill(player, SubSkillType.UNARMED_BERSERK) && Permissions.berserk(player);
-        canIronArm = canUseSubskill(player, SubSkillType.UNARMED_STEEL_ARM_STYLE);
-        canDeflect = canUseSubskill(player, SubSkillType.UNARMED_ARROW_DEFLECT);
-        canDisarm = canUseSubskill(player, SubSkillType.UNARMED_DISARM);
-        canIronGrip = canUseSubskill(player, SubSkillType.UNARMED_IRON_GRIP);
+        canIronArm = Permissions.canUseSubSkill(player, SubSkillType.UNARMED_STEEL_ARM_STYLE);
+        canDeflect = Permissions.canUseSubSkill(player, SubSkillType.UNARMED_ARROW_DEFLECT);
+        canDisarm = Permissions.canUseSubSkill(player, SubSkillType.UNARMED_DISARM);
+        canIronGrip = Permissions.canUseSubSkill(player, SubSkillType.UNARMED_IRON_GRIP);
         // TODO: Apparently we forgot about block cracker?
         // TODO: Apparently we forgot about block cracker?
     }
     }
 
 
@@ -114,7 +114,7 @@ public class UnarmedCommand extends SkillCommand {
             //messages.add(LocaleLoader.getString("Unarmed.Ability.Chance.IronGrip", ironGripChance) + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", ironGripChanceLucky) : ""));
             //messages.add(LocaleLoader.getString("Unarmed.Ability.Chance.IronGrip", ironGripChance) + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", ironGripChanceLucky) : ""));
         }
         }
 
 
-        if(canUseSubskill(player, SubSkillType.UNARMED_UNARMED_LIMIT_BREAK)) {
+        if(Permissions.canUseSubSkill(player, SubSkillType.UNARMED_UNARMED_LIMIT_BREAK)) {
             messages.add(getStatMessage(SubSkillType.UNARMED_UNARMED_LIMIT_BREAK,
             messages.add(getStatMessage(SubSkillType.UNARMED_UNARMED_LIMIT_BREAK,
                     String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, SubSkillType.UNARMED_UNARMED_LIMIT_BREAK, 1000))));
                     String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, SubSkillType.UNARMED_UNARMED_LIMIT_BREAK, 1000))));
         }
         }

+ 24 - 13
src/main/java/com/gmail/nossr50/commands/skills/WoodcuttingCommand.java

@@ -5,8 +5,8 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.Component;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
@@ -18,15 +18,15 @@ public class WoodcuttingCommand extends SkillCommand {
     private String treeFellerLength;
     private String treeFellerLength;
     private String treeFellerLengthEndurance;
     private String treeFellerLengthEndurance;
     private String doubleDropChance;
     private String doubleDropChance;
+    private String tripleDropChance;
     private String doubleDropChanceLucky;
     private String doubleDropChanceLucky;
+    private String tripleDropChanceLucky;
 
 
     private boolean canTreeFell;
     private boolean canTreeFell;
     private boolean canLeafBlow;
     private boolean canLeafBlow;
     private boolean canDoubleDrop;
     private boolean canDoubleDrop;
+    private boolean canTripleDrop;
     private boolean canKnockOnWood;
     private boolean canKnockOnWood;
-    private boolean canSplinter;
-    private boolean canBarkSurgeon;
-    private boolean canNaturesBounty;
 
 
     public WoodcuttingCommand() {
     public WoodcuttingCommand() {
         super(PrimarySkillType.WOODCUTTING);
         super(PrimarySkillType.WOODCUTTING);
@@ -38,7 +38,14 @@ public class WoodcuttingCommand extends SkillCommand {
         if (canDoubleDrop) {
         if (canDoubleDrop) {
             setDoubleDropClassicChanceStrings(player);
             setDoubleDropClassicChanceStrings(player);
         }
         }
-        
+
+        //Clean Cuts
+        if(canTripleDrop) {
+            String[] tripleDropStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.WOODCUTTING_CLEAN_CUTS);
+            tripleDropChance = tripleDropStrings[0];
+            tripleDropChanceLucky = tripleDropStrings[1];
+        }
+
         // TREE FELLER
         // TREE FELLER
         if (canTreeFell) {
         if (canTreeFell) {
             String[] treeFellerStrings = calculateLengthDisplayValues(player, skillValue);
             String[] treeFellerStrings = calculateLengthDisplayValues(player, skillValue);
@@ -48,7 +55,7 @@ public class WoodcuttingCommand extends SkillCommand {
     }
     }
 
 
     private void setDoubleDropClassicChanceStrings(Player player) {
     private void setDoubleDropClassicChanceStrings(Player player) {
-        String[] doubleDropStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.WOODCUTTING_HARVEST_LUMBER);
+        String[] doubleDropStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.WOODCUTTING_HARVEST_LUMBER);
         doubleDropChance = doubleDropStrings[0];
         doubleDropChance = doubleDropStrings[0];
         doubleDropChanceLucky = doubleDropStrings[1];
         doubleDropChanceLucky = doubleDropStrings[1];
     }
     }
@@ -56,14 +63,12 @@ public class WoodcuttingCommand extends SkillCommand {
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
         canTreeFell = RankUtils.hasUnlockedSubskill(player, SubSkillType.WOODCUTTING_TREE_FELLER) && Permissions.treeFeller(player);
         canTreeFell = RankUtils.hasUnlockedSubskill(player, SubSkillType.WOODCUTTING_TREE_FELLER) && Permissions.treeFeller(player);
-        canDoubleDrop = canUseSubskill(player, SubSkillType.WOODCUTTING_HARVEST_LUMBER)
-                && !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill)
+        canDoubleDrop = !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill)
+                && Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_HARVEST_LUMBER)
                 && RankUtils.getRank(player, SubSkillType.WOODCUTTING_HARVEST_LUMBER) >= 1;
                 && RankUtils.getRank(player, SubSkillType.WOODCUTTING_HARVEST_LUMBER) >= 1;
-        canLeafBlow = canUseSubskill(player, SubSkillType.WOODCUTTING_LEAF_BLOWER);
-        canKnockOnWood = canTreeFell && canUseSubskill(player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD);
-        /*canSplinter = canUseSubskill(player, SubSkillType.WOODCUTTING_SPLINTER);
-        canBarkSurgeon = canUseSubskill(player, SubSkillType.WOODCUTTING_BARK_SURGEON);
-        canNaturesBounty = canUseSubskill(player, SubSkillType.WOODCUTTING_NATURES_BOUNTY);*/
+        canTripleDrop = !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill) && Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_CLEAN_CUTS);
+        canLeafBlow = Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_LEAF_BLOWER);
+        canKnockOnWood = canTreeFell && Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD);
     }
     }
 
 
     @Override
     @Override
@@ -75,6 +80,12 @@ public class WoodcuttingCommand extends SkillCommand {
                     + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", doubleDropChanceLucky) : ""));
                     + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", doubleDropChanceLucky) : ""));
         }
         }
 
 
+        if(canTripleDrop) {
+            messages.add(getStatMessage(SubSkillType.WOODCUTTING_CLEAN_CUTS, tripleDropChance)
+                    + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", tripleDropChanceLucky) : ""));
+        }
+
+
         if (canKnockOnWood) {
         if (canKnockOnWood) {
             String lootNote;
             String lootNote;
 
 

+ 9 - 0
src/main/java/com/gmail/nossr50/config/AdvancedConfig.java

@@ -697,6 +697,15 @@ public class AdvancedConfig extends BukkitConfig {
         return config.getDouble("Skills.Axes.SkullSplitter.DamageModifier", 2.0D);
         return config.getDouble("Skills.Axes.SkullSplitter.DamageModifier", 2.0D);
     }
     }
 
 
+    /* CROSSBOWS */
+    public double getPoweredShotRankDamageMultiplier() {
+        return config.getDouble("Skills.Crossbows.PoweredShot.RankDamageMultiplier", 10.0D);
+    }
+
+    public double getPoweredShotDamageMax() {
+        return config.getDouble("Skills.Archery.SkillShot.MaxDamage", 9.0D);
+    }
+
     /* EXCAVATION */
     /* EXCAVATION */
     //Nothing to configure, everything is already configurable in config.yml
     //Nothing to configure, everything is already configurable in config.yml
 
 

+ 6 - 0
src/main/java/com/gmail/nossr50/config/ChatConfig.java

@@ -51,6 +51,12 @@ public class ChatConfig extends BukkitConfig {
         return config.getBoolean(key, true);
         return config.getBoolean(key, true);
     }
     }
 
 
+    public boolean isConsoleIncludedInAudience(@NotNull ChatChannel chatChannel) {
+        String key = "Chat.Channels." + StringUtils.getCapitalized(chatChannel.toString()) + ".Send_To_Console";
+        return config.getBoolean(key, true);
+    }
+
+
     public boolean isSpyingAutomatic() {
     public boolean isSpyingAutomatic() {
         return config.getBoolean("Chat.Channels.Party.Spies.Automatically_Enable_Spying", false);
         return config.getBoolean("Chat.Channels.Party.Spies.Automatically_Enable_Spying", false);
     }
     }

+ 2 - 0
src/main/java/com/gmail/nossr50/config/GeneralConfig.java

@@ -1009,4 +1009,6 @@ public class GeneralConfig extends BukkitConfig {
     public boolean useVerboseLogging() {
     public boolean useVerboseLogging() {
         return config.getBoolean("General.Verbose_Logging", false);
         return config.getBoolean("General.Verbose_Logging", false);
     }
     }
+
+    public boolean isMasterySystemEnabled() { return config.getBoolean( "General.PowerLevel.Skill_Mastery.Enabled"); }
 }
 }

+ 53 - 72
src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java

@@ -260,7 +260,7 @@ public class ExperienceConfig extends BukkitConfig {
 
 
     /* Skill modifiers */
     /* Skill modifiers */
     public double getFormulaSkillModifier(PrimarySkillType skill) {
     public double getFormulaSkillModifier(PrimarySkillType skill) {
-        return config.getDouble("Experience_Formula.Modifier." + StringUtils.getCapitalized(skill.toString()));
+        return config.getDouble("Experience_Formula.Modifier." + StringUtils.getCapitalized(skill.toString()), 1);
     }
     }
 
 
     /* Custom XP perk */
     /* Custom XP perk */
@@ -300,6 +300,10 @@ public class ExperienceConfig extends BukkitConfig {
     }
     }
 
 
     /* Combat XP Multipliers */
     /* Combat XP Multipliers */
+    public double getCombatXP(String entity) {
+        return config.getDouble("Experience_Values.Combat.Multiplier." + entity);
+    }
+
     public double getCombatXP(EntityType entity) {
     public double getCombatXP(EntityType entity) {
         return config.getDouble("Experience_Values.Combat.Multiplier." + StringUtils.getPrettyEntityTypeString(entity).replace(" ", "_"));
         return config.getDouble("Experience_Values.Combat.Multiplier." + StringUtils.getPrettyEntityTypeString(entity).replace(" ", "_"));
     }
     }
@@ -318,96 +322,73 @@ public class ExperienceConfig extends BukkitConfig {
 
 
     /* Materials  */
     /* Materials  */
     public int getXp(PrimarySkillType skill, Material material) {
     public int getXp(PrimarySkillType skill, Material material) {
-        //TODO: Temporary measure to fix an exploit caused by a yet to be fixed Spigot bug (as of 7/3/2020)
-        if (material.toString().equalsIgnoreCase("LILY_PAD"))
-            return 0;
-
-        String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
-        String explicitString = baseString + StringUtils.getExplicitConfigMaterialString(material);
-        if (config.contains(explicitString))
-            return config.getInt(explicitString);
-        String friendlyString = baseString + StringUtils.getFriendlyConfigMaterialString(material);
-        if (config.contains(friendlyString))
-            return config.getInt(friendlyString);
-        String wildcardString = baseString + StringUtils.getWildcardConfigMaterialString(material);
-        if (config.contains(wildcardString))
-            return config.getInt(wildcardString);
-        return 0;
+        return getXpHelper(skill, StringUtils.getExplicitConfigMaterialString(material),
+                StringUtils.getFriendlyConfigMaterialString(material),
+                StringUtils.getWildcardConfigMaterialString(material));
     }
     }
 
 
-    /* Materials  */
     public int getXp(PrimarySkillType skill, BlockState blockState) {
     public int getXp(PrimarySkillType skill, BlockState blockState) {
-        Material data = blockState.getType();
-
-        String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
-        String explicitString = baseString + StringUtils.getExplicitConfigMaterialString(data);
-        if (config.contains(explicitString))
-            return config.getInt(explicitString);
-        String friendlyString = baseString + StringUtils.getFriendlyConfigMaterialString(data);
-        if (config.contains(friendlyString))
-            return config.getInt(friendlyString);
-        String wildcardString = baseString + StringUtils.getWildcardConfigMaterialString(data);
-        if (config.contains(wildcardString))
-            return config.getInt(wildcardString);
-        return 0;
+        Material material = blockState.getType();
+        return getXp(skill, material);
     }
     }
 
 
-    /* Materials  */
     public int getXp(PrimarySkillType skill, Block block) {
     public int getXp(PrimarySkillType skill, Block block) {
-        Material data = block.getType();
-
-        String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
-        String explicitString = baseString + StringUtils.getExplicitConfigMaterialString(data);
-        if (config.contains(explicitString))
-            return config.getInt(explicitString);
-        String friendlyString = baseString + StringUtils.getFriendlyConfigMaterialString(data);
-        if (config.contains(friendlyString))
-            return config.getInt(friendlyString);
-        String wildcardString = baseString + StringUtils.getWildcardConfigMaterialString(data);
-        if (config.contains(wildcardString))
-            return config.getInt(wildcardString);
-        return 0;
+        Material material = block.getType();
+        return getXp(skill, material);
     }
     }
 
 
-    /* Materials  */
     public int getXp(PrimarySkillType skill, BlockData data) {
     public int getXp(PrimarySkillType skill, BlockData data) {
+        return getXpHelper(skill, StringUtils.getExplicitConfigBlockDataString(data),
+                StringUtils.getFriendlyConfigBlockDataString(data),
+                StringUtils.getWildcardConfigBlockDataString(data));
+    }
+
+    private int getXpHelper(PrimarySkillType skill, String explicitString, String friendlyString, String wildcardString) {
+        if (explicitString.equalsIgnoreCase("LILY_PAD")) {
+            return 0;
+        }
+
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
-        String explicitString = baseString + StringUtils.getExplicitConfigBlockDataString(data);
-        if (config.contains(explicitString))
-            return config.getInt(explicitString);
-        String friendlyString = baseString + StringUtils.getFriendlyConfigBlockDataString(data);
-        if (config.contains(friendlyString))
-            return config.getInt(friendlyString);
-        String wildcardString = baseString + StringUtils.getWildcardConfigBlockDataString(data);
-        if (config.contains(wildcardString))
-            return config.getInt(wildcardString);
+        String[] configStrings = {explicitString, friendlyString, wildcardString};
+
+        for (String configString : configStrings) {
+            String fullPath = baseString + configString;
+            if (config.contains(fullPath)) {
+                return config.getInt(fullPath);
+            }
+        }
+
         return 0;
         return 0;
     }
     }
 
 
-    public boolean doesBlockGiveSkillXP(PrimarySkillType skill, Material data) {
-        String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
-        String explicitString = baseString + StringUtils.getExplicitConfigMaterialString(data);
-        if (config.contains(explicitString))
-            return true;
-        String friendlyString = baseString + StringUtils.getFriendlyConfigMaterialString(data);
-        if (config.contains(friendlyString))
-            return true;
-        String wildcardString = baseString + StringUtils.getWildcardConfigMaterialString(data);
-        return config.contains(wildcardString);
+
+    public boolean doesBlockGiveSkillXP(PrimarySkillType skill, Material material) {
+        return doesBlockGiveSkillXPHelper(skill, StringUtils.getExplicitConfigMaterialString(material),
+                StringUtils.getFriendlyConfigMaterialString(material),
+                StringUtils.getWildcardConfigMaterialString(material));
     }
     }
 
 
     public boolean doesBlockGiveSkillXP(PrimarySkillType skill, BlockData data) {
     public boolean doesBlockGiveSkillXP(PrimarySkillType skill, BlockData data) {
+        return doesBlockGiveSkillXPHelper(skill, StringUtils.getExplicitConfigBlockDataString(data),
+                StringUtils.getFriendlyConfigBlockDataString(data),
+                StringUtils.getWildcardConfigBlockDataString(data));
+    }
+
+    private boolean doesBlockGiveSkillXPHelper(PrimarySkillType skill, String explicitString, String friendlyString, String wildcardString) {
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
-        String explicitString = baseString + StringUtils.getExplicitConfigBlockDataString(data);
-        if (config.contains(explicitString))
-            return true;
-        String friendlyString = baseString + StringUtils.getFriendlyConfigBlockDataString(data);
-        if (config.contains(friendlyString))
-            return true;
-        String wildcardString = baseString + StringUtils.getWildcardConfigBlockDataString(data);
-        return config.contains(wildcardString);
+        String[] configStrings = {explicitString, friendlyString, wildcardString};
+
+        for (String configString : configStrings) {
+            String fullPath = baseString + configString;
+            if (config.contains(fullPath)) {
+                return true;
+            }
+        }
+
+        return false;
     }
     }
 
 
+
     /*
     /*
      * Experience Bar Stuff
      * Experience Bar Stuff
      */
      */

+ 3 - 2
src/main/java/com/gmail/nossr50/database/DatabaseManagerFactory.java

@@ -10,6 +10,7 @@ import java.util.logging.Logger;
 
 
 public class DatabaseManagerFactory {
 public class DatabaseManagerFactory {
     private static Class<? extends DatabaseManager> customManager = null;
     private static Class<? extends DatabaseManager> customManager = null;
+    public static final String MYSQL_DRIVER = "com.mysql.cj.jdbc.Driver";
 
 
     public static DatabaseManager getDatabaseManager(@NotNull String userFilePath, @NotNull Logger logger, long purgeTime, int startingLevel) {
     public static DatabaseManager getDatabaseManager(@NotNull String userFilePath, @NotNull Logger logger, long purgeTime, int startingLevel) {
         if (customManager != null) {
         if (customManager != null) {
@@ -27,7 +28,7 @@ public class DatabaseManagerFactory {
             LogUtils.debug(mcMMO.p.getLogger(), "Falling back on " + (mcMMO.p.getGeneralConfig().getUseMySQL() ? "SQL" : "Flatfile") + " database");
             LogUtils.debug(mcMMO.p.getLogger(), "Falling back on " + (mcMMO.p.getGeneralConfig().getUseMySQL() ? "SQL" : "Flatfile") + " database");
         }
         }
 
 
-        return mcMMO.p.getGeneralConfig().getUseMySQL() ? new SQLDatabaseManager() : new FlatFileDatabaseManager(userFilePath, logger, purgeTime, startingLevel);
+        return mcMMO.p.getGeneralConfig().getUseMySQL() ? new SQLDatabaseManager(logger, MYSQL_DRIVER) : new FlatFileDatabaseManager(userFilePath, logger, purgeTime, startingLevel);
     }
     }
 
 
     /**
     /**
@@ -68,7 +69,7 @@ public class DatabaseManagerFactory {
 
 
             case SQL:
             case SQL:
                 LogUtils.debug(mcMMO.p.getLogger(), "Using SQL Database");
                 LogUtils.debug(mcMMO.p.getLogger(), "Using SQL Database");
-                return new SQLDatabaseManager();
+                return new SQLDatabaseManager(logger, "com.mysql.cj.jdbc.Driver");
 
 
             case CUSTOM:
             case CUSTOM:
                 try {
                 try {

+ 7 - 0
src/main/java/com/gmail/nossr50/database/FlatFileDataProcessor.java

@@ -276,6 +276,8 @@ public class FlatFileDataProcessor {
             case SKILLS_TAMING:
             case SKILLS_TAMING:
             case SKILLS_FISHING:
             case SKILLS_FISHING:
             case SKILLS_ALCHEMY:
             case SKILLS_ALCHEMY:
+            case SKILLS_CROSSBOWS:
+            case SKILLS_TRIDENTS:
             case COOLDOWN_BERSERK:
             case COOLDOWN_BERSERK:
             case COOLDOWN_GIGA_DRILL_BREAKER:
             case COOLDOWN_GIGA_DRILL_BREAKER:
             case COOLDOWN_TREE_FELLER:
             case COOLDOWN_TREE_FELLER:
@@ -286,6 +288,9 @@ public class FlatFileDataProcessor {
             case COOLDOWN_BLAST_MINING:
             case COOLDOWN_BLAST_MINING:
             case SCOREBOARD_TIPS:
             case SCOREBOARD_TIPS:
             case COOLDOWN_CHIMAERA_WING:
             case COOLDOWN_CHIMAERA_WING:
+            case COOLDOWN_SUPER_SHOTGUN:
+            case COOLDOWN_TRIDENTS:
+            case COOLDOWN_ARCHERY:
                 return ExpectedType.INTEGER;
                 return ExpectedType.INTEGER;
             case EXP_MINING:
             case EXP_MINING:
             case EXP_WOODCUTTING:
             case EXP_WOODCUTTING:
@@ -300,6 +305,8 @@ public class FlatFileDataProcessor {
             case EXP_TAMING:
             case EXP_TAMING:
             case EXP_FISHING:
             case EXP_FISHING:
             case EXP_ALCHEMY:
             case EXP_ALCHEMY:
+            case EXP_CROSSBOWS:
+            case EXP_TRIDENTS:
                 return ExpectedType.FLOAT;
                 return ExpectedType.FLOAT;
             case UUID_INDEX:
             case UUID_INDEX:
                 return ExpectedType.UUID;
                 return ExpectedType.UUID;

+ 55 - 29
src/main/java/com/gmail/nossr50/database/FlatFileDatabaseManager.java

@@ -80,10 +80,17 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
     public static final int SCOREBOARD_TIPS = 42;
     public static final int SCOREBOARD_TIPS = 42;
     public static final int COOLDOWN_CHIMAERA_WING = 43;
     public static final int COOLDOWN_CHIMAERA_WING = 43;
     public static final int OVERHAUL_LAST_LOGIN = 44;
     public static final int OVERHAUL_LAST_LOGIN = 44;
-
-    public static final int DATA_ENTRY_COUNT = OVERHAUL_LAST_LOGIN + 1; //Update this everytime new data is added
-
-    protected FlatFileDatabaseManager(@NotNull File usersFile, @NotNull Logger logger, long purgeTime, int startingLevel, boolean testing) {
+    public static final int EXP_CROSSBOWS = 45;
+    public static final int SKILLS_CROSSBOWS = 46;
+    public static final int EXP_TRIDENTS = 47;
+    public static final int SKILLS_TRIDENTS = 48;
+    public static final int COOLDOWN_SUPER_SHOTGUN = 49;
+    public static final int COOLDOWN_TRIDENTS = 50;
+    public static final int COOLDOWN_ARCHERY = 51;
+    //Update this everytime new data is added
+    public static final int DATA_ENTRY_COUNT = COOLDOWN_ARCHERY + 1;
+
+    FlatFileDatabaseManager(@NotNull File usersFile, @NotNull Logger logger, long purgeTime, int startingLevel, boolean testing) {
         this.usersFile = usersFile;
         this.usersFile = usersFile;
         this.usersFilePath = usersFile.getPath();
         this.usersFilePath = usersFile.getPath();
         this.logger = logger;
         this.logger = logger;
@@ -99,7 +106,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
             List<FlatFileDataFlag> flatFileDataFlags = checkFileHealthAndStructure();
             List<FlatFileDataFlag> flatFileDataFlags = checkFileHealthAndStructure();
 
 
             if(flatFileDataFlags != null) {
             if(flatFileDataFlags != null) {
-                if(flatFileDataFlags.size() > 0) {
+                if(!flatFileDataFlags.isEmpty()) {
                     logger.info("Detected "+flatFileDataFlags.size() + " data entries which need correction.");
                     logger.info("Detected "+flatFileDataFlags.size() + " data entries which need correction.");
                 }
                 }
             }
             }
@@ -108,7 +115,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
         }
         }
     }
     }
 
 
-    protected FlatFileDatabaseManager(@NotNull String usersFilePath, @NotNull Logger logger, long purgeTime, int startingLevel) {
+    FlatFileDatabaseManager(@NotNull String usersFilePath, @NotNull Logger logger, long purgeTime, int startingLevel) {
         this(new File(usersFilePath), logger, purgeTime, startingLevel, false);
         this(new File(usersFilePath), logger, purgeTime, startingLevel, false);
     }
     }
 
 
@@ -237,7 +244,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
                 out.write(writer.toString());
                 out.write(writer.toString());
 
 
                 if(testing) {
                 if(testing) {
-                    System.out.println(writer.toString());
+                    System.out.println(writer);
                 }
                 }
             }
             }
             catch (IOException e) {
             catch (IOException e) {
@@ -467,6 +474,10 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
         appendable.append(String.valueOf(profile.getScoreboardTipsShown())).append(":");
         appendable.append(String.valueOf(profile.getScoreboardTipsShown())).append(":");
         appendable.append(String.valueOf(profile.getUniqueData(UniqueDataType.CHIMAERA_WING_DATS))).append(":");
         appendable.append(String.valueOf(profile.getUniqueData(UniqueDataType.CHIMAERA_WING_DATS))).append(":");
         appendable.append(String.valueOf(profile.getLastLogin())).append(":"); //overhaul last login
         appendable.append(String.valueOf(profile.getLastLogin())).append(":"); //overhaul last login
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.CROSSBOWS))).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.CROSSBOWS))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.TRIDENTS))).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.TRIDENTS))).append(":");
         appendable.append("\r\n");
         appendable.append("\r\n");
     }
     }
 
 
@@ -565,16 +576,11 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
      * @return a profile with the targets data or an unloaded profile if no data was found
      * @return a profile with the targets data or an unloaded profile if no data was found
      */
      */
     private @NotNull PlayerProfile processUserQuery(@NotNull UserQuery userQuery) throws RuntimeException {
     private @NotNull PlayerProfile processUserQuery(@NotNull UserQuery userQuery) throws RuntimeException {
-        switch(userQuery.getType()) {
-            case UUID_AND_NAME:
-                return queryByUUIDAndName((UserQueryFull) userQuery);
-            case UUID:
-                return queryByUUID((UserQueryUUID) userQuery);
-            case NAME:
-                return queryByName((UserQueryNameImpl) userQuery);
-            default:
-                throw new RuntimeException("No case for this UserQueryType!");
-        }
+        return switch (userQuery.getType()) {
+            case UUID_AND_NAME -> queryByUUIDAndName((UserQueryFull) userQuery);
+            case UUID -> queryByUUID((UserQueryUUID) userQuery);
+            case NAME -> queryByName((UserQueryNameImpl) userQuery);
+        };
     }
     }
 
 
     private @NotNull PlayerProfile queryByName(@NotNull UserQueryName userQuery) {
     private @NotNull PlayerProfile queryByName(@NotNull UserQueryName userQuery) {
@@ -603,8 +609,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
                         continue;
                         continue;
                     }
                     }
 
 
-
-                    //If we couldn't find anyone
+                    // we found the player
                     if(playerName.equalsIgnoreCase(rawSplitData[USERNAME_INDEX])) {
                     if(playerName.equalsIgnoreCase(rawSplitData[USERNAME_INDEX])) {
                         return loadFromLine(rawSplitData);
                         return loadFromLine(rawSplitData);
                     }
                     }
@@ -681,7 +686,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
          * No match was found in the file
          * No match was found in the file
          */
          */
 
 
-        return grabUnloadedProfile(uuid, "Player-Not-Found="+uuid.toString());
+        return grabUnloadedProfile(uuid, "Player-Not-Found="+ uuid);
     }
     }
 
 
     private @NotNull PlayerProfile queryByUUIDAndName(@NotNull UserQueryFull userQuery) {
     private @NotNull PlayerProfile queryByUUIDAndName(@NotNull UserQueryFull userQuery) {
@@ -716,7 +721,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
                             boolean matchingName = dbPlayerName.equalsIgnoreCase(playerName);
                             boolean matchingName = dbPlayerName.equalsIgnoreCase(playerName);
 
 
                             if (!matchingName) {
                             if (!matchingName) {
-                                logger.warning("When loading user: "+playerName +" with UUID of (" + uuid.toString()
+                                logger.warning("When loading user: "+playerName +" with UUID of (" + uuid
                                         +") we found a mismatched name, the name in the DB will be replaced (DB name: "+dbPlayerName+")");
                                         +") we found a mismatched name, the name in the DB will be replaced (DB name: "+dbPlayerName+")");
                                 //logger.info("Name updated for player: " + rawSplitData[USERNAME_INDEX] + " => " + playerName);
                                 //logger.info("Name updated for player: " + rawSplitData[USERNAME_INDEX] + " => " + playerName);
                                 rawSplitData[USERNAME_INDEX] = playerName;
                                 rawSplitData[USERNAME_INDEX] = playerName;
@@ -980,6 +985,8 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
         List<PlayerStat> taming = new ArrayList<>();
         List<PlayerStat> taming = new ArrayList<>();
         List<PlayerStat> fishing = new ArrayList<>();
         List<PlayerStat> fishing = new ArrayList<>();
         List<PlayerStat> alchemy = new ArrayList<>();
         List<PlayerStat> alchemy = new ArrayList<>();
+        List<PlayerStat> crossbows = new ArrayList<>();
+        List<PlayerStat> tridents = new ArrayList<>();
 
 
         BufferedReader in = null;
         BufferedReader in = null;
         String playerName = null;
         String playerName = null;
@@ -1013,6 +1020,8 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
                     powerLevel += putStat(taming, playerName, skills.get(PrimarySkillType.TAMING));
                     powerLevel += putStat(taming, playerName, skills.get(PrimarySkillType.TAMING));
                     powerLevel += putStat(unarmed, playerName, skills.get(PrimarySkillType.UNARMED));
                     powerLevel += putStat(unarmed, playerName, skills.get(PrimarySkillType.UNARMED));
                     powerLevel += putStat(woodcutting, playerName, skills.get(PrimarySkillType.WOODCUTTING));
                     powerLevel += putStat(woodcutting, playerName, skills.get(PrimarySkillType.WOODCUTTING));
+                    powerLevel += putStat(crossbows, playerName, skills.get(PrimarySkillType.CROSSBOWS));
+                    powerLevel += putStat(tridents, playerName, skills.get(PrimarySkillType.TRIDENTS));
 
 
                     putStat(powerLevels, playerName, powerLevel);
                     putStat(powerLevels, playerName, powerLevel);
                 }
                 }
@@ -1048,6 +1057,8 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
         taming.sort(c);
         taming.sort(c);
         fishing.sort(c);
         fishing.sort(c);
         alchemy.sort(c);
         alchemy.sort(c);
+        crossbows.sort(c);
+        tridents.sort(c);
         powerLevels.sort(c);
         powerLevels.sort(c);
 
 
         playerStatHash.put(PrimarySkillType.MINING, mining);
         playerStatHash.put(PrimarySkillType.MINING, mining);
@@ -1063,6 +1074,8 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
         playerStatHash.put(PrimarySkillType.TAMING, taming);
         playerStatHash.put(PrimarySkillType.TAMING, taming);
         playerStatHash.put(PrimarySkillType.FISHING, fishing);
         playerStatHash.put(PrimarySkillType.FISHING, fishing);
         playerStatHash.put(PrimarySkillType.ALCHEMY, alchemy);
         playerStatHash.put(PrimarySkillType.ALCHEMY, alchemy);
+        playerStatHash.put(PrimarySkillType.CROSSBOWS, crossbows);
+        playerStatHash.put(PrimarySkillType.TRIDENTS, tridents);
 
 
         return LeaderboardStatus.UPDATED;
         return LeaderboardStatus.UPDATED;
     }
     }
@@ -1094,7 +1107,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
     public @Nullable List<FlatFileDataFlag> checkFileHealthAndStructure() {
     public @Nullable List<FlatFileDataFlag> checkFileHealthAndStructure() {
         ArrayList<FlatFileDataFlag> flagsFound = null;
         ArrayList<FlatFileDataFlag> flagsFound = null;
         LogUtils.debug(logger, "(" + usersFile.getPath() + ") Validating database file..");
         LogUtils.debug(logger, "(" + usersFile.getPath() + ") Validating database file..");
-        FlatFileDataProcessor dataProcessor = null;
+        FlatFileDataProcessor dataProcessor;
 
 
         if (usersFile.exists()) {
         if (usersFile.exists()) {
             BufferedReader bufferedReader = null;
             BufferedReader bufferedReader = null;
@@ -1126,7 +1139,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
                     }
                     }
 
 
                     //Only update the file if needed
                     //Only update the file if needed
-                    if(dataProcessor.getFlatFileDataFlags().size() > 0) {
+                    if(!dataProcessor.getFlatFileDataFlags().isEmpty()) {
                         flagsFound = new ArrayList<>(dataProcessor.getFlatFileDataFlags());
                         flagsFound = new ArrayList<>(dataProcessor.getFlatFileDataFlags());
                         logger.info("Updating FlatFile Database...");
                         logger.info("Updating FlatFile Database...");
                         fileWriter = new FileWriter(usersFilePath);
                         fileWriter = new FileWriter(usersFilePath);
@@ -1144,7 +1157,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
             }
             }
         }
         }
 
 
-        if(flagsFound == null || flagsFound.size() == 0) {
+        if(flagsFound == null || flagsFound.isEmpty()) {
             return null;
             return null;
         } else {
         } else {
             return flagsFound;
             return flagsFound;
@@ -1224,6 +1237,8 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
         tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.ACROBATICS, EXP_ACROBATICS, username);
         tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.ACROBATICS, EXP_ACROBATICS, username);
         tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.FISHING, EXP_FISHING, username);
         tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.FISHING, EXP_FISHING, username);
         tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.ALCHEMY, EXP_ALCHEMY, username);
         tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.ALCHEMY, EXP_ALCHEMY, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.CROSSBOWS, EXP_CROSSBOWS, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.TRIDENTS, EXP_TRIDENTS, username);
 
 
         // Taming - Unused
         // Taming - Unused
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SUPER_BREAKER, COOLDOWN_SUPER_BREAKER, username);
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SUPER_BREAKER, COOLDOWN_SUPER_BREAKER, username);
@@ -1232,11 +1247,13 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.BERSERK, COOLDOWN_BERSERK, username);
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.BERSERK, COOLDOWN_BERSERK, username);
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.GREEN_TERRA, COOLDOWN_GREEN_TERRA, username);
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.GREEN_TERRA, COOLDOWN_GREEN_TERRA, username);
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.GIGA_DRILL_BREAKER, COOLDOWN_GIGA_DRILL_BREAKER, username);
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.GIGA_DRILL_BREAKER, COOLDOWN_GIGA_DRILL_BREAKER, username);
-        // Archery - Unused
+        tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.EXPLOSIVE_SHOT, COOLDOWN_ARCHERY, username);
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SERRATED_STRIKES, COOLDOWN_SERRATED_STRIKES, username);
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SERRATED_STRIKES, COOLDOWN_SERRATED_STRIKES, username);
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SKULL_SPLITTER, COOLDOWN_SKULL_SPLITTER, username);
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SKULL_SPLITTER, COOLDOWN_SKULL_SPLITTER, username);
         // Acrobatics - Unused
         // Acrobatics - Unused
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.BLAST_MINING, COOLDOWN_BLAST_MINING, username);
         tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.BLAST_MINING, COOLDOWN_BLAST_MINING, username);
+        tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SUPER_SHOTGUN, COOLDOWN_SUPER_SHOTGUN, username);
+        tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.TRIDENTS_SUPER_ABILITY, COOLDOWN_TRIDENTS, username);
 
 
         UUID uuid;
         UUID uuid;
         try {
         try {
@@ -1269,12 +1286,15 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
         return new PlayerProfile(username, uuid, skills, skillsXp, skillsDATS, scoreboardTipsShown, uniquePlayerDataMap, lastLogin);
         return new PlayerProfile(username, uuid, skills, skillsXp, skillsDATS, scoreboardTipsShown, uniquePlayerDataMap, lastLogin);
     }
     }
 
 
-    private void tryLoadSkillCooldownFromRawData(@NotNull Map<SuperAbilityType, Integer> cooldownMap, @NotNull String[] character, @NotNull SuperAbilityType superAbilityType, int cooldownSuperBreaker, @NotNull String userName) {
+    private void tryLoadSkillCooldownFromRawData(@NotNull Map<SuperAbilityType, Integer> cooldownMap, @NotNull String[] splitData, @NotNull SuperAbilityType superAbilityType, int index, @NotNull String userName) {
         try {
         try {
-            cooldownMap.put(superAbilityType, Integer.valueOf(character[cooldownSuperBreaker]));
+            cooldownMap.put(superAbilityType, Integer.valueOf(splitData[index]));
+        } catch (IndexOutOfBoundsException e) {
+            // TODO: Add debug message
+            // set to 0 when data not found
+            cooldownMap.put(superAbilityType, 0);
         } catch (NumberFormatException e) {
         } catch (NumberFormatException e) {
-            logger.severe("Data corruption when trying to load the value for skill "+superAbilityType+" for player named " + userName+ " setting value to zero");
-            e.printStackTrace();
+            throw new NumberFormatException("Data corruption when trying to load the cooldown for skill "+superAbilityType+" for player named " + userName);
         }
         }
     }
     }
 
 
@@ -1293,6 +1313,10 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
         try {
         try {
             int valueFromString = Integer.parseInt(character[index]);
             int valueFromString = Integer.parseInt(character[index]);
             skillMap.put(primarySkillType, valueFromString);
             skillMap.put(primarySkillType, valueFromString);
+        } catch (ArrayIndexOutOfBoundsException e ) {
+            // TODO: Add debug message
+            // set to 0 when data not found
+            skillMap.put(primarySkillType, 0);
         } catch (NumberFormatException e) {
         } catch (NumberFormatException e) {
             skillMap.put(primarySkillType, 0);
             skillMap.put(primarySkillType, 0);
             logger.severe("Data corruption when trying to load the value for skill "+primarySkillType+" for player named " + userName+ " setting value to zero");
             logger.severe("Data corruption when trying to load the value for skill "+primarySkillType+" for player named " + userName+ " setting value to zero");
@@ -1317,6 +1341,8 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
         tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.AXES, SKILLS_AXES, username);
         tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.AXES, SKILLS_AXES, username);
         tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.FISHING, SKILLS_FISHING, username);
         tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.FISHING, SKILLS_FISHING, username);
         tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.ALCHEMY, SKILLS_ALCHEMY, username);
         tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.ALCHEMY, SKILLS_ALCHEMY, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.CROSSBOWS, SKILLS_CROSSBOWS, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.TRIDENTS, SKILLS_TRIDENTS, username);
 
 
         return skills;
         return skills;
     }
     }

+ 230 - 127
src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java

@@ -24,6 +24,7 @@ import org.jetbrains.annotations.Nullable;
 import java.sql.*;
 import java.sql.*;
 import java.util.*;
 import java.util.*;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.logging.Logger;
 
 
 public final class SQLDatabaseManager implements DatabaseManager {
 public final class SQLDatabaseManager implements DatabaseManager {
     private static final String ALL_QUERY_VERSION = "total";
     private static final String ALL_QUERY_VERSION = "total";
@@ -32,6 +33,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
     public static final String USER_VARCHAR = "VARCHAR(40)";
     public static final String USER_VARCHAR = "VARCHAR(40)";
     public static final int CHILD_SKILLS_SIZE = 2;
     public static final int CHILD_SKILLS_SIZE = 2;
     public static final String LEGACY_DRIVER_PATH = "com.mysql.jdbc.Driver";
     public static final String LEGACY_DRIVER_PATH = "com.mysql.jdbc.Driver";
+    public static final int MAGIC_NUMBER = 44;
     private final String tablePrefix = mcMMO.p.getGeneralConfig().getMySQLTablePrefix();
     private final String tablePrefix = mcMMO.p.getGeneralConfig().getMySQLTablePrefix();
 
 
     private final Map<UUID, Integer> cachedUserIDs = new HashMap<>();
     private final Map<UUID, Integer> cachedUserIDs = new HashMap<>();
@@ -45,23 +47,19 @@ public final class SQLDatabaseManager implements DatabaseManager {
     private final ReentrantLock massUpdateLock = new ReentrantLock();
     private final ReentrantLock massUpdateLock = new ReentrantLock();
 
 
     private final String CHARSET_SQL = "utf8mb4"; //This is compliant with UTF-8 while "utf8" is not, confusing but this is how it is.
     private final String CHARSET_SQL = "utf8mb4"; //This is compliant with UTF-8 while "utf8" is not, confusing but this is how it is.
-    private String driverPath = "com.mysql.cj.jdbc.Driver"; //modern driver
+    private final Logger logger;
+    private final boolean h2;
 
 
-    protected SQLDatabaseManager() {
-        String connectionString = "jdbc:mysql://" + mcMMO.p.getGeneralConfig().getMySQLServerName()
-                + ":" + mcMMO.p.getGeneralConfig().getMySQLServerPort() + "/" + mcMMO.p.getGeneralConfig().getMySQLDatabaseName();
+    SQLDatabaseManager(Logger logger, String driverPath) {
+        this(logger, driverPath, false);
+    }
 
 
-        if(!mcMMO.getCompatibilityManager().getMinecraftGameVersion().isAtLeast(1, 17, 0) //Temporary hack for SQL and 1.17 support
-                && mcMMO.p.getGeneralConfig().getMySQLSSL())
-            connectionString +=
-                    "?verifyServerCertificate=false"+
-                    "&useSSL=true"+
-                    "&requireSSL=true";
-        else
-            connectionString+=
-                    "?useSSL=false";
+    SQLDatabaseManager(Logger logger, String driverPath, boolean h2) {
+        this.logger = logger;
+        this.h2 = h2;
+        String connectionString = getConnectionString(h2);
 
 
-        if(mcMMO.p.getGeneralConfig().getMySQLPublicKeyRetrieval()) {
+        if(!h2 && mcMMO.p.getGeneralConfig().getMySQLPublicKeyRetrieval()) {
             connectionString+=
             connectionString+=
                     "&allowPublicKeyRetrieval=true";
                     "&allowPublicKeyRetrieval=true";
         }
         }
@@ -76,7 +74,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             } catch (ClassNotFoundException ex) {
             } catch (ClassNotFoundException ex) {
                 e.printStackTrace();
                 e.printStackTrace();
                 ex.printStackTrace();
                 ex.printStackTrace();
-                mcMMO.p.getLogger().severe("Neither driver found");
+                logger.severe("Neither driver found");
                 return;
                 return;
             }
             }
             //throw e; // aborts onEnable()  Riking if you want to do this, fully implement it.
             //throw e; // aborts onEnable()  Riking if you want to do this, fully implement it.
@@ -133,9 +131,31 @@ public final class SQLDatabaseManager implements DatabaseManager {
         checkStructure();
         checkStructure();
     }
     }
 
 
+    @NotNull
+    private static String getConnectionString(boolean h2) {
+        if (h2) {
+            return "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL";
+        }
+
+        String connectionString = "jdbc:mysql://" + mcMMO.p.getGeneralConfig().getMySQLServerName()
+                + ":" + mcMMO.p.getGeneralConfig().getMySQLServerPort() + "/" + mcMMO.p.getGeneralConfig().getMySQLDatabaseName();
+
+        if(!mcMMO.getCompatibilityManager().getMinecraftGameVersion().isAtLeast(1, 17, 0) //Temporary hack for SQL and 1.17 support
+                && mcMMO.p.getGeneralConfig().getMySQLSSL())
+            connectionString +=
+                    "?verifyServerCertificate=false"+
+                    "&useSSL=true"+
+                    "&requireSSL=true";
+        else
+            connectionString+=
+                    "?useSSL=false";
+        return connectionString;
+    }
+
+    // TODO: unit tests
     public int purgePowerlessUsers() {
     public int purgePowerlessUsers() {
         massUpdateLock.lock();
         massUpdateLock.lock();
-        mcMMO.p.getLogger().info("Purging powerless users...");
+        logger.info("Purging powerless users...");
 
 
         Connection connection = null;
         Connection connection = null;
         Statement statement = null;
         Statement statement = null;
@@ -149,7 +169,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                     + "taming = 0 AND mining = 0 AND woodcutting = 0 AND repair = 0 "
                     + "taming = 0 AND mining = 0 AND woodcutting = 0 AND repair = 0 "
                     + "AND unarmed = 0 AND herbalism = 0 AND excavation = 0 AND "
                     + "AND unarmed = 0 AND herbalism = 0 AND excavation = 0 AND "
                     + "archery = 0 AND swords = 0 AND axes = 0 AND acrobatics = 0 "
                     + "archery = 0 AND swords = 0 AND axes = 0 AND acrobatics = 0 "
-                    + "AND fishing = 0 AND alchemy = 0;");
+                    + "AND fishing = 0 AND alchemy = 0 AND crossbows = 0 AND tridents = 0;");
 
 
             statement.executeUpdate("DELETE FROM `" + tablePrefix + "experience` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "skills` `s` WHERE `" + tablePrefix + "experience`.`user_id` = `s`.`user_id`)");
             statement.executeUpdate("DELETE FROM `" + tablePrefix + "experience` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "skills` `s` WHERE `" + tablePrefix + "experience`.`user_id` = `s`.`user_id`)");
             statement.executeUpdate("DELETE FROM `" + tablePrefix + "huds` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "skills` `s` WHERE `" + tablePrefix + "huds`.`user_id` = `s`.`user_id`)");
             statement.executeUpdate("DELETE FROM `" + tablePrefix + "huds` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "skills` `s` WHERE `" + tablePrefix + "huds`.`user_id` = `s`.`user_id`)");
@@ -165,13 +185,13 @@ public final class SQLDatabaseManager implements DatabaseManager {
             massUpdateLock.unlock();
             massUpdateLock.unlock();
         }
         }
 
 
-        mcMMO.p.getLogger().info("Purged " + purged + " users from the database.");
+        logger.info("Purged " + purged + " users from the database.");
         return purged;
         return purged;
     }
     }
 
 
     public void purgeOldUsers() {
     public void purgeOldUsers() {
         massUpdateLock.lock();
         massUpdateLock.lock();
-        mcMMO.p.getLogger().info("Purging inactive users older than " + (mcMMO.p.getPurgeTime() / 2630000000L) + " months...");
+        logger.info("Purging inactive users older than " + (mcMMO.p.getPurgeTime() / 2630000000L) + " months...");
 
 
         Connection connection = null;
         Connection connection = null;
         Statement statement = null;
         Statement statement = null;
@@ -196,7 +216,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             massUpdateLock.unlock();
             massUpdateLock.unlock();
         }
         }
 
 
-        mcMMO.p.getLogger().info("Purged " + purged + " users from the database.");
+        logger.info("Purged " + purged + " users from the database.");
     }
     }
 
 
     public boolean removeUser(String playerName, UUID uuid) {
     public boolean removeUser(String playerName, UUID uuid) {
@@ -212,7 +232,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                     "JOIN " + tablePrefix + "huds h ON (u.id = h.user_id) " +
                     "JOIN " + tablePrefix + "huds h ON (u.id = h.user_id) " +
                     "JOIN " + tablePrefix + "skills s ON (u.id = s.user_id) " +
                     "JOIN " + tablePrefix + "skills s ON (u.id = s.user_id) " +
                     "JOIN " + tablePrefix + "cooldowns c ON (u.id = c.user_id) " +
                     "JOIN " + tablePrefix + "cooldowns c ON (u.id = c.user_id) " +
-                    "WHERE u.user = ?");
+                    "WHERE u.`user` = ?");
 
 
             statement.setString(1, playerName);
             statement.setString(1, playerName);
 
 
@@ -253,7 +273,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             if (id == -1) {
             if (id == -1) {
                 id = newUser(connection, profile.getPlayerName(), profile.getUniqueId());
                 id = newUser(connection, profile.getPlayerName(), profile.getUniqueId());
                 if (id == -1) {
                 if (id == -1) {
-                    mcMMO.p.getLogger().severe("Failed to create new account for " + profile.getPlayerName());
+                    logger.severe("Failed to create new account for " + profile.getPlayerName());
                     return false;
                     return false;
                 }
                 }
             }
             }
@@ -263,7 +283,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             success &= (statement.executeUpdate() != 0);
             success &= (statement.executeUpdate() != 0);
             statement.close();
             statement.close();
             if (!success) {
             if (!success) {
-                mcMMO.p.getLogger().severe("Failed to update last login for " + profile.getPlayerName());
+                logger.severe("Failed to update last login for " + profile.getPlayerName());
                 return false;
                 return false;
             }
             }
 
 
@@ -271,7 +291,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                     + " taming = ?, mining = ?, repair = ?, woodcutting = ?"
                     + " taming = ?, mining = ?, repair = ?, woodcutting = ?"
                     + ", unarmed = ?, herbalism = ?, excavation = ?"
                     + ", unarmed = ?, herbalism = ?, excavation = ?"
                     + ", archery = ?, swords = ?, axes = ?, acrobatics = ?"
                     + ", archery = ?, swords = ?, axes = ?, acrobatics = ?"
-                    + ", fishing = ?, alchemy = ?, total = ? WHERE user_id = ?");
+                    + ", fishing = ?, alchemy = ?, crossbows = ?, tridents = ?, total = ? WHERE user_id = ?");
             statement.setInt(1, profile.getSkillLevel(PrimarySkillType.TAMING));
             statement.setInt(1, profile.getSkillLevel(PrimarySkillType.TAMING));
             statement.setInt(2, profile.getSkillLevel(PrimarySkillType.MINING));
             statement.setInt(2, profile.getSkillLevel(PrimarySkillType.MINING));
             statement.setInt(3, profile.getSkillLevel(PrimarySkillType.REPAIR));
             statement.setInt(3, profile.getSkillLevel(PrimarySkillType.REPAIR));
@@ -285,15 +305,17 @@ public final class SQLDatabaseManager implements DatabaseManager {
             statement.setInt(11, profile.getSkillLevel(PrimarySkillType.ACROBATICS));
             statement.setInt(11, profile.getSkillLevel(PrimarySkillType.ACROBATICS));
             statement.setInt(12, profile.getSkillLevel(PrimarySkillType.FISHING));
             statement.setInt(12, profile.getSkillLevel(PrimarySkillType.FISHING));
             statement.setInt(13, profile.getSkillLevel(PrimarySkillType.ALCHEMY));
             statement.setInt(13, profile.getSkillLevel(PrimarySkillType.ALCHEMY));
+            statement.setInt(14, profile.getSkillLevel(PrimarySkillType.CROSSBOWS));
+            statement.setInt(15, profile.getSkillLevel(PrimarySkillType.TRIDENTS));
             int total = 0;
             int total = 0;
             for (PrimarySkillType primarySkillType : SkillTools.NON_CHILD_SKILLS)
             for (PrimarySkillType primarySkillType : SkillTools.NON_CHILD_SKILLS)
                 total += profile.getSkillLevel(primarySkillType);
                 total += profile.getSkillLevel(primarySkillType);
-            statement.setInt(14, total);
-            statement.setInt(15, id);
+            statement.setInt(16, total);
+            statement.setInt(17, id);
             success &= (statement.executeUpdate() != 0);
             success &= (statement.executeUpdate() != 0);
             statement.close();
             statement.close();
             if (!success) {
             if (!success) {
-                mcMMO.p.getLogger().severe("Failed to update skills for " + profile.getPlayerName());
+                logger.severe("Failed to update skills for " + profile.getPlayerName());
                 return false;
                 return false;
             }
             }
 
 
@@ -301,7 +323,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                     + " taming = ?, mining = ?, repair = ?, woodcutting = ?"
                     + " taming = ?, mining = ?, repair = ?, woodcutting = ?"
                     + ", unarmed = ?, herbalism = ?, excavation = ?"
                     + ", unarmed = ?, herbalism = ?, excavation = ?"
                     + ", archery = ?, swords = ?, axes = ?, acrobatics = ?"
                     + ", archery = ?, swords = ?, axes = ?, acrobatics = ?"
-                    + ", fishing = ?, alchemy = ? WHERE user_id = ?");
+                    + ", fishing = ?, alchemy = ?, crossbows = ?, tridents = ? WHERE user_id = ?");
             statement.setInt(1, profile.getSkillXpLevel(PrimarySkillType.TAMING));
             statement.setInt(1, profile.getSkillXpLevel(PrimarySkillType.TAMING));
             statement.setInt(2, profile.getSkillXpLevel(PrimarySkillType.MINING));
             statement.setInt(2, profile.getSkillXpLevel(PrimarySkillType.MINING));
             statement.setInt(3, profile.getSkillXpLevel(PrimarySkillType.REPAIR));
             statement.setInt(3, profile.getSkillXpLevel(PrimarySkillType.REPAIR));
@@ -315,18 +337,20 @@ public final class SQLDatabaseManager implements DatabaseManager {
             statement.setInt(11, profile.getSkillXpLevel(PrimarySkillType.ACROBATICS));
             statement.setInt(11, profile.getSkillXpLevel(PrimarySkillType.ACROBATICS));
             statement.setInt(12, profile.getSkillXpLevel(PrimarySkillType.FISHING));
             statement.setInt(12, profile.getSkillXpLevel(PrimarySkillType.FISHING));
             statement.setInt(13, profile.getSkillXpLevel(PrimarySkillType.ALCHEMY));
             statement.setInt(13, profile.getSkillXpLevel(PrimarySkillType.ALCHEMY));
-            statement.setInt(14, id);
+            statement.setInt(14, profile.getSkillXpLevel(PrimarySkillType.CROSSBOWS));
+            statement.setInt(15, profile.getSkillXpLevel(PrimarySkillType.TRIDENTS));
+            statement.setInt(16, id);
             success &= (statement.executeUpdate() != 0);
             success &= (statement.executeUpdate() != 0);
             statement.close();
             statement.close();
             if (!success) {
             if (!success) {
-                mcMMO.p.getLogger().severe("Failed to update experience for " + profile.getPlayerName());
+                logger.severe("Failed to update experience for " + profile.getPlayerName());
                 return false;
                 return false;
             }
             }
 
 
             statement = connection.prepareStatement("UPDATE " + tablePrefix + "cooldowns SET "
             statement = connection.prepareStatement("UPDATE " + tablePrefix + "cooldowns SET "
                     + "  mining = ?, woodcutting = ?, unarmed = ?"
                     + "  mining = ?, woodcutting = ?, unarmed = ?"
                     + ", herbalism = ?, excavation = ?, swords = ?"
                     + ", herbalism = ?, excavation = ?, swords = ?"
-                    + ", axes = ?, blast_mining = ?, chimaera_wing = ? WHERE user_id = ?");
+                    + ", axes = ?, blast_mining = ?, chimaera_wing = ?, crossbows = ?, tridents = ? WHERE user_id = ?");
             statement.setLong(1, profile.getAbilityDATS(SuperAbilityType.SUPER_BREAKER));
             statement.setLong(1, profile.getAbilityDATS(SuperAbilityType.SUPER_BREAKER));
             statement.setLong(2, profile.getAbilityDATS(SuperAbilityType.TREE_FELLER));
             statement.setLong(2, profile.getAbilityDATS(SuperAbilityType.TREE_FELLER));
             statement.setLong(3, profile.getAbilityDATS(SuperAbilityType.BERSERK));
             statement.setLong(3, profile.getAbilityDATS(SuperAbilityType.BERSERK));
@@ -336,11 +360,13 @@ public final class SQLDatabaseManager implements DatabaseManager {
             statement.setLong(7, profile.getAbilityDATS(SuperAbilityType.SKULL_SPLITTER));
             statement.setLong(7, profile.getAbilityDATS(SuperAbilityType.SKULL_SPLITTER));
             statement.setLong(8, profile.getAbilityDATS(SuperAbilityType.BLAST_MINING));
             statement.setLong(8, profile.getAbilityDATS(SuperAbilityType.BLAST_MINING));
             statement.setLong(9, profile.getUniqueData(UniqueDataType.CHIMAERA_WING_DATS));
             statement.setLong(9, profile.getUniqueData(UniqueDataType.CHIMAERA_WING_DATS));
-            statement.setInt(10, id);
+            statement.setLong(10, profile.getAbilityDATS(SuperAbilityType.SUPER_SHOTGUN));
+            statement.setLong(11, profile.getAbilityDATS(SuperAbilityType.TRIDENTS_SUPER_ABILITY));
+            statement.setInt(12, id);
             success = (statement.executeUpdate() != 0);
             success = (statement.executeUpdate() != 0);
             statement.close();
             statement.close();
             if (!success) {
             if (!success) {
-                mcMMO.p.getLogger().severe("Failed to update cooldowns for " + profile.getPlayerName());
+                logger.severe("Failed to update cooldowns for " + profile.getPlayerName());
                 return false;
                 return false;
             }
             }
 
 
@@ -351,7 +377,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             success = (statement.executeUpdate() != 0);
             success = (statement.executeUpdate() != 0);
             statement.close();
             statement.close();
             if (!success) {
             if (!success) {
-                mcMMO.p.getLogger().severe("Failed to update hud settings for " + profile.getPlayerName());
+                logger.severe("Failed to update hud settings for " + profile.getPlayerName());
                 return false;
                 return false;
             }
             }
         }
         }
@@ -371,11 +397,10 @@ public final class SQLDatabaseManager implements DatabaseManager {
 
 
         //Fix for a plugin that people are using that is throwing SQL errors
         //Fix for a plugin that people are using that is throwing SQL errors
         if(skill != null && SkillTools.isChildSkill(skill)) {
         if(skill != null && SkillTools.isChildSkill(skill)) {
-            mcMMO.p.getLogger().severe("A plugin hooking into mcMMO is being naughty with our database commands, update all plugins that hook into mcMMO and contact their devs!");
+            logger.severe("A plugin hooking into mcMMO is being naughty with our database commands, update all plugins that hook into mcMMO and contact their devs!");
             throw new InvalidSkillException("A plugin hooking into mcMMO that you are using is attempting to read leaderboard skills for child skills, child skills do not have leaderboards! This is NOT an mcMMO error!");
             throw new InvalidSkillException("A plugin hooking into mcMMO that you are using is attempting to read leaderboard skills for child skills, child skills do not have leaderboards! This is NOT an mcMMO error!");
         }
         }
 
 
-
         String query = skill == null ? ALL_QUERY_VERSION : skill.name().toLowerCase(Locale.ENGLISH);
         String query = skill == null ? ALL_QUERY_VERSION : skill.name().toLowerCase(Locale.ENGLISH);
         ResultSet resultSet = null;
         ResultSet resultSet = null;
         PreparedStatement statement = null;
         PreparedStatement statement = null;
@@ -383,7 +408,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
 
 
         try {
         try {
             connection = getConnection(PoolIdentifier.MISC);
             connection = getConnection(PoolIdentifier.MISC);
-            statement = connection.prepareStatement("SELECT " + query + ", user FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON (user_id = id) WHERE " + query + " > 0 AND NOT user = '\\_INVALID\\_OLD\\_USERNAME\\_' ORDER BY " + query + " DESC, user LIMIT ?, ?");
+            statement = connection.prepareStatement("SELECT " + query + ", `user` FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON (user_id = id) WHERE " + query + " > 0 AND NOT `user` = '\\_INVALID\\_OLD\\_USERNAME\\_' ORDER BY " + query + " DESC, `user` LIMIT ?, ?");
             statement.setInt(1, (pageNumber * statsPerPage) - statsPerPage);
             statement.setInt(1, (pageNumber * statsPerPage) - statsPerPage);
             statement.setInt(2, statsPerPage);
             statement.setInt(2, statsPerPage);
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
@@ -424,7 +449,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                 // Get count of all users with higher skill level than player
                 // Get count of all users with higher skill level than player
                 String sql = "SELECT COUNT(*) AS 'rank' FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE " + skillName + " > 0 " +
                 String sql = "SELECT COUNT(*) AS 'rank' FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE " + skillName + " > 0 " +
                         "AND " + skillName + " > (SELECT " + skillName + " FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id " +
                         "AND " + skillName + " > (SELECT " + skillName + " FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id " +
-                        "WHERE user = ?)";
+                        "WHERE `user` = ?)";
 
 
                 statement = connection.prepareStatement(sql);
                 statement = connection.prepareStatement(sql);
                 statement.setString(1, playerName);
                 statement.setString(1, playerName);
@@ -437,7 +462,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                 // Ties are settled by alphabetical order
                 // Ties are settled by alphabetical order
                 sql = "SELECT user, " + skillName + " FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE " + skillName + " > 0 " +
                 sql = "SELECT user, " + skillName + " FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE " + skillName + " > 0 " +
                         "AND " + skillName + " = (SELECT " + skillName + " FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id " +
                         "AND " + skillName + " = (SELECT " + skillName + " FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id " +
-                        "WHERE user = '" + playerName + "') ORDER BY user";
+                        "WHERE `user` = '" + playerName + "') ORDER BY user";
 
 
                 resultSet.close();
                 resultSet.close();
                 statement.close();
                 statement.close();
@@ -460,7 +485,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                     "WHERE " + ALL_QUERY_VERSION + " > 0 " +
                     "WHERE " + ALL_QUERY_VERSION + " > 0 " +
                     "AND " + ALL_QUERY_VERSION + " > " +
                     "AND " + ALL_QUERY_VERSION + " > " +
                     "(SELECT " + ALL_QUERY_VERSION + " " +
                     "(SELECT " + ALL_QUERY_VERSION + " " +
-                    "FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE user = ?)";
+                    "FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE `user` = ?)";
 
 
             statement = connection.prepareStatement(sql);
             statement = connection.prepareStatement(sql);
             statement.setString(1, playerName);
             statement.setString(1, playerName);
@@ -478,7 +503,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                     "WHERE " + ALL_QUERY_VERSION + " > 0 " +
                     "WHERE " + ALL_QUERY_VERSION + " > 0 " +
                     "AND " + ALL_QUERY_VERSION + " = " +
                     "AND " + ALL_QUERY_VERSION + " = " +
                     "(SELECT " + ALL_QUERY_VERSION + " " +
                     "(SELECT " + ALL_QUERY_VERSION + " " +
-                    "FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE user = ?) ORDER BY user";
+                    "FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE `user` = ?) ORDER BY user";
 
 
             statement = connection.prepareStatement(sql);
             statement = connection.prepareStatement(sql);
             statement.setString(1, playerName);
             statement.setString(1, playerName);
@@ -546,21 +571,26 @@ public final class SQLDatabaseManager implements DatabaseManager {
         try {
         try {
             statement = connection.prepareStatement(
             statement = connection.prepareStatement(
                     "UPDATE `" + tablePrefix + "users` "
                     "UPDATE `" + tablePrefix + "users` "
-                            + "SET user = ? "
-                            + "WHERE user = ?");
+                            + "SET `user` = ? "
+                            + "WHERE `user` = ?");
             statement.setString(1, "_INVALID_OLD_USERNAME_");
             statement.setString(1, "_INVALID_OLD_USERNAME_");
             statement.setString(2, playerName);
             statement.setString(2, playerName);
             statement.executeUpdate();
             statement.executeUpdate();
             statement.close();
             statement.close();
-            statement = connection.prepareStatement("INSERT INTO " + tablePrefix + "users (user, uuid, lastlogin) VALUES (?, ?, UNIX_TIMESTAMP())", Statement.RETURN_GENERATED_KEYS);
+
+            long currentTimeMillis = System.currentTimeMillis();
+
+            String sql = "INSERT INTO " + tablePrefix + "users (`user`, uuid, lastlogin) VALUES (?, ?, ?)";
+            statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
             statement.setString(1, playerName);
             statement.setString(1, playerName);
             statement.setString(2, uuid != null ? uuid.toString() : null);
             statement.setString(2, uuid != null ? uuid.toString() : null);
+            statement.setLong(3, currentTimeMillis);
             statement.executeUpdate();
             statement.executeUpdate();
 
 
             resultSet = statement.getGeneratedKeys();
             resultSet = statement.getGeneratedKeys();
 
 
             if (!resultSet.next()) {
             if (!resultSet.next()) {
-                mcMMO.p.getLogger().severe("Unable to create new user account in DB");
+                logger.severe("Unable to create new user account in DB");
                 return -1;
                 return -1;
             }
             }
 
 
@@ -600,7 +630,6 @@ public final class SQLDatabaseManager implements DatabaseManager {
         return loadPlayerFromDB(uuid, null);
         return loadPlayerFromDB(uuid, null);
     }
     }
 
 
-
     private PlayerProfile loadPlayerFromDB(@Nullable UUID uuid, @Nullable String playerName) throws RuntimeException {
     private PlayerProfile loadPlayerFromDB(@Nullable UUID uuid, @Nullable String playerName) throws RuntimeException {
         if(uuid == null && playerName == null) {
         if(uuid == null && playerName == null) {
             throw new RuntimeException("Error looking up player, both UUID and playerName are null and one must not be.");
             throw new RuntimeException("Error looking up player, both UUID and playerName are null and one must not be.");
@@ -622,17 +651,18 @@ public final class SQLDatabaseManager implements DatabaseManager {
             writeMissingRows(connection, id);
             writeMissingRows(connection, id);
 
 
             statement = connection.prepareStatement(
             statement = connection.prepareStatement(
-                    "SELECT "
-                            + "s.taming, s.mining, s.repair, s.woodcutting, s.unarmed, s.herbalism, s.excavation, s.archery, s.swords, s.axes, s.acrobatics, s.fishing, s.alchemy, "
-                            + "e.taming, e.mining, e.repair, e.woodcutting, e.unarmed, e.herbalism, e.excavation, e.archery, e.swords, e.axes, e.acrobatics, e.fishing, e.alchemy, "
-                            + "c.taming, c.mining, c.repair, c.woodcutting, c.unarmed, c.herbalism, c.excavation, c.archery, c.swords, c.axes, c.acrobatics, c.blast_mining, c.chimaera_wing, "
-                            + "h.mobhealthbar, h.scoreboardtips, u.uuid, u.user "
+                    "SELECT " +
+                            "s.taming, s.mining, s.repair, s.woodcutting, s.unarmed, s.herbalism, s.excavation, s.archery, s.swords, s.axes, s.acrobatics, s.fishing, s.alchemy, s.crossbows, s.tridents, " +
+                            "e.taming, e.mining, e.repair, e.woodcutting, e.unarmed, e.herbalism, e.excavation, e.archery, e.swords, e.axes, e.acrobatics, e.fishing, e.alchemy, e.crossbows, e.tridents, " +
+                            "c.taming, c.mining, c.repair, c.woodcutting, c.unarmed, c.herbalism, c.excavation, c.archery, c.swords, c.axes, c.acrobatics, c.blast_mining, c.chimaera_wing, c.crossbows, c.tridents, " +
+                            "h.mobhealthbar, h.scoreboardtips, u.uuid, u.`user` "
                             + "FROM " + tablePrefix + "users u "
                             + "FROM " + tablePrefix + "users u "
                             + "JOIN " + tablePrefix + "skills s ON (u.id = s.user_id) "
                             + "JOIN " + tablePrefix + "skills s ON (u.id = s.user_id) "
                             + "JOIN " + tablePrefix + "experience e ON (u.id = e.user_id) "
                             + "JOIN " + tablePrefix + "experience e ON (u.id = e.user_id) "
                             + "JOIN " + tablePrefix + "cooldowns c ON (u.id = c.user_id) "
                             + "JOIN " + tablePrefix + "cooldowns c ON (u.id = c.user_id) "
                             + "JOIN " + tablePrefix + "huds h ON (u.id = h.user_id) "
                             + "JOIN " + tablePrefix + "huds h ON (u.id = h.user_id) "
-                            + "WHERE u.id = ?");
+                            + "WHERE u.id = ?"
+            );
             statement.setInt(1, id);
             statement.setInt(1, id);
 
 
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
@@ -640,7 +670,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             if (resultSet.next()) {
             if (resultSet.next()) {
                 try {
                 try {
                     PlayerProfile profile = loadFromResult(playerName, resultSet);
                     PlayerProfile profile = loadFromResult(playerName, resultSet);
-                    String name = resultSet.getString(42); // TODO: Magic Number, make sure it stays updated
+                    String name = resultSet.getString(MAGIC_NUMBER); // TODO: Magic Number, make sure it stays updated
                     resultSet.close();
                     resultSet.close();
                     statement.close();
                     statement.close();
 
 
@@ -650,15 +680,15 @@ public final class SQLDatabaseManager implements DatabaseManager {
                             && uuid != null) {
                             && uuid != null) {
                         statement = connection.prepareStatement(
                         statement = connection.prepareStatement(
                                 "UPDATE `" + tablePrefix + "users` "
                                 "UPDATE `" + tablePrefix + "users` "
-                                        + "SET user = ? "
-                                        + "WHERE user = ?");
+                                        + "SET `user` = ? "
+                                        + "WHERE `user` = ?");
                         statement.setString(1, "_INVALID_OLD_USERNAME_");
                         statement.setString(1, "_INVALID_OLD_USERNAME_");
                         statement.setString(2, name);
                         statement.setString(2, name);
                         statement.executeUpdate();
                         statement.executeUpdate();
                         statement.close();
                         statement.close();
                         statement = connection.prepareStatement(
                         statement = connection.prepareStatement(
                                 "UPDATE `" + tablePrefix + "users` "
                                 "UPDATE `" + tablePrefix + "users` "
-                                        + "SET user = ?, uuid = ? "
+                                        + "SET `user` = ?, uuid = ? "
                                         + "WHERE id = ?");
                                         + "WHERE id = ?");
                         statement.setString(1, playerName);
                         statement.setString(1, playerName);
                         statement.setString(2, uuid.toString());
                         statement.setString(2, uuid.toString());
@@ -706,7 +736,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                             + "JOIN " + tablePrefix + "experience e ON (u.id = e.user_id) "
                             + "JOIN " + tablePrefix + "experience e ON (u.id = e.user_id) "
                             + "JOIN " + tablePrefix + "cooldowns c ON (u.id = c.user_id) "
                             + "JOIN " + tablePrefix + "cooldowns c ON (u.id = c.user_id) "
                             + "JOIN " + tablePrefix + "huds h ON (u.id = h.user_id) "
                             + "JOIN " + tablePrefix + "huds h ON (u.id = h.user_id) "
-                            + "WHERE u.user = ?");
+                            + "WHERE u.`user` = ?");
             List<String> usernames = getStoredUsers();
             List<String> usernames = getStoredUsers();
             int convertedUsers = 0;
             int convertedUsers = 0;
             long startMillis = System.currentTimeMillis();
             long startMillis = System.currentTimeMillis();
@@ -745,7 +775,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             connection = getConnection(PoolIdentifier.MISC);
             connection = getConnection(PoolIdentifier.MISC);
             statement = connection.prepareStatement(
             statement = connection.prepareStatement(
                     "UPDATE `" + tablePrefix + "users` SET "
                     "UPDATE `" + tablePrefix + "users` SET "
-                            + "  uuid = ? WHERE user = ?");
+                            + "  uuid = ? WHERE `user` = ?");
             statement.setString(1, uuid.toString());
             statement.setString(1, uuid.toString());
             statement.setString(2, userName);
             statement.setString(2, userName);
             statement.execute();
             statement.execute();
@@ -769,7 +799,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
 
 
         try {
         try {
             connection = getConnection(PoolIdentifier.MISC);
             connection = getConnection(PoolIdentifier.MISC);
-            statement = connection.prepareStatement("UPDATE " + tablePrefix + "users SET uuid = ? WHERE user = ?");
+            statement = connection.prepareStatement("UPDATE " + tablePrefix + "users SET uuid = ? WHERE `user` = ?");
 
 
             for (Map.Entry<String, UUID> entry : fetchedUUIDs.entrySet()) {
             for (Map.Entry<String, UUID> entry : fetchedUUIDs.entrySet()) {
                 statement.setString(1, entry.getValue().toString());
                 statement.setString(1, entry.getValue().toString());
@@ -811,7 +841,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
         try {
         try {
             connection = getConnection(PoolIdentifier.MISC);
             connection = getConnection(PoolIdentifier.MISC);
             statement = connection.createStatement();
             statement = connection.createStatement();
-            resultSet = statement.executeQuery("SELECT user FROM " + tablePrefix + "users");
+            resultSet = statement.executeQuery("SELECT `user` FROM " + tablePrefix + "users");
             while (resultSet.next()) {
             while (resultSet.next()) {
                 users.add(resultSet.getString("user"));
                 users.add(resultSet.getString("user"));
             }
             }
@@ -832,7 +862,6 @@ public final class SQLDatabaseManager implements DatabaseManager {
      * Checks that the database structure is present and correct
      * Checks that the database structure is present and correct
      */
      */
     private void checkStructure() {
     private void checkStructure() {
-
         PreparedStatement statement = null;
         PreparedStatement statement = null;
         Statement createStatement = null;
         Statement createStatement = null;
         ResultSet resultSet = null;
         ResultSet resultSet = null;
@@ -840,27 +869,30 @@ public final class SQLDatabaseManager implements DatabaseManager {
 
 
         try {
         try {
             connection = getConnection(PoolIdentifier.MISC);
             connection = getConnection(PoolIdentifier.MISC);
-            statement = connection.prepareStatement("SELECT table_name FROM INFORMATION_SCHEMA.TABLES"
-                    + " WHERE table_schema = ?"
-                    + " AND table_name = ?");
-            statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName());
-            statement.setString(2, tablePrefix + "users");
+            String schemaQuery = this.h2 ? "SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_name = ?"
+                    : "SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = ? AND table_name = ?";
+
+            statement = connection.prepareStatement(schemaQuery);
+
+            setStatementQuery(statement, "users");
+
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
+
             if (!resultSet.next()) {
             if (!resultSet.next()) {
                 createStatement = connection.createStatement();
                 createStatement = connection.createStatement();
-                createStatement.executeUpdate("CREATE TABLE IF NOT EXISTS `" + tablePrefix + "users` ("
-                    + "`id` int(10) unsigned NOT NULL AUTO_INCREMENT,"
-                    + "`user` varchar(40) NOT NULL,"
-                    + "`uuid` varchar(36) NULL DEFAULT NULL,"
-                    + "`lastlogin` int(32) unsigned NOT NULL,"
-                    + "PRIMARY KEY (`id`),"
-                    + "INDEX(`user`(20) ASC),"
-                    + "UNIQUE KEY `uuid` (`uuid`)) DEFAULT CHARSET=" + CHARSET_SQL + " AUTO_INCREMENT=1;");
+                String sql = "CREATE TABLE IF NOT EXISTS `" + tablePrefix + "users` (" +
+                        "`id` int AUTO_INCREMENT," +
+                        "`user` varchar(40) NOT NULL," +
+                        "`uuid` varchar(36)," +
+                        "`lastlogin` bigint NOT NULL," +
+                        "PRIMARY KEY (`id`)," +
+                        "INDEX `user_index`(`user`)," +
+                        "UNIQUE(`uuid`))";
+                createStatement.executeUpdate(sql);
                 tryClose(createStatement);
                 tryClose(createStatement);
             }
             }
             tryClose(resultSet);
             tryClose(resultSet);
-            statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName());
-            statement.setString(2, tablePrefix + "huds");
+            setStatementQuery(statement, "huds");
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
             if (!resultSet.next()) {
             if (!resultSet.next()) {
                 createStatement = connection.createStatement();
                 createStatement = connection.createStatement();
@@ -873,8 +905,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                 tryClose(createStatement);
                 tryClose(createStatement);
             }
             }
             tryClose(resultSet);
             tryClose(resultSet);
-            statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName());
-            statement.setString(2, tablePrefix + "cooldowns");
+            setStatementQuery(statement, "cooldowns");
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
             if (!resultSet.next()) {
             if (!resultSet.next()) {
                 createStatement = connection.createStatement();
                 createStatement = connection.createStatement();
@@ -893,13 +924,14 @@ public final class SQLDatabaseManager implements DatabaseManager {
                         + "`acrobatics` int(32) unsigned NOT NULL DEFAULT '0',"
                         + "`acrobatics` int(32) unsigned NOT NULL DEFAULT '0',"
                         + "`blast_mining` int(32) unsigned NOT NULL DEFAULT '0',"
                         + "`blast_mining` int(32) unsigned NOT NULL DEFAULT '0',"
                         + "`chimaera_wing` int(32) unsigned NOT NULL DEFAULT '0',"
                         + "`chimaera_wing` int(32) unsigned NOT NULL DEFAULT '0',"
+                        + "`crossbows` int(32) unsigned NOT NULL DEFAULT '0',"
+                        + "`tridents` int(32) unsigned NOT NULL DEFAULT '0',"
                         + "PRIMARY KEY (`user_id`)) "
                         + "PRIMARY KEY (`user_id`)) "
                         + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                         + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                 tryClose(createStatement);
                 tryClose(createStatement);
             }
             }
             tryClose(resultSet);
             tryClose(resultSet);
-            statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName());
-            statement.setString(2, tablePrefix + "skills");
+            setStatementQuery(statement, "skills");
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
             if (!resultSet.next()) {
             if (!resultSet.next()) {
                 String startingLevel = "'" + mcMMO.p.getAdvancedConfig().getStartingLevel() + "'";
                 String startingLevel = "'" + mcMMO.p.getAdvancedConfig().getStartingLevel() + "'";
@@ -920,14 +952,15 @@ public final class SQLDatabaseManager implements DatabaseManager {
                         + "`acrobatics` int(10) unsigned NOT NULL DEFAULT "+startingLevel+","
                         + "`acrobatics` int(10) unsigned NOT NULL DEFAULT "+startingLevel+","
                         + "`fishing` int(10) unsigned NOT NULL DEFAULT "+startingLevel+","
                         + "`fishing` int(10) unsigned NOT NULL DEFAULT "+startingLevel+","
                         + "`alchemy` int(10) unsigned NOT NULL DEFAULT "+startingLevel+","
                         + "`alchemy` int(10) unsigned NOT NULL DEFAULT "+startingLevel+","
+                        + "`crossbows` int(10) unsigned NOT NULL DEFAULT "+startingLevel+","
+                        + "`tridents` int(10) unsigned NOT NULL DEFAULT "+startingLevel+","
                         + "`total` int(10) unsigned NOT NULL DEFAULT "+totalLevel+","
                         + "`total` int(10) unsigned NOT NULL DEFAULT "+totalLevel+","
                         + "PRIMARY KEY (`user_id`)) "
                         + "PRIMARY KEY (`user_id`)) "
                         + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                         + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                 tryClose(createStatement);
                 tryClose(createStatement);
             }
             }
             tryClose(resultSet);
             tryClose(resultSet);
-            statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName());
-            statement.setString(2, tablePrefix + "experience");
+            setStatementQuery(statement, "experience");
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
             if (!resultSet.next()) {
             if (!resultSet.next()) {
                 createStatement = connection.createStatement();
                 createStatement = connection.createStatement();
@@ -946,6 +979,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
                         + "`acrobatics` int(10) unsigned NOT NULL DEFAULT '0',"
                         + "`acrobatics` int(10) unsigned NOT NULL DEFAULT '0',"
                         + "`fishing` int(10) unsigned NOT NULL DEFAULT '0',"
                         + "`fishing` int(10) unsigned NOT NULL DEFAULT '0',"
                         + "`alchemy` int(10) unsigned NOT NULL DEFAULT '0',"
                         + "`alchemy` int(10) unsigned NOT NULL DEFAULT '0',"
+                        + "`crossbows` int(10) unsigned NOT NULL DEFAULT '0',"
+                        + "`tridents` int(10) unsigned NOT NULL DEFAULT '0',"
                         + "PRIMARY KEY (`user_id`)) "
                         + "PRIMARY KEY (`user_id`)) "
                         + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                         + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                 tryClose(createStatement);
                 tryClose(createStatement);
@@ -968,7 +1003,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
                 }
                 }
             }
             }
 
 
-            mcMMO.p.getLogger().info("Killing orphans");
+            // TODO: refactor
+            LogUtils.debug(logger, "Killing orphans");
             createStatement = connection.createStatement();
             createStatement = connection.createStatement();
             createStatement.executeUpdate("DELETE FROM `" + tablePrefix + "experience` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "users` `u` WHERE `" + tablePrefix + "experience`.`user_id` = `u`.`id`)");
             createStatement.executeUpdate("DELETE FROM `" + tablePrefix + "experience` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "users` `u` WHERE `" + tablePrefix + "experience`.`user_id` = `u`.`id`)");
             createStatement.executeUpdate("DELETE FROM `" + tablePrefix + "huds` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "users` `u` WHERE `" + tablePrefix + "huds`.`user_id` = `u`.`id`)");
             createStatement.executeUpdate("DELETE FROM `" + tablePrefix + "huds` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "users` `u` WHERE `" + tablePrefix + "huds`.`user_id` = `u`.`id`)");
@@ -985,21 +1021,53 @@ public final class SQLDatabaseManager implements DatabaseManager {
             tryClose(connection);
             tryClose(connection);
         }
         }
 
 
+        String skills = "skills";
+        String crossbows = "crossbows";
+        String tridents = "tridents";
+        String experience = "experience";
+        String cooldowns = "cooldowns";
+
+        updateStructure(skills, crossbows, String.valueOf(32));
+        updateStructure(skills, tridents, String.valueOf(32));
+
+        updateStructure(experience, crossbows, String.valueOf(10));
+        updateStructure(experience, tridents, String.valueOf(10));
+
+        updateStructure(cooldowns, crossbows, String.valueOf(10));
+        updateStructure(cooldowns, tridents, String.valueOf(10));
     }
     }
 
 
-    private Connection getConnection(PoolIdentifier identifier) throws SQLException {
-        Connection connection = null;
-        switch (identifier) {
-            case LOAD:
-                connection = loadPool.getConnection();
-                break;
-            case MISC:
-                connection = miscPool.getConnection();
-                break;
-            case SAVE:
-                connection = savePool.getConnection();
-                break;
+    private void updateStructure(String tableName, String columnName, String columnSize) {
+        try (Connection connection = getConnection(PoolIdentifier.MISC);
+             Statement createStatement = connection.createStatement()) {
+
+            String startingLevel = "'" + mcMMO.p.getAdvancedConfig().getStartingLevel() + "'";
+            createStatement.executeUpdate("ALTER TABLE `" + tablePrefix + tableName + "` "
+                    + "ADD COLUMN IF NOT EXISTS `" + columnName + "` int(" + columnSize + ") unsigned NOT NULL DEFAULT " + startingLevel);
+
+        } catch (SQLException e) {
+            e.printStackTrace(); // Consider more robust logging
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void setStatementQuery(PreparedStatement statement, String tableName) throws SQLException {
+        if (!this.h2) {
+            // Set schema name for MySQL
+            statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName());
+            statement.setString(2, tablePrefix + tableName);
+        } else {
+            // For H2, the schema parameter is not needed
+            statement.setString(1, tablePrefix + tableName);
         }
         }
+    }
+
+    Connection getConnection(PoolIdentifier identifier) throws SQLException {
+        Connection connection = switch (identifier) {
+            case LOAD -> loadPool.getConnection();
+            case MISC -> miscPool.getConnection();
+            case SAVE -> savePool.getConnection();
+        };
         if (connection == null) {
         if (connection == null) {
             throw new RuntimeException("getConnection() for " + identifier.name().toLowerCase(Locale.ENGLISH) + " pool timed out.  Increase max connections settings.");
             throw new RuntimeException("getConnection() for " + identifier.name().toLowerCase(Locale.ENGLISH) + " pool timed out.  Increase max connections settings.");
         }
         }
@@ -1012,8 +1080,9 @@ public final class SQLDatabaseManager implements DatabaseManager {
      * @param upgrade Upgrade to attempt to apply
      * @param upgrade Upgrade to attempt to apply
      */
      */
     private void checkDatabaseStructure(Connection connection, UpgradeType upgrade) {
     private void checkDatabaseStructure(Connection connection, UpgradeType upgrade) {
+        // TODO: Rewrite / Refactor
         if (!mcMMO.getUpgradeManager().shouldUpgrade(upgrade)) {
         if (!mcMMO.getUpgradeManager().shouldUpgrade(upgrade)) {
-            LogUtils.debug(mcMMO.p.getLogger(), "Skipping " + upgrade.name() + " upgrade (unneeded)");
+            LogUtils.debug(logger, "Skipping " + upgrade.name() + " upgrade (unneeded)");
             return;
             return;
         }
         }
 
 
@@ -1132,9 +1201,9 @@ public final class SQLDatabaseManager implements DatabaseManager {
 
 
         final int OFFSET_SKILLS = 0; // TODO update these numbers when the query
         final int OFFSET_SKILLS = 0; // TODO update these numbers when the query
         // changes (a new skill is added)
         // changes (a new skill is added)
-        final int OFFSET_XP = 13;
-        final int OFFSET_DATS = 26;
-        final int OFFSET_OTHER = 39;
+        final int OFFSET_XP = 15;
+        final int OFFSET_DATS = 28;
+        final int OFFSET_OTHER = 41;
 
 
         skills.put(PrimarySkillType.TAMING, result.getInt(OFFSET_SKILLS + 1));
         skills.put(PrimarySkillType.TAMING, result.getInt(OFFSET_SKILLS + 1));
         skills.put(PrimarySkillType.MINING, result.getInt(OFFSET_SKILLS + 2));
         skills.put(PrimarySkillType.MINING, result.getInt(OFFSET_SKILLS + 2));
@@ -1149,6 +1218,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
         skills.put(PrimarySkillType.ACROBATICS, result.getInt(OFFSET_SKILLS + 11));
         skills.put(PrimarySkillType.ACROBATICS, result.getInt(OFFSET_SKILLS + 11));
         skills.put(PrimarySkillType.FISHING, result.getInt(OFFSET_SKILLS + 12));
         skills.put(PrimarySkillType.FISHING, result.getInt(OFFSET_SKILLS + 12));
         skills.put(PrimarySkillType.ALCHEMY, result.getInt(OFFSET_SKILLS + 13));
         skills.put(PrimarySkillType.ALCHEMY, result.getInt(OFFSET_SKILLS + 13));
+        skills.put(PrimarySkillType.CROSSBOWS, result.getInt(OFFSET_SKILLS + 14));
+        skills.put(PrimarySkillType.TRIDENTS, result.getInt(OFFSET_SKILLS + 15));
 
 
         skillsXp.put(PrimarySkillType.TAMING, result.getFloat(OFFSET_XP + 1));
         skillsXp.put(PrimarySkillType.TAMING, result.getFloat(OFFSET_XP + 1));
         skillsXp.put(PrimarySkillType.MINING, result.getFloat(OFFSET_XP + 2));
         skillsXp.put(PrimarySkillType.MINING, result.getFloat(OFFSET_XP + 2));
@@ -1163,6 +1234,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
         skillsXp.put(PrimarySkillType.ACROBATICS, result.getFloat(OFFSET_XP + 11));
         skillsXp.put(PrimarySkillType.ACROBATICS, result.getFloat(OFFSET_XP + 11));
         skillsXp.put(PrimarySkillType.FISHING, result.getFloat(OFFSET_XP + 12));
         skillsXp.put(PrimarySkillType.FISHING, result.getFloat(OFFSET_XP + 12));
         skillsXp.put(PrimarySkillType.ALCHEMY, result.getFloat(OFFSET_XP + 13));
         skillsXp.put(PrimarySkillType.ALCHEMY, result.getFloat(OFFSET_XP + 13));
+        skillsXp.put(PrimarySkillType.CROSSBOWS, result.getFloat(OFFSET_XP + 14));
+        skillsXp.put(PrimarySkillType.TRIDENTS, result.getFloat(OFFSET_XP + 15));
 
 
         // Taming - Unused - result.getInt(OFFSET_DATS + 1)
         // Taming - Unused - result.getInt(OFFSET_DATS + 1)
         skillsDATS.put(SuperAbilityType.SUPER_BREAKER, result.getInt(OFFSET_DATS + 2));
         skillsDATS.put(SuperAbilityType.SUPER_BREAKER, result.getInt(OFFSET_DATS + 2));
@@ -1177,6 +1250,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
         // Acrobatics - Unused - result.getInt(OFFSET_DATS + 11)
         // Acrobatics - Unused - result.getInt(OFFSET_DATS + 11)
         skillsDATS.put(SuperAbilityType.BLAST_MINING, result.getInt(OFFSET_DATS + 12));
         skillsDATS.put(SuperAbilityType.BLAST_MINING, result.getInt(OFFSET_DATS + 12));
         uniqueData.put(UniqueDataType.CHIMAERA_WING_DATS, result.getInt(OFFSET_DATS + 13));
         uniqueData.put(UniqueDataType.CHIMAERA_WING_DATS, result.getInt(OFFSET_DATS + 13));
+        skillsDATS.put(SuperAbilityType.SUPER_SHOTGUN, result.getInt(OFFSET_DATS + 14));
+        skillsDATS.put(SuperAbilityType.TRIDENTS_SUPER_ABILITY, result.getInt(OFFSET_DATS + 15));
 
 
         try {
         try {
             scoreboardTipsShown = result.getInt(OFFSET_OTHER + 2);
             scoreboardTipsShown = result.getInt(OFFSET_OTHER + 2);
@@ -1196,17 +1271,21 @@ public final class SQLDatabaseManager implements DatabaseManager {
     }
     }
 
 
     private void printErrors(SQLException ex) {
     private void printErrors(SQLException ex) {
-        if (debug) {
-            ex.printStackTrace();
-        }
+        ex.printStackTrace();
 
 
-        StackTraceElement element = ex.getStackTrace()[0];
-        mcMMO.p.getLogger().severe("Location: " + element.getClassName() + " " + element.getMethodName() + " " + element.getLineNumber());
-        mcMMO.p.getLogger().severe("SQLException: " + ex.getMessage());
-        mcMMO.p.getLogger().severe("SQLState: " + ex.getSQLState());
-        mcMMO.p.getLogger().severe("VendorError: " + ex.getErrorCode());
+        // logger.severe("SQLException: " + ex.getMessage());
+        logger.severe("SQLState: " + ex.getSQLState());
+        logger.severe("VendorError: " + ex.getErrorCode());
+
+        // Handling SQLException chain
+        SQLException nextException = ex.getNextException();
+        while (nextException != null) {
+            logger.severe("Caused by: " + nextException.getMessage());
+            nextException = nextException.getNextException();
+        }
     }
     }
 
 
+
     public DatabaseType getDatabaseType() {
     public DatabaseType getDatabaseType() {
         return DatabaseType.SQL;
         return DatabaseType.SQL;
     }
     }
@@ -1222,7 +1301,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                 return;
                 return;
             }
             }
             resultSet.close();
             resultSet.close();
-            mcMMO.p.getLogger().info("Updating mcMMO MySQL tables to drop name uniqueness...");
+            logger.info("Updating mcMMO MySQL tables to drop name uniqueness...");
             statement.execute("ALTER TABLE `" + tablePrefix + "users` " 
             statement.execute("ALTER TABLE `" + tablePrefix + "users` " 
                     + "DROP INDEX `user`,"
                     + "DROP INDEX `user`,"
                     + "ADD INDEX `user` (`user`(20) ASC)");
                     + "ADD INDEX `user` (`user`(20) ASC)");
@@ -1240,7 +1319,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_ALCHEMY);
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_ALCHEMY);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
-            mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Alchemy...");
+            logger.info("Updating mcMMO MySQL tables for Alchemy...");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD `alchemy` int(10) NOT NULL DEFAULT '0'");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD `alchemy` int(10) NOT NULL DEFAULT '0'");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "experience` ADD `alchemy` int(10) NOT NULL DEFAULT '0'");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "experience` ADD `alchemy` int(10) NOT NULL DEFAULT '0'");
         }
         }
@@ -1252,7 +1331,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_BLAST_MINING_COOLDOWN);
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_BLAST_MINING_COOLDOWN);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
-            mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Blast Mining...");
+            logger.info("Updating mcMMO MySQL tables for Blast Mining...");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "cooldowns` ADD `blast_mining` int(32) NOT NULL DEFAULT '0'");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "cooldowns` ADD `blast_mining` int(32) NOT NULL DEFAULT '0'");
         }
         }
     }
     }
@@ -1263,7 +1342,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_UNIQUE_PLAYER_DATA);
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_UNIQUE_PLAYER_DATA);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
-            mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Chimaera Wing...");
+            logger.info("Updating mcMMO MySQL tables for Chimaera Wing...");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "cooldowns` ADD `chimaera_wing` int(32) NOT NULL DEFAULT '0'");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "cooldowns` ADD `chimaera_wing` int(32) NOT NULL DEFAULT '0'");
         }
         }
     }
     }
@@ -1274,7 +1353,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_FISHING);
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_FISHING);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
-            mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Fishing...");
+            logger.info("Updating mcMMO MySQL tables for Fishing...");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD `fishing` int(10) NOT NULL DEFAULT '0'");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD `fishing` int(10) NOT NULL DEFAULT '0'");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "experience` ADD `fishing` int(10) NOT NULL DEFAULT '0'");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "experience` ADD `fishing` int(10) NOT NULL DEFAULT '0'");
         }
         }
@@ -1286,7 +1365,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_MOB_HEALTHBARS);
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_MOB_HEALTHBARS);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
-            mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for mob healthbars...");
+            logger.info("Updating mcMMO MySQL tables for mob healthbars...");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` ADD `mobhealthbar` varchar(50) NOT NULL DEFAULT '" + mcMMO.p.getGeneralConfig().getMobHealthbarDefault() + "'");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` ADD `mobhealthbar` varchar(50) NOT NULL DEFAULT '" + mcMMO.p.getGeneralConfig().getMobHealthbarDefault() + "'");
         }
         }
     }
     }
@@ -1297,7 +1376,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_SCOREBOARD_TIPS);
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_SCOREBOARD_TIPS);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
-            mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for scoreboard tips...");
+            logger.info("Updating mcMMO MySQL tables for scoreboard tips...");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` ADD `scoreboardtips` int(10) NOT NULL DEFAULT '0' ;");
             statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` ADD `scoreboardtips` int(10) NOT NULL DEFAULT '0' ;");
         }
         }
     }
     }
@@ -1310,7 +1389,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             resultSet.last();
             resultSet.last();
 
 
             if (resultSet.getRow() != SkillTools.NON_CHILD_SKILLS.size()) {
             if (resultSet.getRow() != SkillTools.NON_CHILD_SKILLS.size()) {
-                mcMMO.p.getLogger().info("Indexing tables, this may take a while on larger databases");
+                logger.info("Indexing tables, this may take a while on larger databases");
 
 
                 for (PrimarySkillType skill : SkillTools.NON_CHILD_SKILLS) {
                 for (PrimarySkillType skill : SkillTools.NON_CHILD_SKILLS) {
                     String skill_name = skill.name().toLowerCase(Locale.ENGLISH);
                     String skill_name = skill.name().toLowerCase(Locale.ENGLISH);
@@ -1351,7 +1430,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             }
             }
 
 
             if (!column_exists) {
             if (!column_exists) {
-                mcMMO.p.getLogger().info("Adding UUIDs to mcMMO MySQL user table...");
+                logger.info("Adding UUIDs to mcMMO MySQL user table...");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` ADD `uuid` varchar(36) NULL DEFAULT NULL");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` ADD `uuid` varchar(36) NULL DEFAULT NULL");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` ADD UNIQUE INDEX `uuid` (`uuid`) USING BTREE");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` ADD UNIQUE INDEX `uuid` (`uuid`) USING BTREE");
 
 
@@ -1420,7 +1499,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             }
             }
 
 
             if (column_exists) {
             if (column_exists) {
-                mcMMO.p.getLogger().info("Removing party name from users table...");
+                logger.info("Removing party name from users table...");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` DROP COLUMN `party`");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` DROP COLUMN `party`");
             }
             }
 
 
@@ -1454,7 +1533,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             }
             }
 
 
             if (!column_exists) {
             if (!column_exists) {
-                mcMMO.p.getLogger().info("Adding skill total column to skills table...");
+                logger.info("Adding skill total column to skills table...");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD COLUMN `total` int NOT NULL DEFAULT '0'");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD COLUMN `total` int NOT NULL DEFAULT '0'");
                 statement.executeUpdate("UPDATE `" + tablePrefix + "skills` SET `total` = (taming+mining+woodcutting+repair+unarmed+herbalism+excavation+archery+swords+axes+acrobatics+fishing+alchemy)");
                 statement.executeUpdate("UPDATE `" + tablePrefix + "skills` SET `total` = (taming+mining+woodcutting+repair+unarmed+herbalism+excavation+archery+swords+axes+acrobatics+fishing+alchemy)");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD INDEX `idx_total` (`total`) USING BTREE");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD INDEX `idx_total` (`total`) USING BTREE");
@@ -1490,7 +1569,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             }
             }
 
 
             if (column_exists) {
             if (column_exists) {
-                mcMMO.p.getLogger().info("Removing Spout HUD type from huds table...");
+                logger.info("Removing Spout HUD type from huds table...");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` DROP COLUMN `hudtype`");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` DROP COLUMN `hudtype`");
             }
             }
 
 
@@ -1515,7 +1594,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
         PreparedStatement statement = null;
         PreparedStatement statement = null;
 
 
         try {
         try {
-            statement = connection.prepareStatement("SELECT id, user FROM " + tablePrefix + "users WHERE uuid = ? OR (uuid IS NULL AND user = ?)");
+            statement = connection.prepareStatement("SELECT id, `user` FROM " + tablePrefix + "users WHERE uuid = ? OR (uuid IS NULL AND `user` = ?)");
             statement.setString(1, uuid.toString());
             statement.setString(1, uuid.toString());
             statement.setString(2, playerName);
             statement.setString(2, playerName);
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
@@ -1544,7 +1623,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
         PreparedStatement statement = null;
         PreparedStatement statement = null;
 
 
         try {
         try {
-            statement = connection.prepareStatement("SELECT id, user FROM " + tablePrefix + "users WHERE user = ?");
+            statement = connection.prepareStatement("SELECT id, `user` FROM " + tablePrefix + "users WHERE `user` = ?");
             statement.setString(1, playerName);
             statement.setString(1, playerName);
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
 
 
@@ -1577,7 +1656,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
 
 
     @Override
     @Override
     public void onDisable() {
     public void onDisable() {
-        LogUtils.debug(mcMMO.p.getLogger(), "Releasing connection pool resource...");
+        LogUtils.debug(logger, "Releasing connection pool resource...");
         miscPool.close();
         miscPool.close();
         loadPool.close();
         loadPool.close();
         savePool.close();
         savePool.close();
@@ -1619,19 +1698,19 @@ public final class SQLDatabaseManager implements DatabaseManager {
          */
          */
 
 
         //Alter users table
         //Alter users table
-        mcMMO.p.getLogger().info("SQL Converting tables from latin1 to utf8mb4");
+        logger.info("SQL Converting tables from latin1 to utf8mb4");
 
 
         //Update "user" column
         //Update "user" column
         try {
         try {
-        mcMMO.p.getLogger().info("Updating user column to new encoding");
+        logger.info("Updating user column to new encoding");
         statement.executeUpdate(getUpdateUserInUsersTableSQLQuery());
         statement.executeUpdate(getUpdateUserInUsersTableSQLQuery());
 
 
         //Update "uuid" column
         //Update "uuid" column
-        mcMMO.p.getLogger().info("Updating user column to new encoding");
+        logger.info("Updating user column to new encoding");
         statement.executeUpdate(getUpdateUUIDInUsersTableSQLQuery());
         statement.executeUpdate(getUpdateUUIDInUsersTableSQLQuery());
 
 
         //Update "mobhealthbar" column
         //Update "mobhealthbar" column
-        mcMMO.p.getLogger().info("Updating mobhealthbar column to new encoding");
+        logger.info("Updating mobhealthbar column to new encoding");
         statement.executeUpdate(getUpdateMobHealthBarInHudsTableSQLQuery());
         statement.executeUpdate(getUpdateMobHealthBarInHudsTableSQLQuery());
 
 
         mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.SQL_CHARSET_UTF8MB4);
         mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.SQL_CHARSET_UTF8MB4);
@@ -1645,7 +1724,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
     private String getUpdateUserInUsersTableSQLQuery() {
     private String getUpdateUserInUsersTableSQLQuery() {
         return "ALTER TABLE\n" +
         return "ALTER TABLE\n" +
                 "    " + tablePrefix + "users\n" +
                 "    " + tablePrefix + "users\n" +
-                "    CHANGE user user\n" +
+                "    CHANGE `user` user\n" +
                 "    " + USER_VARCHAR + "\n" +
                 "    " + USER_VARCHAR + "\n" +
                 "    CHARACTER SET utf8mb4\n" +
                 "    CHARACTER SET utf8mb4\n" +
                 "    COLLATE utf8mb4_unicode_ci;";
                 "    COLLATE utf8mb4_unicode_ci;";
@@ -1670,4 +1749,28 @@ public final class SQLDatabaseManager implements DatabaseManager {
                 "    CHARACTER SET utf8mb4\n" +
                 "    CHARACTER SET utf8mb4\n" +
                 "    COLLATE utf8mb4_unicode_ci;";
                 "    COLLATE utf8mb4_unicode_ci;";
     }
     }
+
+    public void printAllTablesWithColumns(Connection connection) {
+        try {
+            DatabaseMetaData metaData = connection.getMetaData();
+            String[] types = {"TABLE"};
+            ResultSet tables = metaData.getTables(null, null, "%", types);
+
+            while (tables.next()) {
+                String tableName = tables.getString("TABLE_NAME");
+                System.out.println("Table: " + tableName);
+
+                ResultSet columns = metaData.getColumns(null, null, tableName, "%");
+                while (columns.next()) {
+                    String columnName = columns.getString("COLUMN_NAME");
+                    String columnType = columns.getString("TYPE_NAME");
+                    System.out.println("  Column: " + columnName + " Type: " + columnType);
+                }
+                columns.close();
+            }
+            tables.close();
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+    }
 }
 }

+ 7 - 0
src/main/java/com/gmail/nossr50/database/flatfile/FlatFileDataUtil.java

@@ -79,6 +79,8 @@ public class FlatFileDataUtil {
             case SKILLS_TAMING:
             case SKILLS_TAMING:
             case SKILLS_FISHING:
             case SKILLS_FISHING:
             case SKILLS_ALCHEMY:
             case SKILLS_ALCHEMY:
+            case SKILLS_CROSSBOWS:
+            case SKILLS_TRIDENTS:
                 return String.valueOf(startingLevel);
                 return String.valueOf(startingLevel);
             case OVERHAUL_LAST_LOGIN:
             case OVERHAUL_LAST_LOGIN:
                 return String.valueOf(-1L);
                 return String.valueOf(-1L);
@@ -90,6 +92,9 @@ public class FlatFileDataUtil {
             case COOLDOWN_SKULL_SPLITTER:
             case COOLDOWN_SKULL_SPLITTER:
             case COOLDOWN_SUPER_BREAKER:
             case COOLDOWN_SUPER_BREAKER:
             case COOLDOWN_BLAST_MINING:
             case COOLDOWN_BLAST_MINING:
+            case COOLDOWN_SUPER_SHOTGUN:
+            case COOLDOWN_TRIDENTS:
+            case COOLDOWN_ARCHERY:
             case SCOREBOARD_TIPS:
             case SCOREBOARD_TIPS:
             case COOLDOWN_CHIMAERA_WING:
             case COOLDOWN_CHIMAERA_WING:
             case EXP_MINING:
             case EXP_MINING:
@@ -105,6 +110,8 @@ public class FlatFileDataUtil {
             case EXP_TAMING:
             case EXP_TAMING:
             case EXP_FISHING:
             case EXP_FISHING:
             case EXP_ALCHEMY:
             case EXP_ALCHEMY:
+            case EXP_CROSSBOWS:
+            case EXP_TRIDENTS:
                 return "0";
                 return "0";
             case UUID_INDEX:
             case UUID_INDEX:
                 throw new IndexOutOfBoundsException(); //TODO: Add UUID recovery? Might not even be worth it.
                 throw new IndexOutOfBoundsException(); //TODO: Add UUID recovery? Might not even be worth it.

+ 0 - 1
src/main/java/com/gmail/nossr50/datatypes/party/Party.java

@@ -6,7 +6,6 @@ import com.gmail.nossr50.datatypes.experience.FormulaType;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.sounds.SoundManager;
 import com.gmail.nossr50.util.sounds.SoundManager;

+ 19 - 15
src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java

@@ -20,7 +20,6 @@ import com.gmail.nossr50.datatypes.skills.ToolType;
 import com.gmail.nossr50.events.experience.McMMOPlayerPreXpGainEvent;
 import com.gmail.nossr50.events.experience.McMMOPlayerPreXpGainEvent;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.party.ShareHandler;
 import com.gmail.nossr50.party.ShareHandler;
 import com.gmail.nossr50.runnables.skills.AbilityDisableTask;
 import com.gmail.nossr50.runnables.skills.AbilityDisableTask;
 import com.gmail.nossr50.runnables.skills.ToolLowerTask;
 import com.gmail.nossr50.runnables.skills.ToolLowerTask;
@@ -29,7 +28,7 @@ import com.gmail.nossr50.skills.acrobatics.AcrobaticsManager;
 import com.gmail.nossr50.skills.alchemy.AlchemyManager;
 import com.gmail.nossr50.skills.alchemy.AlchemyManager;
 import com.gmail.nossr50.skills.archery.ArcheryManager;
 import com.gmail.nossr50.skills.archery.ArcheryManager;
 import com.gmail.nossr50.skills.axes.AxesManager;
 import com.gmail.nossr50.skills.axes.AxesManager;
-import com.gmail.nossr50.skills.child.FamilyTree;
+import com.gmail.nossr50.skills.crossbows.CrossbowsManager;
 import com.gmail.nossr50.skills.excavation.ExcavationManager;
 import com.gmail.nossr50.skills.excavation.ExcavationManager;
 import com.gmail.nossr50.skills.fishing.FishingManager;
 import com.gmail.nossr50.skills.fishing.FishingManager;
 import com.gmail.nossr50.skills.herbalism.HerbalismManager;
 import com.gmail.nossr50.skills.herbalism.HerbalismManager;
@@ -39,6 +38,7 @@ import com.gmail.nossr50.skills.salvage.SalvageManager;
 import com.gmail.nossr50.skills.smelting.SmeltingManager;
 import com.gmail.nossr50.skills.smelting.SmeltingManager;
 import com.gmail.nossr50.skills.swords.SwordsManager;
 import com.gmail.nossr50.skills.swords.SwordsManager;
 import com.gmail.nossr50.skills.taming.TamingManager;
 import com.gmail.nossr50.skills.taming.TamingManager;
+import com.gmail.nossr50.skills.tridents.TridentsManager;
 import com.gmail.nossr50.skills.unarmed.UnarmedManager;
 import com.gmail.nossr50.skills.unarmed.UnarmedManager;
 import com.gmail.nossr50.skills.woodcutting.WoodcuttingManager;
 import com.gmail.nossr50.skills.woodcutting.WoodcuttingManager;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.*;
@@ -68,7 +68,6 @@ import org.jetbrains.annotations.Nullable;
 
 
 import java.util.EnumMap;
 import java.util.EnumMap;
 import java.util.Map;
 import java.util.Map;
-import java.util.Set;
 import java.util.UUID;
 import java.util.UUID;
 
 
 public class McMMOPlayer implements Identified {
 public class McMMOPlayer implements Identified {
@@ -181,6 +180,9 @@ public class McMMOPlayer implements Identified {
             case AXES:
             case AXES:
                 skillManagers.put(primarySkillType, new AxesManager(this));
                 skillManagers.put(primarySkillType, new AxesManager(this));
                 break;
                 break;
+            case CROSSBOWS:
+                skillManagers.put(primarySkillType, new CrossbowsManager(this));
+                break;
             case EXCAVATION:
             case EXCAVATION:
                 skillManagers.put(primarySkillType, new ExcavationManager(this));
                 skillManagers.put(primarySkillType, new ExcavationManager(this));
                 break;
                 break;
@@ -208,6 +210,9 @@ public class McMMOPlayer implements Identified {
             case TAMING:
             case TAMING:
                 skillManagers.put(primarySkillType, new TamingManager(this));
                 skillManagers.put(primarySkillType, new TamingManager(this));
                 break;
                 break;
+            case TRIDENTS:
+                skillManagers.put(primarySkillType, new TridentsManager(this));
+                break;
             case UNARMED:
             case UNARMED:
                 skillManagers.put(primarySkillType, new UnarmedManager(this));
                 skillManagers.put(primarySkillType, new UnarmedManager(this));
                 break;
                 break;
@@ -227,15 +232,6 @@ public class McMMOPlayer implements Identified {
         return attackStrength;
         return attackStrength;
     }
     }
 
 
-//    public void setAttackStrength(double attackStrength) {
-//        this.attackStrength = attackStrength;
-//    }
-
-    /*public void hideXpBar(PrimarySkillType primarySkillType)
-    {
-        experienceBarManager.hideExperienceBar(primarySkillType);
-    }*/
-
     public @NotNull PrimarySkillType getLastSkillShownScoreboard() {
     public @NotNull PrimarySkillType getLastSkillShownScoreboard() {
         return lastSkillShownScoreboard;
         return lastSkillShownScoreboard;
     }
     }
@@ -308,6 +304,13 @@ public class McMMOPlayer implements Identified {
     public AxesManager getAxesManager() {
     public AxesManager getAxesManager() {
         return (AxesManager) skillManagers.get(PrimarySkillType.AXES);
         return (AxesManager) skillManagers.get(PrimarySkillType.AXES);
     }
     }
+    public CrossbowsManager getCrossbowsManager() {
+        return (CrossbowsManager) skillManagers.get(PrimarySkillType.CROSSBOWS);
+    }
+
+    public TridentsManager getTridentsManager() {
+        return (TridentsManager) skillManagers.get(PrimarySkillType.TRIDENTS);
+    }
 
 
     public ExcavationManager getExcavationManager() {
     public ExcavationManager getExcavationManager() {
         return (ExcavationManager) skillManagers.get(PrimarySkillType.EXCAVATION);
         return (ExcavationManager) skillManagers.get(PrimarySkillType.EXCAVATION);
@@ -384,6 +387,7 @@ public class McMMOPlayer implements Identified {
      * @param isActive True if the ability is active, false otherwise
      * @param isActive True if the ability is active, false otherwise
      */
      */
     public void setAbilityMode(SuperAbilityType ability, boolean isActive) {
     public void setAbilityMode(SuperAbilityType ability, boolean isActive) {
+        // TODO: This should reject "one and done" type abilities
         abilityMode.put(ability, isActive);
         abilityMode.put(ability, isActive);
     }
     }
 
 
@@ -611,7 +615,7 @@ public class McMMOPlayer implements Identified {
         }
         }
 
 
         if (SkillTools.isChildSkill(skill)) {
         if (SkillTools.isChildSkill(skill)) {
-            Set<PrimarySkillType> parentSkills = FamilyTree.getParents(skill);
+            var parentSkills = mcMMO.p.getSkillTools().getChildSkillParents(skill);
             float splitXp = xp / parentSkills.size();
             float splitXp = xp / parentSkills.size();
 
 
             for (PrimarySkillType parentSkill : parentSkills) {
             for (PrimarySkillType parentSkill : parentSkills) {
@@ -668,7 +672,7 @@ public class McMMOPlayer implements Identified {
         xp = mcMMOPlayerPreXpGainEvent.getXpGained();
         xp = mcMMOPlayerPreXpGainEvent.getXpGained();
 
 
         if (SkillTools.isChildSkill(primarySkillType)) {
         if (SkillTools.isChildSkill(primarySkillType)) {
-            Set<PrimarySkillType> parentSkills = FamilyTree.getParents(primarySkillType);
+            var parentSkills = mcMMO.p.getSkillTools().getChildSkillParents(primarySkillType);
 
 
             for (PrimarySkillType parentSkill : parentSkills) {
             for (PrimarySkillType parentSkill : parentSkills) {
                 applyXpGain(parentSkill, xp / parentSkills.size(), xpGainReason, xpGainSource);
                 applyXpGain(parentSkill, xp / parentSkills.size(), xpGainReason, xpGainSource);
@@ -843,7 +847,7 @@ public class McMMOPlayer implements Identified {
             return 0;
             return 0;
         }
         }
 
 
-        xp = (float) (xp / ExperienceConfig.getInstance().getFormulaSkillModifier(primarySkillType) * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier());
+        xp = (float) (xp * ExperienceConfig.getInstance().getFormulaSkillModifier(primarySkillType) * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier());
 
 
         if (mcMMO.p.getGeneralConfig().getToolModsEnabled()) {
         if (mcMMO.p.getGeneralConfig().getToolModsEnabled()) {
             CustomTool tool = mcMMO.getModManager().getTool(player.getInventory().getItemInMainHand());
             CustomTool tool = mcMMO.getModManager().getTool(player.getInventory().getItemInMainHand());

+ 12 - 5
src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java

@@ -7,14 +7,17 @@ import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
 import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.runnables.player.PlayerProfileSaveTask;
 import com.gmail.nossr50.runnables.player.PlayerProfileSaveTask;
-import com.gmail.nossr50.skills.child.FamilyTree;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.skills.SkillTools;
 import com.gmail.nossr50.util.skills.SkillTools;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMap;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.jetbrains.annotations.Nullable;
 
 
-import java.util.*;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
 import java.util.concurrent.DelayQueue;
 import java.util.concurrent.DelayQueue;
 
 
 public class PlayerProfile {
 public class PlayerProfile {
@@ -363,7 +366,7 @@ public class PlayerProfile {
         markProfileDirty();
         markProfileDirty();
 
 
         if (SkillTools.isChildSkill(skill)) {
         if (SkillTools.isChildSkill(skill)) {
-            Set<PrimarySkillType> parentSkills = FamilyTree.getParents(skill);
+            var parentSkills = mcMMO.p.getSkillTools().getChildSkillParents(skill);
             float dividedXP = (xp / parentSkills.size());
             float dividedXP = (xp / parentSkills.size());
 
 
             for (PrimarySkillType parentSkill : parentSkills) {
             for (PrimarySkillType parentSkill : parentSkills) {
@@ -431,8 +434,12 @@ public class PlayerProfile {
         return mcMMO.getFormulaManager().getXPtoNextLevel(level, formulaType);
         return mcMMO.getFormulaManager().getXPtoNextLevel(level, formulaType);
     }
     }
 
 
-    private int getChildSkillLevel(PrimarySkillType primarySkillType) {
-        Set<PrimarySkillType> parents = FamilyTree.getParents(primarySkillType);
+    private int getChildSkillLevel(@NotNull PrimarySkillType primarySkillType) throws IllegalArgumentException {
+        if (!SkillTools.isChildSkill(primarySkillType)) {
+            throw new IllegalArgumentException(primarySkillType + " is not a child skill!");
+        }
+
+        ImmutableList<PrimarySkillType> parents = mcMMO.p.getSkillTools().getChildSkillParents(primarySkillType);
         int sum = 0;
         int sum = 0;
 
 
         for (PrimarySkillType parent : parents) {
         for (PrimarySkillType parent : parents) {

+ 2 - 0
src/main/java/com/gmail/nossr50/datatypes/skills/PrimarySkillType.java

@@ -16,6 +16,7 @@ public enum PrimarySkillType {
     ALCHEMY,
     ALCHEMY,
     ARCHERY,
     ARCHERY,
     AXES,
     AXES,
+    CROSSBOWS,
     EXCAVATION,
     EXCAVATION,
     FISHING,
     FISHING,
     HERBALISM,
     HERBALISM,
@@ -25,6 +26,7 @@ public enum PrimarySkillType {
     SMELTING,
     SMELTING,
     SWORDS,
     SWORDS,
     TAMING,
     TAMING,
+    TRIDENTS,
     UNARMED,
     UNARMED,
     WOODCUTTING;
     WOODCUTTING;
 
 

+ 14 - 5
src/main/java/com/gmail/nossr50/datatypes/skills/SubSkillType.java

@@ -31,6 +31,11 @@ public enum SubSkillType {
     AXES_GREATER_IMPACT(1),
     AXES_GREATER_IMPACT(1),
     AXES_SKULL_SPLITTER(1),
     AXES_SKULL_SPLITTER(1),
 
 
+    /* CROSSBOWS */
+    CROSSBOWS_CROSSBOWS_LIMIT_BREAK(10),
+    CROSSBOWS_TRICK_SHOT(3),
+    CROSSBOWS_POWERED_SHOT(20),
+
     /* Excavation */
     /* Excavation */
     EXCAVATION_ARCHAEOLOGY(8),
     EXCAVATION_ARCHAEOLOGY(8),
     EXCAVATION_GIGA_DRILL_BREAKER(1),
     EXCAVATION_GIGA_DRILL_BREAKER(1),
@@ -45,6 +50,7 @@ public enum SubSkillType {
 
 
     /* Herbalism */
     /* Herbalism */
     HERBALISM_DOUBLE_DROPS(1),
     HERBALISM_DOUBLE_DROPS(1),
+    HERBALISM_VERDANT_BOUNTY(1),
     HERBALISM_FARMERS_DIET(5),
     HERBALISM_FARMERS_DIET(5),
     HERBALISM_GREEN_TERRA(1),
     HERBALISM_GREEN_TERRA(1),
     HERBALISM_GREEN_THUMB(4),
     HERBALISM_GREEN_THUMB(4),
@@ -57,6 +63,7 @@ public enum SubSkillType {
     MINING_DEMOLITIONS_EXPERTISE(1),
     MINING_DEMOLITIONS_EXPERTISE(1),
     MINING_DOUBLE_DROPS(1),
     MINING_DOUBLE_DROPS(1),
     MINING_SUPER_BREAKER(1),
     MINING_SUPER_BREAKER(1),
+    MINING_MOTHER_LODE(1),
 
 
     /* Repair */
     /* Repair */
     REPAIR_ARCANE_FORGING(8),
     REPAIR_ARCANE_FORGING(8),
@@ -91,6 +98,10 @@ public enum SubSkillType {
     TAMING_SHOCK_PROOF(1),
     TAMING_SHOCK_PROOF(1),
     TAMING_THICK_FUR(1),
     TAMING_THICK_FUR(1),
 
 
+    /* Tridents */
+    TRIDENTS_IMPALE(10),
+    TRIDENTS_TRIDENTS_LIMIT_BREAK(10),
+
     /* Unarmed */
     /* Unarmed */
     UNARMED_ARROW_DEFLECT(1),
     UNARMED_ARROW_DEFLECT(1),
     UNARMED_BERSERK(1),
     UNARMED_BERSERK(1),
@@ -101,13 +112,11 @@ public enum SubSkillType {
     UNARMED_UNARMED_LIMIT_BREAK(10),
     UNARMED_UNARMED_LIMIT_BREAK(10),
 
 
     /* Woodcutting */
     /* Woodcutting */
-/*    WOODCUTTING_BARK_SURGEON(3),*/
     WOODCUTTING_KNOCK_ON_WOOD(2),
     WOODCUTTING_KNOCK_ON_WOOD(2),
     WOODCUTTING_HARVEST_LUMBER(1),
     WOODCUTTING_HARVEST_LUMBER(1),
     WOODCUTTING_LEAF_BLOWER(1),
     WOODCUTTING_LEAF_BLOWER(1),
-/*    WOODCUTTING_NATURES_BOUNTY(3),
-    WOODCUTTING_SPLINTER(3),*/
-    WOODCUTTING_TREE_FELLER(1);
+    WOODCUTTING_TREE_FELLER(1),
+    WOODCUTTING_CLEAN_CUTS(1);
 
 
     private final int numRanks;
     private final int numRanks;
     //TODO: SuperAbilityType should also contain flags for active by default? Not sure if it should work that way.
     //TODO: SuperAbilityType should also contain flags for active by default? Not sure if it should work that way.
@@ -134,7 +143,7 @@ public enum SubSkillType {
     /**
     /**
      * !!! This relies on the immutable lists in PrimarySkillType being populated !!!
      * !!! This relies on the immutable lists in PrimarySkillType being populated !!!
      * If we add skills, those immutable lists need to be updated
      * If we add skills, those immutable lists need to be updated
-     * @return
+     * @return the parent skill of this subskill
      */
      */
     public PrimarySkillType getParentSkill() { return mcMMO.p.getSkillTools().getPrimarySkillBySubSkill(this); }
     public PrimarySkillType getParentSkill() { return mcMMO.p.getSkillTools().getPrimarySkillBySubSkill(this); }
 
 

+ 45 - 47
src/main/java/com/gmail/nossr50/datatypes/skills/SuperAbilityType.java

@@ -10,6 +10,12 @@ import org.bukkit.block.BlockState;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
 
 
 public enum SuperAbilityType {
 public enum SuperAbilityType {
+    EXPLOSIVE_SHOT("Archery.Skills.ExplosiveShot.On",
+            "Archery.Skills.ExplosiveShot.Off",
+            "Archery.Skills.ExplosiveShot.Other.On",
+            "Archery.Skills.ExplosiveShot.Refresh",
+            "Archery.Skills.ExplosiveShot.Other.Off",
+            "Archery.SubSkill.ExplosiveShot.Name"),
     BERSERK(
     BERSERK(
             "Unarmed.Skills.Berserk.On",
             "Unarmed.Skills.Berserk.On",
             "Unarmed.Skills.Berserk.Off",
             "Unarmed.Skills.Berserk.Off",
@@ -65,6 +71,20 @@ public enum SuperAbilityType {
             "Swords.Skills.SS.Refresh",
             "Swords.Skills.SS.Refresh",
             "Swords.Skills.SS.Other.Off",
             "Swords.Skills.SS.Other.Off",
             "Swords.SubSkill.SerratedStrikes.Name"),
             "Swords.SubSkill.SerratedStrikes.Name"),
+    SUPER_SHOTGUN(
+            null,
+            null,
+            "Crossbows.Skills.SSG.Other.On",
+            "Crossbows.Skills.SSG.Refresh",
+            null,
+            "Crossbows.SubSkill.SuperShotgun.Name"),
+    TRIDENTS_SUPER_ABILITY(
+            "Tridents.Skills.TA.On",
+            "Tridents.Skills.TA.Off",
+            "Tridents.Skills.TA.Other.On",
+            "Tridents.Skills.TA.Refresh",
+            "Tridents.Skills.TA.Other.Off",
+            "Tridents.SubSkill.TridentAbility.Name"),
 
 
     /**
     /**
      * Has cooldown - but has to share a skill with Super Breaker, so needs special treatment
      * Has cooldown - but has to share a skill with Super Breaker, so needs special treatment
@@ -82,6 +102,7 @@ public enum SuperAbilityType {
      * Defining their associated SubSkillType definitions
      * Defining their associated SubSkillType definitions
      * This is a bit of a band-aid fix until the new skill system is in place
      * This is a bit of a band-aid fix until the new skill system is in place
      */
      */
+    // TODO: This is stupid
     static {
     static {
         BERSERK.subSkillTypeDefinition              = SubSkillType.UNARMED_BERSERK;
         BERSERK.subSkillTypeDefinition              = SubSkillType.UNARMED_BERSERK;
         SUPER_BREAKER.subSkillTypeDefinition        = SubSkillType.MINING_SUPER_BREAKER;
         SUPER_BREAKER.subSkillTypeDefinition        = SubSkillType.MINING_SUPER_BREAKER;
@@ -173,35 +194,22 @@ public enum SuperAbilityType {
      * @param player Player to check permissions for
      * @param player Player to check permissions for
      * @return true if the player has permissions, false otherwise
      * @return true if the player has permissions, false otherwise
      */
      */
+    // TODO: Add unit tests
+    // TODO: This is stupid
     public boolean getPermissions(Player player) {
     public boolean getPermissions(Player player) {
-        switch (this) {
-            case BERSERK:
-                return Permissions.berserk(player);
-
-            case BLAST_MINING:
-                return Permissions.remoteDetonation(player);
-
-            case GIGA_DRILL_BREAKER:
-                return Permissions.gigaDrillBreaker(player);
-
-            case GREEN_TERRA:
-                return Permissions.greenTerra(player);
-
-            case SERRATED_STRIKES:
-                return Permissions.serratedStrikes(player);
-
-            case SKULL_SPLITTER:
-                return Permissions.skullSplitter(player);
-
-            case SUPER_BREAKER:
-                return Permissions.superBreaker(player);
-
-            case TREE_FELLER:
-                return Permissions.treeFeller(player);
-
-            default:
-                return false;
-        }
+        return switch (this) {
+            case BERSERK -> Permissions.berserk(player);
+            case EXPLOSIVE_SHOT -> Permissions.explosiveShot(player);
+            case BLAST_MINING -> Permissions.remoteDetonation(player);
+            case GIGA_DRILL_BREAKER -> Permissions.gigaDrillBreaker(player);
+            case GREEN_TERRA -> Permissions.greenTerra(player);
+            case SERRATED_STRIKES -> Permissions.serratedStrikes(player);
+            case SKULL_SPLITTER -> Permissions.skullSplitter(player);
+            case SUPER_BREAKER -> Permissions.superBreaker(player);
+            case SUPER_SHOTGUN -> Permissions.superShotgun(player);
+            case TREE_FELLER -> Permissions.treeFeller(player);
+            case TRIDENTS_SUPER_ABILITY -> Permissions.tridentsSuper(player);
+        };
     }
     }
 
 
     /**
     /**
@@ -211,25 +219,15 @@ public enum SuperAbilityType {
      * @return true if the block is affected by this ability, false otherwise
      * @return true if the block is affected by this ability, false otherwise
      */
      */
     public boolean blockCheck(BlockState blockState) {
     public boolean blockCheck(BlockState blockState) {
-        switch (this) {
-            case BERSERK:
-                return (BlockUtils.affectedByGigaDrillBreaker(blockState) || blockState.getType() == Material.SNOW || mcMMO.getMaterialMapStore().isGlass(blockState.getType()));
-
-            case GIGA_DRILL_BREAKER:
-                return BlockUtils.affectedByGigaDrillBreaker(blockState);
-
-            case GREEN_TERRA:
-                return BlockUtils.canMakeMossy(blockState);
-
-            case SUPER_BREAKER:
-                return BlockUtils.affectedBySuperBreaker(blockState);
-
-            case TREE_FELLER:
-                return BlockUtils.hasWoodcuttingXP(blockState);
-
-            default:
-                return false;
-        }
+        return switch (this) {
+            case BERSERK ->
+                    (BlockUtils.affectedByGigaDrillBreaker(blockState) || blockState.getType() == Material.SNOW || mcMMO.getMaterialMapStore().isGlass(blockState.getType()));
+            case GIGA_DRILL_BREAKER -> BlockUtils.affectedByGigaDrillBreaker(blockState);
+            case GREEN_TERRA -> BlockUtils.canMakeMossy(blockState);
+            case SUPER_BREAKER -> BlockUtils.affectedBySuperBreaker(blockState);
+            case TREE_FELLER -> BlockUtils.hasWoodcuttingXP(blockState);
+            default -> false;
+        };
     }
     }
 
 
     /**
     /**

+ 8 - 1
src/main/java/com/gmail/nossr50/datatypes/skills/ToolType.java

@@ -10,7 +10,10 @@ public enum ToolType {
     HOE("Herbalism.Ability.Lower", "Herbalism.Ability.Ready"),
     HOE("Herbalism.Ability.Lower", "Herbalism.Ability.Ready"),
     PICKAXE("Mining.Ability.Lower", "Mining.Ability.Ready"),
     PICKAXE("Mining.Ability.Lower", "Mining.Ability.Ready"),
     SHOVEL("Excavation.Ability.Lower", "Excavation.Ability.Ready"),
     SHOVEL("Excavation.Ability.Lower", "Excavation.Ability.Ready"),
-    SWORD("Swords.Ability.Lower", "Swords.Ability.Ready");
+    SWORD("Swords.Ability.Lower", "Swords.Ability.Ready"),
+    CROSSBOW("Crossbows.Ability.Lower", "Crossbows.Ability.Ready"),
+    BOW("Archery.Ability.Lower", "Archery.Ability.Ready"),
+    TRIDENTS("Tridents.Ability.Lower", "Tridents.Ability.Ready");
 
 
     private final String lowerTool;
     private final String lowerTool;
     private final String raiseTool;
     private final String raiseTool;
@@ -38,6 +41,10 @@ public enum ToolType {
         switch (this) {
         switch (this) {
             case AXE:
             case AXE:
                 return ItemUtils.isAxe(itemStack);
                 return ItemUtils.isAxe(itemStack);
+            case CROSSBOW:
+                return ItemUtils.isCrossbow(itemStack);
+            case TRIDENTS:
+                return ItemUtils.isTrident(itemStack);
 
 
             case FISTS:
             case FISTS:
                 return itemStack.getType() == Material.AIR;
                 return itemStack.getType() == Material.AIR;

+ 31 - 23
src/main/java/com/gmail/nossr50/datatypes/skills/subskills/acrobatics/Roll.java

@@ -14,11 +14,10 @@ import com.gmail.nossr50.util.ItemUtils;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
-import com.gmail.nossr50.util.random.RandomChanceSkill;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.Probability;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.PerksUtils;
 import com.gmail.nossr50.util.skills.PerksUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.sounds.SoundManager;
 import com.gmail.nossr50.util.sounds.SoundManager;
 import com.gmail.nossr50.util.sounds.SoundType;
 import com.gmail.nossr50.util.sounds.SoundType;
@@ -33,6 +32,8 @@ import org.bukkit.event.Event;
 import org.bukkit.event.EventPriority;
 import org.bukkit.event.EventPriority;
 import org.bukkit.event.entity.EntityDamageEvent;
 import org.bukkit.event.entity.EntityDamageEvent;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.VisibleForTesting;
 
 
 import java.util.Locale;
 import java.util.Locale;
 
 
@@ -110,7 +111,7 @@ public class Roll extends AcrobaticsSubSkill {
      */
      */
     @Override
     @Override
     public boolean hasPermission(Player player) {
     public boolean hasPermission(Player player) {
-        return Permissions.isSubSkillEnabled(player, this);
+        return Permissions.isSubSkillEnabled(player, getSubSkillType());
     }
     }
 
 
     /**
     /**
@@ -128,14 +129,16 @@ public class Roll extends AcrobaticsSubSkill {
         float skillValue = playerProfile.getSkillLevel(getPrimarySkill());
         float skillValue = playerProfile.getSkillLevel(getPrimarySkill());
         boolean isLucky = Permissions.lucky(player, getPrimarySkill());
         boolean isLucky = Permissions.lucky(player, getPrimarySkill());
 
 
-        String[] rollStrings = RandomChanceUtil.calculateAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.ACROBATICS_ROLL);
+        String[] rollStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.ACROBATICS_ROLL);
         rollChance = rollStrings[0];
         rollChance = rollStrings[0];
         rollChanceLucky = rollStrings[1];
         rollChanceLucky = rollStrings[1];
 
 
         /*
         /*
          * Graceful is double the odds of a normal roll
          * Graceful is double the odds of a normal roll
          */
          */
-        String[] gracefulRollStrings = RandomChanceUtil.calculateAbilityDisplayValuesCustom(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.ACROBATICS_ROLL, 2.0D);
+        Probability probability = getRollProbability(player);
+        Probability gracefulProbability = Probability.ofPercent(probability.getValue() * 2);
+        String[] gracefulRollStrings = ProbabilityUtil.getRNGDisplayValues(gracefulProbability);
         gracefulRollChance = gracefulRollStrings[0];
         gracefulRollChance = gracefulRollStrings[0];
         gracefulRollChanceLucky = gracefulRollStrings[1];
         gracefulRollChanceLucky = gracefulRollStrings[1];
 
 
@@ -166,6 +169,11 @@ public class Roll extends AcrobaticsSubSkill {
 
 
     }
     }
 
 
+    @NotNull
+    private Probability getRollProbability(Player player) {
+        return ProbabilityUtil.getSubSkillProbability(SubSkillType.ACROBATICS_ROLL, player);
+    }
+
     @Override
     @Override
     public boolean isSuperAbility() {
     public boolean isSuperAbility() {
         return false;
         return false;
@@ -191,7 +199,8 @@ public class Roll extends AcrobaticsSubSkill {
      * @param damage The amount of damage initially dealt by the event
      * @param damage The amount of damage initially dealt by the event
      * @return the modified event damage if the ability was successful, the original event damage otherwise
      * @return the modified event damage if the ability was successful, the original event damage otherwise
      */
      */
-    private double rollCheck(Player player, McMMOPlayer mcMMOPlayer, double damage) {
+    @VisibleForTesting
+    public double rollCheck(Player player, McMMOPlayer mcMMOPlayer, double damage) {
 
 
         int skillLevel = mcMMOPlayer.getSkillLevel(getPrimarySkill());
         int skillLevel = mcMMOPlayer.getSkillLevel(getPrimarySkill());
 
 
@@ -202,7 +211,7 @@ public class Roll extends AcrobaticsSubSkill {
         double modifiedDamage = calculateModifiedRollDamage(damage, mcMMO.p.getAdvancedConfig().getRollDamageThreshold());
         double modifiedDamage = calculateModifiedRollDamage(damage, mcMMO.p.getAdvancedConfig().getRollDamageThreshold());
 
 
         if (!isFatal(player, modifiedDamage)
         if (!isFatal(player, modifiedDamage)
-                && RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.ACROBATICS_ROLL, player)) {
+                && ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.ACROBATICS_ROLL, player)) {
             NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE, "Acrobatics.Roll.Text");
             NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE, "Acrobatics.Roll.Text");
             SoundManager.sendCategorizedSound(player, player.getLocation(), SoundType.ROLL_ACTIVATED, SoundCategory.PLAYERS);
             SoundManager.sendCategorizedSound(player, player.getLocation(), SoundType.ROLL_ACTIVATED, SoundCategory.PLAYERS);
             //player.sendMessage(LocaleLoader.getString("Acrobatics.Roll.Text"));
             //player.sendMessage(LocaleLoader.getString("Acrobatics.Roll.Text"));
@@ -239,11 +248,11 @@ public class Roll extends AcrobaticsSubSkill {
     private double gracefulRollCheck(Player player, McMMOPlayer mcMMOPlayer, double damage, int skillLevel) {
     private double gracefulRollCheck(Player player, McMMOPlayer mcMMOPlayer, double damage, int skillLevel) {
         double modifiedDamage = calculateModifiedRollDamage(damage, mcMMO.p.getAdvancedConfig().getRollDamageThreshold() * 2);
         double modifiedDamage = calculateModifiedRollDamage(damage, mcMMO.p.getAdvancedConfig().getRollDamageThreshold() * 2);
 
 
-        RandomChanceSkill rcs = new RandomChanceSkill(player, subSkillType);
-        rcs.setSkillLevel(rcs.getSkillLevel() * 2); //Double the effective odds
+        Probability gracefulProbability = getGracefulProbability(player);
 
 
         if (!isFatal(player, modifiedDamage)
         if (!isFatal(player, modifiedDamage)
-                && RandomChanceUtil.checkRandomChanceExecutionSuccess(rcs))
+                //TODO: Graceful isn't sending out an event
+                && ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.ACROBATICS, player, gracefulProbability))
         {
         {
             NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE, "Acrobatics.Ability.Proc");
             NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE, "Acrobatics.Ability.Proc");
             SoundManager.sendCategorizedSound(player, player.getLocation(), SoundType.ROLL_ACTIVATED, SoundCategory.PLAYERS,0.5F);
             SoundManager.sendCategorizedSound(player, player.getLocation(), SoundType.ROLL_ACTIVATED, SoundCategory.PLAYERS,0.5F);
@@ -263,6 +272,12 @@ public class Roll extends AcrobaticsSubSkill {
         return damage;
         return damage;
     }
     }
 
 
+    @NotNull
+    public static Probability getGracefulProbability(Player player) {
+        double gracefulOdds = ProbabilityUtil.getSubSkillProbability(SubSkillType.ACROBATICS_ROLL, player).getValue() * 2;
+        return Probability.ofPercent(gracefulOdds);
+    }
+
     /**
     /**
      * Check if the player is "farming" Acrobatics XP using
      * Check if the player is "farming" Acrobatics XP using
      * exploits in the game.
      * exploits in the game.
@@ -412,28 +427,21 @@ public class Roll extends AcrobaticsSubSkill {
     @Override
     @Override
     public Double[] getStats(Player player)
     public Double[] getStats(Player player)
     {
     {
-        double playerChanceRoll, playerChanceGrace;
+        double playerChanceRoll = ProbabilityUtil.getSubSkillProbability(subSkillType, player).getValue();
+        double playerChanceGrace = playerChanceRoll * 2;
 
 
-        RandomChanceSkill roll          = new RandomChanceSkill(player, getSubSkillType());
-        RandomChanceSkill graceful      = new RandomChanceSkill(player, getSubSkillType());
-
-        graceful.setSkillLevel(graceful.getSkillLevel() * 2); //Double odds
-
-        //Calculate
-        playerChanceRoll        = RandomChanceUtil.getRandomChanceExecutionChance(roll);
-        playerChanceGrace       = RandomChanceUtil.getRandomChanceExecutionChance(graceful);
+        double gracefulOdds = ProbabilityUtil.getSubSkillProbability(subSkillType, player).getValue() * 2;
 
 
         return new Double[]{ playerChanceRoll, playerChanceGrace };
         return new Double[]{ playerChanceRoll, playerChanceGrace };
     }
     }
 
 
-    public void addFallLocation(Player player)
+    public void addFallLocation(@NotNull Player player)
     {
     {
         UserManager.getPlayer(player).getAcrobaticsManager().addLocationToFallMap(getBlockLocation(player));
         UserManager.getPlayer(player).getAcrobaticsManager().addLocationToFallMap(getBlockLocation(player));
     }
     }
 
 
-    public Location getBlockLocation(Player player)
+    public @NotNull Location getBlockLocation(@NotNull Player player)
     {
     {
         return player.getLocation().getBlock().getLocation();
         return player.getLocation().getBlock().getLocation();
     }
     }
-
 }
 }

+ 38 - 4
src/main/java/com/gmail/nossr50/datatypes/treasure/Treasure.java

@@ -1,25 +1,34 @@
 package com.gmail.nossr50.datatypes.treasure;
 package com.gmail.nossr50.datatypes.treasure;
 
 
+import com.gmail.nossr50.util.random.Probability;
+import com.google.common.base.Objects;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
 
 
 public abstract class Treasure {
 public abstract class Treasure {
     private int xp;
     private int xp;
     private double dropChance;
     private double dropChance;
+    private @NotNull Probability dropProbability;
     private int dropLevel;
     private int dropLevel;
-    private ItemStack drop;
+    private @NotNull ItemStack drop;
 
 
     public Treasure(ItemStack drop, int xp, double dropChance, int dropLevel) {
     public Treasure(ItemStack drop, int xp, double dropChance, int dropLevel) {
         this.drop = drop;
         this.drop = drop;
         this.xp = xp;
         this.xp = xp;
         this.dropChance = dropChance;
         this.dropChance = dropChance;
+        this.dropProbability = Probability.ofPercent(dropChance);
         this.dropLevel = dropLevel;
         this.dropLevel = dropLevel;
     }
     }
 
 
-    public ItemStack getDrop() {
+    public @NotNull Probability getDropProbability() {
+        return dropProbability;
+    }
+
+    public @NotNull ItemStack getDrop() {
         return drop;
         return drop;
     }
     }
 
 
-    public void setDrop(ItemStack drop) {
+    public void setDrop(@NotNull ItemStack drop) {
         this.drop = drop;
         this.drop = drop;
     }
     }
 
 
@@ -35,8 +44,9 @@ public abstract class Treasure {
         return dropChance;
         return dropChance;
     }
     }
 
 
-    public void setDropChance(Double dropChance) {
+    public void setDropChance(double dropChance) {
         this.dropChance = dropChance;
         this.dropChance = dropChance;
+        this.dropProbability = Probability.ofPercent(dropChance);
     }
     }
 
 
     public int getDropLevel() {
     public int getDropLevel() {
@@ -46,4 +56,28 @@ public abstract class Treasure {
     public void setDropLevel(int dropLevel) {
     public void setDropLevel(int dropLevel) {
         this.dropLevel = dropLevel;
         this.dropLevel = dropLevel;
     }
     }
+
+    @Override
+    public String toString() {
+        return "Treasure{" +
+                "xp=" + xp +
+                ", dropChance=" + dropChance +
+                ", dropProbability=" + dropProbability +
+                ", dropLevel=" + dropLevel +
+                ", drop=" + drop +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Treasure treasure = (Treasure) o;
+        return xp == treasure.xp && Double.compare(treasure.dropChance, dropChance) == 0 && dropLevel == treasure.dropLevel && Objects.equal(dropProbability, treasure.dropProbability) && Objects.equal(drop, treasure.drop);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(xp, dropChance, dropProbability, dropLevel, drop);
+    }
 }
 }

+ 3 - 3
src/main/java/com/gmail/nossr50/events/skills/McMMOPlayerSkillEvent.java

@@ -11,10 +11,10 @@ import org.jetbrains.annotations.NotNull;
  * Generic event for mcMMO skill handling.
  * Generic event for mcMMO skill handling.
  */
  */
 public abstract class McMMOPlayerSkillEvent extends PlayerEvent {
 public abstract class McMMOPlayerSkillEvent extends PlayerEvent {
-    protected PrimarySkillType skill;
+    protected @NotNull PrimarySkillType skill;
     protected int skillLevel;
     protected int skillLevel;
 
 
-    protected McMMOPlayerSkillEvent(Player player, PrimarySkillType skill) {
+    protected McMMOPlayerSkillEvent(@NotNull Player player, @NotNull PrimarySkillType skill) {
         super(player);
         super(player);
         this.skill = skill;
         this.skill = skill;
         this.skillLevel = UserManager.getPlayer(player).getSkillLevel(skill);
         this.skillLevel = UserManager.getPlayer(player).getSkillLevel(skill);
@@ -23,7 +23,7 @@ public abstract class McMMOPlayerSkillEvent extends PlayerEvent {
     /**
     /**
      * @return The skill involved in this event
      * @return The skill involved in this event
      */
      */
-    public PrimarySkillType getSkill() {
+    public @NotNull PrimarySkillType getSkill() {
         return skill;
         return skill;
     }
     }
 
 

+ 41 - 47
src/main/java/com/gmail/nossr50/events/skills/secondaryabilities/SubSkillRandomCheckEvent.java

@@ -1,47 +1,41 @@
-package com.gmail.nossr50.events.skills.secondaryabilities;
-
-import com.gmail.nossr50.datatypes.skills.SubSkillType;
-import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill;
-import org.bukkit.entity.Player;
-
-public class SubSkillRandomCheckEvent extends SubSkillEvent {
-    private double chance;
-
-    public SubSkillRandomCheckEvent(Player player, SubSkillType ability, double chance) {
-        super(player, ability);
-        this.chance = chance;
-    }
-
-    public SubSkillRandomCheckEvent(Player player, AbstractSubSkill abstractSubSkill, double chance)
-    {
-        super(player, abstractSubSkill);
-        this.chance = chance;
-    }
-
-    /**
-     * Gets the activation chance of the ability 0D being no chance,  100.0D being 100% chance
-     *
-     * @return The activation chance of the ability
-     */
-    public double getChance() {
-        return chance;
-    }
-
-    /**
-     * Sets the activation chance of the ability [0D-100.0D]
-     *
-     * @param chance The activation chance of the ability
-     */
-    public void setChance(double chance) {
-        this.chance = chance;
-    }
-
-    /**
-     * Sets the activation chance of the ability to 100% or 0%
-     *
-     * @param success whether it should be successful or not
-     */
-    public void setSuccessful(boolean success) {
-        this.chance = success ? 100.0D : 0D;
-    }
-}
+//package com.gmail.nossr50.events.skills.secondaryabilities;
+//
+//import com.gmail.nossr50.datatypes.skills.SubSkillType;
+//import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill;
+//import org.bukkit.entity.Player;
+//
+//public class SubSkillRandomCheckEvent extends SubSkillEvent {
+//    private double chance;
+//
+//    public SubSkillRandomCheckEvent(Player player, SubSkillType ability, double chance) {
+//        super(player, ability);
+//        this.chance = chance;
+//    }
+//
+//    /**
+//     * Gets the activation chance of the ability 0D being no chance,  100.0D being 100% chance
+//     *
+//     * @return The activation chance of the ability
+//     */
+//    public double getChance() {
+//        return chance;
+//    }
+//
+//    /**
+//     * Sets the activation chance of the ability [0D-100.0D]
+//     *
+//     * @param chance The activation chance of the ability
+//     */
+//    public void setChance(double chance) {
+//        this.chance = chance;
+//    }
+//
+//    /**
+//     * Sets the activation chance of the ability to 100% or 0%
+//     *
+//     * @param success whether it should be successful or not
+//     */
+//    public void setSuccessful(boolean success) {
+//        this.chance = success ? 100.0D : 0D;
+//    }
+//}

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

@@ -417,7 +417,7 @@ public class BlockListener implements Listener {
                 woodcuttingManager.processWoodcuttingBlockXP(blockState);
                 woodcuttingManager.processWoodcuttingBlockXP(blockState);
 
 
                 //Check for bonus drops
                 //Check for bonus drops
-                woodcuttingManager.processHarvestLumber(blockState);
+                woodcuttingManager.processBonusDropCheck(blockState);
             }
             }
         }
         }
 
 

+ 0 - 40
src/main/java/com/gmail/nossr50/listeners/CommandListener.java

@@ -1,40 +0,0 @@
-//package com.gmail.nossr50.listeners;
-//
-//import com.gmail.nossr50.datatypes.player.McMMOPlayer;
-//import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
-//import com.gmail.nossr50.mcMMO;
-//import com.gmail.nossr50.util.player.UserManager;
-//import com.gmail.nossr50.util.skills.SkillUtils;
-//import org.bukkit.Bukkit;
-//import org.bukkit.entity.Player;
-//import org.bukkit.event.EventHandler;
-//import org.bukkit.event.EventPriority;
-//import org.bukkit.event.Listener;
-//import org.bukkit.event.player.PlayerCommandPreprocessEvent;
-//
-//public class CommandListener implements Listener {
-//
-//    private final mcMMO pluginRef;
-//
-//    public CommandListener(mcMMO plugin) {
-//        this.pluginRef = plugin;
-//    }
-//
-//    @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
-//    public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) {
-//        Player player = event.getPlayer();
-//
-//        SkillUtils.removeAbilityBoostsFromInventory(player);
-//
-//        McMMOPlayer mmoPlayer = UserManager.getPlayer(player);
-//
-//        if(mmoPlayer == null)
-//            return;
-//
-//        Bukkit.getServer().getScheduler().runTaskLater(pluginRef, () -> {
-//            if(mmoPlayer.getAbilityMode(SuperAbilityType.GIGA_DRILL_BREAKER) || mmoPlayer.getAbilityMode(SuperAbilityType.SUPER_BREAKER)) {
-//                SkillUtils.handleAbilitySpeedIncrease(player);
-//            }
-//        }, 5);
-//    }
-//}

+ 38 - 52
src/main/java/com/gmail/nossr50/listeners/EntityListener.java

@@ -11,6 +11,7 @@ import com.gmail.nossr50.metadata.MobMetaFlagType;
 import com.gmail.nossr50.metadata.MobMetadataService;
 import com.gmail.nossr50.metadata.MobMetadataService;
 import com.gmail.nossr50.runnables.TravelingBlockMetaCleanup;
 import com.gmail.nossr50.runnables.TravelingBlockMetaCleanup;
 import com.gmail.nossr50.skills.archery.Archery;
 import com.gmail.nossr50.skills.archery.Archery;
+import com.gmail.nossr50.skills.crossbows.Crossbows;
 import com.gmail.nossr50.skills.mining.BlastMining;
 import com.gmail.nossr50.skills.mining.BlastMining;
 import com.gmail.nossr50.skills.mining.MiningManager;
 import com.gmail.nossr50.skills.mining.MiningManager;
 import com.gmail.nossr50.skills.taming.Taming;
 import com.gmail.nossr50.skills.taming.Taming;
@@ -19,9 +20,8 @@ import com.gmail.nossr50.skills.unarmed.UnarmedManager;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.CombatUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.worldguard.WorldGuardManager;
 import com.gmail.nossr50.worldguard.WorldGuardManager;
 import com.gmail.nossr50.worldguard.WorldGuardUtils;
 import com.gmail.nossr50.worldguard.WorldGuardUtils;
 import org.bukkit.ChatColor;
 import org.bukkit.ChatColor;
@@ -62,30 +62,6 @@ public class EntityListener implements Listener {
         mobMetadataService = mcMMO.getMetadataService().getMobMetadataService();
         mobMetadataService = mcMMO.getMetadataService().getMobMetadataService();
     }
     }
 
 
-//    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
-//    public void onBlockDropItemEvent(EntityDropItemEvent event) {
-//        if(event.getEntity() instanceof Block) {
-//            Block itemDispensingBlock = (Block) event.getEntity();
-//
-//            //Is it a berry bush?
-//            if(itemDispensingBlock.getType().toString().equalsIgnoreCase("sweet_berry_bush")) {
-//                //Berry Bush Time!
-//                if (event.getEntity().getMetadata(mcMMO.BONUS_DROPS_METAKEY).size() > 0) {
-//                    Bukkit.broadcastMessage("Pop pop!");
-//                    BonusDropMeta bonusDropMeta = (BonusDropMeta) event.getEntity().getMetadata(mcMMO.BONUS_DROPS_METAKEY).get(0);
-//                    int bonusCount = bonusDropMeta.asInt();
-//
-//                    for (int i = 0; i < bonusCount; i++) {
-//                        Misc.spawnItemNaturally(event.getEntity().getLocation(), event.getItemDrop().getItemStack(), ItemSpawnReason.BONUS_DROPS);
-//                    }
-//                }
-//            }
-//
-//            if(event.getEntity().hasMetadata(mcMMO.BONUS_DROPS_METAKEY))
-//                event.getEntity().removeMetadata(mcMMO.BONUS_DROPS_METAKEY, pluginRef);
-//        }
-//    }
-
     @EventHandler(priority = EventPriority.MONITOR)
     @EventHandler(priority = EventPriority.MONITOR)
     public void onEntityTransform(EntityTransformEvent event) {
     public void onEntityTransform(EntityTransformEvent event) {
         if(event.getEntity() instanceof LivingEntity livingEntity) {
         if(event.getEntity() instanceof LivingEntity livingEntity) {
@@ -124,7 +100,7 @@ public class EntityListener implements Listener {
         }
         }
     }
     }
 
 
-    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = false)
     public void onEntityShootBow(EntityShootBowEvent event) {
     public void onEntityShootBow(EntityShootBowEvent event) {
         /* WORLD BLACKLIST CHECK */
         /* WORLD BLACKLIST CHECK */
         if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld()))
         if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld()))
@@ -132,32 +108,28 @@ public class EntityListener implements Listener {
 
 
         if(event.getEntity() instanceof Player player)
         if(event.getEntity() instanceof Player player)
         {
         {
-
-            /* WORLD GUARD MAIN FLAG CHECK */
-            if(WorldGuardUtils.isWorldGuardLoaded())
-            {
-                if(!WorldGuardManager.getInstance().hasMainFlag(player))
-                    return;
-            }
-
             Entity projectile = event.getProjectile();
             Entity projectile = event.getProjectile();
 
 
             //Should be noted that there are API changes regarding Arrow from 1.13.2 to current versions of the game
             //Should be noted that there are API changes regarding Arrow from 1.13.2 to current versions of the game
-            if (!(projectile instanceof Arrow)) {
+            if (!(projectile instanceof Arrow arrow)) {
                 return;
                 return;
             }
             }
 
 
             ItemStack bow = event.getBow();
             ItemStack bow = event.getBow();
 
 
-            if (bow != null
-                    && bow.containsEnchantment(Enchantment.ARROW_INFINITE)) {
+            if (bow == null)
+                return;
+
+            if (bow.containsEnchantment(Enchantment.ARROW_INFINITE)) {
                 projectile.setMetadata(MetadataConstants.METADATA_KEY_INF_ARROW, MetadataConstants.MCMMO_METADATA_VALUE);
                 projectile.setMetadata(MetadataConstants.METADATA_KEY_INF_ARROW, MetadataConstants.MCMMO_METADATA_VALUE);
             }
             }
 
 
+            // Set BowType, Force, and Distance metadata
             projectile.setMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE, new FixedMetadataValue(pluginRef, Math.min(event.getForce() * mcMMO.p.getAdvancedConfig().getForceMultiplier(), 1.0)));
             projectile.setMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE, new FixedMetadataValue(pluginRef, Math.min(event.getForce() * mcMMO.p.getAdvancedConfig().getForceMultiplier(), 1.0)));
-            projectile.setMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE, new FixedMetadataValue(pluginRef, projectile.getLocation()));
+            projectile.setMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE, new FixedMetadataValue(pluginRef, arrow.getLocation()));
+
             //Cleanup metadata in 1 minute in case normal collection falls through
             //Cleanup metadata in 1 minute in case normal collection falls through
-            CombatUtils.delayArrowMetaCleanup((Projectile) projectile);
+            CombatUtils.delayArrowMetaCleanup(arrow);
         }
         }
     }
     }
 
 
@@ -176,25 +148,28 @@ public class EntityListener implements Listener {
                     return;
                     return;
             }
             }
 
 
-            Projectile projectile = event.getEntity();
-            EntityType entityType = projectile.getType();
+            if(event.getEntity() instanceof Arrow arrow) {
+                // Delayed metadata cleanup in case other cleanup hooks fail
+                CombatUtils.delayArrowMetaCleanup(arrow);
 
 
-            if(entityType == EntityType.ARROW || entityType == EntityType.SPECTRAL_ARROW) {
-                CombatUtils.delayArrowMetaCleanup(projectile); //Cleans up metadata 1 minute from now in case other collection methods fall through
+                // If fired from an item with multi-shot, we need to track
+                if(ItemUtils.doesPlayerHaveEnchantmentInHands(player, "multishot")) {
+                    arrow.setMetadata(MetadataConstants.METADATA_KEY_MULTI_SHOT_ARROW, MetadataConstants.MCMMO_METADATA_VALUE);
+                }
 
 
-                if(!projectile.hasMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE))
-                    projectile.setMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE, new FixedMetadataValue(pluginRef, 1.0));
+                if(!arrow.hasMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE))
+                    arrow.setMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE, new FixedMetadataValue(pluginRef, 1.0));
 
 
-                if(!projectile.hasMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE))
-                    projectile.setMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE, new FixedMetadataValue(pluginRef, projectile.getLocation()));
+                if(!arrow.hasMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE))
+                    arrow.setMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE, new FixedMetadataValue(pluginRef, arrow.getLocation()));
 
 
                 //Check both hands
                 //Check both hands
                 if(ItemUtils.doesPlayerHaveEnchantmentInHands(player, "piercing")) {
                 if(ItemUtils.doesPlayerHaveEnchantmentInHands(player, "piercing")) {
                     return;
                     return;
                 }
                 }
 
 
-                if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.ARCHERY_ARROW_RETRIEVAL, player)) {
-                    projectile.setMetadata(MetadataConstants.METADATA_KEY_TRACKED_ARROW, MetadataConstants.MCMMO_METADATA_VALUE);
+                if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.ARCHERY_ARROW_RETRIEVAL, player)) {
+                    arrow.setMetadata(MetadataConstants.METADATA_KEY_TRACKED_ARROW, MetadataConstants.MCMMO_METADATA_VALUE);
                 }
                 }
             }
             }
         }
         }
@@ -424,8 +399,8 @@ public class EntityListener implements Listener {
             }
             }
         }
         }
 
 
-        if(entityDamageEvent.getDamager() instanceof Projectile) {
-            CombatUtils.cleanupArrowMetadata((Projectile) entityDamageEvent.getDamager());
+        if(entityDamageEvent.getDamager() instanceof Arrow arrow) {
+            CombatUtils.delayArrowMetaCleanup(arrow);
         }
         }
 
 
         if(entityDamageEvent.getEntity() instanceof Player player && entityDamageEvent.getDamager() instanceof Player) {
         if(entityDamageEvent.getEntity() instanceof Player player && entityDamageEvent.getDamager() instanceof Player) {
@@ -1115,5 +1090,16 @@ public class EntityListener implements Listener {
         }
         }
     }
     }
 
 
+    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+    public void onProjectileHitEvent(ProjectileHitEvent event) {
+        /* WORLD BLACKLIST CHECK */
+        if (WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld()))
+            return;
 
 
+        if(event.getEntity() instanceof Arrow arrow) {
+            if(arrow.isShotFromCrossbow()) {
+                Crossbows.processCrossbows(event, pluginRef, arrow);
+            }
+        }
+    }
 }
 }

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

@@ -787,6 +787,9 @@ public class PlayerListener implements Listener {
         }
         }
 
 
         McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
         McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
+        if (mcMMOPlayer == null)
+            return;
+
         ItemStack heldItem = player.getInventory().getItemInMainHand();
         ItemStack heldItem = player.getInventory().getItemInMainHand();
 
 
         //Spam Fishing Detection
         //Spam Fishing Detection

+ 1 - 4
src/main/java/com/gmail/nossr50/mcMMO.java

@@ -31,7 +31,6 @@ import com.gmail.nossr50.runnables.player.ClearRegisteredXPGainTask;
 import com.gmail.nossr50.runnables.player.PlayerProfileLoadingTask;
 import com.gmail.nossr50.runnables.player.PlayerProfileLoadingTask;
 import com.gmail.nossr50.runnables.player.PowerLevelUpdatingTask;
 import com.gmail.nossr50.runnables.player.PowerLevelUpdatingTask;
 import com.gmail.nossr50.skills.alchemy.Alchemy;
 import com.gmail.nossr50.skills.alchemy.Alchemy;
-import com.gmail.nossr50.skills.child.ChildConfig;
 import com.gmail.nossr50.skills.repair.repairables.Repairable;
 import com.gmail.nossr50.skills.repair.repairables.Repairable;
 import com.gmail.nossr50.skills.repair.repairables.RepairableManager;
 import com.gmail.nossr50.skills.repair.repairables.RepairableManager;
 import com.gmail.nossr50.skills.repair.repairables.SimpleRepairableManager;
 import com.gmail.nossr50.skills.repair.repairables.SimpleRepairableManager;
@@ -99,7 +98,7 @@ public class mcMMO extends JavaPlugin {
     private static CommandManager commandManager; //ACF
     private static CommandManager commandManager; //ACF
     private static TransientEntityTracker transientEntityTracker;
     private static TransientEntityTracker transientEntityTracker;
 
 
-    private @NotNull SkillTools skillTools;
+    private SkillTools skillTools;
 
 
     private static boolean serverShutdownExecuted = false;
     private static boolean serverShutdownExecuted = false;
 
 
@@ -571,8 +570,6 @@ public class mcMMO extends JavaPlugin {
         SoundConfig.getInstance();
         SoundConfig.getInstance();
         RankConfig.getInstance();
         RankConfig.getInstance();
 
 
-        new ChildConfig();
-
         List<Repairable> repairables = new ArrayList<>();
         List<Repairable> repairables = new ArrayList<>();
 
 
         if (generalConfig.getToolModsEnabled()) {
         if (generalConfig.getToolModsEnabled()) {

+ 0 - 3
src/main/java/com/gmail/nossr50/party/PartyManager.java

@@ -352,9 +352,6 @@ public final class PartyManager {
      * @param password    The password for this party, null if there was no password
      * @param password    The password for this party, null if there was no password
      */
      */
     public void createParty(@NotNull McMMOPlayer mcMMOPlayer, @NotNull String partyName, @Nullable String password) {
     public void createParty(@NotNull McMMOPlayer mcMMOPlayer, @NotNull String partyName, @Nullable String password) {
-        requireNonNull(mcMMOPlayer, "mcMMOPlayer cannot be null!");
-        requireNonNull(partyName, "partyName cannot be null!");
-
         Player player = mcMMOPlayer.getPlayer();
         Player player = mcMMOPlayer.getPlayer();
 
 
         Party party = new Party(new PartyLeader(player.getUniqueId(), player.getName()), partyName.replace(".", ""), password);
         Party party = new Party(new PartyLeader(player.getUniqueId(), player.getName()), partyName.replace(".", ""), password);

+ 5 - 0
src/main/java/com/gmail/nossr50/placeholders/PapiExpansion.java

@@ -41,6 +41,11 @@ public class PapiExpansion extends PlaceholderExpansion {
         return "1.0,0";
         return "1.0,0";
     }
     }
 
 
+    @Override
+    public boolean persist() {
+        return true;
+    }
+
     @Override
     @Override
     public String getRequiredPlugin() {
     public String getRequiredPlugin() {
         return "mcMMO";
         return "mcMMO";

+ 0 - 1
src/main/java/com/gmail/nossr50/runnables/SaveTimerTask.java

@@ -2,7 +2,6 @@ package com.gmail.nossr50.runnables;
 
 
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.runnables.player.PlayerProfileSaveTask;
 import com.gmail.nossr50.runnables.player.PlayerProfileSaveTask;
 import com.gmail.nossr50.util.CancellableRunnable;
 import com.gmail.nossr50.util.CancellableRunnable;
 import com.gmail.nossr50.util.LogUtils;
 import com.gmail.nossr50.util.LogUtils;

+ 0 - 1
src/main/java/com/gmail/nossr50/runnables/items/TeleportationWarmup.java

@@ -3,7 +3,6 @@ package com.gmail.nossr50.runnables.items;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.CancellableRunnable;
 import com.gmail.nossr50.util.CancellableRunnable;
 import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Misc;

+ 0 - 1
src/main/java/com/gmail/nossr50/runnables/party/PartyAutoKickTask.java

@@ -2,7 +2,6 @@ package com.gmail.nossr50.runnables.party;
 
 
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.party.PartyManager;
 import com.gmail.nossr50.util.CancellableRunnable;
 import com.gmail.nossr50.util.CancellableRunnable;
 import org.bukkit.OfflinePlayer;
 import org.bukkit.OfflinePlayer;
 
 

+ 1 - 3
src/main/java/com/gmail/nossr50/runnables/skills/AbilityCooldownTask.java

@@ -21,9 +21,7 @@ public class AbilityCooldownTask extends CancellableRunnable {
             return;
             return;
         }
         }
 
 
-        mcMMOPlayer.setAbilityInformed(ability, true);
-
+        mcMMOPlayer.setAbilityInformed(ability, true); // TODO: ?? What does this do again?
         NotificationManager.sendPlayerInformation(mcMMOPlayer.getPlayer(), NotificationType.ABILITY_REFRESHED, ability.getAbilityRefresh());
         NotificationManager.sendPlayerInformation(mcMMOPlayer.getPlayer(), NotificationType.ABILITY_REFRESHED, ability.getAbilityRefresh());
-        //mcMMOPlayer.getPlayer().sendMessage(ability.getAbilityRefresh());
     }
     }
 }
 }

+ 0 - 1
src/main/java/com/gmail/nossr50/runnables/skills/RuptureTask.java

@@ -1,6 +1,5 @@
 package com.gmail.nossr50.runnables.skills;
 package com.gmail.nossr50.runnables.skills;
 
 
-import com.gmail.nossr50.datatypes.MobHealthbarType;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.events.skills.rupture.McMMOEntityDamageByRuptureEvent;
 import com.gmail.nossr50.events.skills.rupture.McMMOEntityDamageByRuptureEvent;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;

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

@@ -14,10 +14,9 @@ import com.gmail.nossr50.util.MetadataConstants;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.ParticleEffectUtils;
 import com.gmail.nossr50.util.skills.ParticleEffectUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import org.bukkit.Location;
 import org.bukkit.Location;
 import org.bukkit.entity.Entity;
 import org.bukkit.entity.Entity;
@@ -94,7 +93,8 @@ public class AcrobaticsManager extends SkillManager {
         double modifiedDamage = Acrobatics.calculateModifiedDodgeDamage(damage, Acrobatics.dodgeDamageModifier);
         double modifiedDamage = Acrobatics.calculateModifiedDodgeDamage(damage, Acrobatics.dodgeDamageModifier);
         Player player = getPlayer();
         Player player = getPlayer();
 
 
-        if (!isFatal(modifiedDamage) && RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.ACROBATICS_DODGE, player)) {
+        if (!isFatal(modifiedDamage)
+                && ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.ACROBATICS_DODGE, player)) {
             ParticleEffectUtils.playDodgeEffect(player);
             ParticleEffectUtils.playDodgeEffect(player);
 
 
             if (mmoPlayer.useChatNotifications()) {
             if (mmoPlayer.useChatNotifications()) {

+ 6 - 7
src/main/java/com/gmail/nossr50/skills/archery/ArcheryManager.java

@@ -10,9 +10,8 @@ import com.gmail.nossr50.util.MetadataConstants;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import org.bukkit.Location;
 import org.bukkit.Location;
 import org.bukkit.entity.Entity;
 import org.bukkit.entity.Entity;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.LivingEntity;
@@ -53,7 +52,7 @@ public class ArcheryManager extends SkillManager {
      * @param target The {@link LivingEntity} damaged by the arrow
      * @param target The {@link LivingEntity} damaged by the arrow
      * @param arrow The {@link Entity} who shot the arrow
      * @param arrow The {@link Entity} who shot the arrow
      */
      */
-    public double distanceXpBonusMultiplier(LivingEntity target, Entity arrow) {
+    public static double distanceXpBonusMultiplier(LivingEntity target, Entity arrow) {
         //Hacky Fix - some plugins spawn arrows and assign them to players after the ProjectileLaunchEvent fires
         //Hacky Fix - some plugins spawn arrows and assign them to players after the ProjectileLaunchEvent fires
         if(!arrow.hasMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE))
         if(!arrow.hasMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE))
             return 1;
             return 1;
@@ -89,7 +88,7 @@ public class ArcheryManager extends SkillManager {
      * @param defender The {@link Player} being affected by the ability
      * @param defender The {@link Player} being affected by the ability
      */
      */
     public double daze(Player defender) {
     public double daze(Player defender) {
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.ARCHERY_DAZE, getPlayer())) {
+        if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.ARCHERY_DAZE, getPlayer())) {
             return 0;
             return 0;
         }
         }
 
 
@@ -118,10 +117,10 @@ public class ArcheryManager extends SkillManager {
      * @param oldDamage The raw damage value of this arrow before we modify it
      * @param oldDamage The raw damage value of this arrow before we modify it
      */
      */
     public double skillShot(double oldDamage) {
     public double skillShot(double oldDamage) {
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.ALWAYS_FIRES, SubSkillType.ARCHERY_SKILL_SHOT, getPlayer())) {
+        if (ProbabilityUtil.isNonRNGSkillActivationSuccessful(SubSkillType.ARCHERY_SKILL_SHOT, getPlayer())) {
+            return Archery.getSkillShotBonusDamage(getPlayer(), oldDamage);
+        } else {
             return oldDamage;
             return oldDamage;
         }
         }
-
-        return Archery.getSkillShotBonusDamage(getPlayer(), oldDamage);
     }
     }
 }
 }

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

@@ -11,8 +11,11 @@ import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.util.ItemUtils;
 import com.gmail.nossr50.util.ItemUtils;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
-import com.gmail.nossr50.util.skills.*;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
+import com.gmail.nossr50.util.skills.CombatUtils;
+import com.gmail.nossr50.util.skills.ParticleEffectUtils;
+import com.gmail.nossr50.util.skills.RankUtils;
+import com.gmail.nossr50.util.skills.SkillUtils;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.ItemStack;
@@ -66,11 +69,11 @@ public class AxesManager extends SkillManager {
      * Handle the effects of the Axe Mastery ability
      * Handle the effects of the Axe Mastery ability
      */
      */
     public double axeMastery() {
     public double axeMastery() {
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.ALWAYS_FIRES, SubSkillType.AXES_AXE_MASTERY, getPlayer())) {
-            return 0;
+        if (ProbabilityUtil.isNonRNGSkillActivationSuccessful(SubSkillType.AXES_AXE_MASTERY, getPlayer())) {
+            return Axes.getAxeMasteryBonusDamage(getPlayer());
         }
         }
 
 
-        return Axes.getAxeMasteryBonusDamage(getPlayer());
+        return 0;
     }
     }
 
 
     /**
     /**
@@ -80,7 +83,7 @@ public class AxesManager extends SkillManager {
      * @param damage The amount of damage initially dealt by the event
      * @param damage The amount of damage initially dealt by the event
      */
      */
     public double criticalHit(LivingEntity target, double damage) {
     public double criticalHit(LivingEntity target, double damage) {
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.AXES_CRITICAL_STRIKES, getPlayer())) {
+        if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.AXES_CRITICAL_STRIKES, getPlayer())) {
             return 0;
             return 0;
         }
         }
 
 
@@ -115,7 +118,7 @@ public class AxesManager extends SkillManager {
 
 
         for (ItemStack armor : target.getEquipment().getArmorContents()) {
         for (ItemStack armor : target.getEquipment().getArmorContents()) {
             if (armor != null && ItemUtils.isArmor(armor)) {
             if (armor != null && ItemUtils.isArmor(armor)) {
-                if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_STATIC_CHANCE, SubSkillType.AXES_ARMOR_IMPACT, getPlayer())) {
+                if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.AXES_ARMOR_IMPACT, getPlayer())) {
                     SkillUtils.handleArmorDurabilityChange(armor, durabilityDamage, 1);
                     SkillUtils.handleArmorDurabilityChange(armor, durabilityDamage, 1);
                 }
                 }
             }
             }
@@ -133,7 +136,7 @@ public class AxesManager extends SkillManager {
      */
      */
     public double greaterImpact(@NotNull LivingEntity target) {
     public double greaterImpact(@NotNull LivingEntity target) {
         //static chance (3rd param)
         //static chance (3rd param)
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_STATIC_CHANCE, SubSkillType.AXES_GREATER_IMPACT, getPlayer())) {
+        if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.AXES_GREATER_IMPACT, getPlayer())) {
             return 0;
             return 0;
         }
         }
 
 

+ 0 - 64
src/main/java/com/gmail/nossr50/skills/child/ChildConfig.java

@@ -1,64 +0,0 @@
-package com.gmail.nossr50.skills.child;
-
-import com.gmail.nossr50.config.BukkitConfig;
-import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
-import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.LogUtils;
-import com.gmail.nossr50.util.text.StringUtils;
-import org.bukkit.configuration.file.YamlConfiguration;
-
-import java.util.EnumSet;
-import java.util.Locale;
-
-public class ChildConfig extends BukkitConfig {
-    public ChildConfig() {
-        super("child.yml");
-        loadKeys();
-    }
-
-    @Override
-    protected void loadKeys() {
-        config.setDefaults(YamlConfiguration.loadConfiguration(mcMMO.p.getResourceAsReader("child.yml")));
-
-        FamilyTree.clearRegistrations(); // when reloading, need to clear statics
-
-        for (PrimarySkillType skill : mcMMO.p.getSkillTools().CHILD_SKILLS) {
-            LogUtils.debug(mcMMO.p.getLogger(), "Finding parents of " + skill.name());
-
-            EnumSet<PrimarySkillType> parentSkills = EnumSet.noneOf(PrimarySkillType.class);
-            boolean useDefaults = false; // If we had an error we back out and use defaults
-
-            for (String name : config.getStringList(StringUtils.getCapitalized(skill.name()))) {
-                try {
-                    PrimarySkillType parentSkill = PrimarySkillType.valueOf(name.toUpperCase(Locale.ENGLISH));
-                    FamilyTree.enforceNotChildSkill(parentSkill);
-                    parentSkills.add(parentSkill);
-                }
-                catch (IllegalArgumentException ex) {
-                    mcMMO.p.getLogger().warning(name + " is not a valid skill type, or is a child skill!");
-                    useDefaults = true;
-                    break;
-                }
-            }
-
-            if (useDefaults) {
-                parentSkills.clear();
-                for (String name : config.getDefaults().getStringList(StringUtils.getCapitalized(skill.name()))) {
-                    /* We do less checks in here because it's from inside our jar.
-                     * If they're dedicated enough to have modified it, they can have the errors it may produce.
-                     * Alternatively, this can be used to allow child skills to be parent skills, provided there are no circular dependencies this is an advanced sort of configuration.
-                     */
-                    parentSkills.add(PrimarySkillType.valueOf(name.toUpperCase(Locale.ENGLISH)));
-                }
-            }
-
-            // Register them
-            for (PrimarySkillType parentSkill : parentSkills) {
-                LogUtils.debug(mcMMO.p.getLogger(), "Registering " + parentSkill.name() + " as parent of " + skill.name());
-                FamilyTree.registerParent(skill, parentSkill);
-            }
-        }
-
-        FamilyTree.closeRegistration();
-    }
-}

+ 0 - 54
src/main/java/com/gmail/nossr50/skills/child/FamilyTree.java

@@ -1,54 +0,0 @@
-package com.gmail.nossr50.skills.child;
-
-import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
-import com.gmail.nossr50.util.skills.SkillTools;
-
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.Set;
-
-public class FamilyTree {
-    private static final HashMap<PrimarySkillType, Set<PrimarySkillType>> tree = new HashMap<>();
-
-    public static Set<PrimarySkillType> getParents(PrimarySkillType childSkill) {
-        enforceChildSkill(childSkill);
-
-        // We do not check if we have the child skill in question, as not having it would mean we did something wrong, and an NPE is desired.
-        return tree.get(childSkill);
-    }
-
-    protected static void registerParent(PrimarySkillType childSkill, PrimarySkillType parentSkill) {
-        enforceChildSkill(childSkill);
-        enforceNotChildSkill(parentSkill);
-
-        if (!tree.containsKey(childSkill)) {
-            tree.put(childSkill, EnumSet.noneOf(PrimarySkillType.class));
-        }
-
-        tree.get(childSkill).add(parentSkill);
-    }
-
-    protected static void closeRegistration() {
-        for (PrimarySkillType childSkill : tree.keySet()) {
-            Set<PrimarySkillType> immutableSet = Collections.unmodifiableSet(tree.get(childSkill));
-            tree.put(childSkill, immutableSet);
-        }
-    }
-
-    protected static void clearRegistrations() {
-        tree.clear();
-    }
-
-    protected static void enforceChildSkill(PrimarySkillType skill) {
-        if (!SkillTools.isChildSkill(skill)) {
-            throw new IllegalArgumentException(skill.name() + " is not a child skill!");
-        }
-    }
-
-    protected static void enforceNotChildSkill(PrimarySkillType skill) {
-        if (SkillTools.isChildSkill(skill)) {
-            throw new IllegalArgumentException(skill.name() + " is a child skill!");
-        }
-    }
-}

+ 41 - 0
src/main/java/com/gmail/nossr50/skills/crossbows/Crossbows.java

@@ -0,0 +1,41 @@
+package com.gmail.nossr50.skills.crossbows;
+
+import com.gmail.nossr50.datatypes.player.McMMOPlayer;
+import com.gmail.nossr50.util.player.UserManager;
+import org.bukkit.entity.Arrow;
+import org.bukkit.entity.Player;
+import org.bukkit.event.entity.ProjectileHitEvent;
+import org.bukkit.plugin.Plugin;
+
+import static com.gmail.nossr50.util.skills.ProjectileUtils.getNormal;
+
+/**
+ * Util class for crossbows.
+ */
+public class Crossbows {
+    /**
+     * Process events that may happen from a crossbow hitting an entity.
+     *
+     * @param event      the projectile hit event
+     * @param pluginRef  the plugin ref
+     * @param arrow      the arrow
+     */
+    public static void processCrossbows(ProjectileHitEvent event, Plugin pluginRef, Arrow arrow) {
+        if (arrow.getShooter() instanceof Player) {
+            McMMOPlayer mmoPlayer = UserManager.getPlayer((Player) arrow.getShooter());
+            if (mmoPlayer == null)
+                return;
+
+            processTrickShot(event, pluginRef, arrow, mmoPlayer);
+        }
+    }
+
+    private static void processTrickShot(ProjectileHitEvent event, Plugin pluginRef, Arrow arrow, McMMOPlayer mmoPlayer) {
+        if(event.getHitBlock() != null && event.getHitBlockFace() != null) {
+            mmoPlayer.getCrossbowsManager().handleRicochet(
+                    pluginRef,
+                    arrow,
+                    getNormal(event.getHitBlockFace()));
+        }
+    }
+}

+ 108 - 0
src/main/java/com/gmail/nossr50/skills/crossbows/CrossbowsManager.java

@@ -0,0 +1,108 @@
+package com.gmail.nossr50.skills.crossbows;
+
+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.mcMMO;
+import com.gmail.nossr50.skills.SkillManager;
+import com.gmail.nossr50.util.MetadataConstants;
+import com.gmail.nossr50.util.Permissions;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
+import com.gmail.nossr50.util.skills.ProjectileUtils;
+import com.gmail.nossr50.util.skills.RankUtils;
+import org.bukkit.Location;
+import org.bukkit.entity.AbstractArrow;
+import org.bukkit.entity.Arrow;
+import org.bukkit.entity.Player;
+import org.bukkit.metadata.FixedMetadataValue;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.projectiles.ProjectileSource;
+import org.bukkit.util.Vector;
+import org.jetbrains.annotations.NotNull;
+
+import static com.gmail.nossr50.util.skills.CombatUtils.delayArrowMetaCleanup;
+
+public class CrossbowsManager extends SkillManager {
+    public CrossbowsManager(McMMOPlayer mmoPlayer) {
+        super(mmoPlayer, PrimarySkillType.CROSSBOWS);
+    }
+
+    public void handleRicochet(@NotNull Plugin pluginRef, @NotNull Arrow arrow, @NotNull Vector hitBlockNormal) {
+        if(!arrow.isShotFromCrossbow())
+            return;
+
+        // Check player permission
+        if (!Permissions.trickShot(mmoPlayer.getPlayer())) {
+            return;
+        }
+
+        // TODO: Add an event for this for plugins to hook into
+        spawnReflectedArrow(pluginRef, arrow, arrow.getLocation(), hitBlockNormal);
+    }
+
+    private void spawnReflectedArrow(@NotNull Plugin pluginRef, @NotNull Arrow originalArrow,
+                                    @NotNull Location origin, @NotNull Vector normal) {
+        int bounceCount = 0;
+
+        if (originalArrow.hasMetadata(MetadataConstants.METADATA_KEY_BOUNCE_COUNT)) {
+            bounceCount = originalArrow.getMetadata(MetadataConstants.METADATA_KEY_BOUNCE_COUNT).get(0).asInt();
+            if (bounceCount >= getTrickShotMaxBounceCount()) {
+                return;
+            }
+        }
+
+        final ProjectileSource originalArrowShooter = originalArrow.getShooter();
+        final Vector arrowInBlockVector = originalArrow.getVelocity();
+        final Vector reflectedDirection = arrowInBlockVector.subtract(normal.multiply(2 * arrowInBlockVector.dot(normal)));
+        final Vector inverseNormal = normal.multiply(-1);
+
+        // check the angle of the arrow against the inverse normal to see if the angle was too shallow
+        // only checks angle on the first bounce
+        if (bounceCount == 0 && arrowInBlockVector.angle(inverseNormal) < Math.PI / 4) {
+            return;
+        }
+
+        // Spawn new arrow with the reflected direction
+        Arrow spawnedArrow = originalArrow.getWorld().spawnArrow(origin, reflectedDirection, 1, 1);
+        ProjectileUtils.copyArrowMetadata(pluginRef, originalArrow, spawnedArrow);
+        originalArrow.remove();
+        // copy metadata from old arrow
+        spawnedArrow.setShooter(originalArrowShooter);
+        spawnedArrow.setMetadata(MetadataConstants.METADATA_KEY_BOUNCE_COUNT,
+                new FixedMetadataValue(pluginRef, bounceCount + 1));
+        spawnedArrow.setMetadata(MetadataConstants.METADATA_KEY_SPAWNED_ARROW,
+                new FixedMetadataValue(pluginRef, originalArrowShooter));
+
+        // Don't allow multi-shot or infinite arrows to be picked up
+        if (spawnedArrow.hasMetadata(MetadataConstants.METADATA_KEY_MULTI_SHOT_ARROW)
+                || spawnedArrow.hasMetadata(MetadataConstants.METADATA_KEY_INF_ARROW)) {
+            spawnedArrow.setPickupStatus(AbstractArrow.PickupStatus.DISALLOWED);
+        }
+
+        // Schedule cleanup of metadata in case metadata cleanup fails
+        delayArrowMetaCleanup(spawnedArrow);
+    }
+
+    public int getTrickShotMaxBounceCount() {
+        return RankUtils.getRank(mmoPlayer, SubSkillType.CROSSBOWS_TRICK_SHOT);
+    }
+
+    public double getPoweredShotBonusDamage(Player player, double oldDamage)
+    {
+        double damageBonusPercent = getDamageBonusPercent(player);
+        double newDamage = oldDamage + (oldDamage * damageBonusPercent);
+        return Math.min(newDamage, (oldDamage + mcMMO.p.getAdvancedConfig().getPoweredShotDamageMax()));
+    }
+
+    public double getDamageBonusPercent(Player player) {
+        return ((RankUtils.getRank(player, SubSkillType.CROSSBOWS_POWERED_SHOT)) * (mcMMO.p.getAdvancedConfig().getPoweredShotRankDamageMultiplier()) / 100.0D);
+    }
+
+    public double poweredShot(double oldDamage) {
+        if (ProbabilityUtil.isNonRNGSkillActivationSuccessful(SubSkillType.CROSSBOWS_POWERED_SHOT, getPlayer())) {
+            return getPoweredShotBonusDamage(getPlayer(), oldDamage);
+        } else {
+            return oldDamage;
+        }
+    }
+}

+ 32 - 13
src/main/java/com/gmail/nossr50/skills/excavation/ExcavationManager.java

@@ -2,6 +2,7 @@ package com.gmail.nossr50.skills.excavation;
 
 
 import com.gmail.nossr50.api.ItemSpawnReason;
 import com.gmail.nossr50.api.ItemSpawnReason;
 import com.gmail.nossr50.datatypes.experience.XPGainReason;
 import com.gmail.nossr50.datatypes.experience.XPGainReason;
+import com.gmail.nossr50.datatypes.experience.XPGainSource;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
@@ -10,15 +11,19 @@ import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import org.bukkit.Location;
 import org.bukkit.Location;
 import org.bukkit.block.BlockState;
 import org.bukkit.block.BlockState;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.VisibleForTesting;
 
 
 import java.util.List;
 import java.util.List;
 
 
+import static java.util.Objects.requireNonNull;
+
 public class ExcavationManager extends SkillManager {
 public class ExcavationManager extends SkillManager {
     public ExcavationManager(McMMOPlayer mcMMOPlayer) {
     public ExcavationManager(McMMOPlayer mcMMOPlayer) {
         super(mcMMOPlayer, PrimarySkillType.EXCAVATION);
         super(mcMMOPlayer, PrimarySkillType.EXCAVATION);
@@ -31,9 +36,9 @@ public class ExcavationManager extends SkillManager {
      */
      */
     public void excavationBlockCheck(BlockState blockState) {
     public void excavationBlockCheck(BlockState blockState) {
         int xp = Excavation.getBlockXP(blockState);
         int xp = Excavation.getBlockXP(blockState);
-
+        requireNonNull(blockState, "excavationBlockCheck: blockState cannot be null");
         if (Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.EXCAVATION_ARCHAEOLOGY)) {
         if (Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.EXCAVATION_ARCHAEOLOGY)) {
-            List<ExcavationTreasure> treasures = Excavation.getTreasures(blockState);
+            List<ExcavationTreasure> treasures = getTreasures(blockState);
 
 
             if (!treasures.isEmpty()) {
             if (!treasures.isEmpty()) {
                 int skillLevel = getSkillLevel();
                 int skillLevel = getSkillLevel();
@@ -41,21 +46,35 @@ public class ExcavationManager extends SkillManager {
 
 
                 for (ExcavationTreasure treasure : treasures) {
                 for (ExcavationTreasure treasure : treasures) {
                     if (skillLevel >= treasure.getDropLevel()
                     if (skillLevel >= treasure.getDropLevel()
-                            && RandomChanceUtil.checkRandomChanceExecutionSuccess(getPlayer(), PrimarySkillType.EXCAVATION, treasure.getDropChance())) {
-
-                        //Spawn Vanilla XP orbs if a dice roll succeeds
-                        if(RandomChanceUtil.rollDice(getArchaelogyExperienceOrbChance(), 100)) {
-                            Misc.spawnExperienceOrb(location, getExperienceOrbsReward());
-                        }
-
-                        xp += treasure.getXp();
-                        Misc.spawnItem(getPlayer(), location, treasure.getDrop(), ItemSpawnReason.EXCAVATION_TREASURE);
+                            && ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.EXCAVATION, getPlayer(), treasure.getDropProbability())) {
+                        processExcavationBonusesOnBlock(blockState, treasure, location);
                     }
                     }
                 }
                 }
             }
             }
         }
         }
 
 
-        applyXpGain(xp, XPGainReason.PVE);
+        applyXpGain(xp, XPGainReason.PVE, XPGainSource.SELF);
+    }
+
+    @VisibleForTesting
+    public List<ExcavationTreasure> getTreasures(@NotNull BlockState blockState) {
+        requireNonNull(blockState, "blockState cannot be null");
+        return Excavation.getTreasures(blockState);
+    }
+
+    @VisibleForTesting
+    public void processExcavationBonusesOnBlock(BlockState blockState, ExcavationTreasure treasure, Location location) {
+        //Spawn Vanilla XP orbs if a dice roll succeeds
+        if(ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.EXCAVATION, getPlayer(), getArchaelogyExperienceOrbChance())) {
+            Misc.spawnExperienceOrb(location, getExperienceOrbsReward());
+        }
+
+        int xp = 0;
+        xp += treasure.getXp();
+        Misc.spawnItem(getPlayer(), location, treasure.getDrop(), ItemSpawnReason.EXCAVATION_TREASURE);
+        if (xp > 0) {
+            applyXpGain(xp, XPGainReason.PVE, XPGainSource.SELF);
+        }
     }
     }
 
 
     public int getExperienceOrbsReward() {
     public int getExperienceOrbsReward() {

+ 8 - 6
src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java

@@ -18,8 +18,7 @@ import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.compat.layers.skills.MasterAnglerCompatibilityLayer;
 import com.gmail.nossr50.util.compat.layers.skills.MasterAnglerCompatibilityLayer;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
-import com.gmail.nossr50.util.random.RandomChanceSkillStatic;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
@@ -61,11 +60,14 @@ public class FishingManager extends SkillManager {
     }
     }
 
 
     public boolean canShake(Entity target) {
     public boolean canShake(Entity target) {
-        return target instanceof LivingEntity && RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.FISHING_SHAKE) && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.FISHING_SHAKE);
+        return target instanceof LivingEntity && RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.FISHING_SHAKE)
+                && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.FISHING_SHAKE);
     }
     }
 
 
     public boolean canMasterAngler() {
     public boolean canMasterAngler() {
-        return mcMMO.getCompatibilityManager().getMasterAnglerCompatibilityLayer() != null && getSkillLevel() >= RankUtils.getUnlockLevel(SubSkillType.FISHING_MASTER_ANGLER) && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.FISHING_MASTER_ANGLER);
+        return mcMMO.getCompatibilityManager().getMasterAnglerCompatibilityLayer() != null
+                && getSkillLevel() >= RankUtils.getUnlockLevel(SubSkillType.FISHING_MASTER_ANGLER)
+                && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.FISHING_MASTER_ANGLER);
     }
     }
 
 
 //    public void setFishingRodCastTimestamp()
 //    public void setFishingRodCastTimestamp()
@@ -477,8 +479,8 @@ public class FishingManager extends SkillManager {
      *
      *
      * @param target The {@link LivingEntity} affected by the ability
      * @param target The {@link LivingEntity} affected by the ability
      */
      */
-    public void shakeCheck(LivingEntity target) {
-        if (RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(getShakeChance(), getPlayer(), SubSkillType.FISHING_SHAKE))) {
+    public void shakeCheck(@NotNull LivingEntity target) {
+        if (ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.FISHING, getPlayer(), getShakeChance())) {
             List<ShakeTreasure> possibleDrops = Fishing.findPossibleDrops(target);
             List<ShakeTreasure> possibleDrops = Fishing.findPossibleDrops(target);
 
 
             if (possibleDrops == null || possibleDrops.isEmpty()) {
             if (possibleDrops == null || possibleDrops.isEmpty()) {

+ 6 - 8
src/main/java/com/gmail/nossr50/skills/herbalism/HerbalismManager.java

@@ -20,10 +20,8 @@ import com.gmail.nossr50.runnables.skills.DelayedHerbalismXPCheckTask;
 import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
-import com.gmail.nossr50.util.random.RandomChanceSkillStatic;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.sounds.SoundManager;
 import com.gmail.nossr50.util.sounds.SoundManager;
 import com.gmail.nossr50.util.sounds.SoundType;
 import com.gmail.nossr50.util.sounds.SoundType;
@@ -650,7 +648,7 @@ public class HerbalismManager extends SkillManager {
      * @return true if the ability was successful, false otherwise
      * @return true if the ability was successful, false otherwise
      */
      */
     public boolean processGreenThumbBlocks(BlockState blockState) {
     public boolean processGreenThumbBlocks(BlockState blockState) {
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.HERBALISM_GREEN_THUMB, getPlayer())) {
+        if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.HERBALISM_GREEN_THUMB, getPlayer())) {
             NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE_FAILED, "Herbalism.Ability.GTh.Fail");
             NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE_FAILED, "Herbalism.Ability.GTh.Fail");
             return false;
             return false;
         }
         }
@@ -665,7 +663,7 @@ public class HerbalismManager extends SkillManager {
      * @return true if the ability was successful, false otherwise
      * @return true if the ability was successful, false otherwise
      */
      */
     public boolean processHylianLuck(BlockState blockState) {
     public boolean processHylianLuck(BlockState blockState) {
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.HERBALISM_HYLIAN_LUCK, getPlayer())) {
+        if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.HERBALISM_HYLIAN_LUCK, getPlayer())) {
             return false;
             return false;
         }
         }
 
 
@@ -684,7 +682,7 @@ public class HerbalismManager extends SkillManager {
 
 
         for (HylianTreasure treasure : treasures) {
         for (HylianTreasure treasure : treasures) {
             if (skillLevel >= treasure.getDropLevel()
             if (skillLevel >= treasure.getDropLevel()
-                    && RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(treasure.getDropChance(), getPlayer(), SubSkillType.HERBALISM_HYLIAN_LUCK))) {
+                    && ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.HERBALISM, player, treasure.getDropChance())) {
                 if (!EventUtils.simulateBlockBreak(blockState.getBlock(), player)) {
                 if (!EventUtils.simulateBlockBreak(blockState.getBlock(), player)) {
                     return false;
                     return false;
                 }
                 }
@@ -721,7 +719,7 @@ public class HerbalismManager extends SkillManager {
         playerInventory.removeItem(new ItemStack(Material.RED_MUSHROOM));
         playerInventory.removeItem(new ItemStack(Material.RED_MUSHROOM));
         player.updateInventory();
         player.updateInventory();
 
 
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.HERBALISM_SHROOM_THUMB, player)) {
+        if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.HERBALISM_SHROOM_THUMB, player)) {
             NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE_FAILED, "Herbalism.Ability.ShroomThumb.Fail");
             NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE_FAILED, "Herbalism.Ability.ShroomThumb.Fail");
             return false;
             return false;
         }
         }
@@ -805,7 +803,7 @@ public class HerbalismManager extends SkillManager {
             return false;
             return false;
         }
         }
 
 
-        if (!greenTerra && !RandomChanceUtil.checkRandomChanceExecutionSuccess(player, SubSkillType.HERBALISM_GREEN_THUMB, true)) {
+        if (!greenTerra && !ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.HERBALISM_GREEN_THUMB, player)) {
             return false;
             return false;
         }
         }
 
 

+ 32 - 4
src/main/java/com/gmail/nossr50/skills/mining/MiningManager.java

@@ -13,7 +13,7 @@ import com.gmail.nossr50.runnables.skills.AbilityCooldownTask;
 import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import org.apache.commons.lang.math.RandomUtils;
 import org.apache.commons.lang.math.RandomUtils;
@@ -35,7 +35,7 @@ public class MiningManager extends SkillManager {
 
 
     public static final String BUDDING_AMETHYST = "budding_amethyst";
     public static final String BUDDING_AMETHYST = "budding_amethyst";
 
 
-    public MiningManager(McMMOPlayer mcMMOPlayer) {
+    public MiningManager(@NotNull McMMOPlayer mcMMOPlayer) {
         super(mcMMOPlayer, PrimarySkillType.MINING);
         super(mcMMOPlayer, PrimarySkillType.MINING);
     }
     }
 
 
@@ -70,6 +70,11 @@ public class MiningManager extends SkillManager {
         return RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.MINING_DOUBLE_DROPS) && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.MINING_DOUBLE_DROPS);
         return RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.MINING_DOUBLE_DROPS) && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.MINING_DOUBLE_DROPS);
     }
     }
 
 
+    public boolean canMotherLode() {
+        return Permissions.canUseSubSkill(getPlayer(), SubSkillType.MINING_MOTHER_LODE);
+    }
+
+
     /**
     /**
      * Process double drops & XP gain for Mining.
      * Process double drops & XP gain for Mining.
      *
      *
@@ -96,9 +101,32 @@ public class MiningManager extends SkillManager {
         if(silkTouch && !mcMMO.p.getAdvancedConfig().getDoubleDropSilkTouchEnabled())
         if(silkTouch && !mcMMO.p.getAdvancedConfig().getDoubleDropSilkTouchEnabled())
             return;
             return;
 
 
+        //Mining mastery allows for a chance of triple drops
+        if(canMotherLode()) {
+            //Triple Drops failed so do a normal double drops check
+            if(!processTripleDrops(blockState)) {
+                processDoubleDrops(blockState);
+            }
+        } else {
+            //If the user has no mastery, proceed with normal double drop routine
+            processDoubleDrops(blockState);
+        }
+    }
+
+    private boolean processTripleDrops(@NotNull BlockState blockState) {
+        //TODO: Make this readable
+        if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.MINING_MOTHER_LODE, getPlayer())) {
+            BlockUtils.markDropsAsBonus(blockState, 2);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private void processDoubleDrops(@NotNull BlockState blockState) {
         //TODO: Make this readable
         //TODO: Make this readable
-        if (RandomChanceUtil.checkRandomChanceExecutionSuccess(getPlayer(), SubSkillType.MINING_DOUBLE_DROPS, true)) {
-            boolean useTriple = mmoPlayer.getAbilityMode(mcMMO.p.getSkillTools().getSuperAbility(skill)) && mcMMO.p.getAdvancedConfig().getAllowMiningTripleDrops();
+        if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.MINING_DOUBLE_DROPS, getPlayer())) {
+            boolean useTriple = mmoPlayer.getAbilityMode(SuperAbilityType.SUPER_BREAKER) && mcMMO.p.getAdvancedConfig().getAllowMiningTripleDrops();
             BlockUtils.markDropsAsBonus(blockState, useTriple);
             BlockUtils.markDropsAsBonus(blockState, useTriple);
         }
         }
     }
     }

+ 4 - 6
src/main/java/com/gmail/nossr50/skills/repair/RepairManager.java

@@ -14,10 +14,8 @@ import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
-import com.gmail.nossr50.util.random.RandomChanceSkillStatic;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.sounds.SoundManager;
 import com.gmail.nossr50.util.sounds.SoundManager;
 import com.gmail.nossr50.util.sounds.SoundType;
 import com.gmail.nossr50.util.sounds.SoundType;
@@ -322,7 +320,7 @@ public class RepairManager extends SkillManager {
         if(!RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.REPAIR_SUPER_REPAIR))
         if(!RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.REPAIR_SUPER_REPAIR))
             return false;
             return false;
 
 
-        if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.REPAIR_SUPER_REPAIR, getPlayer())) {
+        if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.REPAIR_SUPER_REPAIR, getPlayer())) {
             NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Repair.Skills.FeltEasy");
             NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Repair.Skills.FeltEasy");
             return true;
             return true;
         }
         }
@@ -373,10 +371,10 @@ public class RepairManager extends SkillManager {
 
 
             Enchantment enchantment = enchant.getKey();
             Enchantment enchantment = enchant.getKey();
 
 
-            if (RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(getKeepEnchantChance(), getPlayer(), SubSkillType.REPAIR_ARCANE_FORGING))) {
+            if (ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.REPAIR, getPlayer(), getKeepEnchantChance())) {
 
 
                 if (ArcaneForging.arcaneForgingDowngrades && enchantLevel > 1
                 if (ArcaneForging.arcaneForgingDowngrades && enchantLevel > 1
-                        && (!RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(100 - getDowngradeEnchantChance(), getPlayer(), SubSkillType.REPAIR_ARCANE_FORGING)))) {
+                        && (!ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.REPAIR, getPlayer(), 100 - getDowngradeEnchantChance()))) {
                     item.addUnsafeEnchantment(enchantment, enchantLevel - 1);
                     item.addUnsafeEnchantment(enchantment, enchantLevel - 1);
                     downgraded = true;
                     downgraded = true;
                 }
                 }

+ 4 - 5
src/main/java/com/gmail/nossr50/skills/salvage/SalvageManager.java

@@ -14,8 +14,7 @@ import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
-import com.gmail.nossr50.util.random.RandomChanceSkillStatic;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.sounds.SoundManager;
 import com.gmail.nossr50.util.sounds.SoundManager;
@@ -121,7 +120,7 @@ public class SalvageManager extends SkillManager {
 
 
         for(int x = 0; x < potentialSalvageYield-1; x++) {
         for(int x = 0; x < potentialSalvageYield-1; x++) {
 
 
-            if(RandomChanceUtil.rollDice(chanceOfSuccess, 100)) {
+            if(ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.SALVAGE, player, chanceOfSuccess)) {
                 chanceOfSuccess-=3;
                 chanceOfSuccess-=3;
                 chanceOfSuccess = Math.max(chanceOfSuccess, 90);
                 chanceOfSuccess = Math.max(chanceOfSuccess, 90);
 
 
@@ -252,12 +251,12 @@ public class SalvageManager extends SkillManager {
 
 
             if (!Salvage.arcaneSalvageEnchantLoss
             if (!Salvage.arcaneSalvageEnchantLoss
                     || Permissions.hasSalvageEnchantBypassPerk(player)
                     || Permissions.hasSalvageEnchantBypassPerk(player)
-                    || RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(getExtractFullEnchantChance(), getPlayer(), SubSkillType.SALVAGE_ARCANE_SALVAGE))) {
+                    || ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.SALVAGE, player, getExtractFullEnchantChance())) {
                 enchantMeta.addStoredEnchant(enchant.getKey(), enchantLevel, true);
                 enchantMeta.addStoredEnchant(enchant.getKey(), enchantLevel, true);
             }
             }
             else if (enchantLevel > 1
             else if (enchantLevel > 1
                     && Salvage.arcaneSalvageDowngrades
                     && Salvage.arcaneSalvageDowngrades
-                    && RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(getExtractPartialEnchantChance(), getPlayer(), SubSkillType.SALVAGE_ARCANE_SALVAGE))) {
+                    && ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.SALVAGE, player, getExtractPartialEnchantChance())) {
                 enchantMeta.addStoredEnchant(enchant.getKey(), enchantLevel - 1, true);
                 enchantMeta.addStoredEnchant(enchant.getKey(), enchantLevel - 1, true);
                 downgraded = true;
                 downgraded = true;
             } else {
             } else {

+ 2 - 3
src/main/java/com/gmail/nossr50/skills/smelting/SmeltingManager.java

@@ -8,9 +8,8 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import org.bukkit.block.Furnace;
 import org.bukkit.block.Furnace;
 import org.bukkit.event.inventory.FurnaceBurnEvent;
 import org.bukkit.event.inventory.FurnaceBurnEvent;
 import org.bukkit.event.inventory.FurnaceSmeltEvent;
 import org.bukkit.event.inventory.FurnaceSmeltEvent;
@@ -25,7 +24,7 @@ public class SmeltingManager extends SkillManager {
 
 
     public boolean isSecondSmeltSuccessful() {
     public boolean isSecondSmeltSuccessful() {
         return Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.SMELTING_SECOND_SMELT)
         return Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.SMELTING_SECOND_SMELT)
-                && RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.SMELTING_SECOND_SMELT, getPlayer());
+                && ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.SMELTING_SECOND_SMELT, getPlayer());
     }
     }
 
 
     /**
     /**

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

@@ -14,10 +14,9 @@ import com.gmail.nossr50.util.ItemUtils;
 import com.gmail.nossr50.util.MetadataConstants;
 import com.gmail.nossr50.util.MetadataConstants;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import org.bukkit.entity.Entity;
 import org.bukkit.entity.Entity;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
@@ -76,7 +75,8 @@ public class SwordsManager extends SkillManager {
             return; //Don't apply bleed
             return; //Don't apply bleed
         }
         }
 
 
-        if (RandomChanceUtil.rollDice(mcMMO.p.getAdvancedConfig().getRuptureChanceToApplyOnHit(getRuptureRank()), 100)) {
+        double ruptureOdds = mcMMO.p.getAdvancedConfig().getRuptureChanceToApplyOnHit(getRuptureRank());
+        if (ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.SWORDS, this.getPlayer(), ruptureOdds)) {
 
 
             if (target instanceof Player defender) {
             if (target instanceof Player defender) {
 
 
@@ -141,7 +141,8 @@ public class SwordsManager extends SkillManager {
      * @param damage The amount of damage initially dealt by the event
      * @param damage The amount of damage initially dealt by the event
      */
      */
     public void counterAttackChecks(@NotNull 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())) {
+
+        if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.SWORDS_COUNTER_ATTACK, getPlayer())) {
             CombatUtils.dealDamage(attacker, damage / Swords.counterAttackModifier, getPlayer());
             CombatUtils.dealDamage(attacker, damage / Swords.counterAttackModifier, getPlayer());
 
 
             NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Swords.Combat.Countered");
             NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Swords.Combat.Countered");

+ 8 - 21
src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java

@@ -15,11 +15,9 @@ import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
-import com.gmail.nossr50.util.random.RandomChanceSkillStatic;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.ParticleEffectUtils;
 import com.gmail.nossr50.util.skills.ParticleEffectUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.sounds.SoundManager;
 import com.gmail.nossr50.util.sounds.SoundManager;
 import com.gmail.nossr50.util.sounds.SoundType;
 import com.gmail.nossr50.util.sounds.SoundType;
 import com.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.StringUtils;
@@ -145,7 +143,7 @@ public class TamingManager extends SkillManager {
      * @param damage The damage being absorbed by the wolf
      * @param damage The damage being absorbed by the wolf
      */
      */
     public void fastFoodService(@NotNull Wolf wolf, double damage) {
     public void fastFoodService(@NotNull Wolf wolf, double damage) {
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_STATIC_CHANCE, SubSkillType.TAMING_FAST_FOOD_SERVICE, getPlayer())) {
+        if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.TAMING_FAST_FOOD_SERVICE, getPlayer())) {
             return;
             return;
         }
         }
 
 
@@ -165,12 +163,6 @@ public class TamingManager extends SkillManager {
      * @param damage The initial damage
      * @param damage The initial damage
      */
      */
     public double gore(@NotNull LivingEntity target, double damage) {
     public double gore(@NotNull LivingEntity target, double damage) {
-//        if (target instanceof Player) {
-//            NotificationManager.sendPlayerInformation((Player)target, NotificationType.SUBSKILL_MESSAGE, "Combat.StruckByGore");
-//        }
-//
-//        NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Combat.Gore");
-
         damage = (damage * Taming.goreModifier) - damage;
         damage = (damage * Taming.goreModifier) - damage;
 
 
         return damage;
         return damage;
@@ -270,7 +262,7 @@ public class TamingManager extends SkillManager {
         if(!RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.TAMING_PUMMEL))
         if(!RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.TAMING_PUMMEL))
             return;
             return;
 
 
-        if(!RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(mcMMO.p.getAdvancedConfig().getPummelChance(), getPlayer(), SubSkillType.TAMING_PUMMEL)))
+        if(!ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.TAMING, getPlayer(), mcMMO.p.getAdvancedConfig().getPummelChance()))
             return;
             return;
 
 
         ParticleEffectUtils.playGreaterImpactEffect(target);
         ParticleEffectUtils.playGreaterImpactEffect(target);
@@ -380,17 +372,12 @@ public class TamingManager extends SkillManager {
     }
     }
 
 
     private void spawnCOTWEntity(CallOfTheWildType callOfTheWildType, Location spawnLocation, EntityType entityType) {
     private void spawnCOTWEntity(CallOfTheWildType callOfTheWildType, Location spawnLocation, EntityType entityType) {
-        switch(callOfTheWildType) {
-            case CAT:
+        switch (callOfTheWildType) {
+            case CAT ->
                 //Entity type is needed for cats because in 1.13 and below we spawn ocelots, in 1.14 and above we spawn cats
                 //Entity type is needed for cats because in 1.13 and below we spawn ocelots, in 1.14 and above we spawn cats
-                spawnCat(spawnLocation, entityType);
-                break;
-            case HORSE:
-                spawnHorse(spawnLocation);
-                break;
-            case WOLF:
-                spawnWolf(spawnLocation);
-                break;
+                    spawnCat(spawnLocation, entityType);
+            case HORSE -> spawnHorse(spawnLocation);
+            case WOLF -> spawnWolf(spawnLocation);
         }
         }
     }
     }
 
 

+ 35 - 0
src/main/java/com/gmail/nossr50/skills/tridents/TridentsManager.java

@@ -0,0 +1,35 @@
+package com.gmail.nossr50.skills.tridents;
+
+import com.gmail.nossr50.datatypes.player.McMMOPlayer;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+import com.gmail.nossr50.datatypes.skills.SubSkillType;
+import com.gmail.nossr50.datatypes.skills.ToolType;
+import com.gmail.nossr50.skills.SkillManager;
+import com.gmail.nossr50.util.Permissions;
+import com.gmail.nossr50.util.skills.RankUtils;
+
+public class TridentsManager extends SkillManager {
+    public TridentsManager(McMMOPlayer mmoPlayer) {
+        super(mmoPlayer, PrimarySkillType.TRIDENTS);
+    }
+
+    /**
+     * Checks if the player can activate the Super Ability for Tridents
+     * @return true if the player can activate the Super Ability, false otherwise
+     */
+    public boolean canActivateAbility() {
+        return mmoPlayer.getToolPreparationMode(ToolType.TRIDENTS) && Permissions.tridentsSuper(getPlayer());
+    }
+
+    public double impaleDamageBonus() {
+        int rank = RankUtils.getRank(getPlayer(), SubSkillType.TRIDENTS_IMPALE);
+
+        if(rank > 1) {
+            return (1.0D + (rank * .5D));
+        } else if(rank == 1) {
+            return 1.0D;
+        }
+
+        return 0.0D;
+    }
+}

+ 8 - 9
src/main/java/com/gmail/nossr50/skills/unarmed/UnarmedManager.java

@@ -12,9 +12,8 @@ import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import org.bukkit.Material;
 import org.bukkit.Material;
 import org.bukkit.block.BlockState;
 import org.bukkit.block.BlockState;
 import org.bukkit.entity.Item;
 import org.bukkit.entity.Item;
@@ -68,7 +67,7 @@ public class UnarmedManager extends SkillManager {
     }
     }
 
 
     public boolean blockCrackerCheck(@NotNull BlockState blockState) {
     public boolean blockCrackerCheck(@NotNull BlockState blockState) {
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.ALWAYS_FIRES, SubSkillType.UNARMED_BLOCK_CRACKER, getPlayer())) {
+        if (!ProbabilityUtil.isNonRNGSkillActivationSuccessful(SubSkillType.UNARMED_BLOCK_CRACKER, getPlayer())) {
             return false;
             return false;
         }
         }
 
 
@@ -99,7 +98,7 @@ public class UnarmedManager extends SkillManager {
      * @param defender The defending player
      * @param defender The defending player
      */
      */
     public void disarmCheck(@NotNull Player defender) {
     public void disarmCheck(@NotNull Player defender) {
-        if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.UNARMED_DISARM, getPlayer()) && !hasIronGrip(defender)) {
+        if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.UNARMED_DISARM, getPlayer()) && !hasIronGrip(defender)) {
             if (EventUtils.callDisarmEvent(defender).isCancelled()) {
             if (EventUtils.callDisarmEvent(defender).isCancelled()) {
                 return;
                 return;
             }
             }
@@ -122,7 +121,7 @@ public class UnarmedManager extends SkillManager {
      * Check for arrow deflection.
      * Check for arrow deflection.
      */
      */
     public boolean deflectCheck() {
     public boolean deflectCheck() {
-        if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.UNARMED_ARROW_DEFLECT, getPlayer())) {
+        if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.UNARMED_ARROW_DEFLECT, getPlayer())) {
             NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Combat.ArrowDeflect");
             NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Combat.ArrowDeflect");
             return true;
             return true;
         }
         }
@@ -145,11 +144,11 @@ public class UnarmedManager extends SkillManager {
      * Handle the effects of the Iron Arm ability
      * Handle the effects of the Iron Arm ability
      */
      */
     public double calculateSteelArmStyleDamage() {
     public double calculateSteelArmStyleDamage() {
-        if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.ALWAYS_FIRES, SubSkillType.UNARMED_STEEL_ARM_STYLE, getPlayer())) {
-            return 0;
+        if (ProbabilityUtil.isNonRNGSkillActivationSuccessful(SubSkillType.UNARMED_STEEL_ARM_STYLE, getPlayer())) {
+            return getSteelArmStyleDamage();
         }
         }
 
 
-        return getSteelArmStyleDamage();
+        return 0;
     }
     }
 
 
     public double getSteelArmStyleDamage() {
     public double getSteelArmStyleDamage() {
@@ -179,7 +178,7 @@ public class UnarmedManager extends SkillManager {
     private boolean hasIronGrip(@NotNull Player defender) {
     private boolean hasIronGrip(@NotNull Player defender) {
         if (!Misc.isNPCEntityExcludingVillagers(defender)
         if (!Misc.isNPCEntityExcludingVillagers(defender)
                 && Permissions.isSubSkillEnabled(defender, SubSkillType.UNARMED_IRON_GRIP)
                 && Permissions.isSubSkillEnabled(defender, SubSkillType.UNARMED_IRON_GRIP)
-                && RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.UNARMED_IRON_GRIP, defender)) {
+                && ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.UNARMED_IRON_GRIP, defender)) {
             NotificationManager.sendPlayerInformation(defender, NotificationType.SUBSKILL_MESSAGE, "Unarmed.Ability.IronGrip.Defender");
             NotificationManager.sendPlayerInformation(defender, NotificationType.SUBSKILL_MESSAGE, "Unarmed.Ability.IronGrip.Defender");
             NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Unarmed.Ability.IronGrip.Attacker");
             NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Unarmed.Ability.IronGrip.Attacker");
 
 

+ 46 - 17
src/main/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingManager.java

@@ -14,10 +14,9 @@ import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.*;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.CombatUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
 import com.gmail.nossr50.util.skills.RankUtils;
-import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
 import org.bukkit.Bukkit;
 import org.bukkit.Bukkit;
 import org.bukkit.Material;
 import org.bukkit.Material;
@@ -35,7 +34,9 @@ import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.HashSet;
 import java.util.List;
 import java.util.List;
 import java.util.Set;
 import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
 
 
+//TODO: Seems to not be using the item drop event for bonus drops, may want to change that.. or may not be able to be changed?
 public class WoodcuttingManager extends SkillManager {
 public class WoodcuttingManager extends SkillManager {
     private boolean treeFellerReachedThreshold = false;
     private boolean treeFellerReachedThreshold = false;
     private static int treeFellerThreshold; //TODO: Shared setting, will be removed in 2.2
     private static int treeFellerThreshold; //TODO: Shared setting, will be removed in 2.2
@@ -68,21 +69,46 @@ public class WoodcuttingManager extends SkillManager {
                 && ItemUtils.isAxe(heldItem);
                 && ItemUtils.isAxe(heldItem);
     }
     }
 
 
-    private boolean checkHarvestLumberActivation(@NotNull Material material) {
+    private boolean checkHarvestLumberActivation(Material material) {
         return Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.WOODCUTTING_HARVEST_LUMBER)
         return Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.WOODCUTTING_HARVEST_LUMBER)
                 && RankUtils.hasReachedRank(1, getPlayer(), SubSkillType.WOODCUTTING_HARVEST_LUMBER)
                 && RankUtils.hasReachedRank(1, getPlayer(), SubSkillType.WOODCUTTING_HARVEST_LUMBER)
-                && RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.WOODCUTTING_HARVEST_LUMBER, getPlayer())
+                && ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.WOODCUTTING_HARVEST_LUMBER, getPlayer())
+                && mcMMO.p.getGeneralConfig().getDoubleDropsEnabled(PrimarySkillType.WOODCUTTING, material);
+    }
+
+    private boolean checkCleanCutsActivation(Material material) {
+        return Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.WOODCUTTING_HARVEST_LUMBER)
+                && RankUtils.hasReachedRank(1, getPlayer(), SubSkillType.WOODCUTTING_HARVEST_LUMBER)
+                && ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.WOODCUTTING_CLEAN_CUTS, getPlayer())
                 && mcMMO.p.getGeneralConfig().getDoubleDropsEnabled(PrimarySkillType.WOODCUTTING, material);
                 && mcMMO.p.getGeneralConfig().getDoubleDropsEnabled(PrimarySkillType.WOODCUTTING, material);
     }
     }
 
 
     /**
     /**
-     * Begins Woodcutting
+     * Processes bonus drops for a block
      *
      *
      * @param blockState Block being broken
      * @param blockState Block being broken
      */
      */
-    public void processHarvestLumber(@NotNull BlockState blockState) {
-        if (checkHarvestLumberActivation(blockState.getType())) {
-            spawnHarvestLumberBonusDrops(blockState);
+    public void processBonusDropCheck(@NotNull BlockState blockState) {
+        //TODO: Why isn't this using the item drop event? Potentially because of Tree Feller? This should be adjusted either way.
+        if(mcMMO.p.getGeneralConfig().getDoubleDropsEnabled(PrimarySkillType.WOODCUTTING, blockState.getType())) {
+            //Mastery enabled for player
+            if(Permissions.canUseSubSkill(getPlayer(), SubSkillType.WOODCUTTING_CLEAN_CUTS)) {
+                if(checkCleanCutsActivation(blockState.getType())) {
+                    //Triple drops
+                    spawnHarvestLumberBonusDrops(blockState);
+                    spawnHarvestLumberBonusDrops(blockState);
+                } else {
+                    //Harvest Lumber Check
+                    if(checkHarvestLumberActivation(blockState.getType())) {
+                        spawnHarvestLumberBonusDrops(blockState);
+                    }
+                }
+            //No Mastery (no Clean Cuts)
+            } else if (Permissions.canUseSubSkill(getPlayer(), SubSkillType.WOODCUTTING_HARVEST_LUMBER)) {
+                if(checkHarvestLumberActivation(blockState.getType())) {
+                    spawnHarvestLumberBonusDrops(blockState);
+                }
+            }
         }
         }
     }
     }
 
 
@@ -295,24 +321,23 @@ public class WoodcuttingManager extends SkillManager {
                 Misc.spawnItemsFromCollection(getPlayer(), Misc.getBlockCenter(blockState), block.getDrops(itemStack), ItemSpawnReason.TREE_FELLER_DISPLACED_BLOCK);
                 Misc.spawnItemsFromCollection(getPlayer(), Misc.getBlockCenter(blockState), block.getDrops(itemStack), ItemSpawnReason.TREE_FELLER_DISPLACED_BLOCK);
 
 
                 //Bonus Drops / Harvest lumber checks
                 //Bonus Drops / Harvest lumber checks
-                processHarvestLumber(blockState);
+                processBonusDropCheck(blockState);
             } else if (BlockUtils.isNonWoodPartOfTree(blockState)) {
             } else if (BlockUtils.isNonWoodPartOfTree(blockState)) {
-                //Drop displaced non-woodcutting XP blocks
-
-                if(RankUtils.hasUnlockedSubskill(player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD)) {
+                // 90% of the time do not drop leaf blocks
+                if (ThreadLocalRandom.current().nextInt(100) > 90) {
                     Misc.spawnItemsFromCollection(getPlayer(), Misc.getBlockCenter(blockState), block.getDrops(itemStack), ItemSpawnReason.TREE_FELLER_DISPLACED_BLOCK);
                     Misc.spawnItemsFromCollection(getPlayer(), Misc.getBlockCenter(blockState), block.getDrops(itemStack), ItemSpawnReason.TREE_FELLER_DISPLACED_BLOCK);
+                }
 
 
+                //Drop displaced non-woodcutting XP blocks
+                if(RankUtils.hasUnlockedSubskill(player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD)) {
                     if(RankUtils.hasReachedRank(2, player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD)) {
                     if(RankUtils.hasReachedRank(2, player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD)) {
                         if(mcMMO.p.getAdvancedConfig().isKnockOnWoodXPOrbEnabled()) {
                         if(mcMMO.p.getAdvancedConfig().isKnockOnWoodXPOrbEnabled()) {
-                            if(RandomChanceUtil.rollDice(10, 100)) {
+                            if(ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.WOODCUTTING, player, 10)) {
                                 int randOrbCount = Math.max(1, Misc.getRandom().nextInt(100));
                                 int randOrbCount = Math.max(1, Misc.getRandom().nextInt(100));
                                 Misc.spawnExperienceOrb(blockState.getLocation(), randOrbCount);
                                 Misc.spawnExperienceOrb(blockState.getLocation(), randOrbCount);
                             }
                             }
                         }
                         }
                     }
                     }
-
-                } else {
-                    Misc.spawnItemsFromCollection(getPlayer(), Misc.getBlockCenter(blockState), block.getDrops(itemStack), ItemSpawnReason.TREE_FELLER_DISPLACED_BLOCK, 1);
                 }
                 }
             }
             }
 
 
@@ -381,6 +406,10 @@ public class WoodcuttingManager extends SkillManager {
      * @param blockState Block being broken
      * @param blockState Block being broken
      */
      */
     protected void spawnHarvestLumberBonusDrops(@NotNull BlockState blockState) {
     protected void spawnHarvestLumberBonusDrops(@NotNull BlockState blockState) {
-        Misc.spawnItemsFromCollection(getPlayer(), Misc.getBlockCenter(blockState), blockState.getBlock().getDrops(getPlayer().getInventory().getItemInMainHand()), ItemSpawnReason.BONUS_DROPS);
+        Misc.spawnItemsFromCollection(
+                getPlayer(),
+                Misc.getBlockCenter(blockState),
+                blockState.getBlock().getDrops(getPlayer().getInventory().getItemInMainHand()),
+                ItemSpawnReason.BONUS_DROPS);
     }
     }
 }
 }

+ 18 - 19
src/main/java/com/gmail/nossr50/util/BlockUtils.java

@@ -7,8 +7,7 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.repair.Repair;
 import com.gmail.nossr50.skills.repair.Repair;
 import com.gmail.nossr50.skills.salvage.Salvage;
 import com.gmail.nossr50.skills.salvage.Salvage;
-import com.gmail.nossr50.util.random.RandomChanceSkill;
-import com.gmail.nossr50.util.random.RandomChanceUtil;
+import com.gmail.nossr50.util.random.ProbabilityUtil;
 import org.bukkit.Material;
 import org.bukkit.Material;
 import org.bukkit.World;
 import org.bukkit.World;
 import org.bukkit.block.Block;
 import org.bukkit.block.Block;
@@ -98,7 +97,7 @@ public final class BlockUtils {
      */
      */
     public static boolean checkDoubleDrops(Player player, BlockState blockState, PrimarySkillType skillType, SubSkillType subSkillType) {
     public static boolean checkDoubleDrops(Player player, BlockState blockState, PrimarySkillType skillType, SubSkillType subSkillType) {
         if (mcMMO.p.getGeneralConfig().getDoubleDropsEnabled(skillType, blockState.getType()) && Permissions.isSubSkillEnabled(player, subSkillType)) {
         if (mcMMO.p.getGeneralConfig().getDoubleDropsEnabled(skillType, blockState.getType()) && Permissions.isSubSkillEnabled(player, subSkillType)) {
-            return RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkill(player, subSkillType, true));
+            return ProbabilityUtil.isSkillRNGSuccessful(subSkillType, player);
         }
         }
 
 
         return false;
         return false;
@@ -231,22 +230,22 @@ public final class BlockUtils {
         return mcMMO.getMaterialMapStore().isTreeFellerDestructible(material);
         return mcMMO.getMaterialMapStore().isTreeFellerDestructible(material);
     }
     }
 
 
-    /**
-     * Determine if a given block should be affected by Flux Mining
-     *
-     * @param blockState The {@link BlockState} of the block to check
-     * @return true if the block should affected by Flux Mining, false otherwise
-     */
-    public static boolean affectedByFluxMining(BlockState blockState) {
-        switch (blockState.getType()) {
-            case IRON_ORE:
-            case GOLD_ORE:
-                return true;
-
-            default:
-                return false;
-        }
-    }
+//    /**
+//     * Determine if a given block should be affected by Flux Mining
+//     *
+//     * @param blockState The {@link BlockState} of the block to check
+//     * @return true if the block should affected by Flux Mining, false otherwise
+//     */
+//    public static boolean affectedByFluxMining(BlockState blockState) {
+//        switch (blockState.getType()) {
+//            case IRON_ORE:
+//            case GOLD_ORE:
+//                return true;
+//
+//            default:
+//                return false;
+//        }
+//    }
 
 
     /**
     /**
      * Determine if a given block can activate Herbalism abilities
      * Determine if a given block can activate Herbalism abilities

+ 2 - 3
src/main/java/com/gmail/nossr50/util/EventUtils.java

@@ -183,8 +183,7 @@ public final class EventUtils {
      * @param subSkillType target subskill
      * @param subSkillType target subskill
      * @return the event after it has been fired
      * @return the event after it has been fired
      */
      */
-    @Deprecated
-    public static @NotNull SubSkillEvent callSubSkillEvent(Player player, SubSkillType subSkillType) {
+    public static @NotNull SubSkillEvent callSubSkillEvent(@NotNull Player player, @NotNull SubSkillType subSkillType) {
         SubSkillEvent event = new SubSkillEvent(player, subSkillType);
         SubSkillEvent event = new SubSkillEvent(player, subSkillType);
         mcMMO.p.getServer().getPluginManager().callEvent(event);
         mcMMO.p.getServer().getPluginManager().callEvent(event);
 
 
@@ -399,7 +398,7 @@ public final class EventUtils {
         McMMOPlayer mmoPlayer = UserManager.getPlayer(player);
         McMMOPlayer mmoPlayer = UserManager.getPlayer(player);
         if(mmoPlayer == null)
         if(mmoPlayer == null)
             return true;
             return true;
-        
+
         McMMOPlayerXpGainEvent event = new McMMOPlayerXpGainEvent(player, skill, xpGained, xpGainReason);
         McMMOPlayerXpGainEvent event = new McMMOPlayerXpGainEvent(player, skill, xpGained, xpGainReason);
         mcMMO.p.getServer().getPluginManager().callEvent(event);
         mcMMO.p.getServer().getPluginManager().callEvent(event);
 
 

+ 12 - 0
src/main/java/com/gmail/nossr50/util/ItemUtils.java

@@ -36,14 +36,26 @@ public final class ItemUtils {
      * @param item Item to check
      * @param item Item to check
      * @return true if the item is a bow, false otherwise
      * @return true if the item is a bow, false otherwise
      */
      */
+    // TODO: Unit tests
     public static boolean isBow(@NotNull ItemStack item) {
     public static boolean isBow(@NotNull ItemStack item) {
         return mcMMO.getMaterialMapStore().isBow(item.getType().getKey().getKey());
         return mcMMO.getMaterialMapStore().isBow(item.getType().getKey().getKey());
     }
     }
 
 
+    // TODO: Unit tests
     public static boolean isCrossbow(@NotNull ItemStack item) {
     public static boolean isCrossbow(@NotNull ItemStack item) {
         return mcMMO.getMaterialMapStore().isCrossbow(item.getType().getKey().getKey());
         return mcMMO.getMaterialMapStore().isCrossbow(item.getType().getKey().getKey());
     }
     }
 
 
+    // TODO: Unit tests
+    public static boolean isBowOrCrossbow(@NotNull ItemStack item) {
+        return isBow(item) || isCrossbow(item);
+    }
+
+    // TODO: Unit tests
+    public static boolean isTrident(@NotNull ItemStack item) {
+        return mcMMO.getMaterialMapStore().isTrident(item.getType().getKey().getKey());
+    }
+
     public static boolean hasItemInEitherHand(@NotNull Player player, Material material) {
     public static boolean hasItemInEitherHand(@NotNull Player player, Material material) {
         return player.getInventory().getItemInMainHand().getType() == material || player.getInventory().getItemInOffHand().getType() == material;
         return player.getInventory().getItemInMainHand().getType() == material || player.getInventory().getItemInOffHand().getType() == material;
     }
     }

+ 8 - 4
src/main/java/com/gmail/nossr50/util/MaterialMapStore.java

@@ -10,9 +10,6 @@ import java.util.Locale;
 /**
 /**
  * Stores hash tables for item and block names
  * Stores hash tables for item and block names
  * This allows for better support across multiple versions of Minecraft
  * This allows for better support across multiple versions of Minecraft
- *
- * This is a temporary class, mcMMO is spaghetti and I'l clean it up later
- *
  */
  */
 public class MaterialMapStore {
 public class MaterialMapStore {
 
 
@@ -52,7 +49,6 @@ public class MaterialMapStore {
     private final @NotNull HashSet<String> bows;
     private final @NotNull HashSet<String> bows;
     private final @NotNull HashSet<String> crossbows;
     private final @NotNull HashSet<String> crossbows;
     private final @NotNull HashSet<String> tools;
     private final @NotNull HashSet<String> tools;
-
     private final @NotNull HashSet<String> enchantables;
     private final @NotNull HashSet<String> enchantables;
 
 
     private final @NotNull HashSet<String> ores;
     private final @NotNull HashSet<String> ores;
@@ -820,6 +816,14 @@ public class MaterialMapStore {
         return crossbows.contains(id);
         return crossbows.contains(id);
     }
     }
 
 
+    public boolean isTrident(@NotNull Material material) {
+        return isTrident(material.getKey().getKey());
+    }
+
+    public boolean isTrident(@NotNull String id) {
+        return tridents.contains(id);
+    }
+
     public boolean isLeatherArmor(@NotNull Material material) {
     public boolean isLeatherArmor(@NotNull Material material) {
         return isLeatherArmor(material.getKey().getKey());
         return isLeatherArmor(material.getKey().getKey());
     }
     }

+ 3 - 0
src/main/java/com/gmail/nossr50/util/MetadataConstants.java

@@ -14,6 +14,9 @@ public class MetadataConstants {
      * Take great care if you ever modify the value of these keys
      * Take great care if you ever modify the value of these keys
      */
      */
     public static final @NotNull String METADATA_KEY_REPLANT = "mcMMO: Recently Replanted";
     public static final @NotNull String METADATA_KEY_REPLANT = "mcMMO: Recently Replanted";
+    public static final @NotNull String METADATA_KEY_SPAWNED_ARROW = "mcMMO: Spawned Arrow";
+    public static final @NotNull String METADATA_KEY_MULTI_SHOT_ARROW = "mcMMO: Multi-shot Arrow";
+    public static final @NotNull String METADATA_KEY_BOUNCE_COUNT = "mcMMO: Arrow Bounce Count";
     public static final @NotNull String METADATA_KEY_EXPLOSION_FROM_RUPTURE = "mcMMO: Rupture Explosion";
     public static final @NotNull String METADATA_KEY_EXPLOSION_FROM_RUPTURE = "mcMMO: Rupture Explosion";
     public static final @NotNull String METADATA_KEY_FISH_HOOK_REF = "mcMMO: Fish Hook Tracker";
     public static final @NotNull String METADATA_KEY_FISH_HOOK_REF = "mcMMO: Fish Hook Tracker";
     public static final @NotNull String METADATA_KEY_DODGE_TRACKER = "mcMMO: Dodge Tracker";
     public static final @NotNull String METADATA_KEY_DODGE_TRACKER = "mcMMO: Dodge Tracker";

+ 22 - 0
src/main/java/com/gmail/nossr50/util/Misc.java

@@ -343,4 +343,26 @@ public final class Misc {
             experienceOrb.setExperience(orbExpValue);
             experienceOrb.setExperience(orbExpValue);
         }
         }
     }
     }
+
+//    public static void hackyUnitTest(@NotNull McMMOPlayer normalPlayer) {
+//        mcMMO.p.getLogger().info("Starting hacky unit test...");
+//        int iterations = 1000000;
+//        double ratioDivisor = 10000; //10000 because we run the test 1,000,000 times
+//        double expectedFailRate = 100.0D - RandomChanceUtil.getRandomChanceExecutionSuccess(normalPlayer.getPlayer(), SubSkillType.MINING_MOTHER_LODE, true);
+//
+//        double win = 0, loss = 0;
+//        for(int x = 0; x < iterations; x++) {
+//            if(RandomChanceUtil.checkRandomChanceExecutionSuccess(normalPlayer.getPlayer(), SubSkillType.MINING_MOTHER_LODE, true)) {
+//                win++;
+//            } else {
+//                loss++;
+//            }
+//        }
+//
+//        double lossRatio = (loss / ratioDivisor);
+//        mcMMO.p.getLogger().info("Expected Fail Rate: "+expectedFailRate);
+//        mcMMO.p.getLogger().info("Loss Ratio for hacky test: "+lossRatio);
+////        Assert.assertEquals(lossRatio, expectedFailRate, 0.01D);
+//    }
+
 }
 }

Vissa filer visades inte eftersom för många filer har ändrats