Browse Source

Merge master into endgame branch to prepare merge for beta 2.2.000 update

nossr50 2 years ago
parent
commit
bb44e56bdb
100 changed files with 6117 additions and 3053 deletions
  1. 5 4
      .github/workflows/maven.yml
  2. 568 2
      Changelog.txt
  3. 89 52
      pom.xml
  4. 8 2
      src/main/java/com/gmail/nossr50/api/AbilityAPI.java
  5. 26 6
      src/main/java/com/gmail/nossr50/api/DatabaseAPI.java
  6. 113 28
      src/main/java/com/gmail/nossr50/api/ExperienceAPI.java
  7. 1 2
      src/main/java/com/gmail/nossr50/api/PartyAPI.java
  8. 7 5
      src/main/java/com/gmail/nossr50/api/SkillAPI.java
  9. 1 1
      src/main/java/com/gmail/nossr50/api/exceptions/McMMOPlayerNotFoundException.java
  10. 1 2
      src/main/java/com/gmail/nossr50/chat/SamePartyPredicate.java
  11. 0 46
      src/main/java/com/gmail/nossr50/commands/MHDCommand.java
  12. 2 3
      src/main/java/com/gmail/nossr50/commands/McmmoCommand.java
  13. 12 2
      src/main/java/com/gmail/nossr50/commands/McscoreboardCommand.java
  14. 4 6
      src/main/java/com/gmail/nossr50/commands/XprateCommand.java
  15. 6 2
      src/main/java/com/gmail/nossr50/commands/database/ConvertDatabaseCommand.java
  16. 1 2
      src/main/java/com/gmail/nossr50/commands/database/McpurgeCommand.java
  17. 1 1
      src/main/java/com/gmail/nossr50/commands/database/McremoveCommand.java
  18. 2 2
      src/main/java/com/gmail/nossr50/commands/database/MmoshowdbCommand.java
  19. 2 1
      src/main/java/com/gmail/nossr50/commands/experience/AddlevelsCommand.java
  20. 2 1
      src/main/java/com/gmail/nossr50/commands/experience/AddxpCommand.java
  21. 18 16
      src/main/java/com/gmail/nossr50/commands/experience/ExperienceCommand.java
  22. 2 1
      src/main/java/com/gmail/nossr50/commands/experience/MmoeditCommand.java
  23. 17 14
      src/main/java/com/gmail/nossr50/commands/experience/SkillresetCommand.java
  24. 64 64
      src/main/java/com/gmail/nossr50/commands/hardcore/HardcoreCommand.java
  25. 129 129
      src/main/java/com/gmail/nossr50/commands/hardcore/HardcoreModeCommand.java
  26. 64 64
      src/main/java/com/gmail/nossr50/commands/hardcore/VampirismCommand.java
  27. 6 4
      src/main/java/com/gmail/nossr50/commands/party/PartyDisbandCommand.java
  28. 2 2
      src/main/java/com/gmail/nossr50/commands/party/PartyInfoCommand.java
  29. 2 2
      src/main/java/com/gmail/nossr50/commands/party/PartyInviteCommand.java
  30. 2 2
      src/main/java/com/gmail/nossr50/commands/party/PartyItemShareCommand.java
  31. 2 2
      src/main/java/com/gmail/nossr50/commands/party/PartyXpShareCommand.java
  32. 3 3
      src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceCommand.java
  33. 3 3
      src/main/java/com/gmail/nossr50/commands/party/teleport/PtpAcceptCommand.java
  34. 6 7
      src/main/java/com/gmail/nossr50/commands/party/teleport/PtpCommand.java
  35. 29 15
      src/main/java/com/gmail/nossr50/commands/player/InspectCommand.java
  36. 3 3
      src/main/java/com/gmail/nossr50/commands/player/MccooldownCommand.java
  37. 6 6
      src/main/java/com/gmail/nossr50/commands/player/McrankCommand.java
  38. 4 4
      src/main/java/com/gmail/nossr50/commands/player/McstatsCommand.java
  39. 8 8
      src/main/java/com/gmail/nossr50/commands/player/MctopCommand.java
  40. 3 2
      src/main/java/com/gmail/nossr50/commands/player/XPBarCommand.java
  41. 199 199
      src/main/java/com/gmail/nossr50/commands/skills/AprilCommand.java
  42. 3 2
      src/main/java/com/gmail/nossr50/commands/skills/HerbalismCommand.java
  43. 8 22
      src/main/java/com/gmail/nossr50/commands/skills/MmoInfoCommand.java
  44. 54 25
      src/main/java/com/gmail/nossr50/commands/skills/SkillCommand.java
  45. 2 1
      src/main/java/com/gmail/nossr50/commands/skills/SkillGuideCommand.java
  46. 32 25
      src/main/java/com/gmail/nossr50/commands/skills/SwordsCommand.java
  47. 6 9
      src/main/java/com/gmail/nossr50/commands/skills/WoodcuttingCommand.java
  48. 291 172
      src/main/java/com/gmail/nossr50/config/AdvancedConfig.java
  49. 39 83
      src/main/java/com/gmail/nossr50/config/AutoUpdateConfigLoader.java
  50. 220 0
      src/main/java/com/gmail/nossr50/config/BukkitConfig.java
  51. 3 1
      src/main/java/com/gmail/nossr50/config/ChatConfig.java
  52. 41 26
      src/main/java/com/gmail/nossr50/config/ConfigLoader.java
  53. 16 16
      src/main/java/com/gmail/nossr50/config/CoreSkillsConfig.java
  54. 1008 0
      src/main/java/com/gmail/nossr50/config/GeneralConfig.java
  55. 0 5
      src/main/java/com/gmail/nossr50/config/HiddenConfig.java
  56. 7 2
      src/main/java/com/gmail/nossr50/config/PersistentDataConfig.java
  57. 42 44
      src/main/java/com/gmail/nossr50/config/RankConfig.java
  58. 24 30
      src/main/java/com/gmail/nossr50/config/SoundConfig.java
  59. 18 23
      src/main/java/com/gmail/nossr50/config/WorldBlacklist.java
  60. 219 86
      src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java
  61. 7 8
      src/main/java/com/gmail/nossr50/config/mods/CustomArmorConfig.java
  62. 12 15
      src/main/java/com/gmail/nossr50/config/mods/CustomBlockConfig.java
  63. 6 6
      src/main/java/com/gmail/nossr50/config/mods/CustomEntityConfig.java
  64. 9 11
      src/main/java/com/gmail/nossr50/config/mods/CustomToolConfig.java
  65. 4 3
      src/main/java/com/gmail/nossr50/config/party/ItemWeightConfig.java
  66. 14 19
      src/main/java/com/gmail/nossr50/config/skills/alchemy/PotionConfig.java
  67. 18 29
      src/main/java/com/gmail/nossr50/config/skills/repair/RepairConfig.java
  68. 10 17
      src/main/java/com/gmail/nossr50/config/skills/repair/RepairConfigManager.java
  69. 25 36
      src/main/java/com/gmail/nossr50/config/skills/salvage/SalvageConfig.java
  70. 9 16
      src/main/java/com/gmail/nossr50/config/skills/salvage/SalvageConfigManager.java
  71. 47 41
      src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java
  72. 107 8
      src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java
  73. 11 31
      src/main/java/com/gmail/nossr50/database/DatabaseManager.java
  74. 10 7
      src/main/java/com/gmail/nossr50/database/DatabaseManagerFactory.java
  75. 13 0
      src/main/java/com/gmail/nossr50/database/ExpectedType.java
  76. 13 0
      src/main/java/com/gmail/nossr50/database/FlatFileDataFlag.java
  77. 345 0
      src/main/java/com/gmail/nossr50/database/FlatFileDataProcessor.java
  78. 1332 0
      src/main/java/com/gmail/nossr50/database/FlatFileDatabaseManager.java
  79. 0 1372
      src/main/java/com/gmail/nossr50/database/FlatfileDatabaseManager.java
  80. 220 105
      src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java
  81. 7 0
      src/main/java/com/gmail/nossr50/database/UserQuery.java
  82. 31 0
      src/main/java/com/gmail/nossr50/database/UserQueryFull.java
  83. 7 0
      src/main/java/com/gmail/nossr50/database/UserQueryName.java
  84. 20 0
      src/main/java/com/gmail/nossr50/database/UserQueryNameImpl.java
  85. 7 0
      src/main/java/com/gmail/nossr50/database/UserQueryType.java
  86. 11 0
      src/main/java/com/gmail/nossr50/database/UserQueryUUID.java
  87. 23 0
      src/main/java/com/gmail/nossr50/database/UserQueryUUIDImpl.java
  88. 42 0
      src/main/java/com/gmail/nossr50/database/flatfile/BadCategorizedFlatFileData.java
  89. 58 0
      src/main/java/com/gmail/nossr50/database/flatfile/CategorizedFlatFileData.java
  90. 42 0
      src/main/java/com/gmail/nossr50/database/flatfile/FlatFileDataBuilder.java
  91. 21 0
      src/main/java/com/gmail/nossr50/database/flatfile/FlatFileDataContainer.java
  92. 115 0
      src/main/java/com/gmail/nossr50/database/flatfile/FlatFileDataUtil.java
  93. 7 0
      src/main/java/com/gmail/nossr50/database/flatfile/LeaderboardStatus.java
  94. 7 9
      src/main/java/com/gmail/nossr50/datatypes/LevelUpBroadcastPredicate.java
  95. 7 9
      src/main/java/com/gmail/nossr50/datatypes/PowerLevelUpBroadcastPredicate.java
  96. 2 1
      src/main/java/com/gmail/nossr50/datatypes/database/UpgradeType.java
  97. 7 5
      src/main/java/com/gmail/nossr50/datatypes/interactions/NotificationType.java
  98. 1 1
      src/main/java/com/gmail/nossr50/datatypes/json/McMMOUrl.java
  99. 25 0
      src/main/java/com/gmail/nossr50/datatypes/meta/RuptureTaskMeta.java
  100. 9 12
      src/main/java/com/gmail/nossr50/datatypes/party/Party.java

+ 5 - 4
.github/workflows/maven.yml

@@ -31,12 +31,13 @@ jobs:
     - name: Checkout repository
     - name: Checkout repository
       uses: actions/checkout@v2
       uses: actions/checkout@v2
 
 
-    # 2. Setup Java 1.8 JDK
-    - name: Java 1.8 setup
-      uses: actions/setup-java@v1.4.3
+    # 2. Setup Java 17 JDK (Adopt)
+    - name: Java 17 setup
+      uses: actions/setup-java@v2
       with:
       with:
+        distribution: 'adopt'
         java-package: jdk
         java-package: jdk
-        java-version: 1.8
+        java-version: '17'
 
 
     # 3. Setup local Maven package cache to speed up building
     # 3. Setup local Maven package cache to speed up building
     - name: Cache Maven packages
     - name: Cache Maven packages

+ 568 - 2
Changelog.txt

@@ -1,5 +1,4 @@
-Version 2.1.176
-    Added another measure to prevent item stacks from reaching 65 from double smelt
+Version 2.2.000
     Updated Adventure (our text dependency) fixes some errors when using color codes in party/admin chat (thanks TheBusyBiscuit)
     Updated Adventure (our text dependency) fixes some errors when using color codes in party/admin chat (thanks TheBusyBiscuit)
     Added some support for negative Y values in anticipation of 1.17 world height changes (thanks t00thpick1)
     Added some support for negative Y values in anticipation of 1.17 world height changes (thanks t00thpick1)
     (API) 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
     (API) 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
@@ -57,10 +56,577 @@ Version 2.1.176
     New Power Level Command
     New Power Level Command
     This power level command gives you a view of all your current masteries, it also provides a summary of your power level.
     This power level command gives you a view of all your current masteries, it also provides a summary of your power level.
 
 
+Version 2.1.218
+    Fixed locale pt_BR type (thanks MrPowerGamerBR)
+    Updated Russian Locale (thanks imDaniX)
+    Harvest Lumber checks main hand enchantments when delivering bonuses (thanks destro174)
+    Fixed bug where players could use inspect to determine if vanished players were online (thanks DarkKnights22)
+
+Version 2.1.217
+    Tree Feller will now break blocks within the limit instead of refusing to fell the entire tree (partial big tree destruction)
+    Mangrove trees resulting from growth are now marked as natural (existing trees marked unnatural before this update will not be retroactively fixed)
+    Fixed a bug removing comments from configs (see notes)
+    Fixed mouse-hover tooltip windows (thanks Greymagic27)
+
+    NOTES:
+    Regarding the secure chat feature Minecraft rolled out, once APIs have updated I will look into rolling support for that into admin/party chat.
+    Comments should no longer get removed from config but I haven't added any code to add missing comments back in, for now if you want comments you can delete the config file to regenerate it (sorry for the inconvenience)
+
+Version 2.1.216
+    Reverted Unarmed changes from 2.1.215 (fixes block breaker/beserk active at all time for all players)
+    Added Sculk_* blocks to Mining XP
+    You can use Hoe(s) to gain mining XP, Hoe(s) won't benefit from Super Breaker nor will they activate it (see notes)
+
+    NOTES:
+    Hoe's being able to gain Mining XP is primarily for Sculk blocks which have Hoe as the fastest tool yet to me feel like Mining blocks, let me know what you think
+    The Sculk blocks don't quite fit perfectly as mining-related, especially since hoes are the fastest tool to use for them. Let me know what you think.
+Version 2.1.215
+    Fixed Coal Blocks and potentially other fuel sources not showing burning animation to players (see notes)
+    Level up broadcasts from level milestones will now be visible to the player who achieved them
+    Fixed a bug where hovering over skill descriptions did not display anything (thanks Greymagic27)
+    Added Dripstone to experience.yml (thanks Greymagic27)
+    Added Mangrove_Log to experience.yml (should fix behavior for tree-feller etc)
+    Item in main hand is now used for Tree Feller drops (thanks destro174)
+
+    NOTES:
+    The burning animation bug is actually in Spigot, it takes an int for setBurnTime when it really should only take a short as the game is expecting a short value, I implemented a hacky workaround via Math.min
+    There is a bug where you may lose your config comments, this will be complex to solve, for now you can check our GitHub for the configs with comments.
+
+Version 2.1.214
+    Temporarily removed FakePlayerAnimationEvent (see notes)
+    Players can now see their own level up broadcasts
+
+    NOTES:
+    Spigot updated PlayerAnimationEvent with an API break between versions 1.18 and 1.19, as a temporary fix for mcMMO to be able to support both of these
+    Minecraft versions I have temporarily removed FakePlayerAnimationEvent. I am working on a fix to dynamically use the correct API depending on the version of MC
+    as many people will likely be stuck on 1.18 for a while.
+    FakePlayerAnimationEvent existed to circumvent false positives on anti-cheat plugins such as NoCheat, however
+    I am unsure if it is still needed and removing it may have no ill effects.. however I cannot be certain.
+    Anti-cheat plugin authors should be informed to update if you run into false positives with mcMMO after this update so they can make the appropriate changes.
+Version 2.1.213
+    Updated various mechanics to recognize and understand the new blocks
+    Added 'Packed_Mud' to mining xp
+    Added 'Mud_Bricks' to mining xp
+    Added 'Reinforced_Deepslate' to mining xp
+    Added 'Mud' to excavation xp
+    Added 'Muddy Mangrove Roots' to excavation xp
+    Added 'Mangrove_Roots' to woodcutting xp
+    Added 'Stripped_Mangrove_Log' to woodcutting xp
+    Added 'Stripped_Crimson_Stem' to woodcutting xp
+    Added 'Stripped_Warped_Stem' to woodcutting xp
+    Added 'Allay' to combat experience
+    Added 'Frog' to combat experience and taming experience
+    Added 'Tadpole' to combat experience
+    Added 'Warden' to combat experience
+
+    Changes to excavation treasures (requires manual editing or regenerating the config)
+        Added Mud as a block which can drop various treasures (see notes)
+        Added Muddy_Mangrove_Roots as a block which can drop various treasures (see notes)
+        Added Stick as a new excavation drop, which drops from mud and muddy mangrove roots (see notes)
+        Added Potato as a new excavation drop, which drops from mud and dirt (see notes)
+        Added Feather as a new excavation drop, which drops from mud (see notes)
+        Added Spyglass as a new excavation drop, which drops rarely from mud and dirt (see notes)
+        Added Trident as a new excavation drop, which drops somewhat rarely from mud, muddy mangrove roots, and clay (see notes)
+        Added Heart of the Sea as a new excavation drop, which drops very rarely from mud and requires a very high excavation level to appear
+
+    NOTES:
+    To get the new excavation drop list, the easiest way is to delete treasures.yml and mcMMO will regenerate it with the newest version
+    I'll likely be tweaking the treasures list in the near future, leave feedback on discord if you have suggestions
+
+Version 2.1.212
+    An herbalism exploit has been patched (thanks WhatsTheBadNews)
+    Added 'ExploitFix.Combat.XPCeiling.Enabled' to experience.yml
+    Added 'ExploitFix.Combat.XPCeiling.Damage_Limit' to experience.yml
+    Single instances of combat damage above 100 give are capped to give the same reward as 100 by default (100 is a lot, but you can change this in settings)
+
+    NOTES: The damage ceiling won't affect server that don't have mobs running around with abnormally high health, if your server does you'll want to adjust this limit or disable it.
+Version 2.1.211
+    Added /mmodebug info for players hitting other players
+    Fixed Immortal Player bug
+    Removed all of Spigot's buggy and deprecated DamageModifier API, this fixes the bug where players would become immortal when using certain other plugins
+    Rupture is back to doing "pure" damage (due to the above change)
+
+    NOTES:
+    Rupture damage will be tweaked based on feedback as it is dealing "pure" damage
+Version 2.1.210
+    Fixed a memory leak involving mob metadata
+    Fixed a potential null pointer exception in InventoryListener
+
+    NOTES:
+    If you're having issues with "immortal players" this is a known plugin incompatibility between mcMMO and another plugin, I need more info on what plugins need to be present to cause it. Please post info to GitHub issues or tag me on discord.
+    There was a big rewrite in this update relating to how various types of metadata were being tracked/stored/retrieved
+    If you run into issues with this version of mcMMO, please post about it on GitHub
+
+Version 2.1.209
+    Fixed a bug where some config files did not get trimmed completely
+
+    NOTES:
+    This should fix the issue for everyone, let me know if you still run into trouble!
+    Don't be afraid to ping me on discord
+
+Version 2.1.208
+    Significantly rewrote to how mcMMO loads/updates config files
+    Fixed a bug where huge config files caused the server to take forever to start/shutdown
+    Fixed config files duplicating comments and updated their code to use Spigot API (thanks the456gamer)
+    mcMMO now repairs config files (removing duplicate comments, see notes)
+    Updated lithuanian locale (thanks dexasz)
+
+    NOTES:
+    Due to a change in Spigot mcMMO started growing config files at an alarming rate until they became so big they wouldn't load, and well before they got to that stage they slowed down loading the server
+    mcMMO now uses the Spigot API in a smarter way for config files
+
+Version 2.1.207
+    Fixed an IndexOutOfBounds exception with our BlockTracker
+    Fixed a bug where leveling up a party at level cap would spam the chat with messages
+    mcMMO will no longer use enchanted repair materials (thanks JeBobs)
+    Added an option to allow enchanted repair materials 'Skills.Repair.Use_Enchanted_Materials' in config.yml
+    Temporarily rolling required Java version back to 16
+    Added unicode (UTF-8) support to locale files (no more UTF-16 codes needed)
+    Added locale key 'Scoreboard.Disabled' to en_US
+    Added locale key 'Scoreboard.NotSetupYet' to en_US
+    Fixed a bug where Salvage sent messages even though the event was cancelled (Thanks TheBusyBiscuit)
+
+Version 2.1.206
+    Fixed a memory leak involving Herbalism under specific circumstances
+    Fixed a memory leak involving Rupture under specific circumstances
+    Fixed a memory leak involving Dodge
+    Fixed a memory leak involving Endermen and block pickups
+    Fixed a memory leak from plugin conflicts when double drops get activated
+    Fixed a memory leak that required a specific config.yml setup with mob health bars
+    You can no longer set mob health bars to -1 in config.yml to set it to display permanently (this was problematic behavior)
+    Fixed a bug preventing Action Bar messages from showing
+    Fixed a bug where Alchemy XP wasn't being granted
+    Lowered the default volume of level up from .75 to .3 in sounds.yml (delete sounds.yml to get this change automagically)
+    Updated adventure platform dependency
+    Updated to use Java 17
+
+    NOTES:
+    mcMMO will target the newest version of MC moving forward, any backwards compatibility with prior versions of Minecraft should be considered a side effect rather than intended.
+
+Version 2.1.205
+    Fixed yet another exception preventing Alchemy from working (thanks NemuruYama)
+    Added some code to cleanup potential memory leaks
+
+    NOTES:
+    Sorry for the delay in this patch, I have had a terrible cold all weekend, feeling better now
+
+Version 2.1.204
+    Fixed IndexOutOfBounds exception (thanks gecko10000) (related to Alchemy)
+    Added double smelt to copper ingot and netherite scrap (thanks Lyther)
+
+Version 2.1.203
+    mcMMO now requires Java 16
+    mcMMO now requires the newest version of Minecraft (currently 1.17.1)
+    Fixed several API breaks (mostly affected Alchemy)
+    Fixed a bug relating to Shake percentages (thanks Lyther)
+    Fixed hexcolors not displaying correctly in level up milestone broadcasts (thanks gecko10000)
+    (API) Added deprecated constructors for PlayerProfile (thanks PikaMug)
+    mcMMO has had many of its dependencies updated to newer builds
+
+    NOTES:
+    If you want to play mcMMO on older versions, simply use 2.1.202 instead
+    Keeping mcMMO backwards compatible with older versions is getting messy, and I'd rather be able to focus my attention at newer features than having to make an elaborate build process (or alternatively hacky code) to support older versions of the game
+    Furthermore, it seems most people are playing 1.17.1 by a wide margin
+    You may have trouble compiling the source code if your maven is not setup to run JDK16, if you get any errors when compiling this is likely the reason
+
+    The data from bstats went into making this decision (shoutout to Qixils for typing this up too)
+     71.9% of servers are running 1.17.X
+     97.9% of servers are running ≥1.16
+     99.3% of servers are running ≥1.15
+
+Version 2.1.202
+    Fixed a bug where mcMMO didn't reward XP for Kelp
+    Fixed a bug where mcMMO marked bonemealed Azalea trees as unnatural (and thus did not give XP or get affected by Tree Feller)
+    Added Amethyst_Block to experience.yml for Mining
+    Added Flowering Azalea Leaves to Tree Feller's white list
+    Fixed a bug where mcMMO didn't appropriately flag blocks as natural in some tree growing events
+    (SQL) Added more MySQL/MariaDB settings (allowPublicKeyRetrieval - thanks rosaage)
+    (API) Added CREATED_PARTY and DISBANDED_PARTY to EventReason (used in some party events - thanks PikaMug )
+    Party member name matching is no longer case sensitive (thanks Wariorrrr)
+    Updated zh_CN locale (thanks GhostDC)
+    Added some settings for over fishing (Settings are in experience.yml under Fishing_ExploitFix_Options - thanks tunagohan)
+
+    NOTES:
+    This means tree feller will correctly traverse flowering azalea leaves during its ability
+
+Version 2.1.201
+    Tweaked the visual/audio effect for Rupture
+    (API) TNT is set as the source in Blast Mining (1.16.1 and up)
+    Fixed an exploit related to Ability Buffs remaining on tools
+    Blast Mining no longer drops Budding Amethyst since its not legal to obtain this item through normal gameplay
+    Added mcinspect and mmoinspect aliases to inspect command
+    Portuguese translation of Woodcutting changed back to Lenhador
+    Updated zh_CN (Chinese) locale, thanks GhostDC!
+    Major changes to zh_TW locale, thanks gregman98
+    Added '/skill keep' shortcut (for example /mining keep) thanks GriffinCodes
+    Impact is now more balanced as the formula has been changed (see notes) thanks emanondev
+
+    NOTES:
+    Impact will deal less durability damage to armors without unbreaking, and more to armors with unbreaking
+
+Version 2.1.200
+    Fixed a major 1.17 exploit
+    Dodge will no longer trigger while blocking
+    Action Bar messages can now be disabled (thanks TheBusyBiscuit)
+    mcMMO is better at MC version parsing now (thanks stepech & TheBusyBiscuit)
+
+Version 2.1.199
+    Fixed IndexOutOfBounds error for servers running 1.17 or higher
+    Fixed a bug that caused MySQL/MariaDB to malfunction for 1.17 (see notes)
+    Renamed Deepslate Lapis Lazuli Ore to Deepslate Lapis Ore in experience.yml and config.yml
+    Added some code to prevent mcMMO from breaking if it doesn't recognize the version of the game
+    Optimized CompatibilitySupportLayer - this handles some of the logic for supporting multiple versions of the game
+    Added Unit Tests for MinecraftGameVersion
+
+    NOTES:
+    I have temporarily disabled SSL for MySQL/MariaDB for 1.17 ( proper fix coming soon )
+
+Version 2.1.198
+    Fixed a bug where Smelting didn't work with the new 1.17 materials
+    Updated dependency Adventure to 4.8.0 (thanks TheBusyBiscuit)
+
+Version 2.1.197
+    This update adds compatibility for new mobs and blocks from 1.17
+    This build of mcMMO should be more compatible with certain versions of ViaVersion
+    Players are no longer launched from Minecarts when using a Fishing Rod (they are still dismounted) thanks lexikiq
+    Updated Japanese locale (thanks ViaSnake)
+    Updated Brazil Portuguese (pt_BR) locale (thanks Paulo Guilherme)
+    Added Goat to experience.yml for combat and taming
+    Added Axolotl to experience.yml for combat and taming
+    Added Glow_Squid to experience.yml for combat and taming
+    Added Glow Berries to Farmer's Diet
+    Updated Super Breaker to recognize the new pick-axe appropriate blocks
+    Updated Tree Feller to recognize Azalea Leaves
+
+    Added Rooted Dirt to experience.yml for Excavation
+    Added Small Dripleaf to experience.yml for Herbalism
+    Added Big Dripleaf to experience.yml for Herbalism
+    Added Moss Block to experience.yml for Herbalism
+    Added Cave Vines to experience.yml for Herbalism
+    Added Cave Vines Plant to experience.yml for Herbalism
+    Added Glow Lichen to experience.yml for Herbalism
+    Added Tuff to experience.yml for Mining
+    Added Calcite to experience.yml for Mining
+    Added Smooth Basalt to experience.yml for Mining
+    Added Block_Of_Amethyst to experience.yml for Mining
+    Added Budding Amethyst to experience.yml for Mining
+    Added Small_Amethyst_Bud to experience.yml for Mining
+    Added Medium Amethyst Bud to experience.yml for Mining
+    Added Large Amethyst Bud to experience.yml for Mining
+    Added Amethyst Cluster to experience.yml for Mining
+    Added Deepslate to experience.yml for Mining
+    Added Cobbled Deepslate to experience.yml for Mining
+    Added Copper Ore to experience.yml for Mining
+    Added Deepslate Redstone Ore to experience.yml for Mining
+    Added Deepslate Copper Ore to experience.yml for Mining
+    Added Deepslate Coal Ore to experience.yml for Mining
+    Added Deepslate Diamond Ore to experience.yml for Mining
+    Added Deepslate Emerald Ore to experience.yml for Mining
+    Added Deepslate Iron Ore to experience.yml for Mining
+    Added Deepslate Gold Ore to experience.yml for Mining
+    Added Deepslate Lapis Lazuli Ore to experience.yml for Mining
+    Added Lapis Lazuli Ore to experience.yml for Mining (was missing)
+
+    Added Moss Block to Bonus Drops for Herbalism in config.yml
+    Added Glow Berries to Bonus Drops for Herbalism in config.yml
+    Added Cave Vines to Bonus Drops for Herbalism in config.yml
+    Added Cave Vines Plant to Bonus Drops for Herbalism in config.yml
+    Added Tuff to Bonus Drops for Mining in config.yml
+    Added Amethyst Shard to Bonus Drops for Mining in config.yml
+    Added Calcite to Bonus Drops for Mining in config.yml
+    Added Smooth Basalt to Bonus Drops for Mining in config.yml
+    Added Block_Of_Amethyst to Bonus Drops for Mining in config.yml
+    Added Budding Amethyst to Bonus Drops for Mining in config.yml
+    Added Small_Amethyst_Bud to Bonus Drops for Mining in config.yml
+    Added Medium Amethyst Bud to Bonus Drops for Mining in config.yml
+    Added Large Amethyst Bud to Bonus Drops for Mining in config.yml
+    Added Amethyst Cluster to Bonus Drops for Mining in config.yml
+    Added Deepslate to Bonus Drops for Mining in config.yml
+    Added Cobbled Deepslate to Bonus Drops for Mining in config.yml
+    Added Raw Iron to Bonus Drops for Mining in config.yml
+    Added Raw Gold to Bonus Drops for Mining in config.yml
+    Added Raw Copper to Bonus Drops for Mining in config.yml
+    Added Copper Ore to Bonus Drops for Mining in config.yml
+    Added Deepslate Redstone Ore to Bonus Drops for Mining in config.yml
+    Added Deepslate Copper Ore to Bonus Drops for Mining in config.yml
+    Added Deepslate Coal Ore to Bonus Drops for Mining in config.yml
+    Added Deepslate Diamond Ore to Bonus Drops for Mining in config.yml
+    Added Deepslate Emerald Ore to Bonus Drops for Mining in config.yml
+    Added Deepslate Iron Ore to Bonus Drops for Mining in config.yml
+    Added Deepslate Gold Ore to Bonus Drops for Mining in config.yml
+    Added Deepslate Lapis Lazuli Ore to Bonus Drops for Mining in config.yml
+    Added Lapis Lazuli Ore to Bonus Drops for Mining in config.yml (was missing)
+
+    Added Cobbled Deepslate to experience.yml for Smelting
+    Added Raw Copper to experience.yml for Smelting
+    Added Raw Iron to experience.yml for Smelting
+    Added Raw Gold to experience.yml for Smelting
+    Added Copper Ore to experience.yml for Smelting
+    Added Deepslate Redstone Ore to experience.yml for Smelting
+    Added Deepslate Copper Ore to experience.yml for Smelting
+    Added Deepslate Coal Ore to experience.yml for Smelting
+    Added Deepslate Diamond Ore to experience.yml for Smelting
+    Added Deepslate Emerald Ore to experience.yml for Smelting
+    Added Deepslate Iron Ore to experience.yml for Smelting
+    Added Deepslate Gold Ore to experience.yml for Smelting
+    Added Deepslate Lapis Lazuli Ore to experience.yml for Smelting
+    Added Lapis Lazuli Ore to experience.yml for Smelting (was missing)
+
+    NOTES:
+    Another patch will follow this one shortly to add more 1.17 support, I am waiting on the Spigot API to test some stuff which isn't out yet.
+    You shouldn't need to edit your configs for this update, your configs should update automatically.
+    Expect some patches following this update for Axolotl and other new entities, waiting on the Spigot API to become available
+    If you run into any issues with ViaVersion or ViaBackwards, use the latest dev builds for each.
+
+Version 2.1.196
+    Removed the explosion from Rupture
+    Adjusted Rupture to play its particle effect less often
+    Fixed a bug where Rupture never applied to additional targets during Serrated Strikes
+    Fixed a bug where players without Rupture permission could use Rupture during Serrated Strikes
+    Fixed a possible null error for our SelfListener
+    Added locale string 'Swords.Combat.Rupture.Note.Update.One'
+    Updated locale string 'Guides.Swords.Section.1'
+    Crossbows can now be fished up with enchantments
+    (API) Added McMMOEntityDamageByRuptureEvent (thanks qixils)
+
+    NOTES:
+    For now Rupture is non-lethal, I may add back a lethal component at the end of its damage
+    Rupture will be in a state of change for a while as I receive feedback (give me feedback in Discord!)
+    Crossbows is not in the default fishing loot list, you'd have to add it yourself.
+    For Devs: McMMOEntityDamageByRuptureEvent extends EntityDamageByEntityEvent and uses CUSTOM type damage
+
+Version 2.1.195
+    Fixed a null connection error which affected some SQL users
+
+Version 2.1.194
+    Fixed an XP exploit
+    Updated SQL to not throw errors if upgrades.yml was reset for any reason
+    Updated SQL to use the newest driver path (and fall back to the old one if the new one doesn't exist on the server)
+    Locale override files are now named locale_override.properties (converted automatically/generated automatically)
+    Existing in use locale override files will be renamed to locale_override.properties and have some useful text put in them
+    mcMMO will now generate a locale override file with some detailed instructions if one doesn't exist (will be found in /plugins/mcMMO/locales/locale_override.properties)
+
+    NOTES:
+    If you were overriding locale before this update mcMMO will just rename the existing override file to locale_override.properties add some useful text and then load it
+    Remember you can use /mcreloadlocale to swap the edits in without restarting the server
+
+Version 2.1.193
+    Fixed another bug where mcrank/mctop/leaderboards weren't loading
+    Fixed a bug where override locales weren't being loaded (but worked after a reloadlocale command)
+    (Unit Tests) Added a test to make sure leaderboards for FlatFile were working
+    Fixed blocks being dropped from blast mining even if yield was set to 0 (thanks Warriorrrr)
+    Fixed Tree feller not working entirely if one fake block break event is cancelled. (thanks Warriorrrr)
+    Fixes no woodcutting xp being rewarded if a tree is too big while using tree feller. (thanks Warriorrrr)
+    Updated pl locale (thanks Mich3l3k)
+
+Version 2.1.192
+    Removed some debug messages from FlatFileDatabaseManager
+    Fixed another bug where player names could be saved as null for FlatFileDB (they will update on the players next login at the next save interval)
+    (API) Removed deprecation from com.gmail.nossr50.api.ExperienceAPI.getOfflineProfile(java.lang.String)
+    (API) Added com.gmail.nossr50.api.DatabaseAPI.doesPlayerExistInDB(org.bukkit.OfflinePlayer)
+    (API) Added com.gmail.nossr50.api.ExperienceAPI.getOfflineProfile(org.bukkit.OfflinePlayer)
+    (API) Added com.gmail.nossr50.api.ExperienceAPI.getOfflineXP(org.bukkit.OfflinePlayer, java.lang.String)
+    (API) Added com.gmail.nossr50.api.ExperienceAPI.getOfflineXPRaw(org.bukkit.OfflinePlayer, com.gmail.nossr50.datatypes.skills.PrimarySkillType)
+    (API) Added com.gmail.nossr50.api.ExperienceAPI.getOfflineXPRaw(org.bukkit.OfflinePlayer, java.lang.String)
+    (API) Added com.gmail.nossr50.api.ExperienceAPI.getOfflineXPToNextLevel(org.bukkit.OfflinePlayer, java.lang.String)
+    (API) Added com.gmail.nossr50.api.DatabaseAPI.doesPlayerExistInDB(java.lang.String)
+    (Unit Tests) Added some more unit tests to FlatFileDB
+
+Version 2.1.191
+    Fixed a bug related to our blocktracker
+    Fixed a bug that prevented the leaderboards from working on FlatFile in some circumstances
+    Some minor optimizations to our Block events
+    (Unit Tests) Added a test for initializing the leaderboard on FlatFile
+Version 2.1.190
+    Fixed a null error in BitSetChunkStore
+Version 2.1.189
+    Fixed a bug that would remove components from death messages when players were killed by mobs (thanks lexikiq)
+    Rewrote how FlatFileDatabase verifies data integrity
+    FlatFileDatabase has much better data validation and will repair broken/invalid data much better
+    Fixed a bug where FlatFileDatabase users could have their names saved as "null" (names will be fixed the next time the player logs in)
+    Added 20~ unit tests for FlatFileDatabaseManager (see notes)
+    FlatFileDB now stores the last login of users again (was completely non functional for a while)
+    Newly created flat file databases (mcmmo.users file) will have a comment line at the top noting the date the database was created
+    Minor performance optimizations to FlatFile database
+    mcMMO will once again purge old users if the config option is on (see notes)
+    Fixed a bug where FlatFileDatabaseManager didn't properly upgrade older database entries to the newest schema
+    The setting to disable the mcMMO user block tracker has been moved from our "hidden config" to persistent_data.yml
+    Added 'mcMMO_Region_System.Enabled' to persistent_data.yml (don't touch this setting unless you know what you are doing)
+    Removed MHD command (it didn't do anything for a while now)
+    Removed UltraPermissions warning
+    Updated pl locale (Thanks Mich3l3k)
+    Fixed an IllegalPluginAccessException error that could happen during server shutdown
+    Minor performance optimizations to misc parts of the codebase
+    (API) Added com.gmail.nossr50.database.DatabaseManager.loadPlayerProfile(org.bukkit.OfflinePlayer)
+    (API) Deprecated com.gmail.nossr50.database.DatabaseManager.loadPlayerProfile(java.util.UUID, java.lang.String)
+    (API) Removed com.gmail.nossr50.database.DatabaseManager.newUser(java.lang.String, java.util.UUID)
+    (API) PrimarySkillType will soon be just an enum with nothing special going on
+    (API) Deprecated the members of PrimarySkillType use mcMMO::getSkillTools instead, deprecated members will be removed in Tridents & Crossbows (due soon)
+    (API) Some members of PrimarySkillType were removed and not deprecated (such as the field constants)
+
+    NOTES:
+    I spent over 26 hours refactoring FlatFileDB and writing unit tests for it, this will ensure that any changes in the code that could break the database are caught at compile time (so long as we have enough tests, could probably use more)
+    Regarding purging old users on the FlatFileDB, since this wasn't functioning for a while, the last login of users has been reset and if mcMMO hasn't seen that user since this update, it won't purge them as it has no way to know if they are truly an old user
+    I'm likely going to add SQLite DB as an option in the future, I spent time to fix up the FlatFileDB as some Unit Testing practice.
+    Ultra Permissions is SAFE to use with mcMMO, disregard previous messages stating otherwise
+    After getting in contact with the UltraPermissions devs and exhaustive testing, I have concluded that using UltraPermissions is completely safe with mcMMO. The users who had an issue with performance currently have an unknown cause, potentially it is from a plugin using the UltraPermissions API I really can't say without more data. My apologies to the UltraPermissions team for reporting an issue between our two plugins directly, as that is not the case. I would have tested it myself sooner but UltraPermissions was closed source and premium so I wasn't particularly motivated to do so, however I have been given access to the binaries so now I can do all the testing I want if future issues ever arise which I have zero expectations that they will.
+
+Version 2.1.188
+    Updated default entries in treasures.yml to use "Level_Requirement" instead of "Drop_Level"
+    Fixed a bug where excavation treasures only required level 0 instead of loading the value from the config
+    Fixed a bug where /fishing was showing the wrong shake chance
+    Default Shake chance increased from 15% to 30% (update advanced.yml manually or delete the file to regenerate it and receive these changes)
+    Removed entries for ranks 2-8 of Shake from advanced.yml (Shake only has one rank, the extra entries were a mistake)
+    Modified the warning about UltraPermissions
+    Removed the debug message about potentially unused keys (only shown if you had debug mode enabled in config.yml)
+
+    NOTES:
+    This update makes changes to treasures.yml automatically to apply the fix, you don't need to do anything
+    UltraPermissions devs are working on a fix for the performance issue bug
+
+Version 2.1.187
+    Fixed a ClassCastException error involving Rupture
+
+Version 2.1.186
+    Rupture has been reworked to solve a few outstanding issues (see notes)
+    Fixed an exploit involving enchantments (thanks TheBusyBiscuit)
+    Fixed a very small memory leak that would only happen in very rare situations
+    Fixed a bug where XP wasn't granted while sneaking and interacting with a berry bush
+    Gore no longer applies Rupture
+    Gore no longer sends a message to the Wolf owner when it triggers
+    Gore no longer sends a message to players that are hit by it
+    Rupture no longer sends a message telling you that your target is bleeding
+    Updated locale string 'Swords.SubSkill.Rupture.Description'
+    Updated locale string 'Swords.SubSkill.Rupture.Stat.Extra'
+    Updated locale string 'Swords.Combat.Rupture.Note'
+    Added locale string 'Swords.SubSkill.Rupture.Stat.TickDamage'
+    Added locale string 'Swords.SubSkill.Rupture.Stat.ExplosionDamage'
+    Added 'Skills.Swords.Rupture.Rupture_Mechanics.Chance_To_Apply_On_Hit' to advanced.yml
+    Added 'Skills.Swords.Rupture.Rupture_Mechanics.Duration_In_Seconds.Against_Players' to advanced.yml
+    Added 'Skills.Swords.Rupture.Rupture_Mechanics.Duration_In_Seconds.Against_Mobs' to advanced.yml
+    Added 'Skills.Swords.Rupture.Rupture_Mechanics.Tick_Interval_Damage.Against_Players' to advanced.yml
+    Added 'Skills.Swords.Rupture.Rupture_Mechanics.Tick_Interval_Damage.Against_Mobs' to advanced.yml
+    Added 'Skills.Swords.Rupture.Rupture_Mechanics.Explosion_Damage.Against_Players' to advanced.yml
+    Added 'Skills.Swords.Rupture.Rupture_Mechanics.Explosion_Damage.Against_Mobs' to advanced.yml
+    Removed 'Skills.Swords.Rupture.ChanceMax' from advanced.yml
+    Removed 'Skills.Swords.Rupture.MaxBonusLevel.Standard' from advanced.yml
+    Removed 'Skills.Swords.Rupture.MaxBonusLevel.RetroMode' from advanced.yml
+    Removed 'Skills.Swords.Rupture.MaxTicks' from advanced.yml
+    Removed 'Skills.Swords.Rupture.BaseTicks' from advanced.yml
+    Removed 'Skills.Swords.Rupture.DamagePlayer' from advanced.yml
+    Removed 'Skills.Swords.Rupture.DamageMobs' from advanced.yml
+
+    NOTES:
+    The old Rupture would constantly interfere with your ability to do a Sweep Attack/Swipe with swords, the new one solves this problem
+    Targets will bleed and take "pure" damage while bleeding, this never kills the target. It will reduce them to 0.01 HP.
+    After 5 seconds of not applying Rupture on the target Rupture explodes dealing a large amount of damage, this damage is not pure and is affected by armor etc.
+    Rupture no longer tells you that you that you applied it to the target, it should be obvious from the sounds/particle effects
+    The new Rupture no longer constantly interferes with the vanilla Swipe (the AOE attack built into Minecraft)
+    The new Rupture has not had a fine tuned balance pass, I will be balancing it frequently after this patch, it may be too weak or too strong in its current form
+    Rupture does not stack between players, whoever applied Rupture first determines its strength, this will change in the future (Stronger Ruptures will overwrite weaker ones)
+    When you reapply rupture it immediately triggers a damage tick
+
+
+
+Version 2.1.185
+    Fixed an exploit for Herbalism
+
+Version 2.1.184
+    Removed April Fools event
+    Fixed a bug where the default treasures.yml file had incorrect keys (see notes)
+
+    NOTES:
+    mcMMO will fix bad config files automatically, you don't need to do anything
+
+Version 2.1.183
+    Players now gain Acrobatics XP from falling even if they don't have the Roll permission node (checks for Acrobatics skill permission)
+    treasures.yml now has separate settings for Drop_Level for Standard/Retro mode (see notes / this change is automatic)
+    Updated Russian locale (thanks ImDaniX)
+    Added Donkeys to beat lore (thanks QuantumToasted)
+    Alchemy guide now correctly labels Rabbit's foot for potion of leaping (thanks mldriscoll)
+    Fixed a bug where sweet berry bushes would give XP in situations where they shouldn't
+    The /mmoinfo for Roll is removed for the time being, it will return in a future update (see notes)
+
+    NOTES:
+    Previously treasures.yml would take drop_level and multiply it by 10 if you were on Retro Mode, this is confusing so it has been changed
+    treasures.yml will update old entries to follow the new format automatically, please review them to make sure they match expected values, and if not edit them and save then reboot your server
+    Roll is actually the only skill that had an /mmoinfo, its one big mess, I'll fix it in the future
+
+Version 2.1.182
+    Players now receive XP from harvesting Sweet Berry bushes (double XP for harvesting fully grown berries)
+    Fixed an error when using mcMMO with Featherboard that broke mcMMO skill boards when using certain commands
+    Fixed a NPE with Scoreboards enabled when trying to update scoreboards
+    Added 'Scoreboard.Recovery' locale key
+    Sweet Berry Bush will no longer ready tools for Super Abilities
+    You can now use '.all' (for example: mcmmo.perks.xp.customboost.all) to give an XP perk to all skills
+    Removed hardcore and vampirism commands, these commands are dangerous, just modify the config file if you want to use hardcore / vampirism
+    Fixed several errors in de locale (Thanks TheBusyBiscuit & w1tcherrr)
+    Fixed a bug where double smelt never succeeded if the furnace was empty (but worked normally afterwards)
+    Added some safety so that mcMMO automatic save interval is never more frequent than 1 minute
+    Removed a few silent exceptions for scoreboards & mcMMO
+    Added warning about UltraPermissions to mcMMO
+    Fixed a potential NPE in McMMOPlayerExperienceEvent
+    Added Sweet Berry Bush to config.yml bonus drops for Herbalism
+
+    NOTES:
+    Sweet Berry Bushes won't give double drops for now, looking into an elegant solution
+    mcMMO will do a better job reporting if something went wrong with scoreboards, which may lead to improved plugin compatibility between mcMMO and other plugins touching scoreboards.
+
+Version 2.1.181
+    mcMMO no longer pointlessly tries to check for missing UUIDs for FlatFile database
+    Removed the "name change detected" message as some plugins (such as Plan) invoke API calls which spams the console with this message
+    Refactored code related to loading player data from the database
+    (API) Added DatabaseManager::loadPlayerProfile(String)
+    (API) Removed DatabaseManager::loadPlayerProfile(String, UUID, boolean)
+    (API) Removed DatabaseManager::loadPlayerProfile(String, boolean)
+
+Version 2.1.180
+    mcMMO will now automatically remove corrupted data from mcmmo.users instead of catastrophic failure
+    When using FlatFile database (the default) mcMMO will try its best to inform you which players had corrupted data when it does repairs
+    Various minor optimizations and tweaks to the FlatFile database
+    mcMMO is now much more verbose when things go wrong with the FlatFile database (removed some silent errors, added more error messages/warnings)
+    mcMMO now uses UTF-8 compliant encoding for SQL databases (utf8mb4)
+    Fixed a bug where mcMMO could in some circumstances fail to update SQL schema and mark it as successful
+    Renamed updates.yml to updates_overhaul.yml to avoid some potential issues when upgrading from classic
+
+    NOTES:
+    This update was tested pretty thoroughly so it should be pretty safe, let me know if you have issues in the mcMMO discord or GitHub issues page for mcMMO!
+
+Version 2.1.179
+    Fixed a bug for FlatFile databases where some players with changed nicknames would have their levels not loaded upon login (possibly wiping their data)
+
+    NOTES:
+    Players affected by this bug (introduced in 2.1.177) may have their data lost, but this patch reverts the change which caused this bug.
+    I suspect their data isn't lost and may be restored after this patch is loaded up, however if it is lost mcMMO makes regular backups so you can load one of those (check <Server Directory>/plugins/mcMMO/) or manually edit their levels via MMOEDIT as a solution of sorts.
+
+Version 2.1.178
+    Item replacement in vanilla fishing override back to SALMON from AIR (see notes)
+
+    NOTES:
+    Apparently can't set items to AIR, my bad. I'll look into another solution for fishing plugin compatibility soon.
+
+Version 2.1.177
+    Environmentally aware will now protect Wolves from Magma blocks
+    Fixed a bug where mcMMO would fail to update a players name when it detected a name change
+    mcMMO will treat vanished players as if they are offline when using the inspect command on them now (see notes)
+    mcMMO now listens to PlayerFishEvent at HIGH event priority instead of HIGHEST
+    Changed how vanilla fishing treasures are overridden (AIR instead of SALMON)
+    (API) Added McMMOReplaceVanillaTreasureEvent -- see notes
+
+    NOTES:
+    A few changes were made to the inspect command, it used to reject you when used on vanished players, now it will be processed as if they are offline.
+    Additionally if you do inspect a vanished player, it will not use their display name (consistent with offline players) as that would give them away for being online
+    McMMOReplaceVanillaTreasureEvent is an event which is fired when mcMMO replaces a vanilla treasure with AIR if the server config file is set to override vanilla treasures, this causes some issues for other fishing plugins so this event helps those plugins be more compatible
+
+Version 2.1.176
+    Another fix for Double Smelt bringing item stack size to illegal values
+
 Version 2.1.175
 Version 2.1.175
     Fixed a bug where mcMMO would occasionally give a 65 item stack from a double smelt on a furnace
     Fixed a bug where mcMMO would occasionally give a 65 item stack from a double smelt on a furnace
     Fixed a bug where arrows could be duped when fired from a crossbow with piercing enchantment
     Fixed a bug where arrows could be duped when fired from a crossbow with piercing enchantment
     Added setting to enable or disable Green Thumb automatically replanting crops per crop to config.yml under 'Green_Thumb_Replanting_Crops' section
     Added setting to enable or disable Green Thumb automatically replanting crops per crop to config.yml under 'Green_Thumb_Replanting_Crops' section
+    Updated Adventure (our text dependency) fixes some errors when using color codes in party/admin chat (thanks TheBusyBiscuit)
+    Added some support for negative Y values in anticipation of 1.17 world height changes (thanks t00thpick1)
 
 
 Version 2.1.174
 Version 2.1.174
     Some legacy color codes in our locale file were swapped to &-code equivalents (thanks ViaSnake)
     Some legacy color codes in our locale file were swapped to &-code equivalents (thanks ViaSnake)

+ 89 - 52
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.176-SNAPSHOT</version>
+    <version>2.1.218</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>
@@ -11,11 +11,29 @@
         <developerConnection>scm:git:git@github.com:mcMMO-Dev/mcMMO.git</developerConnection>
         <developerConnection>scm:git:git@github.com:mcMMO-Dev/mcMMO.git</developerConnection>
         <tag>HEAD</tag>
         <tag>HEAD</tag>
     </scm>
     </scm>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <maven.compiler.source>16</maven.compiler.source>
+        <maven.compiler.target>16</maven.compiler.target>
+        <java.version>16</java.version>
+    </properties>
+
     <issueManagement>
     <issueManagement>
         <url>https://github.com/mcMMO-Dev/mcMMO/issues</url>
         <url>https://github.com/mcMMO-Dev/mcMMO/issues</url>
         <system>GitHub</system>
         <system>GitHub</system>
     </issueManagement>
     </issueManagement>
     <packaging>jar</packaging>
     <packaging>jar</packaging>
+    <distributionManagement>
+        <repository>
+            <id>neetgames</id>
+            <url>https://nexus.neetgames.com/repository/maven-releases/</url>
+        </repository>
+        <snapshotRepository>
+            <id>neetgames</id>
+            <url>https://nexus.neetgames.com/repository/maven-snapshots/</url>
+        </snapshotRepository>
+    </distributionManagement>
     <build>
     <build>
         <finalName>${project.artifactId}</finalName>
         <finalName>${project.artifactId}</finalName>
         <sourceDirectory>${basedir}/src/main/java</sourceDirectory>
         <sourceDirectory>${basedir}/src/main/java</sourceDirectory>
@@ -55,21 +73,39 @@
             </resource>
             </resource>
         </resources>
         </resources>
         <plugins>
         <plugins>
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>3.0.0-M7</version>
+
+                <configuration>
+                    <junitArtifactName>org.junit.jupiter:junit-jupiter</junitArtifactName>
+                    <trimStackTrace>false</trimStackTrace>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <version>3.0.0-M7</version>
+
+                <configuration>
+                    <junitArtifactName>org.junit.jupiter:junit-jupiter</junitArtifactName>
+                    <trimStackTrace>false</trimStackTrace>
+                </configuration>
+            </plugin>
+
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-release-plugin</artifactId>
                 <artifactId>maven-release-plugin</artifactId>
-                <version>2.5.2</version>
+                <version>3.0.0-M6</version>
             </plugin>
             </plugin>
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <artifactId>maven-compiler-plugin</artifactId>
-                <version>3.8.1</version>
+                <version>3.10.1</version>
                 <configuration>
                 <configuration>
+                    <release>16</release>
                     <compilerArgs>
                     <compilerArgs>
                         <arg>-parameters</arg> <!-- used for ACF syntax stuff -->
                         <arg>-parameters</arg> <!-- used for ACF syntax stuff -->
                     </compilerArgs>
                     </compilerArgs>
-                    <source>1.8</source>
-                    <target>1.8</target>
                     <excludes>
                     <excludes>
                     </excludes>
                     </excludes>
                 </configuration>
                 </configuration>
@@ -94,7 +130,7 @@
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-shade-plugin</artifactId>
                 <artifactId>maven-shade-plugin</artifactId>
-                <version>3.2.3</version>
+                <version>3.3.0</version>
                 <configuration>
                 <configuration>
                     <artifactSet>
                     <artifactSet>
                         <includes>
                         <includes>
@@ -107,7 +143,6 @@
                             <include>net.kyori:adventure-text-serializer-gson</include>
                             <include>net.kyori:adventure-text-serializer-gson</include>
                             <include>net.kyori:adventure-platform-bukkit</include>
                             <include>net.kyori:adventure-platform-bukkit</include>
                             <include>net.kyori:adventure-platform-api</include>
                             <include>net.kyori:adventure-platform-api</include>
-                            <include>net.kyori:adventure-platform-common</include>
                             <include>net.kyori:adventure-platform-viaversion</include>
                             <include>net.kyori:adventure-platform-viaversion</include>
                             <include>net.kyori:adventure-platform-facet</include>
                             <include>net.kyori:adventure-platform-facet</include>
                             <include>net.kyori:adventure-nbt</include>
                             <include>net.kyori:adventure-nbt</include>
@@ -121,11 +156,10 @@
                             <include>co.aikar:acf-bukkit</include>
                             <include>co.aikar:acf-bukkit</include>
                         </includes>
                         </includes>
                     </artifactSet>
                     </artifactSet>
-<!--                    <dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xml</dependencyReducedPomLocation>-->
                     <relocations>
                     <relocations>
                         <relocation>
                         <relocation>
                             <pattern>net.kyori.examination</pattern>
                             <pattern>net.kyori.examination</pattern>
-                            <shadedPattern>com.gmail.nossr50.kyori.examination</shadedPattern>
+                            <shadedPattern>com.gmail.nossr50.mcmmo.kyori.examination</shadedPattern>
                         </relocation>
                         </relocation>
                         <relocation>
                         <relocation>
                             <pattern>net.kyori.adventure</pattern>
                             <pattern>net.kyori.adventure</pattern>
@@ -171,10 +205,17 @@
             <extension>
             <extension>
                 <groupId>org.apache.maven.wagon</groupId>
                 <groupId>org.apache.maven.wagon</groupId>
                 <artifactId>wagon-file</artifactId>
                 <artifactId>wagon-file</artifactId>
-                <version>2.2</version>
+                <version>3.5.2</version>
             </extension>
             </extension>
         </extensions>
         </extensions>
     </build>
     </build>
+    <pluginRepositories>
+        <pluginRepository>
+            <id>maven-snapshots</id>
+            <url>https://repository.apache.org/content/repositories/snapshots/</url>
+        </pluginRepository>
+    </pluginRepositories>
+
     <repositories>
     <repositories>
         <repository>
         <repository>
             <id>spigot-repo</id>
             <id>spigot-repo</id>
@@ -185,88 +226,87 @@
             <url>https://repo.codemc.org/repository/maven-public</url>
             <url>https://repo.codemc.org/repository/maven-public</url>
         </repository>
         </repository>
         <repository>
         <repository>
-            <id>sk89q-repo</id>
-            <url>https://maven.sk89q.com/repo/</url>
+            <id>enginehub-repo</id>
+            <url>https://maven.enginehub.org/repo/</url>
+        </repository>
+        <repository> <!-- for development builds -->
+            <id>sonatype-oss</id>
+            <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
         </repository>
         </repository>
         <repository>
         <repository>
             <id>aikar</id>
             <id>aikar</id>
             <url>https://repo.aikar.co/content/groups/aikar/</url>
             <url>https://repo.aikar.co/content/groups/aikar/</url>
         </repository>
         </repository>
-        <!-- ... -->
-        <repository> <!-- for development builds -->
-            <id>sonatype-oss</id>
-            <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
+        <repository>
+            <id>sonatype-oss-snapshots1</id>
+            <url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
         </repository>
         </repository>
         <!-- ... -->
         <!-- ... -->
+        <!-- ... -->
     </repositories>
     </repositories>
     <dependencies>
     <dependencies>
         <dependency>
         <dependency>
             <groupId>co.aikar</groupId>
             <groupId>co.aikar</groupId>
             <artifactId>acf-bukkit</artifactId> <!-- Don't forget to replace this -->
             <artifactId>acf-bukkit</artifactId> <!-- Don't forget to replace this -->
-            <version>0.5.0-SNAPSHOT</version> <!-- Replace this as well -->
+            <version>0.5.1-SNAPSHOT</version> <!-- Replace this as well -->
         </dependency>
         </dependency>
 <!--        adventure-api, adventure-text-serializer-gson, adventure-platform-bukkit-->
 <!--        adventure-api, adventure-text-serializer-gson, adventure-platform-bukkit-->
         <dependency>
         <dependency>
             <groupId>net.kyori</groupId>
             <groupId>net.kyori</groupId>
             <artifactId>adventure-text-serializer-gson</artifactId>
             <artifactId>adventure-text-serializer-gson</artifactId>
-            <version>4.5.1</version>
+            <version>4.11.0</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>net.kyori</groupId>
             <groupId>net.kyori</groupId>
             <artifactId>adventure-api</artifactId>
             <artifactId>adventure-api</artifactId>
-            <version>4.5.1</version>
+            <version>4.11.0</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>net.kyori</groupId>
             <groupId>net.kyori</groupId>
             <artifactId>adventure-nbt</artifactId>
             <artifactId>adventure-nbt</artifactId>
-            <version>4.5.1</version>
+            <version>4.11.0</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>net.kyori</groupId>
             <groupId>net.kyori</groupId>
             <artifactId>adventure-key</artifactId>
             <artifactId>adventure-key</artifactId>
-            <version>4.5.1</version>
+            <version>4.11.0</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>net.kyori</groupId>
             <groupId>net.kyori</groupId>
             <artifactId>adventure-text-serializer-gson-legacy-impl</artifactId>
             <artifactId>adventure-text-serializer-gson-legacy-impl</artifactId>
-            <version>4.5.1</version>
+            <version>4.11.0</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>net.kyori</groupId>
             <groupId>net.kyori</groupId>
             <artifactId>adventure-platform-bukkit</artifactId>
             <artifactId>adventure-platform-bukkit</artifactId>
-            <version>4.0.0-SNAPSHOT</version>
+            <version>4.1.2</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>net.kyori</groupId>
             <groupId>net.kyori</groupId>
             <artifactId>adventure-platform-api</artifactId>
             <artifactId>adventure-platform-api</artifactId>
-            <version>4.0.0-SNAPSHOT</version>
-        </dependency>
-        <dependency>
-            <groupId>net.kyori</groupId>
-            <artifactId>adventure-platform-common</artifactId>
-            <version>4.0.0-SNAPSHOT</version>
+            <version>4.1.2</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.apache.maven.scm</groupId>
             <groupId>org.apache.maven.scm</groupId>
             <artifactId>maven-scm-provider-gitexe</artifactId>
             <artifactId>maven-scm-provider-gitexe</artifactId>
-            <version>1.9.4</version>
+            <version>2.0.0-M1</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.bstats</groupId>
             <groupId>org.bstats</groupId>
             <artifactId>bstats-bukkit</artifactId>
             <artifactId>bstats-bukkit</artifactId>
-            <version>2.2.1</version>
+            <version>3.0.0</version>
             <scope>compile</scope>
             <scope>compile</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.spigotmc</groupId>
             <groupId>org.spigotmc</groupId>
             <artifactId>spigot-api</artifactId>
             <artifactId>spigot-api</artifactId>
-            <version>1.16.5-R0.1-SNAPSHOT</version>
+            <version>1.19.2-R0.1-SNAPSHOT</version>
             <scope>provided</scope>
             <scope>provided</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>com.sk89q.worldguard</groupId>
             <groupId>com.sk89q.worldguard</groupId>
             <artifactId>worldguard-core</artifactId>
             <artifactId>worldguard-core</artifactId>
-            <version>7.0.1-SNAPSHOT</version>
+            <version>7.0.7</version>
             <exclusions>
             <exclusions>
                 <exclusion>
                 <exclusion>
                     <!-- We use jetbrains instead. Excluding this -->
                     <!-- We use jetbrains instead. Excluding this -->
@@ -288,42 +328,39 @@
             </exclusions>
             </exclusions>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit-dep</artifactId>
-            <version>4.11</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.powermock</groupId>
-            <artifactId>powermock-module-junit4</artifactId>
-            <version>2.0.7</version>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>5.9.0</version>
             <scope>test</scope>
             <scope>test</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
-            <groupId>org.powermock</groupId>
-            <artifactId>powermock-api-mockito2</artifactId>
-            <version>2.0.7</version>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>4.6.1</version>
             <scope>test</scope>
             <scope>test</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.mockito</groupId>
             <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <version>3.8.0</version>
+            <artifactId>mockito-inline</artifactId>
+            <version>4.6.1</version>
             <scope>test</scope>
             <scope>test</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.apache.tomcat</groupId>
             <groupId>org.apache.tomcat</groupId>
             <artifactId>tomcat-jdbc</artifactId>
             <artifactId>tomcat-jdbc</artifactId>
-            <version>7.0.52</version>
+            <version>10.1.0-M17</version>
             <scope>compile</scope>
             <scope>compile</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.jetbrains</groupId>
             <groupId>org.jetbrains</groupId>
             <artifactId>annotations</artifactId>
             <artifactId>annotations</artifactId>
-            <version>19.0.0</version>
+            <version>23.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>31.1-jre</version> <!-- At this time Spigot is including 29.0 Guava classes that we are using -->
+            <scope>compile</scope>
         </dependency>
         </dependency>
     </dependencies>
     </dependencies>
-    <properties>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-    </properties>
 </project>
 </project>

+ 8 - 2
src/main/java/com/gmail/nossr50/api/AbilityAPI.java

@@ -2,7 +2,7 @@ package com.gmail.nossr50.api;
 
 
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
 import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
-import com.gmail.nossr50.runnables.skills.BleedTimerTask;
+import com.gmail.nossr50.util.MetadataConstants;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
@@ -83,6 +83,12 @@ public final class AbilityAPI {
     }
     }
 
 
     public static boolean isBleeding(LivingEntity entity) {
     public static boolean isBleeding(LivingEntity entity) {
-        return BleedTimerTask.isBleeding(entity);
+        if(entity.isValid()) {
+            if(entity.hasMetadata(MetadataConstants.METADATA_KEY_RUPTURE)) {
+                return true;
+            }
+        }
+
+        return false;
     }
     }
 }
 }

+ 26 - 6
src/main/java/com/gmail/nossr50/api/DatabaseAPI.java

@@ -2,6 +2,8 @@ package com.gmail.nossr50.api;
 
 
 import com.gmail.nossr50.datatypes.player.PlayerProfile;
 import com.gmail.nossr50.datatypes.player.PlayerProfile;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
+import org.bukkit.OfflinePlayer;
+import org.jetbrains.annotations.NotNull;
 
 
 import java.util.UUID;
 import java.util.UUID;
 
 
@@ -9,20 +11,38 @@ public class DatabaseAPI {
 
 
     /**
     /**
      * Checks if a player exists in the mcMMO Database
      * Checks if a player exists in the mcMMO Database
-     * @param uuid player UUID
+     * @param offlinePlayer target player
      * @return true if the player exists in the DB, false if they do not
      * @return true if the player exists in the DB, false if they do not
      */
      */
-    public boolean doesPlayerExistInDB(String uuid) {
-        return doesPlayerExistInDB(UUID.fromString(uuid));
+    public boolean doesPlayerExistInDB(@NotNull OfflinePlayer offlinePlayer) {
+        PlayerProfile playerProfile = mcMMO.getDatabaseManager().loadPlayerProfile(offlinePlayer);
+
+        return playerProfile.isLoaded();
+    }
+
+    /**
+     * Checks if a player exists in the mcMMO Database
+     * @param uuid target player
+     * @return true if the player exists in the DB, false if they do not
+     */
+    public boolean doesPlayerExistInDB(@NotNull UUID uuid) {
+        PlayerProfile playerProfile = null;
+        try {
+            playerProfile = mcMMO.getDatabaseManager().loadPlayerProfile(uuid);
+        } catch (Exception e) {
+            return false;
+        }
+
+        return playerProfile.isLoaded();
     }
     }
 
 
     /**
     /**
      * Checks if a player exists in the mcMMO Database
      * Checks if a player exists in the mcMMO Database
-     * @param uuid player UUID
+     * @param playerName target player
      * @return true if the player exists in the DB, false if they do not
      * @return true if the player exists in the DB, false if they do not
      */
      */
-    public boolean doesPlayerExistInDB(UUID uuid) {
-        PlayerProfile playerProfile = mcMMO.getDatabaseManager().loadPlayerProfile(uuid);
+    public boolean doesPlayerExistInDB(@NotNull String playerName) {
+        PlayerProfile playerProfile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName);
 
 
         return playerProfile.isLoaded();
         return playerProfile.isLoaded();
     }
     }

+ 113 - 28
src/main/java/com/gmail/nossr50/api/ExperienceAPI.java

@@ -1,7 +1,6 @@
 package com.gmail.nossr50.api;
 package com.gmail.nossr50.api;
 
 
 import com.gmail.nossr50.api.exceptions.*;
 import com.gmail.nossr50.api.exceptions.*;
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.config.experience.ExperienceConfig;
 import com.gmail.nossr50.config.experience.ExperienceConfig;
 import com.gmail.nossr50.datatypes.experience.FormulaType;
 import com.gmail.nossr50.datatypes.experience.FormulaType;
 import com.gmail.nossr50.datatypes.experience.XPGainReason;
 import com.gmail.nossr50.datatypes.experience.XPGainReason;
@@ -13,9 +12,12 @@ import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.child.FamilyTree;
 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 org.bukkit.OfflinePlayer;
 import org.bukkit.block.BlockState;
 import org.bukkit.block.BlockState;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Set;
 import java.util.Set;
@@ -33,8 +35,8 @@ public final class ExperienceAPI {
      * @param skillType A string that may or may not be a skill
      * @param skillType A string that may or may not be a skill
      * @return true if this is a valid mcMMO skill
      * @return true if this is a valid mcMMO skill
      */
      */
-    public static boolean isValidSkillType(String skillType) {
-        return PrimarySkillType.getSkill(skillType) != null;
+    public static boolean isValidSkillType(@NotNull String skillType) {
+        return mcMMO.p.getSkillTools().matchSkill(skillType) != null;
     }
     }
 
 
     /**
     /**
@@ -77,9 +79,9 @@ public final class ExperienceAPI {
      * @return true if this is a valid, non-child mcMMO skill
      * @return true if this is a valid, non-child mcMMO skill
      */
      */
     public static boolean isNonChildSkill(String skillType) {
     public static boolean isNonChildSkill(String skillType) {
-        PrimarySkillType skill = PrimarySkillType.getSkill(skillType);
+        PrimarySkillType skill = mcMMO.p.getSkillTools().matchSkill(skillType);
 
 
-        return skill != null && !skill.isChildSkill();
+        return skill != null && !SkillTools.isChildSkill(skill);
     }
     }
 
 
     @Deprecated
     @Deprecated
@@ -293,11 +295,12 @@ public final class ExperienceAPI {
         PrimarySkillType skill = getSkillType(skillType);
         PrimarySkillType skill = getSkillType(skillType);
 
 
         if (isUnshared) {
         if (isUnshared) {
-            getPlayer(player).beginUnsharedXpGain(skill, (int) (XP / skill.getXpModifier() * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()), getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
+            getPlayer(player).beginUnsharedXpGain(skill,
+                    (int) (XP / ExperienceConfig.getInstance().getFormulaSkillModifier(skill) * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()), getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
             return;
             return;
         }
         }
 
 
-        getPlayer(player).applyXpGain(skill, (int) (XP / skill.getXpModifier() * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()), getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
+        getPlayer(player).applyXpGain(skill, (int) (XP / ExperienceConfig.getInstance().getFormulaSkillModifier(skill) * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()), getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
     }
     }
 
 
     /**
     /**
@@ -316,7 +319,7 @@ public final class ExperienceAPI {
     public static void addModifiedXPOffline(String playerName, String skillType, int XP) {
     public static void addModifiedXPOffline(String playerName, String skillType, int XP) {
         PrimarySkillType skill = getSkillType(skillType);
         PrimarySkillType skill = getSkillType(skillType);
 
 
-        addOfflineXP(playerName, skill, (int) (XP / skill.getXpModifier() * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()));
+        addOfflineXP(playerName, skill, (int) (XP / ExperienceConfig.getInstance().getFormulaSkillModifier(skill) * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()));
     }
     }
 
 
     /**
     /**
@@ -429,6 +432,23 @@ public final class ExperienceAPI {
         return getOfflineProfile(uuid).getSkillXpLevel(getNonChildSkillType(skillType));
         return getOfflineProfile(uuid).getSkillXpLevel(getNonChildSkillType(skillType));
     }
     }
 
 
+    /**
+     * Get the amount of XP an offline player has in a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param offlinePlayer The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the amount of XP in a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static int getOfflineXP(@NotNull OfflinePlayer offlinePlayer, @NotNull String skillType) throws InvalidPlayerException {
+        return getOfflineProfile(offlinePlayer).getSkillXpLevel(getNonChildSkillType(skillType));
+    }
+
     /**
     /**
      * Get the raw amount of XP a player has in a specific skill.
      * Get the raw amount of XP a player has in a specific skill.
      * </br>
      * </br>
@@ -480,6 +500,30 @@ public final class ExperienceAPI {
         return getOfflineProfile(uuid).getSkillXpLevelRaw(getNonChildSkillType(skillType));
         return getOfflineProfile(uuid).getSkillXpLevelRaw(getNonChildSkillType(skillType));
     }
     }
 
 
+    /**
+     * Get the raw amount of XP an offline player has in a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param offlinePlayer The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the amount of XP in a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static float getOfflineXPRaw(@NotNull OfflinePlayer offlinePlayer, @NotNull String skillType) throws InvalidPlayerException, UnsupportedOperationException, InvalidSkillException {
+        return getOfflineProfile(offlinePlayer).getSkillXpLevelRaw(getNonChildSkillType(skillType));
+    }
+
+    public static float getOfflineXPRaw(@NotNull OfflinePlayer offlinePlayer, @NotNull PrimarySkillType skillType) throws InvalidPlayerException, UnsupportedOperationException {
+        if(SkillTools.isChildSkill(skillType))
+            throw new UnsupportedOperationException();
+
+        return getOfflineProfile(offlinePlayer).getSkillXpLevelRaw(skillType);
+    }
+
     /**
     /**
      * Get the total amount of XP needed to reach the next level.
      * Get the total amount of XP needed to reach the next level.
      * </br>
      * </br>
@@ -527,10 +571,27 @@ public final class ExperienceAPI {
      * @throws InvalidPlayerException if the given player does not exist in the database
      * @throws InvalidPlayerException if the given player does not exist in the database
      * @throws UnsupportedOperationException if the given skill is a child skill
      * @throws UnsupportedOperationException if the given skill is a child skill
      */
      */
-    public static int getOfflineXPToNextLevel(UUID uuid, String skillType) {
+    public static int getOfflineXPToNextLevel(@NotNull UUID uuid, @NotNull String skillType) {
         return getOfflineProfile(uuid).getXpToLevel(getNonChildSkillType(skillType));
         return getOfflineProfile(uuid).getXpToLevel(getNonChildSkillType(skillType));
     }
     }
 
 
+    /**
+     * Get the total amount of XP an offline player needs to reach the next level.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param offlinePlayer The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the total amount of XP needed to reach the next level
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static int getOfflineXPToNextLevel(@NotNull OfflinePlayer offlinePlayer, @NotNull String skillType) throws UnsupportedOperationException, InvalidSkillException, InvalidPlayerException {
+        return getOfflineProfile(offlinePlayer).getXpToLevel(getNonChildSkillType(skillType));
+    }
+
     /**
     /**
      * Get the amount of XP remaining until the next level.
      * Get the amount of XP remaining until the next level.
      * </br>
      * </br>
@@ -592,6 +653,26 @@ public final class ExperienceAPI {
         return profile.getXpToLevel(skill) - profile.getSkillXpLevelRaw(skill);
         return profile.getXpToLevel(skill) - profile.getSkillXpLevelRaw(skill);
     }
     }
 
 
+    /**
+     * Get the amount of XP an offline player has left before leveling up.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param offlinePlayer The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the amount of XP needed to reach the next level
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static float getOfflineXPRemaining(OfflinePlayer offlinePlayer, String skillType) throws InvalidSkillException, InvalidPlayerException, UnsupportedOperationException {
+        PrimarySkillType skill = getNonChildSkillType(skillType);
+        PlayerProfile profile = getOfflineProfile(offlinePlayer);
+
+        return profile.getXpToLevel(skill) - profile.getSkillXpLevelRaw(skill);
+    }
+
     /**
     /**
      * Add levels to a skill.
      * Add levels to a skill.
      * </br>
      * </br>
@@ -624,7 +705,7 @@ public final class ExperienceAPI {
         PlayerProfile profile = getOfflineProfile(playerName);
         PlayerProfile profile = getOfflineProfile(playerName);
         PrimarySkillType skill = getSkillType(skillType);
         PrimarySkillType skill = getSkillType(skillType);
 
 
-        if (skill.isChildSkill()) {
+        if (SkillTools.isChildSkill(skill)) {
             Set<PrimarySkillType> parentSkills = FamilyTree.getParents(skill);
             Set<PrimarySkillType> parentSkills = FamilyTree.getParents(skill);
 
 
             for (PrimarySkillType parentSkill : parentSkills) {
             for (PrimarySkillType parentSkill : parentSkills) {
@@ -655,7 +736,7 @@ public final class ExperienceAPI {
         PlayerProfile profile = getOfflineProfile(uuid);
         PlayerProfile profile = getOfflineProfile(uuid);
         PrimarySkillType skill = getSkillType(skillType);
         PrimarySkillType skill = getSkillType(skillType);
 
 
-        if (skill.isChildSkill()) {
+        if (SkillTools.isChildSkill(skill)) {
             Set<PrimarySkillType> parentSkills = FamilyTree.getParents(skill);
             Set<PrimarySkillType> parentSkills = FamilyTree.getParents(skill);
 
 
             for (PrimarySkillType parentSkill : parentSkills) {
             for (PrimarySkillType parentSkill : parentSkills) {
@@ -714,7 +795,6 @@ public final class ExperienceAPI {
      * @throws InvalidSkillException if the given skill is not valid
      * @throws InvalidSkillException if the given skill is not valid
      * @throws InvalidPlayerException if the given player does not exist in the database
      * @throws InvalidPlayerException if the given player does not exist in the database
      */
      */
-    @Deprecated
     public static int getLevelOffline(String playerName, String skillType) {
     public static int getLevelOffline(String playerName, String skillType) {
         return getOfflineProfile(playerName).getSkillLevel(getSkillType(skillType));
         return getOfflineProfile(playerName).getSkillLevel(getSkillType(skillType));
     }
     }
@@ -762,7 +842,7 @@ public final class ExperienceAPI {
         int powerLevel = 0;
         int powerLevel = 0;
         PlayerProfile profile = getOfflineProfile(playerName);
         PlayerProfile profile = getOfflineProfile(playerName);
 
 
-        for (PrimarySkillType type : PrimarySkillType.NON_CHILD_SKILLS) {
+        for (PrimarySkillType type : SkillTools.NON_CHILD_SKILLS) {
             powerLevel += profile.getSkillLevel(type);
             powerLevel += profile.getSkillLevel(type);
         }
         }
 
 
@@ -783,7 +863,7 @@ public final class ExperienceAPI {
         int powerLevel = 0;
         int powerLevel = 0;
         PlayerProfile profile = getOfflineProfile(uuid);
         PlayerProfile profile = getOfflineProfile(uuid);
 
 
-        for (PrimarySkillType type : PrimarySkillType.NON_CHILD_SKILLS) {
+        for (PrimarySkillType type : SkillTools.NON_CHILD_SKILLS) {
             powerLevel += profile.getSkillLevel(type);
             powerLevel += profile.getSkillLevel(type);
         }
         }
 
 
@@ -801,7 +881,7 @@ public final class ExperienceAPI {
      * @throws InvalidSkillException if the given skill is not valid
      * @throws InvalidSkillException if the given skill is not valid
      */
      */
     public static int getLevelCap(String skillType) {
     public static int getLevelCap(String skillType) {
-        return Config.getInstance().getLevelCap(getSkillType(skillType));
+        return mcMMO.p.getSkillTools().getLevelCap(getSkillType(skillType));
     }
     }
 
 
     /**
     /**
@@ -812,7 +892,7 @@ public final class ExperienceAPI {
      * @return the overall power level cap
      * @return the overall power level cap
      */
      */
     public static int getPowerLevelCap() {
     public static int getPowerLevelCap() {
-        return Config.getInstance().getPowerLevelCap();
+        return mcMMO.p.getGeneralConfig().getPowerLevelCap();
     }
     }
 
 
     /**
     /**
@@ -1126,25 +1206,22 @@ public final class ExperienceAPI {
         }
         }
     }
     }
 
 
-
-
     // Utility methods follow.
     // Utility methods follow.
-    private static void addOfflineXP(UUID playerUniqueId, PrimarySkillType skill, int XP) {
+    private static void addOfflineXP(@NotNull UUID playerUniqueId, @NotNull PrimarySkillType skill, int XP) {
         PlayerProfile profile = getOfflineProfile(playerUniqueId);
         PlayerProfile profile = getOfflineProfile(playerUniqueId);
 
 
         profile.addXp(skill, XP);
         profile.addXp(skill, XP);
         profile.save(true);
         profile.save(true);
     }
     }
 
 
-    @Deprecated
-    private static void addOfflineXP(String playerName, PrimarySkillType skill, int XP) {
+    private static void addOfflineXP(@NotNull String playerName, @NotNull PrimarySkillType skill, int XP) {
         PlayerProfile profile = getOfflineProfile(playerName);
         PlayerProfile profile = getOfflineProfile(playerName);
 
 
         profile.addXp(skill, XP);
         profile.addXp(skill, XP);
         profile.scheduleAsyncSave();
         profile.scheduleAsyncSave();
     }
     }
 
 
-    private static PlayerProfile getOfflineProfile(UUID uuid) {
+    private static @NotNull PlayerProfile getOfflineProfile(@NotNull UUID uuid) throws InvalidPlayerException {
         PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(uuid);
         PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(uuid);
 
 
         if (!profile.isLoaded()) {
         if (!profile.isLoaded()) {
@@ -1154,10 +1231,18 @@ public final class ExperienceAPI {
         return profile;
         return profile;
     }
     }
 
 
-    @Deprecated
-    private static PlayerProfile getOfflineProfile(String playerName) {
-        UUID uuid = mcMMO.p.getServer().getOfflinePlayer(playerName).getUniqueId();
-        PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(uuid);
+    private static @NotNull PlayerProfile getOfflineProfile(@NotNull OfflinePlayer offlinePlayer) throws InvalidPlayerException {
+        PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(offlinePlayer);
+
+        if (!profile.isLoaded()) {
+            throw new InvalidPlayerException();
+        }
+
+        return profile;
+    }
+
+    private static @NotNull PlayerProfile getOfflineProfile(@NotNull String playerName) throws InvalidPlayerException {
+        PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName);
 
 
         if (!profile.isLoaded()) {
         if (!profile.isLoaded()) {
             throw new InvalidPlayerException();
             throw new InvalidPlayerException();
@@ -1167,7 +1252,7 @@ public final class ExperienceAPI {
     }
     }
 
 
     private static PrimarySkillType getSkillType(String skillType) throws InvalidSkillException {
     private static PrimarySkillType getSkillType(String skillType) throws InvalidSkillException {
-        PrimarySkillType skill = PrimarySkillType.getSkill(skillType);
+        PrimarySkillType skill = mcMMO.p.getSkillTools().matchSkill(skillType);
 
 
         if (skill == null) {
         if (skill == null) {
             throw new InvalidSkillException();
             throw new InvalidSkillException();
@@ -1179,7 +1264,7 @@ public final class ExperienceAPI {
     private static PrimarySkillType getNonChildSkillType(String skillType) throws InvalidSkillException, UnsupportedOperationException {
     private static PrimarySkillType getNonChildSkillType(String skillType) throws InvalidSkillException, UnsupportedOperationException {
         PrimarySkillType skill = getSkillType(skillType);
         PrimarySkillType skill = getSkillType(skillType);
 
 
-        if (skill.isChildSkill()) {
+        if (SkillTools.isChildSkill(skill)) {
             throw new UnsupportedOperationException("Child skills do not have XP");
             throw new UnsupportedOperationException("Child skills do not have XP");
         }
         }
 
 

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

@@ -1,6 +1,5 @@
 package com.gmail.nossr50.api;
 package com.gmail.nossr50.api;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.interactions.NotificationType;
 import com.gmail.nossr50.datatypes.interactions.NotificationType;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.PartyLeader;
 import com.gmail.nossr50.datatypes.party.PartyLeader;
@@ -108,7 +107,7 @@ public final class PartyAPI {
      */
      */
     public static int getMaxPartySize()
     public static int getMaxPartySize()
     {
     {
-        return Config.getInstance().getPartyMaxSize();
+        return mcMMO.p.getGeneralConfig().getPartyMaxSize();
     }
     }
 
 
     /**
     /**

+ 7 - 5
src/main/java/com/gmail/nossr50/api/SkillAPI.java

@@ -1,6 +1,8 @@
 package com.gmail.nossr50.api;
 package com.gmail.nossr50.api;
 
 
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+import com.gmail.nossr50.mcMMO;
+import com.gmail.nossr50.util.skills.SkillTools;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Arrays;
@@ -30,7 +32,7 @@ public final class SkillAPI {
      * @return a list of strings with valid skill names
      * @return a list of strings with valid skill names
      */
      */
     public static List<String> getNonChildSkills() {
     public static List<String> getNonChildSkills() {
-        return getListFromEnum(PrimarySkillType.NON_CHILD_SKILLS);
+        return getListFromEnum(SkillTools.NON_CHILD_SKILLS);
     }
     }
 
 
     /**
     /**
@@ -42,7 +44,7 @@ public final class SkillAPI {
      * @return a list of strings with valid skill names
      * @return a list of strings with valid skill names
      */
      */
     public static List<String> getChildSkills() {
     public static List<String> getChildSkills() {
-        return getListFromEnum(PrimarySkillType.CHILD_SKILLS);
+        return getListFromEnum(mcMMO.p.getSkillTools().CHILD_SKILLS);
     }
     }
 
 
     /**
     /**
@@ -54,7 +56,7 @@ public final class SkillAPI {
      * @return a list of strings with valid skill names
      * @return a list of strings with valid skill names
      */
      */
     public static List<String> getCombatSkills() {
     public static List<String> getCombatSkills() {
-        return getListFromEnum(PrimarySkillType.COMBAT_SKILLS);
+        return getListFromEnum(mcMMO.p.getSkillTools().COMBAT_SKILLS);
     }
     }
 
 
     /**
     /**
@@ -66,7 +68,7 @@ public final class SkillAPI {
      * @return a list of strings with valid skill names
      * @return a list of strings with valid skill names
      */
      */
     public static List<String> getGatheringSkills() {
     public static List<String> getGatheringSkills() {
-        return getListFromEnum(PrimarySkillType.GATHERING_SKILLS);
+        return getListFromEnum(mcMMO.p.getSkillTools().GATHERING_SKILLS);
     }
     }
 
 
     /**
     /**
@@ -78,7 +80,7 @@ public final class SkillAPI {
      * @return a list of strings with valid skill names
      * @return a list of strings with valid skill names
      */
      */
     public static List<String> getMiscSkills() {
     public static List<String> getMiscSkills() {
-        return getListFromEnum(PrimarySkillType.MISC_SKILLS);
+        return getListFromEnum(mcMMO.p.getSkillTools().MISC_SKILLS);
     }
     }
 
 
     private static List<String> getListFromEnum(List<PrimarySkillType> skillsTypes) {
     private static List<String> getListFromEnum(List<PrimarySkillType> skillsTypes) {

+ 1 - 1
src/main/java/com/gmail/nossr50/api/exceptions/McMMOPlayerNotFoundException.java

@@ -7,6 +7,6 @@ public class McMMOPlayerNotFoundException extends RuntimeException {
     private static final long serialVersionUID = 761917904993202836L;
     private static final long serialVersionUID = 761917904993202836L;
 
 
     public McMMOPlayerNotFoundException(@NotNull Player player) {
     public McMMOPlayerNotFoundException(@NotNull Player player) {
-        super("McMMOPlayer object was not found for [NOTE: This can mean the profile is not loaded yet!] : " + player.getName() + " " + player.getUniqueId());
+        super("McMMOPlayer object was not found for [NOTE: This can mean the profile is not loaded yet! : " + player.getName() + " " + player.getUniqueId());
     }
     }
 }
 }

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

@@ -23,8 +23,7 @@ public class SamePartyPredicate<T extends CommandSender> implements Predicate<T>
         if(t instanceof ConsoleCommandSender) {
         if(t instanceof ConsoleCommandSender) {
             return false; //Party audiences are special, we exclude console from them to avoid double messaging since we send a more verbose version to consoles
             return false; //Party audiences are special, we exclude console from them to avoid double messaging since we send a more verbose version to consoles
         } else {
         } else {
-            if(t instanceof Player) {
-                Player player = (Player) t;
+            if(t instanceof Player player) {
                 McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
                 McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
                 if(mcMMOPlayer != null) {
                 if(mcMMOPlayer != null) {
                     return mcMMOPlayer.getParty() == party;
                     return mcMMOPlayer.getParty() == party;

+ 0 - 46
src/main/java/com/gmail/nossr50/commands/MHDCommand.java

@@ -1,46 +0,0 @@
-package com.gmail.nossr50.commands;
-
-import com.gmail.nossr50.config.Config;
-import com.gmail.nossr50.database.FlatfileDatabaseManager;
-import com.gmail.nossr50.database.SQLDatabaseManager;
-import com.gmail.nossr50.datatypes.player.McMMOPlayer;
-import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.player.UserManager;
-import com.google.common.collect.ImmutableList;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandSender;
-import org.bukkit.command.TabExecutor;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.List;
-
-public class MHDCommand implements TabExecutor {
-
-    @Override
-    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
-        if (mcMMO.getDatabaseManager() instanceof SQLDatabaseManager) {
-            SQLDatabaseManager m = (SQLDatabaseManager) mcMMO.getDatabaseManager();
-            m.resetMobHealthSettings();
-            for (McMMOPlayer player : UserManager.getPlayers()) {
-                player.getProfile().setMobHealthbarType(Config.getInstance().getMobHealthbarDefault());
-            }
-            sender.sendMessage("Mob health reset");
-            return true;
-        }
-        if (mcMMO.getDatabaseManager() instanceof FlatfileDatabaseManager) {
-            FlatfileDatabaseManager m = (FlatfileDatabaseManager) mcMMO.getDatabaseManager();
-            m.resetMobHealthSettings();
-            for (McMMOPlayer player : UserManager.getPlayers()) {
-                player.getProfile().setMobHealthbarType(Config.getInstance().getMobHealthbarDefault());
-            }
-            sender.sendMessage("Mob health reset");
-            return true;
-        }
-        return false;
-    }
-
-    @Override
-    public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
-        return ImmutableList.of();
-    }
-}

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

@@ -1,7 +1,6 @@
 package com.gmail.nossr50.commands;
 package com.gmail.nossr50.commands;
 
 
 import com.gmail.nossr50.commands.party.PartySubcommandType;
 import com.gmail.nossr50.commands.party.PartySubcommandType;
-import com.gmail.nossr50.config.Config;
 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;
@@ -26,7 +25,7 @@ public class McmmoCommand implements CommandExecutor {
                 sender.sendMessage(mcSplit);
                 sender.sendMessage(mcSplit);
                 sender.sendMessage(LocaleLoader.getString("mcMMO.Description.FormerDevs"));
                 sender.sendMessage(LocaleLoader.getString("mcMMO.Description.FormerDevs"));
 
 
-                if (Config.getInstance().getDonateMessageEnabled()) {
+                if (mcMMO.p.getGeneralConfig().getDonateMessageEnabled()) {
                     sender.sendMessage(LocaleLoader.getString("MOTD.Donate"));
                     sender.sendMessage(LocaleLoader.getString("MOTD.Donate"));
                     sender.sendMessage(ChatColor.GOLD + " - " + ChatColor.GREEN + "nossr50@gmail.com" + ChatColor.GOLD + " Paypal");
                     sender.sendMessage(ChatColor.GOLD + " - " + ChatColor.GREEN + "nossr50@gmail.com" + ChatColor.GOLD + " Paypal");
                 }
                 }
@@ -35,7 +34,7 @@ public class McmmoCommand implements CommandExecutor {
                     sender.sendMessage(LocaleLoader.getString("MOTD.Version", mcMMO.p.getDescription().getVersion()));
                     sender.sendMessage(LocaleLoader.getString("MOTD.Version", mcMMO.p.getDescription().getVersion()));
                 }
                 }
 
 
-                mcMMO.getHolidayManager().anniversaryCheck(sender);
+//                mcMMO.getHolidayManager().anniversaryCheck(sender);
                 return true;
                 return true;
 
 
             case 1:
             case 1:

+ 12 - 2
src/main/java/com/gmail/nossr50/commands/McscoreboardCommand.java

@@ -1,7 +1,7 @@
 package com.gmail.nossr50.commands;
 package com.gmail.nossr50.commands;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.scoreboards.ScoreboardManager;
 import com.gmail.nossr50.util.scoreboards.ScoreboardManager;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList;
@@ -23,6 +23,16 @@ public class McscoreboardCommand implements TabExecutor {
             return true;
             return true;
         }
         }
 
 
+        if(!mcMMO.p.getGeneralConfig().getScoreboardsEnabled()) {
+            sender.sendMessage(LocaleLoader.getString("Scoreboard.Disabled"));
+            return true;
+        }
+
+        if(!ScoreboardManager.isPlayerBoardSetup(sender.getName())) {
+            sender.sendMessage(LocaleLoader.getString("Scoreboard.NotSetupYet"));
+            return true;
+        }
+
         switch (args.length) {
         switch (args.length) {
             case 1:
             case 1:
                 if (args[0].equalsIgnoreCase("clear") || args[0].equalsIgnoreCase("reset")) {
                 if (args[0].equalsIgnoreCase("clear") || args[0].equalsIgnoreCase("reset")) {
@@ -32,7 +42,7 @@ public class McscoreboardCommand implements TabExecutor {
                 }
                 }
 
 
                 if (args[0].equalsIgnoreCase("keep")) {
                 if (args[0].equalsIgnoreCase("keep")) {
-                    if (!Config.getInstance().getAllowKeepBoard() || !Config.getInstance().getScoreboardsEnabled()) {
+                    if (!mcMMO.p.getGeneralConfig().getAllowKeepBoard() || !mcMMO.p.getGeneralConfig().getScoreboardsEnabled()) {
                         sender.sendMessage(LocaleLoader.getString("Commands.Disabled"));
                         sender.sendMessage(LocaleLoader.getString("Commands.Disabled"));
                         return true;
                         return true;
                     }
                     }

+ 4 - 6
src/main/java/com/gmail/nossr50/commands/XprateCommand.java

@@ -1,7 +1,5 @@
 package com.gmail.nossr50.commands;
 package com.gmail.nossr50.commands;
 
 
-import com.gmail.nossr50.config.AdvancedConfig;
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.config.experience.ExperienceConfig;
 import com.gmail.nossr50.config.experience.ExperienceConfig;
 import com.gmail.nossr50.datatypes.notifications.SensitiveCommandType;
 import com.gmail.nossr50.datatypes.notifications.SensitiveCommandType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
@@ -39,7 +37,7 @@ public class XprateCommand implements TabExecutor {
 
 
                 if (mcMMO.p.isXPEventEnabled()) {
                 if (mcMMO.p.isXPEventEnabled()) {
 
 
-                    if(AdvancedConfig.getInstance().useTitlesForXPEvent())
+                    if(mcMMO.p.getAdvancedConfig().useTitlesForXPEvent())
                     {
                     {
                         NotificationManager.broadcastTitle(mcMMO.p.getServer(),
                         NotificationManager.broadcastTitle(mcMMO.p.getServer(),
                                 LocaleLoader.getString("Commands.Event.Stop"),
                                 LocaleLoader.getString("Commands.Event.Stop"),
@@ -47,7 +45,7 @@ public class XprateCommand implements TabExecutor {
                                 10, 10*20, 20);
                                 10, 10*20, 20);
                     }
                     }
 
 
-                    if(Config.getInstance().broadcastEventMessages())
+                    if(mcMMO.p.getGeneralConfig().broadcastEventMessages())
                     {
                     {
                         mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Commands.Event.Stop"));
                         mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Commands.Event.Stop"));
                         mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Commands.Event.Stop.Subtitle"));
                         mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Commands.Event.Stop.Subtitle"));
@@ -92,7 +90,7 @@ public class XprateCommand implements TabExecutor {
 
 
                 ExperienceConfig.getInstance().setExperienceGainsGlobalMultiplier(newXpRate);
                 ExperienceConfig.getInstance().setExperienceGainsGlobalMultiplier(newXpRate);
 
 
-                if(AdvancedConfig.getInstance().useTitlesForXPEvent())
+                if(mcMMO.p.getAdvancedConfig().useTitlesForXPEvent())
                 {
                 {
                     NotificationManager.broadcastTitle(mcMMO.p.getServer(),
                     NotificationManager.broadcastTitle(mcMMO.p.getServer(),
                             LocaleLoader.getString("Commands.Event.Start"),
                             LocaleLoader.getString("Commands.Event.Start"),
@@ -100,7 +98,7 @@ public class XprateCommand implements TabExecutor {
                             10, 10*20, 20);
                             10, 10*20, 20);
                 }
                 }
 
 
-                if(Config.getInstance().broadcastEventMessages())
+                if(mcMMO.p.getGeneralConfig().broadcastEventMessages())
                 {
                 {
                     mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Commands.Event.Start"));
                     mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Commands.Event.Start"));
                     mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Commands.Event.XP", newXpRate));
                     mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Commands.Event.XP", newXpRate));

+ 6 - 2
src/main/java/com/gmail/nossr50/commands/database/ConvertDatabaseCommand.java

@@ -27,7 +27,11 @@ public class ConvertDatabaseCommand implements CommandExecutor {
                 return true;
                 return true;
             }
             }
 
 
-            DatabaseManager oldDatabase = DatabaseManagerFactory.createDatabaseManager(previousType);
+            DatabaseManager oldDatabase = DatabaseManagerFactory.createDatabaseManager(previousType, mcMMO.getUsersFilePath(), mcMMO.p.getLogger(), mcMMO.p.getPurgeTime(), mcMMO.p.getAdvancedConfig().getStartingLevel());
+            if(oldDatabase == null) {
+                sender.sendMessage("Unable to load the old database! Check your log for errors.");
+                return true;
+            }
 
 
             if (previousType == DatabaseType.CUSTOM) {
             if (previousType == DatabaseType.CUSTOM) {
                 Class<?> clazz;
                 Class<?> clazz;
@@ -54,7 +58,7 @@ public class ConvertDatabaseCommand implements CommandExecutor {
             UserManager.clearAll();
             UserManager.clearAll();
 
 
             for (Player player : mcMMO.p.getServer().getOnlinePlayers()) {
             for (Player player : mcMMO.p.getServer().getOnlinePlayers()) {
-                PlayerProfile profile = oldDatabase.loadPlayerProfile(player.getUniqueId());
+                PlayerProfile profile = oldDatabase.loadPlayerProfile(player);
 
 
                 if (profile.isLoaded()) {
                 if (profile.isLoaded()) {
                     mcMMO.getDatabaseManager().saveUser(profile);
                     mcMMO.getDatabaseManager().saveUser(profile);

+ 1 - 2
src/main/java/com/gmail/nossr50/commands/database/McpurgeCommand.java

@@ -1,6 +1,5 @@
 package com.gmail.nossr50.commands.database;
 package com.gmail.nossr50.commands.database;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList;
@@ -17,7 +16,7 @@ public class McpurgeCommand implements TabExecutor {
         if (args.length == 0) {
         if (args.length == 0) {
             mcMMO.getDatabaseManager().purgePowerlessUsers();
             mcMMO.getDatabaseManager().purgePowerlessUsers();
 
 
-            if (Config.getInstance().getOldUsersCutoff() != -1) {
+            if (mcMMO.p.getGeneralConfig().getOldUsersCutoff() != -1) {
                 mcMMO.getDatabaseManager().purgeOldUsers();
                 mcMMO.getDatabaseManager().purgeOldUsers();
             }
             }
 
 

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/database/McremoveCommand.java

@@ -22,7 +22,7 @@ public class McremoveCommand implements TabExecutor {
         if (args.length == 1) {
         if (args.length == 1) {
             String playerName = CommandUtils.getMatchedPlayerName(args[0]);
             String playerName = CommandUtils.getMatchedPlayerName(args[0]);
 
 
-            if (UserManager.getOfflinePlayer(playerName) == null && CommandUtils.unloadedProfile(sender, mcMMO.getDatabaseManager().loadPlayerProfile(playerName, false))) {
+            if (UserManager.getOfflinePlayer(playerName) == null && CommandUtils.unloadedProfile(sender, mcMMO.getDatabaseManager().loadPlayerProfile(playerName))) {
                 return true;
                 return true;
             }
             }
 
 

+ 2 - 2
src/main/java/com/gmail/nossr50/commands/database/MmoshowdbCommand.java

@@ -1,8 +1,8 @@
 package com.gmail.nossr50.commands.database;
 package com.gmail.nossr50.commands.database;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.database.DatabaseManagerFactory;
 import com.gmail.nossr50.database.DatabaseManagerFactory;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandSender;
 import org.bukkit.command.CommandSender;
@@ -22,7 +22,7 @@ public class MmoshowdbCommand implements TabExecutor {
                 return true;
                 return true;
             }
             }
 
 
-            sender.sendMessage(LocaleLoader.getString("Commands.mmoshowdb", (Config.getInstance().getUseMySQL() ? "sql" : "flatfile")));
+            sender.sendMessage(LocaleLoader.getString("Commands.mmoshowdb", (mcMMO.p.getGeneralConfig().getUseMySQL() ? "sql" : "flatfile")));
             return true;
             return true;
         }
         }
         return false;
         return false;

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

@@ -5,6 +5,7 @@ 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.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.EventUtils;
 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;
@@ -55,6 +56,6 @@ public class AddlevelsCommand extends ExperienceCommand {
         if(isSilent)
         if(isSilent)
             return;
             return;
 
 
-        player.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.1", value, skill.getName()));
+        player.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.1", value, mcMMO.p.getSkillTools().getLocalizedSkillName(skill)));
     }
     }
 }
 }

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

@@ -5,6 +5,7 @@ import com.gmail.nossr50.datatypes.experience.XPGainSource;
 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.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+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 org.bukkit.command.CommandSender;
 import org.bukkit.command.CommandSender;
@@ -49,6 +50,6 @@ public class AddxpCommand extends ExperienceCommand {
         if(isSilent)
         if(isSilent)
             return;
             return;
 
 
-        player.sendMessage(LocaleLoader.getString("Commands.addxp.AwardSkill", value, skill.getName()));
+        player.sendMessage(LocaleLoader.getString("Commands.addxp.AwardSkill", value, mcMMO.p.getSkillTools().getLocalizedSkillName(skill)));
     }
     }
 }
 }

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

@@ -7,8 +7,8 @@ import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 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.gmail.nossr50.util.skills.SkillTools;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList;
-import org.bukkit.OfflinePlayer;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandSender;
 import org.bukkit.command.CommandSender;
 import org.bukkit.command.TabExecutor;
 import org.bukkit.command.TabExecutor;
@@ -18,7 +18,6 @@ import org.jetbrains.annotations.NotNull;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
-import java.util.UUID;
 
 
 public abstract class ExperienceCommand implements TabExecutor {
 public abstract class ExperienceCommand implements TabExecutor {
     @Override
     @Override
@@ -44,13 +43,13 @@ public abstract class ExperienceCommand implements TabExecutor {
                     return true;
                     return true;
                 }
                 }
 
 
-                skill = PrimarySkillType.getSkill(args[0]);
+                skill = mcMMO.p.getSkillTools().matchSkill(args[0]);
 
 
                 if (args[1].equalsIgnoreCase("all")) {
                 if (args[1].equalsIgnoreCase("all")) {
                     skill = null;
                     skill = null;
                 }
                 }
 
 
-                if (skill != null && skill.isChildSkill())
+                if (skill != null && SkillTools.isChildSkill(skill))
                 {
                 {
                     sender.sendMessage(LocaleLoader.getString("Commands.Skill.ChildSkill"));
                     sender.sendMessage(LocaleLoader.getString("Commands.Skill.ChildSkill"));
                     return true;
                     return true;
@@ -77,13 +76,13 @@ public abstract class ExperienceCommand implements TabExecutor {
                     return true;
                     return true;
                 }
                 }
 
 
-                skill = PrimarySkillType.getSkill(args[1]);
+                skill = mcMMO.p.getSkillTools().matchSkill(args[1]);
 
 
                 if (args[1].equalsIgnoreCase("all")) {
                 if (args[1].equalsIgnoreCase("all")) {
                     skill = null;
                     skill = null;
                 }
                 }
 
 
-                if (skill != null && skill.isChildSkill())
+                if (skill != null && SkillTools.isChildSkill(skill))
                 {
                 {
                     sender.sendMessage(LocaleLoader.getString("Commands.Skill.ChildSkill"));
                     sender.sendMessage(LocaleLoader.getString("Commands.Skill.ChildSkill"));
                     return true;
                     return true;
@@ -96,15 +95,18 @@ public abstract class ExperienceCommand implements TabExecutor {
 
 
                 // If the mcMMOPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
                 // If the mcMMOPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
                 if (mcMMOPlayer == null) {
                 if (mcMMOPlayer == null) {
-                    UUID uuid = null;
-                    OfflinePlayer player = mcMMO.p.getServer().getOfflinePlayer(playerName);
-                    if (player != null) {
-                        uuid = player.getUniqueId();
-                    }
-                    PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName, uuid, false);
+                    PlayerProfile profile;
+
+                    profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName);
 
 
+                    //Check loading by UUID
                     if (CommandUtils.unloadedProfile(sender, profile)) {
                     if (CommandUtils.unloadedProfile(sender, profile)) {
-                        return true;
+                        //Check loading by name
+                        profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName);
+
+                        if(CommandUtils.unloadedProfile(sender, profile)) {
+                            return true;
+                        }
                     }
                     }
 
 
                     editValues(null, profile, skill, value, isSilent(args));
                     editValues(null, profile, skill, value, isSilent(args));
@@ -138,7 +140,7 @@ public abstract class ExperienceCommand implements TabExecutor {
                 List<String> playerNames = CommandUtils.getOnlinePlayerNames(sender);
                 List<String> playerNames = CommandUtils.getOnlinePlayerNames(sender);
                 return StringUtil.copyPartialMatches(args[0], playerNames, new ArrayList<>(playerNames.size()));
                 return StringUtil.copyPartialMatches(args[0], playerNames, new ArrayList<>(playerNames.size()));
             case 2:
             case 2:
-                return StringUtil.copyPartialMatches(args[1], PrimarySkillType.SKILL_NAMES, new ArrayList<>(PrimarySkillType.SKILL_NAMES.size()));
+                return StringUtil.copyPartialMatches(args[1], mcMMO.p.getSkillTools().LOCALIZED_SKILL_NAMES, new ArrayList<>(mcMMO.p.getSkillTools().LOCALIZED_SKILL_NAMES.size()));
             default:
             default:
                 return ImmutableList.of();
                 return ImmutableList.of();
         }
         }
@@ -159,13 +161,13 @@ public abstract class ExperienceCommand implements TabExecutor {
             sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardAll.2", playerName));
             sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardAll.2", playerName));
         }
         }
         else {
         else {
-            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", skill.getName(), playerName));
+            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", mcMMO.p.getSkillTools().getLocalizedSkillName(skill), playerName));
         }
         }
     }
     }
 
 
     protected void editValues(Player player, PlayerProfile profile, PrimarySkillType skill, int value, boolean isSilent) {
     protected void editValues(Player player, PlayerProfile profile, PrimarySkillType skill, int value, boolean isSilent) {
         if (skill == null) {
         if (skill == null) {
-            for (PrimarySkillType primarySkillType : PrimarySkillType.NON_CHILD_SKILLS) {
+            for (PrimarySkillType primarySkillType : SkillTools.NON_CHILD_SKILLS) {
                 handleCommand(player, profile, primarySkillType, value);
                 handleCommand(player, profile, primarySkillType, value);
             }
             }
 
 

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

@@ -5,6 +5,7 @@ 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.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.EventUtils;
 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;
@@ -61,6 +62,6 @@ public class MmoeditCommand extends ExperienceCommand {
         if(isSilent)
         if(isSilent)
             return;
             return;
 
 
-        player.sendMessage(LocaleLoader.getString("Commands.mmoedit.Modified.1", skill.getName(), value));
+        player.sendMessage(LocaleLoader.getString("Commands.mmoedit.Modified.1", mcMMO.p.getSkillTools().getLocalizedSkillName(skill), value));
     }
     }
 }
 }

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

@@ -10,6 +10,7 @@ import com.gmail.nossr50.util.EventUtils;
 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.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
+import com.gmail.nossr50.util.skills.SkillTools;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList;
 import org.bukkit.OfflinePlayer;
 import org.bukkit.OfflinePlayer;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;
@@ -21,7 +22,6 @@ import org.jetbrains.annotations.NotNull;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
-import java.util.UUID;
 
 
 /**
 /**
  * This class mirrors the structure of ExperienceCommand, except the
  * This class mirrors the structure of ExperienceCommand, except the
@@ -50,7 +50,7 @@ public class SkillresetCommand implements TabExecutor {
                     skill = null;
                     skill = null;
                 }
                 }
                 else {
                 else {
-                    skill = PrimarySkillType.getSkill(args[0]);
+                    skill = mcMMO.p.getSkillTools().matchSkill(args[0]);
                 }
                 }
 
 
                 editValues((Player) sender, UserManager.getPlayer(sender.getName()).getProfile(), skill);
                 editValues((Player) sender, UserManager.getPlayer(sender.getName()).getProfile(), skill);
@@ -70,7 +70,7 @@ public class SkillresetCommand implements TabExecutor {
                     skill = null;
                     skill = null;
                 }
                 }
                 else {
                 else {
-                    skill = PrimarySkillType.getSkill(args[1]);
+                    skill = mcMMO.p.getSkillTools().matchSkill(args[1]);
                 }
                 }
 
 
                 String playerName = CommandUtils.getMatchedPlayerName(args[0]);
                 String playerName = CommandUtils.getMatchedPlayerName(args[0]);
@@ -78,15 +78,18 @@ public class SkillresetCommand implements TabExecutor {
 
 
                 // If the mcMMOPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
                 // If the mcMMOPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
                 if (mcMMOPlayer == null) {
                 if (mcMMOPlayer == null) {
-                    UUID uuid = null;
-                    OfflinePlayer player = mcMMO.p.getServer().getOfflinePlayer(playerName);
-                    if (player != null) {
-                        uuid = player.getUniqueId();
-                    }
-                    PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName, uuid, false);
+                    OfflinePlayer offlinePlayer = mcMMO.p.getServer().getOfflinePlayer(playerName);
+                    PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(offlinePlayer);
 
 
+                    //Check loading by UUID
                     if (CommandUtils.unloadedProfile(sender, profile)) {
                     if (CommandUtils.unloadedProfile(sender, profile)) {
-                        return true;
+                        //Didn't find it by UUID so try to find it by name
+                        profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName);
+
+                        //Check if it was present in DB
+                        if(CommandUtils.unloadedProfile(sender, profile)) {
+                            return true;
+                        }
                     }
                     }
 
 
                     editValues(null, profile, skill);
                     editValues(null, profile, skill);
@@ -110,7 +113,7 @@ public class SkillresetCommand implements TabExecutor {
                 List<String> playerNames = CommandUtils.getOnlinePlayerNames(sender);
                 List<String> playerNames = CommandUtils.getOnlinePlayerNames(sender);
                 return StringUtil.copyPartialMatches(args[0], playerNames, new ArrayList<>(playerNames.size()));
                 return StringUtil.copyPartialMatches(args[0], playerNames, new ArrayList<>(playerNames.size()));
             case 2:
             case 2:
-                return StringUtil.copyPartialMatches(args[1], PrimarySkillType.SKILL_NAMES, new ArrayList<>(PrimarySkillType.SKILL_NAMES.size()));
+                return StringUtil.copyPartialMatches(args[1], mcMMO.p.getSkillTools().LOCALIZED_SKILL_NAMES, new ArrayList<>(mcMMO.p.getSkillTools().LOCALIZED_SKILL_NAMES.size()));
             default:
             default:
                 return ImmutableList.of();
                 return ImmutableList.of();
         }
         }
@@ -143,7 +146,7 @@ public class SkillresetCommand implements TabExecutor {
     }
     }
 
 
     protected void handlePlayerMessageSkill(Player player, PrimarySkillType skill) {
     protected void handlePlayerMessageSkill(Player player, PrimarySkillType skill) {
-        player.sendMessage(LocaleLoader.getString("Commands.Reset.Single", skill.getName()));
+        player.sendMessage(LocaleLoader.getString("Commands.Reset.Single", mcMMO.p.getSkillTools().getLocalizedSkillName(skill)));
     }
     }
 
 
     private boolean validateArguments(CommandSender sender, String skillName) {
     private boolean validateArguments(CommandSender sender, String skillName) {
@@ -155,13 +158,13 @@ public class SkillresetCommand implements TabExecutor {
             sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardAll.2", playerName));
             sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardAll.2", playerName));
         }
         }
         else {
         else {
-            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", skill.getName(), playerName));
+            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", mcMMO.p.getSkillTools().getLocalizedSkillName(skill), playerName));
         }
         }
     }
     }
 
 
     protected void editValues(Player player, PlayerProfile profile, PrimarySkillType skill) {
     protected void editValues(Player player, PlayerProfile profile, PrimarySkillType skill) {
         if (skill == null) {
         if (skill == null) {
-            for (PrimarySkillType primarySkillType : PrimarySkillType.NON_CHILD_SKILLS) {
+            for (PrimarySkillType primarySkillType : SkillTools.NON_CHILD_SKILLS) {
                 handleCommand(player, profile, primarySkillType);
                 handleCommand(player, profile, primarySkillType);
             }
             }
 
 

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

@@ -1,64 +1,64 @@
-package com.gmail.nossr50.commands.hardcore;
-
-import com.gmail.nossr50.config.Config;
-import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
-import com.gmail.nossr50.locale.LocaleLoader;
-import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.Permissions;
-import org.bukkit.command.CommandSender;
-
-public class HardcoreCommand extends HardcoreModeCommand {
-    @Override
-    protected boolean checkTogglePermissions(CommandSender sender) {
-        return Permissions.hardcoreToggle(sender);
-    }
-
-    @Override
-    protected boolean checkModifyPermissions(CommandSender sender) {
-        return Permissions.hardcoreModify(sender);
-    }
-
-    @Override
-    protected boolean checkEnabled(PrimarySkillType skill) {
-        if (skill == null) {
-            for (PrimarySkillType primarySkillType : PrimarySkillType.values()) {
-                if (!primarySkillType.getHardcoreStatLossEnabled()) {
-                    return false;
-                }
-            }
-
-            return true;
-        }
-
-        return skill.getHardcoreStatLossEnabled();
-    }
-
-    @Override
-    protected void enable(PrimarySkillType skill) {
-        toggle(true, skill);
-    }
-
-    @Override
-    protected void disable(PrimarySkillType skill) {
-        toggle(false, skill);
-    }
-
-    @Override
-    protected void modify(CommandSender sender, double newPercentage) {
-        Config.getInstance().setHardcoreDeathStatPenaltyPercentage(newPercentage);
-        sender.sendMessage(LocaleLoader.getString("Hardcore.DeathStatLoss.PercentageChanged", percent.format(newPercentage / 100.0D)));
-    }
-
-    private void toggle(boolean enable, PrimarySkillType skill) {
-        if (skill == null) {
-            for (PrimarySkillType primarySkillType : PrimarySkillType.NON_CHILD_SKILLS) {
-                primarySkillType.setHardcoreStatLossEnabled(enable);
-            }
-        }
-        else {
-            skill.setHardcoreStatLossEnabled(enable);
-        }
-
-        mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Hardcore.Mode." + (enable ? "Enabled" : "Disabled"), LocaleLoader.getString("Hardcore.DeathStatLoss.Name"), (skill == null ? "all skills" : skill.getName())));
-    }
-}
+//package com.gmail.nossr50.commands.hardcore;
+//
+//import com.gmail.nossr50.config.Config;
+//import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+//import com.gmail.nossr50.locale.LocaleLoader;
+//import com.gmail.nossr50.mcMMO;
+//import com.gmail.nossr50.util.Permissions;
+//import org.bukkit.command.CommandSender;
+//
+//public class HardcoreCommand extends HardcoreModeCommand {
+//    @Override
+//    protected boolean checkTogglePermissions(CommandSender sender) {
+//        return Permissions.hardcoreToggle(sender);
+//    }
+//
+//    @Override
+//    protected boolean checkModifyPermissions(CommandSender sender) {
+//        return Permissions.hardcoreModify(sender);
+//    }
+//
+//    @Override
+//    protected boolean checkEnabled(PrimarySkillType skill) {
+//        if (skill == null) {
+//            for (PrimarySkillType primarySkillType : PrimarySkillType.values()) {
+//                if (!primarySkillType.getHardcoreStatLossEnabled()) {
+//                    return false;
+//                }
+//            }
+//
+//            return true;
+//        }
+//
+//        return skill.getHardcoreStatLossEnabled();
+//    }
+//
+//    @Override
+//    protected void enable(PrimarySkillType skill) {
+//        toggle(true, skill);
+//    }
+//
+//    @Override
+//    protected void disable(PrimarySkillType skill) {
+//        toggle(false, skill);
+//    }
+//
+//    @Override
+//    protected void modify(CommandSender sender, double newPercentage) {
+//        Config.getInstance().setHardcoreDeathStatPenaltyPercentage(newPercentage);
+//        sender.sendMessage(LocaleLoader.getString("Hardcore.DeathStatLoss.PercentageChanged", percent.format(newPercentage / 100.0D)));
+//    }
+//
+//    private void toggle(boolean enable, PrimarySkillType skill) {
+//        if (skill == null) {
+//            for (PrimarySkillType primarySkillType : SkillTools.NON_CHILD_SKILLS) {
+//                primarySkillType.setHardcoreStatLossEnabled(enable);
+//            }
+//        }
+//        else {
+//            skill.setHardcoreStatLossEnabled(enable);
+//        }
+//
+//        mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Hardcore.Mode." + (enable ? "Enabled" : "Disabled"), LocaleLoader.getString("Hardcore.DeathStatLoss.Name"), (skill == null ? "all skills" : mcMMO.p.getSkillTools().getLocalizedSkillName(skill))));
+//    }
+//}

+ 129 - 129
src/main/java/com/gmail/nossr50/commands/hardcore/HardcoreModeCommand.java

@@ -1,129 +1,129 @@
-package com.gmail.nossr50.commands.hardcore;
-
-import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
-import com.gmail.nossr50.util.Permissions;
-import com.gmail.nossr50.util.commands.CommandUtils;
-import com.gmail.nossr50.util.text.StringUtils;
-import com.google.common.collect.ImmutableList;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandSender;
-import org.bukkit.command.TabExecutor;
-import org.bukkit.util.StringUtil;
-import org.jetbrains.annotations.NotNull;
-
-import java.text.DecimalFormat;
-import java.util.ArrayList;
-import java.util.List;
-
-public abstract class HardcoreModeCommand implements TabExecutor {
-    protected final DecimalFormat percent = new DecimalFormat("##0.00%");
-
-    @Override
-    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
-        switch (args.length) {
-            case 0:
-                if (!checkTogglePermissions(sender)) {
-                    sender.sendMessage(command.getPermissionMessage());
-                    return true;
-                }
-
-                if (checkEnabled(null)) {
-                    disable(null);
-                }
-                else {
-                    enable(null);
-                }
-
-                return true;
-
-            case 1:
-                if (CommandUtils.shouldEnableToggle(args[0])) {
-                    if (!Permissions.hardcoreToggle(sender)) {
-                        sender.sendMessage(command.getPermissionMessage());
-                        return true;
-                    }
-
-                    enable(null);
-                    return true;
-                }
-
-                if (CommandUtils.shouldDisableToggle(args[0])) {
-                    if (!Permissions.hardcoreToggle(sender)) {
-                        sender.sendMessage(command.getPermissionMessage());
-                        return true;
-                    }
-
-                    disable(null);
-                    return true;
-                }
-
-                if (CommandUtils.isInvalidDouble(sender, args[0])) {
-                    return true;
-                }
-
-                if (!Permissions.hardcoreModify(sender)) {
-                    sender.sendMessage(command.getPermissionMessage());
-                    return true;
-                }
-
-                modify(sender, Double.parseDouble(args[0]));
-                return true;
-
-
-            case 2:
-                if (CommandUtils.isInvalidSkill(sender, args[0])) {
-                    return true;
-                }
-
-                PrimarySkillType skill = PrimarySkillType.getSkill(args[0]);
-
-                if (!CommandUtils.isChildSkill(sender, skill)) {
-                    return true;
-                }
-
-                if (CommandUtils.shouldEnableToggle(args[1])) {
-                    if (!Permissions.hardcoreToggle(sender)) {
-                        sender.sendMessage(command.getPermissionMessage());
-                        return true;
-                    }
-
-                    enable(skill);
-                    return true;
-                }
-
-                if (CommandUtils.shouldDisableToggle(args[1])) {
-                    if (!Permissions.hardcoreToggle(sender)) {
-                        sender.sendMessage(command.getPermissionMessage());
-                        return true;
-                    }
-
-                    enable(skill);
-                    return true;
-                }
-
-                return true;
-
-            default:
-                return false;
-        }
-    }
-
-    @Override
-    public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
-        if (args.length == 1) {
-            if (StringUtils.isDouble(args[0])) {
-                return ImmutableList.of();
-            }
-
-            return StringUtil.copyPartialMatches(args[0], CommandUtils.TRUE_FALSE_OPTIONS, new ArrayList<>(CommandUtils.TRUE_FALSE_OPTIONS.size()));
-        }
-        return ImmutableList.of();
-    }
-
-    protected abstract boolean checkTogglePermissions(CommandSender sender);
-    protected abstract boolean checkModifyPermissions(CommandSender sender);
-    protected abstract boolean checkEnabled(PrimarySkillType skill);
-    protected abstract void enable(PrimarySkillType skill);
-    protected abstract void disable(PrimarySkillType skill);
-    protected abstract void modify(CommandSender sender, double newPercentage);
-}
+//package com.gmail.nossr50.commands.hardcore;
+//
+//import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+//import com.gmail.nossr50.util.Permissions;
+//import com.gmail.nossr50.util.commands.CommandUtils;
+//import com.gmail.nossr50.util.text.StringUtils;
+//import com.google.common.collect.ImmutableList;
+//import org.bukkit.command.Command;
+//import org.bukkit.command.CommandSender;
+//import org.bukkit.command.TabExecutor;
+//import org.bukkit.util.StringUtil;
+//import org.jetbrains.annotations.NotNull;
+//
+//import java.text.DecimalFormat;
+//import java.util.ArrayList;
+//import java.util.List;
+//
+//public abstract class HardcoreModeCommand implements TabExecutor {
+//    protected final DecimalFormat percent = new DecimalFormat("##0.00%");
+//
+//    @Override
+//    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
+//        switch (args.length) {
+//            case 0:
+//                if (!checkTogglePermissions(sender)) {
+//                    sender.sendMessage(command.getPermissionMessage());
+//                    return true;
+//                }
+//
+//                if (checkEnabled(null)) {
+//                    disable(null);
+//                }
+//                else {
+//                    enable(null);
+//                }
+//
+//                return true;
+//
+//            case 1:
+//                if (CommandUtils.shouldEnableToggle(args[0])) {
+//                    if (!Permissions.hardcoreToggle(sender)) {
+//                        sender.sendMessage(command.getPermissionMessage());
+//                        return true;
+//                    }
+//
+//                    enable(null);
+//                    return true;
+//                }
+//
+//                if (CommandUtils.shouldDisableToggle(args[0])) {
+//                    if (!Permissions.hardcoreToggle(sender)) {
+//                        sender.sendMessage(command.getPermissionMessage());
+//                        return true;
+//                    }
+//
+//                    disable(null);
+//                    return true;
+//                }
+//
+//                if (CommandUtils.isInvalidDouble(sender, args[0])) {
+//                    return true;
+//                }
+//
+//                if (!Permissions.hardcoreModify(sender)) {
+//                    sender.sendMessage(command.getPermissionMessage());
+//                    return true;
+//                }
+//
+//                modify(sender, Double.parseDouble(args[0]));
+//                return true;
+//
+//
+//            case 2:
+//                if (CommandUtils.isInvalidSkill(sender, args[0])) {
+//                    return true;
+//                }
+//
+//                PrimarySkillType skill = PrimarySkillType.getSkill(args[0]);
+//
+//                if (!CommandUtils.isChildSkill(sender, skill)) {
+//                    return true;
+//                }
+//
+//                if (CommandUtils.shouldEnableToggle(args[1])) {
+//                    if (!Permissions.hardcoreToggle(sender)) {
+//                        sender.sendMessage(command.getPermissionMessage());
+//                        return true;
+//                    }
+//
+//                    enable(skill);
+//                    return true;
+//                }
+//
+//                if (CommandUtils.shouldDisableToggle(args[1])) {
+//                    if (!Permissions.hardcoreToggle(sender)) {
+//                        sender.sendMessage(command.getPermissionMessage());
+//                        return true;
+//                    }
+//
+//                    enable(skill);
+//                    return true;
+//                }
+//
+//                return true;
+//
+//            default:
+//                return false;
+//        }
+//    }
+//
+//    @Override
+//    public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
+//        if (args.length == 1) {
+//            if (StringUtils.isDouble(args[0])) {
+//                return ImmutableList.of();
+//            }
+//
+//            return StringUtil.copyPartialMatches(args[0], CommandUtils.TRUE_FALSE_OPTIONS, new ArrayList<>(CommandUtils.TRUE_FALSE_OPTIONS.size()));
+//        }
+//        return ImmutableList.of();
+//    }
+//
+//    protected abstract boolean checkTogglePermissions(CommandSender sender);
+//    protected abstract boolean checkModifyPermissions(CommandSender sender);
+//    protected abstract boolean checkEnabled(PrimarySkillType skill);
+//    protected abstract void enable(PrimarySkillType skill);
+//    protected abstract void disable(PrimarySkillType skill);
+//    protected abstract void modify(CommandSender sender, double newPercentage);
+//}

+ 64 - 64
src/main/java/com/gmail/nossr50/commands/hardcore/VampirismCommand.java

@@ -1,64 +1,64 @@
-package com.gmail.nossr50.commands.hardcore;
-
-import com.gmail.nossr50.config.Config;
-import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
-import com.gmail.nossr50.locale.LocaleLoader;
-import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.Permissions;
-import org.bukkit.command.CommandSender;
-
-public class VampirismCommand extends HardcoreModeCommand {
-    @Override
-    protected boolean checkTogglePermissions(CommandSender sender) {
-        return Permissions.vampirismToggle(sender);
-    }
-
-    @Override
-    protected boolean checkModifyPermissions(CommandSender sender) {
-        return Permissions.vampirismModify(sender);
-    }
-
-    @Override
-    protected boolean checkEnabled(PrimarySkillType skill) {
-        if (skill == null) {
-            for (PrimarySkillType primarySkillType : PrimarySkillType.values()) {
-                if (!primarySkillType.getHardcoreVampirismEnabled()) {
-                    return false;
-                }
-            }
-
-            return true;
-        }
-
-        return skill.getHardcoreVampirismEnabled();
-    }
-
-    @Override
-    protected void enable(PrimarySkillType skill) {
-        toggle(true, skill);
-    }
-
-    @Override
-    protected void disable(PrimarySkillType skill) {
-        toggle(false, skill);
-    }
-
-    @Override
-    protected void modify(CommandSender sender, double newPercentage) {
-        Config.getInstance().setHardcoreVampirismStatLeechPercentage(newPercentage);
-        sender.sendMessage(LocaleLoader.getString("Hardcore.Vampirism.PercentageChanged", percent.format(newPercentage / 100.0D)));
-    }
-
-    private void toggle(boolean enable, PrimarySkillType skill) {
-        if (skill == null) {
-            for (PrimarySkillType primarySkillType : PrimarySkillType.NON_CHILD_SKILLS) {
-                primarySkillType.setHardcoreVampirismEnabled(enable);
-            }
-        }
-        else {
-            skill.setHardcoreVampirismEnabled(enable);
-        }
-
-        mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Hardcore.Mode." + (enable ? "Enabled" : "Disabled"), LocaleLoader.getString("Hardcore.Vampirism.Name"), (skill == null ? "all skills" : skill)));
-    }
-}
+//package com.gmail.nossr50.commands.hardcore;
+//
+//import com.gmail.nossr50.config.Config;
+//import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+//import com.gmail.nossr50.locale.LocaleLoader;
+//import com.gmail.nossr50.mcMMO;
+//import com.gmail.nossr50.util.Permissions;
+//import org.bukkit.command.CommandSender;
+//
+//public class VampirismCommand extends HardcoreModeCommand {
+//    @Override
+//    protected boolean checkTogglePermissions(CommandSender sender) {
+//        return Permissions.vampirismToggle(sender);
+//    }
+//
+//    @Override
+//    protected boolean checkModifyPermissions(CommandSender sender) {
+//        return Permissions.vampirismModify(sender);
+//    }
+//
+//    @Override
+//    protected boolean checkEnabled(PrimarySkillType skill) {
+//        if (skill == null) {
+//            for (PrimarySkillType primarySkillType : PrimarySkillType.values()) {
+//                if (!primarySkillType.getHardcoreVampirismEnabled()) {
+//                    return false;
+//                }
+//            }
+//
+//            return true;
+//        }
+//
+//        return skill.getHardcoreVampirismEnabled();
+//    }
+//
+//    @Override
+//    protected void enable(PrimarySkillType skill) {
+//        toggle(true, skill);
+//    }
+//
+//    @Override
+//    protected void disable(PrimarySkillType skill) {
+//        toggle(false, skill);
+//    }
+//
+//    @Override
+//    protected void modify(CommandSender sender, double newPercentage) {
+//        Config.getInstance().setHardcoreVampirismStatLeechPercentage(newPercentage);
+//        sender.sendMessage(LocaleLoader.getString("Hardcore.Vampirism.PercentageChanged", percent.format(newPercentage / 100.0D)));
+//    }
+//
+//    private void toggle(boolean enable, PrimarySkillType skill) {
+//        if (skill == null) {
+//            for (PrimarySkillType primarySkillType : SkillTools.NON_CHILD_SKILLS) {
+//                primarySkillType.setHardcoreVampirismEnabled(enable);
+//            }
+//        }
+//        else {
+//            skill.setHardcoreVampirismEnabled(enable);
+//        }
+//
+//        mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Hardcore.Mode." + (enable ? "Enabled" : "Disabled"), LocaleLoader.getString("Hardcore.Vampirism.Name"), (skill == null ? "all skills" : skill)));
+//    }
+//}

+ 6 - 4
src/main/java/com/gmail/nossr50/commands/party/PartyDisbandCommand.java

@@ -1,6 +1,7 @@
 package com.gmail.nossr50.commands.party;
 package com.gmail.nossr50.commands.party;
 
 
 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.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.party.PartyManager;
 import com.gmail.nossr50.party.PartyManager;
@@ -15,13 +16,14 @@ public class PartyDisbandCommand implements CommandExecutor {
     @Override
     @Override
     public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
     public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
         if (args.length == 1) {
         if (args.length == 1) {
-            if (UserManager.getPlayer((Player) sender) == null) {
+            final McMMOPlayer mcMMOPlayer = UserManager.getPlayer((Player) sender);
+            if (mcMMOPlayer == null) {
                 sender.sendMessage(LocaleLoader.getString("Profile.PendingLoad"));
                 sender.sendMessage(LocaleLoader.getString("Profile.PendingLoad"));
                 return true;
                 return true;
             }
             }
 
 
-            Party playerParty = UserManager.getPlayer((Player) sender).getParty();
-            String partyName = playerParty.getName();
+            final Party playerParty = mcMMOPlayer.getParty();
+            final String partyName = playerParty.getName();
 
 
             for (Player member : playerParty.getOnlineMembers()) {
             for (Player member : playerParty.getOnlineMembers()) {
                 if (!PartyManager.handlePartyChangeEvent(member, partyName, null, EventReason.KICKED_FROM_PARTY)) {
                 if (!PartyManager.handlePartyChangeEvent(member, partyName, null, EventReason.KICKED_FROM_PARTY)) {
@@ -31,7 +33,7 @@ public class PartyDisbandCommand implements CommandExecutor {
                 member.sendMessage(LocaleLoader.getString("Party.Disband"));
                 member.sendMessage(LocaleLoader.getString("Party.Disband"));
             }
             }
 
 
-            PartyManager.disbandParty(playerParty);
+            PartyManager.disbandParty(mcMMOPlayer, playerParty);
             return true;
             return true;
         }
         }
         sender.sendMessage(LocaleLoader.getString("Commands.Usage.1", "party", "disband"));
         sender.sendMessage(LocaleLoader.getString("Commands.Usage.1", "party", "disband"));

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

@@ -1,11 +1,11 @@
 package com.gmail.nossr50.commands.party;
 package com.gmail.nossr50.commands.party;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.PartyFeature;
 import com.gmail.nossr50.datatypes.party.PartyFeature;
 import com.gmail.nossr50.datatypes.party.ShareMode;
 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.party.PartyManager;
 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;
@@ -85,7 +85,7 @@ public class PartyInfoCommand implements CommandExecutor {
     }
     }
 
 
     private boolean isUnlockedFeature(Party party, PartyFeature partyFeature) {
     private boolean isUnlockedFeature(Party party, PartyFeature partyFeature) {
-        return party.getLevel() >= Config.getInstance().getPartyFeatureUnlockLevel(partyFeature);
+        return party.getLevel() >= mcMMO.p.getGeneralConfig().getPartyFeatureUnlockLevel(partyFeature);
     }
     }
 
 
     private void displayShareModeInfo(Player player, Party party) {
     private void displayShareModeInfo(Player player, Party party) {

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

@@ -1,9 +1,9 @@
 package com.gmail.nossr50.commands.party;
 package com.gmail.nossr50.commands.party;
 
 
-import com.gmail.nossr50.config.Config;
 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.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.party.PartyManager;
 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;
@@ -53,7 +53,7 @@ public class PartyInviteCommand implements CommandExecutor {
             Party playerParty = mcMMOPlayer.getParty();
             Party playerParty = mcMMOPlayer.getParty();
 
 
             if (PartyManager.isPartyFull(target, playerParty)) {
             if (PartyManager.isPartyFull(target, playerParty)) {
-                player.sendMessage(LocaleLoader.getString("Commands.Party.PartyFull.Invite", target.getName(), playerParty.toString(), Config.getInstance().getPartyMaxSize()));
+                player.sendMessage(LocaleLoader.getString("Commands.Party.PartyFull.Invite", target.getName(), playerParty.toString(), mcMMO.p.getGeneralConfig().getPartyMaxSize()));
                 return true;
                 return true;
             }
             }
 
 

+ 2 - 2
src/main/java/com/gmail/nossr50/commands/party/PartyItemShareCommand.java

@@ -1,11 +1,11 @@
 package com.gmail.nossr50.commands.party;
 package com.gmail.nossr50.commands.party;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.party.ItemShareType;
 import com.gmail.nossr50.datatypes.party.ItemShareType;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.PartyFeature;
 import com.gmail.nossr50.datatypes.party.PartyFeature;
 import com.gmail.nossr50.datatypes.party.ShareMode;
 import com.gmail.nossr50.datatypes.party.ShareMode;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 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.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.StringUtils;
@@ -28,7 +28,7 @@ public class PartyItemShareCommand implements CommandExecutor {
 
 
         Party party = UserManager.getPlayer((Player) sender).getParty();
         Party party = UserManager.getPlayer((Player) sender).getParty();
 
 
-        if (party.getLevel() < Config.getInstance().getPartyFeatureUnlockLevel(PartyFeature.ITEM_SHARE)) {
+        if (party.getLevel() < mcMMO.p.getGeneralConfig().getPartyFeatureUnlockLevel(PartyFeature.ITEM_SHARE)) {
             sender.sendMessage(LocaleLoader.getString("Party.Feature.Disabled.4"));
             sender.sendMessage(LocaleLoader.getString("Party.Feature.Disabled.4"));
             return true;
             return true;
         }
         }

+ 2 - 2
src/main/java/com/gmail/nossr50/commands/party/PartyXpShareCommand.java

@@ -1,10 +1,10 @@
 package com.gmail.nossr50.commands.party;
 package com.gmail.nossr50.commands.party;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.PartyFeature;
 import com.gmail.nossr50.datatypes.party.PartyFeature;
 import com.gmail.nossr50.datatypes.party.ShareMode;
 import com.gmail.nossr50.datatypes.party.ShareMode;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 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.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.StringUtils;
@@ -25,7 +25,7 @@ public class PartyXpShareCommand implements CommandExecutor {
 
 
         Party party = UserManager.getPlayer((Player) sender).getParty();
         Party party = UserManager.getPlayer((Player) sender).getParty();
 
 
-        if (party.getLevel() < Config.getInstance().getPartyFeatureUnlockLevel(PartyFeature.XP_SHARE)) {
+        if (party.getLevel() < mcMMO.p.getGeneralConfig().getPartyFeatureUnlockLevel(PartyFeature.XP_SHARE)) {
             sender.sendMessage(LocaleLoader.getString("Party.Feature.Disabled.5"));
             sender.sendMessage(LocaleLoader.getString("Party.Feature.Disabled.5"));
             return true;
             return true;
         }
         }

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

@@ -1,10 +1,10 @@
 package com.gmail.nossr50.commands.party.alliance;
 package com.gmail.nossr50.commands.party.alliance;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.PartyFeature;
 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.party.PartyManager;
 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;
@@ -51,7 +51,7 @@ public class PartyAllianceCommand implements TabExecutor {
 
 
         switch (args.length) {
         switch (args.length) {
             case 1:
             case 1:
-                if (playerParty.getLevel() < Config.getInstance().getPartyFeatureUnlockLevel(PartyFeature.ALLIANCE)) {
+                if (playerParty.getLevel() < mcMMO.p.getGeneralConfig().getPartyFeatureUnlockLevel(PartyFeature.ALLIANCE)) {
                     sender.sendMessage(LocaleLoader.getString("Party.Feature.Disabled.3"));
                     sender.sendMessage(LocaleLoader.getString("Party.Feature.Disabled.3"));
                     return true;
                     return true;
                 }
                 }
@@ -69,7 +69,7 @@ public class PartyAllianceCommand implements TabExecutor {
 
 
             case 2:
             case 2:
             case 3:
             case 3:
-                if (playerParty.getLevel() < Config.getInstance().getPartyFeatureUnlockLevel(PartyFeature.ALLIANCE)) {
+                if (playerParty.getLevel() < mcMMO.p.getGeneralConfig().getPartyFeatureUnlockLevel(PartyFeature.ALLIANCE)) {
                     sender.sendMessage(LocaleLoader.getString("Party.Feature.Disabled.3"));
                     sender.sendMessage(LocaleLoader.getString("Party.Feature.Disabled.3"));
                     return true;
                     return true;
                 }
                 }

+ 3 - 3
src/main/java/com/gmail/nossr50/commands/party/teleport/PtpAcceptCommand.java

@@ -1,8 +1,8 @@
 package com.gmail.nossr50.commands.party.teleport;
 package com.gmail.nossr50.commands.party.teleport;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.party.PartyTeleportRecord;
 import com.gmail.nossr50.datatypes.party.PartyTeleportRecord;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+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.skills.SkillUtils;
 import com.gmail.nossr50.util.skills.SkillUtils;
@@ -35,7 +35,7 @@ public class PtpAcceptCommand implements CommandExecutor {
             return true;
             return true;
         }
         }
 
 
-        if (SkillUtils.cooldownExpired(ptpRecord.getTimeout(), Config.getInstance().getPTPCommandTimeout())) {
+        if (SkillUtils.cooldownExpired(ptpRecord.getTimeout(), mcMMO.p.getGeneralConfig().getPTPCommandTimeout())) {
             ptpRecord.removeRequest();
             ptpRecord.removeRequest();
             player.sendMessage(LocaleLoader.getString("Commands.ptp.RequestExpired"));
             player.sendMessage(LocaleLoader.getString("Commands.ptp.RequestExpired"));
             return true;
             return true;
@@ -48,7 +48,7 @@ public class PtpAcceptCommand implements CommandExecutor {
             return true;
             return true;
         }
         }
 
 
-        if (Config.getInstance().getPTPCommandWorldPermissions()) {
+        if (mcMMO.p.getGeneralConfig().getPTPCommandWorldPermissions()) {
             World targetWorld = target.getWorld();
             World targetWorld = target.getWorld();
             World playerWorld = player.getWorld();
             World playerWorld = player.getWorld();
 
 

+ 6 - 7
src/main/java/com/gmail/nossr50/commands/party/teleport/PtpCommand.java

@@ -1,6 +1,5 @@
 package com.gmail.nossr50.commands.party.teleport;
 package com.gmail.nossr50.commands.party.teleport;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.config.WorldBlacklist;
 import com.gmail.nossr50.config.WorldBlacklist;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.Party;
 import com.gmail.nossr50.datatypes.party.PartyFeature;
 import com.gmail.nossr50.datatypes.party.PartyFeature;
@@ -76,7 +75,7 @@ public class PtpCommand implements TabExecutor {
 
 
         Party party = mcMMOPlayer.getParty();
         Party party = mcMMOPlayer.getParty();
 
 
-        if (party.getLevel() < Config.getInstance().getPartyFeatureUnlockLevel(PartyFeature.TELEPORT)) {
+        if (party.getLevel() < mcMMO.p.getGeneralConfig().getPartyFeatureUnlockLevel(PartyFeature.TELEPORT)) {
             sender.sendMessage(LocaleLoader.getString("Party.Feature.Disabled.2"));
             sender.sendMessage(LocaleLoader.getString("Party.Feature.Disabled.2"));
             return true;
             return true;
         }
         }
@@ -91,7 +90,7 @@ public class PtpCommand implements TabExecutor {
             }
             }
 
 
             long recentlyHurt = mcMMOPlayer.getRecentlyHurt();
             long recentlyHurt = mcMMOPlayer.getRecentlyHurt();
-            int hurtCooldown = Config.getInstance().getPTPCommandRecentlyHurtCooldown();
+            int hurtCooldown = mcMMO.p.getGeneralConfig().getPTPCommandRecentlyHurtCooldown();
 
 
             if (hurtCooldown > 0) {
             if (hurtCooldown > 0) {
                 int timeRemaining = SkillUtils.calculateTimeLeft(recentlyHurt * Misc.TIME_CONVERSION_FACTOR, hurtCooldown, player);
                 int timeRemaining = SkillUtils.calculateTimeLeft(recentlyHurt * Misc.TIME_CONVERSION_FACTOR, hurtCooldown, player);
@@ -111,7 +110,7 @@ public class PtpCommand implements TabExecutor {
                 return true;
                 return true;
             }
             }
 
 
-            int ptpCooldown = Config.getInstance().getPTPCommandCooldown();
+            int ptpCooldown = mcMMO.p.getGeneralConfig().getPTPCommandCooldown();
             long ptpLastUse = mcMMOPlayer.getPartyTeleportRecord().getLastUse();
             long ptpLastUse = mcMMOPlayer.getPartyTeleportRecord().getLastUse();
 
 
             if (ptpCooldown > 0) {
             if (ptpCooldown > 0) {
@@ -165,7 +164,7 @@ public class PtpCommand implements TabExecutor {
         Player target = mcMMOTarget.getPlayer();
         Player target = mcMMOTarget.getPlayer();
 
 
 
 
-        if (Config.getInstance().getPTPCommandWorldPermissions()) {
+        if (mcMMO.p.getGeneralConfig().getPTPCommandWorldPermissions()) {
             World targetWorld = target.getWorld();
             World targetWorld = target.getWorld();
             World playerWorld = player.getWorld();
             World playerWorld = player.getWorld();
 
 
@@ -194,7 +193,7 @@ public class PtpCommand implements TabExecutor {
         player.sendMessage(LocaleLoader.getString("Commands.Invite.Success"));
         player.sendMessage(LocaleLoader.getString("Commands.Invite.Success"));
 
 
         target.sendMessage(LocaleLoader.getString("Commands.ptp.Request1", player.getName()));
         target.sendMessage(LocaleLoader.getString("Commands.ptp.Request1", player.getName()));
-        target.sendMessage(LocaleLoader.getString("Commands.ptp.Request2", Config.getInstance().getPTPCommandTimeout()));
+        target.sendMessage(LocaleLoader.getString("Commands.ptp.Request2", mcMMO.p.getGeneralConfig().getPTPCommandTimeout()));
     }
     }
 
 
     protected static boolean canTeleport(CommandSender sender, Player player, String targetName) {
     protected static boolean canTeleport(CommandSender sender, Player player, String targetName) {
@@ -245,7 +244,7 @@ public class PtpCommand implements TabExecutor {
         McMMOPlayer mcMMOPlayer = UserManager.getPlayer(teleportingPlayer);
         McMMOPlayer mcMMOPlayer = UserManager.getPlayer(teleportingPlayer);
         McMMOPlayer mcMMOTarget = UserManager.getPlayer(targetPlayer);
         McMMOPlayer mcMMOTarget = UserManager.getPlayer(targetPlayer);
 
 
-        long warmup = Config.getInstance().getPTPCommandWarmup();
+        long warmup = mcMMO.p.getGeneralConfig().getPTPCommandWarmup();
 
 
         mcMMOPlayer.actualizeTeleportCommenceLocation(teleportingPlayer);
         mcMMOPlayer.actualizeTeleportCommenceLocation(teleportingPlayer);
 
 

+ 29 - 15
src/main/java/com/gmail/nossr50/commands/player/InspectCommand.java

@@ -1,6 +1,5 @@
 package com.gmail.nossr50.commands.player;
 package com.gmail.nossr50.commands.player;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 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;
@@ -30,16 +29,18 @@ public class InspectCommand implements TabExecutor {
 
 
             // If the mcMMOPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
             // If the mcMMOPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
             if (mcMMOPlayer == null) {
             if (mcMMOPlayer == null) {
-                PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName, false); // Temporary Profile
+                PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName); // Temporary Profile
 
 
                 if (!CommandUtils.isLoaded(sender, profile)) {
                 if (!CommandUtils.isLoaded(sender, profile)) {
                     return true;
                     return true;
                 }
                 }
 
 
-                if (Config.getInstance().getScoreboardsEnabled() && sender instanceof Player && Config.getInstance().getInspectUseBoard()) {
+                if (mcMMO.p.getGeneralConfig().getScoreboardsEnabled()
+                        && sender instanceof Player
+                        && mcMMO.p.getGeneralConfig().getInspectUseBoard()) {
                     ScoreboardManager.enablePlayerInspectScoreboard((Player) sender, profile);
                     ScoreboardManager.enablePlayerInspectScoreboard((Player) sender, profile);
 
 
-                    if (!Config.getInstance().getInspectUseChat()) {
+                    if (!mcMMO.p.getGeneralConfig().getInspectUseChat()) {
                         return true;
                         return true;
                     }
                     }
                 }
                 }
@@ -47,43 +48,56 @@ public class InspectCommand implements TabExecutor {
                 sender.sendMessage(LocaleLoader.getString("Inspect.OfflineStats", playerName));
                 sender.sendMessage(LocaleLoader.getString("Inspect.OfflineStats", playerName));
 
 
                 sender.sendMessage(LocaleLoader.getString("Stats.Header.Gathering"));
                 sender.sendMessage(LocaleLoader.getString("Stats.Header.Gathering"));
-                for (PrimarySkillType skill : PrimarySkillType.GATHERING_SKILLS) {
+                for (PrimarySkillType skill : mcMMO.p.getSkillTools().GATHERING_SKILLS) {
                     sender.sendMessage(CommandUtils.displaySkill(profile, skill));
                     sender.sendMessage(CommandUtils.displaySkill(profile, skill));
                 }
                 }
 
 
                 sender.sendMessage(LocaleLoader.getString("Stats.Header.Combat"));
                 sender.sendMessage(LocaleLoader.getString("Stats.Header.Combat"));
-                for (PrimarySkillType skill : PrimarySkillType.COMBAT_SKILLS) {
+                for (PrimarySkillType skill : mcMMO.p.getSkillTools().COMBAT_SKILLS) {
                     sender.sendMessage(CommandUtils.displaySkill(profile, skill));
                     sender.sendMessage(CommandUtils.displaySkill(profile, skill));
                 }
                 }
 
 
                 sender.sendMessage(LocaleLoader.getString("Stats.Header.Misc"));
                 sender.sendMessage(LocaleLoader.getString("Stats.Header.Misc"));
-                for (PrimarySkillType skill : PrimarySkillType.MISC_SKILLS) {
+                for (PrimarySkillType skill : mcMMO.p.getSkillTools().MISC_SKILLS) {
                     sender.sendMessage(CommandUtils.displaySkill(profile, skill));
                     sender.sendMessage(CommandUtils.displaySkill(profile, skill));
                 }
                 }
 
 
             } else {
             } else {
                 Player target = mcMMOPlayer.getPlayer();
                 Player target = mcMMOPlayer.getPlayer();
+                boolean isVanished = false;
 
 
                 if (CommandUtils.hidden(sender, target, Permissions.inspectHidden(sender))) {
                 if (CommandUtils.hidden(sender, target, Permissions.inspectHidden(sender))) {
-                    sender.sendMessage(LocaleLoader.getString("Inspect.Offline"));
-                    return true;
-                } else if (CommandUtils.tooFar(sender, target, Permissions.inspectFar(sender))) {
+                    isVanished = true;
+                }
+
+                //Only distance check players who are online and not vanished
+                if (!isVanished && CommandUtils.tooFar(sender, target, Permissions.inspectFar(sender))) {
                     return true;
                     return true;
                 }
                 }
 
 
-                if (Config.getInstance().getScoreboardsEnabled() && sender instanceof Player && Config.getInstance().getInspectUseBoard()) {
-                    ScoreboardManager.enablePlayerInspectScoreboard((Player) sender, mcMMOPlayer.getProfile());
+                if (mcMMO.p.getGeneralConfig().getScoreboardsEnabled()
+                        && sender instanceof Player
+                        && mcMMO.p.getGeneralConfig().getInspectUseBoard()) {
+                    ScoreboardManager.enablePlayerInspectScoreboard((Player) sender, mcMMOPlayer);
 
 
-                    if (!Config.getInstance().getInspectUseChat()) {
+                    if (!mcMMO.p.getGeneralConfig().getInspectUseChat()) {
                         return true;
                         return true;
                     }
                     }
                 }
                 }
 
 
-                sender.sendMessage(LocaleLoader.getString("Inspect.Stats", target.getName()));
+                if (isVanished) {
+                    sender.sendMessage(LocaleLoader.getString("Inspect.OfflineStats", playerName));
+                } else {
+                    sender.sendMessage(LocaleLoader.getString("Inspect.Stats", target.getName()));
+                }
+
                 CommandUtils.printGatheringSkills(target, sender);
                 CommandUtils.printGatheringSkills(target, sender);
                 CommandUtils.printCombatSkills(target, sender);
                 CommandUtils.printCombatSkills(target, sender);
                 CommandUtils.printMiscSkills(target, sender);
                 CommandUtils.printMiscSkills(target, sender);
-                sender.sendMessage(LocaleLoader.getString("Commands.PowerLevel", mcMMOPlayer.getPowerLevel()));
+
+                if (!isVanished) {
+                    sender.sendMessage(LocaleLoader.getString("Commands.PowerLevel", mcMMOPlayer.getPowerLevel()));
+                }
             }
             }
 
 
             return true;
             return true;

+ 3 - 3
src/main/java/com/gmail/nossr50/commands/player/MccooldownCommand.java

@@ -1,9 +1,9 @@
 package com.gmail.nossr50.commands.player;
 package com.gmail.nossr50.commands.player;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
 import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 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.gmail.nossr50.util.scoreboards.ScoreboardManager;
 import com.gmail.nossr50.util.scoreboards.ScoreboardManager;
@@ -30,10 +30,10 @@ public class MccooldownCommand implements TabExecutor {
         if (args.length == 0) {
         if (args.length == 0) {
             Player player = (Player) sender;
             Player player = (Player) sender;
 
 
-            if (Config.getInstance().getScoreboardsEnabled() && Config.getInstance().getCooldownUseBoard()) {
+            if (mcMMO.p.getGeneralConfig().getScoreboardsEnabled() && mcMMO.p.getGeneralConfig().getCooldownUseBoard()) {
                 ScoreboardManager.enablePlayerCooldownScoreboard(player);
                 ScoreboardManager.enablePlayerCooldownScoreboard(player);
 
 
-                if (!Config.getInstance().getCooldownUseChat()) {
+                if (!mcMMO.p.getGeneralConfig().getCooldownUseChat()) {
                     return true;
                     return true;
                 }
                 }
             }
             }

+ 6 - 6
src/main/java/com/gmail/nossr50/commands/player/McrankCommand.java

@@ -1,10 +1,10 @@
 package com.gmail.nossr50.commands.player;
 package com.gmail.nossr50.commands.player;
 
 
-import com.gmail.nossr50.config.Config;
 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.runnables.commands.McrankCommandAsyncTask;
 import com.gmail.nossr50.runnables.commands.McrankCommandAsyncTask;
+import com.gmail.nossr50.util.MetadataConstants;
 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.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
@@ -91,25 +91,25 @@ public class McrankCommand implements TabExecutor {
                 return;
                 return;
             }
             }
 
 
-            long cooldownMillis = Math.min(Config.getInstance().getDatabasePlayerCooldown(), 1750);
+            long cooldownMillis = Math.min(mcMMO.p.getGeneralConfig().getDatabasePlayerCooldown(), 1750);
 
 
             if (mcMMOPlayer.getDatabaseATS() + cooldownMillis > System.currentTimeMillis()) {
             if (mcMMOPlayer.getDatabaseATS() + cooldownMillis > System.currentTimeMillis()) {
                 sender.sendMessage(LocaleLoader.getString("Commands.Database.CooldownMS", getCDSeconds(mcMMOPlayer, cooldownMillis)));
                 sender.sendMessage(LocaleLoader.getString("Commands.Database.CooldownMS", getCDSeconds(mcMMOPlayer, cooldownMillis)));
                 return;
                 return;
             }
             }
 
 
-            if (((Player) sender).hasMetadata(mcMMO.databaseCommandKey)) {
+            if (((Player) sender).hasMetadata(MetadataConstants.METADATA_KEY_DATABASE_COMMAND)) {
                 sender.sendMessage(LocaleLoader.getString("Commands.Database.Processing"));
                 sender.sendMessage(LocaleLoader.getString("Commands.Database.Processing"));
                 return;
                 return;
             } else {
             } else {
-                ((Player) sender).setMetadata(mcMMO.databaseCommandKey, new FixedMetadataValue(mcMMO.p, null));
+                ((Player) sender).setMetadata(MetadataConstants.METADATA_KEY_DATABASE_COMMAND, new FixedMetadataValue(mcMMO.p, null));
             }
             }
 
 
             mcMMOPlayer.actualizeDatabaseATS();
             mcMMOPlayer.actualizeDatabaseATS();
         }
         }
 
 
-        boolean useBoard = Config.getInstance().getScoreboardsEnabled() && (sender instanceof Player) && (Config.getInstance().getRankUseBoard());
-        boolean useChat = !useBoard || Config.getInstance().getRankUseChat();
+        boolean useBoard = mcMMO.p.getGeneralConfig().getScoreboardsEnabled() && (sender instanceof Player) && (mcMMO.p.getGeneralConfig().getRankUseBoard());
+        boolean useChat = !useBoard || mcMMO.p.getGeneralConfig().getRankUseChat();
 
 
         new McrankCommandAsyncTask(playerName, sender, useBoard, useChat).runTaskAsynchronously(mcMMO.p);
         new McrankCommandAsyncTask(playerName, sender, useBoard, useChat).runTaskAsynchronously(mcMMO.p);
     }
     }

+ 4 - 4
src/main/java/com/gmail/nossr50/commands/player/McstatsCommand.java

@@ -1,7 +1,7 @@
 package com.gmail.nossr50.commands.player;
 package com.gmail.nossr50.commands.player;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 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.gmail.nossr50.util.scoreboards.ScoreboardManager;
 import com.gmail.nossr50.util.scoreboards.ScoreboardManager;
@@ -33,10 +33,10 @@ public class McstatsCommand implements TabExecutor {
 
 
             Player player = (Player) sender;
             Player player = (Player) sender;
 
 
-            if (Config.getInstance().getStatsUseBoard() && Config.getInstance().getScoreboardsEnabled()) {
+            if (mcMMO.p.getGeneralConfig().getStatsUseBoard() && mcMMO.p.getGeneralConfig().getScoreboardsEnabled()) {
                 ScoreboardManager.enablePlayerStatsScoreboard(player);
                 ScoreboardManager.enablePlayerStatsScoreboard(player);
 
 
-                if (!Config.getInstance().getStatsUseChat()) {
+                if (!mcMMO.p.getGeneralConfig().getStatsUseChat()) {
                     return true;
                     return true;
                 }
                 }
             }
             }
@@ -48,7 +48,7 @@ public class McstatsCommand implements TabExecutor {
             CommandUtils.printCombatSkills(player);
             CommandUtils.printCombatSkills(player);
             CommandUtils.printMiscSkills(player);
             CommandUtils.printMiscSkills(player);
 
 
-            int powerLevelCap = Config.getInstance().getPowerLevelCap();
+            int powerLevelCap = mcMMO.p.getGeneralConfig().getPowerLevelCap();
 
 
             if (powerLevelCap != Integer.MAX_VALUE) {
             if (powerLevelCap != Integer.MAX_VALUE) {
                 player.sendMessage(LocaleLoader.getString("Commands.PowerLevel.Capped", UserManager.getPlayer(player).getPowerLevel(), powerLevelCap));
                 player.sendMessage(LocaleLoader.getString("Commands.PowerLevel.Capped", UserManager.getPlayer(player).getPowerLevel(), powerLevelCap));

+ 8 - 8
src/main/java/com/gmail/nossr50/commands/player/MctopCommand.java

@@ -1,11 +1,11 @@
 package com.gmail.nossr50.commands.player;
 package com.gmail.nossr50.commands.player;
 
 
-import com.gmail.nossr50.config.Config;
 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.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.runnables.commands.MctopCommandAsyncTask;
 import com.gmail.nossr50.runnables.commands.MctopCommandAsyncTask;
+import com.gmail.nossr50.util.MetadataConstants;
 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.UserManager;
 import com.gmail.nossr50.util.player.UserManager;
@@ -69,7 +69,7 @@ public class MctopCommand implements TabExecutor {
     @Override
     @Override
     public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
     public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
         if (args.length == 1) {
         if (args.length == 1) {
-            return StringUtil.copyPartialMatches(args[0], PrimarySkillType.SKILL_NAMES, new ArrayList<>(PrimarySkillType.SKILL_NAMES.size()));
+            return StringUtil.copyPartialMatches(args[0], mcMMO.p.getSkillTools().LOCALIZED_SKILL_NAMES, new ArrayList<>(mcMMO.p.getSkillTools().LOCALIZED_SKILL_NAMES.size()));
         }
         }
         return ImmutableList.of();
         return ImmutableList.of();
     }
     }
@@ -86,7 +86,7 @@ public class MctopCommand implements TabExecutor {
             }
             }
 
 
             McMMOPlayer mcMMOPlayer = UserManager.getPlayer(sender.getName());
             McMMOPlayer mcMMOPlayer = UserManager.getPlayer(sender.getName());
-            long cooldownMillis = Math.max(Config.getInstance().getDatabasePlayerCooldown(), 1750);
+            long cooldownMillis = Math.max(mcMMO.p.getGeneralConfig().getDatabasePlayerCooldown(), 1750);
 
 
             if (mcMMOPlayer.getDatabaseATS() + cooldownMillis > System.currentTimeMillis()) {
             if (mcMMOPlayer.getDatabaseATS() + cooldownMillis > System.currentTimeMillis()) {
                 double seconds = ((mcMMOPlayer.getDatabaseATS() + cooldownMillis) - System.currentTimeMillis()) / 1000.0D;
                 double seconds = ((mcMMOPlayer.getDatabaseATS() + cooldownMillis) - System.currentTimeMillis()) / 1000.0D;
@@ -98,11 +98,11 @@ public class MctopCommand implements TabExecutor {
                 return;
                 return;
             }
             }
 
 
-            if (((Player) sender).hasMetadata(mcMMO.databaseCommandKey)) {
+            if (((Player) sender).hasMetadata(MetadataConstants.METADATA_KEY_DATABASE_COMMAND)) {
                 sender.sendMessage(LocaleLoader.getString("Commands.Database.Processing"));
                 sender.sendMessage(LocaleLoader.getString("Commands.Database.Processing"));
                 return;
                 return;
             } else {
             } else {
-                ((Player) sender).setMetadata(mcMMO.databaseCommandKey, new FixedMetadataValue(mcMMO.p, null));
+                ((Player) sender).setMetadata(MetadataConstants.METADATA_KEY_DATABASE_COMMAND, new FixedMetadataValue(mcMMO.p, null));
             }
             }
 
 
             mcMMOPlayer.actualizeDatabaseATS();
             mcMMOPlayer.actualizeDatabaseATS();
@@ -112,8 +112,8 @@ public class MctopCommand implements TabExecutor {
     }
     }
 
 
     private void display(int page, PrimarySkillType skill, CommandSender sender) {
     private void display(int page, PrimarySkillType skill, CommandSender sender) {
-        boolean useBoard = (sender instanceof Player) && (Config.getInstance().getTopUseBoard());
-        boolean useChat = !useBoard || Config.getInstance().getTopUseChat();
+        boolean useBoard = (sender instanceof Player) && (mcMMO.p.getGeneralConfig().getTopUseBoard());
+        boolean useChat = !useBoard || mcMMO.p.getGeneralConfig().getTopUseChat();
 
 
         new MctopCommandAsyncTask(page, skill, sender, useBoard, useChat).runTaskAsynchronously(mcMMO.p);
         new MctopCommandAsyncTask(page, skill, sender, useBoard, useChat).runTaskAsynchronously(mcMMO.p);
     }
     }
@@ -123,7 +123,7 @@ public class MctopCommand implements TabExecutor {
             return null;
             return null;
         }
         }
 
 
-        PrimarySkillType skill = PrimarySkillType.getSkill(skillName);
+        PrimarySkillType skill = mcMMO.p.getSkillTools().matchSkill(skillName);
 
 
         if (CommandUtils.isChildSkill(sender, skill)) {
         if (CommandUtils.isChildSkill(sender, skill)) {
             return null;
             return null;

+ 3 - 2
src/main/java/com/gmail/nossr50/commands/player/XPBarCommand.java

@@ -2,6 +2,7 @@ package com.gmail.nossr50.commands.player;
 
 
 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.mcMMO;
 import com.gmail.nossr50.util.experience.ExperienceBarManager;
 import com.gmail.nossr50.util.experience.ExperienceBarManager;
 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;
@@ -51,7 +52,7 @@ public class XPBarCommand implements TabExecutor {
 
 
                 if(SkillUtils.isSkill(skillName)) {
                 if(SkillUtils.isSkill(skillName)) {
 
 
-                    PrimarySkillType targetSkill = PrimarySkillType.getSkill(skillName);
+                    PrimarySkillType targetSkill = mcMMO.p.getSkillTools().matchSkill(skillName);
 
 
                     //Target setting
                     //Target setting
                     String option = args[0].toLowerCase();
                     String option = args[0].toLowerCase();
@@ -103,7 +104,7 @@ public class XPBarCommand implements TabExecutor {
                 return StringUtil.copyPartialMatches(args[0], options, new ArrayList<>(ExperienceBarManager.XPBarSettingTarget.values().length));
                 return StringUtil.copyPartialMatches(args[0], options, new ArrayList<>(ExperienceBarManager.XPBarSettingTarget.values().length));
             case 2:
             case 2:
                 if(!args[0].equalsIgnoreCase(ExperienceBarManager.XPBarSettingTarget.RESET.toString()))
                 if(!args[0].equalsIgnoreCase(ExperienceBarManager.XPBarSettingTarget.RESET.toString()))
-                    return StringUtil.copyPartialMatches(args[1], PrimarySkillType.SKILL_NAMES, new ArrayList<>(PrimarySkillType.SKILL_NAMES.size()));
+                    return StringUtil.copyPartialMatches(args[1], mcMMO.p.getSkillTools().LOCALIZED_SKILL_NAMES, new ArrayList<>(mcMMO.p.getSkillTools().LOCALIZED_SKILL_NAMES.size()));
             default:
             default:
                 return ImmutableList.of();
                 return ImmutableList.of();
         }
         }

+ 199 - 199
src/main/java/com/gmail/nossr50/commands/skills/AprilCommand.java

@@ -1,199 +1,199 @@
-package com.gmail.nossr50.commands.skills;
-
-import com.gmail.nossr50.locale.LocaleLoader;
-import com.gmail.nossr50.util.HolidayManager.FakeSkillType;
-import com.gmail.nossr50.util.Misc;
-import com.gmail.nossr50.util.commands.CommandUtils;
-import com.gmail.nossr50.util.text.StringUtils;
-import com.google.common.collect.ImmutableList;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandSender;
-import org.bukkit.command.TabExecutor;
-import org.bukkit.entity.Player;
-import org.jetbrains.annotations.NotNull;
-
-import java.text.DecimalFormat;
-import java.util.ArrayList;
-import java.util.List;
-
-public class AprilCommand implements TabExecutor {
-    private String skillName;
-
-    protected DecimalFormat percent = new DecimalFormat("##0.00%");
-    protected DecimalFormat decimal = new DecimalFormat("##0.00");
-
-    @Override
-    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
-        if (CommandUtils.noConsoleUsage(sender)) {
-            return true;
-        }
-
-        skillName = StringUtils.getCapitalized(label);
-
-        if (args.length == 0) {
-            Player player = (Player) sender;
-            FakeSkillType fakeSkillType = FakeSkillType.getByName(skillName);
-
-            float skillValue = Misc.getRandom().nextInt(99);
-
-            player.sendMessage(LocaleLoader.getString("Skills.Header", skillName));
-            player.sendMessage(LocaleLoader.getString("Commands.XPGain", getXPGainString(fakeSkillType)));
-            player.sendMessage(LocaleLoader.getString("Effects.Level", (int) skillValue, Misc.getRandom().nextInt(1000), 1000 + Misc.getRandom().nextInt(1000)));
-
-
-            List<String> effectMessages = effectsDisplay(fakeSkillType);
-
-            if (!effectMessages.isEmpty()) {
-                player.sendMessage(LocaleLoader.getString("Skills.Header", LocaleLoader.getString("Effects.Effects")));
-
-                for (String message : effectMessages) {
-                    player.sendMessage(message);
-                }
-            }
-
-            List<String> statsMessages = statsDisplay(fakeSkillType);
-
-            if (!statsMessages.isEmpty()) {
-                player.sendMessage(LocaleLoader.getString("Skills.Header", LocaleLoader.getString("Commands.Stats.Self")));
-
-                for (String message : statsMessages) {
-                    player.sendMessage(message);
-                }
-            }
-
-            player.sendMessage(LocaleLoader.formatString("[[DARK_AQUA]]Guide for {0} available - type /APRIL FOOLS ! :D", skillName));
-            return true;
-        }
-        return true;
-    }
-
-    private String getXPGainString(FakeSkillType fakeSkillType) {
-        switch (fakeSkillType) {
-            case MACHO:
-                return "Get beaten up";
-            case JUMPING:
-                return "Kris Kross will make ya Jump Jump";
-            case THROWING:
-                return "Chuck your items on the floor";
-            case WRECKING:
-                return "I'M GONNA WRECK IT!";
-            case CRAFTING:
-                return "Craft apple pies";
-            case WALKING:
-                return "Walk around the park";
-            case SWIMMING:
-                return "Like a fish on a bicycle";
-            case FALLING:
-                return "Faceplant the floor, headbutt the ground";
-            case CLIMBING:
-                return "Climb the highest mountain";
-            case FLYING:
-                return "I believe I can fly";
-            case DIVING:
-                return "Scuba club 4000";
-            case PIGGY:
-                return "OINK! OINK!";
-            default:
-                return "Sit and wait?";
-        }
-    }
-
-    @Override
-    public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
-        if (args.length == 1) {
-            return ImmutableList.of("?");
-        }
-        return ImmutableList.of();
-    }
-
-    private List<String> effectsDisplay(FakeSkillType fakeSkillType) {
-        List<String> messages = new ArrayList<>();
-
-        switch (fakeSkillType) {
-            case MACHO:
-                messages.add(LocaleLoader.getString("Effects.Template", "Punching bag", "Absorb damage, like a bag of sand"));
-                break;
-            case JUMPING:
-                messages.add(LocaleLoader.getString("Effects.Template", "Jump", "PRESS SPACE TO JUMP"));
-                messages.add(LocaleLoader.getString("Effects.Template", "Jump Twice", "PRESS SPACE TWICE TO JUMP TWICE"));
-                break;
-            case THROWING:
-                messages.add(LocaleLoader.getString("Effects.Template", "Drop Item", "Randomly drop items, at random"));
-                break;
-            case WRECKING:
-                messages.add(LocaleLoader.getString("Effects.Template", "Ralphinator", "Smash windows with your fists"));
-                break;
-            case CRAFTING:
-                messages.add(LocaleLoader.getString("Effects.Template", "Crafting", "Chance of successful craft"));
-                break;
-            case WALKING:
-                messages.add(LocaleLoader.getString("Effects.Template", "Walk", "Traveling gracefully by foot"));
-                break;
-            case SWIMMING:
-                messages.add(LocaleLoader.getString("Effects.Template", "Swim", "Just keep swimming, swimming, swimming"));
-                break;
-            case FALLING:
-                messages.add(LocaleLoader.getString("Effects.Template", "Skydiving", "Go jump of a cliff. No, seriously."));
-                break;
-            case CLIMBING:
-                messages.add(LocaleLoader.getString("Effects.Template", "Rock Climber", "Use string to climb mountains faster"));
-                break;
-            case FLYING:
-                messages.add(LocaleLoader.getString("Effects.Template", "Fly", "Throw yourself at the ground and miss"));
-                break;
-            case DIVING:
-                messages.add(LocaleLoader.getString("Effects.Template", "Hold Breath", "Press shift to hold your breath longer"));
-                break;
-            case PIGGY:
-                messages.add(LocaleLoader.getString("Effects.Template", "Carrot Turbo", "Supercharge your pigs with carrots"));
-                break;
-        }
-
-        return messages;
-    }
-
-    private List<String> statsDisplay(FakeSkillType fakeSkillType) {
-        List<String> messages = new ArrayList<>();
-
-        switch (fakeSkillType) {
-            case MACHO:
-                messages.add(LocaleLoader.formatString("&cDamage Taken: &e{0}%", decimal.format(Misc.getRandom().nextInt(77))));
-                break;
-            case JUMPING:
-                messages.add(LocaleLoader.formatString("&cDouble Jump Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
-                break;
-            case THROWING:
-                messages.add(LocaleLoader.formatString("&cDrop Item Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(87))));
-                break;
-            case WRECKING:
-                messages.add(LocaleLoader.formatString("&cWrecking Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(14))));
-                break;
-            case CRAFTING:
-                messages.add(LocaleLoader.formatString("&cCrafting Success: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
-                break;
-            case WALKING:
-                messages.add(LocaleLoader.formatString("&cWalk Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
-                break;
-            case SWIMMING:
-                messages.add(LocaleLoader.formatString("&cSwim Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
-                break;
-            case FALLING:
-                messages.add(LocaleLoader.formatString("&cSkydiving Success: &e{0}%", decimal.format(Misc.getRandom().nextInt(37))));
-                break;
-            case CLIMBING:
-                messages.add(LocaleLoader.formatString("&cRock Climber Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
-                break;
-            case FLYING:
-                messages.add(LocaleLoader.formatString("&cFly Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
-                break;
-            case DIVING:
-                messages.add(LocaleLoader.formatString("&cHold Breath Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
-                break;
-            case PIGGY:
-                messages.add(LocaleLoader.formatString("&cCarrot Turbo Boost: &e{0}%", decimal.format(Misc.getRandom().nextInt(80)) + 10));
-                break;
-        }
-
-        return messages;
-    }
-}
+//package com.gmail.nossr50.commands.skills;
+//
+//import com.gmail.nossr50.locale.LocaleLoader;
+//import com.gmail.nossr50.util.HolidayManager.FakeSkillType;
+//import com.gmail.nossr50.util.Misc;
+//import com.gmail.nossr50.util.commands.CommandUtils;
+//import com.gmail.nossr50.util.text.StringUtils;
+//import com.google.common.collect.ImmutableList;
+//import org.bukkit.command.Command;
+//import org.bukkit.command.CommandSender;
+//import org.bukkit.command.TabExecutor;
+//import org.bukkit.entity.Player;
+//import org.jetbrains.annotations.NotNull;
+//
+//import java.text.DecimalFormat;
+//import java.util.ArrayList;
+//import java.util.List;
+//
+//public class AprilCommand implements TabExecutor {
+//    private String skillName;
+//
+//    protected DecimalFormat percent = new DecimalFormat("##0.00%");
+//    protected DecimalFormat decimal = new DecimalFormat("##0.00");
+//
+//    @Override
+//    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
+//        if (CommandUtils.noConsoleUsage(sender)) {
+//            return true;
+//        }
+//
+//        skillName = StringUtils.getCapitalized(label);
+//
+//        if (args.length == 0) {
+//            Player player = (Player) sender;
+//            FakeSkillType fakeSkillType = FakeSkillType.getByName(skillName);
+//
+//            float skillValue = Misc.getRandom().nextInt(99);
+//
+//            player.sendMessage(LocaleLoader.getString("Skills.Header", skillName));
+//            player.sendMessage(LocaleLoader.getString("Commands.XPGain", getXPGainString(fakeSkillType)));
+//            player.sendMessage(LocaleLoader.getString("Effects.Level", (int) skillValue, Misc.getRandom().nextInt(1000), 1000 + Misc.getRandom().nextInt(1000)));
+//
+//
+//            List<String> effectMessages = effectsDisplay(fakeSkillType);
+//
+//            if (!effectMessages.isEmpty()) {
+//                player.sendMessage(LocaleLoader.getString("Skills.Header", LocaleLoader.getString("Effects.Effects")));
+//
+//                for (String message : effectMessages) {
+//                    player.sendMessage(message);
+//                }
+//            }
+//
+//            List<String> statsMessages = statsDisplay(fakeSkillType);
+//
+//            if (!statsMessages.isEmpty()) {
+//                player.sendMessage(LocaleLoader.getString("Skills.Header", LocaleLoader.getString("Commands.Stats.Self")));
+//
+//                for (String message : statsMessages) {
+//                    player.sendMessage(message);
+//                }
+//            }
+//
+//            player.sendMessage(LocaleLoader.formatString("[[DARK_AQUA]]Guide for {0} available - type /APRIL FOOLS ! :D", skillName));
+//            return true;
+//        }
+//        return true;
+//    }
+//
+//    private String getXPGainString(FakeSkillType fakeSkillType) {
+//        switch (fakeSkillType) {
+//            case MACHO:
+//                return "Get beaten up";
+//            case JUMPING:
+//                return "Kris Kross will make ya Jump Jump";
+//            case THROWING:
+//                return "Chuck your items on the floor";
+//            case WRECKING:
+//                return "I'M GONNA WRECK IT!";
+//            case CRAFTING:
+//                return "Craft apple pies";
+//            case WALKING:
+//                return "Walk around the park";
+//            case SWIMMING:
+//                return "Like a fish on a bicycle";
+//            case FALLING:
+//                return "Faceplant the floor, headbutt the ground";
+//            case CLIMBING:
+//                return "Climb the highest mountain";
+//            case FLYING:
+//                return "I believe I can fly";
+//            case DIVING:
+//                return "Scuba club 4000";
+//            case PIGGY:
+//                return "OINK! OINK!";
+//            default:
+//                return "Sit and wait?";
+//        }
+//    }
+//
+//    @Override
+//    public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
+//        if (args.length == 1) {
+//            return ImmutableList.of("?");
+//        }
+//        return ImmutableList.of();
+//    }
+//
+//    private List<String> effectsDisplay(FakeSkillType fakeSkillType) {
+//        List<String> messages = new ArrayList<>();
+//
+//        switch (fakeSkillType) {
+//            case MACHO:
+//                messages.add(LocaleLoader.getString("Effects.Template", "Punching bag", "Absorb damage, like a bag of sand"));
+//                break;
+//            case JUMPING:
+//                messages.add(LocaleLoader.getString("Effects.Template", "Jump", "PRESS SPACE TO JUMP"));
+//                messages.add(LocaleLoader.getString("Effects.Template", "Jump Twice", "PRESS SPACE TWICE TO JUMP TWICE"));
+//                break;
+//            case THROWING:
+//                messages.add(LocaleLoader.getString("Effects.Template", "Drop Item", "Randomly drop items, at random"));
+//                break;
+//            case WRECKING:
+//                messages.add(LocaleLoader.getString("Effects.Template", "Ralphinator", "Smash windows with your fists"));
+//                break;
+//            case CRAFTING:
+//                messages.add(LocaleLoader.getString("Effects.Template", "Crafting", "Chance of successful craft"));
+//                break;
+//            case WALKING:
+//                messages.add(LocaleLoader.getString("Effects.Template", "Walk", "Traveling gracefully by foot"));
+//                break;
+//            case SWIMMING:
+//                messages.add(LocaleLoader.getString("Effects.Template", "Swim", "Just keep swimming, swimming, swimming"));
+//                break;
+//            case FALLING:
+//                messages.add(LocaleLoader.getString("Effects.Template", "Skydiving", "Go jump of a cliff. No, seriously."));
+//                break;
+//            case CLIMBING:
+//                messages.add(LocaleLoader.getString("Effects.Template", "Rock Climber", "Use string to climb mountains faster"));
+//                break;
+//            case FLYING:
+//                messages.add(LocaleLoader.getString("Effects.Template", "Fly", "Throw yourself at the ground and miss"));
+//                break;
+//            case DIVING:
+//                messages.add(LocaleLoader.getString("Effects.Template", "Hold Breath", "Press shift to hold your breath longer"));
+//                break;
+//            case PIGGY:
+//                messages.add(LocaleLoader.getString("Effects.Template", "Carrot Turbo", "Supercharge your pigs with carrots"));
+//                break;
+//        }
+//
+//        return messages;
+//    }
+//
+//    private List<String> statsDisplay(FakeSkillType fakeSkillType) {
+//        List<String> messages = new ArrayList<>();
+//
+//        switch (fakeSkillType) {
+//            case MACHO:
+//                messages.add(LocaleLoader.formatString("&cDamage Taken: &e{0}%", decimal.format(Misc.getRandom().nextInt(77))));
+//                break;
+//            case JUMPING:
+//                messages.add(LocaleLoader.formatString("&cDouble Jump Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
+//                break;
+//            case THROWING:
+//                messages.add(LocaleLoader.formatString("&cDrop Item Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(87))));
+//                break;
+//            case WRECKING:
+//                messages.add(LocaleLoader.formatString("&cWrecking Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(14))));
+//                break;
+//            case CRAFTING:
+//                messages.add(LocaleLoader.formatString("&cCrafting Success: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
+//                break;
+//            case WALKING:
+//                messages.add(LocaleLoader.formatString("&cWalk Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
+//                break;
+//            case SWIMMING:
+//                messages.add(LocaleLoader.formatString("&cSwim Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
+//                break;
+//            case FALLING:
+//                messages.add(LocaleLoader.formatString("&cSkydiving Success: &e{0}%", decimal.format(Misc.getRandom().nextInt(37))));
+//                break;
+//            case CLIMBING:
+//                messages.add(LocaleLoader.formatString("&cRock Climber Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
+//                break;
+//            case FLYING:
+//                messages.add(LocaleLoader.formatString("&cFly Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
+//                break;
+//            case DIVING:
+//                messages.add(LocaleLoader.formatString("&cHold Breath Chance: &e{0}%", decimal.format(Misc.getRandom().nextInt(27))));
+//                break;
+//            case PIGGY:
+//                messages.add(LocaleLoader.formatString("&cCarrot Turbo Boost: &e{0}%", decimal.format(Misc.getRandom().nextInt(80)) + 10));
+//                break;
+//        }
+//
+//        return messages;
+//    }
+//}

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

@@ -3,6 +3,7 @@ package com.gmail.nossr50.commands.skills;
 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;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 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;
@@ -90,10 +91,10 @@ public class HerbalismCommand extends SkillCommand {
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
         hasHylianLuck = Permissions.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.BEETROOT) || 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 = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_FARMERS_DIET);
         canFarmersDiet = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_FARMERS_DIET);
-        canDoubleDrop = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_DOUBLE_DROPS) && !skill.getDoubleDropsDisabled();
+        canDoubleDrop = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_DOUBLE_DROPS) && !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill);
         canShroomThumb = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_SHROOM_THUMB);
         canShroomThumb = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_SHROOM_THUMB);
     }
     }
 
 

+ 8 - 22
src/main/java/com/gmail/nossr50/commands/skills/MmoInfoCommand.java

@@ -1,10 +1,9 @@
 package com.gmail.nossr50.commands.skills;
 package com.gmail.nossr50.commands.skills;
 
 
-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.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.mcMMO;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList;
@@ -28,12 +27,11 @@ public class MmoInfoCommand implements TabExecutor {
         /*
         /*
          * Only allow players to use this command
          * Only allow players to use this command
          */
          */
-        if(commandSender instanceof Player)
+        if(commandSender instanceof Player player)
         {
         {
             if(args.length < 1)
             if(args.length < 1)
                 return false;
                 return false;
 
 
-            Player player = (Player) commandSender;
             if(Permissions.mmoinfo(player))
             if(Permissions.mmoinfo(player))
             {
             {
                 if(args == null || args[0] == null)
                 if(args == null || args[0] == null)
@@ -46,7 +44,7 @@ public class MmoInfoCommand implements TabExecutor {
                     player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.DetailsHeader"));
                     player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.DetailsHeader"));
                     player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.Mystery"));
                     player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.Mystery"));
                     return true;
                     return true;
-                } else if(InteractionManager.getAbstractByName(args[0]) != null || PrimarySkillType.SUBSKILL_NAMES.contains(args[0]))
+                } else if(InteractionManager.getAbstractByName(args[0]) != null || mcMMO.p.getSkillTools().EXACT_SUBSKILL_NAMES.contains(args[0]))
                 {
                 {
                     displayInfo(player, args[0]);
                     displayInfo(player, args[0]);
                     return true;
                     return true;
@@ -64,29 +62,17 @@ public class MmoInfoCommand implements TabExecutor {
     @Override
     @Override
     public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
     public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
         if (args.length == 1) {
         if (args.length == 1) {
-            return StringUtil.copyPartialMatches(args[0], PrimarySkillType.SUBSKILL_NAMES, new ArrayList<>(PrimarySkillType.SUBSKILL_NAMES.size()));
+            return StringUtil.copyPartialMatches(args[0], mcMMO.p.getSkillTools().EXACT_SUBSKILL_NAMES, new ArrayList<>(mcMMO.p.getSkillTools().EXACT_SUBSKILL_NAMES.size()));
         }
         }
         return ImmutableList.of();
         return ImmutableList.of();
     }
     }
 
 
     private void displayInfo(Player player, String subSkillName)
     private void displayInfo(Player player, String subSkillName)
     {
     {
-        //Check to see if the skill exists in the new system
-        AbstractSubSkill abstractSubSkill = InteractionManager.getAbstractByName(subSkillName);
-        if(abstractSubSkill != null)
-        {
-            /* New System Skills are programmable */
-            abstractSubSkill.printInfo(player);
-            //TextComponentFactory.sendPlayerUrlHeader(player);
-        } else {
-            /*
-             * Skill is only in the old system
-             */
-            player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.Header"));
-            player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.SubSkillHeader", subSkillName));
-            player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.DetailsHeader"));
-            player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.OldSkill"));
-        }
+        player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.Header"));
+        player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.SubSkillHeader", subSkillName));
+        player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.DetailsHeader"));
+        player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.OldSkill"));
 
 
         for(SubSkillType subSkillType : SubSkillType.values())
         for(SubSkillType subSkillType : SubSkillType.values())
         {
         {

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

@@ -1,18 +1,21 @@
 package com.gmail.nossr50.commands.skills;
 package com.gmail.nossr50.commands.skills;
 
 
-import com.gmail.nossr50.config.AdvancedConfig;
-import com.gmail.nossr50.config.Config;
 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;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.child.FamilyTree;
 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.text.StringUtils;
 import com.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.gmail.nossr50.util.text.TextComponentFactory;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList;
@@ -42,7 +45,7 @@ public abstract class SkillCommand implements TabExecutor {
 
 
     public SkillCommand(PrimarySkillType skill) {
     public SkillCommand(PrimarySkillType skill) {
         this.skill = skill;
         this.skill = skill;
-        skillName = skill.getName();
+        skillName = mcMMO.p.getSkillTools().getLocalizedSkillName(skill);
         skillGuideCommand = new SkillGuideCommand(skill);
         skillGuideCommand = new SkillGuideCommand(skill);
     }
     }
 
 
@@ -56,26 +59,24 @@ public abstract class SkillCommand implements TabExecutor {
             return true;
             return true;
         }
         }
 
 
-        if(UserManager.getPlayer((Player) sender) == null)
-        {
+        Player player = (Player) sender;
+        McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
+
+        if (mcMMOPlayer == null) {
             sender.sendMessage(LocaleLoader.getString("Profile.PendingLoad"));
             sender.sendMessage(LocaleLoader.getString("Profile.PendingLoad"));
             return true;
             return true;
         }
         }
 
 
         if (args.length == 0) {
         if (args.length == 0) {
-            Player player = (Player) sender;
-            McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
-
             boolean isLucky = Permissions.lucky(player, skill);
             boolean isLucky = Permissions.lucky(player, skill);
-            boolean hasEndurance = (PerksUtils.handleActivationPerks(player, 0, 0) != 0);
+            boolean hasEndurance = PerksUtils.handleActivationPerks(player, 0, 0) != 0;
             float skillValue = mcMMOPlayer.getSkillLevel(skill);
             float skillValue = mcMMOPlayer.getSkillLevel(skill);
 
 
             //Send the players a few blank lines to make finding the top of the skill command easier
             //Send the players a few blank lines to make finding the top of the skill command easier
-            if (AdvancedConfig.getInstance().doesSkillCommandSendBlankLines()) {
+            if (mcMMO.p.getAdvancedConfig().doesSkillCommandSendBlankLines())
                 for (int i = 0; i < 2; i++) {
                 for (int i = 0; i < 2; i++) {
                     player.sendMessage("");
                     player.sendMessage("");
                 }
                 }
-            }
 
 
             permissionsCheck(player);
             permissionsCheck(player);
             dataCalculations(player, skillValue);
             dataCalculations(player, skillValue);
@@ -104,18 +105,31 @@ public abstract class SkillCommand implements TabExecutor {
 
 
 
 
             //Link Header
             //Link Header
-            if (Config.getInstance().getUrlLinksEnabled()) {
+            if (mcMMO.p.getGeneralConfig().getUrlLinksEnabled()) {
                 player.sendMessage(LocaleLoader.getString("Overhaul.mcMMO.Header"));
                 player.sendMessage(LocaleLoader.getString("Overhaul.mcMMO.Header"));
                 TextComponentFactory.sendPlayerUrlHeader(player);
                 TextComponentFactory.sendPlayerUrlHeader(player);
             }
             }
 
 
 
 
-            if (Config.getInstance().getScoreboardsEnabled() && Config.getInstance().getSkillUseBoard()) {
+            if (mcMMO.p.getGeneralConfig().getScoreboardsEnabled() && mcMMO.p.getGeneralConfig().getSkillUseBoard()) {
                 ScoreboardManager.enablePlayerSkillScoreboard(player, skill);
                 ScoreboardManager.enablePlayerSkillScoreboard(player, skill);
             }
             }
 
 
+            return true;
+        } else if ("keep".equals(args[0].toLowerCase())) {
+            if (!mcMMO.p.getGeneralConfig().getAllowKeepBoard()
+                    || !mcMMO.p.getGeneralConfig().getScoreboardsEnabled()
+                    || !mcMMO.p.getGeneralConfig().getSkillUseBoard()) {
+                sender.sendMessage(LocaleLoader.getString("Commands.Disabled"));
+                return true;
+            }
+
+            ScoreboardManager.enablePlayerSkillScoreboard(player, skill);
+            ScoreboardManager.keepBoard(sender.getName());
+            sender.sendMessage(LocaleLoader.getString("Commands.Scoreboard.Keep"));
             return true;
             return true;
         }
         }
+
         return skillGuideCommand.onCommand(sender, command, label, args);
         return skillGuideCommand.onCommand(sender, command, label, args);
     }
     }
 
 
@@ -134,13 +148,14 @@ public abstract class SkillCommand implements TabExecutor {
     }
     }
 
 
     private void sendSkillCommandHeader(Player player, McMMOPlayer mcMMOPlayer, int skillValue) {
     private void sendSkillCommandHeader(Player player, McMMOPlayer mcMMOPlayer, int skillValue) {
-//        ChatColor hd1 = ChatColor.DARK_AQUA;
-//        ChatColor c1 = ChatColor.GOLD;
-//        ChatColor c2 = ChatColor.RED;
+        ChatColor hd1 = ChatColor.DARK_AQUA;
+        ChatColor c1 = ChatColor.GOLD;
+        ChatColor c2 = ChatColor.RED;
+
 
 
         player.sendMessage(LocaleLoader.getString("Skills.Overhaul.Header", skillName));
         player.sendMessage(LocaleLoader.getString("Skills.Overhaul.Header", skillName));
 
 
-        if(!skill.isChildSkill())
+        if(!SkillTools.isChildSkill(skill))
         {
         {
             /*
             /*
              * NON-CHILD SKILLS
              * NON-CHILD SKILLS
@@ -170,10 +185,10 @@ public abstract class SkillCommand implements TabExecutor {
             {
             {
                 if(i+1 < parentList.size())
                 if(i+1 < parentList.size())
                 {
                 {
-                    parentMessage.append(LocaleLoader.getString("Effects.Child.ParentList", parentList.get(i).getName(), mcMMOPlayer.getSkillLevel(parentList.get(i))));
+                    parentMessage.append(LocaleLoader.getString("Effects.Child.ParentList", mcMMO.p.getSkillTools().getLocalizedSkillName(parentList.get(i)), mcMMOPlayer.getSkillLevel(parentList.get(i))));
                     parentMessage.append(ChatColor.GRAY).append(", ");
                     parentMessage.append(ChatColor.GRAY).append(", ");
                 } else {
                 } else {
-                    parentMessage.append(LocaleLoader.getString("Effects.Child.ParentList", parentList.get(i).getName(), mcMMOPlayer.getSkillLevel(parentList.get(i))));
+                    parentMessage.append(LocaleLoader.getString("Effects.Child.ParentList", mcMMO.p.getSkillTools().getLocalizedSkillName(parentList.get(i)), mcMMOPlayer.getSkillLevel(parentList.get(i))));
                 }
                 }
             }
             }
 
 
@@ -186,7 +201,7 @@ public abstract class SkillCommand implements TabExecutor {
 
 
         }
         }
         /*
         /*
-        if (!skill.isChildSkill()) {
+        if (!SkillTools.isChildSkill(skill)) {
             player.sendMessage(LocaleLoader.getString("Skills.Header", skillName));
             player.sendMessage(LocaleLoader.getString("Skills.Header", skillName));
             player.sendMessage(LocaleLoader.getString("Commands.XPGain", LocaleLoader.getString("Commands.XPGain." + StringUtils.getCapitalized(skill.toString()))));
             player.sendMessage(LocaleLoader.getString("Commands.XPGain", LocaleLoader.getString("Commands.XPGain." + StringUtils.getCapitalized(skill.toString()))));
             player.sendMessage(LocaleLoader.getString("Effects.Level", skillValue, mcMMOPlayer.getSkillXpLevel(skill), mcMMOPlayer.getXpToLevel(skill)));
             player.sendMessage(LocaleLoader.getString("Effects.Level", skillValue, mcMMOPlayer.getSkillXpLevel(skill), mcMMOPlayer.getXpToLevel(skill)));
@@ -208,7 +223,7 @@ public abstract class SkillCommand implements TabExecutor {
     @Override
     @Override
     public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
     public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
         if (args.length == 1) {
         if (args.length == 1) {
-            return ImmutableList.of("?");
+            return ImmutableList.of("?", "keep");
         }
         }
         return ImmutableList.of();
         return ImmutableList.of();
     }
     }
@@ -217,10 +232,14 @@ 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[] calculateLengthDisplayValues(Player player, float skillValue) {
     protected String[] calculateLengthDisplayValues(Player player, float skillValue) {
-        int maxLength = skill.getAbility().getMaxLength();
-        int abilityLengthVar = AdvancedConfig.getInstance().getAbilityLength();
-        int abilityLengthCap = AdvancedConfig.getInstance().getAbilityLengthCap();
+        int maxLength = mcMMO.p.getSkillTools().getSuperAbilityMaxLength(mcMMO.p.getSkillTools().getSuperAbility(skill));
+        int abilityLengthVar = mcMMO.p.getAdvancedConfig().getAbilityLength();
+        int abilityLengthCap = mcMMO.p.getAdvancedConfig().getAbilityLengthCap();
 
 
         int length;
         int length;
 
 
@@ -261,7 +280,7 @@ public abstract class SkillCommand implements TabExecutor {
     }
     }
 
 
     protected String getLimitBreakDescriptionParameter() {
     protected String getLimitBreakDescriptionParameter() {
-        if(AdvancedConfig.getInstance().canApplyLimitBreakPVE()) {
+        if(mcMMO.p.getAdvancedConfig().canApplyLimitBreakPVE()) {
             return "(PVP/PVE)";
             return "(PVP/PVE)";
         } else {
         } else {
             return "(PVP)";
             return "(PVP)";
@@ -278,4 +297,14 @@ 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);
+    }
 }
 }

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

@@ -2,6 +2,7 @@ package com.gmail.nossr50.commands.skills;
 
 
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.StringUtils;
 import org.bukkit.command.Command;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandExecutor;
 import org.bukkit.command.CommandExecutor;
@@ -18,7 +19,7 @@ public class SkillGuideCommand implements CommandExecutor {
     private final String invalidPage = LocaleLoader.getString("Guides.Page.Invalid");
     private final String invalidPage = LocaleLoader.getString("Guides.Page.Invalid");
 
 
     public SkillGuideCommand(PrimarySkillType skill) {
     public SkillGuideCommand(PrimarySkillType skill) {
-        header = LocaleLoader.getString("Guides.Header", skill.getName());
+        header = LocaleLoader.getString("Guides.Header", mcMMO.p.getSkillTools().getLocalizedSkillName(skill));
         guide = getGuide(skill);
         guide = getGuide(skill);
     }
     }
 
 

+ 32 - 25
src/main/java/com/gmail/nossr50/commands/skills/SwordsCommand.java

@@ -1,14 +1,15 @@
 package com.gmail.nossr50.commands.skills;
 package com.gmail.nossr50.commands.skills;
 
 
-import com.gmail.nossr50.config.AdvancedConfig;
 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;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+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.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;
+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;
@@ -19,15 +20,16 @@ import java.util.List;
 public class SwordsCommand extends SkillCommand {
 public class SwordsCommand extends SkillCommand {
     private String counterChance;
     private String counterChance;
     private String counterChanceLucky;
     private String counterChanceLucky;
-    private int bleedLength;
-    private String bleedChance;
-    private String bleedChanceLucky;
     private String serratedStrikesLength;
     private String serratedStrikesLength;
     private String serratedStrikesLengthEndurance;
     private String serratedStrikesLengthEndurance;
 
 
+    private String rupturePureTickDamageAgainstPlayers, rupturePureTickDamageAgainstMobs,
+            ruptureExplosionDamageAgainstPlayers, ruptureExplosionDamageAgainstMobs,
+            ruptureLengthSecondsAgainstPlayers, ruptureLengthSecondsAgainstMobs, ruptureChanceToApply, ruptureChanceToApplyLucky;
+
     private boolean canCounter;
     private boolean canCounter;
     private boolean canSerratedStrike;
     private boolean canSerratedStrike;
-    private boolean canBleed;
+    private boolean canRupture;
 
 
     public SwordsCommand() {
     public SwordsCommand() {
         super(PrimarySkillType.SWORDS);
         super(PrimarySkillType.SWORDS);
@@ -43,12 +45,19 @@ public class SwordsCommand extends SkillCommand {
         }
         }
 
 
         // SWORDS_RUPTURE
         // SWORDS_RUPTURE
-        if (canBleed) {
-            bleedLength = UserManager.getPlayer(player).getSwordsManager().getRuptureBleedTicks();
+        if (canRupture) {
+            int ruptureRank = RankUtils.getRank(player, SubSkillType.SWORDS_RUPTURE);
+            ruptureLengthSecondsAgainstPlayers = String.valueOf(mcMMO.p.getAdvancedConfig().getRuptureDurationSeconds(true));
+            ruptureLengthSecondsAgainstMobs = String.valueOf(mcMMO.p.getAdvancedConfig().getRuptureDurationSeconds(false));
+
+            rupturePureTickDamageAgainstPlayers = String.valueOf(mcMMO.p.getAdvancedConfig().getRuptureTickDamage(true, ruptureRank));
+            rupturePureTickDamageAgainstMobs = String.valueOf(mcMMO.p.getAdvancedConfig().getRuptureTickDamage(false, ruptureRank));
 
 
-            String[] bleedStrings = SkillUtils.getRNGDisplayValues(player, SubSkillType.SWORDS_RUPTURE);
-            bleedChance = bleedStrings[0];
-            bleedChanceLucky = bleedStrings[1];
+            ruptureExplosionDamageAgainstPlayers = String.valueOf(mcMMO.p.getAdvancedConfig().getRuptureExplosionDamage(true, ruptureRank));
+            ruptureExplosionDamageAgainstMobs = String.valueOf(mcMMO.p.getAdvancedConfig().getRuptureExplosionDamage(false, ruptureRank));
+
+            ruptureChanceToApply = String.valueOf(mcMMO.p.getAdvancedConfig().getRuptureChanceToApplyOnHit(ruptureRank) + "%");
+            ruptureChanceToApplyLucky = String.valueOf(mcMMO.p.getAdvancedConfig().getRuptureChanceToApplyOnHit(ruptureRank) * 1.33);
         }
         }
         
         
         // SERRATED STRIKES
         // SERRATED STRIKES
@@ -61,8 +70,8 @@ public class SwordsCommand extends SkillCommand {
 
 
     @Override
     @Override
     protected void permissionsCheck(Player player) {
     protected void permissionsCheck(Player player) {
-        canBleed = Permissions.canUseSubSkill(player, SubSkillType.SWORDS_RUPTURE);
-        canCounter = Permissions.canUseSubSkill(player, SubSkillType.SWORDS_COUNTER_ATTACK);
+        canRupture = canUseSubskill(player, SubSkillType.SWORDS_RUPTURE);
+        canCounter = 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);
     }
     }
 
 
@@ -70,24 +79,22 @@ public class SwordsCommand extends SkillCommand {
     protected List<String> statsDisplay(Player player, float skillValue, boolean hasEndurance, boolean isLucky) {
     protected List<String> statsDisplay(Player player, float skillValue, boolean hasEndurance, boolean isLucky) {
         List<String> messages = new ArrayList<>();
         List<String> messages = new ArrayList<>();
 
 
-        int ruptureTicks = UserManager.getPlayer(player).getSwordsManager().getRuptureBleedTicks();
-        double ruptureDamagePlayers =  RankUtils.getRank(player, SubSkillType.SWORDS_RUPTURE) >= 3 ? AdvancedConfig.getInstance().getRuptureDamagePlayer() * 1.5D : AdvancedConfig.getInstance().getRuptureDamagePlayer();
-        double ruptureDamageMobs =  RankUtils.getRank(player, SubSkillType.SWORDS_RUPTURE) >= 3 ? AdvancedConfig.getInstance().getRuptureDamageMobs() * 1.5D : AdvancedConfig.getInstance().getRuptureDamageMobs();
-
         if (canCounter) {
         if (canCounter) {
             messages.add(getStatMessage(SubSkillType.SWORDS_COUNTER_ATTACK, counterChance)
             messages.add(getStatMessage(SubSkillType.SWORDS_COUNTER_ATTACK, counterChance)
                     + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", counterChanceLucky) : ""));
                     + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", counterChanceLucky) : ""));
         }
         }
 
 
-        if (canBleed) {
-            messages.add(getStatMessage(SubSkillType.SWORDS_RUPTURE, bleedChance)
-                    + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", bleedChanceLucky) : ""));
+        if (canRupture) {
+            messages.add(getStatMessage(SubSkillType.SWORDS_RUPTURE, ruptureChanceToApply)
+                    + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", ruptureChanceToApplyLucky) : ""));
             messages.add(getStatMessage(true, true, SubSkillType.SWORDS_RUPTURE,
             messages.add(getStatMessage(true, true, SubSkillType.SWORDS_RUPTURE,
-                    String.valueOf(ruptureTicks),
-                    String.valueOf(ruptureDamagePlayers),
-                    String.valueOf(ruptureDamageMobs)));
+                    ruptureLengthSecondsAgainstPlayers,
+                    ruptureLengthSecondsAgainstMobs));
+
+            messages.add(LocaleLoader.getString("Swords.SubSkill.Rupture.Stat.TickDamage", rupturePureTickDamageAgainstPlayers, rupturePureTickDamageAgainstMobs));
+//            messages.add(LocaleLoader.getString("Swords.SubSkill.Rupture.Stat.ExplosionDamage", ruptureExplosionDamageAgainstPlayers, ruptureExplosionDamageAgainstMobs));
 
 
-            messages.add(LocaleLoader.getString("Swords.Combat.Rupture.Note"));
+            messages.add(LocaleLoader.getString("Swords.Combat.Rupture.Note.Update.One"));
         }
         }
 
 
         if (canSerratedStrike) {
         if (canSerratedStrike) {
@@ -95,13 +102,13 @@ public class SwordsCommand extends SkillCommand {
                     + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", serratedStrikesLengthEndurance) : ""));
                     + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", serratedStrikesLengthEndurance) : ""));
         }
         }
 
 
-        if(Permissions.canUseSubSkill(player, SubSkillType.SWORDS_STAB))
+        if(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(Permissions.canUseSubSkill(player, SubSkillType.SWORDS_SWORDS_LIMIT_BREAK)) {
+        if(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))));
         }
         }

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

@@ -3,6 +3,7 @@ package com.gmail.nossr50.commands.skills;
 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;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
 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;
@@ -26,9 +27,6 @@ public class WoodcuttingCommand extends SkillCommand {
     private boolean canDoubleDrop;
     private boolean canDoubleDrop;
     private boolean canTripleDrop;
     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);
@@ -47,7 +45,7 @@ public class WoodcuttingCommand extends SkillCommand {
             tripleDropChance = tripleDropStrings[0];
             tripleDropChance = tripleDropStrings[0];
             tripleDropChanceLucky = tripleDropStrings[1];
             tripleDropChanceLucky = tripleDropStrings[1];
         }
         }
-        
+
         // TREE FELLER
         // TREE FELLER
         if (canTreeFell) {
         if (canTreeFell) {
             String[] treeFellerStrings = calculateLengthDisplayValues(player, skillValue);
             String[] treeFellerStrings = calculateLengthDisplayValues(player, skillValue);
@@ -65,13 +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 = Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_HARVEST_LUMBER) && !skill.getDoubleDropsDisabled();
-        canTripleDrop = Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_CLEAN_CUTS) && !skill.getDoubleDropsDisabled();
+        canDoubleDrop = !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill)
+                && Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_HARVEST_LUMBER)
+                && RankUtils.getRank(player, SubSkillType.WOODCUTTING_HARVEST_LUMBER) >= 1;
+        canTripleDrop = !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill) && Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_CLEAN_CUTS);
         canLeafBlow = Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_LEAF_BLOWER);
         canLeafBlow = Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_LEAF_BLOWER);
         canKnockOnWood = canTreeFell && Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD);
         canKnockOnWood = canTreeFell && Permissions.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);*/
     }
     }
 
 
     @Override
     @Override

+ 291 - 172
src/main/java/com/gmail/nossr50/config/AdvancedConfig.java

@@ -6,23 +6,20 @@ import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import net.md_5.bungee.api.ChatColor;
 import net.md_5.bungee.api.ChatColor;
 
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
 
 
-public class AdvancedConfig extends AutoUpdateConfigLoader {
-    private static AdvancedConfig instance;
+public class AdvancedConfig extends BukkitConfig {
 
 
-    private AdvancedConfig() {
-        super("advanced.yml");
+    public AdvancedConfig(File dataFolder) {
+        super("advanced.yml", dataFolder);
         validate();
         validate();
     }
     }
 
 
-    public static AdvancedConfig getInstance() {
-        if (instance == null) {
-            instance = new AdvancedConfig();
-        }
-
-        return instance;
+    @Override
+    public void initDefaults() {
+        config.addDefault("Skills.General.StartingLevel", 0);
     }
     }
 
 
     @Override
     @Override
@@ -68,15 +65,6 @@ public class AdvancedConfig extends AutoUpdateConfigLoader {
             reason.add("Skills.Acrobatics.GracefulRoll.DamageThreshold should be at least 0!");
             reason.add("Skills.Acrobatics.GracefulRoll.DamageThreshold should be at least 0!");
         }
         }
 
 
-        /* ALCHEMY */
-        /*if (getCatalysisUnlockLevel() < 0) {
-            reason.add("Skills.Alchemy.Catalysis.UnlockLevel should be at least 0!");
-        }
-
-        if (getCatalysisMaxBonusLevel() <= getCatalysisUnlockLevel()) {
-            reason.add("Skills.Alchemy.Catalysis.MaxBonusLevel should be greater than Skills.Alchemy.Catalysis.UnlockLevel!");
-        }*/
-
         if (getCatalysisMinSpeed() <= 0) {
         if (getCatalysisMinSpeed() <= 0) {
             reason.add("Skills.Alchemy.Catalysis.MinSpeed must be greater than 0!");
             reason.add("Skills.Alchemy.Catalysis.MinSpeed must be greater than 0!");
         }
         }
@@ -85,21 +73,6 @@ public class AdvancedConfig extends AutoUpdateConfigLoader {
             reason.add("Skills.Alchemy.Catalysis.MaxSpeed should be at least Skills.Alchemy.Catalysis.MinSpeed!");
             reason.add("Skills.Alchemy.Catalysis.MaxSpeed should be at least Skills.Alchemy.Catalysis.MinSpeed!");
         }
         }
 
 
-        /*List<Alchemy.Tier> alchemyTierList = Arrays.asList(Alchemy.Tier.values());
-        for (Alchemy.Tier tier : alchemyTierList) {
-            if (getConcoctionsTierLevel(tier) < 0) {
-                reason.add("Skills.Alchemy.Rank_Levels.Rank_" + rank + " should be at least 0!");
-            }
-
-            if (tier != Alchemy.Tier.fromNumerical(Alchemy.Tier.values().length)) {
-                Alchemy.Tier nextTier = alchemyTierList.get(alchemyTierList.indexOf(tier) - 1);
-
-                if (getConcoctionsTierLevel(tier) > getConcoctionsTierLevel(nextTier)) {
-                    reason.add("Skills.Alchemy.Rank_Levels.Rank_" + rank + " should be less than or equal to Skills.Alchemy.Rank_Levels.Rank_" + nextrank + "!");
-                }
-            }
-        }*/
-
         /* ARCHERY */
         /* ARCHERY */
 
 
         if (getSkillShotRankDamageMultiplier() <= 0) {
         if (getSkillShotRankDamageMultiplier() <= 0) {
@@ -131,8 +104,7 @@ public class AdvancedConfig extends AutoUpdateConfigLoader {
         }
         }
 
 
         /* AXES */
         /* AXES */
-        if(getAxeMasteryRankDamageMultiplier() < 0)
-        {
+        if (getAxeMasteryRankDamageMultiplier() < 0) {
             reason.add("Skills.Axes.AxeMastery.RankDamageMultiplier should be at least 0!");
             reason.add("Skills.Axes.AxeMastery.RankDamageMultiplier should be at least 0!");
         }
         }
 
 
@@ -310,25 +282,6 @@ public class AdvancedConfig extends AutoUpdateConfigLoader {
         }
         }
 
 
         /* SWORDS */
         /* SWORDS */
-        if (getMaximumProbability(SubSkillType.SWORDS_RUPTURE) < 1) {
-            reason.add("Skills.Swords.Rupture.ChanceMax should be at least 1!");
-        }
-
-        if (getMaxBonusLevel(SubSkillType.SWORDS_RUPTURE) < 1) {
-            reason.add("Skills.Swords.Rupture.MaxBonusLevel should be at least 1!");
-        }
-
-        if (getRuptureMaxTicks() < 1) {
-            reason.add("Skills.Swords.Rupture.MaxTicks should be at least 1!");
-        }
-
-        if (getRuptureMaxTicks() < getRuptureBaseTicks()) {
-            reason.add("Skills.Swords.Rupture.MaxTicks should be at least Skills.Swords.Rupture.BaseTicks!");
-        }
-
-        if (getRuptureBaseTicks() < 1) {
-            reason.add("Skills.Swords.Rupture.BaseTicks should be at least 1!");
-        }
 
 
         if (getMaximumProbability(SubSkillType.SWORDS_COUNTER_ATTACK) < 1) {
         if (getMaximumProbability(SubSkillType.SWORDS_COUNTER_ATTACK) < 1) {
             reason.add("Skills.Swords.CounterAttack.ChanceMax should be at least 1!");
             reason.add("Skills.Swords.CounterAttack.ChanceMax should be at least 1!");
@@ -455,12 +408,18 @@ public class AdvancedConfig extends AutoUpdateConfigLoader {
     }
     }
 
 
     @Override
     @Override
-    protected void loadKeys() {}
+    protected void loadKeys() {
+    }
 
 
     /* GENERAL */
     /* GENERAL */
 
 
-    public boolean canApplyLimitBreakPVE() { return config.getBoolean("Skills.General.LimitBreak.AllowPVE", false); }
-    public int getStartingLevel() { return config.getInt("Skills.General.StartingLevel", 1); }
+    public boolean canApplyLimitBreakPVE() {
+        return config.getBoolean("Skills.General.LimitBreak.AllowPVE", false);
+    }
+
+    public int getStartingLevel() {
+        return config.getInt("Skills.General.StartingLevel", 1);
+    }
 
 
     public boolean allowPlayerTips() {
     public boolean allowPlayerTips() {
         return config.getBoolean("Feedback.PlayerTips", true);
         return config.getBoolean("Feedback.PlayerTips", true);
@@ -469,10 +428,11 @@ public class AdvancedConfig extends AutoUpdateConfigLoader {
     /**
     /**
      * This returns the maximum level at which superabilities will stop lengthening from scaling alongside skill level.
      * This returns the maximum level at which superabilities will stop lengthening from scaling alongside skill level.
      * It returns a different value depending on whether or not the server is in retro mode
      * It returns a different value depending on whether or not the server is in retro mode
+     *
      * @return the level at which abilities stop increasing in length
      * @return the level at which abilities stop increasing in length
      */
      */
     public int getAbilityLengthCap() {
     public int getAbilityLengthCap() {
-        if(!mcMMO.isRetroModeEnabled())
+        if (!mcMMO.isRetroModeEnabled())
             return config.getInt("Skills.General.Ability.Length.Standard.CapLevel", 50);
             return config.getInt("Skills.General.Ability.Length.Standard.CapLevel", 50);
         else
         else
             return config.getInt("Skills.General.Ability.Length.RetroMode.CapLevel", 500);
             return config.getInt("Skills.General.Ability.Length.RetroMode.CapLevel", 500);
@@ -481,27 +441,32 @@ public class AdvancedConfig extends AutoUpdateConfigLoader {
     /**
     /**
      * This returns the frequency at which abilities will increase in length
      * This returns the frequency at which abilities will increase in length
      * It returns a different value depending on whether or not the server is in retro mode
      * It returns a different value depending on whether or not the server is in retro mode
+     *
      * @return the number of levels required per ability length increase
      * @return the number of levels required per ability length increase
      */
      */
     public int getAbilityLength() {
     public int getAbilityLength() {
-        if(!mcMMO.isRetroModeEnabled())
+        if (!mcMMO.isRetroModeEnabled())
             return config.getInt("Skills.General.Ability.Length.Standard.IncreaseLevel", 5);
             return config.getInt("Skills.General.Ability.Length.Standard.IncreaseLevel", 5);
         else
         else
             return config.getInt("Skills.General.Ability.Length.RetroMode.IncreaseLevel", 50);
             return config.getInt("Skills.General.Ability.Length.RetroMode.IncreaseLevel", 50);
     }
     }
 
 
-    public int getEnchantBuff() { return config.getInt("Skills.General.Ability.EnchantBuff", 5); }
+    public int getEnchantBuff() {
+        return config.getInt("Skills.General.Ability.EnchantBuff", 5);
+    }
 
 
     /**
     /**
      * Grabs the max bonus level for a skill used in RNG calculations
      * Grabs the max bonus level for a skill used in RNG calculations
      * All max level values in the config are multiplied by 10 if the server is in retro mode as the values in the config are based around the new 1-100 skill system scaling
      * All max level values in the config are multiplied by 10 if the server is in retro mode as the values in the config are based around the new 1-100 skill system scaling
      * A value of 10 in the file will be returned as 100 for retro mode servers to accommodate the change in scaling
      * A value of 10 in the file will be returned as 100 for retro mode servers to accommodate the change in scaling
+     *
      * @param subSkillType target subskill
      * @param subSkillType target subskill
+     *
      * @return the level at which this skills max benefits will be reached on the curve
      * @return the level at which this skills max benefits will be reached on the curve
      */
      */
     public int getMaxBonusLevel(SubSkillType subSkillType) {
     public int getMaxBonusLevel(SubSkillType subSkillType) {
         String keyPath = subSkillType.getAdvConfigAddress() + ".MaxBonusLevel.";
         String keyPath = subSkillType.getAdvConfigAddress() + ".MaxBonusLevel.";
-        return mcMMO.isRetroModeEnabled() ? config.getInt(keyPath+"RetroMode", 1000) : config.getInt(keyPath+"Standard", 100);
+        return mcMMO.isRetroModeEnabled() ? config.getInt(keyPath + "RetroMode", 1000) : config.getInt(keyPath + "Standard", 100);
     }
     }
 
 
     public int getMaxBonusLevel(AbstractSubSkill abstractSubSkill) {
     public int getMaxBonusLevel(AbstractSubSkill abstractSubSkill) {
@@ -513,35 +478,29 @@ public class AdvancedConfig extends AutoUpdateConfigLoader {
         return config.getDouble(subSkillType.getAdvConfigAddress() + ".ChanceMax", 100.0D);
         return config.getDouble(subSkillType.getAdvConfigAddress() + ".ChanceMax", 100.0D);
     }
     }
 
 
-    public double getMaximumProbability(AbstractSubSkill abstractSubSkill)
-    {
+    public double getMaximumProbability(AbstractSubSkill abstractSubSkill) {
         return getMaximumProbability(abstractSubSkill.getSubSkillType());
         return getMaximumProbability(abstractSubSkill.getSubSkillType());
     }
     }
 
 
     /* Notification Settings */
     /* Notification Settings */
 
 
-    public boolean doesSkillCommandSendBlankLines()
-    {
+    public boolean doesSkillCommandSendBlankLines() {
         return config.getBoolean("Feedback.SkillCommand.BlankLinesAboveHeader", true);
         return config.getBoolean("Feedback.SkillCommand.BlankLinesAboveHeader", true);
     }
     }
 
 
-    public boolean doesNotificationUseActionBar(NotificationType notificationType)
-    {
-        return config.getBoolean("Feedback.ActionBarNotifications."+notificationType.toString()+".Enabled", true);
+    public boolean doesNotificationUseActionBar(NotificationType notificationType) {
+        return config.getBoolean("Feedback.ActionBarNotifications." + notificationType.toString() + ".Enabled", true);
     }
     }
 
 
-    public boolean doesNotificationSendCopyToChat(NotificationType notificationType)
-    {
-        return config.getBoolean("Feedback.ActionBarNotifications."+notificationType.toString()+".SendCopyOfMessageToChat", false);
+    public boolean doesNotificationSendCopyToChat(NotificationType notificationType) {
+        return config.getBoolean("Feedback.ActionBarNotifications." + notificationType.toString() + ".SendCopyOfMessageToChat", false);
     }
     }
 
 
-    public boolean useTitlesForXPEvent()
-    {
+    public boolean useTitlesForXPEvent() {
         return config.getBoolean("Feedback.Events.XP.SendTitles", true);
         return config.getBoolean("Feedback.Events.XP.SendTitles", true);
     }
     }
 
 
-    public boolean sendAbilityNotificationToOtherPlayers()
-    {
+    public boolean sendAbilityNotificationToOtherPlayers() {
         return config.getBoolean("Feedback.Events.AbilityActivation.SendNotificationToOtherPlayers", true);
         return config.getBoolean("Feedback.Events.AbilityActivation.SendNotificationToOtherPlayers", true);
     }
     }
 
 
@@ -560,6 +519,7 @@ public class AdvancedConfig extends AutoUpdateConfigLoader {
 
 
     /**
     /**
      * Used to color our details header in our JSON Hover Object tooltips
      * Used to color our details header in our JSON Hover Object tooltips
+     *
      * @return the ChatColor for this element
      * @return the ChatColor for this element
      */
      */
     /*public ChatColor getJSONStatHoverDetailsColor()
     /*public ChatColor getJSONStatHoverDetailsColor()
@@ -608,7 +568,6 @@ public class AdvancedConfig extends AutoUpdateConfigLoader {
     {
     {
         return getChatColor(config.getString("Style.JSON.Notification."+notificationType.toString()+".Color"));
         return getChatColor(config.getString("Style.JSON.Notification."+notificationType.toString()+".Color"));
     }*/
     }*/
-
     private ChatColor getChatColorFromKey(String keyLocation) {
     private ChatColor getChatColorFromKey(String keyLocation) {
         String colorName = config.getString(keyLocation);
         String colorName = config.getString(keyLocation);
 
 
@@ -649,174 +608,334 @@ public class AdvancedConfig extends AutoUpdateConfigLoader {
 
 
     /**
     /**
      * Some SubSkills have the ability to retain classic functionality
      * Some SubSkills have the ability to retain classic functionality
+     *
      * @param subSkillType SubSkillType with classic functionality
      * @param subSkillType SubSkillType with classic functionality
+     *
      * @return true if the subskill is in classic mode
      * @return true if the subskill is in classic mode
      */
      */
-    public boolean isSubSkillClassic(SubSkillType subSkillType)
-    {
-        return config.getBoolean(subSkillType.getAdvConfigAddress()+".Classic");
+    public boolean isSubSkillClassic(SubSkillType subSkillType) {
+        return config.getBoolean(subSkillType.getAdvConfigAddress() + ".Classic");
     }
     }
 
 
     /* ACROBATICS */
     /* ACROBATICS */
-    public double getDodgeDamageModifier() { return config.getDouble("Skills.Acrobatics.Dodge.DamageModifier", 2.0D); }
+    public double getDodgeDamageModifier() {
+        return config.getDouble("Skills.Acrobatics.Dodge.DamageModifier", 2.0D);
+    }
 
 
-    public double getRollDamageThreshold() { return config.getDouble("Skills.Acrobatics.Roll.DamageThreshold", 7.0D); }
+    public double getRollDamageThreshold() {
+        return config.getDouble("Skills.Acrobatics.Roll.DamageThreshold", 7.0D);
+    }
 
 
-    public double getGracefulRollDamageThreshold() { return config.getDouble("Skills.Acrobatics.GracefulRoll.DamageThreshold", 14.0D); }
+    public double getGracefulRollDamageThreshold() {
+        return config.getDouble("Skills.Acrobatics.GracefulRoll.DamageThreshold", 14.0D);
+    }
 
 
     /* ALCHEMY */
     /* ALCHEMY */
-    /*public int getCatalysisUnlockLevel() { return config.getInt("Skills.Alchemy.Catalysis.UnlockLevel", 100); }*/
-    public int getCatalysisMaxBonusLevel() { return config.getInt("Skills.Alchemy.Catalysis.MaxBonusLevel", 1000); }
+    public int getCatalysisMaxBonusLevel() {
+        return config.getInt("Skills.Alchemy.Catalysis.MaxBonusLevel", 1000);
+    }
 
 
-    public double getCatalysisMinSpeed() { return config.getDouble("Skills.Alchemy.Catalysis.MinSpeed", 1.0D); }
-    public double getCatalysisMaxSpeed() { return config.getDouble("Skills.Alchemy.Catalysis.MaxSpeed", 4.0D); }
+    public double getCatalysisMinSpeed() {
+        return config.getDouble("Skills.Alchemy.Catalysis.MinSpeed", 1.0D);
+    }
+
+    public double getCatalysisMaxSpeed() {
+        return config.getDouble("Skills.Alchemy.Catalysis.MaxSpeed", 4.0D);
+    }
 
 
-    //public int getConcoctionsTierLevel(Alchemy.Tier tier) { return config.getInt("Skills.Alchemy.Rank_Levels.Rank_" + rank); }
 
 
     /* ARCHERY */
     /* ARCHERY */
-    public double getSkillShotRankDamageMultiplier() { return config.getDouble("Skills.Archery.SkillShot.RankDamageMultiplier", 10.0D); }
-    public double getSkillShotDamageMax() { return config.getDouble("Skills.Archery.SkillShot.MaxDamage", 9.0D); }
+    public double getSkillShotRankDamageMultiplier() {
+        return config.getDouble("Skills.Archery.SkillShot.RankDamageMultiplier", 10.0D);
+    }
 
 
-    public double getDazeBonusDamage() { return config.getDouble("Skills.Archery.Daze.BonusDamage", 4.0D); }
+    public double getSkillShotDamageMax() {
+        return config.getDouble("Skills.Archery.SkillShot.MaxDamage", 9.0D);
+    }
+
+    public double getDazeBonusDamage() {
+        return config.getDouble("Skills.Archery.Daze.BonusDamage", 4.0D);
+    }
 
 
-    public double getForceMultiplier() { return config.getDouble("Skills.Archery.ForceMultiplier", 2.0D); }
+    public double getForceMultiplier() {
+        return config.getDouble("Skills.Archery.ForceMultiplier", 2.0D);
+    }
 
 
     /* AXES */
     /* AXES */
-    public double getAxeMasteryRankDamageMultiplier() { return config.getDouble("Skills.Axes.AxeMastery.RankDamageMultiplier", 1.0D); }
+    public double getAxeMasteryRankDamageMultiplier() {
+        return config.getDouble("Skills.Axes.AxeMastery.RankDamageMultiplier", 1.0D);
+    }
+
+    public double getCriticalStrikesPVPModifier() {
+        return config.getDouble("Skills.Axes.CriticalStrikes.PVP_Modifier", 1.5D);
+    }
+
+    public double getCriticalStrikesPVEModifier() {
+        return config.getDouble("Skills.Axes.CriticalStrikes.PVE_Modifier", 2.0D);
+    }
 
 
-    public double getCriticalStrikesPVPModifier() { return config.getDouble("Skills.Axes.CriticalStrikes.PVP_Modifier", 1.5D); }
-    public double getCriticalStrikesPVEModifier() { return config.getDouble("Skills.Axes.CriticalStrikes.PVE_Modifier", 2.0D); }
+    public double getGreaterImpactChance() {
+        return config.getDouble("Skills.Axes.GreaterImpact.Chance", 25.0D);
+    }
 
 
-    public double getGreaterImpactChance() { return config.getDouble("Skills.Axes.GreaterImpact.Chance", 25.0D); }
-    public double getGreaterImpactModifier() { return config.getDouble("Skills.Axes.GreaterImpact.KnockbackModifier", 1.5D); }
-    public double getGreaterImpactBonusDamage() { return config.getDouble("Skills.Axes.GreaterImpact.BonusDamage", 2.0D); }
+    public double getGreaterImpactModifier() {
+        return config.getDouble("Skills.Axes.GreaterImpact.KnockbackModifier", 1.5D);
+    }
 
 
-    public double getImpactChance() { return config.getDouble("Skills.Axes.ArmorImpact.Chance", 25.0D); }
-    public double getImpactDurabilityDamageMultiplier() { return config.getDouble("Skills.Axes.ArmorImpact.DamagePerRank", 6.5D); }
+    public double getGreaterImpactBonusDamage() {
+        return config.getDouble("Skills.Axes.GreaterImpact.BonusDamage", 2.0D);
+    }
 
 
-    public double getSkullSplitterModifier() { return config.getDouble("Skills.Axes.SkullSplitter.DamageModifier", 2.0D); }
+    public double getImpactChance() {
+        return config.getDouble("Skills.Axes.ArmorImpact.Chance", 25.0D);
+    }
+
+    public double getImpactDurabilityDamageMultiplier() {
+        return config.getDouble("Skills.Axes.ArmorImpact.DamagePerRank", 6.5D);
+    }
+
+    public double getSkullSplitterModifier() {
+        return config.getDouble("Skills.Axes.SkullSplitter.DamageModifier", 2.0D);
+    }
 
 
     /* EXCAVATION */
     /* EXCAVATION */
     //Nothing to configure, everything is already configurable in config.yml
     //Nothing to configure, everything is already configurable in config.yml
 
 
     /* FISHING */
     /* FISHING */
-    //public int getFishingTierLevel(int rank) { return config.getInt("Skills.Fishing.Rank_Levels.Rank_" + rank); }
-    public double getShakeChance(int rank) { return config.getDouble("Skills.Fishing.ShakeChance.Rank_" + rank); }
-    public int getFishingVanillaXPModifier(int rank) { return config.getInt("Skills.Fishing.VanillaXPMultiplier.Rank_" + rank); }
+    public double getShakeChance(int rank) {
+        return config.getDouble("Skills.Fishing.ShakeChance.Rank_" + rank);
+    }
+
+    public int getFishingVanillaXPModifier(int rank) {
+        return config.getInt("Skills.Fishing.VanillaXPMultiplier.Rank_" + rank);
+    }
+
+    public int getFishingReductionMinWaitTicks() {
+        return config.getInt("Skills.Fishing.MasterAngler.Tick_Reduction_Per_Rank.Min_Wait", 10);
+    }
+
+    public int getFishingReductionMaxWaitTicks() {
+        return config.getInt("Skills.Fishing.MasterAngler.Tick_Reduction_Per_Rank.Max_Wait", 30);
+    }
+
+    public int getFishingBoatReductionMinWaitTicks() {
+        return config.getInt("Skills.Fishing.MasterAngler.Boat_Tick_Reduction.Min_Wait", 10);
+    }
+
+    public int getFishingBoatReductionMaxWaitTicks() {
+        return config.getInt("Skills.Fishing.MasterAngler.Boat_Tick_Reduction.Max_Wait", 30);
+    }
+
+    public int getFishingReductionMinWaitCap() {
+        return config.getInt("Skills.Fishing.MasterAngler.Tick_Reduction_Caps.Min_Wait", 40);
+    }
+
+    public int getFishingReductionMaxWaitCap() {
+        return config.getInt("Skills.Fishing.MasterAngler.Tick_Reduction_Caps.Max_Wait", 100);
+    }
+
+    public int getFishermanDietRankChange() {
+        return config.getInt("Skills.Fishing.FishermansDiet.RankChange", 200);
+    }
 
 
-    public int getFishingReductionMinWaitTicks() { return config.getInt("Skills.Fishing.MasterAngler.Tick_Reduction_Per_Rank.Min_Wait", 10);}
-    public int getFishingReductionMaxWaitTicks() { return config.getInt("Skills.Fishing.MasterAngler.Tick_Reduction_Per_Rank.Max_Wait", 30);}
-    public int getFishingBoatReductionMinWaitTicks() { return config.getInt("Skills.Fishing.MasterAngler.Boat_Tick_Reduction.Min_Wait", 10);}
-    public int getFishingBoatReductionMaxWaitTicks() { return config.getInt("Skills.Fishing.MasterAngler.Boat_Tick_Reduction.Max_Wait", 30);}
-    public int getFishingReductionMinWaitCap() { return config.getInt("Skills.Fishing.MasterAngler.Tick_Reduction_Caps.Min_Wait", 40);}
-    public int getFishingReductionMaxWaitCap() { return config.getInt("Skills.Fishing.MasterAngler.Tick_Reduction_Caps.Max_Wait", 100);}
-    public int getFishermanDietRankChange() { return config.getInt("Skills.Fishing.FishermansDiet.RankChange", 200); }
 
 
-    /*public int getIceFishingUnlockLevel() { return config.getInt("Skills.Fishing.IceFishing.UnlockLevel", 50); }
+    public double getMasterAnglerBoatModifier() {
+        return config.getDouble("Skills.Fishing.MasterAngler.BoatModifier", 2.0);
+    }
 
 
-    public int getMasterAnglerUnlockLevel() {return config.getInt("Skills.Fishing.MasterAngler.UnlockLevel", 125); }*/
-    public double getMasterAnglerBoatModifier() {return config.getDouble("Skills.Fishing.MasterAngler.BoatModifier", 2.0); }
-    public double getMasterAnglerBiomeModifier() {return config.getDouble("Skills.Fishing.MasterAngler.BiomeModifier", 2.0); }
+    public double getMasterAnglerBiomeModifier() {
+        return config.getDouble("Skills.Fishing.MasterAngler.BiomeModifier", 2.0);
+    }
 
 
     /* HERBALISM */
     /* HERBALISM */
-    public int getFarmerDietRankChange() { return config.getInt("Skills.Herbalism.FarmersDiet.RankChange", 200); }
+    public int getFarmerDietRankChange() {
+        return config.getInt("Skills.Herbalism.FarmersDiet.RankChange", 200);
+    }
 
 
-    public int getGreenThumbStageChange() { return config.getInt("Skills.Herbalism.GreenThumb.StageChange", 200); }
+    public int getGreenThumbStageChange() {
+        return config.getInt("Skills.Herbalism.GreenThumb.StageChange", 200);
+    }
 
 
     /* MINING */
     /* MINING */
-    public boolean getDoubleDropSilkTouchEnabled() { return config.getBoolean("Skills.Mining.DoubleDrops.SilkTouch", true); }
-    public boolean getAllowMiningTripleDrops() { return config.getBoolean("Skills.Mining.SuperBreaker.AllowTripleDrops", true); }
-    public int getBlastMiningRankLevel(int rank) { return config.getInt("Skills.Mining.BlastMining.Rank_Levels.Rank_" + rank); }
-    public double getBlastDamageDecrease(int rank) { return config.getDouble("Skills.Mining.BlastMining.BlastDamageDecrease.Rank_" + rank); }
-    public double getOreBonus(int rank) { return config.getDouble("Skills.Mining.BlastMining.OreBonus.Rank_" + rank); }
-    public double getDebrisReduction(int rank) { return config.getDouble("Skills.Mining.BlastMining.DebrisReduction.Rank_" + rank); }
-    public int getDropMultiplier(int rank) { return config.getInt("Skills.Mining.BlastMining.DropMultiplier.Rank_" + rank); }
-    public double getBlastRadiusModifier(int rank) { return config.getDouble("Skills.Mining.BlastMining.BlastRadiusModifier.Rank_" + rank); }
+    public boolean getDoubleDropSilkTouchEnabled() {
+        return config.getBoolean("Skills.Mining.DoubleDrops.SilkTouch", true);
+    }
+
+    public boolean getAllowMiningTripleDrops() {
+        return config.getBoolean("Skills.Mining.SuperBreaker.AllowTripleDrops", true);
+    }
+
+    public int getBlastMiningRankLevel(int rank) {
+        return config.getInt("Skills.Mining.BlastMining.Rank_Levels.Rank_" + rank);
+    }
+
+    public double getBlastDamageDecrease(int rank) {
+        return config.getDouble("Skills.Mining.BlastMining.BlastDamageDecrease.Rank_" + rank);
+    }
+
+    public double getOreBonus(int rank) {
+        return config.getDouble("Skills.Mining.BlastMining.OreBonus.Rank_" + rank);
+    }
+
+    public double getDebrisReduction(int rank) {
+        return config.getDouble("Skills.Mining.BlastMining.DebrisReduction.Rank_" + rank);
+    }
+
+    public int getDropMultiplier(int rank) {
+        return config.getInt("Skills.Mining.BlastMining.DropMultiplier.Rank_" + rank);
+    }
+
+    public double getBlastRadiusModifier(int rank) {
+        return config.getDouble("Skills.Mining.BlastMining.BlastRadiusModifier.Rank_" + rank);
+    }
 
 
     /* REPAIR */
     /* REPAIR */
-    public double getRepairMasteryMaxBonus() { return config.getDouble("Skills.Repair.RepairMastery.MaxBonusPercentage", 200.0D); }
-    public int getRepairMasteryMaxLevel() { return config.getInt("Skills.Repair.RepairMastery.MaxBonusLevel", 100); }
+    public double getRepairMasteryMaxBonus() {
+        return config.getDouble("Skills.Repair.RepairMastery.MaxBonusPercentage", 200.0D);
+    }
 
 
-    /* Arcane Forging */
-    //public int getArcaneForgingRankLevel(int rank) { return config.getInt("Skills.Repair.ArcaneForging.Rank_Levels.Rank_" + rank); }
+    public int getRepairMasteryMaxLevel() {
+        return config.getInt("Skills.Repair.RepairMastery.MaxBonusLevel", 100);
+    }
 
 
-    public boolean getArcaneForgingEnchantLossEnabled() { return config.getBoolean("Skills.Repair.ArcaneForging.May_Lose_Enchants", true); }
-    public double getArcaneForgingKeepEnchantsChance(int rank) { return config.getDouble("Skills.Repair.ArcaneForging.Keep_Enchants_Chance.Rank_" + rank); }
+    public boolean getAllowEnchantedRepairMaterials() {
+        return config.getBoolean("Skills.Repair.Use_Enchanted_Materials", false);
+    }
 
 
-    public boolean getArcaneForgingDowngradeEnabled() { return config.getBoolean("Skills.Repair.ArcaneForging.Downgrades_Enabled", true); }
-    public double getArcaneForgingDowngradeChance(int rank) { return config.getDouble("Skills.Repair.ArcaneForging.Downgrades_Chance.Rank_" + rank); }
+    public boolean getArcaneForgingEnchantLossEnabled() {
+        return config.getBoolean("Skills.Repair.ArcaneForging.May_Lose_Enchants", true);
+    }
 
 
-    /* SALVAGE */
-    //public double getSalvageMaxPercentage() { return config.getDouble("Skills.Salvage.MaxPercentage", 100.0D); }
-    //public int getSalvageMaxPercentageLevel() { return config.getInt("Skills.Salvage.MaxPercentageLevel", 1000); }
+    public double getArcaneForgingKeepEnchantsChance(int rank) {
+        return config.getDouble("Skills.Repair.ArcaneForging.Keep_Enchants_Chance.Rank_" + rank);
+    }
 
 
-    public boolean getArcaneSalvageEnchantDowngradeEnabled() { return config.getBoolean("Skills.Salvage.ArcaneSalvage.EnchantDowngradeEnabled", true); }
-    public boolean getArcaneSalvageEnchantLossEnabled() { return config.getBoolean("Skills.Salvage.ArcaneSalvage.EnchantLossEnabled", true); }
+    public boolean getArcaneForgingDowngradeEnabled() {
+        return config.getBoolean("Skills.Repair.ArcaneForging.Downgrades_Enabled", true);
+    }
 
 
-    //public int getArcaneSalvageRankLevel(int rank) { return config.getInt("Skills.Salvage.ArcaneSalvage.Rank_Levels.Rank_" + rank); }
-    public double getArcaneSalvageExtractFullEnchantsChance(int rank) { return config.getDouble("Skills.Salvage.ArcaneSalvage.ExtractFullEnchant.Rank_" + rank); }
-    public double getArcaneSalvageExtractPartialEnchantsChance(int rank) { return config.getDouble("Skills.Salvage.ArcaneSalvage.ExtractPartialEnchant.Rank_" + rank); }
+    public double getArcaneForgingDowngradeChance(int rank) {
+        return config.getDouble("Skills.Repair.ArcaneForging.Downgrades_Chance.Rank_" + rank);
+    }
+
+    public boolean getArcaneSalvageEnchantDowngradeEnabled() {
+        return config.getBoolean("Skills.Salvage.ArcaneSalvage.EnchantDowngradeEnabled", true);
+    }
+
+    public boolean getArcaneSalvageEnchantLossEnabled() {
+        return config.getBoolean("Skills.Salvage.ArcaneSalvage.EnchantLossEnabled", true);
+    }
+
+    public double getArcaneSalvageExtractFullEnchantsChance(int rank) {
+        return config.getDouble("Skills.Salvage.ArcaneSalvage.ExtractFullEnchant.Rank_" + rank);
+    }
+
+    public double getArcaneSalvageExtractPartialEnchantsChance(int rank) {
+        return config.getDouble("Skills.Salvage.ArcaneSalvage.ExtractPartialEnchant.Rank_" + rank);
+    }
 
 
     /* SMELTING */
     /* SMELTING */
     public int getBurnModifierMaxLevel() {
     public int getBurnModifierMaxLevel() {
-        if(mcMMO.isRetroModeEnabled())
+        if (mcMMO.isRetroModeEnabled())
             return config.getInt("Skills.Smelting.FuelEfficiency.RetroMode.MaxBonusLevel", 1000);
             return config.getInt("Skills.Smelting.FuelEfficiency.RetroMode.MaxBonusLevel", 1000);
         else
         else
             return config.getInt("Skills.Smelting.FuelEfficiency.Standard.MaxBonusLevel", 100);
             return config.getInt("Skills.Smelting.FuelEfficiency.Standard.MaxBonusLevel", 100);
     }
     }
 
 
-    /*public int getFluxMiningUnlockLevel() { return config.getInt("Skills.Smelting.FluxMining.UnlockLevel", 250); }*/
-    public double getFluxMiningChance() { return config.getDouble("Skills.Smelting.FluxMining.Chance", 33.0D); }
+    public double getFluxMiningChance() {
+        return config.getDouble("Skills.Smelting.FluxMining.Chance", 33.0D);
+    }
 
 
-    public int getSmeltingRankLevel(int rank) { return config.getInt("Skills.Smelting.Rank_Levels.Rank_" + rank); }
+    /* SWORDS */
+    public double getRuptureTickDamage(boolean isTargetPlayer, int rank) {
+        String root = "Skills.Swords.Rupture.Rupture_Mechanics.Tick_Interval_Damage.Against_";
+        String targetType = isTargetPlayer ? "Players" : "Mobs";
+        String key = root + targetType + ".Rank_" + rank;
 
 
-    public int getSmeltingVanillaXPBoostMultiplier(int rank) { return config.getInt("Skills.Smelting.VanillaXPMultiplier.Rank_" + rank); }
+        return config.getDouble(key, 1.0D);
+    }
 
 
-    /* SWORDS */
-    public double getRuptureDamagePlayer() { return config.getDouble("Skills.Swords.Rupture.DamagePlayer", 1.0); }
-    public double getRuptureDamageMobs() { return config.getDouble("Skills.Swords.Rupture.DamageMobs", 2.0); }
+    public int getRuptureDurationSeconds(boolean isTargetPlayer) {
+        String root = "Skills.Swords.Rupture.Rupture_Mechanics.Duration_In_Seconds.Against_";
+        String targetType = isTargetPlayer ? "Players" : "Mobs";
+        return config.getInt(root + targetType, 5);
+    }
+
+    public double getRuptureExplosionDamage(boolean isTargetPlayer, int rank) {
+        String root = "Skills.Swords.Rupture.Rupture_Mechanics.Explosion_Damage.Against_";
+        String targetType = isTargetPlayer ? "Players" : "Mobs";
+        String key = root + targetType + ".Rank_" + rank;
+
+        return config.getDouble(key, 40.0D);
+    }
+
+    public double getRuptureChanceToApplyOnHit(int rank) {
+        String root = "Skills.Swords.Rupture.Rupture_Mechanics.Chance_To_Apply_On_Hit.Rank_";
+        return config.getDouble(root + rank, 33);
+    }
 
 
-    public int getRuptureMaxTicks() { return config.getInt("Skills.Swords.Rupture.MaxTicks", 8); }
-    public int getRuptureBaseTicks() { return config.getInt("Skills.Swords.Rupture.BaseTicks", 2); }
+    public double getCounterModifier() {
+        return config.getDouble("Skills.Swords.CounterAttack.DamageModifier", 2.0D);
+    }
 
 
-    public double getCounterModifier() { return config.getDouble("Skills.Swords.CounterAttack.DamageModifier", 2.0D); }
+    public double getSerratedStrikesModifier() {
+        return config.getDouble("Skills.Swords.SerratedStrikes.DamageModifier", 4.0D);
+    }
 
 
-    public double getSerratedStrikesModifier() { return config.getDouble("Skills.Swords.SerratedStrikes.DamageModifier", 4.0D); }
-    public int getSerratedStrikesTicks() { return config.getInt("Skills.Swords.SerratedStrikes.RuptureTicks", 5); }
+    public int getSerratedStrikesTicks() {
+        return config.getInt("Skills.Swords.SerratedStrikes.RuptureTicks", 5);
+    }
 
 
     /* TAMING */
     /* TAMING */
-    //public int getGoreRuptureTicks() { return config.getInt("Skills.Taming.Gore.RuptureTicks", 2); }
-    public double getGoreModifier() { return config.getDouble("Skills.Taming.Gore.Modifier", 2.0D); }
+    public double getGoreModifier() {
+        return config.getDouble("Skills.Taming.Gore.Modifier", 2.0D);
+    }
 
 
-    /*public int getFastFoodUnlock() { return config.getInt("Skills.Taming.FastFood.UnlockLevel", 50); }*/
-    public double getFastFoodChance() { return config.getDouble("Skills.Taming.FastFoodService.Chance", 50.0D); }
-    public double getPummelChance() { return config.getDouble("Skills.Taming.Pummel.Chance", 10.0D); }
+    public double getFastFoodChance() {
+        return config.getDouble("Skills.Taming.FastFoodService.Chance", 50.0D);
+    }
 
 
-    //public int getEnviromentallyAwareUnlock() { return config.getInt("Skills.Taming.EnvironmentallyAware.UnlockLevel", 100); }
+    public double getPummelChance() {
+        return config.getDouble("Skills.Taming.Pummel.Chance", 10.0D);
+    }
 
 
-    /*public int getThickFurUnlock() { return config.getInt("Skills.Taming.ThickFur.UnlockLevel", 250); }*/
-    public double getThickFurModifier() { return config.getDouble("Skills.Taming.ThickFur.Modifier", 2.0D); }
+    public double getThickFurModifier() {
+        return config.getDouble("Skills.Taming.ThickFur.Modifier", 2.0D);
+    }
 
 
-    /*public int getHolyHoundUnlock() {return config.getInt("Skills.Taming.HolyHound.UnlockLevel", 375); }*/
+    public double getShockProofModifier() {
+        return config.getDouble("Skills.Taming.ShockProof.Modifier", 6.0D);
+    }
 
 
-    /*public int getShockProofUnlock() { return config.getInt("Skills.Taming.ShockProof.UnlockLevel", 500); }*/
-    public double getShockProofModifier() { return config.getDouble("Skills.Taming.ShockProof.Modifier", 6.0D); }
+    public double getSharpenedClawsBonus() {
+        return config.getDouble("Skills.Taming.SharpenedClaws.Bonus", 2.0D);
+    }
 
 
-    /*public int getSharpenedClawsUnlock() { return config.getInt("Skills.Taming.SharpenedClaws.UnlockLevel", 750); }*/
-    public double getSharpenedClawsBonus() { return config.getDouble("Skills.Taming.SharpenedClaws.Bonus", 2.0D); }
+    public double getMinHorseJumpStrength() {
+        return config.getDouble("Skills.Taming.CallOfTheWild.MinHorseJumpStrength", 0.7D);
+    }
 
 
-    public double getMinHorseJumpStrength() { return config.getDouble("Skills.Taming.CallOfTheWild.MinHorseJumpStrength", 0.7D); }
-    public double getMaxHorseJumpStrength() { return config.getDouble("Skills.Taming.CallOfTheWild.MaxHorseJumpStrength", 2.0D); }
+    public double getMaxHorseJumpStrength() {
+        return config.getDouble("Skills.Taming.CallOfTheWild.MaxHorseJumpStrength", 2.0D);
+    }
 
 
     /* UNARMED */
     /* UNARMED */
 
 
-    public boolean isSteelArmDamageCustom() { return config.getBoolean("Skills.Unarmed.SteelArmStyle.Damage_Override", false); }
+    public boolean isSteelArmDamageCustom() {
+        return config.getBoolean("Skills.Unarmed.SteelArmStyle.Damage_Override", false);
+    }
+
     public double getSteelArmOverride(int rank, double def) {
     public double getSteelArmOverride(int rank, double def) {
         String key = "Rank_" + rank;
         String key = "Rank_" + rank;
         return config.getDouble("Skills.Unarmed.SteelArmStyle.Override." + key, def);
         return config.getDouble("Skills.Unarmed.SteelArmStyle.Override." + key, def);
     }
     }
-    public boolean getDisarmProtected() { return config.getBoolean("Skills.Unarmed.Disarm.AntiTheft", false); }
+
+    public boolean getDisarmProtected() {
+        return config.getBoolean("Skills.Unarmed.Disarm.AntiTheft", false);
+    }
 
 
     /* WOODCUTTING */
     /* WOODCUTTING */
-    public boolean isKnockOnWoodXPOrbEnabled() { return config.getBoolean("Skills.Woodcutting.TreeFeller.Knock_On_Wood.Add_XP_Orbs_To_Drops", true); }
+    public boolean isKnockOnWoodXPOrbEnabled() {
+        return config.getBoolean("Skills.Woodcutting.TreeFeller.Knock_On_Wood.Add_XP_Orbs_To_Drops", true);
+    }
 }
 }

+ 39 - 83
src/main/java/com/gmail/nossr50/config/AutoUpdateConfigLoader.java

@@ -1,27 +1,38 @@
 package com.gmail.nossr50.config;
 package com.gmail.nossr50.config;
 
 
+import com.gmail.nossr50.mcMMO;
 import org.bukkit.configuration.file.FileConfiguration;
 import org.bukkit.configuration.file.FileConfiguration;
 import org.bukkit.configuration.file.YamlConfiguration;
 import org.bukkit.configuration.file.YamlConfiguration;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
 
 
-import java.io.*;
-import java.util.HashMap;
+import java.io.File;
+import java.io.IOException;
 import java.util.HashSet;
 import java.util.HashSet;
-import java.util.LinkedHashMap;
 import java.util.Set;
 import java.util.Set;
 
 
 public abstract class AutoUpdateConfigLoader extends ConfigLoader {
 public abstract class AutoUpdateConfigLoader extends ConfigLoader {
+    public AutoUpdateConfigLoader(String relativePath, String fileName, File dataFolder) {
+        super(relativePath, fileName, dataFolder);
+    }
+
+    public AutoUpdateConfigLoader(String fileName, File dataFolder) {
+        super(fileName, dataFolder);
+    }
+
+    @Deprecated
     public AutoUpdateConfigLoader(String relativePath, String fileName) {
     public AutoUpdateConfigLoader(String relativePath, String fileName) {
         super(relativePath, fileName);
         super(relativePath, fileName);
     }
     }
 
 
+    @Deprecated
     public AutoUpdateConfigLoader(String fileName) {
     public AutoUpdateConfigLoader(String fileName) {
         super(fileName);
         super(fileName);
     }
     }
 
 
     protected void saveConfig() {
     protected void saveConfig() {
         try {
         try {
-            plugin.getLogger().info("Saving changes to config file - "+fileName);
+            mcMMO.p.getLogger().info("Saving changes to config file - " + fileName);
+            config.options().indent(2);
             config.save(configFile);
             config.save(configFile);
         } catch (IOException e) {
         } catch (IOException e) {
             e.printStackTrace();
             e.printStackTrace();
@@ -29,119 +40,64 @@ public abstract class AutoUpdateConfigLoader extends ConfigLoader {
     }
     }
 
 
     protected @NotNull FileConfiguration getInternalConfig() {
     protected @NotNull FileConfiguration getInternalConfig() {
-        return YamlConfiguration.loadConfiguration(plugin.getResourceAsReader(fileName));
+        return YamlConfiguration.loadConfiguration(mcMMO.p.getResourceAsReader(fileName));
     }
     }
 
 
     @Override
     @Override
     protected void loadFile() {
     protected void loadFile() {
         super.loadFile();
         super.loadFile();
-        FileConfiguration internalConfig = YamlConfiguration.loadConfiguration(plugin.getResourceAsReader(fileName));
+        FileConfiguration internalConfig = YamlConfiguration.loadConfiguration(mcMMO.p.getResourceAsReader(fileName));
 
 
         Set<String> configKeys = config.getKeys(true);
         Set<String> configKeys = config.getKeys(true);
         Set<String> internalConfigKeys = internalConfig.getKeys(true);
         Set<String> internalConfigKeys = internalConfig.getKeys(true);
 
 
         boolean needSave = false;
         boolean needSave = false;
 
 
+        // keys present in current config file that are not in the template
         Set<String> oldKeys = new HashSet<>(configKeys);
         Set<String> oldKeys = new HashSet<>(configKeys);
         oldKeys.removeAll(internalConfigKeys);
         oldKeys.removeAll(internalConfigKeys);
 
 
+        if (!oldKeys.isEmpty()) {
+            mcMMO.p.debug("old key(s) in \"" + fileName + "\"");
+            for (String key : oldKeys) {
+                mcMMO.p.debug("  old-key:" + key);
+            }
+        }
+
+        // keys present in template that are not in current file
         Set<String> newKeys = new HashSet<>(internalConfigKeys);
         Set<String> newKeys = new HashSet<>(internalConfigKeys);
         newKeys.removeAll(configKeys);
         newKeys.removeAll(configKeys);
 
 
-        // Don't need a re-save if we have old keys sticking around?
-        // Would be less saving, but less... correct?
-        if (!newKeys.isEmpty() || !oldKeys.isEmpty()) {
+        if (!newKeys.isEmpty()) {
             needSave = true;
             needSave = true;
         }
         }
 
 
-        for (String key : oldKeys) {
-            plugin.debug("Detected potentially unused key: " + key);
-            //config.set(key, null);
-        }
-
         for (String key : newKeys) {
         for (String key : newKeys) {
-            plugin.debug("Adding new key: " + key + " = " + internalConfig.get(key));
+            mcMMO.p.debug("Adding new key: " + key + " = " + internalConfig.get(key));
             config.set(key, internalConfig.get(key));
             config.set(key, internalConfig.get(key));
         }
         }
 
 
         if (needSave) {
         if (needSave) {
-            // Get Bukkit's version of an acceptable config with new keys, and no old keys
-            String output = config.saveToString();
-
-            // Convert to the superior 4 space indentation
-            output = output.replace("  ", "    ");
-
-            // Rip out Bukkit's attempt to save comments at the top of the file
-            while (output.replaceAll("[//s]", "").startsWith("#")) {
-                output = output.substring(output.indexOf('\n', output.indexOf('#')) + 1);
-            }
-
-            // Read the internal config to get comments, then put them in the new one
-            try {
-                // Read internal
-                BufferedReader reader = new BufferedReader(new InputStreamReader(plugin.getResource(fileName)));
-                LinkedHashMap<String, String> comments = new LinkedHashMap<>();
-                StringBuilder temp = new StringBuilder();
-
-                String line;
-                while ((line = reader.readLine()) != null) {
-                    if (line.contains("#")) {
-                        temp.append(line).append("\n");
-                    }
-                    else if (line.contains(":")) {
-                        line = line.substring(0, line.indexOf(":") + 1);
-                        if (temp.length() > 0) {
-                            if(comments.containsKey(line)) {
-                                int index = 0;
-                                while(comments.containsKey(line + index)) {
-                                    index++;
-                                }
-                                
-                                line = line + index;
-                            }
-
-                            comments.put(line, temp.toString());
-                            temp = new StringBuilder();
-                        }
-                    }
-                }
+            // Save it
 
 
-                // Dump to the new one
-                HashMap<String, Integer> indexed = new HashMap<>();
-                for (String key : comments.keySet()) {
-                    String actualkey = key.substring(0, key.indexOf(":") + 1);
-
-                    int index = 0;
-                    if(indexed.containsKey(actualkey)) {
-                        index = indexed.get(actualkey);
-                    }
-                    boolean isAtTop = !output.contains("\n" + actualkey);
-                    index = output.indexOf((isAtTop ? "" : "\n") + actualkey, index);
-
-                    if (index >= 0) {
-                        output = output.substring(0, index) + "\n" + comments.get(key) + output.substring(isAtTop ? index : index + 1);
-                        indexed.put(actualkey, index + comments.get(key).length() + actualkey.length() + 1);
-                    }
-                }
-            }
-            catch (Exception e) {
-                e.printStackTrace();
+            if (dataFolder == null) {
+                mcMMO.p.getLogger().severe("Data folder should never be null!");
+                return;
             }
             }
 
 
-            // Save it
             try {
             try {
                 String saveName = fileName;
                 String saveName = fileName;
                 // At this stage we cannot guarantee that Config has been loaded, so we do the check directly here
                 // At this stage we cannot guarantee that Config has been loaded, so we do the check directly here
-                if (!plugin.getConfig().getBoolean("General.Config_Update_Overwrite", true)) {
+                if (!mcMMO.p.getConfig().getBoolean("General.Config_Update_Overwrite", true)) {
                     saveName += ".new";
                     saveName += ".new";
                 }
                 }
 
 
-                BufferedWriter writer = new BufferedWriter(new FileWriter(new File(plugin.getDataFolder(), saveName)));
-                writer.write(output);
-                writer.flush();
-                writer.close();
-            }
-            catch (Exception e) {
+                File newSaveFile = new File(dataFolder, saveName);
+                YamlConfiguration yamlConfiguration = config;
+                yamlConfiguration.options().indent(4);
+                yamlConfiguration.save(newSaveFile);
+
+            } catch (Exception e) {
                 e.printStackTrace();
                 e.printStackTrace();
             }
             }
         }
         }

+ 220 - 0
src/main/java/com/gmail/nossr50/config/BukkitConfig.java

@@ -0,0 +1,220 @@
+package com.gmail.nossr50.config;
+
+import com.gmail.nossr50.mcMMO;
+import org.bukkit.configuration.InvalidConfigurationException;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.*;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public abstract class BukkitConfig {
+    public static final String CONFIG_PATCH_PREFIX = "ConfigPatchVersion:";
+    public static final String CURRENT_CONFIG_PATCH_VER = "ConfigPatchVersion: 2";
+    public static final char COMMENT_PREFIX = '#';
+    protected final String fileName;
+    protected final File configFile;
+    protected YamlConfiguration config;
+    protected @NotNull
+    final File dataFolder;
+
+    public BukkitConfig(@NotNull String fileName, @NotNull File dataFolder) {
+        mcMMO.p.getLogger().info("[config] Initializing config: " + fileName);
+        this.fileName = fileName;
+        this.dataFolder = dataFolder;
+        configFile = new File(dataFolder, fileName);
+        // purgeComments(true);
+        this.config = initConfig();
+        initDefaults();
+        updateFile();
+        mcMMO.p.getLogger().info("[config] Config initialized: " + fileName);
+    }
+
+    @Deprecated
+    public BukkitConfig(@NotNull String fileName) {
+        this(fileName, mcMMO.p.getDataFolder());
+    }
+
+    /**
+     * Initialize default values for the config
+     */
+    public void initDefaults() {}
+
+    /**
+     * Update the file on the disk by copying out any new and missing defaults
+     */
+    public void updateFile() {
+        try {
+            config.save(configFile);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private YamlConfiguration initConfig() {
+        if (!configFile.exists()) {
+            mcMMO.p.getLogger().info("[config] User config file not found, copying a default config to disk: " + fileName);
+            mcMMO.p.saveResource(fileName, false);
+        }
+
+        mcMMO.p.getLogger().info("[config] Loading config from disk: " + fileName);
+        YamlConfiguration config = new YamlConfiguration();
+        config.options().indent(4);
+
+        try {
+            config.options().parseComments(true);
+        } catch (NoSuchMethodError e) {
+            //e.printStackTrace();
+            // mcMMO.p.getLogger().severe("Your Spigot/CraftBukkit API is out of date, update your server software!");
+        }
+
+        config.options().copyDefaults(true);
+
+        try {
+            config.load(configFile);
+        } catch (IOException | InvalidConfigurationException e) {
+            e.printStackTrace();
+        }
+
+        return config;
+    }
+
+    protected abstract void loadKeys();
+
+    protected boolean validateKeys() {
+        return true;
+    }
+
+    protected boolean noErrorsInConfig(List<String> issues) {
+        for (String issue : issues) {
+            mcMMO.p.getLogger().warning(issue);
+        }
+
+        return issues.isEmpty();
+    }
+
+    protected void validate() {
+        if (validateKeys()) {
+            mcMMO.p.debug("No errors found in " + fileName + "!");
+        } else {
+            mcMMO.p.getLogger().warning("Errors were found in " + fileName + "! mcMMO was disabled!");
+            mcMMO.p.getServer().getPluginManager().disablePlugin(mcMMO.p);
+            mcMMO.p.noErrorsInConfigFiles = false;
+        }
+    }
+
+    public void backup() {
+        mcMMO.p.getLogger().severe("You are using an old version of the " + fileName + " file.");
+        mcMMO.p.getLogger().severe("Your old file has been renamed to " + fileName + ".old and has been replaced by an updated version.");
+
+        configFile.renameTo(new File(configFile.getPath() + ".old"));
+
+        if (mcMMO.p.getResource(fileName) != null) {
+            mcMMO.p.saveResource(fileName, true);
+        }
+
+        mcMMO.p.getLogger().warning("Reloading " + fileName + " with new values...");
+        initConfig();
+        loadKeys();
+    }
+
+    public File getFile() {
+        return configFile;
+    }
+
+//    /**
+//     * Somewhere between December 2021-January 2022 Spigot updated their
+//     * SnakeYAML dependency/API and due to our own crappy legacy code
+//     * this introduced a very problematic bug where comments got duplicated
+//     * <p>
+//     * This method hotfixes the problem by just deleting any existing comments
+//     * it's ugly, but it gets the job done
+//     *
+//     * @param silentFail when true mcMMO will report errors during the patch process or debug information
+//     *                   the option to have it fail silently is because mcMMO wants to check files before they are parsed as a file with a zillion comments will fail to even load
+//     */
+//    private void purgeComments(boolean silentFail) {
+//        if(!configFile.exists())
+//            return;
+//
+//        int dupedLines = 0, lineCount = 0, lineCountAfter = 0;
+//        try (FileReader fileReader = new FileReader(configFile);
+//             BufferedReader bufferedReader = new BufferedReader(fileReader)) {
+//            StringBuilder stringBuilder = new StringBuilder();
+//            String line;
+//            Set<String> seenBefore = new HashSet<>();
+//
+//            stringBuilder.append(CURRENT_CONFIG_PATCH_VER).append(System.lineSeparator());
+//            boolean noPatchNeeded = false;
+//
+//            // While not at the end of the file
+//            while ((line = bufferedReader.readLine()) != null) {
+//                lineCount++;
+//
+//                if(line.startsWith(CURRENT_CONFIG_PATCH_VER)) {
+//                    noPatchNeeded = true;
+//                    break;
+//                }
+//
+//                //Older version, don't append this line
+//                if(line.startsWith(CONFIG_PATCH_PREFIX))
+//                    continue;
+//
+//                if (isFirstCharAsciiCharacter(line, COMMENT_PREFIX)) {
+//                    if(seenBefore.contains(line))
+//                        dupedLines++;
+//                    else
+//                        seenBefore.add(line);
+//
+//                    continue; //Delete the line by not appending it
+//                }
+//
+//                stringBuilder
+//                        .append(line) //Convert existing files into two-spaced format
+//                        .append(System.lineSeparator());
+//                lineCountAfter++;
+//            }
+//
+//            if(noPatchNeeded)
+//                return;
+//
+//            if(lineCount == 0 && !silentFail) {
+//                mcMMO.p.getLogger().info("[config patcher] Config line count: " + lineCount);
+//                throw new InvalidConfigurationException("[config patcher] Patching of config file resulted in an empty file, this will not be saved. Contact the mcMMO devs!");
+//            }
+//
+//            if(dupedLines > 0 && !silentFail) {
+//                mcMMO.p.getLogger().info("[config patcher] Found "+dupedLines+" duplicate comments in config file: " + configFile.getName());
+//                mcMMO.p.getLogger().info("[config patcher] Purging the duplicate comments... (Nothing is broken, this is just info used for debugging)");
+//                mcMMO.p.getLogger().info("[config patcher] Line count before: "+lineCount);
+//                mcMMO.p.getLogger().info("[config patcher] Line count after: "+lineCountAfter);
+//            }
+//
+//            // Write out the *patched* file
+//            // AKA the file without any comments
+//            try (FileWriter fileWriter = new FileWriter(configFile)) {
+//                fileWriter.write(stringBuilder.toString());
+//            }
+//        } catch (IOException | InvalidConfigurationException ex) {
+//            mcMMO.p.getLogger().severe("Failed to patch config file: " + configFile.getName());
+//            ex.printStackTrace();
+//        }
+//    }
+
+    private boolean isFirstCharAsciiCharacter(String line, char character) {
+        if(line == null || line.isEmpty()) {
+            return true;
+        }
+
+        for(Character c : line.toCharArray()) {
+            if(c.equals(' '))
+                continue;
+
+            return c.equals(character);
+        }
+
+        return false;
+    }
+}

+ 3 - 1
src/main/java/com/gmail/nossr50/config/ChatConfig.java

@@ -4,7 +4,7 @@ import com.gmail.nossr50.datatypes.chat.ChatChannel;
 import com.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.StringUtils;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
 
 
-public class ChatConfig extends AutoUpdateConfigLoader {
+public class ChatConfig extends BukkitConfig {
     private static ChatConfig instance;
     private static ChatConfig instance;
 
 
     private ChatConfig() {
     private ChatConfig() {
@@ -41,7 +41,9 @@ public class ChatConfig extends AutoUpdateConfigLoader {
 
 
     /**
     /**
      * Whether or not to use display names for players in target {@link ChatChannel}
      * Whether or not to use display names for players in target {@link ChatChannel}
+     *
      * @param chatChannel target chat channel
      * @param chatChannel target chat channel
+     *
      * @return true if display names should be used
      * @return true if display names should be used
      */
      */
     public boolean useDisplayNames(@NotNull ChatChannel chatChannel) {
     public boolean useDisplayNames(@NotNull ChatChannel chatChannel) {

+ 41 - 26
src/main/java/com/gmail/nossr50/config/ConfigLoader.java

@@ -1,43 +1,59 @@
 package com.gmail.nossr50.config;
 package com.gmail.nossr50.config;
 
 
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import org.bukkit.configuration.file.FileConfiguration;
 import org.bukkit.configuration.file.YamlConfiguration;
 import org.bukkit.configuration.file.YamlConfiguration;
+import org.jetbrains.annotations.NotNull;
 
 
 import java.io.File;
 import java.io.File;
 import java.util.List;
 import java.util.List;
 
 
 public abstract class ConfigLoader {
 public abstract class ConfigLoader {
-    protected static final mcMMO plugin = mcMMO.p;
-    protected String fileName;
     protected final File configFile;
     protected final File configFile;
-    protected FileConfiguration config;
+    protected final @NotNull File dataFolder;
+    protected String fileName;
+    protected YamlConfiguration config;
 
 
+    public ConfigLoader(String relativePath, String fileName, @NotNull File dataFolder) {
+        this.fileName = fileName;
+        this.dataFolder = dataFolder;
+        configFile = new File(dataFolder, relativePath + File.separator + fileName);
+        loadFile();
+    }
+
+    public ConfigLoader(String fileName, @NotNull File dataFolder) {
+        this.fileName = fileName;
+        this.dataFolder = dataFolder;
+        configFile = new File(dataFolder, fileName);
+        loadFile();
+    }
+
+    @Deprecated
     public ConfigLoader(String relativePath, String fileName) {
     public ConfigLoader(String relativePath, String fileName) {
         this.fileName = fileName;
         this.fileName = fileName;
-        configFile = new File(plugin.getDataFolder(), relativePath + File.separator + fileName);
+        configFile = new File(mcMMO.p.getDataFolder(), relativePath + File.separator + fileName);
+        this.dataFolder = mcMMO.p.getDataFolder();
         loadFile();
         loadFile();
     }
     }
 
 
+    @Deprecated
     public ConfigLoader(String fileName) {
     public ConfigLoader(String fileName) {
         this.fileName = fileName;
         this.fileName = fileName;
-        configFile = new File(plugin.getDataFolder(), fileName);
+        configFile = new File(mcMMO.p.getDataFolder(), fileName);
+        this.dataFolder = mcMMO.p.getDataFolder();
         loadFile();
         loadFile();
     }
     }
 
 
     protected void loadFile() {
     protected void loadFile() {
         if (!configFile.exists()) {
         if (!configFile.exists()) {
-            plugin.debug("Creating mcMMO " + fileName + " File...");
+            mcMMO.p.getLogger().info("Creating mcMMO " + fileName + " File...");
 
 
             try {
             try {
-                plugin.saveResource(fileName, false); // Normal files
+                mcMMO.p.saveResource(fileName, false); // Normal files
+            } catch (IllegalArgumentException ex) {
+                mcMMO.p.saveResource(configFile.getParentFile().getName() + File.separator + fileName, false); // Mod files
             }
             }
-            catch (IllegalArgumentException ex) {
-                plugin.saveResource(configFile.getParentFile().getName() + File.separator + fileName, false); // Mod files
-            }
-        }
-        else {
-            plugin.debug("Loading mcMMO " + fileName + " File...");
+        } else {
+            mcMMO.p.getLogger().info("Loading mcMMO " + fileName + " File...");
         }
         }
 
 
         config = YamlConfiguration.loadConfiguration(configFile);
         config = YamlConfiguration.loadConfiguration(configFile);
@@ -51,7 +67,7 @@ public abstract class ConfigLoader {
 
 
     protected boolean noErrorsInConfig(List<String> issues) {
     protected boolean noErrorsInConfig(List<String> issues) {
         for (String issue : issues) {
         for (String issue : issues) {
-            plugin.getLogger().warning(issue);
+            mcMMO.p.getLogger().warning(issue);
         }
         }
 
 
         return issues.isEmpty();
         return issues.isEmpty();
@@ -59,12 +75,11 @@ public abstract class ConfigLoader {
 
 
     protected void validate() {
     protected void validate() {
         if (validateKeys()) {
         if (validateKeys()) {
-            plugin.debug("No errors found in " + fileName + "!");
-        }
-        else {
-            plugin.getLogger().warning("Errors were found in " + fileName + "! mcMMO was disabled!");
-            plugin.getServer().getPluginManager().disablePlugin(plugin);
-            plugin.noErrorsInConfigFiles = false;
+            mcMMO.p.debug("No errors found in " + fileName + "!");
+        } else {
+            mcMMO.p.getLogger().warning("Errors were found in " + fileName + "! mcMMO was disabled!");
+            mcMMO.p.getServer().getPluginManager().disablePlugin(mcMMO.p);
+            mcMMO.p.noErrorsInConfigFiles = false;
         }
         }
     }
     }
 
 
@@ -73,16 +88,16 @@ public abstract class ConfigLoader {
     }
     }
 
 
     public void backup() {
     public void backup() {
-        plugin.getLogger().warning("You are using an old version of the " + fileName + " file.");
-        plugin.getLogger().warning("Your old file has been renamed to " + fileName + ".old and has been replaced by an updated version.");
+        mcMMO.p.getLogger().warning("You are using an old version of the " + fileName + " file.");
+        mcMMO.p.getLogger().warning("Your old file has been renamed to " + fileName + ".old and has been replaced by an updated version.");
 
 
         configFile.renameTo(new File(configFile.getPath() + ".old"));
         configFile.renameTo(new File(configFile.getPath() + ".old"));
 
 
-        if (plugin.getResource(fileName) != null) {
-            plugin.saveResource(fileName, true);
+        if (mcMMO.p.getResource(fileName) != null) {
+            mcMMO.p.saveResource(fileName, true);
         }
         }
 
 
-        plugin.getLogger().warning("Reloading " + fileName + " with new values...");
+        mcMMO.p.getLogger().warning("Reloading " + fileName + " with new values...");
         loadFile();
         loadFile();
         loadKeys();
         loadKeys();
     }
     }

+ 16 - 16
src/main/java/com/gmail/nossr50/config/CoreSkillsConfig.java

@@ -4,26 +4,24 @@ import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill;
 import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill;
 import com.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.StringUtils;
 
 
-public class CoreSkillsConfig extends AutoUpdateConfigLoader {
+public class CoreSkillsConfig extends BukkitConfig {
     private static CoreSkillsConfig instance;
     private static CoreSkillsConfig instance;
 
 
-    public CoreSkillsConfig()
-    {
+    public CoreSkillsConfig() {
         super("coreskills.yml");
         super("coreskills.yml");
         validate();
         validate();
     }
     }
 
 
-    @Override
-    protected void loadKeys() {
+    public static CoreSkillsConfig getInstance() {
+        if (instance == null)
+            instance = new CoreSkillsConfig();
 
 
+        return instance;
     }
     }
 
 
-    public static CoreSkillsConfig getInstance()
-    {
-        if(instance == null)
-            return new CoreSkillsConfig();
+    @Override
+    protected void loadKeys() {
 
 
-        return instance;
     }
     }
 
 
     @Override
     @Override
@@ -39,21 +37,23 @@ public class CoreSkillsConfig extends AutoUpdateConfigLoader {
     /**
     /**
      * Whether or not a skill is enabled
      * Whether or not a skill is enabled
      * Defaults true
      * Defaults true
+     *
      * @param abstractSubSkill SubSkill definition to check
      * @param abstractSubSkill SubSkill definition to check
+     *
      * @return true if subskill is enabled
      * @return true if subskill is enabled
      */
      */
-    public boolean isSkillEnabled(AbstractSubSkill abstractSubSkill)
-    {
-        return config.getBoolean(StringUtils.getCapitalized(abstractSubSkill.getPrimarySkill().toString())+"."+ abstractSubSkill.getConfigKeyName()+".Enabled", true);
+    public boolean isSkillEnabled(AbstractSubSkill abstractSubSkill) {
+        return config.getBoolean(StringUtils.getCapitalized(abstractSubSkill.getPrimarySkill().toString()) + "." + abstractSubSkill.getConfigKeyName() + ".Enabled", true);
     }
     }
 
 
     /**
     /**
      * Whether or not this primary skill is enabled
      * Whether or not this primary skill is enabled
+     *
      * @param primarySkillType target primary skill
      * @param primarySkillType target primary skill
+     *
      * @return true if enabled
      * @return true if enabled
      */
      */
-    public boolean isPrimarySkillEnabled(PrimarySkillType primarySkillType)
-    {
-        return config.getBoolean(StringUtils.getCapitalized(primarySkillType.toString())+".Enabled", true);
+    public boolean isPrimarySkillEnabled(PrimarySkillType primarySkillType) {
+        return config.getBoolean(StringUtils.getCapitalized(primarySkillType.toString()) + ".Enabled", true);
     }
     }
 }
 }

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

@@ -0,0 +1,1008 @@
+package com.gmail.nossr50.config;
+
+import com.gmail.nossr50.database.SQLDatabaseManager.PoolIdentifier;
+import com.gmail.nossr50.datatypes.MobHealthbarType;
+import com.gmail.nossr50.datatypes.party.PartyFeature;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
+import com.gmail.nossr50.util.text.StringUtils;
+import org.bukkit.Material;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.configuration.ConfigurationSection;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+public class GeneralConfig extends BukkitConfig {
+
+    public GeneralConfig(@NotNull File dataFolder) {
+        super("config.yml", dataFolder);
+        validate();
+    }
+
+    @Override
+    protected void loadKeys() {
+
+    }
+
+    @Override
+    protected boolean validateKeys() {
+        // Validate all the settings!
+        List<String> reason = new ArrayList<>();
+
+        /* General Settings */
+        if (getSaveInterval() <= 0) {
+            reason.add("General.Save_Interval should be greater than 0!");
+        }
+
+        /* MySQL Settings */
+        for (PoolIdentifier identifier : PoolIdentifier.values()) {
+            if (getMySQLMaxConnections(identifier) <= 0) {
+                reason.add("MySQL.Database.MaxConnections." + StringUtils.getCapitalized(identifier.toString()) + " should be greater than 0!");
+            }
+            if (getMySQLMaxPoolSize(identifier) <= 0) {
+                reason.add("MySQL.Database.MaxPoolSize." + StringUtils.getCapitalized(identifier.toString()) + " should be greater than 0!");
+            }
+        }
+
+        /* Mob Healthbar */
+        if (getMobHealthbarTime() == 0) {
+            reason.add("Mob_Healthbar.Display_Time cannot be 0! Set to -1 to disable or set a valid value.");
+        }
+
+        /* Database Purging */
+        if (getPurgeInterval() < -1) {
+            reason.add("Database_Purging.Purge_Interval should be greater than, or equal to -1!");
+        }
+
+        if (getOldUsersCutoff() != -1 && getOldUsersCutoff() <= 0) {
+            reason.add("Database_Purging.Old_User_Cutoff should be greater than 0 or -1!");
+        }
+
+        /* Hardcore Mode */
+        if (getHardcoreDeathStatPenaltyPercentage() < 0.01 || getHardcoreDeathStatPenaltyPercentage() > 100) {
+            reason.add("Hardcore.Death_Stat_Loss.Penalty_Percentage only accepts values from 0.01 to 100!");
+        }
+
+        if (getHardcoreVampirismStatLeechPercentage() < 0.01 || getHardcoreVampirismStatLeechPercentage() > 100) {
+            reason.add("Hardcore.Vampirism.Leech_Percentage only accepts values from 0.01 to 100!");
+        }
+
+        /* Items */
+        if (getChimaeraUseCost() < 1 || getChimaeraUseCost() > 64) {
+            reason.add("Items.Chimaera_Wing.Use_Cost only accepts values from 1 to 64!");
+        }
+
+        if (getChimaeraRecipeCost() < 1 || getChimaeraRecipeCost() > 9) {
+            reason.add("Items.Chimaera_Wing.Recipe_Cost only accepts values from 1 to 9!");
+        }
+
+        if (getChimaeraItem() == null) {
+            reason.add("Items.Chimaera_Wing.Item_Name is invalid!");
+        }
+
+        /* Particles */
+        if (getLevelUpEffectsTier() < 1) {
+            reason.add("Particles.LevelUp_Tier should be at least 1!");
+        }
+
+        /* PARTY SETTINGS */
+        if (getAutoPartyKickInterval() < -1) {
+            reason.add("Party.AutoKick_Interval should be at least -1!");
+        }
+
+        if (getAutoPartyKickTime() < 0) {
+            reason.add("Party.Old_Party_Member_Cutoff should be at least 0!");
+        }
+
+        if (getPartyShareBonusBase() <= 0) {
+            reason.add("Party.Sharing.ExpShare_bonus_base should be greater than 0!");
+        }
+
+        if (getPartyShareBonusIncrease() < 0) {
+            reason.add("Party.Sharing.ExpShare_bonus_increase should be at least 0!");
+        }
+
+        if (getPartyShareBonusCap() <= 0) {
+            reason.add("Party.Sharing.ExpShare_bonus_cap should be greater than 0!");
+        }
+
+        if (getPartyShareRange() <= 0) {
+            reason.add("Party.Sharing.Range should be greater than 0!");
+        }
+
+        if (getPartyXpCurveMultiplier() < 1) {
+            reason.add("Party.Leveling.Xp_Curve_Modifier should be at least 1!");
+        }
+
+        for (PartyFeature partyFeature : PartyFeature.values()) {
+            if (getPartyFeatureUnlockLevel(partyFeature) < 0) {
+                reason.add("Party.Leveling." + StringUtils.getPrettyPartyFeatureString(partyFeature).replace(" ", "") + "_UnlockLevel should be at least 0!");
+            }
+        }
+
+        /* Inspect command distance */
+        if (getInspectDistance() <= 0) {
+            reason.add("Commands.inspect.Max_Distance should be greater than 0!");
+        }
+
+        if (getTreeFellerThreshold() <= 0) {
+            reason.add("Abilities.Limits.Tree_Feller_Threshold should be greater than 0!");
+        }
+
+        if (getFishingLureModifier() < 0) {
+            reason.add("Abilities.Fishing.Lure_Modifier should be at least 0!");
+        }
+
+        if (getRepairAnvilMaterial() == null) {
+            reason.add("Skills.Repair.Anvil_Type is invalid!!");
+        }
+
+        if (getSalvageAnvilMaterial() == null) {
+            reason.add("Skills.Repair.Salvage_Anvil_Type is invalid!");
+        }
+
+        if (getRepairAnvilMaterial() == getSalvageAnvilMaterial()) {
+            reason.add("Cannot use the same item for Repair and Salvage anvils!");
+        }
+
+        return noErrorsInConfig(reason);
+    }
+
+    /*
+     * GENERAL SETTINGS
+     */
+
+    /* General Settings */
+    public boolean getIsMetricsEnabled() {
+        return config.getBoolean("Metrics.bstats", true);
+    }
+
+    //Retro mode will default the value to true if the config file doesn't contain the entry (server is from a previous mcMMO install)
+    public boolean getIsRetroMode() {
+        return config.getBoolean("General.RetroMode.Enabled", true);
+    }
+
+    public String getLocale() {
+        return config.getString("General.Locale", "en_US");
+    }
+
+    public boolean getMOTDEnabled() {
+        return config.getBoolean("General.MOTD_Enabled", true);
+    }
+
+    public boolean getShowProfileLoadedMessage() {
+        return config.getBoolean("General.Show_Profile_Loaded", true);
+    }
+
+    public boolean getDonateMessageEnabled() {
+        return config.getBoolean("Commands.mcmmo.Donate_Message", true);
+    }
+
+    public int getSaveInterval() {
+        return config.getInt("General.Save_Interval", 10);
+    }
+
+    public boolean getStatsTrackingEnabled() {
+        return config.getBoolean("General.Stats_Tracking", true);
+    }
+
+    public boolean getUpdateCheckEnabled() {
+        return config.getBoolean("General.Update_Check", true);
+    }
+
+    public boolean getPreferBeta() {
+        return config.getBoolean("General.Prefer_Beta", false);
+    }
+
+    public boolean getVerboseLoggingEnabled() {
+        return config.getBoolean("General.Verbose_Logging", false);
+    }
+
+
+    public boolean getMatchOfflinePlayers() {
+        return config.getBoolean("Commands.Generic.Match_OfflinePlayers", false);
+    }
+
+    public long getDatabasePlayerCooldown() {
+        return config.getLong("Commands.Database.Player_Cooldown", 1750);
+    }
+
+    public boolean getLevelUpSoundsEnabled() {
+        return config.getBoolean("General.LevelUp_Sounds", true);
+    }
+
+    public boolean getRefreshChunksEnabled() {
+        return config.getBoolean("General.Refresh_Chunks", false);
+    }
+
+    public boolean getMobHealthbarEnabled() {
+        return config.getBoolean("Mob_Healthbar.Enabled", true);
+    }
+
+    /* Mob Healthbar */
+    public MobHealthbarType getMobHealthbarDefault() {
+        try {
+            return MobHealthbarType.valueOf(config.getString("Mob_Healthbar.Display_Type", "HEARTS").toUpperCase(Locale.ENGLISH).trim());
+        } catch (IllegalArgumentException ex) {
+            return MobHealthbarType.HEARTS;
+        }
+    }
+
+    public int getMobHealthbarTime() {
+        return Math.max(1, config.getInt("Mob_Healthbar.Display_Time", 3));
+    }
+
+    /* Scoreboards */
+    public boolean getScoreboardsEnabled() {
+        return config.getBoolean("Scoreboard.UseScoreboards", true);
+    }
+
+    public boolean getPowerLevelTagsEnabled() {
+        return config.getBoolean("Scoreboard.Power_Level_Tags", false);
+    }
+
+    public boolean getAllowKeepBoard() {
+        return config.getBoolean("Scoreboard.Allow_Keep", true);
+    }
+
+    public int getTipsAmount() {
+        return config.getInt("Scoreboard.Tips_Amount", 5);
+    }
+
+    public boolean getShowStatsAfterLogin() {
+        return config.getBoolean("Scoreboard.Show_Stats_After_Login", false);
+    }
+
+    public boolean getScoreboardRainbows() {
+        return config.getBoolean("Scoreboard.Rainbows", false);
+    }
+
+    public boolean getShowAbilityNames() {
+        return config.getBoolean("Scoreboard.Ability_Names", true);
+    }
+
+    public boolean getRankUseChat() {
+        return config.getBoolean("Scoreboard.Types.Rank.Print", false);
+    }
+
+    public boolean getRankUseBoard() {
+        return config.getBoolean("Scoreboard.Types.Rank.Board", true);
+    }
+
+    public int getRankScoreboardTime() {
+        return config.getInt("Scoreboard.Types.Rank.Display_Time", 10);
+    }
+
+    public boolean getTopUseChat() {
+        return config.getBoolean("Scoreboard.Types.Top.Print", true);
+    }
+
+    public boolean getTopUseBoard() {
+        return config.getBoolean("Scoreboard.Types.Top.Board", true);
+    }
+
+    public int getTopScoreboardTime() {
+        return config.getInt("Scoreboard.Types.Top.Display_Time", 15);
+    }
+
+    public boolean getStatsUseChat() {
+        return config.getBoolean("Scoreboard.Types.Stats.Print", true);
+    }
+
+    public boolean getStatsUseBoard() {
+        return config.getBoolean("Scoreboard.Types.Stats.Board", true);
+    }
+
+    public int getStatsScoreboardTime() {
+        return config.getInt("Scoreboard.Types.Stats.Display_Time", 10);
+    }
+
+    public boolean getInspectUseChat() {
+        return config.getBoolean("Scoreboard.Types.Inspect.Print", true);
+    }
+
+    public boolean getInspectUseBoard() {
+        return config.getBoolean("Scoreboard.Types.Inspect.Board", true);
+    }
+
+    public int getInspectScoreboardTime() {
+        return config.getInt("Scoreboard.Types.Inspect.Display_Time", 25);
+    }
+
+    public boolean getCooldownUseChat() {
+        return config.getBoolean("Scoreboard.Types.Cooldown.Print", false);
+    }
+
+    public boolean getCooldownUseBoard() {
+        return config.getBoolean("Scoreboard.Types.Cooldown.Board", true);
+    }
+
+    public int getCooldownScoreboardTime() {
+        return config.getInt("Scoreboard.Types.Cooldown.Display_Time", 41);
+    }
+
+    public boolean getSkillUseBoard() {
+        return config.getBoolean("Scoreboard.Types.Skill.Board", true);
+    }
+
+    public int getSkillScoreboardTime() {
+        return config.getInt("Scoreboard.Types.Skill.Display_Time", 30);
+    }
+
+    public boolean getSkillLevelUpBoard() {
+        return config.getBoolean("Scoreboard.Types.Skill.LevelUp_Board", true);
+    }
+
+    public int getSkillLevelUpTime() {
+        return config.getInt("Scoreboard.Types.Skill.LevelUp_Time", 5);
+    }
+
+    /* Database Purging */
+    public int getPurgeInterval() {
+        return config.getInt("Database_Purging.Purge_Interval", -1);
+    }
+
+    public int getOldUsersCutoff() {
+        return config.getInt("Database_Purging.Old_User_Cutoff", 6);
+    }
+
+    /* Backups */
+    public boolean getBackupsEnabled() {
+        return config.getBoolean("Backups.Enabled", true);
+    }
+
+    public boolean getKeepLast24Hours() {
+        return config.getBoolean("Backups.Keep.Last_24_Hours", true);
+    }
+
+    public boolean getKeepDailyLastWeek() {
+        return config.getBoolean("Backups.Keep.Daily_Last_Week", true);
+    }
+
+    public boolean getKeepWeeklyPastMonth() {
+        return config.getBoolean("Backups.Keep.Weekly_Past_Months", true);
+    }
+
+    /* mySQL */
+    public boolean getUseMySQL() {
+        return config.getBoolean("MySQL.Enabled", false);
+    }
+
+    public String getMySQLTablePrefix() {
+        return config.getString("MySQL.Database.TablePrefix", "mcmmo_");
+    }
+
+    public String getMySQLDatabaseName() {
+        return getStringIncludingInts("MySQL.Database.Name");
+    }
+
+    public String getMySQLUserName() {
+        return getStringIncludingInts("MySQL.Database.User_Name");
+    }
+
+    public int getMySQLServerPort() {
+        return config.getInt("MySQL.Server.Port", 3306);
+    }
+
+    public String getMySQLServerName() {
+        return config.getString("MySQL.Server.Address", "localhost");
+    }
+
+    public String getMySQLUserPassword() {
+        return getStringIncludingInts("MySQL.Database.User_Password");
+    }
+
+    public int getMySQLMaxConnections(PoolIdentifier identifier) {
+        return config.getInt("MySQL.Database.MaxConnections." + StringUtils.getCapitalized(identifier.toString()), 30);
+    }
+
+    public int getMySQLMaxPoolSize(PoolIdentifier identifier) {
+        return config.getInt("MySQL.Database.MaxPoolSize." + StringUtils.getCapitalized(identifier.toString()), 10);
+    }
+
+    public boolean getMySQLSSL() {
+        return config.getBoolean("MySQL.Server.SSL", true);
+    }
+
+    public boolean getMySQLDebug() {
+        return config.getBoolean("MySQL.Debug", false);
+    }
+
+    public boolean getMySQLPublicKeyRetrieval() {
+        return config.getBoolean("MySQL.Server.allowPublicKeyRetrieval", true);
+    }
+
+    private String getStringIncludingInts(String key) {
+        String str = config.getString(key);
+
+        if (str == null) {
+            str = String.valueOf(config.getInt(key));
+        }
+
+        if (str.equals("0")) {
+            str = "No value set for '" + key + "'";
+        }
+        return str;
+    }
+
+    /* Hardcore Mode */
+    public boolean getHardcoreStatLossEnabled(PrimarySkillType primarySkillType) {
+        return config.getBoolean("Hardcore.Death_Stat_Loss.Enabled." + StringUtils.getCapitalized(primarySkillType.toString()), false);
+    }
+
+    public void setHardcoreStatLossEnabled(PrimarySkillType primarySkillType, boolean enabled) {
+        config.set("Hardcore.Death_Stat_Loss.Enabled." + StringUtils.getCapitalized(primarySkillType.toString()), enabled);
+    }
+
+    public double getHardcoreDeathStatPenaltyPercentage() {
+        return config.getDouble("Hardcore.Death_Stat_Loss.Penalty_Percentage", 75.0D);
+    }
+
+    public void setHardcoreDeathStatPenaltyPercentage(double value) {
+        config.set("Hardcore.Death_Stat_Loss.Penalty_Percentage", value);
+    }
+
+    public int getHardcoreDeathStatPenaltyLevelThreshold() {
+        return config.getInt("Hardcore.Death_Stat_Loss.Level_Threshold", 0);
+    }
+
+    public boolean getHardcoreVampirismEnabled(PrimarySkillType primarySkillType) {
+        return config.getBoolean("Hardcore.Vampirism.Enabled." + StringUtils.getCapitalized(primarySkillType.toString()), false);
+    }
+
+    public void setHardcoreVampirismEnabled(PrimarySkillType primarySkillType, boolean enabled) {
+        config.set("Hardcore.Vampirism.Enabled." + StringUtils.getCapitalized(primarySkillType.toString()), enabled);
+    }
+
+    public double getHardcoreVampirismStatLeechPercentage() {
+        return config.getDouble("Hardcore.Vampirism.Leech_Percentage", 5.0D);
+    }
+
+    public void setHardcoreVampirismStatLeechPercentage(double value) {
+        config.set("Hardcore.Vampirism.Leech_Percentage", value);
+    }
+
+    public int getHardcoreVampirismLevelThreshold() {
+        return config.getInt("Hardcore.Vampirism.Level_Threshold", 0);
+    }
+
+    /* SMP Mods */
+    public boolean getToolModsEnabled() {
+        return config.getBoolean("Mods.Tool_Mods_Enabled", false);
+    }
+
+    public boolean getArmorModsEnabled() {
+        return config.getBoolean("Mods.Armor_Mods_Enabled", false);
+    }
+
+    public boolean getBlockModsEnabled() {
+        return config.getBoolean("Mods.Block_Mods_Enabled", false);
+    }
+
+    public boolean getEntityModsEnabled() {
+        return config.getBoolean("Mods.Entity_Mods_Enabled", false);
+    }
+
+    /* Items */
+    public int getChimaeraUseCost() {
+        return config.getInt("Items.Chimaera_Wing.Use_Cost", 1);
+    }
+
+    public int getChimaeraRecipeCost() {
+        return config.getInt("Items.Chimaera_Wing.Recipe_Cost", 5);
+    }
+
+    public Material getChimaeraItem() {
+        return Material.matchMaterial(config.getString("Items.Chimaera_Wing.Item_Name", "Feather"));
+    }
+
+    public boolean getChimaeraEnabled() {
+        return config.getBoolean("Items.Chimaera_Wing.Enabled", true);
+    }
+
+    public boolean getChimaeraPreventUseUnderground() {
+        return config.getBoolean("Items.Chimaera_Wing.Prevent_Use_Underground", true);
+    }
+
+    public boolean getChimaeraUseBedSpawn() {
+        return config.getBoolean("Items.Chimaera_Wing.Use_Bed_Spawn", true);
+    }
+
+    public int getChimaeraCooldown() {
+        return config.getInt("Items.Chimaera_Wing.Cooldown", 240);
+    }
+
+    public int getChimaeraWarmup() {
+        return config.getInt("Items.Chimaera_Wing.Warmup", 5);
+    }
+
+    public int getChimaeraRecentlyHurtCooldown() {
+        return config.getInt("Items.Chimaera_Wing.RecentlyHurt_Cooldown", 60);
+    }
+
+    public boolean getChimaeraSoundEnabled() {
+        return config.getBoolean("Items.Chimaera_Wing.Sound_Enabled", true);
+    }
+
+    public boolean getFluxPickaxeSoundEnabled() {
+        return config.getBoolean("Items.Flux_Pickaxe.Sound_Enabled", true);
+    }
+
+    /* Particles */
+    public boolean getAbilityActivationEffectEnabled() {
+        return config.getBoolean("Particles.Ability_Activation", true);
+    }
+
+    public boolean getAbilityDeactivationEffectEnabled() {
+        return config.getBoolean("Particles.Ability_Deactivation", true);
+    }
+
+    public boolean getBleedEffectEnabled() {
+        return config.getBoolean("Particles.Bleed", true);
+    }
+
+    public boolean getDodgeEffectEnabled() {
+        return config.getBoolean("Particles.Dodge", true);
+    }
+
+    public boolean getFluxEffectEnabled() {
+        return config.getBoolean("Particles.Flux", true);
+    }
+
+    public boolean getGreaterImpactEffectEnabled() {
+        return config.getBoolean("Particles.Greater_Impact", true);
+    }
+
+    public boolean getCallOfTheWildEffectEnabled() {
+        return config.getBoolean("Particles.Call_of_the_Wild", true);
+    }
+
+    public boolean getLevelUpEffectsEnabled() {
+        return config.getBoolean("Particles.LevelUp_Enabled", true);
+    }
+
+    public int getLevelUpEffectsTier() {
+        return config.getInt("Particles.LevelUp_Tier", 100);
+    }
+//    public boolean getLargeFireworks() { return config.getBoolean("Particles.LargeFireworks", true); }
+
+    /* PARTY SETTINGS */
+    public boolean getPartyFriendlyFire() {
+        return config.getBoolean("Party.FriendlyFire", false);
+    }
+
+    public int getPartyMaxSize() {
+        return config.getInt("Party.MaxSize", -1);
+    }
+
+    public int getAutoPartyKickInterval() {
+        return config.getInt("Party.AutoKick_Interval", 12);
+    }
+
+    public int getAutoPartyKickTime() {
+        return config.getInt("Party.Old_Party_Member_Cutoff", 7);
+    }
+
+    public double getPartyShareBonusBase() {
+        return config.getDouble("Party.Sharing.ExpShare_bonus_base", 1.1D);
+    }
+
+    public double getPartyShareBonusIncrease() {
+        return config.getDouble("Party.Sharing.ExpShare_bonus_increase", 0.05D);
+    }
+
+    public double getPartyShareBonusCap() {
+        return config.getDouble("Party.Sharing.ExpShare_bonus_cap", 1.5D);
+    }
+
+    public double getPartyShareRange() {
+        return config.getDouble("Party.Sharing.Range", 75.0D);
+    }
+
+    public int getPartyLevelCap() {
+        int cap = config.getInt("Party.Leveling.Level_Cap", 10);
+        return (cap <= 0) ? Integer.MAX_VALUE : cap;
+    }
+
+    public int getPartyXpCurveMultiplier() {
+        return config.getInt("Party.Leveling.Xp_Curve_Modifier", 3);
+    }
+
+    public boolean getPartyXpNearMembersNeeded() {
+        return config.getBoolean("Party.Leveling.Near_Members_Needed", false);
+    }
+
+    public boolean getPartyInformAllMembers() {
+        return config.getBoolean("Party.Leveling.Inform_All_Party_Members_On_LevelUp", false);
+    }
+
+    public int getPartyFeatureUnlockLevel(PartyFeature partyFeature) {
+        return config.getInt("Party.Leveling." + StringUtils.getPrettyPartyFeatureString(partyFeature).replace(" ", "") + "_UnlockLevel", 0);
+    }
+
+    /* Party Teleport Settings */
+    public int getPTPCommandCooldown() {
+        return config.getInt("Commands.ptp.Cooldown", 120);
+    }
+
+    public int getPTPCommandWarmup() {
+        return config.getInt("Commands.ptp.Warmup", 5);
+    }
+
+    public int getPTPCommandRecentlyHurtCooldown() {
+        return config.getInt("Commands.ptp.RecentlyHurt_Cooldown", 60);
+    }
+
+    public int getPTPCommandTimeout() {
+        return config.getInt("Commands.ptp.Request_Timeout", 300);
+    }
+
+    public boolean getPTPCommandConfirmRequired() {
+        return config.getBoolean("Commands.ptp.Accept_Required", true);
+    }
+
+    public boolean getPTPCommandWorldPermissions() {
+        return config.getBoolean("Commands.ptp.World_Based_Permissions", false);
+    }
+
+    /* Inspect command distance */
+    public double getInspectDistance() {
+        return config.getDouble("Commands.inspect.Max_Distance", 30.0D);
+    }
+
+    /*
+     * ABILITY SETTINGS
+     */
+
+    /* General Settings */
+    public boolean getUrlLinksEnabled() {
+        return config.getBoolean("Commands.Skills.URL_Links");
+    }
+
+    public boolean getAbilityMessagesEnabled() {
+        return config.getBoolean("Abilities.Messages", true);
+    }
+
+    public boolean getAbilitiesEnabled() {
+        return config.getBoolean("Abilities.Enabled", true);
+    }
+
+    public boolean getAbilitiesOnlyActivateWhenSneaking() {
+        return config.getBoolean("Abilities.Activation.Only_Activate_When_Sneaking", false);
+    }
+
+    public boolean getAbilitiesGateEnabled() {
+        return config.getBoolean("Abilities.Activation.Level_Gate_Abilities");
+    }
+
+    public int getCooldown(SuperAbilityType ability) {
+        return config.getInt("Abilities.Cooldowns." + ability.toString());
+    }
+
+    public int getMaxLength(SuperAbilityType ability) {
+        return config.getInt("Abilities.Max_Seconds." + ability.toString());
+    }
+
+    /* Durability Settings */
+    public int getAbilityToolDamage() {
+        return config.getInt("Abilities.Tools.Durability_Loss", 1);
+    }
+
+    /* Thresholds */
+    public int getTreeFellerThreshold() {
+        return config.getInt("Abilities.Limits.Tree_Feller_Threshold", 1000);
+    }
+
+    /*
+     * SKILL SETTINGS
+     */
+    public boolean getDoubleDropsEnabled(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 false;
+
+        return config.getBoolean("Bonus_Drops." + StringUtils.getCapitalized(skill.toString()) + "." + StringUtils.getPrettyItemString(material).replace(" ", "_"));
+    }
+
+    public boolean getDoubleDropsDisabled(PrimarySkillType skill) {
+        String skillName = StringUtils.getCapitalized(skill.toString());
+        ConfigurationSection section = config.getConfigurationSection("Bonus_Drops." + skillName);
+        if (section == null)
+            return false;
+        Set<String> keys = section.getKeys(false);
+        boolean disabled = true;
+
+        for (String key : keys) {
+            if (config.getBoolean("Bonus_Drops." + skillName + "." + key)) {
+                disabled = false;
+                break;
+            }
+        }
+
+        return disabled;
+    }
+
+    /* Axes */
+    public int getAxesGate() {
+        return config.getInt("Skills.Axes.Ability_Activation_Level_Gate", 10);
+    }
+
+    /* Acrobatics */
+    public boolean getDodgeLightningDisabled() {
+        return config.getBoolean("Skills.Acrobatics.Prevent_Dodge_Lightning", false);
+    }
+
+    public int getXPAfterTeleportCooldown() {
+        return config.getInt("Skills.Acrobatics.XP_After_Teleport_Cooldown", 5);
+    }
+
+    /* Alchemy */
+    public boolean getEnabledForHoppers() {
+        return config.getBoolean("Skills.Alchemy.Enabled_for_Hoppers", true);
+    }
+
+    public boolean getPreventHopperTransferIngredients() {
+        return config.getBoolean("Skills.Alchemy.Prevent_Hopper_Transfer_Ingredients", false);
+    }
+
+    public boolean getPreventHopperTransferBottles() {
+        return config.getBoolean("Skills.Alchemy.Prevent_Hopper_Transfer_Bottles", false);
+    }
+
+    /* Fishing */
+    public boolean getFishingDropsEnabled() {
+        return config.getBoolean("Skills.Fishing.Drops_Enabled", true);
+    }
+
+    public boolean getFishingOverrideTreasures() {
+        return config.getBoolean("Skills.Fishing.Override_Vanilla_Treasures", true);
+    }
+
+    public boolean getFishingExtraFish() {
+        return config.getBoolean("Skills.Fishing.Extra_Fish", true);
+    }
+
+    public double getFishingLureModifier() {
+        return config.getDouble("Skills.Fishing.Lure_Modifier", 4.0D);
+    }
+
+    /* Mining */
+    public Material getDetonatorItem() {
+        return Material.matchMaterial(config.getString("Skills.Mining.Detonator_Name", "FLINT_AND_STEEL"));
+    }
+
+    /* Excavation */
+    public int getExcavationGate() {
+        return config.getInt("Skills.Excavation.Ability_Activation_Level_Gate", 10);
+    }
+
+    /* Repair */
+    public boolean getRepairAnvilMessagesEnabled() {
+        return config.getBoolean("Skills.Repair.Anvil_Messages", true);
+    }
+
+    public boolean getRepairAnvilPlaceSoundsEnabled() {
+        return config.getBoolean("Skills.Repair.Anvil_Placed_Sounds", true);
+    }
+
+    public boolean getRepairAnvilUseSoundsEnabled() {
+        return config.getBoolean("Skills.Repair.Anvil_Use_Sounds", true);
+    }
+
+    public @Nullable Material getRepairAnvilMaterial() {
+        return Material.matchMaterial(config.getString("Skills.Repair.Anvil_Material", "IRON_BLOCK"));
+    }
+
+    public boolean getRepairConfirmRequired() {
+        return config.getBoolean("Skills.Repair.Confirm_Required", true);
+    }
+
+    public boolean getAllowVanillaInventoryRepair() {
+        return config.getBoolean("Skills.Repair.Allow_Vanilla_Anvil_Repair", false);
+    }
+
+    public boolean getAllowVanillaAnvilRepair() {
+        return config.getBoolean("Skills.Repair.Allow_Vanilla_Inventory_Repair", false);
+    }
+
+    public boolean getAllowVanillaGrindstoneRepair() {
+        return config.getBoolean("Skills.Repair.Allow_Vanilla_Grindstone_Repair", false);
+    }
+
+    /* Salvage */
+    public boolean getSalvageAnvilMessagesEnabled() {
+        return config.getBoolean("Skills.Salvage.Anvil_Messages", true);
+    }
+
+    public boolean getSalvageAnvilPlaceSoundsEnabled() {
+        return config.getBoolean("Skills.Salvage.Anvil_Placed_Sounds", true);
+    }
+
+    public boolean getSalvageAnvilUseSoundsEnabled() {
+        return config.getBoolean("Skills.Salvage.Anvil_Use_Sounds", true);
+    }
+
+    public @Nullable Material getSalvageAnvilMaterial() {
+        return Material.matchMaterial(config.getString("Skills.Salvage.Anvil_Material", "GOLD_BLOCK"));
+    }
+
+    public boolean getSalvageConfirmRequired() {
+        return config.getBoolean("Skills.Salvage.Confirm_Required", true);
+    }
+
+    /* Unarmed */
+    public boolean getUnarmedBlockCrackerSmoothbrickToCracked() {
+        return config.getBoolean("Skills.Unarmed.Block_Cracker.SmoothBrick_To_CrackedBrick", true);
+    }
+
+    public boolean getUnarmedItemPickupDisabled() {
+        return config.getBoolean("Skills.Unarmed.Item_Pickup_Disabled_Full_Inventory", true);
+    }
+
+    public boolean getUnarmedItemsAsUnarmed() {
+        return config.getBoolean("Skills.Unarmed.Items_As_Unarmed", false);
+    }
+
+    public int getUnarmedGate() {
+        return config.getInt("Skills.Unarmed.Ability_Activation_Level_Gate", 10);
+    }
+
+    /* Swords */
+    public int getSwordsGate() {
+        return config.getInt("Skills.Swords.Ability_Activation_Level_Gate", 10);
+    }
+
+    /* Taming */
+//    public Material getTamingCOTWMaterial(EntityType type) { return Material.matchMaterial(config.getString("Skills.Taming.Call_Of_The_Wild." + StringUtils.getPrettyEntityTypeString(type) + ".Item_Material")); }
+//    public int getTamingCOTWCost(EntityType type) { return config.getInt("Skills.Taming.Call_Of_The_Wild." + StringUtils.getPrettyEntityTypeString(type) + ".Item_Amount"); }
+//    public int getTamingCOTWAmount(EntityType type) { return config.getInt("Skills.Taming.Call_Of_The_Wild." + StringUtils.getPrettyEntityTypeString(type) + ".Summon_Amount"); }
+//    public int getTamingCOTWLength(EntityType type) { return config.getInt("Skills.Taming.Call_Of_The_Wild." + StringUtils.getPrettyEntityTypeString(type)+ ".Summon_Length"); }
+//    public int getTamingCOTWMaxAmount(EntityType type) { return config.getInt("Skills.Taming.Call_Of_The_Wild." + StringUtils.getPrettyEntityTypeString(type)+ ".Summon_Max_Amount"); }
+
+    public Material getTamingCOTWMaterial(String cotwEntity) {
+        return Material.matchMaterial(config.getString("Skills.Taming.Call_Of_The_Wild." + cotwEntity + ".Item_Material"));
+    }
+
+    public int getTamingCOTWCost(String cotwEntity) {
+        return config.getInt("Skills.Taming.Call_Of_The_Wild." + cotwEntity + ".Item_Amount");
+    }
+
+    public int getTamingCOTWAmount(String cotwEntity) {
+        return config.getInt("Skills.Taming.Call_Of_The_Wild." + cotwEntity + ".Summon_Amount");
+    }
+
+    public int getTamingCOTWLength(String cotwEntity) {
+        return config.getInt("Skills.Taming.Call_Of_The_Wild." + cotwEntity + ".Summon_Length");
+    }
+
+    public int getTamingCOTWMaxAmount(String cotwEntity) {
+        return config.getInt("Skills.Taming.Call_Of_The_Wild." + cotwEntity + ".Per_Player_Limit", 1);
+    }
+
+    /* Woodcutting */
+    public boolean getWoodcuttingDoubleDropsEnabled(BlockData material) {
+        return config.getBoolean("Bonus_Drops.Woodcutting." + StringUtils.getFriendlyConfigBlockDataString(material));
+    }
+
+    public boolean getTreeFellerSoundsEnabled() {
+        return config.getBoolean("Skills.Woodcutting.Tree_Feller_Sounds", true);
+    }
+
+    public int getWoodcuttingGate() {
+        return config.getInt("Skills.Woodcutting.Ability_Activation_Level_Gate", 10);
+    }
+
+    /* AFK Leveling */
+    public boolean getHerbalismPreventAFK() {
+        return config.getBoolean("Skills.Herbalism.Prevent_AFK_Leveling", true);
+    }
+
+    /* Level Caps */
+    public int getPowerLevelCap() {
+        int cap = config.getInt("General.Power_Level_Cap", 0);
+        return (cap <= 0) ? Integer.MAX_VALUE : cap;
+    }
+
+    public int getLevelCap(PrimarySkillType skill) {
+        int cap = config.getInt("Skills." + StringUtils.getCapitalized(skill.toString()) + ".Level_Cap");
+        return (cap <= 0) ? Integer.MAX_VALUE : cap;
+    }
+
+
+    /*public int isSuperAbilityUnlocked(PrimarySkillType skill) {
+        return config.getInt("Skills." + StringUtils.getCapitalized(skill.toString()) + ".Ability_Activation_Level_Gate");
+    }*/
+
+    public boolean getTruncateSkills() {
+        return config.getBoolean("General.TruncateSkills", false);
+    }
+
+    /* PVP & PVE Settings */
+    public boolean getPVPEnabled(PrimarySkillType skill) {
+        return config.getBoolean("Skills." + StringUtils.getCapitalized(skill.toString()) + ".Enabled_For_PVP", true);
+    }
+
+    public boolean getPVEEnabled(PrimarySkillType skill) {
+        return config.getBoolean("Skills." + StringUtils.getCapitalized(skill.toString()) + ".Enabled_For_PVE", true);
+    }
+
+    //public float getMasterVolume() { return (float) config.getDouble("Sounds.MasterVolume", 1.0); }
+
+    public boolean broadcastEventMessages() {
+        return config.getBoolean("General.EventBroadcasts", true);
+    }
+
+    public boolean playerJoinEventInfo() {
+        return config.getBoolean("General.EventInfoOnPlayerJoin", true);
+    }
+
+    public boolean adminNotifications() {
+        return config.getBoolean("General.AdminNotifications", true);
+    }
+
+    public boolean shouldLevelUpBroadcasts() {
+        return config.getBoolean("General.Level_Up_Chat_Broadcasts.Enabled", true);
+    }
+
+    public boolean shouldLevelUpBroadcastToConsole() {
+        return config.getBoolean("General.Level_Up_Chat_Broadcasts.Broadcast_Targets.Send_To_Console", true);
+    }
+
+    public boolean isLevelUpBroadcastsPartyMembersOnly() {
+        return config.getBoolean("General.Level_Up_Chat_Broadcasts.Broadcast_Targets.Only_Party_Members", false);
+    }
+
+    public boolean isLevelUpBroadcastsSameWorldOnly() {
+        return config.getBoolean("General.Level_Up_Chat_Broadcasts.Broadcast_Targets.Only_Same_World", false);
+    }
+
+    public boolean shouldLevelUpBroadcastsRestrictDistance() {
+        return config.getBoolean("General.Level_Up_Chat_Broadcasts.Broadcast_Targets.Distance_Restrictions.Restrict_Distance", false);
+    }
+
+    public int getLevelUpBroadcastRadius() {
+        return config.getInt("General.Level_Up_Chat_Broadcasts.Broadcast_Targets.Distance_Restrictions.Restricted_Radius", 100);
+    }
+
+    public int getLevelUpBroadcastInterval() {
+        return config.getInt("General.Level_Up_Chat_Broadcasts.Milestone_Interval", 100);
+    }
+
+    public boolean shouldPowerLevelUpBroadcasts() {
+        return config.getBoolean("General.Level_Up_Chat_Broadcasts.Broadcast_Powerlevels.Enabled", true);
+    }
+
+    public boolean shouldPowerLevelUpBroadcastToConsole() {
+        return config.getBoolean("General.Level_Up_Chat_Broadcasts.Broadcast_Powerlevels.Broadcast_Targets.Send_To_Console", true);
+    }
+
+    public boolean isPowerLevelUpBroadcastsPartyMembersOnly() {
+        return config.getBoolean("General.Level_Up_Chat_Broadcasts.Broadcast_Powerlevels.Broadcast_Targets.Only_Party_Members", false);
+    }
+
+    public boolean isPowerLevelUpBroadcastsSameWorldOnly() {
+        return config.getBoolean("General.Level_Up_Chat_Broadcasts.Broadcast_Powerlevels.Broadcast_Targets.Only_Same_World", false);
+    }
+
+    public boolean shouldPowerLevelUpBroadcastsRestrictDistance() {
+        return config.getBoolean("General.Level_Up_Chat_Broadcasts.Broadcast_Powerlevels.Broadcast_Targets.Distance_Restrictions.Restrict_Distance", false);
+    }
+
+    public int getPowerLevelUpBroadcastRadius() {
+        return config.getInt("General.Level_Up_Chat_Broadcasts.Broadcast_Powerlevels.Broadcast_Targets.Distance_Restrictions.Restricted_Radius", 100);
+    }
+
+    public int getPowerLevelUpBroadcastInterval() {
+        return config.getInt("General.Level_Up_Chat_Broadcasts.Broadcast_Powerlevels.Milestone_Interval", 100);
+    }
+
+    public boolean isGreenThumbReplantableCrop(@NotNull Material material) {
+        return config.getBoolean("Green_Thumb_Replanting_Crops." + StringUtils.getCapitalized(material.toString()), true);
+    }
+}

+ 0 - 5
src/main/java/com/gmail/nossr50/config/HiddenConfig.java

@@ -9,7 +9,6 @@ public class HiddenConfig {
     private static HiddenConfig instance;
     private static HiddenConfig instance;
     private final String fileName;
     private final String fileName;
     private YamlConfiguration config;
     private YamlConfiguration config;
-    private boolean chunkletsEnabled;
     private int conversionRate;
     private int conversionRate;
     private boolean useEnchantmentBuffs;
     private boolean useEnchantmentBuffs;
 
 
@@ -30,15 +29,11 @@ public class HiddenConfig {
         InputStreamReader reader = mcMMO.p.getResourceAsReader(fileName);
         InputStreamReader reader = mcMMO.p.getResourceAsReader(fileName);
         if (reader != null) {
         if (reader != null) {
             config = YamlConfiguration.loadConfiguration(reader);
             config = YamlConfiguration.loadConfiguration(reader);
-            chunkletsEnabled = config.getBoolean("Options.Chunklets", true);
             conversionRate = config.getInt("Options.ConversionRate", 1);
             conversionRate = config.getInt("Options.ConversionRate", 1);
             useEnchantmentBuffs = config.getBoolean("Options.EnchantmentBuffs", true);
             useEnchantmentBuffs = config.getBoolean("Options.EnchantmentBuffs", true);
         }
         }
     }
     }
 
 
-    public boolean getChunkletsEnabled() {
-        return chunkletsEnabled;
-    }
 
 
     public int getConversionRate() {
     public int getConversionRate() {
         return conversionRate;
         return conversionRate;

+ 7 - 2
src/main/java/com/gmail/nossr50/config/PersistentDataConfig.java

@@ -1,8 +1,8 @@
 package com.gmail.nossr50.config;
 package com.gmail.nossr50.config;
 
 
-import com.gmail.nossr50.util.compat.layers.persistentdata.MobMetaFlagType;
+import com.gmail.nossr50.metadata.MobMetaFlagType;
 
 
-public class PersistentDataConfig extends AutoUpdateConfigLoader {
+public class PersistentDataConfig extends BukkitConfig {
     private static PersistentDataConfig instance;
     private static PersistentDataConfig instance;
 
 
     private PersistentDataConfig() {
     private PersistentDataConfig() {
@@ -34,4 +34,9 @@ public class PersistentDataConfig extends AutoUpdateConfigLoader {
         return config.getBoolean(key, false);
         return config.getBoolean(key, false);
     }
     }
 
 
+    public boolean useBlockTracker() {
+        return config.getBoolean("mcMMO_Region_System.Enabled", true);
+    }
+
+
 }
 }

+ 42 - 44
src/main/java/com/gmail/nossr50/config/RankConfig.java

@@ -2,6 +2,7 @@ package com.gmail.nossr50.config;
 
 
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
 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.mcMMO;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
@@ -11,24 +12,22 @@ import java.util.List;
 public class RankConfig extends AutoUpdateConfigLoader {
 public class RankConfig extends AutoUpdateConfigLoader {
     private static RankConfig instance;
     private static RankConfig instance;
 
 
-    public RankConfig()
-    {
+    public RankConfig() {
         super("skillranks.yml");
         super("skillranks.yml");
         validate();
         validate();
         instance = this;
         instance = this;
     }
     }
 
 
-    @Override
-    protected void loadKeys() {
+    public static RankConfig getInstance() {
+        if (instance == null)
+            return new RankConfig();
 
 
+        return instance;
     }
     }
 
 
-    public static RankConfig getInstance()
-    {
-        if(instance == null)
-            return new RankConfig();
+    @Override
+    protected void loadKeys() {
 
 
-        return instance;
     }
     }
 
 
     @Override
     @Override
@@ -45,12 +44,13 @@ public class RankConfig extends AutoUpdateConfigLoader {
 
 
     /**
     /**
      * Returns the unlock level for a subskill depending on the gamemode
      * Returns the unlock level for a subskill depending on the gamemode
+     *
      * @param subSkillType target subskill
      * @param subSkillType target subskill
-     * @param rank the rank we are checking
+     * @param rank         the rank we are checking
+     *
      * @return the level requirement for a subskill at this particular rank
      * @return the level requirement for a subskill at this particular rank
      */
      */
-    public int getSubSkillUnlockLevel(SubSkillType subSkillType, int rank)
-    {
+    public int getSubSkillUnlockLevel(SubSkillType subSkillType, int rank) {
         String key = subSkillType.getRankConfigAddress();
         String key = subSkillType.getRankConfigAddress();
 
 
         return findRankByRootAddress(rank, key);
         return findRankByRootAddress(rank, key);
@@ -58,37 +58,41 @@ public class RankConfig extends AutoUpdateConfigLoader {
 
 
     /**
     /**
      * Returns the unlock level for a subskill depending on the gamemode
      * Returns the unlock level for a subskill depending on the gamemode
+     *
      * @param subSkillType target subskill
      * @param subSkillType target subskill
-     * @param rank the rank we are checking
+     * @param rank         the rank we are checking
+     *
      * @return the level requirement for a subskill at this particular rank
      * @return the level requirement for a subskill at this particular rank
      */
      */
-    public int getSubSkillUnlockLevel(SubSkillType subSkillType, int rank, boolean retroMode)
-    {
+    public int getSubSkillUnlockLevel(SubSkillType subSkillType, int rank, boolean retroMode) {
         String key = getRankAddressKey(subSkillType, rank, retroMode);
         String key = getRankAddressKey(subSkillType, rank, retroMode);
         return config.getInt(key, getInternalConfig().getInt(key));
         return config.getInt(key, getInternalConfig().getInt(key));
     }
     }
 
 
     /**
     /**
      * Returns the unlock level for a subskill depending on the gamemode
      * Returns the unlock level for a subskill depending on the gamemode
+     *
      * @param abstractSubSkill target subskill
      * @param abstractSubSkill target subskill
-     * @param rank the rank we are checking
+     * @param rank             the rank we are checking
+     *
      * @return the level requirement for a subskill at this particular rank
      * @return the level requirement for a subskill at this particular rank
      */
      */
-    public int getSubSkillUnlockLevel(AbstractSubSkill abstractSubSkill, int rank)
-    {
-        String key = abstractSubSkill.getPrimaryKeyName()+"."+abstractSubSkill.getConfigKeyName();
+    public int getSubSkillUnlockLevel(AbstractSubSkill abstractSubSkill, int rank) {
+        String key = abstractSubSkill.getPrimaryKeyName() + "." + abstractSubSkill.getConfigKeyName();
 
 
         return findRankByRootAddress(rank, key);
         return findRankByRootAddress(rank, key);
     }
     }
 
 
     /**
     /**
      * Returns the unlock level for a subskill depending on the gamemode
      * Returns the unlock level for a subskill depending on the gamemode
-     * @param key root address of the subskill in the rankskills.yml file
+     *
+     * @param key  root address of the subskill in the rankskills.yml file
      * @param rank the rank we are checking
      * @param rank the rank we are checking
+     *
      * @return the level requirement for a subskill at this particular rank
      * @return the level requirement for a subskill at this particular rank
      */
      */
     private int findRankByRootAddress(int rank, String key) {
     private int findRankByRootAddress(int rank, String key) {
-        String scalingKey = Config.getInstance().getIsRetroMode() ? ".RetroMode." : ".Standard.";
+        String scalingKey = mcMMO.p.getGeneralConfig().getIsRetroMode() ? ".RetroMode." : ".Standard.";
 
 
         String targetRank = "Rank_" + rank;
         String targetRank = "Rank_" + rank;
 
 
@@ -126,60 +130,55 @@ public class RankConfig extends AutoUpdateConfigLoader {
         String key = getRankAddressKey(subSkillType, rank, retroMode);
         String key = getRankAddressKey(subSkillType, rank, retroMode);
         int defaultValue = getInternalConfig().getInt(key);
         int defaultValue = getInternalConfig().getInt(key);
         config.set(key, defaultValue);
         config.set(key, defaultValue);
-        plugin.getLogger().info(key +" SET -> " + defaultValue);
+        mcMMO.p.getLogger().info(key + " SET -> " + defaultValue);
     }
     }
 
 
     /**
     /**
      * Checks for valid keys for subskill ranks
      * Checks for valid keys for subskill ranks
      */
      */
-    private void checkKeys(@NotNull List<String> reasons)
-    {
+    private void checkKeys(@NotNull List<String> reasons) {
         HashSet<SubSkillType> badSkillSetup = new HashSet<>();
         HashSet<SubSkillType> badSkillSetup = new HashSet<>();
-        
+
         //For now we will only check ranks of stuff I've overhauled
         //For now we will only check ranks of stuff I've overhauled
         checkConfig(reasons, badSkillSetup, true);
         checkConfig(reasons, badSkillSetup, true);
         checkConfig(reasons, badSkillSetup, false);
         checkConfig(reasons, badSkillSetup, false);
 
 
         //Fix bad entries
         //Fix bad entries
-        if(badSkillSetup.isEmpty())
+        if (badSkillSetup.isEmpty())
             return;
             return;
 
 
-        plugin.getLogger().info("(FIXING CONFIG) mcMMO is correcting a few mistakes found in your skill rank config setup");
+        mcMMO.p.getLogger().info("(FIXING CONFIG) mcMMO is correcting a few mistakes found in your skill rank config setup");
 
 
-        for(SubSkillType subSkillType : badSkillSetup) {
-            plugin.getLogger().info("(FIXING CONFIG) Resetting rank config settings for skill named - "+subSkillType.toString());
+        for (SubSkillType subSkillType : badSkillSetup) {
+            mcMMO.p.getLogger().info("(FIXING CONFIG) Resetting rank config settings for skill named - " + subSkillType.toString());
             fixBadEntries(subSkillType);
             fixBadEntries(subSkillType);
         }
         }
     }
     }
 
 
     private void checkConfig(@NotNull List<String> reasons, @NotNull HashSet<SubSkillType> badSkillSetup, boolean retroMode) {
     private void checkConfig(@NotNull List<String> reasons, @NotNull HashSet<SubSkillType> badSkillSetup, boolean retroMode) {
-        for(SubSkillType subSkillType : SubSkillType.values())
-        {
+        for (SubSkillType subSkillType : SubSkillType.values()) {
             //Keeping track of the rank requirements and making sure there are no logical errors
             //Keeping track of the rank requirements and making sure there are no logical errors
             int curRank = 0;
             int curRank = 0;
             int prevRank = 0;
             int prevRank = 0;
 
 
-            for(int x = 0; x < subSkillType.getNumRanks(); x++)
-            {
-                int index = x+1;
+            for (int x = 0; x < subSkillType.getNumRanks(); x++) {
+                int index = x + 1;
 
 
-                if(curRank > 0)
+                if (curRank > 0)
                     prevRank = curRank;
                     prevRank = curRank;
 
 
                 curRank = getSubSkillUnlockLevel(subSkillType, index, retroMode);
                 curRank = getSubSkillUnlockLevel(subSkillType, index, retroMode);
 
 
                 //Do we really care if its below 0? Probably not
                 //Do we really care if its below 0? Probably not
-                if(curRank < 0)
-                {
-                    reasons.add("(CONFIG ISSUE) " + subSkillType.toString() + " should not have any ranks that require a negative level!");
+                if (curRank < 0) {
+                    reasons.add("(CONFIG ISSUE) " + subSkillType + " should not have any ranks that require a negative level!");
                     badSkillSetup.add(subSkillType);
                     badSkillSetup.add(subSkillType);
                     continue;
                     continue;
                 }
                 }
 
 
-                if(prevRank > curRank)
-                {
+                if (prevRank > curRank) {
                     //We're going to allow this but we're going to warn them
                     //We're going to allow this but we're going to warn them
-                    plugin.getLogger().info("(CONFIG ISSUE) You have the ranks for the subskill "+ subSkillType.toString()+" set up poorly, sequential ranks should have ascending requirements");
+                    mcMMO.p.getLogger().info("(CONFIG ISSUE) You have the ranks for the subskill " + subSkillType + " set up poorly, sequential ranks should have ascending requirements");
                     badSkillSetup.add(subSkillType);
                     badSkillSetup.add(subSkillType);
                 }
                 }
             }
             }
@@ -187,9 +186,8 @@ public class RankConfig extends AutoUpdateConfigLoader {
     }
     }
 
 
     private void fixBadEntries(@NotNull SubSkillType subSkillType) {
     private void fixBadEntries(@NotNull SubSkillType subSkillType) {
-        for(int x = 0; x < subSkillType.getNumRanks(); x++)
-        {
-            int index = x+1;
+        for (int x = 0; x < subSkillType.getNumRanks(); x++) {
+            int index = x + 1;
 
 
             //Reset Retromode entries
             //Reset Retromode entries
             resetRankValue(subSkillType, index, true);
             resetRankValue(subSkillType, index, true);

+ 24 - 30
src/main/java/com/gmail/nossr50/config/SoundConfig.java

@@ -1,46 +1,41 @@
 package com.gmail.nossr50.config;
 package com.gmail.nossr50.config;
 
 
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.sounds.SoundType;
 import com.gmail.nossr50.util.sounds.SoundType;
 
 
-public class SoundConfig extends AutoUpdateConfigLoader {
+public class SoundConfig extends BukkitConfig {
     private static SoundConfig instance;
     private static SoundConfig instance;
 
 
-    public SoundConfig()
-    {
+    public SoundConfig() {
         super("sounds.yml");
         super("sounds.yml");
         validate();
         validate();
         instance = this;
         instance = this;
     }
     }
 
 
-    @Override
-    protected void loadKeys() {
+    public static SoundConfig getInstance() {
+        if (instance == null)
+            return new SoundConfig();
 
 
+        return instance;
     }
     }
 
 
-    public static SoundConfig getInstance()
-    {
-        if(instance == null)
-            return new SoundConfig();
+    @Override
+    protected void loadKeys() {
 
 
-        return instance;
     }
     }
 
 
     @Override
     @Override
     protected boolean validateKeys() {
     protected boolean validateKeys() {
-        for(SoundType soundType : SoundType.values())
-        {
-            if(config.getDouble("Sounds."+soundType.toString()+".Volume") < 0)
-            {
-                plugin.getLogger().info("[mcMMO] Sound volume cannot be below 0 for "+soundType.toString());
+        for (SoundType soundType : SoundType.values()) {
+            if (config.getDouble("Sounds." + soundType.toString() + ".Volume") < 0) {
+                mcMMO.p.getLogger().info("[mcMMO] Sound volume cannot be below 0 for " + soundType);
                 return false;
                 return false;
             }
             }
 
 
             //Sounds with custom pitching don't use pitch values
             //Sounds with custom pitching don't use pitch values
-            if(!soundType.usesCustomPitch())
-            {
-                if(config.getDouble("Sounds."+soundType.toString()+".Pitch") < 0)
-                {
-                    plugin.getLogger().info("[mcMMO] Sound pitch cannot be below 0 for "+soundType.toString());
+            if (!soundType.usesCustomPitch()) {
+                if (config.getDouble("Sounds." + soundType + ".Pitch") < 0) {
+                    mcMMO.p.getLogger().info("[mcMMO] Sound pitch cannot be below 0 for " + soundType);
                     return false;
                     return false;
                 }
                 }
             }
             }
@@ -48,23 +43,22 @@ public class SoundConfig extends AutoUpdateConfigLoader {
         return true;
         return true;
     }
     }
 
 
-    public float getMasterVolume() { return (float) config.getDouble("Sounds.MasterVolume", 1.0); }
+    public float getMasterVolume() {
+        return (float) config.getDouble("Sounds.MasterVolume", 1.0);
+    }
 
 
-    public float getVolume(SoundType soundType)
-    {
-        String key = "Sounds."+soundType.toString()+".Volume";
+    public float getVolume(SoundType soundType) {
+        String key = "Sounds." + soundType.toString() + ".Volume";
         return (float) config.getDouble(key);
         return (float) config.getDouble(key);
     }
     }
 
 
-    public float getPitch(SoundType soundType)
-    {
-        String key = "Sounds."+soundType.toString()+".Pitch";
+    public float getPitch(SoundType soundType) {
+        String key = "Sounds." + soundType.toString() + ".Pitch";
         return (float) config.getDouble(key);
         return (float) config.getDouble(key);
     }
     }
 
 
-    public boolean getIsEnabled(SoundType soundType)
-    {
-        String key = "Sounds."+soundType.toString()+".Enabled";
+    public boolean getIsEnabled(SoundType soundType) {
+        String key = "Sounds." + soundType.toString() + ".Enabled";
         return config.getBoolean(key, true);
         return config.getBoolean(key, true);
     }
     }
 }
 }

+ 18 - 23
src/main/java/com/gmail/nossr50/config/WorldBlacklist.java

@@ -15,20 +15,28 @@ public class WorldBlacklist {
 
 
     private final String blackListFileName = "world_blacklist.txt";
     private final String blackListFileName = "world_blacklist.txt";
 
 
-    public WorldBlacklist(mcMMO plugin)
-    {
+    public WorldBlacklist(mcMMO plugin) {
         this.plugin = plugin;
         this.plugin = plugin;
         blacklist = new ArrayList<>();
         blacklist = new ArrayList<>();
         init();
         init();
     }
     }
 
 
-    public void init()
-    {
+    public static boolean isWorldBlacklisted(World world) {
+
+        for (String s : blacklist) {
+            if (world.getName().equalsIgnoreCase(s))
+                return true;
+        }
+
+        return false;
+    }
+
+    public void init() {
         //Make the blacklist file if it doesn't exist
         //Make the blacklist file if it doesn't exist
         File blackListFile = new File(plugin.getDataFolder() + File.separator + blackListFileName);
         File blackListFile = new File(plugin.getDataFolder() + File.separator + blackListFileName);
 
 
         try {
         try {
-            if(!blackListFile.exists())
+            if (!blackListFile.exists())
                 blackListFile.createNewFile();
                 blackListFile.createNewFile();
         } catch (IOException e) {
         } catch (IOException e) {
             e.printStackTrace();
             e.printStackTrace();
@@ -48,12 +56,11 @@ public class WorldBlacklist {
 
 
             String currentLine;
             String currentLine;
 
 
-            while((currentLine = bufferedReader.readLine()) != null)
-            {
-                if(currentLine.length() == 0)
+            while ((currentLine = bufferedReader.readLine()) != null) {
+                if (currentLine.length() == 0)
                     continue;
                     continue;
 
 
-                if(!blacklist.contains(currentLine))
+                if (!blacklist.contains(currentLine))
                     blacklist.add(currentLine);
                     blacklist.add(currentLine);
             }
             }
 
 
@@ -66,11 +73,11 @@ public class WorldBlacklist {
             closeRead(fileReader);
             closeRead(fileReader);
         }
         }
 
 
-        plugin.getLogger().info(blacklist.size()+" entries in mcMMO World Blacklist");
+        plugin.getLogger().info(blacklist.size() + " entries in mcMMO World Blacklist");
     }
     }
 
 
     private void closeRead(Reader reader) {
     private void closeRead(Reader reader) {
-        if(reader != null) {
+        if (reader != null) {
             try {
             try {
                 reader.close();
                 reader.close();
             } catch (IOException e) {
             } catch (IOException e) {
@@ -78,16 +85,4 @@ public class WorldBlacklist {
             }
             }
         }
         }
     }
     }
-
-    public static boolean isWorldBlacklisted(World world)
-    {
-
-        for(String s : blacklist)
-        {
-            if(world.getName().equalsIgnoreCase(s))
-                return true;
-        }
-
-        return false;
-    }
 }
 }

+ 219 - 86
src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java

@@ -1,6 +1,6 @@
 package com.gmail.nossr50.config.experience;
 package com.gmail.nossr50.config.experience;
 
 
-import com.gmail.nossr50.config.AutoUpdateConfigLoader;
+import com.gmail.nossr50.config.BukkitConfig;
 import com.gmail.nossr50.datatypes.experience.FormulaType;
 import com.gmail.nossr50.datatypes.experience.FormulaType;
 import com.gmail.nossr50.datatypes.skills.MaterialType;
 import com.gmail.nossr50.datatypes.skills.MaterialType;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
@@ -17,7 +17,7 @@ import org.bukkit.entity.EntityType;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
 
 
-public class ExperienceConfig extends AutoUpdateConfigLoader {
+public class ExperienceConfig extends BukkitConfig {
     private static ExperienceConfig instance;
     private static ExperienceConfig instance;
 
 
     private ExperienceConfig() {
     private ExperienceConfig() {
@@ -25,6 +25,12 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
         validate();
         validate();
     }
     }
 
 
+    @Override
+    public void initDefaults() {
+       config.addDefault("ExploitFix.Combat.XPCeiling.Enabled", true);
+       config.addDefault("ExploitFix.Combat.XPCeiling.Damage_Limit", 100);
+    }
+
     public static ExperienceConfig getInstance() {
     public static ExperienceConfig getInstance() {
         if (instance == null) {
         if (instance == null) {
             instance = new ExperienceConfig();
             instance = new ExperienceConfig();
@@ -34,7 +40,8 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
     }
     }
 
 
     @Override
     @Override
-    protected void loadKeys() {}
+    protected void loadKeys() {
+    }
 
 
     @Override
     @Override
     protected boolean validateKeys() {
     protected boolean validateKeys() {
@@ -139,81 +146,182 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
         return noErrorsInConfig(reason);
         return noErrorsInConfig(reason);
     }
     }
 
 
-    public boolean isEarlyGameBoostEnabled() { return config.getBoolean("EarlyGameBoost.Enabled", true); }
+    public boolean isEarlyGameBoostEnabled() {
+        return config.getBoolean("EarlyGameBoost.Enabled", true);
+    }
 
 
     /*
     /*
      * FORMULA SETTINGS
      * FORMULA SETTINGS
      */
      */
 
 
     /* EXPLOIT TOGGLES */
     /* EXPLOIT TOGGLES */
-    public boolean isSnowExploitPrevented() { return config.getBoolean("ExploitFix.SnowGolemExcavation", true); }
-    public boolean isEndermanEndermiteFarmingPrevented() { return config.getBoolean("ExploitFix.EndermanEndermiteFarms", true); }
-    public boolean isPistonCheatingPrevented() { return config.getBoolean("ExploitFix.PistonCheating", true); }
-    public boolean isPistonExploitPrevented() { return config.getBoolean("ExploitFix.Pistons", false); }
-    public boolean allowUnsafeEnchantments() { return config.getBoolean("ExploitFix.UnsafeEnchantments", false); }
-    public boolean isCOTWBreedingPrevented() { return config.getBoolean("ExploitFix.COTWBreeding", true); }
-    public boolean isNPCInteractionPrevented() { return config.getBoolean("ExploitFix.PreventPluginNPCInteraction", true); }
-
-    public boolean isFishingExploitingPrevented() { return config.getBoolean("ExploitFix.Fishing", true); }
-    public boolean isAcrobaticsExploitingPrevented() { return config.getBoolean("ExploitFix.Acrobatics", true); }
-    public boolean isTreeFellerXPReduced() { return config.getBoolean("ExploitFix.TreeFellerReducedXP", true); }
+    public boolean isSnowExploitPrevented() {
+        return config.getBoolean("ExploitFix.SnowGolemExcavation", true);
+    }
+
+    public boolean isEndermanEndermiteFarmingPrevented() {
+        return config.getBoolean("ExploitFix.EndermanEndermiteFarms", true);
+    }
+
+    public boolean isPistonCheatingPrevented() {
+        return config.getBoolean("ExploitFix.PistonCheating", true);
+    }
+
+    public boolean isPistonExploitPrevented() {
+        return config.getBoolean("ExploitFix.Pistons", false);
+    }
+
+    public boolean allowUnsafeEnchantments() {
+        return config.getBoolean("ExploitFix.UnsafeEnchantments", false);
+    }
+
+    public boolean isCOTWBreedingPrevented() {
+        return config.getBoolean("ExploitFix.COTWBreeding", true);
+    }
+
+    public boolean isNPCInteractionPrevented() {
+        return config.getBoolean("ExploitFix.PreventPluginNPCInteraction", true);
+    }
+
+    public boolean isFishingExploitingPrevented() {
+        return config.getBoolean("ExploitFix.Fishing", true);
+    }
+
+    public int getFishingExploitingOptionMoveRange() {
+        return config.getInt("Fishing_ExploitFix_Options.MoveRange", 3);
+    }
+
+    public int getFishingExploitingOptionOverFishLimit() {
+        return config.getInt("Fishing_ExploitFix_Options.OverFishLimit", 10);
+    }
+
+    public boolean isAcrobaticsExploitingPrevented() {
+        return config.getBoolean("ExploitFix.Acrobatics", true);
+    }
+
+    public boolean isTreeFellerXPReduced() {
+        return config.getBoolean("ExploitFix.TreeFellerReducedXP", true);
+    }
 
 
     /* Curve settings */
     /* Curve settings */
-    public FormulaType getFormulaType() { return FormulaType.getFormulaType(config.getString("Experience_Formula.Curve")); }
-    public boolean getCumulativeCurveEnabled() { return config.getBoolean("Experience_Formula.Cumulative_Curve", false); }
+    public FormulaType getFormulaType() {
+        return FormulaType.getFormulaType(config.getString("Experience_Formula.Curve"));
+    }
+
+    public boolean getCumulativeCurveEnabled() {
+        return config.getBoolean("Experience_Formula.Cumulative_Curve", false);
+    }
 
 
     /* Curve values */
     /* Curve values */
-    public double getMultiplier(FormulaType type) { return config.getDouble("Experience_Formula." + StringUtils.getCapitalized(type.toString()) + "_Values.multiplier"); }
-    public int getBase(FormulaType type) { return config.getInt("Experience_Formula." + StringUtils.getCapitalized(type.toString()) + "_Values.base"); }
-    public double getExponent(FormulaType type) { return config.getDouble("Experience_Formula." + StringUtils.getCapitalized(type.toString()) + "_Values.exponent"); }
+    public double getMultiplier(FormulaType type) {
+        return config.getDouble("Experience_Formula." + StringUtils.getCapitalized(type.toString()) + "_Values.multiplier");
+    }
+
+    public int getBase(FormulaType type) {
+        return config.getInt("Experience_Formula." + StringUtils.getCapitalized(type.toString()) + "_Values.base");
+    }
+
+    public double getExponent(FormulaType type) {
+        return config.getDouble("Experience_Formula." + StringUtils.getCapitalized(type.toString()) + "_Values.exponent");
+    }
 
 
     /* Global modifier */
     /* Global modifier */
-    public double getExperienceGainsGlobalMultiplier() { return config.getDouble("Experience_Formula.Multiplier.Global", 1.0); }
-    public void setExperienceGainsGlobalMultiplier(double value) { config.set("Experience_Formula.Multiplier.Global", value); }
+    public double getExperienceGainsGlobalMultiplier() {
+        return config.getDouble("Experience_Formula.Multiplier.Global", 1.0);
+    }
+
+    public void setExperienceGainsGlobalMultiplier(double value) {
+        config.set("Experience_Formula.Multiplier.Global", value);
+    }
 
 
     /* PVP modifier */
     /* PVP modifier */
-    public double getPlayerVersusPlayerXP() { return config.getDouble("Experience_Formula.Multiplier.PVP", 1.0); }
+    public double getPlayerVersusPlayerXP() {
+        return config.getDouble("Experience_Formula.Multiplier.PVP", 1.0);
+    }
 
 
     /* Spawned Mob modifier */
     /* Spawned Mob modifier */
-    public double getSpawnedMobXpMultiplier() { return config.getDouble("Experience_Formula.Mobspawners.Multiplier", 0.0); }
-    public double getEggXpMultiplier() { return config.getDouble("Experience_Formula.Eggs.Multiplier", 0.0); }
-    public double getTamedMobXpMultiplier() { return config.getDouble("Experience_Formula.Player_Tamed.Multiplier", 0.0); }
-    public double getNetherPortalXpMultiplier() { return config.getDouble("Experience_Formula.Nether_Portal.Multiplier", 0.0); }
-    public double getBredMobXpMultiplier() { return config.getDouble("Experience_Formula.Breeding.Multiplier", 1.0); }
+    public double getSpawnedMobXpMultiplier() {
+        return config.getDouble("Experience_Formula.Mobspawners.Multiplier", 0.0);
+    }
+
+    public double getEggXpMultiplier() {
+        return config.getDouble("Experience_Formula.Eggs.Multiplier", 0.0);
+    }
+
+    public double getTamedMobXpMultiplier() {
+        return config.getDouble("Experience_Formula.Player_Tamed.Multiplier", 0.0);
+    }
+
+    public double getNetherPortalXpMultiplier() {
+        return config.getDouble("Experience_Formula.Nether_Portal.Multiplier", 0.0);
+    }
+
+    public double getBredMobXpMultiplier() {
+        return config.getDouble("Experience_Formula.Breeding.Multiplier", 1.0);
+    }
 
 
     /* Skill modifiers */
     /* Skill modifiers */
-    public double getFormulaSkillModifier(PrimarySkillType skill) { return config.getDouble("Experience_Formula.Modifier." + StringUtils.getCapitalized(skill.toString())); }
+    public double getFormulaSkillModifier(PrimarySkillType skill) {
+        return config.getDouble("Experience_Formula.Modifier." + StringUtils.getCapitalized(skill.toString()));
+    }
 
 
     /* Custom XP perk */
     /* Custom XP perk */
-    public double getCustomXpPerkBoost() { return config.getDouble("Experience_Formula.Custom_XP_Perk.Boost", 1.25); }
+    public double getCustomXpPerkBoost() {
+        return config.getDouble("Experience_Formula.Custom_XP_Perk.Boost", 1.25);
+    }
 
 
     /* Diminished Returns */
     /* Diminished Returns */
-    public float getDiminishedReturnsCap() { return (float) config.getDouble("Dimished_Returns.Guaranteed_Minimum_Percentage", 0.05D); }
-    public boolean getDiminishedReturnsEnabled() { return config.getBoolean("Diminished_Returns.Enabled", false); }
-    public int getDiminishedReturnsThreshold(PrimarySkillType skill) { return config.getInt("Diminished_Returns.Threshold." + StringUtils.getCapitalized(skill.toString()), 20000); }
-    public int getDiminishedReturnsTimeInterval() { return config.getInt("Diminished_Returns.Time_Interval", 10); }
+    public float getDiminishedReturnsCap() {
+        return (float) config.getDouble("Dimished_Returns.Guaranteed_Minimum_Percentage", 0.05D);
+    }
+
+    public boolean getDiminishedReturnsEnabled() {
+        return config.getBoolean("Diminished_Returns.Enabled", false);
+    }
+
+    public int getDiminishedReturnsThreshold(PrimarySkillType skill) {
+        return config.getInt("Diminished_Returns.Threshold." + StringUtils.getCapitalized(skill.toString()), 20000);
+    }
+
+    public int getDiminishedReturnsTimeInterval() {
+        return config.getInt("Diminished_Returns.Time_Interval", 10);
+    }
 
 
     /* Conversion */
     /* Conversion */
-    public double getExpModifier() { return config.getDouble("Conversion.Exp_Modifier", 1); }
+    public double getExpModifier() {
+        return config.getDouble("Conversion.Exp_Modifier", 1);
+    }
 
 
     /*
     /*
      * XP SETTINGS
      * XP SETTINGS
      */
      */
 
 
     /* General Settings */
     /* General Settings */
-    public boolean getExperienceGainsPlayerVersusPlayerEnabled() { return config.getBoolean("Experience_Values.PVP.Rewards", true); }
+    public boolean getExperienceGainsPlayerVersusPlayerEnabled() {
+        return config.getBoolean("Experience_Values.PVP.Rewards", true);
+    }
 
 
     /* Combat XP Multipliers */
     /* Combat XP Multipliers */
-    public double getCombatXP(EntityType entity) { return config.getDouble("Experience_Values.Combat.Multiplier." + StringUtils.getPrettyEntityTypeString(entity).replace(" ", "_")); }
-    public double getAnimalsXP(EntityType entity) { return config.getDouble("Experience_Values.Combat.Multiplier." + StringUtils.getPrettyEntityTypeString(entity).replace(" ", "_"), getAnimalsXP()); }
-    public double getAnimalsXP() { return config.getDouble("Experience_Values.Combat.Multiplier.Animals", 1.0); }
-    public boolean hasCombatXP(EntityType entity) {return config.contains("Experience_Values.Combat.Multiplier." + StringUtils.getPrettyEntityTypeString(entity).replace(" ", "_")); }
+    public double getCombatXP(EntityType entity) {
+        return config.getDouble("Experience_Values.Combat.Multiplier." + StringUtils.getPrettyEntityTypeString(entity).replace(" ", "_"));
+    }
+
+    public double getAnimalsXP(EntityType entity) {
+        return config.getDouble("Experience_Values.Combat.Multiplier." + StringUtils.getPrettyEntityTypeString(entity).replace(" ", "_"), getAnimalsXP());
+    }
+
+    public double getAnimalsXP() {
+        return config.getDouble("Experience_Values.Combat.Multiplier.Animals", 1.0);
+    }
+
+    public boolean hasCombatXP(EntityType entity) {
+        return config.contains("Experience_Values.Combat.Multiplier." + StringUtils.getPrettyEntityTypeString(entity).replace(" ", "_"));
+    }
 
 
     /* 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)
         //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"))
+        if (material.toString().equalsIgnoreCase("LILY_PAD"))
             return 0;
             return 0;
 
 
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
@@ -230,8 +338,7 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
     }
     }
 
 
     /* Materials  */
     /* Materials  */
-    public int getXp(PrimarySkillType skill, BlockState blockState)
-    {
+    public int getXp(PrimarySkillType skill, BlockState blockState) {
         Material data = blockState.getType();
         Material data = blockState.getType();
 
 
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
@@ -248,8 +355,7 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
     }
     }
 
 
     /* Materials  */
     /* Materials  */
-    public int getXp(PrimarySkillType skill, Block block)
-    {
+    public int getXp(PrimarySkillType skill, Block block) {
         Material data = block.getType();
         Material data = block.getType();
 
 
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
@@ -266,8 +372,7 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
     }
     }
 
 
     /* Materials  */
     /* Materials  */
-    public int getXp(PrimarySkillType skill, BlockData data)
-    {
+    public int getXp(PrimarySkillType skill, BlockData data) {
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
         String explicitString = baseString + StringUtils.getExplicitConfigBlockDataString(data);
         String explicitString = baseString + StringUtils.getExplicitConfigBlockDataString(data);
         if (config.contains(explicitString))
         if (config.contains(explicitString))
@@ -281,8 +386,7 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
         return 0;
         return 0;
     }
     }
 
 
-    public boolean doesBlockGiveSkillXP(PrimarySkillType skill, Material data)
-    {
+    public boolean doesBlockGiveSkillXP(PrimarySkillType skill, Material data) {
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
         String explicitString = baseString + StringUtils.getExplicitConfigMaterialString(data);
         String explicitString = baseString + StringUtils.getExplicitConfigMaterialString(data);
         if (config.contains(explicitString))
         if (config.contains(explicitString))
@@ -294,8 +398,7 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
         return config.contains(wildcardString);
         return config.contains(wildcardString);
     }
     }
 
 
-    public boolean doesBlockGiveSkillXP(PrimarySkillType skill, BlockData data)
-    {
+    public boolean doesBlockGiveSkillXP(PrimarySkillType skill, BlockData data) {
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
         String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + ".";
         String explicitString = baseString + StringUtils.getExplicitConfigBlockDataString(data);
         String explicitString = baseString + StringUtils.getExplicitConfigBlockDataString(data);
         if (config.contains(explicitString))
         if (config.contains(explicitString))
@@ -311,32 +414,42 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
      * Experience Bar Stuff
      * Experience Bar Stuff
      */
      */
 
 
-    public boolean isPartyExperienceBarsEnabled()
-    {
+    public boolean isPartyExperienceBarsEnabled() {
         return config.getBoolean("Experience_Bars.Update.Party", true);
         return config.getBoolean("Experience_Bars.Update.Party", true);
     }
     }
 
 
-    public boolean isPassiveGainsExperienceBarsEnabled()
-    {
+    public boolean isPassiveGainsExperienceBarsEnabled() {
         return config.getBoolean("Experience_Bars.Update.Passive", true);
         return config.getBoolean("Experience_Bars.Update.Passive", true);
     }
     }
 
 
-    public boolean getDoExperienceBarsAlwaysUpdateTitle()
-    {
+    public boolean getDoExperienceBarsAlwaysUpdateTitle() {
         return config.getBoolean("Experience_Bars.ThisMayCauseLag.AlwaysUpdateTitlesWhenXPIsGained.Enable", false) || getAddExtraDetails();
         return config.getBoolean("Experience_Bars.ThisMayCauseLag.AlwaysUpdateTitlesWhenXPIsGained.Enable", false) || getAddExtraDetails();
     }
     }
 
 
-    public boolean getAddExtraDetails() { return config.getBoolean("Experience_Bars.ThisMayCauseLag.AlwaysUpdateTitlesWhenXPIsGained.ExtraDetails", false);}
-    public boolean isExperienceBarsEnabled() { return config.getBoolean("Experience_Bars.Enable", true); }
-    public boolean isExperienceBarEnabled(PrimarySkillType primarySkillType) { return config.getBoolean("Experience_Bars."+StringUtils.getCapitalized(primarySkillType.toString())+".Enable", true);}
+    public boolean getAddExtraDetails() {
+        return config.getBoolean("Experience_Bars.ThisMayCauseLag.AlwaysUpdateTitlesWhenXPIsGained.ExtraDetails", false);
+    }
+    public boolean useCombatHPCeiling() {
+        return config.getBoolean("ExploitFix.Combat.XPCeiling.Enabled", true);
+    }
+
+    public int getCombatHPCeiling() {
+        return config.getInt("ExploitFix.Combat.XPCeiling.HP_Modifier_Limit", 100);
+    }
 
 
-    public BarColor getExperienceBarColor(PrimarySkillType primarySkillType)
-    {
-        String colorValueFromConfig = config.getString("Experience_Bars."+StringUtils.getCapitalized(primarySkillType.toString())+".Color");
+    public boolean isExperienceBarsEnabled() {
+        return config.getBoolean("Experience_Bars.Enable", true);
+    }
 
 
-        for(BarColor barColor : BarColor.values())
-        {
-            if(barColor.toString().equalsIgnoreCase(colorValueFromConfig))
+    public boolean isExperienceBarEnabled(PrimarySkillType primarySkillType) {
+        return config.getBoolean("Experience_Bars." + StringUtils.getCapitalized(primarySkillType.toString()) + ".Enable", true);
+    }
+
+    public BarColor getExperienceBarColor(PrimarySkillType primarySkillType) {
+        String colorValueFromConfig = config.getString("Experience_Bars." + StringUtils.getCapitalized(primarySkillType.toString()) + ".Color");
+
+        for (BarColor barColor : BarColor.values()) {
+            if (barColor.toString().equalsIgnoreCase(colorValueFromConfig))
                 return barColor;
                 return barColor;
         }
         }
 
 
@@ -344,13 +457,11 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
         return BarColor.WHITE;
         return BarColor.WHITE;
     }
     }
 
 
-    public BarStyle getExperienceBarStyle(PrimarySkillType primarySkillType)
-    {
-        String colorValueFromConfig = config.getString("Experience_Bars."+StringUtils.getCapitalized(primarySkillType.toString())+".BarStyle");
+    public BarStyle getExperienceBarStyle(PrimarySkillType primarySkillType) {
+        String colorValueFromConfig = config.getString("Experience_Bars." + StringUtils.getCapitalized(primarySkillType.toString()) + ".BarStyle");
 
 
-        for(BarStyle barStyle : BarStyle.values())
-        {
-            if(barStyle.toString().equalsIgnoreCase(colorValueFromConfig))
+        for (BarStyle barStyle : BarStyle.values()) {
+            if (barStyle.toString().equalsIgnoreCase(colorValueFromConfig))
                 return barStyle;
                 return barStyle;
         }
         }
 
 
@@ -359,29 +470,51 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
     }
     }
 
 
     /* Acrobatics */
     /* Acrobatics */
-    public int getDodgeXPModifier() { return config.getInt("Experience_Values.Acrobatics.Dodge", 120); }
-    public int getRollXPModifier() { return config.getInt("Experience_Values.Acrobatics.Roll", 80); }
-    public int getFallXPModifier() { return config.getInt("Experience_Values.Acrobatics.Fall", 120); }
+    public int getDodgeXPModifier() {
+        return config.getInt("Experience_Values.Acrobatics.Dodge", 120);
+    }
+
+    public int getRollXPModifier() {
+        return config.getInt("Experience_Values.Acrobatics.Roll", 80);
+    }
+
+    public int getFallXPModifier() {
+        return config.getInt("Experience_Values.Acrobatics.Fall", 120);
+    }
 
 
-    public double getFeatherFallXPModifier() { return config.getDouble("Experience_Values.Acrobatics.FeatherFall_Multiplier", 2.0); }
+    public double getFeatherFallXPModifier() {
+        return config.getDouble("Experience_Values.Acrobatics.FeatherFall_Multiplier", 2.0);
+    }
 
 
     /* Alchemy */
     /* Alchemy */
-    public double getPotionXP(PotionStage stage) { return config.getDouble("Experience_Values.Alchemy.Potion_Stage_" + stage.toNumerical(), 10D); }
+    public double getPotionXP(PotionStage stage) {
+        return config.getDouble("Experience_Values.Alchemy.Potion_Stage_" + stage.toNumerical(), 10D);
+    }
 
 
     /* Archery */
     /* Archery */
-    public double getArcheryDistanceMultiplier() { return config.getDouble("Experience_Values.Archery.Distance_Multiplier", 0.025); }
+    public double getArcheryDistanceMultiplier() {
+        return config.getDouble("Experience_Values.Archery.Distance_Multiplier", 0.025);
+    }
 
 
-    public int getFishingShakeXP() { return config.getInt("Experience_Values.Fishing.Shake", 50); }
+    public int getFishingShakeXP() {
+        return config.getInt("Experience_Values.Fishing.Shake", 50);
+    }
 
 
     /* Repair */
     /* Repair */
-    public double getRepairXPBase() { return config.getDouble("Experience_Values.Repair.Base", 1000.0); }
-    public double getRepairXP(MaterialType repairMaterialType) { return config.getDouble("Experience_Values.Repair." + StringUtils.getCapitalized(repairMaterialType.toString())); }
+    public double getRepairXPBase() {
+        return config.getDouble("Experience_Values.Repair.Base", 1000.0);
+    }
+
+    public double getRepairXP(MaterialType repairMaterialType) {
+        return config.getDouble("Experience_Values.Repair." + StringUtils.getCapitalized(repairMaterialType.toString()));
+    }
 
 
     /* Taming */
     /* Taming */
-    public int getTamingXP(EntityType type)
-    {
+    public int getTamingXP(EntityType type) {
         return config.getInt("Experience_Values.Taming.Animal_Taming." + StringUtils.getPrettyEntityTypeString(type));
         return config.getInt("Experience_Values.Taming.Animal_Taming." + StringUtils.getPrettyEntityTypeString(type));
     }
     }
 
 
-    public boolean preventStoneLavaFarming() { return config.getBoolean("ExploitFix.LavaStoneAndCobbleFarming", true);}
+    public boolean preventStoneLavaFarming() {
+        return config.getBoolean("ExploitFix.LavaStoneAndCobbleFarming", true);
+    }
 }
 }

+ 7 - 8
src/main/java/com/gmail/nossr50/config/mods/CustomArmorConfig.java

@@ -3,6 +3,7 @@ package com.gmail.nossr50.config.mods;
 import com.gmail.nossr50.config.ConfigLoader;
 import com.gmail.nossr50.config.ConfigLoader;
 import com.gmail.nossr50.datatypes.skills.ItemType;
 import com.gmail.nossr50.datatypes.skills.ItemType;
 import com.gmail.nossr50.datatypes.skills.MaterialType;
 import com.gmail.nossr50.datatypes.skills.MaterialType;
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.repair.repairables.Repairable;
 import com.gmail.nossr50.skills.repair.repairables.Repairable;
 import com.gmail.nossr50.skills.repair.repairables.RepairableFactory;
 import com.gmail.nossr50.skills.repair.repairables.RepairableFactory;
 import org.bukkit.Material;
 import org.bukkit.Material;
@@ -13,14 +14,12 @@ import java.util.List;
 import java.util.Set;
 import java.util.Set;
 
 
 public class CustomArmorConfig extends ConfigLoader {
 public class CustomArmorConfig extends ConfigLoader {
-    private boolean needsUpdate = false;
-
-    public List<Material> customBoots       = new ArrayList<>();
+    public List<Material> customBoots = new ArrayList<>();
     public List<Material> customChestplates = new ArrayList<>();
     public List<Material> customChestplates = new ArrayList<>();
-    public List<Material> customHelmets     = new ArrayList<>();
-    public List<Material> customLeggings    = new ArrayList<>();
-
+    public List<Material> customHelmets = new ArrayList<>();
+    public List<Material> customLeggings = new ArrayList<>();
     public List<Repairable> repairables = new ArrayList<>();
     public List<Repairable> repairables = new ArrayList<>();
+    private boolean needsUpdate = false;
 
 
     protected CustomArmorConfig(String fileName) {
     protected CustomArmorConfig(String fileName) {
         super("mods", fileName);
         super("mods", fileName);
@@ -62,7 +61,7 @@ public class CustomArmorConfig extends ConfigLoader {
             Material armorMaterial = Material.matchMaterial(armorName);
             Material armorMaterial = Material.matchMaterial(armorName);
 
 
             if (armorMaterial == null) {
             if (armorMaterial == null) {
-                plugin.getLogger().warning("Invalid material name. This item will be skipped. - " + armorName);
+                mcMMO.p.getLogger().warning("Invalid material name. This item will be skipped. - " + armorName);
                 continue;
                 continue;
             }
             }
 
 
@@ -70,7 +69,7 @@ public class CustomArmorConfig extends ConfigLoader {
             Material repairMaterial = Material.matchMaterial(config.getString(armorType + "." + armorName + ".Repair_Material", ""));
             Material repairMaterial = Material.matchMaterial(config.getString(armorType + "." + armorName + ".Repair_Material", ""));
 
 
             if (repairable && (repairMaterial == null)) {
             if (repairable && (repairMaterial == null)) {
-                plugin.getLogger().warning("Incomplete repair information. This item will be unrepairable. - " + armorName);
+                mcMMO.p.getLogger().warning("Incomplete repair information. This item will be unrepairable. - " + armorName);
                 repairable = false;
                 repairable = false;
             }
             }
 
 

+ 12 - 15
src/main/java/com/gmail/nossr50/config/mods/CustomBlockConfig.java

@@ -2,6 +2,7 @@ package com.gmail.nossr50.config.mods;
 
 
 import com.gmail.nossr50.config.ConfigLoader;
 import com.gmail.nossr50.config.ConfigLoader;
 import com.gmail.nossr50.datatypes.mods.CustomBlock;
 import com.gmail.nossr50.datatypes.mods.CustomBlock;
+import com.gmail.nossr50.mcMMO;
 import org.bukkit.Material;
 import org.bukkit.Material;
 import org.bukkit.configuration.ConfigurationSection;
 import org.bukkit.configuration.ConfigurationSection;
 
 
@@ -11,17 +12,15 @@ import java.util.List;
 import java.util.Set;
 import java.util.Set;
 
 
 public class CustomBlockConfig extends ConfigLoader {
 public class CustomBlockConfig extends ConfigLoader {
-    private boolean needsUpdate = false;
-
-    public List<Material> customExcavationBlocks  = new ArrayList<>();
-    public List<Material> customHerbalismBlocks   = new ArrayList<>();
-    public List<Material> customMiningBlocks      = new ArrayList<>();
-    public List<Material> customOres              = new ArrayList<>();
-    public List<Material> customLogs              = new ArrayList<>();
-    public List<Material> customLeaves            = new ArrayList<>();
-    public List<Material> customAbilityBlocks     = new ArrayList<>();
-
+    public List<Material> customExcavationBlocks = new ArrayList<>();
+    public List<Material> customHerbalismBlocks = new ArrayList<>();
+    public List<Material> customMiningBlocks = new ArrayList<>();
+    public List<Material> customOres = new ArrayList<>();
+    public List<Material> customLogs = new ArrayList<>();
+    public List<Material> customLeaves = new ArrayList<>();
+    public List<Material> customAbilityBlocks = new ArrayList<>();
     public HashMap<Material, CustomBlock> customBlockMap = new HashMap<>();
     public HashMap<Material, CustomBlock> customBlockMap = new HashMap<>();
+    private boolean needsUpdate = false;
 
 
     protected CustomBlockConfig(String fileName) {
     protected CustomBlockConfig(String fileName) {
         super("mods", fileName);
         super("mods", fileName);
@@ -66,7 +65,7 @@ public class CustomBlockConfig extends ConfigLoader {
             Material blockMaterial = Material.matchMaterial(blockInfo[0]);
             Material blockMaterial = Material.matchMaterial(blockInfo[0]);
 
 
             if (blockMaterial == null) {
             if (blockMaterial == null) {
-                plugin.getLogger().warning("Invalid material name. This item will be skipped. - " + blockInfo[0]);
+                mcMMO.p.getLogger().warning("Invalid material name. This item will be skipped. - " + blockInfo[0]);
                 continue;
                 continue;
             }
             }
 
 
@@ -84,12 +83,10 @@ public class CustomBlockConfig extends ConfigLoader {
             if (skillType.equals("Mining") && config.getBoolean(skillType + "." + blockName + ".Is_Ore")) {
             if (skillType.equals("Mining") && config.getBoolean(skillType + "." + blockName + ".Is_Ore")) {
                 customOres.add(blockMaterial);
                 customOres.add(blockMaterial);
                 smeltingXp = config.getInt(skillType + "." + blockName + ".Smelting_XP_Gain", xp / 10);
                 smeltingXp = config.getInt(skillType + "." + blockName + ".Smelting_XP_Gain", xp / 10);
-            }
-            else if (skillType.equals("Woodcutting")) {
+            } else if (skillType.equals("Woodcutting")) {
                 if (config.getBoolean(skillType + "." + blockName + ".Is_Log")) {
                 if (config.getBoolean(skillType + "." + blockName + ".Is_Log")) {
                     customLogs.add(blockMaterial);
                     customLogs.add(blockMaterial);
-                }
-                else {
+                } else {
                     customLeaves.add(blockMaterial);
                     customLeaves.add(blockMaterial);
                     xp = 0; // Leaves don't grant XP
                     xp = 0; // Leaves don't grant XP
                 }
                 }

+ 6 - 6
src/main/java/com/gmail/nossr50/config/mods/CustomEntityConfig.java

@@ -2,6 +2,7 @@ package com.gmail.nossr50.config.mods;
 
 
 import com.gmail.nossr50.config.ConfigLoader;
 import com.gmail.nossr50.config.ConfigLoader;
 import com.gmail.nossr50.datatypes.mods.CustomEntity;
 import com.gmail.nossr50.datatypes.mods.CustomEntity;
+import com.gmail.nossr50.mcMMO;
 import org.apache.commons.lang.ClassUtils;
 import org.apache.commons.lang.ClassUtils;
 import org.bukkit.Material;
 import org.bukkit.Material;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.ItemStack;
@@ -10,7 +11,7 @@ import java.util.HashMap;
 
 
 public class CustomEntityConfig extends ConfigLoader {
 public class CustomEntityConfig extends ConfigLoader {
     public HashMap<String, CustomEntity> customEntityClassMap = new HashMap<>();
     public HashMap<String, CustomEntity> customEntityClassMap = new HashMap<>();
-    public HashMap<String, CustomEntity> customEntityTypeMap  = new HashMap<>();
+    public HashMap<String, CustomEntity> customEntityTypeMap = new HashMap<>();
 
 
     protected CustomEntityConfig(String fileName) {
     protected CustomEntityConfig(String fileName) {
         super("mods", fileName);
         super("mods", fileName);
@@ -30,10 +31,9 @@ public class CustomEntityConfig extends ConfigLoader {
 
 
             try {
             try {
                 clazz = ClassUtils.getClass(className);
                 clazz = ClassUtils.getClass(className);
-            }
-            catch (ClassNotFoundException e) {
-                plugin.getLogger().warning("Invalid class (" + className + ") detected for " + entityName + ".");
-                plugin.getLogger().warning("This custom entity may not function properly.");
+            } catch (ClassNotFoundException e) {
+                mcMMO.p.getLogger().warning("Invalid class (" + className + ") detected for " + entityName + ".");
+                mcMMO.p.getLogger().warning("This custom entity may not function properly.");
             }
             }
 
 
             String entityTypeName = entityName.replace("_", ".");
             String entityTypeName = entityName.replace("_", ".");
@@ -48,7 +48,7 @@ public class CustomEntityConfig extends ConfigLoader {
             int callOfTheWildAmount = config.getInt(entityName + ".COTW_Material_Amount");
             int callOfTheWildAmount = config.getInt(entityName + ".COTW_Material_Amount");
 
 
             if (canBeSummoned && (callOfTheWildMaterial == null || callOfTheWildAmount == 0)) {
             if (canBeSummoned && (callOfTheWildMaterial == null || callOfTheWildAmount == 0)) {
-                plugin.getLogger().warning("Incomplete Call of the Wild information. This entity will not be able to be summoned by Call of the Wild.");
+                mcMMO.p.getLogger().warning("Incomplete Call of the Wild information. This entity will not be able to be summoned by Call of the Wild.");
                 canBeSummoned = false;
                 canBeSummoned = false;
             }
             }
 
 

+ 9 - 11
src/main/java/com/gmail/nossr50/config/mods/CustomToolConfig.java

@@ -4,6 +4,7 @@ import com.gmail.nossr50.config.ConfigLoader;
 import com.gmail.nossr50.datatypes.mods.CustomTool;
 import com.gmail.nossr50.datatypes.mods.CustomTool;
 import com.gmail.nossr50.datatypes.skills.ItemType;
 import com.gmail.nossr50.datatypes.skills.ItemType;
 import com.gmail.nossr50.datatypes.skills.MaterialType;
 import com.gmail.nossr50.datatypes.skills.MaterialType;
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.repair.repairables.Repairable;
 import com.gmail.nossr50.skills.repair.repairables.Repairable;
 import com.gmail.nossr50.skills.repair.repairables.RepairableFactory;
 import com.gmail.nossr50.skills.repair.repairables.RepairableFactory;
 import org.bukkit.Material;
 import org.bukkit.Material;
@@ -15,18 +16,15 @@ import java.util.List;
 import java.util.Set;
 import java.util.Set;
 
 
 public class CustomToolConfig extends ConfigLoader {
 public class CustomToolConfig extends ConfigLoader {
-    private boolean needsUpdate = false;
-
-    public List<Material> customAxes     = new ArrayList<>();
-    public List<Material> customBows     = new ArrayList<>();
-    public List<Material> customHoes     = new ArrayList<>();
+    public List<Material> customAxes = new ArrayList<>();
+    public List<Material> customBows = new ArrayList<>();
+    public List<Material> customHoes = new ArrayList<>();
     public List<Material> customPickaxes = new ArrayList<>();
     public List<Material> customPickaxes = new ArrayList<>();
-    public List<Material> customShovels  = new ArrayList<>();
-    public List<Material> customSwords   = new ArrayList<>();
-
+    public List<Material> customShovels = new ArrayList<>();
+    public List<Material> customSwords = new ArrayList<>();
     public HashMap<Material, CustomTool> customToolMap = new HashMap<>();
     public HashMap<Material, CustomTool> customToolMap = new HashMap<>();
-
     public List<Repairable> repairables = new ArrayList<>();
     public List<Repairable> repairables = new ArrayList<>();
+    private boolean needsUpdate = false;
 
 
     protected CustomToolConfig(String fileName) {
     protected CustomToolConfig(String fileName) {
         super("mods", fileName);
         super("mods", fileName);
@@ -70,7 +68,7 @@ public class CustomToolConfig extends ConfigLoader {
             Material toolMaterial = Material.matchMaterial(toolName);
             Material toolMaterial = Material.matchMaterial(toolName);
 
 
             if (toolMaterial == null) {
             if (toolMaterial == null) {
-                plugin.getLogger().warning("Invalid material name. This item will be skipped. - " + toolName);
+                mcMMO.p.getLogger().warning("Invalid material name. This item will be skipped. - " + toolName);
                 continue;
                 continue;
             }
             }
 
 
@@ -78,7 +76,7 @@ public class CustomToolConfig extends ConfigLoader {
             Material repairMaterial = Material.matchMaterial(config.getString(toolType + "." + toolName + ".Repair_Material", ""));
             Material repairMaterial = Material.matchMaterial(config.getString(toolType + "." + toolName + ".Repair_Material", ""));
 
 
             if (repairable && (repairMaterial == null)) {
             if (repairable && (repairMaterial == null)) {
-                plugin.getLogger().warning("Incomplete repair information. This item will be unrepairable. - " + toolName);
+                mcMMO.p.getLogger().warning("Incomplete repair information. This item will be unrepairable. - " + toolName);
                 repairable = false;
                 repairable = false;
             }
             }
 
 

+ 4 - 3
src/main/java/com/gmail/nossr50/config/party/ItemWeightConfig.java

@@ -1,13 +1,13 @@
 package com.gmail.nossr50.config.party;
 package com.gmail.nossr50.config.party;
 
 
-import com.gmail.nossr50.config.ConfigLoader;
+import com.gmail.nossr50.config.BukkitConfig;
 import com.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.StringUtils;
 import org.bukkit.Material;
 import org.bukkit.Material;
 
 
 import java.util.HashSet;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.Locale;
 
 
-public class ItemWeightConfig extends ConfigLoader {
+public class ItemWeightConfig extends BukkitConfig {
     private static ItemWeightConfig instance;
     private static ItemWeightConfig instance;
 
 
     private ItemWeightConfig() {
     private ItemWeightConfig() {
@@ -40,5 +40,6 @@ public class ItemWeightConfig extends ConfigLoader {
     }
     }
 
 
     @Override
     @Override
-    protected void loadKeys() {}
+    protected void loadKeys() {
+    }
 }
 }

+ 14 - 19
src/main/java/com/gmail/nossr50/config/skills/alchemy/PotionConfig.java

@@ -95,8 +95,7 @@ public class PotionConfig extends ConfigLoader {
             if (potion != null) {
             if (potion != null) {
                 potionMap.put(potionName, potion);
                 potionMap.put(potionName, potion);
                 pass++;
                 pass++;
-            }
-            else {
+            } else {
                 fail++;
                 fail++;
             }
             }
         }
         }
@@ -114,13 +113,13 @@ public class PotionConfig extends ConfigLoader {
      */
      */
     private AlchemyPotion loadPotion(ConfigurationSection potion_section) {
     private AlchemyPotion loadPotion(ConfigurationSection potion_section) {
         try {
         try {
-            
+
 
 
             String name = potion_section.getString("Name");
             String name = potion_section.getString("Name");
             if (name != null) {
             if (name != null) {
                 name = ChatColor.translateAlternateColorCodes('&', name);
                 name = ChatColor.translateAlternateColorCodes('&', name);
             }
             }
-            
+
             PotionData data;
             PotionData data;
             if (!potion_section.contains("PotionData")) { // Backwards config compatability
             if (!potion_section.contains("PotionData")) { // Backwards config compatability
                 short dataValue = Short.parseShort(potion_section.getName());
                 short dataValue = Short.parseShort(potion_section.getName());
@@ -130,7 +129,7 @@ public class PotionConfig extends ConfigLoader {
                 ConfigurationSection potionData = potion_section.getConfigurationSection("PotionData");
                 ConfigurationSection potionData = potion_section.getConfigurationSection("PotionData");
                 data = new PotionData(PotionType.valueOf(potionData.getString("PotionType", "WATER")), potionData.getBoolean("Extended", false), potionData.getBoolean("Upgraded", false));
                 data = new PotionData(PotionType.valueOf(potionData.getString("PotionType", "WATER")), potionData.getBoolean("Extended", false), potionData.getBoolean("Upgraded", false));
             }
             }
-            
+
             Material material = Material.POTION;
             Material material = Material.POTION;
             String mat = potion_section.getString("Material", null);
             String mat = potion_section.getString("Material", null);
             if (mat != null) {
             if (mat != null) {
@@ -155,18 +154,16 @@ public class PotionConfig extends ConfigLoader {
 
 
                     if (type != null) {
                     if (type != null) {
                         effects.add(new PotionEffect(type, duration, amplifier));
                         effects.add(new PotionEffect(type, duration, amplifier));
-                    }
-                    else {
+                    } else {
                         mcMMO.p.getLogger().warning("Failed to parse effect for potion " + name + ": " + effect);
                         mcMMO.p.getLogger().warning("Failed to parse effect for potion " + name + ": " + effect);
                     }
                     }
                 }
                 }
             }
             }
-            
+
             Color color;
             Color color;
             if (potion_section.contains("Color")) {
             if (potion_section.contains("Color")) {
                 color = Color.fromRGB(potion_section.getInt("Color"));
                 color = Color.fromRGB(potion_section.getInt("Color"));
-            }
-            else {
+            } else {
                 color = this.generateColor(effects);
                 color = this.generateColor(effects);
             }
             }
 
 
@@ -176,16 +173,14 @@ public class PotionConfig extends ConfigLoader {
                     ItemStack ingredient = loadIngredient(child);
                     ItemStack ingredient = loadIngredient(child);
                     if (ingredient != null) {
                     if (ingredient != null) {
                         children.put(ingredient, potion_section.getConfigurationSection("Children").getString(child));
                         children.put(ingredient, potion_section.getConfigurationSection("Children").getString(child));
-                    }
-                    else {
+                    } else {
                         mcMMO.p.getLogger().warning("Failed to parse child for potion " + name + ": " + child);
                         mcMMO.p.getLogger().warning("Failed to parse child for potion " + name + ": " + child);
                     }
                     }
                 }
                 }
             }
             }
 
 
             return new AlchemyPotion(material, data, name, lore, effects, color, children);
             return new AlchemyPotion(material, data, name, lore, effects, color, children);
-        }
-        catch (Exception e) {
+        } catch (Exception e) {
             mcMMO.p.getLogger().warning("Failed to load Alchemy potion: " + potion_section.getName());
             mcMMO.p.getLogger().warning("Failed to load Alchemy potion: " + potion_section.getName());
             return null;
             return null;
         }
         }
@@ -243,7 +238,7 @@ public class PotionConfig extends ConfigLoader {
     public AlchemyPotion getPotion(String name) {
     public AlchemyPotion getPotion(String name) {
         return potionMap.get(name);
         return potionMap.get(name);
     }
     }
-    
+
     public AlchemyPotion getPotion(ItemStack item) {
     public AlchemyPotion getPotion(ItemStack item) {
         for (AlchemyPotion potion : potionMap.values()) {
         for (AlchemyPotion potion : potionMap.values()) {
             if (potion.isSimilar(item)) {
             if (potion.isSimilar(item)) {
@@ -252,7 +247,7 @@ public class PotionConfig extends ConfigLoader {
         }
         }
         return null;
         return null;
     }
     }
-    
+
     public Color generateColor(List<PotionEffect> effects) {
     public Color generateColor(List<PotionEffect> effects) {
         if (effects != null && !effects.isEmpty()) {
         if (effects != null && !effects.isEmpty()) {
             List<Color> colors = new ArrayList<>();
             List<Color> colors = new ArrayList<>();
@@ -270,7 +265,7 @@ public class PotionConfig extends ConfigLoader {
         }
         }
         return null;
         return null;
     }
     }
-    
+
     public Color calculateAverageColor(List<Color> colors) {
     public Color calculateAverageColor(List<Color> colors) {
         int red = 0;
         int red = 0;
         int green = 0;
         int green = 0;
@@ -280,7 +275,7 @@ public class PotionConfig extends ConfigLoader {
             green += color.getGreen();
             green += color.getGreen();
             blue += color.getBlue();
             blue += color.getBlue();
         }
         }
-        return Color.fromRGB(red/colors.size(), green/colors.size(), blue/colors.size());
+        return Color.fromRGB(red / colors.size(), green / colors.size(), blue / colors.size());
     }
     }
-    
+
 }
 }

+ 18 - 29
src/main/java/com/gmail/nossr50/config/skills/repair/RepairConfig.java

@@ -1,6 +1,6 @@
 package com.gmail.nossr50.config.skills.repair;
 package com.gmail.nossr50.config.skills.repair;
 
 
-import com.gmail.nossr50.config.ConfigLoader;
+import com.gmail.nossr50.config.BukkitConfig;
 import com.gmail.nossr50.datatypes.skills.ItemType;
 import com.gmail.nossr50.datatypes.skills.ItemType;
 import com.gmail.nossr50.datatypes.skills.MaterialType;
 import com.gmail.nossr50.datatypes.skills.MaterialType;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
@@ -13,9 +13,9 @@ import org.bukkit.inventory.ItemStack;
 
 
 import java.util.*;
 import java.util.*;
 
 
-public class RepairConfig extends ConfigLoader {
-    private List<Repairable> repairables;
+public class RepairConfig extends BukkitConfig {
     private final HashSet<String> notSupported;
     private final HashSet<String> notSupported;
+    private List<Repairable> repairables;
 
 
     public RepairConfig(String fileName) {
     public RepairConfig(String fileName) {
         super(fileName);
         super(fileName);
@@ -62,33 +62,25 @@ public class RepairConfig extends ConfigLoader {
 
 
                 if (ItemUtils.isWoodTool(repairItem)) {
                 if (ItemUtils.isWoodTool(repairItem)) {
                     repairMaterialType = MaterialType.WOOD;
                     repairMaterialType = MaterialType.WOOD;
-                }
-                else if (ItemUtils.isStoneTool(repairItem)) {
+                } else if (ItemUtils.isStoneTool(repairItem)) {
                     repairMaterialType = MaterialType.STONE;
                     repairMaterialType = MaterialType.STONE;
-                }
-                else if (ItemUtils.isStringTool(repairItem)) {
+                } else if (ItemUtils.isStringTool(repairItem)) {
                     repairMaterialType = MaterialType.STRING;
                     repairMaterialType = MaterialType.STRING;
-                }
-                else if (ItemUtils.isLeatherArmor(repairItem)) {
+                } else if (ItemUtils.isLeatherArmor(repairItem)) {
                     repairMaterialType = MaterialType.LEATHER;
                     repairMaterialType = MaterialType.LEATHER;
-                }
-                else if (ItemUtils.isIronArmor(repairItem) || ItemUtils.isIronTool(repairItem)) {
+                } else if (ItemUtils.isIronArmor(repairItem) || ItemUtils.isIronTool(repairItem)) {
                     repairMaterialType = MaterialType.IRON;
                     repairMaterialType = MaterialType.IRON;
-                }
-                else if (ItemUtils.isGoldArmor(repairItem) || ItemUtils.isGoldTool(repairItem)) {
+                } else if (ItemUtils.isGoldArmor(repairItem) || ItemUtils.isGoldTool(repairItem)) {
                     repairMaterialType = MaterialType.GOLD;
                     repairMaterialType = MaterialType.GOLD;
-                }
-                else if (ItemUtils.isDiamondArmor(repairItem) || ItemUtils.isDiamondTool(repairItem)) {
+                } else if (ItemUtils.isDiamondArmor(repairItem) || ItemUtils.isDiamondTool(repairItem)) {
                     repairMaterialType = MaterialType.DIAMOND;
                     repairMaterialType = MaterialType.DIAMOND;
                 } else if (ItemUtils.isNetheriteArmor(repairItem) || ItemUtils.isNetheriteTool(repairItem)) {
                 } else if (ItemUtils.isNetheriteArmor(repairItem) || ItemUtils.isNetheriteTool(repairItem)) {
                     repairMaterialType = MaterialType.NETHERITE;
                     repairMaterialType = MaterialType.NETHERITE;
                 }
                 }
-            }
-            else {
+            } else {
                 try {
                 try {
                     repairMaterialType = MaterialType.valueOf(repairMaterialTypeString);
                     repairMaterialType = MaterialType.valueOf(repairMaterialTypeString);
-                }
-                catch (IllegalArgumentException ex) {
+                } catch (IllegalArgumentException ex) {
                     reason.add(key + " has an invalid MaterialType of " + repairMaterialTypeString);
                     reason.add(key + " has an invalid MaterialType of " + repairMaterialTypeString);
                 }
                 }
             }
             }
@@ -122,16 +114,13 @@ public class RepairConfig extends ConfigLoader {
 
 
                 if (ItemUtils.isMinecraftTool(repairItem)) {
                 if (ItemUtils.isMinecraftTool(repairItem)) {
                     repairItemType = ItemType.TOOL;
                     repairItemType = ItemType.TOOL;
-                }
-                else if (ItemUtils.isArmor(repairItem)) {
+                } else if (ItemUtils.isArmor(repairItem)) {
                     repairItemType = ItemType.ARMOR;
                     repairItemType = ItemType.ARMOR;
                 }
                 }
-            }
-            else {
+            } else {
                 try {
                 try {
                     repairItemType = ItemType.valueOf(repairItemTypeString);
                     repairItemType = ItemType.valueOf(repairItemTypeString);
-                }
-                catch (IllegalArgumentException ex) {
+                } catch (IllegalArgumentException ex) {
                     reason.add(key + " has an invalid ItemType of " + repairItemTypeString);
                     reason.add(key + " has an invalid ItemType of " + repairItemTypeString);
                 }
                 }
             }
             }
@@ -146,7 +135,7 @@ public class RepairConfig extends ConfigLoader {
             // Minimum Quantity
             // Minimum Quantity
             int minimumQuantity = config.getInt("Repairables." + key + ".MinimumQuantity");
             int minimumQuantity = config.getInt("Repairables." + key + ".MinimumQuantity");
 
 
-            if(minimumQuantity == 0) {
+            if (minimumQuantity == 0) {
                 minimumQuantity = -1;
                 minimumQuantity = -1;
             }
             }
 
 
@@ -158,13 +147,13 @@ public class RepairConfig extends ConfigLoader {
         //Report unsupported
         //Report unsupported
         StringBuilder stringBuilder = new StringBuilder();
         StringBuilder stringBuilder = new StringBuilder();
 
 
-        if(notSupported.size() > 0) {
+        if (notSupported.size() > 0) {
             stringBuilder.append("mcMMO found the following materials in the Repair config that are not supported by the version of Minecraft running on this server: ");
             stringBuilder.append("mcMMO found the following materials in the Repair config that are not supported by the version of Minecraft running on this server: ");
 
 
             for (Iterator<String> iterator = notSupported.iterator(); iterator.hasNext(); ) {
             for (Iterator<String> iterator = notSupported.iterator(); iterator.hasNext(); ) {
                 String unsupportedMaterial = iterator.next();
                 String unsupportedMaterial = iterator.next();
 
 
-                if(!iterator.hasNext()) {
+                if (!iterator.hasNext()) {
                     stringBuilder.append(unsupportedMaterial);
                     stringBuilder.append(unsupportedMaterial);
                 } else {
                 } else {
                     stringBuilder.append(unsupportedMaterial).append(", ");
                     stringBuilder.append(unsupportedMaterial).append(", ");
@@ -182,7 +171,7 @@ public class RepairConfig extends ConfigLoader {
 
 
     private boolean noErrorsInRepairable(List<String> issues) {
     private boolean noErrorsInRepairable(List<String> issues) {
         for (String issue : issues) {
         for (String issue : issues) {
-            plugin.getLogger().warning(issue);
+            mcMMO.p.getLogger().warning(issue);
         }
         }
 
 
         return issues.isEmpty();
         return issues.isEmpty();

+ 10 - 17
src/main/java/com/gmail/nossr50/config/skills/repair/RepairConfigManager.java

@@ -1,28 +1,28 @@
 package com.gmail.nossr50.config.skills.repair;
 package com.gmail.nossr50.config.skills.repair;
 
 
-import com.gmail.nossr50.datatypes.database.UpgradeType;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.repair.repairables.Repairable;
 import com.gmail.nossr50.skills.repair.repairables.Repairable;
-import com.gmail.nossr50.util.FixSpellingNetheriteUtil;
 
 
 import java.io.File;
 import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
 
 
 public class RepairConfigManager {
 public class RepairConfigManager {
-    private final List<Repairable> repairables = new ArrayList<>();
+    public static final String REPAIR_VANILLA_YML = "repair.vanilla.yml";
+    private static final Collection<Repairable> repairables = new HashSet<>();
 
 
     public RepairConfigManager(mcMMO plugin) {
     public RepairConfigManager(mcMMO plugin) {
         Pattern pattern = Pattern.compile("repair\\.(?:.+)\\.yml");
         Pattern pattern = Pattern.compile("repair\\.(?:.+)\\.yml");
         File dataFolder = plugin.getDataFolder();
         File dataFolder = plugin.getDataFolder();
-        File vanilla = new File(dataFolder, "repair.vanilla.yml");
 
 
-        if (!vanilla.exists()) {
-            plugin.saveResource("repair.vanilla.yml", false);
-        }
+        RepairConfig mainRepairConfig = new RepairConfig(REPAIR_VANILLA_YML);
+        repairables.addAll(mainRepairConfig.getLoadedRepairables());
 
 
         for (String fileName : dataFolder.list()) {
         for (String fileName : dataFolder.list()) {
+            if(fileName.equals(REPAIR_VANILLA_YML))
+                continue;
+
             if (!pattern.matcher(fileName).matches()) {
             if (!pattern.matcher(fileName).matches()) {
                 continue;
                 continue;
             }
             }
@@ -33,19 +33,12 @@ public class RepairConfigManager {
                 continue;
                 continue;
             }
             }
 
 
-
-            if(mcMMO.getUpgradeManager().shouldUpgrade(UpgradeType.FIX_SPELLING_NETHERITE_REPAIR)) {
-                //Check spelling mistakes (early versions of 1.16 support had Netherite misspelled)
-                plugin.getLogger().info("Checking for certain invalid material names in Repair config...");
-                FixSpellingNetheriteUtil.processFileCheck(mcMMO.p, fileName, UpgradeType.FIX_SPELLING_NETHERITE_REPAIR);
-            }
-
             RepairConfig rConfig = new RepairConfig(fileName);
             RepairConfig rConfig = new RepairConfig(fileName);
             repairables.addAll(rConfig.getLoadedRepairables());
             repairables.addAll(rConfig.getLoadedRepairables());
         }
         }
     }
     }
 
 
-    public List<Repairable> getLoadedRepairables() {
+    public Collection<Repairable> getLoadedRepairables() {
         return repairables;
         return repairables;
     }
     }
 }
 }

+ 25 - 36
src/main/java/com/gmail/nossr50/config/skills/salvage/SalvageConfig.java

@@ -1,6 +1,6 @@
 package com.gmail.nossr50.config.skills.salvage;
 package com.gmail.nossr50.config.skills.salvage;
 
 
-import com.gmail.nossr50.config.ConfigLoader;
+import com.gmail.nossr50.config.BukkitConfig;
 import com.gmail.nossr50.datatypes.database.UpgradeType;
 import com.gmail.nossr50.datatypes.database.UpgradeType;
 import com.gmail.nossr50.datatypes.skills.ItemType;
 import com.gmail.nossr50.datatypes.skills.ItemType;
 import com.gmail.nossr50.datatypes.skills.MaterialType;
 import com.gmail.nossr50.datatypes.skills.MaterialType;
@@ -16,9 +16,9 @@ import org.bukkit.inventory.ItemStack;
 import java.io.IOException;
 import java.io.IOException;
 import java.util.*;
 import java.util.*;
 
 
-public class SalvageConfig extends ConfigLoader {
-    private List<Salvageable> salvageables;
+public class SalvageConfig extends BukkitConfig {
     private final HashSet<String> notSupported;
     private final HashSet<String> notSupported;
+    private Set<Salvageable> salvageables;
 
 
     public SalvageConfig(String fileName) {
     public SalvageConfig(String fileName) {
         super(fileName);
         super(fileName);
@@ -28,7 +28,7 @@ public class SalvageConfig extends ConfigLoader {
 
 
     @Override
     @Override
     protected void loadKeys() {
     protected void loadKeys() {
-        salvageables = new ArrayList<>();
+        salvageables = new HashSet<>();
 
 
         if (!config.isConfigurationSection("Salvageables")) {
         if (!config.isConfigurationSection("Salvageables")) {
             mcMMO.p.getLogger().severe("Could not find Salvageables section in " + fileName);
             mcMMO.p.getLogger().severe("Could not find Salvageables section in " + fileName);
@@ -40,10 +40,10 @@ public class SalvageConfig extends ConfigLoader {
 
 
         //Original version of 1.16 support had maximum quantities that were bad, this fixes it
         //Original version of 1.16 support had maximum quantities that were bad, this fixes it
 
 
-        if(mcMMO.getUpgradeManager().shouldUpgrade(UpgradeType.FIX_NETHERITE_SALVAGE_QUANTITIES)) {
+        if (mcMMO.getUpgradeManager().shouldUpgrade(UpgradeType.FIX_NETHERITE_SALVAGE_QUANTITIES)) {
             mcMMO.p.getLogger().info("Fixing incorrect Salvage quantities on Netherite gear, this will only run once...");
             mcMMO.p.getLogger().info("Fixing incorrect Salvage quantities on Netherite gear, this will only run once...");
-            for(String namespacedkey : mcMMO.getMaterialMapStore().getNetheriteArmor()) {
-                config.set("Salvageables." + namespacedkey.toUpperCase() + ".MaximumQuantity", 4);
+            for (String namespacedkey : mcMMO.getMaterialMapStore().getNetheriteArmor()) {
+                config.set("Salvageables." + namespacedkey.toUpperCase() + ".MaximumQuantity", 4); //TODO: Doesn't make sense to default to 4 for everything
             }
             }
 
 
             try {
             try {
@@ -78,33 +78,25 @@ public class SalvageConfig extends ConfigLoader {
 
 
                 if (ItemUtils.isWoodTool(salvageItem)) {
                 if (ItemUtils.isWoodTool(salvageItem)) {
                     salvageMaterialType = MaterialType.WOOD;
                     salvageMaterialType = MaterialType.WOOD;
-                }
-                else if (ItemUtils.isStoneTool(salvageItem)) {
+                } else if (ItemUtils.isStoneTool(salvageItem)) {
                     salvageMaterialType = MaterialType.STONE;
                     salvageMaterialType = MaterialType.STONE;
-                }
-                else if (ItemUtils.isStringTool(salvageItem)) {
+                } else if (ItemUtils.isStringTool(salvageItem)) {
                     salvageMaterialType = MaterialType.STRING;
                     salvageMaterialType = MaterialType.STRING;
-                }
-                else if (ItemUtils.isLeatherArmor(salvageItem)) {
+                } else if (ItemUtils.isLeatherArmor(salvageItem)) {
                     salvageMaterialType = MaterialType.LEATHER;
                     salvageMaterialType = MaterialType.LEATHER;
-                }
-                else if (ItemUtils.isIronArmor(salvageItem) || ItemUtils.isIronTool(salvageItem)) {
+                } else if (ItemUtils.isIronArmor(salvageItem) || ItemUtils.isIronTool(salvageItem)) {
                     salvageMaterialType = MaterialType.IRON;
                     salvageMaterialType = MaterialType.IRON;
-                }
-                else if (ItemUtils.isGoldArmor(salvageItem) || ItemUtils.isGoldTool(salvageItem)) {
+                } else if (ItemUtils.isGoldArmor(salvageItem) || ItemUtils.isGoldTool(salvageItem)) {
                     salvageMaterialType = MaterialType.GOLD;
                     salvageMaterialType = MaterialType.GOLD;
-                }
-                else if (ItemUtils.isDiamondArmor(salvageItem) || ItemUtils.isDiamondTool(salvageItem)) {
+                } else if (ItemUtils.isDiamondArmor(salvageItem) || ItemUtils.isDiamondTool(salvageItem)) {
                     salvageMaterialType = MaterialType.DIAMOND;
                     salvageMaterialType = MaterialType.DIAMOND;
                 } else if (ItemUtils.isNetheriteTool(salvageItem) || ItemUtils.isNetheriteArmor(salvageItem)) {
                 } else if (ItemUtils.isNetheriteTool(salvageItem) || ItemUtils.isNetheriteArmor(salvageItem)) {
                     salvageMaterialType = MaterialType.NETHERITE;
                     salvageMaterialType = MaterialType.NETHERITE;
                 }
                 }
-            }
-            else {
+            } else {
                 try {
                 try {
                     salvageMaterialType = MaterialType.valueOf(salvageMaterialTypeString.replace(" ", "_").toUpperCase(Locale.ENGLISH));
                     salvageMaterialType = MaterialType.valueOf(salvageMaterialTypeString.replace(" ", "_").toUpperCase(Locale.ENGLISH));
-                }
-                catch (IllegalArgumentException ex) {
+                } catch (IllegalArgumentException ex) {
                     reason.add(key + " has an invalid MaterialType of " + salvageMaterialTypeString);
                     reason.add(key + " has an invalid MaterialType of " + salvageMaterialTypeString);
                 }
                 }
             }
             }
@@ -130,16 +122,13 @@ public class SalvageConfig extends ConfigLoader {
 
 
                 if (ItemUtils.isMinecraftTool(salvageItem)) {
                 if (ItemUtils.isMinecraftTool(salvageItem)) {
                     salvageItemType = ItemType.TOOL;
                     salvageItemType = ItemType.TOOL;
-                }
-                else if (ItemUtils.isArmor(salvageItem)) {
+                } else if (ItemUtils.isArmor(salvageItem)) {
                     salvageItemType = ItemType.ARMOR;
                     salvageItemType = ItemType.ARMOR;
                 }
                 }
-            }
-            else {
+            } else {
                 try {
                 try {
                     salvageItemType = ItemType.valueOf(salvageItemTypeString.replace(" ", "_").toUpperCase(Locale.ENGLISH));
                     salvageItemType = ItemType.valueOf(salvageItemTypeString.replace(" ", "_").toUpperCase(Locale.ENGLISH));
-                }
-                catch (IllegalArgumentException ex) {
+                } catch (IllegalArgumentException ex) {
                     reason.add(key + " has an invalid ItemType of " + salvageItemTypeString);
                     reason.add(key + " has an invalid ItemType of " + salvageItemTypeString);
                 }
                 }
             }
             }
@@ -176,13 +165,13 @@ public class SalvageConfig extends ConfigLoader {
         //Report unsupported
         //Report unsupported
         StringBuilder stringBuilder = new StringBuilder();
         StringBuilder stringBuilder = new StringBuilder();
 
 
-        if(notSupported.size() > 0) {
+        if (notSupported.size() > 0) {
             stringBuilder.append("mcMMO found the following materials in the Salvage config that are not supported by the version of Minecraft running on this server: ");
             stringBuilder.append("mcMMO found the following materials in the Salvage config that are not supported by the version of Minecraft running on this server: ");
 
 
             for (Iterator<String> iterator = notSupported.iterator(); iterator.hasNext(); ) {
             for (Iterator<String> iterator = notSupported.iterator(); iterator.hasNext(); ) {
                 String unsupportedMaterial = iterator.next();
                 String unsupportedMaterial = iterator.next();
 
 
-                if(!iterator.hasNext()) {
+                if (!iterator.hasNext()) {
                     stringBuilder.append(unsupportedMaterial);
                     stringBuilder.append(unsupportedMaterial);
                 } else {
                 } else {
                     stringBuilder.append(unsupportedMaterial).append(", ");
                     stringBuilder.append(unsupportedMaterial).append(", ");
@@ -194,18 +183,18 @@ public class SalvageConfig extends ConfigLoader {
         }
         }
     }
     }
 
 
-    protected List<Salvageable> getLoadedSalvageables() {
-        return salvageables == null ? new ArrayList<>() : salvageables;
+    protected Collection<Salvageable> getLoadedSalvageables() {
+        return salvageables == null ? new HashSet<>() : salvageables;
     }
     }
 
 
     private boolean noErrorsInSalvageable(List<String> issues) {
     private boolean noErrorsInSalvageable(List<String> issues) {
         if (!issues.isEmpty()) {
         if (!issues.isEmpty()) {
-            plugin.getLogger().warning("Errors have been found in: " + fileName);
-            plugin.getLogger().warning("The following issues were found:");
+            mcMMO.p.getLogger().warning("Errors have been found in: " + fileName);
+            mcMMO.p.getLogger().warning("The following issues were found:");
         }
         }
 
 
         for (String issue : issues) {
         for (String issue : issues) {
-            plugin.getLogger().warning(issue);
+            mcMMO.p.getLogger().warning(issue);
         }
         }
 
 
         return issues.isEmpty();
         return issues.isEmpty();

+ 9 - 16
src/main/java/com/gmail/nossr50/config/skills/salvage/SalvageConfigManager.java

@@ -1,9 +1,7 @@
 package com.gmail.nossr50.config.skills.salvage;
 package com.gmail.nossr50.config.skills.salvage;
 
 
-import com.gmail.nossr50.datatypes.database.UpgradeType;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.salvage.salvageables.Salvageable;
 import com.gmail.nossr50.skills.salvage.salvageables.Salvageable;
-import com.gmail.nossr50.util.FixSpellingNetheriteUtil;
 
 
 import java.io.File;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.ArrayList;
@@ -11,18 +9,21 @@ import java.util.List;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
 
 
 public class SalvageConfigManager {
 public class SalvageConfigManager {
-    private final List<Salvageable> salvageables = new ArrayList<>();
+    public static final String SALVAGE_VANILLA_YML = "salvage.vanilla.yml";
+    private final List<Salvageable> salvageables = new ArrayList<>(); //TODO: Collision checking, make the list a set
+
 
 
     public SalvageConfigManager(mcMMO plugin) {
     public SalvageConfigManager(mcMMO plugin) {
         Pattern pattern = Pattern.compile("salvage\\.(?:.+)\\.yml");
         Pattern pattern = Pattern.compile("salvage\\.(?:.+)\\.yml");
         File dataFolder = plugin.getDataFolder();
         File dataFolder = plugin.getDataFolder();
-        File vanilla = new File(dataFolder, "salvage.vanilla.yml");
 
 
-        if (!vanilla.exists()) {
-            plugin.saveResource("salvage.vanilla.yml", false);
-        }
+        SalvageConfig mainSalvageConfig = new SalvageConfig(SALVAGE_VANILLA_YML);
+        salvageables.addAll(mainSalvageConfig.getLoadedSalvageables());
 
 
         for (String fileName : dataFolder.list()) {
         for (String fileName : dataFolder.list()) {
+            if(fileName.equals(SALVAGE_VANILLA_YML))
+                continue;
+
             if (!pattern.matcher(fileName).matches()) {
             if (!pattern.matcher(fileName).matches()) {
                 continue;
                 continue;
             }
             }
@@ -33,20 +34,12 @@ public class SalvageConfigManager {
                 continue;
                 continue;
             }
             }
 
 
-
-            if(mcMMO.getUpgradeManager().shouldUpgrade(UpgradeType.FIX_SPELLING_NETHERITE_SALVAGE)) {
-                //Check spelling mistakes (early versions of 1.16 support had Netherite misspelled)
-                plugin.getLogger().info("Checking for certain invalid material names in Salvage config...");
-                FixSpellingNetheriteUtil.processFileCheck(mcMMO.p, fileName, UpgradeType.FIX_SPELLING_NETHERITE_SALVAGE);
-            }
-
-
             SalvageConfig salvageConfig = new SalvageConfig(fileName);
             SalvageConfig salvageConfig = new SalvageConfig(fileName);
             salvageables.addAll(salvageConfig.getLoadedSalvageables());
             salvageables.addAll(salvageConfig.getLoadedSalvageables());
         }
         }
     }
     }
 
 
     public List<Salvageable> getLoadedSalvageables() {
     public List<Salvageable> getLoadedSalvageables() {
-        return salvageables;
+        return new ArrayList<>(salvageables);
     }
     }
 }
 }

+ 47 - 41
src/main/java/com/gmail/nossr50/config/treasure/FishingTreasureConfig.java

@@ -1,6 +1,6 @@
 package com.gmail.nossr50.config.treasure;
 package com.gmail.nossr50.config.treasure;
 
 
-import com.gmail.nossr50.config.ConfigLoader;
+import com.gmail.nossr50.config.BukkitConfig;
 import com.gmail.nossr50.datatypes.treasure.*;
 import com.gmail.nossr50.datatypes.treasure.*;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.EnchantmentUtils;
 import com.gmail.nossr50.util.EnchantmentUtils;
@@ -18,14 +18,14 @@ import org.jetbrains.annotations.NotNull;
 
 
 import java.util.*;
 import java.util.*;
 
 
-public class FishingTreasureConfig extends ConfigLoader {
+public class FishingTreasureConfig extends BukkitConfig {
 
 
     public static final String FILENAME = "fishing_treasures.yml";
     public static final String FILENAME = "fishing_treasures.yml";
     private static FishingTreasureConfig instance;
     private static FishingTreasureConfig instance;
 
 
-    public @NotNull HashMap<Rarity, List<FishingTreasure>>     fishingRewards      = new HashMap<>();
+    public @NotNull HashMap<Rarity, List<FishingTreasure>> fishingRewards = new HashMap<>();
     public @NotNull HashMap<Rarity, List<EnchantmentTreasure>> fishingEnchantments = new HashMap<>();
     public @NotNull HashMap<Rarity, List<EnchantmentTreasure>> fishingEnchantments = new HashMap<>();
-    public @NotNull HashMap<EntityType, List<ShakeTreasure>> shakeMap  = new HashMap<>();
+    public @NotNull HashMap<EntityType, List<ShakeTreasure>> shakeMap = new HashMap<>();
 
 
     private FishingTreasureConfig() {
     private FishingTreasureConfig() {
         super(FILENAME);
         super(FILENAME);
@@ -45,33 +45,39 @@ public class FishingTreasureConfig extends ConfigLoader {
     protected boolean validateKeys() {
     protected boolean validateKeys() {
         // Validate all the settings!
         // Validate all the settings!
         List<String> reason = new ArrayList<>();
         List<String> reason = new ArrayList<>();
-        for (String tier : config.getConfigurationSection("Enchantment_Drop_Rates").getKeys(false)) {
-            double totalEnchantDropRate = 0;
-            double totalItemDropRate = 0;
+        ConfigurationSection enchantment_drop_rates = config.getConfigurationSection("Enchantment_Drop_Rates");
 
 
-            for (Rarity rarity : Rarity.values()) {
-                double enchantDropRate = config.getDouble("Enchantment_Drop_Rates." + tier + "." + rarity.toString());
-                double itemDropRate = config.getDouble("Item_Drop_Rates." + tier + "." + rarity.toString());
+        if(enchantment_drop_rates != null) {
+            for (String tier : enchantment_drop_rates.getKeys(false)) {
+                double totalEnchantDropRate = 0;
+                double totalItemDropRate = 0;
 
 
-                if ((enchantDropRate < 0.0 || enchantDropRate > 100.0)) {
-                    reason.add("The enchant drop rate for " + tier + " items that are " + rarity.toString() + "should be between 0.0 and 100.0!");
-                }
+                for (Rarity rarity : Rarity.values()) {
+                    double enchantDropRate = config.getDouble("Enchantment_Drop_Rates." + tier + "." + rarity.toString());
+                    double itemDropRate = config.getDouble("Item_Drop_Rates." + tier + "." + rarity);
 
 
-                if (itemDropRate < 0.0 || itemDropRate > 100.0) {
-                    reason.add("The item drop rate for " + tier + " items that are " + rarity.toString() + "should be between 0.0 and 100.0!");
-                }
+                    if ((enchantDropRate < 0.0 || enchantDropRate > 100.0)) {
+                        reason.add("The enchant drop rate for " + tier + " items that are " + rarity + "should be between 0.0 and 100.0!");
+                    }
 
 
-                totalEnchantDropRate += enchantDropRate;
-                totalItemDropRate += itemDropRate;
-            }
+                    if (itemDropRate < 0.0 || itemDropRate > 100.0) {
+                        reason.add("The item drop rate for " + tier + " items that are " + rarity + "should be between 0.0 and 100.0!");
+                    }
 
 
-            if (totalEnchantDropRate < 0 || totalEnchantDropRate > 100.0) {
-                reason.add("The total enchant drop rate for " + tier + " should be between 0.0 and 100.0!");
-            }
+                    totalEnchantDropRate += enchantDropRate;
+                    totalItemDropRate += itemDropRate;
+                }
+
+                if (totalEnchantDropRate < 0 || totalEnchantDropRate > 100.0) {
+                    reason.add("The total enchant drop rate for " + tier + " should be between 0.0 and 100.0!");
+                }
 
 
-            if (totalItemDropRate < 0 || totalItemDropRate > 100.0) {
-                reason.add("The total item drop rate for " + tier + " should be between 0.0 and 100.0!");
+                if (totalItemDropRate < 0 || totalItemDropRate > 100.0) {
+                    reason.add("The total item drop rate for " + tier + " should be between 0.0 and 100.0!");
+                }
             }
             }
+        } else {
+            mcMMO.p.getLogger().warning("Your fishing treasures config is empty, is this intentional? Delete it to regenerate.");
         }
         }
 
 
         return noErrorsInConfig(reason);
         return noErrorsInConfig(reason);
@@ -89,7 +95,7 @@ public class FishingTreasureConfig extends ConfigLoader {
 
 
         for (EntityType entity : EntityType.values()) {
         for (EntityType entity : EntityType.values()) {
             if (entity.isAlive()) {
             if (entity.isAlive()) {
-                loadTreasures("Shake." + entity.toString());
+                loadTreasures("Shake." + entity);
             }
             }
         }
         }
     }
     }
@@ -164,7 +170,7 @@ public class FishingTreasureConfig extends ConfigLoader {
             }
             }
 
 
             if (dropLevel < 0) {
             if (dropLevel < 0) {
-                reason.add(treasureName + " has an invalid Drop_Level: " + dropLevel);
+                reason.add("Fishing Config: " + treasureName + " has an invalid Drop_Level: " + dropLevel);
             }
             }
 
 
             /*
             /*
@@ -175,7 +181,7 @@ public class FishingTreasureConfig extends ConfigLoader {
             if (isFishing) {
             if (isFishing) {
                 String rarityStr = config.getString(type + "." + treasureName + ".Rarity");
                 String rarityStr = config.getString(type + "." + treasureName + ".Rarity");
 
 
-                if(rarityStr != null) {
+                if (rarityStr != null) {
                     rarity = Rarity.getRarity(rarityStr);
                     rarity = Rarity.getRarity(rarityStr);
                 } else {
                 } else {
                     mcMMO.p.getLogger().severe("Please edit your config and add a Rarity definition for - " + treasureName);
                     mcMMO.p.getLogger().severe("Please edit your config and add a Rarity definition for - " + treasureName);
@@ -192,7 +198,7 @@ public class FishingTreasureConfig extends ConfigLoader {
 
 
             String customName = null;
             String customName = null;
 
 
-            if(hasCustomName(type, treasureName)) {
+            if (hasCustomName(type, treasureName)) {
                 customName = config.getString(type + "." + treasureName + ".Custom_Name");
                 customName = config.getString(type + "." + treasureName + ".Custom_Name");
             }
             }
 
 
@@ -204,7 +210,7 @@ public class FishingTreasureConfig extends ConfigLoader {
                     item = new ItemStack(mat, amount, data);
                     item = new ItemStack(mat, amount, data);
                     PotionMeta itemMeta = (PotionMeta) item.getItemMeta();
                     PotionMeta itemMeta = (PotionMeta) item.getItemMeta();
 
 
-                    if(itemMeta == null) {
+                    if (itemMeta == null) {
                         mcMMO.p.getLogger().severe("Item meta when adding potion to fishing treasure was null, contact the mcMMO devs!");
                         mcMMO.p.getLogger().severe("Item meta when adding potion to fishing treasure was null, contact the mcMMO devs!");
                         continue;
                         continue;
                     }
                     }
@@ -232,7 +238,7 @@ public class FishingTreasureConfig extends ConfigLoader {
                     }
                     }
                     item.setItemMeta(itemMeta);
                     item.setItemMeta(itemMeta);
                 }
                 }
-            } else if(material == Material.ENCHANTED_BOOK) {
+            } else if (material == Material.ENCHANTED_BOOK) {
                 //If any whitelisted enchants exist we use whitelist-based matching
                 //If any whitelisted enchants exist we use whitelist-based matching
                 item = new ItemStack(material, 1);
                 item = new ItemStack(material, 1);
                 ItemMeta itemMeta = item.getItemMeta();
                 ItemMeta itemMeta = item.getItemMeta();
@@ -276,7 +282,6 @@ public class FishingTreasureConfig extends ConfigLoader {
             }
             }
 
 
 
 
-
             if (noErrorsInConfig(reason)) {
             if (noErrorsInConfig(reason)) {
                 if (isFishing) {
                 if (isFishing) {
                     addFishingTreasure(rarity, new FishingTreasure(item, xp));
                     addFishingTreasure(rarity, new FishingTreasure(item, xp));
@@ -307,26 +312,27 @@ public class FishingTreasureConfig extends ConfigLoader {
     /**
     /**
      * Matches enchantments on a list (user provided string) to known enchantments in the Spigot API
      * Matches enchantments on a list (user provided string) to known enchantments in the Spigot API
      * Any matches are added to the passed set
      * Any matches are added to the passed set
+     *
      * @param enchantListStr the users string list of enchantments
      * @param enchantListStr the users string list of enchantments
      * @param permissiveList the permissive list of enchantments
      * @param permissiveList the permissive list of enchantments
      */
      */
     private void matchAndFillSet(@NotNull List<String> enchantListStr, @NotNull Set<Enchantment> permissiveList) {
     private void matchAndFillSet(@NotNull List<String> enchantListStr, @NotNull Set<Enchantment> permissiveList) {
-        if(enchantListStr.isEmpty()) {
+        if (enchantListStr.isEmpty()) {
             return;
             return;
         }
         }
 
 
-        for(String str : enchantListStr) {
+        for (String str : enchantListStr) {
             boolean foundMatch = false;
             boolean foundMatch = false;
-            for(Enchantment enchantment : Enchantment.values()) {
-                if(enchantment.getKey().getKey().equalsIgnoreCase(str)) {
+            for (Enchantment enchantment : Enchantment.values()) {
+                if (enchantment.getKey().getKey().equalsIgnoreCase(str)) {
                     permissiveList.add(enchantment);
                     permissiveList.add(enchantment);
                     foundMatch = true;
                     foundMatch = true;
                     break;
                     break;
                 }
                 }
             }
             }
 
 
-            if(!foundMatch) {
-                mcMMO.p.getLogger().info("[Fishing Treasure Init] Could not find any enchantments which matched the user defined enchantment named: "+str);
+            if (!foundMatch) {
+                mcMMO.p.getLogger().info("[Fishing Treasure Init] Could not find any enchantments which matched the user defined enchantment named: " + str);
             }
             }
         }
         }
     }
     }
@@ -344,11 +350,11 @@ public class FishingTreasureConfig extends ConfigLoader {
             }
             }
 
 
             for (String enchantmentName : enchantmentSection.getKeys(false)) {
             for (String enchantmentName : enchantmentSection.getKeys(false)) {
-                int level = config.getInt("Enchantments_Rarity." + rarity.toString() + "." + enchantmentName);
+                int level = config.getInt("Enchantments_Rarity." + rarity + "." + enchantmentName);
                 Enchantment enchantment = EnchantmentUtils.getByName(enchantmentName);
                 Enchantment enchantment = EnchantmentUtils.getByName(enchantmentName);
 
 
                 if (enchantment == null) {
                 if (enchantment == null) {
-                    plugin.getLogger().warning("Skipping invalid enchantment in " + FILENAME + ": " + enchantmentName);
+                    mcMMO.p.getLogger().warning("Skipping invalid enchantment in " + FILENAME + ": " + enchantmentName);
                     continue;
                     continue;
                 }
                 }
 
 
@@ -374,10 +380,10 @@ public class FishingTreasureConfig extends ConfigLoader {
     }
     }
 
 
     public double getItemDropRate(int tier, @NotNull Rarity rarity) {
     public double getItemDropRate(int tier, @NotNull Rarity rarity) {
-        return config.getDouble("Item_Drop_Rates.Tier_" + tier + "." + rarity.toString());
+        return config.getDouble("Item_Drop_Rates.Tier_" + tier + "." + rarity);
     }
     }
 
 
     public double getEnchantmentDropRate(int tier, @NotNull Rarity rarity) {
     public double getEnchantmentDropRate(int tier, @NotNull Rarity rarity) {
-        return config.getDouble("Enchantment_Drop_Rates.Tier_" + tier + "." + rarity.toString());
+        return config.getDouble("Enchantment_Drop_Rates.Tier_" + tier + "." + rarity);
     }
     }
 }
 }

+ 107 - 8
src/main/java/com/gmail/nossr50/config/treasure/TreasureConfig.java

@@ -1,8 +1,9 @@
 package com.gmail.nossr50.config.treasure;
 package com.gmail.nossr50.config.treasure;
 
 
-import com.gmail.nossr50.config.ConfigLoader;
+import com.gmail.nossr50.config.BukkitConfig;
 import com.gmail.nossr50.datatypes.treasure.ExcavationTreasure;
 import com.gmail.nossr50.datatypes.treasure.ExcavationTreasure;
 import com.gmail.nossr50.datatypes.treasure.HylianTreasure;
 import com.gmail.nossr50.datatypes.treasure.HylianTreasure;
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.text.StringUtils;
 import com.gmail.nossr50.util.text.StringUtils;
 import org.bukkit.ChatColor;
 import org.bukkit.ChatColor;
 import org.bukkit.Material;
 import org.bukkit.Material;
@@ -14,17 +15,24 @@ import org.bukkit.inventory.meta.PotionMeta;
 import org.bukkit.potion.PotionData;
 import org.bukkit.potion.PotionData;
 import org.bukkit.potion.PotionType;
 import org.bukkit.potion.PotionType;
 
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.List;
 
 
-public class TreasureConfig extends ConfigLoader {
+public class TreasureConfig extends BukkitConfig {
 
 
     public static final String FILENAME = "treasures.yml";
     public static final String FILENAME = "treasures.yml";
+    public static final String LEVEL_REQUIREMENT_RETRO_MODE = ".Level_Requirement.Retro_Mode";
+    public static final String LEVEL_REQUIREMENT_STANDARD_MODE = ".Level_Requirement.Standard_Mode";
+    public static final String WRONG_KEY_VALUE_STANDARD = ".Drop_Level.Standard_Mode";
+    public static final String WRONG_KEY_VALUE_RETRO = ".Drop_Level.Retro_Mode";
+    public static final String LEGACY_DROP_LEVEL = ".Drop_Level";
+    public static final String WRONG_KEY_ROOT = ".Drop_Level";
     private static TreasureConfig instance;
     private static TreasureConfig instance;
 
 
     public HashMap<String, List<ExcavationTreasure>> excavationMap = new HashMap<>();
     public HashMap<String, List<ExcavationTreasure>> excavationMap = new HashMap<>();
-    public HashMap<String, List<HylianTreasure>>    hylianMap = new HashMap<>();
+    public HashMap<String, List<HylianTreasure>> hylianMap = new HashMap<>();
 
 
     private TreasureConfig() {
     private TreasureConfig() {
         super(FILENAME);
         super(FILENAME);
@@ -60,6 +68,7 @@ public class TreasureConfig extends ConfigLoader {
     }
     }
 
 
     private void loadTreasures(String type) {
     private void loadTreasures(String type) {
+        boolean shouldWeUpdateFile = false;
         boolean isExcavation = type.equals("Excavation");
         boolean isExcavation = type.equals("Excavation");
         boolean isHylian = type.equals("Hylian_Luck");
         boolean isHylian = type.equals("Hylian_Luck");
 
 
@@ -103,7 +112,39 @@ public class TreasureConfig extends ConfigLoader {
 
 
             int xp = config.getInt(type + "." + treasureName + ".XP");
             int xp = config.getInt(type + "." + treasureName + ".XP");
             double dropChance = config.getDouble(type + "." + treasureName + ".Drop_Chance");
             double dropChance = config.getDouble(type + "." + treasureName + ".Drop_Chance");
-            int dropLevel = config.getInt(type + "." + treasureName + ".Drop_Level");
+            DropLevelKeyConversionType conversionType;
+
+            //Check for legacy drop level values and convert
+            if (getWrongKeyValue(type, treasureName, DropLevelKeyConversionType.LEGACY) != -1) {
+                //Legacy Drop level, needs to be converted
+                shouldWeUpdateFile = processAutomaticKeyConversion(type, shouldWeUpdateFile, treasureName, DropLevelKeyConversionType.LEGACY);
+            }
+
+            //Check for a bad key that was accidentally shipped out to some users
+            if (getWrongKeyValue(type, treasureName, DropLevelKeyConversionType.WRONG_KEY_STANDARD) != -1) {
+                //Partially converted to the new system, I had a dyslexic moment so some configs have this
+                shouldWeUpdateFile = processAutomaticKeyConversion(type, shouldWeUpdateFile, treasureName, DropLevelKeyConversionType.WRONG_KEY_STANDARD);
+            }
+
+            //Check for a bad key that was accidentally shipped out to some users
+            if (getWrongKeyValue(type, treasureName, DropLevelKeyConversionType.WRONG_KEY_RETRO) != -1) {
+                //Partially converted to the new system, I had a dyslexic moment so some configs have this
+                shouldWeUpdateFile = processAutomaticKeyConversion(type, shouldWeUpdateFile, treasureName, DropLevelKeyConversionType.WRONG_KEY_RETRO);
+            }
+
+            int dropLevel = -1;
+
+            if (mcMMO.isRetroModeEnabled()) {
+                dropLevel = config.getInt(type + "." + treasureName + LEVEL_REQUIREMENT_RETRO_MODE, -1);
+            } else {
+                dropLevel = config.getInt(type + "." + treasureName + LEVEL_REQUIREMENT_STANDARD_MODE, -1);
+            }
+
+            if (dropLevel == -1) {
+                mcMMO.p.getLogger().severe("Could not find a Level_Requirement entry for treasure " + treasureName);
+                mcMMO.p.getLogger().severe("Skipping treasure");
+                continue;
+            }
 
 
             if (xp < 0) {
             if (xp < 0) {
                 reason.add(treasureName + " has an invalid XP value: " + xp);
                 reason.add(treasureName + " has an invalid XP value: " + xp);
@@ -113,10 +154,6 @@ public class TreasureConfig extends ConfigLoader {
                 reason.add(treasureName + " has an invalid Drop_Chance: " + dropChance);
                 reason.add(treasureName + " has an invalid Drop_Chance: " + dropChance);
             }
             }
 
 
-            if (dropLevel < 0) {
-                reason.add(treasureName + " has an invalid Drop_Level: " + dropLevel);
-            }
-
             /*
             /*
              * Itemstack
              * Itemstack
              */
              */
@@ -219,6 +256,62 @@ public class TreasureConfig extends ConfigLoader {
                 }
                 }
             }
             }
         }
         }
+
+        //Apply our fix
+        if (shouldWeUpdateFile) {
+            try {
+                config.save(getFile());
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private boolean processAutomaticKeyConversion(String type, boolean shouldWeUpdateTheFile, String treasureName, DropLevelKeyConversionType conversionType) {
+        switch (conversionType) {
+            case LEGACY:
+                int legacyDropLevel = getWrongKeyValue(type, treasureName, conversionType); //Legacy only had one value, Retro Mode didn't have a setting
+                //Config needs to be updated to be more specific
+                mcMMO.p.getLogger().info("(" + treasureName + ") [Fixing bad address: Legacy] Converting Drop_Level to Level_Requirement in treasures.yml for treasure to match new expected format");
+                config.set(type + "." + treasureName + LEGACY_DROP_LEVEL, null); //Remove legacy entry
+                config.set(type + "." + treasureName + LEVEL_REQUIREMENT_RETRO_MODE, legacyDropLevel * 10); //Multiply by 10 for Retro
+                config.set(type + "." + treasureName + LEVEL_REQUIREMENT_STANDARD_MODE, legacyDropLevel);
+                shouldWeUpdateTheFile = true;
+                break;
+            case WRONG_KEY_STANDARD:
+                mcMMO.p.getLogger().info("(" + treasureName + ") [Fixing bad address: STANDARD] Converting Drop_Level to Level_Requirement in treasures.yml for treasure to match new expected format");
+                int wrongKeyValueStandard = getWrongKeyValue(type, treasureName, conversionType);
+                config.set(type + "." + treasureName + WRONG_KEY_ROOT, null); //We also kill the Retro key here as we have enough information for setting in values if needed
+
+                if (wrongKeyValueStandard != -1) {
+                    config.set(type + "." + treasureName + LEVEL_REQUIREMENT_STANDARD_MODE, wrongKeyValueStandard);
+                    config.set(type + "." + treasureName + LEVEL_REQUIREMENT_RETRO_MODE, wrongKeyValueStandard * 10); //Multiply by 10 for Retro
+                }
+
+                shouldWeUpdateTheFile = true;
+                break;
+            case WRONG_KEY_RETRO:
+                mcMMO.p.getLogger().info("(" + treasureName + ") [Fixing bad address: RETRO] Converting Drop_Level to Level_Requirement in treasures.yml for treasure to match new expected format");
+                int wrongKeyValueRetro = getWrongKeyValue(type, treasureName, conversionType);
+                config.set(type + "." + treasureName + WRONG_KEY_ROOT, null); //We also kill the Retro key here as we have enough information for setting in values if needed
+
+                if (wrongKeyValueRetro != -1) {
+                    config.set(type + "." + treasureName + LEVEL_REQUIREMENT_RETRO_MODE, wrongKeyValueRetro);
+                }
+
+                shouldWeUpdateTheFile = true;
+                break;
+        }
+        return shouldWeUpdateTheFile;
+    }
+
+    private int getWrongKeyValue(String type, String treasureName, DropLevelKeyConversionType dropLevelKeyConversionType) {
+        return switch (dropLevelKeyConversionType) {
+            case LEGACY -> config.getInt(type + "." + treasureName + LEGACY_DROP_LEVEL, -1);
+            case WRONG_KEY_STANDARD -> config.getInt(type + "." + treasureName + WRONG_KEY_VALUE_STANDARD, -1);
+            case WRONG_KEY_RETRO -> config.getInt(type + "." + treasureName + WRONG_KEY_VALUE_RETRO, -1);
+        };
+
     }
     }
 
 
     private void AddHylianTreasure(String dropper, HylianTreasure treasure) {
     private void AddHylianTreasure(String dropper, HylianTreasure treasure) {
@@ -226,4 +319,10 @@ public class TreasureConfig extends ConfigLoader {
             hylianMap.put(dropper, new ArrayList<>());
             hylianMap.put(dropper, new ArrayList<>());
         hylianMap.get(dropper).add(treasure);
         hylianMap.get(dropper).add(treasure);
     }
     }
+
+    private enum DropLevelKeyConversionType {
+        LEGACY,
+        WRONG_KEY_STANDARD,
+        WRONG_KEY_RETRO
+    }
 }
 }

+ 11 - 31
src/main/java/com/gmail/nossr50/database/DatabaseManager.java

@@ -1,11 +1,12 @@
 package com.gmail.nossr50.database;
 package com.gmail.nossr50.database;
 
 
 import com.gmail.nossr50.api.exceptions.InvalidSkillException;
 import com.gmail.nossr50.api.exceptions.InvalidSkillException;
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.database.DatabaseType;
 import com.gmail.nossr50.datatypes.database.DatabaseType;
 import com.gmail.nossr50.datatypes.database.PlayerStat;
 import com.gmail.nossr50.datatypes.database.PlayerStat;
 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 org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.jetbrains.annotations.Nullable;
 
 
@@ -14,15 +15,13 @@ import java.util.Map;
 import java.util.UUID;
 import java.util.UUID;
 
 
 public interface DatabaseManager {
 public interface DatabaseManager {
-    // One month in milliseconds
-    long PURGE_TIME = 2630000000L * Config.getInstance().getOldUsersCutoff();
     // During convertUsers, how often to output a status
     // During convertUsers, how often to output a status
     int progressInterval = 200;
     int progressInterval = 200;
 
 
     /**
     /**
      * Purge users with 0 power level from the database.
      * Purge users with 0 power level from the database.
      */
      */
-    void purgePowerlessUsers();
+    int purgePowerlessUsers();
 
 
     /**
     /**
      * Purge users who haven't logged on in over a certain time frame from the database.
      * Purge users who haven't logged on in over a certain time frame from the database.
@@ -77,45 +76,26 @@ public interface DatabaseManager {
 
 
     /**
     /**
      * Add a new user to the database.
      * Add a new user to the database.
-     *
-     * @param playerName The name of the player to be added to the database
+     *  @param playerName The name of the player to be added to the database
      * @param uuid The uuid of the player to be added to the database
      * @param uuid The uuid of the player to be added to the database
+     * @return
      */
      */
-    void newUser(String playerName, UUID uuid);
+    @NotNull PlayerProfile newUser(String playerName, UUID uuid);
+
+    @NotNull PlayerProfile newUser(@NotNull Player player);
 
 
     /**
     /**
      * Load a player from the database.
      * Load a player from the database.
      *
      *
-     * @deprecated replaced by {@link #loadPlayerProfile(String playerName, UUID uuid, boolean createNew)}
-     *
      * @param playerName The name of the player to load from the database
      * @param playerName The name of the player to load from the database
-     * @param createNew Whether to create a new record if the player is not
-     *          found
      * @return The player's data, or an unloaded PlayerProfile if not found
      * @return The player's data, or an unloaded PlayerProfile if not found
      *          and createNew is false
      *          and createNew is false
      */
      */
-    @Deprecated
-    PlayerProfile loadPlayerProfile(String playerName, boolean createNew);
+    @NotNull PlayerProfile loadPlayerProfile(@NotNull String playerName);
 
 
-    /**
-     * Load a player from the database.
-     *
-     * @param uuid The uuid of the player to load from the database
-     * @return The player's data, or an unloaded PlayerProfile if not found
-     */
-    PlayerProfile loadPlayerProfile(UUID uuid);
+    @NotNull PlayerProfile loadPlayerProfile(@NotNull OfflinePlayer offlinePlayer);
 
 
-    /**
-     * Load a player from the database. Attempt to use uuid, fall back on playername
-     *
-     * @param playerName The name of the player to load from the database
-     * @param uuid The uuid of the player to load from the database
-     * @param createNew Whether to create a new record if the player is not
-     *          found
-     * @return The player's data, or an unloaded PlayerProfile if not found
-     *          and createNew is false
-     */
-    PlayerProfile loadPlayerProfile(String playerName, UUID uuid, boolean createNew);
+    @NotNull PlayerProfile loadPlayerProfile(@NotNull UUID uuid);
 
 
     /**
     /**
      * Get all users currently stored in the database.
      * Get all users currently stored in the database.

+ 10 - 7
src/main/java/com/gmail/nossr50/database/DatabaseManagerFactory.java

@@ -1,13 +1,16 @@
 package com.gmail.nossr50.database;
 package com.gmail.nossr50.database;
 
 
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.database.DatabaseType;
 import com.gmail.nossr50.datatypes.database.DatabaseType;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+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 DatabaseManager getDatabaseManager() {
+    public static DatabaseManager getDatabaseManager(@NotNull String userFilePath, @NotNull Logger logger, long purgeTime, int startingLevel) {
         if (customManager != null) {
         if (customManager != null) {
             try {
             try {
                 return createDefaultCustomDatabaseManager();
                 return createDefaultCustomDatabaseManager();
@@ -20,10 +23,10 @@ public class DatabaseManagerFactory {
                 mcMMO.p.debug("Failed to create custom database manager");
                 mcMMO.p.debug("Failed to create custom database manager");
                 e.printStackTrace();
                 e.printStackTrace();
             }
             }
-            mcMMO.p.debug("Falling back on " + (Config.getInstance().getUseMySQL() ? "SQL" : "Flatfile") + " database");
+            mcMMO.p.debug("Falling back on " + (mcMMO.p.getGeneralConfig().getUseMySQL() ? "SQL" : "Flatfile") + " database");
         }
         }
 
 
-        return Config.getInstance().getUseMySQL() ? new SQLDatabaseManager() : new FlatfileDatabaseManager();
+        return mcMMO.p.getGeneralConfig().getUseMySQL() ? new SQLDatabaseManager() : new FlatFileDatabaseManager(userFilePath, logger, purgeTime, startingLevel);
     }
     }
 
 
     /**
     /**
@@ -56,11 +59,11 @@ public class DatabaseManagerFactory {
         return customManager;
         return customManager;
     }
     }
 
 
-    public static DatabaseManager createDatabaseManager(DatabaseType type) {
+    public static @Nullable DatabaseManager createDatabaseManager(@NotNull DatabaseType type, @NotNull String userFilePath, @NotNull Logger logger, long purgeTime, int startingLevel) {
         switch (type) {
         switch (type) {
             case FLATFILE:
             case FLATFILE:
                 mcMMO.p.getLogger().info("Using FlatFile Database");
                 mcMMO.p.getLogger().info("Using FlatFile Database");
-                return new FlatfileDatabaseManager();
+                return new FlatFileDatabaseManager(userFilePath, logger, purgeTime, startingLevel);
 
 
             case SQL:
             case SQL:
                 mcMMO.p.getLogger().info("Using SQL Database");
                 mcMMO.p.getLogger().info("Using SQL Database");
@@ -80,7 +83,7 @@ public class DatabaseManagerFactory {
         }
         }
     }
     }
 
 
-    public static DatabaseManager createDefaultCustomDatabaseManager() throws Throwable {
+    private static DatabaseManager createDefaultCustomDatabaseManager() throws Throwable {
         return customManager.getConstructor().newInstance();
         return customManager.getConstructor().newInstance();
     }
     }
 
 

+ 13 - 0
src/main/java/com/gmail/nossr50/database/ExpectedType.java

@@ -0,0 +1,13 @@
+package com.gmail.nossr50.database;
+
+public enum ExpectedType {
+    STRING,
+    INTEGER,
+    LONG,
+    BOOLEAN,
+    FLOAT,
+    DOUBLE,
+    UUID,
+    IGNORED,
+    OUT_OF_RANGE
+}

+ 13 - 0
src/main/java/com/gmail/nossr50/database/FlatFileDataFlag.java

@@ -0,0 +1,13 @@
+package com.gmail.nossr50.database;
+
+public enum FlatFileDataFlag {
+    INCOMPLETE,
+    BAD_VALUES,
+    LAST_LOGIN_SCHEMA_UPGRADE,
+    MISSING_NAME,
+    DUPLICATE_NAME,
+    DUPLICATE_UUID,
+    BAD_UUID_DATA, //Can be because it is missing, null, or corrupted or some other reason
+    TOO_INCOMPLETE,
+    CORRUPTED_OR_UNRECOGNIZABLE,
+}

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

@@ -0,0 +1,345 @@
+package com.gmail.nossr50.database;
+
+import com.gmail.nossr50.database.flatfile.FlatFileDataBuilder;
+import com.gmail.nossr50.database.flatfile.FlatFileDataContainer;
+import com.gmail.nossr50.database.flatfile.FlatFileDataUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.logging.Logger;
+
+import static com.gmail.nossr50.database.FlatFileDatabaseManager.*;
+
+public class FlatFileDataProcessor {
+    private final @NotNull List<FlatFileDataContainer> flatFileDataContainers;
+    private final @NotNull List<FlatFileDataFlag> flatFileDataFlags;
+    private final @NotNull Logger logger;
+    private final HashSet<String> names;
+    private final HashSet<UUID> uuids;
+    private int uniqueProcessingID; //TODO: Not being used, should we use it?
+    boolean corruptDataFound;
+
+    public FlatFileDataProcessor(@NotNull Logger logger) {
+        this.logger = logger;
+        flatFileDataContainers = new ArrayList<>();
+        flatFileDataFlags = new ArrayList<>();
+        names = new HashSet<>();
+        uuids = new HashSet<>();
+        uniqueProcessingID = 0;
+    }
+
+    public void processData(@NotNull String lineData) {
+        assert !lineData.isEmpty();
+
+        //Make sure the data line is "correct"
+        if(lineData.charAt(lineData.length() - 1) != ':') {
+            // Length checks depend on last rawSplitData being ':'
+            // We add it here if it is missing
+            lineData = lineData.concat(":");
+        }
+
+        //Split the data into an array
+        String[] splitDataLine = lineData.split(":");
+
+        FlatFileDataBuilder builder = new FlatFileDataBuilder(splitDataLine, uniqueProcessingID);
+        uniqueProcessingID++;
+        boolean[] badDataValues = new boolean[DATA_ENTRY_COUNT];
+        boolean anyBadData = false;
+
+        //This is the minimum size of the split array needed to be considered proper data
+        if(splitDataLine.length < getMinimumSplitDataLength()) {
+            //Data is considered junk
+            if(!corruptDataFound) {
+                logger.severe("Some corrupt data was found in mcmmo.users and has been repaired, it is possible that some player data has been lost in this process.");
+                corruptDataFound = true;
+            }
+
+            //Flag as junk (corrupt)
+            builder.appendFlag(FlatFileDataFlag.CORRUPTED_OR_UNRECOGNIZABLE);
+
+            //TODO: This block here is probably pointless
+            if(splitDataLine.length >= 10 //The value here is kind of arbitrary, it shouldn't be too low to avoid false positives, but also we aren't really going to correctly identify when player data has been corrupted or not with 100% accuracy ever
+                    && splitDataLine[0] != null && !splitDataLine[0].isEmpty()) {
+                if(splitDataLine[0].length() <= 16 && splitDataLine[0].length() >= 3) {
+                    logger.severe("Not enough data found to recover corrupted player data for user: "+splitDataLine[0]);
+                    registerData(builder.appendFlag(FlatFileDataFlag.TOO_INCOMPLETE));
+                    return;
+                }
+            }
+
+            registerData(builder.appendFlag(FlatFileDataFlag.CORRUPTED_OR_UNRECOGNIZABLE));
+            return;
+        }
+
+        /*
+         * Check for duplicate names
+         */
+
+        boolean invalidUUID = false;
+
+        String name = splitDataLine[USERNAME_INDEX];
+        String strOfUUID = splitDataLine[UUID_INDEX];
+
+        if(name.isEmpty()) {
+            reportBadDataLine("No name found for data", "[MISSING NAME]", lineData);
+            builder.appendFlag(FlatFileDataFlag.MISSING_NAME);
+            anyBadData = true;
+            badDataValues[USERNAME_INDEX] = true;
+        }
+
+        if(strOfUUID.isEmpty() || strOfUUID.equalsIgnoreCase("NULL")) {
+            invalidUUID = true;
+            badDataValues[UUID_INDEX] = true;
+            reportBadDataLine("Empty/null UUID for user", "Empty/null", lineData);
+            builder.appendFlag(FlatFileDataFlag.BAD_UUID_DATA);
+
+            anyBadData = true;
+        }
+
+        UUID uuid = null;
+
+        try {
+            uuid = UUID.fromString(strOfUUID);
+        } catch (IllegalArgumentException e) {
+            //UUID does not conform
+            invalidUUID = true;
+            badDataValues[UUID_INDEX] = true;
+            reportBadDataLine("Invalid UUID data found for user", strOfUUID, lineData);
+            builder.appendFlag(FlatFileDataFlag.BAD_UUID_DATA);
+        }
+
+        //Duplicate UUID is no good, reject them
+        if(!invalidUUID && uuid != null && uuids.contains(uuid)) {
+            registerData(builder.appendFlag(FlatFileDataFlag.DUPLICATE_UUID));
+            return;
+        }
+
+        uuids.add(uuid);
+
+        if(names.contains(name)) {
+            builder.appendFlag(FlatFileDataFlag.DUPLICATE_NAME);
+            anyBadData = true;
+            badDataValues[USERNAME_INDEX] = true;
+        }
+
+        if(!name.isEmpty())
+            names.add(name);
+
+        //Make sure the data is up to date schema wise, if it isn't we adjust it to the correct size and flag it for repair
+        splitDataLine = isDataSchemaUpToDate(splitDataLine, builder, badDataValues);
+
+        /*
+         * After establishing this data has at least an identity we check for bad data
+         * Bad Value checks
+         */
+
+        //Check each data for bad values
+        for(int i = 0; i < DATA_ENTRY_COUNT; i++) {
+            if(shouldNotBeEmpty(splitDataLine[i], i)) {
+
+                if(i == OVERHAUL_LAST_LOGIN) {
+                    builder.appendFlag(FlatFileDataFlag.LAST_LOGIN_SCHEMA_UPGRADE);
+                }
+
+                badDataValues[i] = true;
+                anyBadData = true;
+                continue;
+            }
+
+            boolean isCorrectType = isOfExpectedType(splitDataLine[i], getExpectedValueType(i));
+
+            if(!isCorrectType) {
+                anyBadData = true;
+                badDataValues[i] = true;
+            }
+        }
+
+        if(anyBadData) {
+            builder.appendFlag(FlatFileDataFlag.BAD_VALUES);
+            builder.appendBadDataValues(badDataValues);
+        }
+
+        registerData(builder);
+    }
+
+    public @NotNull String[] isDataSchemaUpToDate(@NotNull String[] splitDataLine, @NotNull FlatFileDataBuilder builder, boolean[] badDataValues) {
+        assert splitDataLine.length <= DATA_ENTRY_COUNT; //should NEVER be higher
+
+        if(splitDataLine.length < DATA_ENTRY_COUNT) {
+            int oldLength = splitDataLine.length;
+            splitDataLine = Arrays.copyOf(splitDataLine, DATA_ENTRY_COUNT);
+            int newLength = splitDataLine.length;
+
+            //TODO: Test this
+            for(int i = oldLength; i < (newLength - 1); i++){
+                badDataValues[i] = true;
+            }
+
+            builder.appendFlag(FlatFileDataFlag.INCOMPLETE);
+            builder.setSplitStringData(splitDataLine);
+        }
+        return splitDataLine;
+    }
+
+
+    public boolean shouldNotBeEmpty(@Nullable String data, int index) {
+        if(getExpectedValueType(index) == ExpectedType.IGNORED) {
+            return false;
+        } else {
+            return data == null || data.isEmpty();
+        }
+    }
+
+    public boolean isOfExpectedType(@NotNull String data, @NotNull ExpectedType expectedType) {
+        switch(expectedType) {
+            case STRING:
+                return true;
+            case INTEGER:
+                try {
+                    Integer.valueOf(data);
+                    return true;
+                } catch (Exception e) {
+                    return false;
+                }
+            case BOOLEAN:
+                return data.equalsIgnoreCase("true") || data.equalsIgnoreCase("false");
+            case FLOAT:
+                try {
+                    Float.valueOf(data);
+                    return true;
+                } catch (NumberFormatException e) {
+                    return false;
+                }
+            case DOUBLE:
+                try {
+                    Double.valueOf(data);
+                    return true;
+                } catch (NumberFormatException e) {
+                    return false;
+                }
+            case UUID:
+                try {
+                    UUID.fromString(data);
+                    return true;
+                } catch (IllegalArgumentException e) {
+                    return false;
+                }
+            case OUT_OF_RANGE:
+                throw new ArrayIndexOutOfBoundsException("Value matched type OUT_OF_RANGE, this should never happen.");
+            case IGNORED:
+            default:
+                return true;
+        }
+
+    }
+
+    private void reportBadDataLine(String warning, String context, String dataLine) {
+        logger.warning("FlatFileDatabaseBuilder Warning: " + warning + " - " + context);
+        logger.warning("FlatFileDatabaseBuilder: (Line Data) - " + dataLine);
+        logger.warning("mcMMO will repair this data if automatically (if it is possible).");
+    }
+
+    private int getMinimumSplitDataLength() {
+        return UUID_INDEX + 1;
+    }
+
+    private void registerData(@NotNull FlatFileDataBuilder builder) {
+        FlatFileDataContainer flatFileDataContainer = builder.build();
+        flatFileDataContainers.add(flatFileDataContainer);
+
+        if(flatFileDataContainer.getDataFlags() != null)
+            flatFileDataFlags.addAll(flatFileDataContainer.getDataFlags());
+    }
+
+    public static @NotNull ExpectedType getExpectedValueType(int dataIndex) throws IndexOutOfBoundsException {
+        switch(dataIndex) {
+            case USERNAME_INDEX:
+                return ExpectedType.STRING;
+            case 2: //Assumption: Used to be for something, no longer used
+            case 3: //Assumption: Used to be for something, no longer used
+            case 23: //Assumption: Used to be used for something, no longer used
+            case 33: //Assumption: Used to be used for something, no longer used
+            case HEALTHBAR:
+            case LEGACY_LAST_LOGIN:
+                return ExpectedType.IGNORED;
+            case SKILLS_MINING:
+            case SKILLS_REPAIR:
+            case SKILLS_UNARMED:
+            case SKILLS_HERBALISM:
+            case SKILLS_EXCAVATION:
+            case SKILLS_ARCHERY:
+            case SKILLS_SWORDS:
+            case SKILLS_AXES:
+            case SKILLS_WOODCUTTING:
+            case SKILLS_ACROBATICS:
+            case SKILLS_TAMING:
+            case SKILLS_FISHING:
+            case SKILLS_ALCHEMY:
+            case COOLDOWN_BERSERK:
+            case COOLDOWN_GIGA_DRILL_BREAKER:
+            case COOLDOWN_TREE_FELLER:
+            case COOLDOWN_GREEN_TERRA:
+            case COOLDOWN_SERRATED_STRIKES:
+            case COOLDOWN_SKULL_SPLITTER:
+            case COOLDOWN_SUPER_BREAKER:
+            case COOLDOWN_BLAST_MINING:
+            case SCOREBOARD_TIPS:
+            case COOLDOWN_CHIMAERA_WING:
+                return ExpectedType.INTEGER;
+            case EXP_MINING:
+            case EXP_WOODCUTTING:
+            case EXP_REPAIR:
+            case EXP_UNARMED:
+            case EXP_HERBALISM:
+            case EXP_EXCAVATION:
+            case EXP_ARCHERY:
+            case EXP_SWORDS:
+            case EXP_AXES:
+            case EXP_ACROBATICS:
+            case EXP_TAMING:
+            case EXP_FISHING:
+            case EXP_ALCHEMY:
+                return ExpectedType.FLOAT;
+            case UUID_INDEX:
+                return ExpectedType.UUID;
+            case OVERHAUL_LAST_LOGIN:
+                return ExpectedType.LONG;
+        }
+
+        throw new IndexOutOfBoundsException();
+    }
+
+    public @NotNull List<FlatFileDataContainer> getFlatFileDataContainers() {
+        return flatFileDataContainers;
+    }
+
+    public @NotNull List<FlatFileDataFlag> getFlatFileDataFlags() {
+        return flatFileDataFlags;
+    }
+
+    public int getDataFlagCount() {
+        return flatFileDataFlags.size();
+    }
+
+    public @NotNull StringBuilder processDataForSave() {
+        StringBuilder stringBuilder = new StringBuilder();
+
+        //Fix our data if needed and prepare it to be saved
+
+        for(FlatFileDataContainer dataContainer : flatFileDataContainers) {
+            String[] splitData = FlatFileDataUtil.getPreparedSaveDataLine(dataContainer);
+
+            if(splitData == null)
+                continue;
+
+            //We add a trailing : as it is needed for some reason (is it?)
+            //TODO: Is the trailing ":" actually necessary?
+            String fromSplit = org.apache.commons.lang.StringUtils.join(splitData, ":") + ":";
+            stringBuilder.append(fromSplit).append("\r\n");
+        }
+
+        return stringBuilder;
+    }
+
+}

+ 1332 - 0
src/main/java/com/gmail/nossr50/database/FlatFileDatabaseManager.java

@@ -0,0 +1,1332 @@
+package com.gmail.nossr50.database;
+
+import com.gmail.nossr50.api.exceptions.InvalidSkillException;
+import com.gmail.nossr50.database.flatfile.LeaderboardStatus;
+import com.gmail.nossr50.datatypes.database.DatabaseType;
+import com.gmail.nossr50.datatypes.database.PlayerStat;
+import com.gmail.nossr50.datatypes.player.PlayerProfile;
+import com.gmail.nossr50.datatypes.player.UniqueDataType;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
+import com.gmail.nossr50.mcMMO;
+import com.gmail.nossr50.util.Misc;
+import com.gmail.nossr50.util.skills.SkillTools;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.*;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.logging.Logger;
+
+public final class FlatFileDatabaseManager implements DatabaseManager {
+    public static final String IGNORED = "IGNORED";
+    public static final String LEGACY_INVALID_OLD_USERNAME = "_INVALID_OLD_USERNAME_'";
+    private final @NotNull EnumMap<PrimarySkillType, List<PlayerStat>> playerStatHash = new EnumMap<>(PrimarySkillType.class);
+    private final @NotNull List<PlayerStat> powerLevels = new ArrayList<>();
+    private long lastUpdate = 0;
+    private final @NotNull String usersFilePath;
+    private final @NotNull Logger logger;
+    private final long purgeTime;
+    private final int startingLevel;
+    private final boolean testing;
+
+    private final long UPDATE_WAIT_TIME = 600000L; // 10 minutes
+    private final @NotNull File usersFile;
+    private static final Object fileWritingLock = new Object();
+
+    public static final int USERNAME_INDEX = 0;
+    public static final int SKILLS_MINING = 1;
+    public static final int EXP_MINING = 4;
+    public static final int SKILLS_WOODCUTTING = 5;
+    public static final int EXP_WOODCUTTING = 6;
+    public static final int SKILLS_REPAIR = 7;
+    public static final int SKILLS_UNARMED = 8;
+    public static final int SKILLS_HERBALISM = 9;
+    public static final int SKILLS_EXCAVATION = 10;
+    public static final int SKILLS_ARCHERY = 11;
+    public static final int SKILLS_SWORDS = 12;
+    public static final int SKILLS_AXES = 13;
+    public static final int SKILLS_ACROBATICS = 14;
+    public static final int EXP_REPAIR = 15;
+    public static final int EXP_UNARMED = 16;
+    public static final int EXP_HERBALISM = 17;
+    public static final int EXP_EXCAVATION = 18;
+    public static final int EXP_ARCHERY = 19;
+    public static final int EXP_SWORDS = 20;
+    public static final int EXP_AXES = 21;
+    public static final int EXP_ACROBATICS = 22;
+    public static final int SKILLS_TAMING = 24;
+    public static final int EXP_TAMING = 25;
+    public static final int COOLDOWN_BERSERK = 26;
+    public static final int COOLDOWN_GIGA_DRILL_BREAKER = 27;
+    public static final int COOLDOWN_TREE_FELLER = 28;
+    public static final int COOLDOWN_GREEN_TERRA = 29;
+    public static final int COOLDOWN_SERRATED_STRIKES = 30;
+    public static final int COOLDOWN_SKULL_SPLITTER = 31;
+    public static final int COOLDOWN_SUPER_BREAKER = 32;
+    public static final int SKILLS_FISHING = 34;
+    public static final int EXP_FISHING = 35;
+    public static final int COOLDOWN_BLAST_MINING = 36;
+    public static final int LEGACY_LAST_LOGIN = 37;
+    public static final int HEALTHBAR = 38;
+    public static final int SKILLS_ALCHEMY = 39;
+    public static final int EXP_ALCHEMY = 40;
+    public static final int UUID_INDEX = 41;
+    public static final int SCOREBOARD_TIPS = 42;
+    public static final int COOLDOWN_CHIMAERA_WING = 43;
+    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) {
+        this.usersFile = usersFile;
+        this.usersFilePath = usersFile.getPath();
+        this.logger = logger;
+        this.purgeTime = purgeTime;
+        this.startingLevel = startingLevel;
+        this.testing = testing;
+
+        if(!usersFile.exists()) {
+            initEmptyDB();
+        }
+
+        if(!testing) {
+            List<FlatFileDataFlag> flatFileDataFlags = checkFileHealthAndStructure();
+
+            if(flatFileDataFlags != null) {
+                if(flatFileDataFlags.size() > 0) {
+                    logger.info("Detected "+flatFileDataFlags.size() + " data entries which need correction.");
+                }
+            }
+
+            updateLeaderboards();
+        }
+    }
+
+    protected FlatFileDatabaseManager(@NotNull String usersFilePath, @NotNull Logger logger, long purgeTime, int startingLevel) {
+        this(new File(usersFilePath), logger, purgeTime, startingLevel, false);
+    }
+
+
+    public int purgePowerlessUsers() {
+        int purgedUsers = 0;
+
+        logger.info("Purging powerless users...");
+
+        BufferedReader in = null;
+        FileWriter out = null;
+
+        synchronized (fileWritingLock) {
+            try {
+                in = new BufferedReader(new FileReader(usersFilePath));
+                StringBuilder writer = new StringBuilder();
+                String line;
+
+                while ((line = in.readLine()) != null) {
+                    String[] character = line.split(":");
+                    Map<PrimarySkillType, Integer> skills = getSkillMapFromLine(character);
+
+                    boolean powerless = true;
+                    for (int skill : skills.values()) {
+                        if (skill != 0) {
+                            powerless = false;
+                            break;
+                        }
+                    }
+
+                    // If they're still around, rewrite them to the file.
+                    if (!powerless) {
+                        writer.append(line).append("\r\n");
+                    }
+                    else {
+                        purgedUsers++;
+                    }
+                }
+
+                // Write the new file
+                out = new FileWriter(usersFilePath);
+                out.write(writer.toString());
+            }
+            catch (IOException e) {
+                logger.severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e);
+            }
+            finally {
+                if (in != null) {
+                    try {
+                        in.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+                if (out != null) {
+                    try {
+                        out.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+
+        logger.info("Purged " + purgedUsers + " users from the database.");
+        return purgedUsers;
+    }
+
+    //TODO: Test this
+    public void purgeOldUsers() {
+        int removedPlayers = 0;
+        long currentTime = System.currentTimeMillis();
+
+        logger.info("Purging old users...");
+
+        BufferedReader in = null;
+        FileWriter out = null;
+
+        // This code is O(n) instead of O(n²)
+        synchronized (fileWritingLock) {
+            try {
+                in = new BufferedReader(new FileReader(usersFilePath));
+                StringBuilder writer = new StringBuilder();
+                String line;
+
+                while ((line = in.readLine()) != null) {
+                    String[] character = line.split(":");
+                    String name = character[USERNAME_INDEX];
+                    long lastPlayed = 0;
+                    boolean rewrite = false;
+
+                    try {
+                        lastPlayed = Long.parseLong(character[OVERHAUL_LAST_LOGIN]);
+                    } catch (NumberFormatException e) {
+                        e.printStackTrace();
+                    }
+
+                    if (lastPlayed == -1) {
+                        OfflinePlayer player = mcMMO.p.getServer().getOfflinePlayer(name);
+
+                        if(player.getLastPlayed() != 0) {
+                            lastPlayed = player.getLastPlayed();
+                            rewrite = true;
+                        }
+                    }
+
+                    if (lastPlayed != -1 && lastPlayed != 0 && currentTime - lastPlayed > purgeTime) {
+                        removedPlayers++;
+                    } else {
+                        if (rewrite) {
+                            // Rewrite their data with a valid time
+                            character[OVERHAUL_LAST_LOGIN] = Long.toString(lastPlayed);
+                            String newLine = org.apache.commons.lang.StringUtils.join(character, ":");
+                            writer.append(newLine).append("\r\n");
+                        } else {
+                            writer.append(line).append("\r\n");
+                        }
+                    }
+                }
+
+                // Write the new file
+                out = new FileWriter(usersFilePath);
+                out.write(writer.toString());
+
+                if(testing) {
+                    System.out.println(writer.toString());
+                }
+            }
+            catch (IOException e) {
+                logger.severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e);
+            }
+            finally {
+                if (in != null) {
+                    try {
+                        in.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+                if (out != null) {
+                    try {
+                        out.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+
+        logger.info("Purged " + removedPlayers + " users from the database.");
+    }
+
+    public boolean removeUser(String playerName, UUID uuid) {
+        //NOTE: UUID is unused for FlatFile for this interface implementation
+        boolean worked = false;
+
+        BufferedReader in = null;
+        FileWriter out = null;
+
+        synchronized (fileWritingLock) {
+            try {
+                in = new BufferedReader(new FileReader(usersFilePath));
+                StringBuilder writer = new StringBuilder();
+                String line;
+
+                while ((line = in.readLine()) != null) {
+                    // Write out the same file but when we get to the player we want to remove, we skip his line.
+                    if (!worked && line.split(":")[USERNAME_INDEX].equalsIgnoreCase(playerName)) {
+                        logger.info("User found, removing...");
+                        worked = true;
+                        continue; // Skip the player
+                    }
+
+                    writer.append(line).append("\r\n");
+                }
+
+                out = new FileWriter(usersFilePath); // Write out the new file
+                out.write(writer.toString());
+            }
+            catch (Exception e) {
+                logger.severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e);
+            }
+            finally {
+                if (in != null) {
+                    try {
+                        in.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+                if (out != null) {
+                    try {
+                        out.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+
+        Misc.profileCleanup(playerName);
+
+        return worked;
+    }
+
+    @Override
+    public void cleanupUser(UUID uuid) {
+        //Not used in FlatFile
+    }
+
+    public boolean saveUser(@NotNull PlayerProfile profile) {
+        String playerName = profile.getPlayerName();
+        UUID uuid = profile.getUniqueId();
+
+        BufferedReader in = null;
+        FileWriter out = null;
+        boolean corruptDataFound = false;
+
+        synchronized (fileWritingLock) {
+            try {
+                // Open the file
+                in = new BufferedReader(new FileReader(usersFilePath));
+                StringBuilder writer = new StringBuilder();
+                String line;
+
+                boolean wroteUser = false;
+                // While not at the end of the file
+                while ((line = in.readLine()) != null) {
+                    if(line.startsWith("#")) {
+                        writer.append(line).append("\r\n");
+                        continue;
+                    }
+
+                    //Check for incomplete or corrupted data
+                    if(!line.contains(":")) {
+
+                        if(!corruptDataFound) {
+                            logger.severe("mcMMO found some unexpected or corrupted data in mcmmo.users and is removing it, it is possible some data has been lost.");
+                            corruptDataFound = true;
+                        }
+
+                        continue;
+                    }
+
+                    String[] splitData = line.split(":");
+
+                    //This would be rare, but check the splitData for having enough entries to contain a UUID
+                    if(splitData.length < UUID_INDEX) { //UUID have been in mcMMO DB for a very long time so any user without
+
+                        if(!corruptDataFound) {
+                            logger.severe("mcMMO found some unexpected or corrupted data in mcmmo.users and is removing it, it is possible some data has been lost.");
+                            corruptDataFound = true;
+                        }
+
+                        continue;
+                    }
+
+                    if (!(uuid != null
+                                    && splitData[UUID_INDEX].equalsIgnoreCase(uuid.toString()))
+                                    && !splitData[USERNAME_INDEX].equalsIgnoreCase(playerName)) {
+                        writer.append(line).append("\r\n"); //Not the user so write it to file and move on
+                    } else {
+                        //User found
+                        writeUserToLine(profile, writer);
+                        wroteUser = true;
+                    }
+                }
+
+                /*
+                 * If we couldn't find the user in the DB we need to add him
+                 */
+                if(!wroteUser) {
+                    writeUserToLine(profile, writer);
+                }
+
+                // Write the new file
+                out = new FileWriter(usersFilePath);
+                out.write(writer.toString());
+                return true;
+            }
+            catch (Exception e) {
+                e.printStackTrace();
+                return false;
+            }
+            finally {
+                if (in != null) {
+                    try {
+                        in.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+                if (out != null) {
+                    try {
+                        out.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+    }
+
+    public void writeUserToLine(@NotNull PlayerProfile profile, @NotNull Appendable appendable) throws IOException {
+        appendable.append(profile.getPlayerName()).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.MINING))).append(":");
+        appendable.append(IGNORED).append(":");
+        appendable.append(IGNORED).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.MINING))).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.WOODCUTTING))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.WOODCUTTING))).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.REPAIR))).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.UNARMED))).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.HERBALISM))).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.EXCAVATION))).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.ARCHERY))).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.SWORDS))).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.AXES))).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.ACROBATICS))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.REPAIR))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.UNARMED))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.HERBALISM))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.EXCAVATION))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.ARCHERY))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.SWORDS))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.AXES))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.ACROBATICS))).append(":");
+        appendable.append(IGNORED).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.TAMING))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.TAMING))).append(":");
+        appendable.append(String.valueOf(profile.getAbilityDATS(SuperAbilityType.BERSERK))).append(":");
+        appendable.append(String.valueOf(profile.getAbilityDATS(SuperAbilityType.GIGA_DRILL_BREAKER))).append(":");
+        appendable.append(String.valueOf(profile.getAbilityDATS(SuperAbilityType.TREE_FELLER))).append(":");
+        appendable.append(String.valueOf(profile.getAbilityDATS(SuperAbilityType.GREEN_TERRA))).append(":");
+        appendable.append(String.valueOf(profile.getAbilityDATS(SuperAbilityType.SERRATED_STRIKES))).append(":");
+        appendable.append(String.valueOf(profile.getAbilityDATS(SuperAbilityType.SKULL_SPLITTER))).append(":");
+        appendable.append(String.valueOf(profile.getAbilityDATS(SuperAbilityType.SUPER_BREAKER))).append(":");
+        appendable.append(IGNORED).append(":");
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.FISHING))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.FISHING))).append(":");
+        appendable.append(String.valueOf(profile.getAbilityDATS(SuperAbilityType.BLAST_MINING))).append(":");
+        appendable.append(IGNORED).append(":"); //Legacy last login
+        appendable.append(IGNORED).append(":"); //mob health bar
+        appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.ALCHEMY))).append(":");
+        appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.ALCHEMY))).append(":");
+        appendable.append(profile.getUniqueId() != null ? profile.getUniqueId().toString() : "NULL").append(":");
+        appendable.append(String.valueOf(profile.getScoreboardTipsShown())).append(":");
+        appendable.append(String.valueOf(profile.getUniqueData(UniqueDataType.CHIMAERA_WING_DATS))).append(":");
+        appendable.append(String.valueOf(profile.getLastLogin())).append(":"); //overhaul last login
+        appendable.append("\r\n");
+    }
+
+    public @NotNull List<PlayerStat> readLeaderboard(@Nullable PrimarySkillType primarySkillType, int pageNumber, int statsPerPage) throws InvalidSkillException {
+        //Fix for a plugin that people are using that is throwing SQL errors
+        if(primarySkillType != null && SkillTools.isChildSkill(primarySkillType)) {
+            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!");
+        }
+
+        updateLeaderboards();
+        List<PlayerStat> statsList = primarySkillType == null ? powerLevels : playerStatHash.get(primarySkillType);
+        int fromIndex = (Math.max(pageNumber, 1) - 1) * statsPerPage;
+
+        return statsList.subList(Math.min(fromIndex, statsList.size()), Math.min(fromIndex + statsPerPage, statsList.size()));
+    }
+
+    public @NotNull HashMap<PrimarySkillType, Integer> readRank(String playerName) {
+        updateLeaderboards();
+
+        HashMap<PrimarySkillType, Integer> skills = new HashMap<>();
+
+        for (PrimarySkillType skill : SkillTools.NON_CHILD_SKILLS) {
+            skills.put(skill, getPlayerRank(playerName, playerStatHash.get(skill)));
+        }
+
+        //TODO: Gross
+        skills.put(null, getPlayerRank(playerName, powerLevels));
+
+        return skills;
+    }
+
+    public @NotNull PlayerProfile newUser(@NotNull Player player) {
+        return new PlayerProfile(player.getName(), player.getUniqueId(), true, startingLevel);
+    }
+
+    public @NotNull PlayerProfile newUser(@NotNull String playerName, @NotNull UUID uuid) {
+        PlayerProfile playerProfile = new PlayerProfile(playerName, uuid, true, startingLevel);
+
+        synchronized (fileWritingLock) {
+            try(BufferedReader bufferedReader = new BufferedReader(new FileReader(usersFilePath))) {
+                StringBuilder stringBuilder = new StringBuilder();
+
+                String line;
+
+                //Build up the file
+                while((line = bufferedReader.readLine()) != null) {
+                    stringBuilder.append(line).append("\r\n");
+                }
+
+                try (FileWriter fileWriter = new FileWriter(usersFile)) {
+                    writeUserToLine(playerProfile, stringBuilder);
+                    fileWriter.write(stringBuilder.toString());
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+
+        return playerProfile;
+    }
+
+    public @NotNull PlayerProfile loadPlayerProfile(@NotNull OfflinePlayer offlinePlayer) {
+        return processUserQuery(getUserQuery(offlinePlayer.getUniqueId(), offlinePlayer.getName()));
+    }
+
+    public @NotNull PlayerProfile loadPlayerProfile(@NotNull String playerName) {
+        return processUserQuery(getUserQuery(null, playerName));
+    }
+
+    public @NotNull PlayerProfile loadPlayerProfile(@NotNull UUID uuid) {
+        return processUserQuery(getUserQuery(uuid, null));
+    }
+
+    private @NotNull UserQuery getUserQuery(@Nullable UUID uuid, @Nullable String playerName) throws NullPointerException {
+        boolean hasName = playerName != null && !playerName.equalsIgnoreCase("null");
+
+        if(hasName && uuid != null) {
+            return new UserQueryFull(playerName, uuid);
+        } else if (uuid != null) {
+            return new UserQueryUUIDImpl(uuid);
+        } else if(hasName) {
+            return new UserQueryNameImpl(playerName);
+        } else {
+            throw new NullPointerException("Both name and UUID cannot be null, at least one must be non-null!");
+        }
+    }
+
+    /**
+     * Find and load a player by UUID/Name
+     * If the name isn't null and doesn't match the name in the DB, the players name is then replaced/updated
+     *
+     * @param userQuery the query
+     * @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 {
+        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!");
+        }
+    }
+
+    private @NotNull PlayerProfile queryByName(@NotNull UserQueryName userQuery) {
+        String playerName = userQuery.getName();
+        BufferedReader in = null;
+
+        synchronized (fileWritingLock) {
+            try {
+                // Open the user file
+                in = new BufferedReader(new FileReader(usersFilePath));
+                String line;
+
+
+                while ((line = in.readLine()) != null) {
+                    if(line.startsWith("#")) {
+                        continue;
+                    }
+
+
+                    // Find if the line contains the player we want.
+                    String[] rawSplitData = line.split(":");
+
+
+                    /* Don't read corrupt data */
+                    if(rawSplitData.length < (USERNAME_INDEX + 1)) {
+                        continue;
+                    }
+
+
+                    //If we couldn't find anyone
+                    if(playerName.equalsIgnoreCase(rawSplitData[USERNAME_INDEX])) {
+                        return loadFromLine(rawSplitData);
+                    }
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            } finally {
+                // I have no idea why it's necessary to inline tryClose() here, but it removes
+                // a resource leak warning, and I'm trusting the compiler on this one.
+                if (in != null) {
+                    try {
+                        in.close();
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+
+
+        //Return a new blank profile
+        return new PlayerProfile(playerName, new UUID(0, 0), startingLevel);
+    }
+
+    private @NotNull PlayerProfile queryByUUID(@NotNull UserQueryUUID userQuery) {
+        BufferedReader in = null;
+        UUID uuid = userQuery.getUUID();
+
+        synchronized (fileWritingLock) {
+            try {
+                // Open the user file
+                in = new BufferedReader(new FileReader(usersFilePath));
+                String line;
+
+                while ((line = in.readLine()) != null) {
+                    if(line.startsWith("#")) {
+                        continue;
+                    }
+                    // Find if the line contains the player we want.
+                    String[] rawSplitData = line.split(":");
+
+                    /* Don't read corrupt data */
+                    if(rawSplitData.length < (UUID_INDEX + 1)) {
+                        continue;
+                    }
+
+                    try {
+                        UUID fromDataUUID = UUID.fromString(rawSplitData[UUID_INDEX]);
+                        if(fromDataUUID.equals(uuid)) {
+                            return loadFromLine(rawSplitData);
+                        }
+                    } catch (Exception e) {
+                        if(testing) {
+                            e.printStackTrace();
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            } finally {
+                // I have no idea why it's necessary to inline tryClose() here, but it removes
+                // a resource leak warning, and I'm trusting the compiler on this one.
+                if (in != null) {
+                    try {
+                        in.close();
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+
+        /*
+         * No match was found in the file
+         */
+
+        return grabUnloadedProfile(uuid, "Player-Not-Found="+uuid.toString());
+    }
+
+    private @NotNull PlayerProfile queryByUUIDAndName(@NotNull UserQueryFull userQuery) {
+        BufferedReader in = null;
+        String playerName = userQuery.getName();
+        UUID uuid = userQuery.getUUID();
+
+        synchronized (fileWritingLock) {
+            try {
+                // Open the user file
+                in = new BufferedReader(new FileReader(usersFilePath));
+                String line;
+
+                while ((line = in.readLine()) != null) {
+                    if(line.startsWith("#")) {
+                        continue;
+                    }
+                    // Find if the line contains the player we want.
+                    String[] rawSplitData = line.split(":");
+
+                    /* Don't read corrupt data */
+                    if(rawSplitData.length < (UUID_INDEX + 1)) {
+                        continue;
+                    }
+
+                    try {
+                        UUID fromDataUUID = UUID.fromString(rawSplitData[UUID_INDEX]);
+                        if(fromDataUUID.equals(uuid)) {
+                            //Matched UUID, now check if name matches
+                            String dbPlayerName = rawSplitData[USERNAME_INDEX];
+
+                            boolean matchingName = dbPlayerName.equalsIgnoreCase(playerName);
+
+                            if (!matchingName) {
+                                logger.info("When loading user: "+playerName +" with UUID of (" + uuid.toString()
+                                        +") 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);
+                                rawSplitData[USERNAME_INDEX] = playerName;
+                            }
+
+                            //TODO: Logic to replace name here
+                            return loadFromLine(rawSplitData);
+                        }
+                    } catch (Exception e) {
+                        if(testing) {
+                            e.printStackTrace();
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            } finally {
+                // I have no idea why it's necessary to inline tryClose() here, but it removes
+                // a resource leak warning, and I'm trusting the compiler on this one.
+                if (in != null) {
+                    try {
+                        in.close();
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+
+        /*
+         * No match was found in the file
+         */
+
+        return grabUnloadedProfile(uuid, playerName); //Create an empty new profile and return
+    }
+
+    private @NotNull PlayerProfile grabUnloadedProfile(@NotNull UUID uuid, @Nullable String playerName) {
+        if(playerName == null) {
+            playerName = ""; //No name for you boy!
+        }
+
+        return new PlayerProfile(playerName, uuid, 0);
+    }
+
+    public void convertUsers(DatabaseManager destination) {
+        BufferedReader in = null;
+        int convertedUsers = 0;
+        long startMillis = System.currentTimeMillis();
+
+        synchronized (fileWritingLock) {
+            try {
+                // Open the user file
+                in = new BufferedReader(new FileReader(usersFilePath));
+                String line;
+
+                while ((line = in.readLine()) != null) {
+                    if(line.startsWith("#")) {
+                        continue;
+                    }
+
+                    String[] character = line.split(":");
+
+                    try {
+                        destination.saveUser(loadFromLine(character));
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                    convertedUsers++;
+                    Misc.printProgress(convertedUsers, progressInterval, startMillis);
+                }
+            }
+            catch (Exception e) {
+                e.printStackTrace();
+            }
+            finally {
+                if (in != null) {
+                    try {
+                        in.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+    }
+
+    public boolean saveUserUUID(String userName, UUID uuid) {
+        boolean worked = false;
+
+        int i = 0;
+        BufferedReader in = null;
+        FileWriter out = null;
+
+        synchronized (fileWritingLock) {
+            try {
+                in = new BufferedReader(new FileReader(usersFilePath));
+                StringBuilder writer = new StringBuilder();
+                String line;
+
+                while ((line = in.readLine()) != null) {
+                    String[] character = line.split(":");
+                    if (!worked && character[USERNAME_INDEX].equalsIgnoreCase(userName)) {
+                        if (character.length < 42) {
+                            logger.severe("Could not update UUID for " + userName + "!");
+                            logger.severe("Database entry is invalid.");
+                            continue;
+                        }
+
+                        line = line.replace(character[UUID_INDEX], uuid.toString());
+                        worked = true;
+                    }
+
+                    i++;
+                    writer.append(line).append("\r\n");
+                }
+
+                out = new FileWriter(usersFilePath); // Write out the new file
+                out.write(writer.toString());
+            }
+            catch (Exception e) {
+                logger.severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e);
+            }
+            finally {
+                logger.info(i + " entries written while saving UUID for " + userName);
+                if (in != null) {
+                    try {
+                        in.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+                if (out != null) {
+                    try {
+                        out.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+
+        return worked;
+    }
+
+    public boolean saveUserUUIDs(Map<String, UUID> fetchedUUIDs) {
+        BufferedReader in = null;
+        FileWriter out = null;
+        int i = 0;
+
+        synchronized (fileWritingLock) {
+            try {
+                in = new BufferedReader(new FileReader(usersFilePath));
+                StringBuilder writer = new StringBuilder();
+                String line;
+
+                while (((line = in.readLine()) != null)) {
+                    String[] character = line.split(":");
+                    if (!fetchedUUIDs.isEmpty() && fetchedUUIDs.containsKey(character[USERNAME_INDEX])) {
+                        if (character.length < 42) {
+                            logger.severe("Could not update UUID for " + character[USERNAME_INDEX] + "!");
+                            logger.severe("Database entry is invalid.");
+                            continue;
+                        }
+
+                        character[UUID_INDEX] = fetchedUUIDs.remove(character[USERNAME_INDEX]).toString();
+                        line = org.apache.commons.lang.StringUtils.join(character, ":") + ":";
+                    }
+
+                    i++;
+                    writer.append(line).append("\r\n");
+                }
+
+                out = new FileWriter(usersFilePath); // Write out the new file
+                out.write(writer.toString());
+            }
+            catch (Exception e) {
+                logger.severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e);
+            }
+            finally {
+                logger.info(i + " entries written while saving UUID batch");
+                if (in != null) {
+                    try {
+                        in.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+                if (out != null) {
+                    try {
+                        out.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+
+        return true;
+    }
+
+    public List<String> getStoredUsers() {
+        ArrayList<String> users = new ArrayList<>();
+        BufferedReader in = null;
+
+        synchronized (fileWritingLock) {
+            try {
+                // Open the user file
+                in = new BufferedReader(new FileReader(usersFilePath));
+                String line;
+
+                while ((line = in.readLine()) != null) {
+                    String[] character = line.split(":");
+                    users.add(character[USERNAME_INDEX]);
+                }
+            }
+            catch (Exception e) {
+                e.printStackTrace();
+            }
+            finally {
+                if (in != null) {
+                    try {
+                        in.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+        return users;
+    }
+
+    /**
+     * Update the leader boards.
+     */
+    public @NotNull LeaderboardStatus updateLeaderboards() {
+        // Only update FFS leaderboards every 10 minutes.. this puts a lot of strain on the server (depending on the size of the database) and should not be done frequently
+        if (System.currentTimeMillis() < lastUpdate + UPDATE_WAIT_TIME) {
+            return LeaderboardStatus.TOO_SOON_TO_UPDATE;
+        }
+
+        lastUpdate = System.currentTimeMillis(); // Log when the last update was run
+        powerLevels.clear(); // Clear old values from the power levels
+
+        // Initialize lists
+        List<PlayerStat> mining = new ArrayList<>();
+        List<PlayerStat> woodcutting = new ArrayList<>();
+        List<PlayerStat> herbalism = new ArrayList<>();
+        List<PlayerStat> excavation = new ArrayList<>();
+        List<PlayerStat> acrobatics = new ArrayList<>();
+        List<PlayerStat> repair = new ArrayList<>();
+        List<PlayerStat> swords = new ArrayList<>();
+        List<PlayerStat> axes = new ArrayList<>();
+        List<PlayerStat> archery = new ArrayList<>();
+        List<PlayerStat> unarmed = new ArrayList<>();
+        List<PlayerStat> taming = new ArrayList<>();
+        List<PlayerStat> fishing = new ArrayList<>();
+        List<PlayerStat> alchemy = new ArrayList<>();
+
+        BufferedReader in = null;
+        String playerName = null;
+        // Read from the FlatFile database and fill our arrays with information
+        synchronized (fileWritingLock) {
+            try {
+                in = new BufferedReader(new FileReader(usersFilePath));
+                String line;
+
+                while ((line = in.readLine()) != null) {
+
+                    if(line.startsWith("#"))
+                        continue;
+
+                    String[] data = line.split(":");
+                    playerName = data[USERNAME_INDEX];
+                    int powerLevel = 0;
+
+                    Map<PrimarySkillType, Integer> skills = getSkillMapFromLine(data);
+
+                    powerLevel += putStat(acrobatics, playerName, skills.get(PrimarySkillType.ACROBATICS));
+                    powerLevel += putStat(alchemy, playerName, skills.get(PrimarySkillType.ALCHEMY));
+                    powerLevel += putStat(archery, playerName, skills.get(PrimarySkillType.ARCHERY));
+                    powerLevel += putStat(axes, playerName, skills.get(PrimarySkillType.AXES));
+                    powerLevel += putStat(excavation, playerName, skills.get(PrimarySkillType.EXCAVATION));
+                    powerLevel += putStat(fishing, playerName, skills.get(PrimarySkillType.FISHING));
+                    powerLevel += putStat(herbalism, playerName, skills.get(PrimarySkillType.HERBALISM));
+                    powerLevel += putStat(mining, playerName, skills.get(PrimarySkillType.MINING));
+                    powerLevel += putStat(repair, playerName, skills.get(PrimarySkillType.REPAIR));
+                    powerLevel += putStat(swords, playerName, skills.get(PrimarySkillType.SWORDS));
+                    powerLevel += putStat(taming, playerName, skills.get(PrimarySkillType.TAMING));
+                    powerLevel += putStat(unarmed, playerName, skills.get(PrimarySkillType.UNARMED));
+                    powerLevel += putStat(woodcutting, playerName, skills.get(PrimarySkillType.WOODCUTTING));
+
+                    putStat(powerLevels, playerName, powerLevel);
+                }
+            }
+            catch (Exception e) {
+                logger.severe("Exception while reading " + usersFilePath + " during user " + playerName + " (Are you sure you formatted it correctly?) " + e);
+                return LeaderboardStatus.FAILED;
+            } finally {
+                if (in != null) {
+                    try {
+                        in.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+
+        }
+
+        SkillComparator c = new SkillComparator();
+
+        mining.sort(c);
+        woodcutting.sort(c);
+        repair.sort(c);
+        unarmed.sort(c);
+        herbalism.sort(c);
+        excavation.sort(c);
+        archery.sort(c);
+        swords.sort(c);
+        axes.sort(c);
+        acrobatics.sort(c);
+        taming.sort(c);
+        fishing.sort(c);
+        alchemy.sort(c);
+        powerLevels.sort(c);
+
+        playerStatHash.put(PrimarySkillType.MINING, mining);
+        playerStatHash.put(PrimarySkillType.WOODCUTTING, woodcutting);
+        playerStatHash.put(PrimarySkillType.REPAIR, repair);
+        playerStatHash.put(PrimarySkillType.UNARMED, unarmed);
+        playerStatHash.put(PrimarySkillType.HERBALISM, herbalism);
+        playerStatHash.put(PrimarySkillType.EXCAVATION, excavation);
+        playerStatHash.put(PrimarySkillType.ARCHERY, archery);
+        playerStatHash.put(PrimarySkillType.SWORDS, swords);
+        playerStatHash.put(PrimarySkillType.AXES, axes);
+        playerStatHash.put(PrimarySkillType.ACROBATICS, acrobatics);
+        playerStatHash.put(PrimarySkillType.TAMING, taming);
+        playerStatHash.put(PrimarySkillType.FISHING, fishing);
+        playerStatHash.put(PrimarySkillType.ALCHEMY, alchemy);
+
+        return LeaderboardStatus.UPDATED;
+    }
+
+    private void initEmptyDB() {
+        BufferedWriter bufferedWriter = null;
+        synchronized (fileWritingLock) {
+            try {
+                // Open the file to write the player
+                bufferedWriter = new BufferedWriter(new FileWriter(usersFilePath, true));
+                DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm");
+                LocalDateTime localDateTime = LocalDateTime.now();
+                bufferedWriter.append("# mcMMO Database created on ").append(localDateTime.format(dateTimeFormatter)).append("\r\n"); //Empty file
+            } catch (Exception e) {
+                e.printStackTrace();
+            } finally {
+                if (bufferedWriter != null) {
+                    try {
+                        bufferedWriter.close();
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+    }
+
+    public @Nullable List<FlatFileDataFlag> checkFileHealthAndStructure() {
+        ArrayList<FlatFileDataFlag> flagsFound = null;
+        logger.info("(" + usersFile.getPath() + ") Validating database file..");
+        FlatFileDataProcessor dataProcessor = null;
+
+        if (usersFile.exists()) {
+            BufferedReader bufferedReader = null;
+            FileWriter fileWriter = null;
+
+            synchronized (fileWritingLock) {
+
+                dataProcessor = new FlatFileDataProcessor(logger);
+
+                try {
+                    String currentLine;
+                    String dbCommentDate = null;
+
+                    bufferedReader = new BufferedReader(new FileReader(usersFilePath));
+
+                    //Analyze the data
+                    while ((currentLine = bufferedReader.readLine()) != null) {
+                        //Commented lines
+                        if(currentLine.startsWith("#") && dbCommentDate == null) { //The first commented line in the file is likely to be our note about when the file was created
+                            dbCommentDate = currentLine;
+                            continue;
+                        }
+
+                        if(currentLine.isEmpty())
+                            continue;
+
+                        //TODO: We are never passing empty lines, should we remove the flag for them?
+                        dataProcessor.processData(currentLine);
+                    }
+
+                    //Only update the file if needed
+                    if(dataProcessor.getFlatFileDataFlags().size() > 0) {
+                        flagsFound = new ArrayList<>(dataProcessor.getFlatFileDataFlags());
+                        logger.info("Saving the updated and or repaired FlatFile Database...");
+                        fileWriter = new FileWriter(usersFilePath);
+                        //Write data to file
+                        if(dbCommentDate != null)
+                            fileWriter.write(dbCommentDate + "\r\n");
+
+                        fileWriter.write(dataProcessor.processDataForSave().toString());
+                    }
+                } catch (IOException e) {
+                    e.printStackTrace();
+                } finally {
+                    closeResources(bufferedReader, fileWriter);
+                }
+            }
+        }
+
+        if(flagsFound == null || flagsFound.size() == 0) {
+            return null;
+        } else {
+            return flagsFound;
+        }
+    }
+
+    private void closeResources(BufferedReader bufferedReader, FileWriter fileWriter) {
+        if(bufferedReader != null) {
+            try {
+                bufferedReader.close();
+            }
+            catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+
+        if (fileWriter != null) {
+            try {
+                fileWriter.close();
+            }
+            catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private Integer getPlayerRank(String playerName, List<PlayerStat> statsList) {
+        if (statsList == null) {
+            return null;
+        }
+
+        int currentPos = 1;
+
+        for (PlayerStat stat : statsList) {
+            if (stat.name.equalsIgnoreCase(playerName)) {
+                return currentPos;
+            }
+
+            currentPos++;
+        }
+
+        return null;
+    }
+
+    private int putStat(List<PlayerStat> statList, String playerName, int statValue) {
+        statList.add(new PlayerStat(playerName, statValue));
+        return statValue;
+    }
+
+    private static class SkillComparator implements Comparator<PlayerStat> {
+        @Override
+        public int compare(PlayerStat o1, PlayerStat o2) {
+            return (o2.statVal - o1.statVal);
+        }
+    }
+
+    private PlayerProfile loadFromLine(@NotNull String[] character) {
+        Map<PrimarySkillType, Integer>   skills     = getSkillMapFromLine(character);      // Skill levels
+        Map<PrimarySkillType, Float>     skillsXp   = new EnumMap<>(PrimarySkillType.class);     // Skill & XP
+        Map<SuperAbilityType, Integer> skillsDATS = new EnumMap<>(SuperAbilityType.class); // Ability & Cooldown
+        Map<UniqueDataType, Integer> uniquePlayerDataMap = new EnumMap<>(UniqueDataType.class);
+        int scoreboardTipsShown;
+        long lastLogin;
+
+        String username = character[USERNAME_INDEX];
+
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.TAMING, EXP_TAMING, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.MINING, EXP_MINING, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.REPAIR, EXP_REPAIR, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.WOODCUTTING, EXP_WOODCUTTING, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.UNARMED, EXP_UNARMED, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.HERBALISM, EXP_HERBALISM, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.EXCAVATION, EXP_EXCAVATION, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.ARCHERY, EXP_ARCHERY, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.SWORDS, EXP_SWORDS, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.AXES, EXP_AXES, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.ACROBATICS, EXP_ACROBATICS, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.FISHING, EXP_FISHING, username);
+        tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.ALCHEMY, EXP_ALCHEMY, username);
+
+        // Taming - Unused
+        tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SUPER_BREAKER, COOLDOWN_SUPER_BREAKER, username);
+        // Repair - Unused
+        tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.TREE_FELLER, COOLDOWN_TREE_FELLER, username);
+        tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.BERSERK, COOLDOWN_BERSERK, username);
+        tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.GREEN_TERRA, COOLDOWN_GREEN_TERRA, username);
+        tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.GIGA_DRILL_BREAKER, COOLDOWN_GIGA_DRILL_BREAKER, username);
+        // Archery - Unused
+        tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SERRATED_STRIKES, COOLDOWN_SERRATED_STRIKES, username);
+        tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SKULL_SPLITTER, COOLDOWN_SKULL_SPLITTER, username);
+        // Acrobatics - Unused
+        tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.BLAST_MINING, COOLDOWN_BLAST_MINING, username);
+
+        UUID uuid;
+        try {
+            uuid = UUID.fromString(character[UUID_INDEX]);
+        }
+        catch (Exception e) {
+            uuid = null;
+        }
+
+        try {
+            scoreboardTipsShown = Integer.parseInt(character[SCOREBOARD_TIPS]);
+        }
+        catch (Exception e) {
+            scoreboardTipsShown = 0;
+        }
+
+        try {
+            uniquePlayerDataMap.put(UniqueDataType.CHIMAERA_WING_DATS, Integer.valueOf(character[COOLDOWN_CHIMAERA_WING]));
+        }
+        catch (Exception e) {
+            uniquePlayerDataMap.put(UniqueDataType.CHIMAERA_WING_DATS, 0);
+        }
+
+        try {
+            lastLogin = Long.parseLong(character[OVERHAUL_LAST_LOGIN]);
+        } catch (Exception e) {
+            lastLogin = -1;
+        }
+
+        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) {
+        try {
+            cooldownMap.put(superAbilityType, Integer.valueOf(character[cooldownSuperBreaker]));
+        } 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();
+        }
+    }
+
+    private void tryLoadSkillFloatValuesFromRawData(@NotNull Map<PrimarySkillType, Float> skillMap, @NotNull String[] character, @NotNull PrimarySkillType primarySkillType, int index, @NotNull String userName) {
+        try {
+            float valueFromString = Integer.parseInt(character[index]);
+            skillMap.put(primarySkillType, valueFromString);
+        } catch (NumberFormatException e) {
+            skillMap.put(primarySkillType, 0F);
+            logger.severe("Data corruption when trying to load the value for skill "+primarySkillType+" for player named " + userName+ " setting value to zero");
+            e.printStackTrace();
+        }
+    }
+
+    private void tryLoadSkillIntValuesFromRawData(@NotNull Map<PrimarySkillType, Integer> skillMap, @NotNull String[] character, @NotNull PrimarySkillType primarySkillType, int index, @NotNull String userName) {
+        try {
+            int valueFromString = Integer.parseInt(character[index]);
+            skillMap.put(primarySkillType, valueFromString);
+        } catch (NumberFormatException e) {
+            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");
+            e.printStackTrace();
+        }
+    }
+
+    private @NotNull Map<PrimarySkillType, Integer> getSkillMapFromLine(@NotNull String[] character) {
+        EnumMap<PrimarySkillType, Integer> skills = new EnumMap<>(PrimarySkillType.class);   // Skill & Level
+        String username = character[USERNAME_INDEX];
+
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.ACROBATICS, SKILLS_ACROBATICS, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.TAMING, SKILLS_TAMING, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.MINING, SKILLS_MINING, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.REPAIR, SKILLS_REPAIR, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.WOODCUTTING, SKILLS_WOODCUTTING, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.UNARMED, SKILLS_UNARMED, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.HERBALISM, SKILLS_HERBALISM, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.EXCAVATION, SKILLS_EXCAVATION, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.ARCHERY, SKILLS_ARCHERY, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.SWORDS, SKILLS_SWORDS, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.AXES, SKILLS_AXES, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.FISHING, SKILLS_FISHING, username);
+        tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.ALCHEMY, SKILLS_ALCHEMY, username);
+
+        return skills;
+    }
+
+    public DatabaseType getDatabaseType() {
+        return DatabaseType.FLATFILE;
+    }
+
+    public @NotNull File getUsersFile() {
+        return usersFile;
+    }
+
+    @Override
+    public void onDisable() { }
+}

+ 0 - 1372
src/main/java/com/gmail/nossr50/database/FlatfileDatabaseManager.java

@@ -1,1372 +0,0 @@
-package com.gmail.nossr50.database;
-
-import com.gmail.nossr50.api.exceptions.InvalidSkillException;
-import com.gmail.nossr50.config.AdvancedConfig;
-import com.gmail.nossr50.config.Config;
-import com.gmail.nossr50.datatypes.MobHealthbarType;
-import com.gmail.nossr50.datatypes.database.DatabaseType;
-import com.gmail.nossr50.datatypes.database.PlayerStat;
-import com.gmail.nossr50.datatypes.database.UpgradeType;
-import com.gmail.nossr50.datatypes.player.PlayerProfile;
-import com.gmail.nossr50.datatypes.player.UniqueDataType;
-import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
-import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
-import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.runnables.database.UUIDUpdateAsyncTask;
-import com.gmail.nossr50.util.Misc;
-import com.gmail.nossr50.util.text.StringUtils;
-import org.bukkit.OfflinePlayer;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.*;
-import java.util.*;
-
-public final class FlatfileDatabaseManager implements DatabaseManager {
-    private final HashMap<PrimarySkillType, List<PlayerStat>> playerStatHash = new HashMap<>();
-    private final List<PlayerStat> powerLevels = new ArrayList<>();
-    private long lastUpdate = 0;
-
-    private final long UPDATE_WAIT_TIME = 600000L; // 10 minutes
-    private final File usersFile;
-    private static final Object fileWritingLock = new Object();
-
-    protected FlatfileDatabaseManager() {
-        usersFile = new File(mcMMO.getUsersFilePath());
-        checkStructure();
-        updateLeaderboards();
-
-        if (mcMMO.getUpgradeManager().shouldUpgrade(UpgradeType.ADD_UUIDS)) {
-            new UUIDUpdateAsyncTask(mcMMO.p, getStoredUsers()).start();
-        }
-    }
-
-    public void purgePowerlessUsers() {
-        int purgedUsers = 0;
-
-        mcMMO.p.getLogger().info("Purging powerless users...");
-
-        BufferedReader in = null;
-        FileWriter out = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        // This code is O(n) instead of O(n²)
-        synchronized (fileWritingLock) {
-            try {
-                in = new BufferedReader(new FileReader(usersFilePath));
-                StringBuilder writer = new StringBuilder();
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    String[] character = line.split(":");
-                    Map<PrimarySkillType, Integer> skills = getSkillMapFromLine(character);
-
-                    boolean powerless = true;
-                    for (int skill : skills.values()) {
-                        if (skill != 0) {
-                            powerless = false;
-                            break;
-                        }
-                    }
-
-                    // If they're still around, rewrite them to the file.
-                    if (!powerless) {
-                        writer.append(line).append("\r\n");
-                    }
-                    else {
-                        purgedUsers++;
-                    }
-                }
-
-                // Write the new file
-                out = new FileWriter(usersFilePath);
-                out.write(writer.toString());
-            }
-            catch (IOException e) {
-                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        mcMMO.p.getLogger().info("Purged " + purgedUsers + " users from the database.");
-    }
-
-    public void purgeOldUsers() {
-        int removedPlayers = 0;
-        long currentTime = System.currentTimeMillis();
-
-        mcMMO.p.getLogger().info("Purging old users...");
-
-        BufferedReader in = null;
-        FileWriter out = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        // This code is O(n) instead of O(n²)
-        synchronized (fileWritingLock) {
-            try {
-                in = new BufferedReader(new FileReader(usersFilePath));
-                StringBuilder writer = new StringBuilder();
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    String[] character = line.split(":");
-                    String name = character[USERNAME];
-                    long lastPlayed = 0;
-                    boolean rewrite = false;
-                    try {
-                        lastPlayed = Long.parseLong(character[37]) * Misc.TIME_CONVERSION_FACTOR;
-                    }
-                    catch (NumberFormatException e) {
-                        e.printStackTrace();
-                    }
-                    if (lastPlayed == 0) {
-                        OfflinePlayer player = mcMMO.p.getServer().getOfflinePlayer(name);
-                        lastPlayed = player.getLastPlayed();
-                        rewrite = true;
-                    }
-
-                    if (currentTime - lastPlayed > PURGE_TIME) {
-                        removedPlayers++;
-                    }
-                    else {
-                        if (rewrite) {
-                            // Rewrite their data with a valid time
-                            character[37] = Long.toString(lastPlayed);
-                            String newLine = org.apache.commons.lang.StringUtils.join(character, ":");
-                            writer.append(newLine).append("\r\n");
-                        }
-                        else {
-                            writer.append(line).append("\r\n");
-                        }
-                    }
-                }
-
-                // Write the new file
-                out = new FileWriter(usersFilePath);
-                out.write(writer.toString());
-            }
-            catch (IOException e) {
-                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        mcMMO.p.getLogger().info("Purged " + removedPlayers + " users from the database.");
-    }
-
-    public boolean removeUser(String playerName, UUID uuid) {
-        //NOTE: UUID is unused for FlatFile for this interface implementation
-        boolean worked = false;
-
-        BufferedReader in = null;
-        FileWriter out = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        synchronized (fileWritingLock) {
-            try {
-                in = new BufferedReader(new FileReader(usersFilePath));
-                StringBuilder writer = new StringBuilder();
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    // Write out the same file but when we get to the player we want to remove, we skip his line.
-                    if (!worked && line.split(":")[USERNAME].equalsIgnoreCase(playerName)) {
-                        mcMMO.p.getLogger().info("User found, removing...");
-                        worked = true;
-                        continue; // Skip the player
-                    }
-
-                    writer.append(line).append("\r\n");
-                }
-
-                out = new FileWriter(usersFilePath); // Write out the new file
-                out.write(writer.toString());
-            }
-            catch (Exception e) {
-                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        Misc.profileCleanup(playerName);
-
-        return worked;
-    }
-
-    @Override
-    public void cleanupUser(UUID uuid) {
-        //Not used in FlatFile
-    }
-
-    public boolean saveUser(PlayerProfile profile) {
-        String playerName = profile.getPlayerName();
-        UUID uuid = profile.getUniqueId();
-
-        BufferedReader in = null;
-        FileWriter out = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        synchronized (fileWritingLock) {
-            try {
-                // Open the file
-                in = new BufferedReader(new FileReader(usersFilePath));
-                StringBuilder writer = new StringBuilder();
-                String line;
-
-                boolean wroteUser = false;
-                // While not at the end of the file
-                while ((line = in.readLine()) != null) {
-                    // Read the line in and copy it to the output if it's not the player we want to edit
-                    String[] character = line.split(":");
-                    if (!(uuid != null && character[UUID_INDEX].equalsIgnoreCase(uuid.toString())) && !character[USERNAME].equalsIgnoreCase(playerName)) {
-                        writer.append(line).append("\r\n");
-                    }
-                    else {
-                        // Otherwise write the new player information
-                        writeUserToLine(profile, playerName, uuid, writer);
-                        wroteUser = true;
-                    }
-                }
-
-                /*
-                 * If we couldn't find the user in the DB we need to add him
-                 */
-                if(!wroteUser)
-                {
-                    writeUserToLine(profile, playerName, uuid, writer);
-                }
-
-                // Write the new file
-                out = new FileWriter(usersFilePath);
-                out.write(writer.toString());
-                return true;
-            }
-            catch (Exception e) {
-                e.printStackTrace();
-                return false;
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-    }
-
-    private void writeUserToLine(PlayerProfile profile, String playerName, UUID uuid, StringBuilder writer) {
-        writer.append(playerName).append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.MINING)).append(":");
-        writer.append(":");
-        writer.append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.MINING)).append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.WOODCUTTING)).append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.WOODCUTTING)).append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.REPAIR)).append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.UNARMED)).append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.HERBALISM)).append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.EXCAVATION)).append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.ARCHERY)).append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.SWORDS)).append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.AXES)).append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.ACROBATICS)).append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.REPAIR)).append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.UNARMED)).append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.HERBALISM)).append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.EXCAVATION)).append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.ARCHERY)).append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.SWORDS)).append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.AXES)).append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.ACROBATICS)).append(":");
-        writer.append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.TAMING)).append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.TAMING)).append(":");
-        writer.append((int) profile.getAbilityDATS(SuperAbilityType.BERSERK)).append(":");
-        writer.append((int) profile.getAbilityDATS(SuperAbilityType.GIGA_DRILL_BREAKER)).append(":");
-        writer.append((int) profile.getAbilityDATS(SuperAbilityType.TREE_FELLER)).append(":");
-        writer.append((int) profile.getAbilityDATS(SuperAbilityType.GREEN_TERRA)).append(":");
-        writer.append((int) profile.getAbilityDATS(SuperAbilityType.SERRATED_STRIKES)).append(":");
-        writer.append((int) profile.getAbilityDATS(SuperAbilityType.SKULL_SPLITTER)).append(":");
-        writer.append((int) profile.getAbilityDATS(SuperAbilityType.SUPER_BREAKER)).append(":");
-        writer.append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.FISHING)).append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.FISHING)).append(":");
-        writer.append((int) profile.getAbilityDATS(SuperAbilityType.BLAST_MINING)).append(":");
-        writer.append(System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR).append(":");
-        MobHealthbarType mobHealthbarType = profile.getMobHealthbarType();
-        writer.append(mobHealthbarType == null ? Config.getInstance().getMobHealthbarDefault().toString() : mobHealthbarType.toString()).append(":");
-        writer.append(profile.getSkillLevel(PrimarySkillType.ALCHEMY)).append(":");
-        writer.append(profile.getSkillXpLevel(PrimarySkillType.ALCHEMY)).append(":");
-        writer.append(uuid != null ? uuid.toString() : "NULL").append(":");
-        writer.append(profile.getScoreboardTipsShown()).append(":");
-        writer.append(profile.getUniqueData(UniqueDataType.CHIMAERA_WING_DATS)).append(":");
-        writer.append("\r\n");
-    }
-
-    public @NotNull List<PlayerStat> readLeaderboard(@Nullable PrimarySkillType skill, int pageNumber, int statsPerPage) throws InvalidSkillException {
-        //Fix for a plugin that people are using that is throwing SQL errors
-        if(skill != null && skill.isChildSkill()) {
-            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!");
-            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!");
-        }
-
-        updateLeaderboards();
-        List<PlayerStat> statsList = skill == null ? powerLevels : playerStatHash.get(skill);
-        int fromIndex = (Math.max(pageNumber, 1) - 1) * statsPerPage;
-
-        return statsList.subList(Math.min(fromIndex, statsList.size()), Math.min(fromIndex + statsPerPage, statsList.size()));
-    }
-
-    public Map<PrimarySkillType, Integer> readRank(String playerName) {
-        updateLeaderboards();
-
-        Map<PrimarySkillType, Integer> skills = new HashMap<>();
-
-        for (PrimarySkillType skill : PrimarySkillType.NON_CHILD_SKILLS) {
-            skills.put(skill, getPlayerRank(playerName, playerStatHash.get(skill)));
-        }
-
-        skills.put(null, getPlayerRank(playerName, powerLevels));
-
-        return skills;
-    }
-
-    public void newUser(String playerName, UUID uuid) {
-        BufferedWriter out = null;
-        synchronized (fileWritingLock) {
-            try {
-                // Open the file to write the player
-                out = new BufferedWriter(new FileWriter(mcMMO.getUsersFilePath(), true));
-
-                String startingLevel = AdvancedConfig.getInstance().getStartingLevel() + ":";
-
-                // Add the player to the end
-                out.append(playerName).append(":");
-                out.append(startingLevel); // Mining
-                out.append(":");
-                out.append(":");
-                out.append("0:"); // Xp
-                out.append(startingLevel); // Woodcutting
-                out.append("0:"); // WoodCuttingXp
-                out.append(startingLevel); // Repair
-                out.append(startingLevel); // Unarmed
-                out.append(startingLevel); // Herbalism
-                out.append(startingLevel); // Excavation
-                out.append(startingLevel); // Archery
-                out.append(startingLevel); // Swords
-                out.append(startingLevel); // Axes
-                out.append(startingLevel); // Acrobatics
-                out.append("0:"); // RepairXp
-                out.append("0:"); // UnarmedXp
-                out.append("0:"); // HerbalismXp
-                out.append("0:"); // ExcavationXp
-                out.append("0:"); // ArcheryXp
-                out.append("0:"); // SwordsXp
-                out.append("0:"); // AxesXp
-                out.append("0:"); // AcrobaticsXp
-                out.append(":");
-                out.append(startingLevel); // Taming
-                out.append("0:"); // TamingXp
-                out.append("0:"); // DATS
-                out.append("0:"); // DATS
-                out.append("0:"); // DATS
-                out.append("0:"); // DATS
-                out.append("0:"); // DATS
-                out.append("0:"); // DATS
-                out.append("0:"); // DATS
-                out.append(":");
-                out.append(startingLevel); // Fishing
-                out.append("0:"); // FishingXp
-                out.append("0:"); // Blast Mining
-                out.append(String.valueOf(System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR)).append(":"); // LastLogin
-                out.append(Config.getInstance().getMobHealthbarDefault().toString()).append(":"); // Mob Healthbar HUD
-                out.append(startingLevel); // Alchemy
-                out.append("0:"); // AlchemyXp
-                out.append(uuid != null ? uuid.toString() : "NULL").append(":"); // UUID
-                out.append("0:"); // Scoreboard tips shown
-                // Add more in the same format as the line above
-
-                out.newLine();
-            }
-            catch (Exception e) {
-                e.printStackTrace();
-            }
-            finally {
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-    }
-
-    @Deprecated
-    public PlayerProfile loadPlayerProfile(String playerName, boolean create) {
-        return loadPlayerProfile(playerName, null, false);
-    }
-
-    public PlayerProfile loadPlayerProfile(UUID uuid) {
-        return loadPlayerProfile("", uuid, false);
-    }
-
-    public PlayerProfile loadPlayerProfile(String playerName, UUID uuid, boolean create) {
-        BufferedReader in = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        synchronized (fileWritingLock) {
-            try {
-                // Open the user file
-                in = new BufferedReader(new FileReader(usersFilePath));
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    // Find if the line contains the player we want.
-                    String[] character = line.split(":");
-
-                    // Compare names because we don't have a valid uuid for that player even
-                    // if input uuid is not null
-                    if (character[UUID_INDEX].equalsIgnoreCase("NULL")) {
-                        if (!character[USERNAME].equalsIgnoreCase(playerName)) {
-                            continue;
-                        }
-                    }
-                    // If input uuid is not null then we should compare uuids
-                    else if ((uuid != null && !character[UUID_INDEX].equalsIgnoreCase(uuid.toString())) || (uuid == null && !character[USERNAME].equalsIgnoreCase(playerName))) {
-                        continue;
-                    }
-
-                    // Update playerName in database after name change
-                    if (!character[USERNAME].equalsIgnoreCase(playerName)) {
-                        mcMMO.p.debug("Name change detected: " + character[USERNAME] + " => " + playerName);
-                        character[USERNAME] = playerName;
-                    }
-
-                    return loadFromLine(character);
-                }
-
-                // Didn't find the player, create a new one
-                if (create) {
-                    if (uuid == null) {
-                        newUser(playerName, uuid);
-                        return new PlayerProfile(playerName, true);
-                    }
-
-                    newUser(playerName, uuid);
-                    return new PlayerProfile(playerName, uuid, true);
-                }
-            }
-            catch (Exception e) {
-                e.printStackTrace();
-            }
-            finally {
-                // I have no idea why it's necessary to inline tryClose() here, but it removes
-                // a resource leak warning, and I'm trusting the compiler on this one.
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        // Return unloaded profile
-        if (uuid == null) {
-            return new PlayerProfile(playerName);
-        }
-
-        return new PlayerProfile(playerName, uuid);
-    }
-
-    public void convertUsers(DatabaseManager destination) {
-        BufferedReader in = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-        int convertedUsers = 0;
-        long startMillis = System.currentTimeMillis();
-
-        synchronized (fileWritingLock) {
-            try {
-                // Open the user file
-                in = new BufferedReader(new FileReader(usersFilePath));
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    String[] character = line.split(":");
-
-                    try {
-                        destination.saveUser(loadFromLine(character));
-                    }
-                    catch (Exception e) {
-                        e.printStackTrace();
-                    }
-                    convertedUsers++;
-                    Misc.printProgress(convertedUsers, progressInterval, startMillis);
-                }
-            }
-            catch (Exception e) {
-                e.printStackTrace();
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-    }
-
-    public boolean saveUserUUID(String userName, UUID uuid) {
-        boolean worked = false;
-
-        int i = 0;
-        BufferedReader in = null;
-        FileWriter out = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        synchronized (fileWritingLock) {
-            try {
-                in = new BufferedReader(new FileReader(usersFilePath));
-                StringBuilder writer = new StringBuilder();
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    String[] character = line.split(":");
-                    if (!worked && character[USERNAME].equalsIgnoreCase(userName)) {
-                        if (character.length < 42) {
-                            mcMMO.p.getLogger().severe("Could not update UUID for " + userName + "!");
-                            mcMMO.p.getLogger().severe("Database entry is invalid.");
-                            continue;
-                        }
-
-                        line = line.replace(character[UUID_INDEX], uuid.toString());
-                        worked = true;
-                    }
-
-                    i++;
-                    writer.append(line).append("\r\n");
-                }
-
-                out = new FileWriter(usersFilePath); // Write out the new file
-                out.write(writer.toString());
-            }
-            catch (Exception e) {
-                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
-            }
-            finally {
-                mcMMO.p.getLogger().info(i + " entries written while saving UUID for " + userName);
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        return worked;
-    }
-
-    public boolean saveUserUUIDs(Map<String, UUID> fetchedUUIDs) {
-        BufferedReader in = null;
-        FileWriter out = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-        int i = 0;
-
-        synchronized (fileWritingLock) {
-            try {
-                in = new BufferedReader(new FileReader(usersFilePath));
-                StringBuilder writer = new StringBuilder();
-                String line;
-
-                while (((line = in.readLine()) != null)) {
-                    String[] character = line.split(":");
-                    if (!fetchedUUIDs.isEmpty() && fetchedUUIDs.containsKey(character[USERNAME])) {
-                        if (character.length < 42) {
-                            mcMMO.p.getLogger().severe("Could not update UUID for " + character[USERNAME] + "!");
-                            mcMMO.p.getLogger().severe("Database entry is invalid.");
-                            continue;
-                        }
-
-                        character[UUID_INDEX] = fetchedUUIDs.remove(character[USERNAME]).toString();
-                        line = org.apache.commons.lang.StringUtils.join(character, ":") + ":";
-                    }
-
-                    i++;
-                    writer.append(line).append("\r\n");
-                }
-
-                out = new FileWriter(usersFilePath); // Write out the new file
-                out.write(writer.toString());
-            }
-            catch (Exception e) {
-                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
-            }
-            finally {
-                mcMMO.p.getLogger().info(i + " entries written while saving UUID batch");
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        return true;
-    }
-
-    public List<String> getStoredUsers() {
-        ArrayList<String> users = new ArrayList<>();
-        BufferedReader in = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        synchronized (fileWritingLock) {
-            try {
-                // Open the user file
-                in = new BufferedReader(new FileReader(usersFilePath));
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    String[] character = line.split(":");
-                    users.add(character[USERNAME]);
-                }
-            }
-            catch (Exception e) {
-                e.printStackTrace();
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-        return users;
-    }
-
-    /**
-     * Update the leader boards.
-     */
-    private void updateLeaderboards() {
-        // Only update FFS leaderboards every 10 minutes.. this puts a lot of strain on the server (depending on the size of the database) and should not be done frequently
-        if (System.currentTimeMillis() < lastUpdate + UPDATE_WAIT_TIME) {
-            return;
-        }
-
-        String usersFilePath = mcMMO.getUsersFilePath();
-        lastUpdate = System.currentTimeMillis(); // Log when the last update was run
-        powerLevels.clear(); // Clear old values from the power levels
-
-        // Initialize lists
-        List<PlayerStat> mining = new ArrayList<>();
-        List<PlayerStat> woodcutting = new ArrayList<>();
-        List<PlayerStat> herbalism = new ArrayList<>();
-        List<PlayerStat> excavation = new ArrayList<>();
-        List<PlayerStat> acrobatics = new ArrayList<>();
-        List<PlayerStat> repair = new ArrayList<>();
-        List<PlayerStat> swords = new ArrayList<>();
-        List<PlayerStat> axes = new ArrayList<>();
-        List<PlayerStat> archery = new ArrayList<>();
-        List<PlayerStat> unarmed = new ArrayList<>();
-        List<PlayerStat> taming = new ArrayList<>();
-        List<PlayerStat> fishing = new ArrayList<>();
-        List<PlayerStat> alchemy = new ArrayList<>();
-
-        BufferedReader in = null;
-        String playerName = null;
-        // Read from the FlatFile database and fill our arrays with information
-        synchronized (fileWritingLock) {
-            try {
-                in = new BufferedReader(new FileReader(usersFilePath));
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    String[] data = line.split(":");
-                    playerName = data[USERNAME];
-                    int powerLevel = 0;
-
-                    Map<PrimarySkillType, Integer> skills = getSkillMapFromLine(data);
-
-                    powerLevel += putStat(acrobatics, playerName, skills.get(PrimarySkillType.ACROBATICS));
-                    powerLevel += putStat(alchemy, playerName, skills.get(PrimarySkillType.ALCHEMY));
-                    powerLevel += putStat(archery, playerName, skills.get(PrimarySkillType.ARCHERY));
-                    powerLevel += putStat(axes, playerName, skills.get(PrimarySkillType.AXES));
-                    powerLevel += putStat(excavation, playerName, skills.get(PrimarySkillType.EXCAVATION));
-                    powerLevel += putStat(fishing, playerName, skills.get(PrimarySkillType.FISHING));
-                    powerLevel += putStat(herbalism, playerName, skills.get(PrimarySkillType.HERBALISM));
-                    powerLevel += putStat(mining, playerName, skills.get(PrimarySkillType.MINING));
-                    powerLevel += putStat(repair, playerName, skills.get(PrimarySkillType.REPAIR));
-                    powerLevel += putStat(swords, playerName, skills.get(PrimarySkillType.SWORDS));
-                    powerLevel += putStat(taming, playerName, skills.get(PrimarySkillType.TAMING));
-                    powerLevel += putStat(unarmed, playerName, skills.get(PrimarySkillType.UNARMED));
-                    powerLevel += putStat(woodcutting, playerName, skills.get(PrimarySkillType.WOODCUTTING));
-
-                    putStat(powerLevels, playerName, powerLevel);
-                }
-            }
-            catch (Exception e) {
-                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " during user " + playerName + " (Are you sure you formatted it correctly?) " + e.toString());
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        SkillComparator c = new SkillComparator();
-
-        mining.sort(c);
-        woodcutting.sort(c);
-        repair.sort(c);
-        unarmed.sort(c);
-        herbalism.sort(c);
-        excavation.sort(c);
-        archery.sort(c);
-        swords.sort(c);
-        axes.sort(c);
-        acrobatics.sort(c);
-        taming.sort(c);
-        fishing.sort(c);
-        alchemy.sort(c);
-        powerLevels.sort(c);
-
-        playerStatHash.put(PrimarySkillType.MINING, mining);
-        playerStatHash.put(PrimarySkillType.WOODCUTTING, woodcutting);
-        playerStatHash.put(PrimarySkillType.REPAIR, repair);
-        playerStatHash.put(PrimarySkillType.UNARMED, unarmed);
-        playerStatHash.put(PrimarySkillType.HERBALISM, herbalism);
-        playerStatHash.put(PrimarySkillType.EXCAVATION, excavation);
-        playerStatHash.put(PrimarySkillType.ARCHERY, archery);
-        playerStatHash.put(PrimarySkillType.SWORDS, swords);
-        playerStatHash.put(PrimarySkillType.AXES, axes);
-        playerStatHash.put(PrimarySkillType.ACROBATICS, acrobatics);
-        playerStatHash.put(PrimarySkillType.TAMING, taming);
-        playerStatHash.put(PrimarySkillType.FISHING, fishing);
-        playerStatHash.put(PrimarySkillType.ALCHEMY, alchemy);
-    }
-
-    /**
-     * Checks that the file is present and valid
-     */
-    private void checkStructure() {
-        if (usersFile.exists()) {
-            BufferedReader in = null;
-            FileWriter out = null;
-            String usersFilePath = mcMMO.getUsersFilePath();
-
-            synchronized (fileWritingLock) {
-                try {
-                    in = new BufferedReader(new FileReader(usersFilePath));
-                    StringBuilder writer = new StringBuilder();
-                    String line;
-                    HashSet<String> usernames = new HashSet<>();
-                    HashSet<String> players = new HashSet<>();
-
-                    while ((line = in.readLine()) != null) {
-                        // Remove empty lines from the file
-                        if (line.isEmpty()) {
-                            continue;
-                        }
-
-                        // Length checks depend on last character being ':'
-                        if (line.charAt(line.length() - 1) != ':') {
-                            line = line.concat(":");
-                        }
-                        boolean updated = false;
-                        String[] character = line.split(":");
-
-                        // Prevent the same username from being present multiple times
-                        if (!usernames.add(character[USERNAME])) {
-                            character[USERNAME] = "_INVALID_OLD_USERNAME_'";
-                            updated = true;
-                            if (character.length < UUID_INDEX + 1 || character[UUID_INDEX].equals("NULL")) {
-                                continue;
-                            }
-                        }
-
-                        // Prevent the same player from being present multiple times
-                        if (character.length >= 42 && (!character[UUID_INDEX].isEmpty() && !character[UUID_INDEX].equals("NULL") && !players.add(character[UUID_INDEX]))) {
-                            continue;
-                        }
-
-                        if (character.length < 33) {
-                            // Before Version 1.0 - Drop
-                            mcMMO.p.getLogger().warning("Dropping malformed or before version 1.0 line from database - " + line);
-                            continue;
-                        }
-
-                        String oldVersion = null;
-
-                        if (character.length > 33 && !character[33].isEmpty()) {
-                            // Removal of Spout Support
-                            // Version 1.4.07-dev2
-                            // commit 7bac0e2ca5143bce84dc160617fed97f0b1cb968
-                            character[33] = "";
-                            if (oldVersion == null) {
-                                oldVersion = "1.4.07";
-                            }
-                            updated = true;
-                        }
-
-                        if (Config.getInstance().getTruncateSkills()) {
-                            for (PrimarySkillType skill : PrimarySkillType.NON_CHILD_SKILLS) {
-                                int index = getSkillIndex(skill);
-                                if (index >= character.length) {
-                                    continue;
-                                }
-                                int cap = Config.getInstance().getLevelCap(skill);
-                                if (Integer.parseInt(character[index]) > cap) {
-                                    mcMMO.p.getLogger().warning("Truncating " + skill.getName() + " to configured max level for player " + character[USERNAME]);
-                                    character[index] = cap + "";
-                                    updated = true;
-                                }
-                            }
-                        }
-
-                        // If they're valid, rewrite them to the file.
-                        if (!updated && character.length == 43) {
-                            writer.append(line).append("\r\n");
-                            continue;
-                        }
-
-                        if (character.length <= 33) {
-                            // Introduction of HUDType
-                            // Version 1.1.06
-                            // commit 78f79213cdd7190cd11ae54526f3b4ea42078e8a
-                            character = Arrays.copyOf(character, character.length + 1);
-                            character[character.length - 1] = "";
-                            oldVersion = "1.1.06";
-                        }
-
-                        if (character.length <= 35) {
-                            // Introduction of Fishing
-                            // Version 1.2.00
-                            // commit a814b57311bc7734661109f0e77fc8bab3a0bd29
-                            character = Arrays.copyOf(character, character.length + 2);
-                            character[character.length - 1] = "0";
-                            character[character.length - 2] = "0";
-                            if (oldVersion == null) {
-                                oldVersion = "1.2.00";
-                            }
-                        }
-                        if (character.length <= 36) {
-                            // Introduction of Blast Mining cooldowns
-                            // Version 1.3.00-dev
-                            // commit fadbaf429d6b4764b8f1ad0efaa524a090e82ef5
-                            character = Arrays.copyOf(character, character.length + 1);
-                            character[character.length - 1] = "0";
-                            if (oldVersion == null) {
-                                oldVersion = "1.3.00";
-                            }
-                        }
-                        if (character.length <= 37) {
-                            // Making old-purge work with flatfile
-                            // Version 1.4.00-dev
-                            // commmit 3f6c07ba6aaf44e388cc3b882cac3d8f51d0ac28
-                            // XXX Cannot create an OfflinePlayer at startup, use 0 and fix in purge
-                            character = Arrays.copyOf(character, character.length + 1);
-                            character[character.length - 1] = "0";
-                            if (oldVersion == null) {
-                                oldVersion = "1.4.00";
-                            }
-                        }
-                        if (character.length <= 38) {
-                            // Addition of mob healthbars
-                            // Version 1.4.06
-                            // commit da29185b7dc7e0d992754bba555576d48fa08aa6
-                            character = Arrays.copyOf(character, character.length + 1);
-                            character[character.length - 1] = Config.getInstance().getMobHealthbarDefault().toString();
-                            if (oldVersion == null) {
-                                oldVersion = "1.4.06";
-                            }
-                        }
-                        if (character.length <= 39) {
-                            // Addition of Alchemy
-                            // Version 1.4.08
-                            character = Arrays.copyOf(character, character.length + 2);
-                            character[character.length - 1] = "0";
-                            character[character.length - 2] = "0";
-                            if (oldVersion == null) {
-                                oldVersion = "1.4.08";
-                            }
-                        }
-                        if (character.length <= 41) {
-                            // Addition of UUIDs
-                            // Version 1.5.01
-                            // Add a value because otherwise it gets removed
-                            character = Arrays.copyOf(character, character.length + 1);
-                            character[character.length - 1] = "NULL";
-                            if (oldVersion == null) {
-                                oldVersion = "1.5.01";
-                            }
-                        }
-                        if (character.length <= 42) {
-                            // Addition of scoreboard tips auto disable
-                            // Version 1.5.02
-                            character = Arrays.copyOf(character, character.length + 1);
-                            character[character.length - 1] = "0";
-                            if (oldVersion == null) {
-                                oldVersion = "1.5.02";
-                            }
-                        }
-
-                        boolean corrupted = false;
-
-                        for (int i = 0; i < character.length; i++) {
-                            if (character[i].isEmpty() && !(i == 2 || i == 3 || i == 23 || i == 33 || i == 41)) {
-                                corrupted = true;
-                                if (i == 37) {
-                                    character[i] = String.valueOf(System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR);
-                                }
-                                else if (i == 38) {
-                                    character[i] = Config.getInstance().getMobHealthbarDefault().toString();
-                                }
-                                else {
-                                    character[i] = "0";
-                                }
-                            }
-
-                            if (StringUtils.isInt(character[i]) && i == 38) {
-                                corrupted = true;
-                                character[i] = Config.getInstance().getMobHealthbarDefault().toString();
-                            }
-
-                            if (!StringUtils.isInt(character[i]) && !(i == 0 || i == 2 || i == 3 || i == 23 || i == 33 || i == 38 || i == 41)) {
-                                corrupted = true;
-                                character[i] = "0";
-                            }
-                        }
-
-                        if (corrupted) {
-                            mcMMO.p.debug("Updating corrupted database line for player " + character[USERNAME]);
-                        }
-
-                        if (oldVersion != null) {
-                            mcMMO.p.debug("Updating database line from before version " + oldVersion + " for player " + character[USERNAME]);
-                        }
-
-                        updated |= corrupted;
-                        updated |= oldVersion != null;
-
-                        if (Config.getInstance().getTruncateSkills()) {
-                            Map<PrimarySkillType, Integer> skills = getSkillMapFromLine(character);
-                            for (PrimarySkillType skill : PrimarySkillType.NON_CHILD_SKILLS) {
-                                int cap = Config.getInstance().getLevelCap(skill);
-                                if (skills.get(skill) > cap) {
-                                    updated = true;
-                                }
-                            }
-                        }
-
-                        if (updated) {
-                            line = org.apache.commons.lang.StringUtils.join(character, ":") + ":";
-                        }
-
-                        writer.append(line).append("\r\n");
-                    }
-
-                    // Write the new file
-                    out = new FileWriter(usersFilePath);
-                    out.write(writer.toString());
-                }
-                catch (IOException e) {
-                    mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
-                }
-                finally {
-                    if (in != null) {
-                        try {
-                            in.close();
-                        }
-                        catch (IOException e) {
-                            // Ignore
-                        }
-                    }
-                    if (out != null) {
-                        try {
-                            out.close();
-                        }
-                        catch (IOException e) {
-                            // Ignore
-                        }
-                    }
-                }
-            }
-
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_FISHING);
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_BLAST_MINING_COOLDOWN);
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_SQL_INDEXES);
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_MOB_HEALTHBARS);
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.DROP_SQL_PARTY_NAMES);
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.DROP_SPOUT);
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_ALCHEMY);
-            return;
-        }
-
-        usersFile.getParentFile().mkdir();
-
-        try {
-            mcMMO.p.debug("Creating mcmmo.users file...");
-            new File(mcMMO.getUsersFilePath()).createNewFile();
-        }
-        catch (IOException e) {
-            e.printStackTrace();
-        }
-    }
-
-    private Integer getPlayerRank(String playerName, List<PlayerStat> statsList) {
-        if (statsList == null) {
-            return null;
-        }
-
-        int currentPos = 1;
-
-        for (PlayerStat stat : statsList) {
-            if (stat.name.equalsIgnoreCase(playerName)) {
-                return currentPos;
-            }
-
-            currentPos++;
-        }
-
-        return null;
-    }
-
-    private int putStat(List<PlayerStat> statList, String playerName, int statValue) {
-        statList.add(new PlayerStat(playerName, statValue));
-        return statValue;
-    }
-
-    private static class SkillComparator implements Comparator<PlayerStat> {
-        @Override
-        public int compare(PlayerStat o1, PlayerStat o2) {
-            return (o2.statVal - o1.statVal);
-        }
-    }
-
-    private PlayerProfile loadFromLine(String[] character) {
-        Map<PrimarySkillType, Integer>   skills     = getSkillMapFromLine(character);      // Skill levels
-        Map<PrimarySkillType, Float>     skillsXp   = new EnumMap<>(PrimarySkillType.class);     // Skill & XP
-        Map<SuperAbilityType, Integer> skillsDATS = new EnumMap<>(SuperAbilityType.class); // Ability & Cooldown
-        Map<UniqueDataType, Integer> uniquePlayerDataMap = new EnumMap<>(UniqueDataType.class);
-        MobHealthbarType mobHealthbarType;
-        int scoreboardTipsShown;
-
-        // TODO on updates, put new values in a try{} ?
-
-        skillsXp.put(PrimarySkillType.TAMING, (float) Integer.parseInt(character[EXP_TAMING]));
-        skillsXp.put(PrimarySkillType.MINING, (float) Integer.parseInt(character[EXP_MINING]));
-        skillsXp.put(PrimarySkillType.REPAIR, (float) Integer.parseInt(character[EXP_REPAIR]));
-        skillsXp.put(PrimarySkillType.WOODCUTTING, (float) Integer.parseInt(character[EXP_WOODCUTTING]));
-        skillsXp.put(PrimarySkillType.UNARMED, (float) Integer.parseInt(character[EXP_UNARMED]));
-        skillsXp.put(PrimarySkillType.HERBALISM, (float) Integer.parseInt(character[EXP_HERBALISM]));
-        skillsXp.put(PrimarySkillType.EXCAVATION, (float) Integer.parseInt(character[EXP_EXCAVATION]));
-        skillsXp.put(PrimarySkillType.ARCHERY, (float) Integer.parseInt(character[EXP_ARCHERY]));
-        skillsXp.put(PrimarySkillType.SWORDS, (float) Integer.parseInt(character[EXP_SWORDS]));
-        skillsXp.put(PrimarySkillType.AXES, (float) Integer.parseInt(character[EXP_AXES]));
-        skillsXp.put(PrimarySkillType.ACROBATICS, (float) Integer.parseInt(character[EXP_ACROBATICS]));
-        skillsXp.put(PrimarySkillType.FISHING, (float) Integer.parseInt(character[EXP_FISHING]));
-        skillsXp.put(PrimarySkillType.ALCHEMY, (float) Integer.parseInt(character[EXP_ALCHEMY]));
-
-        // Taming - Unused
-        skillsDATS.put(SuperAbilityType.SUPER_BREAKER, Integer.valueOf(character[COOLDOWN_SUPER_BREAKER]));
-        // Repair - Unused
-        skillsDATS.put(SuperAbilityType.TREE_FELLER, Integer.valueOf(character[COOLDOWN_TREE_FELLER]));
-        skillsDATS.put(SuperAbilityType.BERSERK, Integer.valueOf(character[COOLDOWN_BERSERK]));
-        skillsDATS.put(SuperAbilityType.GREEN_TERRA, Integer.valueOf(character[COOLDOWN_GREEN_TERRA]));
-        skillsDATS.put(SuperAbilityType.GIGA_DRILL_BREAKER, Integer.valueOf(character[COOLDOWN_GIGA_DRILL_BREAKER]));
-        // Archery - Unused
-        skillsDATS.put(SuperAbilityType.SERRATED_STRIKES, Integer.valueOf(character[COOLDOWN_SERRATED_STRIKES]));
-        skillsDATS.put(SuperAbilityType.SKULL_SPLITTER, Integer.valueOf(character[COOLDOWN_SKULL_SPLITTER]));
-        // Acrobatics - Unused
-        skillsDATS.put(SuperAbilityType.BLAST_MINING, Integer.valueOf(character[COOLDOWN_BLAST_MINING]));
-
-        try {
-            mobHealthbarType = MobHealthbarType.valueOf(character[HEALTHBAR]);
-        }
-        catch (Exception e) {
-            mobHealthbarType = Config.getInstance().getMobHealthbarDefault();
-        }
-
-        UUID uuid;
-        try {
-            uuid = UUID.fromString(character[UUID_INDEX]);
-        }
-        catch (Exception e) {
-            uuid = null;
-        }
-
-        try {
-            scoreboardTipsShown = Integer.parseInt(character[SCOREBOARD_TIPS]);
-        }
-        catch (Exception e) {
-            scoreboardTipsShown = 0;
-        }
-
-        try {
-            uniquePlayerDataMap.put(UniqueDataType.CHIMAERA_WING_DATS, Integer.valueOf(character[COOLDOWN_CHIMAERA_WING]));
-        }
-        catch (Exception e) {
-            uniquePlayerDataMap.put(UniqueDataType.CHIMAERA_WING_DATS, 0);
-        }
-
-        return new PlayerProfile(character[USERNAME], uuid, skills, skillsXp, skillsDATS, mobHealthbarType, scoreboardTipsShown, uniquePlayerDataMap);
-    }
-
-    private Map<PrimarySkillType, Integer> getSkillMapFromLine(String[] character) {
-        Map<PrimarySkillType, Integer> skills = new EnumMap<>(PrimarySkillType.class);   // Skill & Level
-
-        skills.put(PrimarySkillType.TAMING, Integer.valueOf(character[SKILLS_TAMING]));
-        skills.put(PrimarySkillType.MINING, Integer.valueOf(character[SKILLS_MINING]));
-        skills.put(PrimarySkillType.REPAIR, Integer.valueOf(character[SKILLS_REPAIR]));
-        skills.put(PrimarySkillType.WOODCUTTING, Integer.valueOf(character[SKILLS_WOODCUTTING]));
-        skills.put(PrimarySkillType.UNARMED, Integer.valueOf(character[SKILLS_UNARMED]));
-        skills.put(PrimarySkillType.HERBALISM, Integer.valueOf(character[SKILLS_HERBALISM]));
-        skills.put(PrimarySkillType.EXCAVATION, Integer.valueOf(character[SKILLS_EXCAVATION]));
-        skills.put(PrimarySkillType.ARCHERY, Integer.valueOf(character[SKILLS_ARCHERY]));
-        skills.put(PrimarySkillType.SWORDS, Integer.valueOf(character[SKILLS_SWORDS]));
-        skills.put(PrimarySkillType.AXES, Integer.valueOf(character[SKILLS_AXES]));
-        skills.put(PrimarySkillType.ACROBATICS, Integer.valueOf(character[SKILLS_ACROBATICS]));
-        skills.put(PrimarySkillType.FISHING, Integer.valueOf(character[SKILLS_FISHING]));
-        skills.put(PrimarySkillType.ALCHEMY, Integer.valueOf(character[SKILLS_ALCHEMY]));
-
-        return skills;
-    }
-
-    public DatabaseType getDatabaseType() {
-        return DatabaseType.FLATFILE;
-    }
-
-    @Override
-    public void onDisable() { }
-
-    private int getSkillIndex(PrimarySkillType skill) {
-        switch (skill) {
-            case ACROBATICS:
-                return SKILLS_ACROBATICS;
-            case ALCHEMY:
-                return SKILLS_ALCHEMY;
-            case ARCHERY:
-                return SKILLS_ARCHERY;
-            case AXES:
-                return SKILLS_AXES;
-            case EXCAVATION:
-                return SKILLS_EXCAVATION;
-            case FISHING:
-                return SKILLS_FISHING;
-            case HERBALISM:
-                return SKILLS_HERBALISM;
-            case MINING:
-                return SKILLS_MINING;
-            case REPAIR:
-                return SKILLS_REPAIR;
-            case SWORDS:
-                return SKILLS_SWORDS;
-            case TAMING:
-                return SKILLS_TAMING;
-            case UNARMED:
-                return SKILLS_UNARMED;
-            case WOODCUTTING:
-                return SKILLS_WOODCUTTING;
-            default:
-                throw new RuntimeException("Primary Skills only");
-            
-        }
-    }
-    
-    public static int USERNAME = 0;
-    public static int SKILLS_MINING = 1;
-    public static int EXP_MINING = 4;
-    public static int SKILLS_WOODCUTTING = 5;
-    public static int EXP_WOODCUTTING = 6;
-    public static int SKILLS_REPAIR = 7;
-    public static int SKILLS_UNARMED = 8;
-    public static int SKILLS_HERBALISM = 9;
-    public static int SKILLS_EXCAVATION = 10;
-    public static int SKILLS_ARCHERY = 11;
-    public static int SKILLS_SWORDS = 12;
-    public static int SKILLS_AXES = 13;
-    public static int SKILLS_ACROBATICS = 14;
-    public static int EXP_REPAIR = 15;
-    public static int EXP_UNARMED = 16;
-    public static int EXP_HERBALISM = 17;
-    public static int EXP_EXCAVATION = 18;
-    public static int EXP_ARCHERY = 19;
-    public static int EXP_SWORDS = 20;
-    public static int EXP_AXES = 21;
-    public static int EXP_ACROBATICS = 22;
-    public static int SKILLS_TAMING = 24;
-    public static int EXP_TAMING = 25;
-    public static int COOLDOWN_BERSERK = 26;
-    public static int COOLDOWN_GIGA_DRILL_BREAKER = 27;
-    public static int COOLDOWN_TREE_FELLER = 28;
-    public static int COOLDOWN_GREEN_TERRA = 29;
-    public static int COOLDOWN_SERRATED_STRIKES = 30;
-    public static int COOLDOWN_SKULL_SPLITTER = 31;
-    public static int COOLDOWN_SUPER_BREAKER = 32;
-    public static int SKILLS_FISHING = 34;
-    public static int EXP_FISHING = 35;
-    public static int COOLDOWN_BLAST_MINING = 36;
-    public static int LAST_LOGIN = 37;
-    public static int HEALTHBAR = 38;
-    public static int SKILLS_ALCHEMY = 39;
-    public static int EXP_ALCHEMY = 40;
-    public static int UUID_INDEX = 41;
-    public static int SCOREBOARD_TIPS = 42;
-    public static int COOLDOWN_CHIMAERA_WING = 43;
-
-    public void resetMobHealthSettings() {
-        BufferedReader in = null;
-        FileWriter out = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        synchronized (fileWritingLock) {
-            try {
-                in = new BufferedReader(new FileReader(usersFilePath));
-                StringBuilder writer = new StringBuilder();
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    // Remove empty lines from the file
-                    if (line.isEmpty()) {
-                        continue;
-                    }
-                    String[] character = line.split(":");
-                    
-                    character[HEALTHBAR] = Config.getInstance().getMobHealthbarDefault().toString();
-                    
-                    line = org.apache.commons.lang.StringUtils.join(character, ":") + ":";
-
-                    writer.append(line).append("\r\n");
-                }
-
-                // Write the new file
-                out = new FileWriter(usersFilePath);
-                out.write(writer.toString());
-            }
-            catch (IOException e) {
-                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-    }
-}

+ 220 - 105
src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java

@@ -1,8 +1,6 @@
 package com.gmail.nossr50.database;
 package com.gmail.nossr50.database;
 
 
 import com.gmail.nossr50.api.exceptions.InvalidSkillException;
 import com.gmail.nossr50.api.exceptions.InvalidSkillException;
-import com.gmail.nossr50.config.AdvancedConfig;
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.MobHealthbarType;
 import com.gmail.nossr50.datatypes.MobHealthbarType;
 import com.gmail.nossr50.datatypes.database.DatabaseType;
 import com.gmail.nossr50.datatypes.database.DatabaseType;
 import com.gmail.nossr50.datatypes.database.PlayerStat;
 import com.gmail.nossr50.datatypes.database.PlayerStat;
@@ -14,8 +12,11 @@ import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.runnables.database.UUIDUpdateAsyncTask;
 import com.gmail.nossr50.runnables.database.UUIDUpdateAsyncTask;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Misc;
+import com.gmail.nossr50.util.skills.SkillTools;
 import org.apache.tomcat.jdbc.pool.DataSource;
 import org.apache.tomcat.jdbc.pool.DataSource;
 import org.apache.tomcat.jdbc.pool.PoolProperties;
 import org.apache.tomcat.jdbc.pool.PoolProperties;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
 import org.bukkit.scheduler.BukkitRunnable;
 import org.bukkit.scheduler.BukkitRunnable;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.jetbrains.annotations.Nullable;
@@ -26,7 +27,12 @@ import java.util.concurrent.locks.ReentrantLock;
 
 
 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";
-    private final String tablePrefix = Config.getInstance().getMySQLTablePrefix();
+    public static final String MOBHEALTHBAR_VARCHAR = "VARCHAR(50)";
+    public static final String UUID_VARCHAR = "VARCHAR(36)";
+    public static final String USER_VARCHAR = "VARCHAR(40)";
+    public static final int CHILD_SKILLS_SIZE = 2;
+    public static final String LEGACY_DRIVER_PATH = "com.mysql.jdbc.Driver";
+    private final String tablePrefix = mcMMO.p.getGeneralConfig().getMySQLTablePrefix();
 
 
     private final Map<UUID, Integer> cachedUserIDs = new HashMap<>();
     private final Map<UUID, Integer> cachedUserIDs = new HashMap<>();
 
 
@@ -38,11 +44,15 @@ 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 String driverPath = "com.mysql.cj.jdbc.Driver"; //modern driver
+
     protected SQLDatabaseManager() {
     protected SQLDatabaseManager() {
-        String connectionString = "jdbc:mysql://" + Config.getInstance().getMySQLServerName()
-                + ":" + Config.getInstance().getMySQLServerPort() + "/" + Config.getInstance().getMySQLDatabaseName();
+        String connectionString = "jdbc:mysql://" + mcMMO.p.getGeneralConfig().getMySQLServerName()
+                + ":" + mcMMO.p.getGeneralConfig().getMySQLServerPort() + "/" + mcMMO.p.getGeneralConfig().getMySQLDatabaseName();
 
 
-        if(Config.getInstance().getMySQLSSL())
+        if(!mcMMO.getCompatibilityManager().getMinecraftGameVersion().isAtLeast(1, 17, 0) //Temporary hack for SQL and 1.17 support
+                && mcMMO.p.getGeneralConfig().getMySQLSSL())
             connectionString +=
             connectionString +=
                     "?verifyServerCertificate=false"+
                     "?verifyServerCertificate=false"+
                     "&useSSL=true"+
                     "&useSSL=true"+
@@ -51,26 +61,36 @@ public final class SQLDatabaseManager implements DatabaseManager {
             connectionString+=
             connectionString+=
                     "?useSSL=false";
                     "?useSSL=false";
 
 
+        if(mcMMO.p.getGeneralConfig().getMySQLPublicKeyRetrieval()) {
+            connectionString+=
+                    "&allowPublicKeyRetrieval=true";
+        }
+
         try {
         try {
             // Force driver to load if not yet loaded
             // Force driver to load if not yet loaded
-            Class.forName("com.mysql.jdbc.Driver");
-        }
-        catch (ClassNotFoundException e) {
-            e.printStackTrace();
-            return;
+            Class.forName(driverPath);
+        } catch (ClassNotFoundException e) {
+            try {
+                driverPath = LEGACY_DRIVER_PATH; //fall on deprecated path if new path isn't found
+                Class.forName(driverPath);
+            } catch (ClassNotFoundException ex) {
+                e.printStackTrace();
+                ex.printStackTrace();
+                mcMMO.p.getLogger().severe("Neither driver found");
+                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.
         }
         }
 
 
-        debug = Config.getInstance().getMySQLDebug();
-
+        debug = mcMMO.p.getGeneralConfig().getMySQLDebug();
 
 
         PoolProperties poolProperties = new PoolProperties();
         PoolProperties poolProperties = new PoolProperties();
-        poolProperties.setDriverClassName("com.mysql.jdbc.Driver");
+        poolProperties.setDriverClassName(driverPath);
         poolProperties.setUrl(connectionString);
         poolProperties.setUrl(connectionString);
-        poolProperties.setUsername(Config.getInstance().getMySQLUserName());
-        poolProperties.setPassword(Config.getInstance().getMySQLUserPassword());
-        poolProperties.setMaxIdle(Config.getInstance().getMySQLMaxPoolSize(PoolIdentifier.MISC));
-        poolProperties.setMaxActive(Config.getInstance().getMySQLMaxConnections(PoolIdentifier.MISC));
+        poolProperties.setUsername(mcMMO.p.getGeneralConfig().getMySQLUserName());
+        poolProperties.setPassword(mcMMO.p.getGeneralConfig().getMySQLUserPassword());
+        poolProperties.setMaxIdle(mcMMO.p.getGeneralConfig().getMySQLMaxPoolSize(PoolIdentifier.MISC));
+        poolProperties.setMaxActive(mcMMO.p.getGeneralConfig().getMySQLMaxConnections(PoolIdentifier.MISC));
         poolProperties.setInitialSize(0);
         poolProperties.setInitialSize(0);
         poolProperties.setMaxWait(-1);
         poolProperties.setMaxWait(-1);
         poolProperties.setRemoveAbandoned(true);
         poolProperties.setRemoveAbandoned(true);
@@ -80,13 +100,13 @@ public final class SQLDatabaseManager implements DatabaseManager {
         poolProperties.setValidationInterval(30000);
         poolProperties.setValidationInterval(30000);
         miscPool = new DataSource(poolProperties);
         miscPool = new DataSource(poolProperties);
         poolProperties = new PoolProperties();
         poolProperties = new PoolProperties();
-        poolProperties.setDriverClassName("com.mysql.jdbc.Driver");
+        poolProperties.setDriverClassName(driverPath);
         poolProperties.setUrl(connectionString);
         poolProperties.setUrl(connectionString);
-        poolProperties.setUsername(Config.getInstance().getMySQLUserName());
-        poolProperties.setPassword(Config.getInstance().getMySQLUserPassword());
+        poolProperties.setUsername(mcMMO.p.getGeneralConfig().getMySQLUserName());
+        poolProperties.setPassword(mcMMO.p.getGeneralConfig().getMySQLUserPassword());
         poolProperties.setInitialSize(0);
         poolProperties.setInitialSize(0);
-        poolProperties.setMaxIdle(Config.getInstance().getMySQLMaxPoolSize(PoolIdentifier.SAVE));
-        poolProperties.setMaxActive(Config.getInstance().getMySQLMaxConnections(PoolIdentifier.SAVE));
+        poolProperties.setMaxIdle(mcMMO.p.getGeneralConfig().getMySQLMaxPoolSize(PoolIdentifier.SAVE));
+        poolProperties.setMaxActive(mcMMO.p.getGeneralConfig().getMySQLMaxConnections(PoolIdentifier.SAVE));
         poolProperties.setMaxWait(-1);
         poolProperties.setMaxWait(-1);
         poolProperties.setRemoveAbandoned(true);
         poolProperties.setRemoveAbandoned(true);
         poolProperties.setRemoveAbandonedTimeout(60);
         poolProperties.setRemoveAbandonedTimeout(60);
@@ -95,13 +115,13 @@ public final class SQLDatabaseManager implements DatabaseManager {
         poolProperties.setValidationInterval(30000);
         poolProperties.setValidationInterval(30000);
         savePool = new DataSource(poolProperties);
         savePool = new DataSource(poolProperties);
         poolProperties = new PoolProperties();
         poolProperties = new PoolProperties();
-        poolProperties.setDriverClassName("com.mysql.jdbc.Driver");
+        poolProperties.setDriverClassName(driverPath);
         poolProperties.setUrl(connectionString);
         poolProperties.setUrl(connectionString);
-        poolProperties.setUsername(Config.getInstance().getMySQLUserName());
-        poolProperties.setPassword(Config.getInstance().getMySQLUserPassword());
+        poolProperties.setUsername(mcMMO.p.getGeneralConfig().getMySQLUserName());
+        poolProperties.setPassword(mcMMO.p.getGeneralConfig().getMySQLUserPassword());
         poolProperties.setInitialSize(0);
         poolProperties.setInitialSize(0);
-        poolProperties.setMaxIdle(Config.getInstance().getMySQLMaxPoolSize(PoolIdentifier.LOAD));
-        poolProperties.setMaxActive(Config.getInstance().getMySQLMaxConnections(PoolIdentifier.LOAD));
+        poolProperties.setMaxIdle(mcMMO.p.getGeneralConfig().getMySQLMaxPoolSize(PoolIdentifier.LOAD));
+        poolProperties.setMaxActive(mcMMO.p.getGeneralConfig().getMySQLMaxConnections(PoolIdentifier.LOAD));
         poolProperties.setMaxWait(-1);
         poolProperties.setMaxWait(-1);
         poolProperties.setRemoveAbandoned(true);
         poolProperties.setRemoveAbandoned(true);
         poolProperties.setRemoveAbandonedTimeout(60);
         poolProperties.setRemoveAbandonedTimeout(60);
@@ -113,7 +133,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
         checkStructure();
         checkStructure();
     }
     }
 
 
-    public void purgePowerlessUsers() {
+    public int purgePowerlessUsers() {
         massUpdateLock.lock();
         massUpdateLock.lock();
         mcMMO.p.getLogger().info("Purging powerless users...");
         mcMMO.p.getLogger().info("Purging powerless users...");
 
 
@@ -146,11 +166,12 @@ public final class SQLDatabaseManager implements DatabaseManager {
         }
         }
 
 
         mcMMO.p.getLogger().info("Purged " + purged + " users from the database.");
         mcMMO.p.getLogger().info("Purged " + purged + " users from the database.");
+        return purged;
     }
     }
 
 
     public void purgeOldUsers() {
     public void purgeOldUsers() {
         massUpdateLock.lock();
         massUpdateLock.lock();
-        mcMMO.p.getLogger().info("Purging inactive users older than " + (PURGE_TIME / 2630000000L) + " months...");
+        mcMMO.p.getLogger().info("Purging inactive users older than " + (mcMMO.p.getPurgeTime() / 2630000000L) + " months...");
 
 
         Connection connection = null;
         Connection connection = null;
         Statement statement = null;
         Statement statement = null;
@@ -165,12 +186,11 @@ 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 ((UNIX_TIMESTAMP() - lastlogin) > " + PURGE_TIME + ")");
+                    "WHERE ((UNIX_TIMESTAMP() - lastlogin) > " + mcMMO.p.getPurgeTime() + ")");
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
             printErrors(ex);
             printErrors(ex);
-        }
-        finally {
+        } finally {
             tryClose(statement);
             tryClose(statement);
             tryClose(connection);
             tryClose(connection);
             massUpdateLock.unlock();
             massUpdateLock.unlock();
@@ -266,7 +286,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             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));
             int total = 0;
             int total = 0;
-            for (PrimarySkillType primarySkillType : PrimarySkillType.NON_CHILD_SKILLS)
+            for (PrimarySkillType primarySkillType : SkillTools.NON_CHILD_SKILLS)
                 total += profile.getSkillLevel(primarySkillType);
                 total += profile.getSkillLevel(primarySkillType);
             statement.setInt(14, total);
             statement.setInt(14, total);
             statement.setInt(15, id);
             statement.setInt(15, id);
@@ -325,7 +345,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             }
             }
 
 
             statement = connection.prepareStatement("UPDATE " + tablePrefix + "huds SET mobhealthbar = ?, scoreboardtips = ? WHERE user_id = ?");
             statement = connection.prepareStatement("UPDATE " + tablePrefix + "huds SET mobhealthbar = ?, scoreboardtips = ? WHERE user_id = ?");
-            statement.setString(1, profile.getMobHealthbarType() == null ? Config.getInstance().getMobHealthbarDefault().name() : profile.getMobHealthbarType().name());
+            statement.setString(1, MobHealthbarType.HEARTS.name());
             statement.setInt(2, profile.getScoreboardTipsShown());
             statement.setInt(2, profile.getScoreboardTipsShown());
             statement.setInt(3, id);
             statement.setInt(3, id);
             success = (statement.executeUpdate() != 0);
             success = (statement.executeUpdate() != 0);
@@ -350,7 +370,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
         List<PlayerStat> stats = new ArrayList<>();
         List<PlayerStat> stats = new ArrayList<>();
 
 
         //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 && skill.isChildSkill()) {
+        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!");
             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!");
             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!");
         }
         }
@@ -399,7 +419,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
 
 
         try {
         try {
             connection = getConnection(PoolIdentifier.MISC);
             connection = getConnection(PoolIdentifier.MISC);
-            for (PrimarySkillType primarySkillType : PrimarySkillType.NON_CHILD_SKILLS) {
+            for (PrimarySkillType primarySkillType : SkillTools.NON_CHILD_SKILLS) {
                 String skillName = primarySkillType.name().toLowerCase(Locale.ENGLISH);
                 String skillName = primarySkillType.name().toLowerCase(Locale.ENGLISH);
                 // 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 " +
@@ -486,19 +506,37 @@ public final class SQLDatabaseManager implements DatabaseManager {
         return skills;
         return skills;
     }
     }
 
 
-    public void newUser(String playerName, UUID uuid) {
+    public @NotNull PlayerProfile newUser(String playerName, UUID uuid) {
         Connection connection = null;
         Connection connection = null;
 
 
         try {
         try {
             connection = getConnection(PoolIdentifier.MISC);
             connection = getConnection(PoolIdentifier.MISC);
             newUser(connection, playerName, uuid);
             newUser(connection, playerName, uuid);
-        }
-        catch (SQLException ex) {
+        } catch (SQLException ex) {
             printErrors(ex);
             printErrors(ex);
-        }
-        finally {
+        } finally {
             tryClose(connection);
             tryClose(connection);
         }
         }
+
+        return new PlayerProfile(playerName, uuid, true, mcMMO.p.getAdvancedConfig().getStartingLevel());
+    }
+
+    @Override
+    public @NotNull PlayerProfile newUser(@NotNull Player player) {
+        try {
+            Connection connection = getConnection(PoolIdentifier.SAVE);
+            int id = newUser(connection, player.getName(), player.getUniqueId());
+
+            if (id == -1) {
+                return new PlayerProfile(player.getName(), player.getUniqueId(), false, mcMMO.p.getAdvancedConfig().getStartingLevel());
+            } else {
+                return loadPlayerProfile(player);
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+
+        return new PlayerProfile(player.getName(), player.getUniqueId(), false, mcMMO.p.getAdvancedConfig().getStartingLevel());
     }
     }
 
 
     private int newUser(Connection connection, String playerName, UUID uuid) {
     private int newUser(Connection connection, String playerName, UUID uuid) {
@@ -539,20 +577,35 @@ public final class SQLDatabaseManager implements DatabaseManager {
         return -1;
         return -1;
     }
     }
 
 
-    @Deprecated
-    public PlayerProfile loadPlayerProfile(String playerName, boolean create) {
-        return loadPlayerProfile(playerName, null, false, true);
+    public @NotNull PlayerProfile loadPlayerProfile(@NotNull String playerName) {
+        try {
+            return loadPlayerFromDB(null, playerName);
+        } catch (RuntimeException e) {
+            e.printStackTrace();
+            return new PlayerProfile(playerName, false, mcMMO.p.getAdvancedConfig().getStartingLevel());
+        }
+    }
+
+    @Override
+    public @NotNull PlayerProfile loadPlayerProfile(@NotNull OfflinePlayer offlinePlayer) {
+        return loadPlayerFromDB(offlinePlayer.getUniqueId(), offlinePlayer.getName());
     }
     }
 
 
-    public PlayerProfile loadPlayerProfile(UUID uuid) {
-        return loadPlayerProfile("", uuid, false, true);
+        public @NotNull PlayerProfile loadPlayerProfile(@NotNull UUID uuid, @Nullable String playerName) {
+        return loadPlayerFromDB(uuid, playerName);
     }
     }
 
 
-    public PlayerProfile loadPlayerProfile(String playerName, UUID uuid, boolean create) {
-        return loadPlayerProfile(playerName, uuid, create, true);
+    @Override
+    public @NotNull PlayerProfile loadPlayerProfile(@NotNull UUID uuid) {
+        return loadPlayerFromDB(uuid, null);
     }
     }
 
 
-    private PlayerProfile loadPlayerProfile(String playerName, UUID uuid, boolean create, boolean retry) {
+
+    private PlayerProfile loadPlayerFromDB(@Nullable UUID uuid, @Nullable String playerName) throws RuntimeException {
+        if(uuid == null && playerName == null) {
+            throw new RuntimeException("Error looking up player, both UUID and playerName are null and one must not be.");
+        }
+
         PreparedStatement statement = null;
         PreparedStatement statement = null;
         Connection connection = null;
         Connection connection = null;
         ResultSet resultSet = null;
         ResultSet resultSet = null;
@@ -562,16 +615,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
             int id = getUserID(connection, playerName, uuid);
             int id = getUserID(connection, playerName, uuid);
 
 
             if (id == -1) {
             if (id == -1) {
-                // There is no such user
-                if (create) {
-                    id = newUser(connection, playerName, uuid);
-                    create = false;
-                    if (id == -1) {
-                        return new PlayerProfile(playerName, false);
-                    }
-                } else {
-                    return new PlayerProfile(playerName, false);
-                }
+            // There is no such user
+                return new PlayerProfile(playerName, mcMMO.p.getAdvancedConfig().getStartingLevel());
             }
             }
             // There is such a user
             // There is such a user
             writeMissingRows(connection, id);
             writeMissingRows(connection, id);
@@ -599,7 +644,10 @@ public final class SQLDatabaseManager implements DatabaseManager {
                     resultSet.close();
                     resultSet.close();
                     statement.close();
                     statement.close();
 
 
-                    if (!playerName.isEmpty() && !playerName.equalsIgnoreCase(name) && uuid != null) {
+                    if (playerName != null
+                            && !playerName.isEmpty()
+                            && !playerName.equalsIgnoreCase(name)
+                            && uuid != null) {
                         statement = connection.prepareStatement(
                         statement = connection.prepareStatement(
                                 "UPDATE `" + tablePrefix + "users` "
                                 "UPDATE `" + tablePrefix + "users` "
                                         + "SET user = ? "
                                         + "SET user = ? "
@@ -636,15 +684,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
             tryClose(connection);
             tryClose(connection);
         }
         }
 
 
-        // Problem, nothing was returned
-
-        // return unloaded profile
-        if (!retry) {
-            return new PlayerProfile(playerName, false);
-        }
-
-        // Retry, and abort on re-failure
-        return loadPlayerProfile(playerName, uuid, create, false);
+        //Return empty profile
+        return new PlayerProfile(playerName, mcMMO.p.getAdvancedConfig().getStartingLevel());
     }
     }
 
 
     public void convertUsers(DatabaseManager destination) {
     public void convertUsers(DatabaseManager destination) {
@@ -802,7 +843,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             statement = connection.prepareStatement("SELECT table_name FROM INFORMATION_SCHEMA.TABLES"
             statement = connection.prepareStatement("SELECT table_name FROM INFORMATION_SCHEMA.TABLES"
                     + " WHERE table_schema = ?"
                     + " WHERE table_schema = ?"
                     + " AND table_name = ?");
                     + " AND table_name = ?");
-            statement.setString(1, Config.getInstance().getMySQLDatabaseName());
+            statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName());
             statement.setString(2, tablePrefix + "users");
             statement.setString(2, tablePrefix + "users");
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
             if (!resultSet.next()) {
             if (!resultSet.next()) {
@@ -814,25 +855,25 @@ public final class SQLDatabaseManager implements DatabaseManager {
                     + "`lastlogin` int(32) unsigned NOT NULL,"
                     + "`lastlogin` int(32) unsigned NOT NULL,"
                     + "PRIMARY KEY (`id`),"
                     + "PRIMARY KEY (`id`),"
                     + "INDEX(`user`(20) ASC),"
                     + "INDEX(`user`(20) ASC),"
-                    + "UNIQUE KEY `uuid` (`uuid`)) DEFAULT CHARSET=latin1 AUTO_INCREMENT=1;");
+                    + "UNIQUE KEY `uuid` (`uuid`)) DEFAULT CHARSET=" + CHARSET_SQL + " AUTO_INCREMENT=1;");
                 tryClose(createStatement);
                 tryClose(createStatement);
             }
             }
             tryClose(resultSet);
             tryClose(resultSet);
-            statement.setString(1, Config.getInstance().getMySQLDatabaseName());
+            statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName());
             statement.setString(2, tablePrefix + "huds");
             statement.setString(2, tablePrefix + "huds");
             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 + "huds` ("
                 createStatement.executeUpdate("CREATE TABLE IF NOT EXISTS `" + tablePrefix + "huds` ("
                         + "`user_id` int(10) unsigned NOT NULL,"
                         + "`user_id` int(10) unsigned NOT NULL,"
-                        + "`mobhealthbar` varchar(50) NOT NULL DEFAULT '" + Config.getInstance().getMobHealthbarDefault() + "',"
+                        + "`mobhealthbar` varchar(50) NOT NULL DEFAULT '" + mcMMO.p.getGeneralConfig().getMobHealthbarDefault() + "',"
                         + "`scoreboardtips` int(10) NOT NULL DEFAULT '0',"
                         + "`scoreboardtips` int(10) NOT NULL DEFAULT '0',"
                         + "PRIMARY KEY (`user_id`)) "
                         + "PRIMARY KEY (`user_id`)) "
-                        + "DEFAULT CHARSET=latin1;");
+                        + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                 tryClose(createStatement);
                 tryClose(createStatement);
             }
             }
             tryClose(resultSet);
             tryClose(resultSet);
-            statement.setString(1, Config.getInstance().getMySQLDatabaseName());
+            statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName());
             statement.setString(2, tablePrefix + "cooldowns");
             statement.setString(2, tablePrefix + "cooldowns");
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
             if (!resultSet.next()) {
             if (!resultSet.next()) {
@@ -853,16 +894,16 @@ public final class SQLDatabaseManager implements DatabaseManager {
                         + "`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',"
                         + "PRIMARY KEY (`user_id`)) "
                         + "PRIMARY KEY (`user_id`)) "
-                        + "DEFAULT CHARSET=latin1;");
+                        + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                 tryClose(createStatement);
                 tryClose(createStatement);
             }
             }
             tryClose(resultSet);
             tryClose(resultSet);
-            statement.setString(1, Config.getInstance().getMySQLDatabaseName());
+            statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName());
             statement.setString(2, tablePrefix + "skills");
             statement.setString(2, tablePrefix + "skills");
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
             if (!resultSet.next()) {
             if (!resultSet.next()) {
-                String startingLevel = "'" + AdvancedConfig.getInstance().getStartingLevel() + "'";
-                String totalLevel = "'" + (AdvancedConfig.getInstance().getStartingLevel() * (PrimarySkillType.values().length - PrimarySkillType.CHILD_SKILLS.size())) + "'";
+                String startingLevel = "'" + mcMMO.p.getAdvancedConfig().getStartingLevel() + "'";
+                String totalLevel = "'" + (mcMMO.p.getAdvancedConfig().getStartingLevel() * (PrimarySkillType.values().length - CHILD_SKILLS_SIZE)) + "'";
                 createStatement = connection.createStatement();
                 createStatement = connection.createStatement();
                 createStatement.executeUpdate("CREATE TABLE IF NOT EXISTS `" + tablePrefix + "skills` ("
                 createStatement.executeUpdate("CREATE TABLE IF NOT EXISTS `" + tablePrefix + "skills` ("
                         + "`user_id` int(10) unsigned NOT NULL,"
                         + "`user_id` int(10) unsigned NOT NULL,"
@@ -881,11 +922,11 @@ public final class SQLDatabaseManager implements DatabaseManager {
                         + "`alchemy` int(10) unsigned NOT NULL DEFAULT "+startingLevel+","
                         + "`alchemy` 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=latin1;");
+                        + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                 tryClose(createStatement);
                 tryClose(createStatement);
             }
             }
             tryClose(resultSet);
             tryClose(resultSet);
-            statement.setString(1, Config.getInstance().getMySQLDatabaseName());
+            statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName());
             statement.setString(2, tablePrefix + "experience");
             statement.setString(2, tablePrefix + "experience");
             resultSet = statement.executeQuery();
             resultSet = statement.executeQuery();
             if (!resultSet.next()) {
             if (!resultSet.next()) {
@@ -906,7 +947,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                         + "`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',"
                         + "PRIMARY KEY (`user_id`)) "
                         + "PRIMARY KEY (`user_id`)) "
-                        + "DEFAULT CHARSET=latin1;");
+                        + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                 tryClose(createStatement);
                 tryClose(createStatement);
             }
             }
             tryClose(resultSet);
             tryClose(resultSet);
@@ -916,9 +957,9 @@ public final class SQLDatabaseManager implements DatabaseManager {
                 checkDatabaseStructure(connection, updateType);
                 checkDatabaseStructure(connection, updateType);
             }
             }
 
 
-            if (Config.getInstance().getTruncateSkills()) {
-                for (PrimarySkillType skill : PrimarySkillType.NON_CHILD_SKILLS) {
-                    int cap = Config.getInstance().getLevelCap(skill);
+            if (mcMMO.p.getGeneralConfig().getTruncateSkills()) {
+                for (PrimarySkillType skill : SkillTools.NON_CHILD_SKILLS) {
+                    int cap = mcMMO.p.getSkillTools().getLevelCap(skill);
                     if (cap != Integer.MAX_VALUE) {
                     if (cap != Integer.MAX_VALUE) {
                         statement = connection.prepareStatement("UPDATE `" + tablePrefix + "skills` SET `" + skill.name().toLowerCase(Locale.ENGLISH) + "` = " + cap + " WHERE `" + skill.name().toLowerCase(Locale.ENGLISH) + "` > " + cap);
                         statement = connection.prepareStatement("UPDATE `" + tablePrefix + "skills` SET `" + skill.name().toLowerCase(Locale.ENGLISH) + "` = " + cap + " WHERE `" + skill.name().toLowerCase(Locale.ENGLISH) + "` > " + cap);
                         statement.executeUpdate();
                         statement.executeUpdate();
@@ -991,7 +1032,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
                     break;
                     break;
 
 
                 case ADD_SQL_INDEXES:
                 case ADD_SQL_INDEXES:
-                    checkUpgradeAddSQLIndexes(statement);
+//                    checkUpgradeAddSQLIndexes(statement);
                     break;
                     break;
 
 
                 case ADD_MOB_HEALTHBARS:
                 case ADD_MOB_HEALTHBARS:
@@ -1029,12 +1070,14 @@ public final class SQLDatabaseManager implements DatabaseManager {
                     checkUpgradeAddUniqueChimaeraWing(statement);
                     checkUpgradeAddUniqueChimaeraWing(statement);
                     break;
                     break;
 
 
+                case SQL_CHARSET_UTF8MB4:
+                    updateCharacterSet(statement);
+                    break;
+
                 default:
                 default:
                     break;
                     break;
 
 
             }
             }
-
-            mcMMO.getUpgradeManager().setUpgradeCompleted(upgrade);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
             printErrors(ex);
             printErrors(ex);
@@ -1065,7 +1108,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
 
 
             statement = connection.prepareStatement("INSERT IGNORE INTO " + tablePrefix + "huds (user_id, mobhealthbar, scoreboardtips) VALUES (?, ?, ?)");
             statement = connection.prepareStatement("INSERT IGNORE INTO " + tablePrefix + "huds (user_id, mobhealthbar, scoreboardtips) VALUES (?, ?, ?)");
             statement.setInt(1, id);
             statement.setInt(1, id);
-            statement.setString(2, Config.getInstance().getMobHealthbarDefault().name());
+            statement.setString(2, mcMMO.p.getGeneralConfig().getMobHealthbarDefault().name());
             statement.setInt(3, 0);
             statement.setInt(3, 0);
             statement.execute();
             statement.execute();
             statement.close();
             statement.close();
@@ -1135,14 +1178,6 @@ public final class SQLDatabaseManager implements DatabaseManager {
         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));
 
 
-
-        try {
-            mobHealthbarType = MobHealthbarType.valueOf(result.getString(OFFSET_OTHER + 1));
-        }
-        catch (Exception e) {
-            mobHealthbarType = Config.getInstance().getMobHealthbarDefault();
-        }
-
         try {
         try {
             scoreboardTipsShown = result.getInt(OFFSET_OTHER + 2);
             scoreboardTipsShown = result.getInt(OFFSET_OTHER + 2);
         }
         }
@@ -1157,7 +1192,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             uuid = null;
             uuid = null;
         }
         }
 
 
-        return new PlayerProfile(playerName, uuid, skills, skillsXp, skillsDATS, mobHealthbarType, scoreboardTipsShown, uniqueData);
+        return new PlayerProfile(playerName, uuid, skills, skillsXp, skillsDATS, scoreboardTipsShown, uniqueData, null);
     }
     }
 
 
     private void printErrors(SQLException ex) {
     private void printErrors(SQLException ex) {
@@ -1191,6 +1226,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
             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)");
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.DROP_NAME_UNIQUENESS);
         } catch (SQLException ex) {
         } catch (SQLException ex) {
             ex.printStackTrace();
             ex.printStackTrace();
         } finally {
         } finally {
@@ -1201,6 +1237,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
     private void checkUpgradeAddAlchemy(final Statement statement) throws SQLException {
     private void checkUpgradeAddAlchemy(final Statement statement) throws SQLException {
         try {
         try {
             statement.executeQuery("SELECT `alchemy` FROM `" + tablePrefix + "skills` LIMIT 1");
             statement.executeQuery("SELECT `alchemy` FROM `" + tablePrefix + "skills` LIMIT 1");
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_ALCHEMY);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Alchemy...");
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Alchemy...");
@@ -1212,6 +1249,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
     private void checkUpgradeAddBlastMiningCooldown(final Statement statement) throws SQLException {
     private void checkUpgradeAddBlastMiningCooldown(final Statement statement) throws SQLException {
         try {
         try {
             statement.executeQuery("SELECT `blast_mining` FROM `" + tablePrefix + "cooldowns` LIMIT 1");
             statement.executeQuery("SELECT `blast_mining` FROM `" + tablePrefix + "cooldowns` LIMIT 1");
+            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...");
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Blast Mining...");
@@ -1222,6 +1260,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
     private void checkUpgradeAddUniqueChimaeraWing(final Statement statement) throws SQLException {
     private void checkUpgradeAddUniqueChimaeraWing(final Statement statement) throws SQLException {
         try {
         try {
             statement.executeQuery("SELECT `chimaera_wing` FROM `" + tablePrefix + "cooldowns` LIMIT 1");
             statement.executeQuery("SELECT `chimaera_wing` FROM `" + tablePrefix + "cooldowns` LIMIT 1");
+            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...");
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Chimaera Wing...");
@@ -1232,6 +1271,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
     private void checkUpgradeAddFishing(final Statement statement) throws SQLException {
     private void checkUpgradeAddFishing(final Statement statement) throws SQLException {
         try {
         try {
             statement.executeQuery("SELECT `fishing` FROM `" + tablePrefix + "skills` LIMIT 1");
             statement.executeQuery("SELECT `fishing` FROM `" + tablePrefix + "skills` LIMIT 1");
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_FISHING);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Fishing...");
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Fishing...");
@@ -1243,16 +1283,18 @@ public final class SQLDatabaseManager implements DatabaseManager {
     private void checkUpgradeAddMobHealthbars(final Statement statement) throws SQLException {
     private void checkUpgradeAddMobHealthbars(final Statement statement) throws SQLException {
         try {
         try {
             statement.executeQuery("SELECT `mobhealthbar` FROM `" + tablePrefix + "huds` LIMIT 1");
             statement.executeQuery("SELECT `mobhealthbar` FROM `" + tablePrefix + "huds` LIMIT 1");
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_MOB_HEALTHBARS);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for mob healthbars...");
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for mob healthbars...");
-            statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` ADD `mobhealthbar` varchar(50) NOT NULL DEFAULT '" + Config.getInstance().getMobHealthbarDefault() + "'");
+            statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` ADD `mobhealthbar` varchar(50) NOT NULL DEFAULT '" + mcMMO.p.getGeneralConfig().getMobHealthbarDefault() + "'");
         }
         }
     }
     }
 
 
     private void checkUpgradeAddScoreboardTips(final Statement statement) throws SQLException {
     private void checkUpgradeAddScoreboardTips(final Statement statement) throws SQLException {
         try {
         try {
             statement.executeQuery("SELECT `scoreboardtips` FROM `" + tablePrefix + "huds` LIMIT 1");
             statement.executeQuery("SELECT `scoreboardtips` FROM `" + tablePrefix + "huds` LIMIT 1");
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_SCOREBOARD_TIPS);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for scoreboard tips...");
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for scoreboard tips...");
@@ -1267,10 +1309,10 @@ public final class SQLDatabaseManager implements DatabaseManager {
             resultSet = statement.executeQuery("SHOW INDEX FROM `" + tablePrefix + "skills` WHERE `Key_name` LIKE 'idx\\_%'");
             resultSet = statement.executeQuery("SHOW INDEX FROM `" + tablePrefix + "skills` WHERE `Key_name` LIKE 'idx\\_%'");
             resultSet.last();
             resultSet.last();
 
 
-            if (resultSet.getRow() != PrimarySkillType.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");
                 mcMMO.p.getLogger().info("Indexing tables, this may take a while on larger databases");
 
 
-                for (PrimarySkillType skill : PrimarySkillType.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);
 
 
                     try {
                     try {
@@ -1281,6 +1323,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
                     }
                     }
                 }
                 }
             }
             }
+
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_SQL_INDEXES);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
             printErrors(ex);
             printErrors(ex);
@@ -1310,7 +1354,11 @@ public final class SQLDatabaseManager implements DatabaseManager {
                 mcMMO.p.getLogger().info("Adding UUIDs to mcMMO MySQL user table...");
                 mcMMO.p.getLogger().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");
+
+                new GetUUIDUpdatesRequired().runTaskLaterAsynchronously(mcMMO.p, 100); // wait until after first purge
             }
             }
+
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_UUIDS);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
             printErrors(ex);
             printErrors(ex);
@@ -1318,8 +1366,6 @@ public final class SQLDatabaseManager implements DatabaseManager {
         finally {
         finally {
             tryClose(resultSet);
             tryClose(resultSet);
         }
         }
-
-        new GetUUIDUpdatesRequired().runTaskLaterAsynchronously(mcMMO.p, 100); // wait until after first purge
     }
     }
 
 
     private class GetUUIDUpdatesRequired extends BukkitRunnable {
     private class GetUUIDUpdatesRequired extends BukkitRunnable {
@@ -1377,6 +1423,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
                 mcMMO.p.getLogger().info("Removing party name from users table...");
                 mcMMO.p.getLogger().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`");
             }
             }
+
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.DROP_SQL_PARTY_NAMES);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
             printErrors(ex);
             printErrors(ex);
@@ -1412,6 +1460,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
                 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");
                 connection.commit();
                 connection.commit();
             }
             }
+
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_SKILL_TOTAL);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
             printErrors(ex);
             printErrors(ex);
@@ -1443,6 +1493,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
                 mcMMO.p.getLogger().info("Removing Spout HUD type from huds table...");
                 mcMMO.p.getLogger().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`");
             }
             }
+
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.DROP_SPOUT);
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
             printErrors(ex);
             printErrors(ex);
@@ -1544,7 +1596,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
         try {
         try {
             connection = getConnection(PoolIdentifier.MISC);
             connection = getConnection(PoolIdentifier.MISC);
             statement = connection.prepareStatement("UPDATE " + tablePrefix + "huds SET mobhealthbar = ?");
             statement = connection.prepareStatement("UPDATE " + tablePrefix + "huds SET mobhealthbar = ?");
-            statement.setString(1, Config.getInstance().getMobHealthbarDefault().toString());
+            statement.setString(1, mcMMO.p.getGeneralConfig().getMobHealthbarDefault().toString());
             statement.executeUpdate();
             statement.executeUpdate();
         }
         }
         catch (SQLException ex) {
         catch (SQLException ex) {
@@ -1555,4 +1607,67 @@ public final class SQLDatabaseManager implements DatabaseManager {
             tryClose(connection);
             tryClose(connection);
         }
         }
     }
     }
+
+    private void updateCharacterSet(@NotNull Statement statement) {
+        //TODO: Could check the tables for being latin1 before executing queries but it seems moot because it is likely the same computational effort
+        /*
+            The following columns were set to use latin1 historically (now utf8mb4)
+            column user in <tablePrefix>users
+            column uuid in <tablePrefix>users
+
+            column mobhealthbar in <tablePrefix>huds
+         */
+
+        //Alter users table
+        mcMMO.p.getLogger().info("SQL Converting tables from latin1 to utf8mb4");
+
+        //Update "user" column
+        try {
+        mcMMO.p.getLogger().info("Updating user column to new encoding");
+        statement.executeUpdate(getUpdateUserInUsersTableSQLQuery());
+
+        //Update "uuid" column
+        mcMMO.p.getLogger().info("Updating user column to new encoding");
+        statement.executeUpdate(getUpdateUUIDInUsersTableSQLQuery());
+
+        //Update "mobhealthbar" column
+        mcMMO.p.getLogger().info("Updating mobhealthbar column to new encoding");
+        statement.executeUpdate(getUpdateMobHealthBarInHudsTableSQLQuery());
+
+        mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.SQL_CHARSET_UTF8MB4);
+
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @NotNull
+    private String getUpdateUserInUsersTableSQLQuery() {
+        return "ALTER TABLE\n" +
+                "    " + tablePrefix + "users\n" +
+                "    CHANGE user user\n" +
+                "    " + USER_VARCHAR + "\n" +
+                "    CHARACTER SET utf8mb4\n" +
+                "    COLLATE utf8mb4_unicode_ci;";
+    }
+
+    @NotNull
+    private String getUpdateUUIDInUsersTableSQLQuery() {
+        return "ALTER TABLE\n" +
+                "    " + tablePrefix + "users\n" +
+                "    CHANGE uuid uuid\n" +
+                "    " + UUID_VARCHAR + "\n" +
+                "    CHARACTER SET utf8mb4\n" +
+                "    COLLATE utf8mb4_unicode_ci;";
+    }
+
+    @NotNull
+    private String getUpdateMobHealthBarInHudsTableSQLQuery() {
+        return "ALTER TABLE\n" +
+                "    " + tablePrefix + "huds\n" +
+                "    CHANGE mobhealthbar mobhealthbar\n" +
+                "    " + MOBHEALTHBAR_VARCHAR + "\n" +
+                "    CHARACTER SET utf8mb4\n" +
+                "    COLLATE utf8mb4_unicode_ci;";
+    }
 }
 }

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

@@ -0,0 +1,7 @@
+package com.gmail.nossr50.database;
+
+import org.jetbrains.annotations.NotNull;
+
+public interface UserQuery {
+    @NotNull UserQueryType getType();
+}

+ 31 - 0
src/main/java/com/gmail/nossr50/database/UserQueryFull.java

@@ -0,0 +1,31 @@
+package com.gmail.nossr50.database;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public class UserQueryFull implements UserQueryUUID, UserQueryName {
+
+    private final @NotNull String name;
+    private final @NotNull UUID uuid;
+
+    public UserQueryFull(@NotNull String name, @NotNull UUID uuid) {
+        this.name = name;
+        this.uuid = uuid;
+    }
+
+    @Override
+    public @NotNull UserQueryType getType() {
+        return UserQueryType.UUID_AND_NAME;
+    }
+
+    @Override
+    public @NotNull String getName() {
+        return name;
+    }
+
+    @Override
+    public @NotNull UUID getUUID() {
+        return uuid;
+    }
+}

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

@@ -0,0 +1,7 @@
+package com.gmail.nossr50.database;
+
+import org.jetbrains.annotations.NotNull;
+
+public interface UserQueryName extends UserQuery {
+    @NotNull String getName();
+}

+ 20 - 0
src/main/java/com/gmail/nossr50/database/UserQueryNameImpl.java

@@ -0,0 +1,20 @@
+package com.gmail.nossr50.database;
+
+import org.jetbrains.annotations.NotNull;
+
+public class UserQueryNameImpl implements UserQueryName {
+    private final @NotNull String name;
+
+    public UserQueryNameImpl(@NotNull String name) {
+        this.name = name;
+    }
+
+    @Override
+    public @NotNull UserQueryType getType() {
+        return UserQueryType.NAME;
+    }
+
+    public @NotNull String getName() {
+        return name;
+    }
+}

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

@@ -0,0 +1,7 @@
+package com.gmail.nossr50.database;
+
+public enum UserQueryType {
+    UUID_AND_NAME,
+    UUID,
+    NAME
+}

+ 11 - 0
src/main/java/com/gmail/nossr50/database/UserQueryUUID.java

@@ -0,0 +1,11 @@
+package com.gmail.nossr50.database;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public interface UserQueryUUID extends UserQuery {
+
+    @NotNull UUID getUUID();
+
+}

+ 23 - 0
src/main/java/com/gmail/nossr50/database/UserQueryUUIDImpl.java

@@ -0,0 +1,23 @@
+package com.gmail.nossr50.database;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public class UserQueryUUIDImpl implements UserQueryUUID {
+    private final @NotNull UUID uuid;
+
+    public UserQueryUUIDImpl(@NotNull UUID uuid) {
+        this.uuid = uuid;
+    }
+
+    @Override
+    public @NotNull UserQueryType getType() {
+        return UserQueryType.UUID;
+    }
+
+    @Override
+    public @NotNull UUID getUUID() {
+        return uuid;
+    }
+}

+ 42 - 0
src/main/java/com/gmail/nossr50/database/flatfile/BadCategorizedFlatFileData.java

@@ -0,0 +1,42 @@
+package com.gmail.nossr50.database.flatfile;
+
+import com.gmail.nossr50.database.FlatFileDataFlag;
+import com.google.common.base.Objects;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+public class BadCategorizedFlatFileData extends CategorizedFlatFileData {
+    private final boolean[] badDataIndexes;
+
+    protected BadCategorizedFlatFileData(int uniqueProcessingId, @NotNull HashSet<FlatFileDataFlag> dataFlags, @NotNull String[] splitData, boolean[] badDataIndexes) {
+        super(uniqueProcessingId, dataFlags, splitData);
+        this.badDataIndexes = badDataIndexes;
+    }
+
+    public boolean[] getBadDataIndexes() {
+        return badDataIndexes;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        if (!super.equals(o)) return false;
+        BadCategorizedFlatFileData that = (BadCategorizedFlatFileData) o;
+        return Objects.equal(badDataIndexes, that.badDataIndexes);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(super.hashCode(), badDataIndexes);
+    }
+
+    @Override
+    public String toString() {
+        return "BadCategorizedFlatFileData{" +
+                "badDataIndexes=" + Arrays.toString(badDataIndexes) +
+                '}';
+    }
+}

+ 58 - 0
src/main/java/com/gmail/nossr50/database/flatfile/CategorizedFlatFileData.java

@@ -0,0 +1,58 @@
+package com.gmail.nossr50.database.flatfile;
+
+import com.gmail.nossr50.database.FlatFileDataFlag;
+import com.google.common.base.Objects;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class CategorizedFlatFileData implements FlatFileDataContainer {
+    private final @NotNull Set<FlatFileDataFlag> dataFlags;
+    private final @NotNull String[] splitData;
+    private final int uniqueProcessingId;
+
+    public CategorizedFlatFileData(int uniqueProcessingId, @NotNull HashSet<FlatFileDataFlag> dataFlags, @NotNull String[] splitData) {
+        this.uniqueProcessingId = uniqueProcessingId;
+        this.dataFlags = dataFlags;
+        this.splitData = splitData;
+    }
+
+    public @NotNull Set<FlatFileDataFlag> getDataFlags() {
+        return dataFlags;
+    }
+
+    public @NotNull String[] getSplitData() {
+        return splitData;
+    }
+
+    public int getUniqueProcessingId() {
+        return uniqueProcessingId;
+    }
+
+    public boolean isHealthyData() {
+        return dataFlags.size() == 0;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        CategorizedFlatFileData that = (CategorizedFlatFileData) o;
+        return uniqueProcessingId == that.uniqueProcessingId && Objects.equal(dataFlags, that.dataFlags) && Objects.equal(splitData, that.splitData);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(dataFlags, splitData, uniqueProcessingId);
+    }
+
+    @Override
+    public String toString() {
+        return "CategorizedFlatFileData{" +
+                "dataFlags=" + dataFlags +
+                ", stringDataRepresentation='" + splitData + '\'' +
+                ", uniqueProcessingId=" + uniqueProcessingId +
+                '}';
+    }
+}

+ 42 - 0
src/main/java/com/gmail/nossr50/database/flatfile/FlatFileDataBuilder.java

@@ -0,0 +1,42 @@
+package com.gmail.nossr50.database.flatfile;
+
+import com.gmail.nossr50.database.FlatFileDataFlag;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.HashSet;
+
+public class FlatFileDataBuilder {
+    private final @NotNull HashSet<FlatFileDataFlag> dataFlags;
+    private @NotNull String[] splitStringData;
+    private final int uniqueProcessingId;
+    private boolean[] badDataValues;
+
+    public FlatFileDataBuilder(@NotNull String[] splitStringData, int uniqueProcessingId) {
+        this.uniqueProcessingId = uniqueProcessingId;
+        this.splitStringData = splitStringData;
+        dataFlags = new HashSet<>();
+    }
+
+    public @NotNull FlatFileDataBuilder appendFlag(@NotNull FlatFileDataFlag dataFlag) {
+        dataFlags.add(dataFlag);
+        return this;
+    }
+
+    public @NotNull FlatFileDataBuilder appendBadDataValues(boolean[] badDataValues) {
+        this.badDataValues = badDataValues;
+        return this;
+    }
+
+    public @NotNull FlatFileDataContainer build() {
+        if(dataFlags.contains(FlatFileDataFlag.BAD_VALUES)) {
+            return new BadCategorizedFlatFileData(uniqueProcessingId, dataFlags, splitStringData, badDataValues);
+        }
+
+        return new CategorizedFlatFileData(uniqueProcessingId, dataFlags, splitStringData);
+    }
+
+    public @NotNull FlatFileDataBuilder setSplitStringData(@NotNull String[] splitStringData) {
+        this.splitStringData = splitStringData;
+        return this;
+    }
+}

+ 21 - 0
src/main/java/com/gmail/nossr50/database/flatfile/FlatFileDataContainer.java

@@ -0,0 +1,21 @@
+package com.gmail.nossr50.database.flatfile;
+
+import com.gmail.nossr50.database.FlatFileDataFlag;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Set;
+
+public interface FlatFileDataContainer {
+    default @Nullable Set<FlatFileDataFlag> getDataFlags() {
+        return null;
+    }
+
+    @NotNull String[] getSplitData();
+
+    int getUniqueProcessingId();
+
+    default boolean isHealthyData() {
+        return getDataFlags() == null || getDataFlags().size() == 0;
+    }
+}

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

@@ -0,0 +1,115 @@
+package com.gmail.nossr50.database.flatfile;
+
+import com.gmail.nossr50.database.FlatFileDataFlag;
+import com.gmail.nossr50.database.FlatFileDatabaseManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static com.gmail.nossr50.database.FlatFileDatabaseManager.*;
+
+public class FlatFileDataUtil {
+
+    public static @Nullable String[] getPreparedSaveDataLine(@NotNull FlatFileDataContainer dataContainer) {
+        if(dataContainer.getDataFlags() == null) {
+            return dataContainer.getSplitData();
+        }
+
+        //Data of this type is not salvageable
+        //TODO: Test that we ignore the things we are supposed to ignore
+        //TODO: Should we even keep track of the bad data or just not even build data containers for it? Making containers for it is only really useful for debugging.. well I suppose operations are typically async so it shouldn't matter
+        if(dataContainer.getDataFlags().contains(FlatFileDataFlag.CORRUPTED_OR_UNRECOGNIZABLE)
+                || dataContainer.getDataFlags().contains(FlatFileDataFlag.DUPLICATE_UUID) //For now we will not try to fix any issues with UUIDs
+                || dataContainer.getDataFlags().contains(FlatFileDataFlag.BAD_UUID_DATA) //For now we will not try to fix any issues with UUIDs
+                || dataContainer.getDataFlags().contains(FlatFileDataFlag.TOO_INCOMPLETE)) {
+            return null;
+        }
+
+        String[] splitData;
+
+        /*
+         * First fix the bad data values if they exist
+         */
+        if(dataContainer instanceof BadCategorizedFlatFileData badData) {
+            splitData = repairBadData(dataContainer.getSplitData(), badData.getBadDataIndexes());
+        } else {
+            splitData = dataContainer.getSplitData();
+        }
+
+        //Make sure we have as many values as we are supposed to
+        assert splitData.length == FlatFileDatabaseManager.DATA_ENTRY_COUNT;
+        return splitData;
+    }
+
+    public static @NotNull String[] repairBadData(@NotNull String[] splitData, boolean[] badDataValues) {
+        for(int i = 0; i < FlatFileDatabaseManager.DATA_ENTRY_COUNT; i++) {
+            if(badDataValues[i]) {
+                //This data value was marked as bad so we zero initialize it
+                splitData[i] = getZeroInitialisedData(i, 0);
+            }
+        }
+
+        return splitData;
+    }
+
+    /**
+     * @param index "zero" Initialization will depend on what the index is for
+     * @return the "zero" initialized data corresponding to the index
+     */
+    public static @NotNull String getZeroInitialisedData(int index, int startingLevel) throws IndexOutOfBoundsException {
+        switch(index) {
+            case USERNAME_INDEX:
+                return LEGACY_INVALID_OLD_USERNAME; //We'll keep using this value for legacy compatibility reasons (not sure if needed but don't care)
+            case 2: //Assumption: Used to be for something, no longer used
+            case 3: //Assumption: Used to be for something, no longer used
+            case 23: //Assumption: Used to be used for something, no longer used
+            case 33: //Assumption: Used to be used for something, no longer used
+            case LEGACY_LAST_LOGIN:
+            case HEALTHBAR:
+                return "IGNORED";
+            case SKILLS_MINING:
+            case SKILLS_REPAIR:
+            case SKILLS_UNARMED:
+            case SKILLS_HERBALISM:
+            case SKILLS_EXCAVATION:
+            case SKILLS_ARCHERY:
+            case SKILLS_SWORDS:
+            case SKILLS_AXES:
+            case SKILLS_WOODCUTTING:
+            case SKILLS_ACROBATICS:
+            case SKILLS_TAMING:
+            case SKILLS_FISHING:
+            case SKILLS_ALCHEMY:
+                return String.valueOf(startingLevel);
+            case OVERHAUL_LAST_LOGIN:
+                return String.valueOf(-1L);
+            case COOLDOWN_BERSERK:
+            case COOLDOWN_GIGA_DRILL_BREAKER:
+            case COOLDOWN_TREE_FELLER:
+            case COOLDOWN_GREEN_TERRA:
+            case COOLDOWN_SERRATED_STRIKES:
+            case COOLDOWN_SKULL_SPLITTER:
+            case COOLDOWN_SUPER_BREAKER:
+            case COOLDOWN_BLAST_MINING:
+            case SCOREBOARD_TIPS:
+            case COOLDOWN_CHIMAERA_WING:
+            case EXP_MINING:
+            case EXP_WOODCUTTING:
+            case EXP_REPAIR:
+            case EXP_UNARMED:
+            case EXP_HERBALISM:
+            case EXP_EXCAVATION:
+            case EXP_ARCHERY:
+            case EXP_SWORDS:
+            case EXP_AXES:
+            case EXP_ACROBATICS:
+            case EXP_TAMING:
+            case EXP_FISHING:
+            case EXP_ALCHEMY:
+                return "0";
+            case UUID_INDEX:
+                throw new IndexOutOfBoundsException(); //TODO: Add UUID recovery? Might not even be worth it.
+        }
+
+        throw new IndexOutOfBoundsException();
+    }
+}

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

@@ -0,0 +1,7 @@
+package com.gmail.nossr50.database.flatfile;
+
+public enum LeaderboardStatus {
+    TOO_SOON_TO_UPDATE,
+    UPDATED,
+    FAILED
+}

+ 7 - 9
src/main/java/com/gmail/nossr50/datatypes/LevelUpBroadcastPredicate.java

@@ -1,6 +1,5 @@
 package com.gmail.nossr50.datatypes;
 package com.gmail.nossr50.datatypes;
 
 
-import com.gmail.nossr50.config.Config;
 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;
@@ -38,11 +37,10 @@ public class LevelUpBroadcastPredicate<T extends CommandSender> implements Predi
             return false;
             return false;
         }
         }
 
 
-        if(t instanceof Player) {
-            Player listeningPlayer = (Player) t;
+        if(t instanceof Player listeningPlayer) {
 
 
             //Party Member Check
             //Party Member Check
-            if(Config.getInstance().isLevelUpBroadcastsPartyMembersOnly()) {
+            if(mcMMO.p.getGeneralConfig().isLevelUpBroadcastsPartyMembersOnly()) {
                 McMMOPlayer mmoListeningPlayer = UserManager.getPlayer(listeningPlayer);
                 McMMOPlayer mmoListeningPlayer = UserManager.getPlayer(listeningPlayer);
 
 
                 if(mmoListeningPlayer == null) {
                 if(mmoListeningPlayer == null) {
@@ -68,27 +66,27 @@ public class LevelUpBroadcastPredicate<T extends CommandSender> implements Predi
                 }
                 }
 
 
                 //Distance checks
                 //Distance checks
-                if(Config.getInstance().shouldLevelUpBroadcastsRestrictDistance()) {
-                    if(!Misc.isNear(mmoBroadcastingPlayer.getPlayer().getLocation(), listeningPlayer.getLocation(), Config.getInstance().getLevelUpBroadcastRadius())) {
+                if(mcMMO.p.getGeneralConfig().shouldLevelUpBroadcastsRestrictDistance()) {
+                    if(!Misc.isNear(mmoBroadcastingPlayer.getPlayer().getLocation(), listeningPlayer.getLocation(), mcMMO.p.getGeneralConfig().getLevelUpBroadcastRadius())) {
                         return false;
                         return false;
                     }
                     }
                 }
                 }
             }
             }
 
 
             //Visibility checks
             //Visibility checks
-            if(!listeningPlayer.canSee(mmoBroadcastingPlayer.getPlayer())) {
+            if(!listeningPlayer.canSee(mmoBroadcastingPlayer.getPlayer()) && listeningPlayer != mmoBroadcastingPlayer.getPlayer()) {
                 return false; //Player who leveled should be invisible to this player so don't send the message
                 return false; //Player who leveled should be invisible to this player so don't send the message
             }
             }
 
 
             return true;
             return true;
         } else {
         } else {
             //Send out to console
             //Send out to console
-            return Config.getInstance().shouldLevelUpBroadcastToConsole();
+            return mcMMO.p.getGeneralConfig().shouldLevelUpBroadcastToConsole();
         }
         }
     }
     }
 
 
     private static boolean isLevelUpBroadcastsSameWorldOnly() {
     private static boolean isLevelUpBroadcastsSameWorldOnly() {
-        return Config.getInstance().isLevelUpBroadcastsSameWorldOnly();
+        return mcMMO.p.getGeneralConfig().isLevelUpBroadcastsSameWorldOnly();
     }
     }
 
 
     @Override
     @Override

+ 7 - 9
src/main/java/com/gmail/nossr50/datatypes/PowerLevelUpBroadcastPredicate.java

@@ -1,6 +1,5 @@
 package com.gmail.nossr50.datatypes;
 package com.gmail.nossr50.datatypes;
 
 
-import com.gmail.nossr50.config.Config;
 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;
@@ -38,11 +37,10 @@ public class PowerLevelUpBroadcastPredicate<T extends CommandSender> implements
             return false;
             return false;
         }
         }
 
 
-        if(t instanceof Player) {
-            Player listeningPlayer = (Player) t;
+        if(t instanceof Player listeningPlayer) {
 
 
             //Party Member Check
             //Party Member Check
-            if(Config.getInstance().isPowerLevelUpBroadcastsPartyMembersOnly()) {
+            if(mcMMO.p.getGeneralConfig().isPowerLevelUpBroadcastsPartyMembersOnly()) {
                 McMMOPlayer mmoListeningPlayer = UserManager.getPlayer(listeningPlayer);
                 McMMOPlayer mmoListeningPlayer = UserManager.getPlayer(listeningPlayer);
 
 
                 if(mmoListeningPlayer == null) {
                 if(mmoListeningPlayer == null) {
@@ -68,27 +66,27 @@ public class PowerLevelUpBroadcastPredicate<T extends CommandSender> implements
                 }
                 }
 
 
                 //Distance checks
                 //Distance checks
-                if(Config.getInstance().shouldPowerLevelUpBroadcastsRestrictDistance()) {
-                    if(!Misc.isNear(mmoBroadcastingPlayer.getPlayer().getLocation(), listeningPlayer.getLocation(), Config.getInstance().getPowerLevelUpBroadcastRadius())) {
+                if(mcMMO.p.getGeneralConfig().shouldPowerLevelUpBroadcastsRestrictDistance()) {
+                    if(!Misc.isNear(mmoBroadcastingPlayer.getPlayer().getLocation(), listeningPlayer.getLocation(), mcMMO.p.getGeneralConfig().getPowerLevelUpBroadcastRadius())) {
                         return false;
                         return false;
                     }
                     }
                 }
                 }
             }
             }
 
 
             //Visibility checks
             //Visibility checks
-            if(!listeningPlayer.canSee(mmoBroadcastingPlayer.getPlayer())) {
+            if(!listeningPlayer.canSee(mmoBroadcastingPlayer.getPlayer()) && listeningPlayer != mmoBroadcastingPlayer.getPlayer()) {
                 return false; //Player who leveled should be invisible to this player so don't send the message
                 return false; //Player who leveled should be invisible to this player so don't send the message
             }
             }
 
 
             return true;
             return true;
         } else {
         } else {
             //Send out to console
             //Send out to console
-            return Config.getInstance().shouldPowerLevelUpBroadcastToConsole();
+            return mcMMO.p.getGeneralConfig().shouldPowerLevelUpBroadcastToConsole();
         }
         }
     }
     }
 
 
     private static boolean isPowerLevelUpBroadcastsSameWorldOnly() {
     private static boolean isPowerLevelUpBroadcastsSameWorldOnly() {
-        return Config.getInstance().isPowerLevelUpBroadcastsSameWorldOnly();
+        return mcMMO.p.getGeneralConfig().isPowerLevelUpBroadcastsSameWorldOnly();
     }
     }
 
 
     @Override
     @Override

+ 2 - 1
src/main/java/com/gmail/nossr50/datatypes/database/UpgradeType.java

@@ -16,5 +16,6 @@ public enum UpgradeType {
     ADD_UNIQUE_PLAYER_DATA,
     ADD_UNIQUE_PLAYER_DATA,
     FIX_SPELLING_NETHERITE_SALVAGE,
     FIX_SPELLING_NETHERITE_SALVAGE,
     FIX_SPELLING_NETHERITE_REPAIR,
     FIX_SPELLING_NETHERITE_REPAIR,
-    FIX_NETHERITE_SALVAGE_QUANTITIES
+    FIX_NETHERITE_SALVAGE_QUANTITIES,
+    SQL_CHARSET_UTF8MB4
 }
 }

+ 7 - 5
src/main/java/com/gmail/nossr50/datatypes/interactions/NotificationType.java

@@ -1,5 +1,7 @@
 package com.gmail.nossr50.datatypes.interactions;
 package com.gmail.nossr50.datatypes.interactions;
 
 
+import org.jetbrains.annotations.NotNull;
+
 /**
 /**
  * This class helps define the types of information interactions we will have with players
  * This class helps define the types of information interactions we will have with players
  */
  */
@@ -23,14 +25,14 @@ public enum NotificationType {
     CHAT_ONLY("ChatOnly"),
     CHAT_ONLY("ChatOnly"),
     PARTY_MESSAGE("PartyMessage");
     PARTY_MESSAGE("PartyMessage");
 
 
-    final String niceName;
+    private final String niceName;
 
 
-    NotificationType(String niceName)
-    {
+    NotificationType(@NotNull String niceName) {
         this.niceName = niceName;
         this.niceName = niceName;
     }
     }
 
 
     @Override
     @Override
-    public String toString() {
+    public @NotNull String toString() {
         return niceName;
         return niceName;
-    }}
+    }
+}

+ 1 - 1
src/main/java/com/gmail/nossr50/datatypes/json/McMMOUrl.java

@@ -5,7 +5,7 @@ public class McMMOUrl {
     public static final String urlDiscord   = "https://discord.gg/bJ7pFS9";
     public static final String urlDiscord   = "https://discord.gg/bJ7pFS9";
     public static final String urlPatreon   = "https://www.patreon.com/nossr50";
     public static final String urlPatreon   = "https://www.patreon.com/nossr50";
     public static final String urlWiki      = "https://www.mcmmo.org/wiki/";
     public static final String urlWiki      = "https://www.mcmmo.org/wiki/";
-    public static final String urlSpigot    = "http://spigot.mcmmo.org";
+    public static final String urlSpigot    = "https://spigot.mcmmo.org";
     public static final String urlTranslate = "https://translate.mcmmo.org/";
     public static final String urlTranslate = "https://translate.mcmmo.org/";
 
 
     public static String getUrl(McMMOWebLinks webLinks)
     public static String getUrl(McMMOWebLinks webLinks)

+ 25 - 0
src/main/java/com/gmail/nossr50/datatypes/meta/RuptureTaskMeta.java

@@ -0,0 +1,25 @@
+package com.gmail.nossr50.datatypes.meta;
+
+import com.gmail.nossr50.runnables.skills.RuptureTask;
+import org.bukkit.metadata.FixedMetadataValue;
+import org.bukkit.plugin.Plugin;
+import org.jetbrains.annotations.NotNull;
+
+public class RuptureTaskMeta extends FixedMetadataValue {
+
+    private final @NotNull RuptureTask ruptureTask;
+    /**
+     * Initializes a FixedMetadataValue with an Object
+     *
+     * @param owningPlugin the {@link Plugin} that created this metadata value
+     * @param ruptureTask        the value assigned to this metadata value
+     */
+    public RuptureTaskMeta(@NotNull Plugin owningPlugin, @NotNull RuptureTask ruptureTask) {
+        super(owningPlugin, ruptureTask);
+        this.ruptureTask = ruptureTask;
+    }
+
+    public @NotNull RuptureTask getRuptureTimerTask() {
+        return ruptureTask;
+    }
+}

+ 9 - 12
src/main/java/com/gmail/nossr50/datatypes/party/Party.java

@@ -1,7 +1,6 @@
 package com.gmail.nossr50.datatypes.party;
 package com.gmail.nossr50.datatypes.party;
 
 
 import com.gmail.nossr50.chat.SamePartyPredicate;
 import com.gmail.nossr50.chat.SamePartyPredicate;
-import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.config.experience.ExperienceConfig;
 import com.gmail.nossr50.config.experience.ExperienceConfig;
 import com.gmail.nossr50.datatypes.experience.FormulaType;
 import com.gmail.nossr50.datatypes.experience.FormulaType;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
@@ -204,7 +203,7 @@ public class Party {
 
 
     public int getXpToLevel() {
     public int getXpToLevel() {
         FormulaType formulaType = ExperienceConfig.getInstance().getFormulaType();
         FormulaType formulaType = ExperienceConfig.getInstance().getFormulaType();
-        return (mcMMO.getFormulaManager().getXPtoNextLevel(level, formulaType)) * (getOnlineMembers().size() + Config.getInstance().getPartyXpCurveMultiplier());
+        return (mcMMO.getFormulaManager().getXPtoNextLevel(level, formulaType)) * (getOnlineMembers().size() + mcMMO.p.getGeneralConfig().getPartyXpCurveMultiplier());
     }
     }
 
 
     public String getXpToLevelPercentage() {
     public String getXpToLevelPercentage() {
@@ -243,24 +242,24 @@ public class Party {
             return;
             return;
         }
         }
 
 
-        if (!Config.getInstance().getPartyInformAllMembers()) {
+        if (!mcMMO.p.getGeneralConfig().getPartyInformAllMembers()) {
             Player leader = mcMMO.p.getServer().getPlayer(this.leader.getUniqueId());
             Player leader = mcMMO.p.getServer().getPlayer(this.leader.getUniqueId());
 
 
             if (leader != null) {
             if (leader != null) {
                 leader.sendMessage(LocaleLoader.getString("Party.LevelUp", levelsGained, getLevel()));
                 leader.sendMessage(LocaleLoader.getString("Party.LevelUp", levelsGained, getLevel()));
 
 
-                if (Config.getInstance().getLevelUpSoundsEnabled()) {
+                if (mcMMO.p.getGeneralConfig().getLevelUpSoundsEnabled()) {
                     SoundManager.sendSound(leader, leader.getLocation(), SoundType.LEVEL_UP);
                     SoundManager.sendSound(leader, leader.getLocation(), SoundType.LEVEL_UP);
                 }
                 }
             }
             }
-            return;
+        } else {
+            PartyManager.informPartyMembersLevelUp(this, levelsGained, getLevel());
         }
         }
 
 
-        PartyManager.informPartyMembersLevelUp(this, levelsGained, getLevel());
     }
     }
 
 
     public boolean hasReachedLevelCap() {
     public boolean hasReachedLevelCap() {
-        return Config.getInstance().getPartyLevelCap() < getLevel() + 1;
+        return mcMMO.p.getGeneralConfig().getPartyLevelCap() < getLevel() + 1;
     }
     }
 
 
     public void setXpShareMode(ShareMode xpShareMode) {
     public void setXpShareMode(ShareMode xpShareMode) {
@@ -328,7 +327,7 @@ public class Party {
     }
     }
 
 
     public boolean hasMember(String memberName) {
     public boolean hasMember(String memberName) {
-        return this.getMembers().containsValue(memberName);
+        return this.getMembers().values().stream().anyMatch(memberName::equalsIgnoreCase);
     }
     }
 
 
     public boolean hasMember(UUID uuid) {
     public boolean hasMember(UUID uuid) {
@@ -386,7 +385,7 @@ public class Party {
 
 
         if (party != null) {
         if (party != null) {
             Player player = mcMMOPlayer.getPlayer();
             Player player = mcMMOPlayer.getPlayer();
-            double range = Config.getInstance().getPartyShareRange();
+            double range = mcMMO.p.getGeneralConfig().getPartyShareRange();
 
 
             for (Player member : party.getOnlineMembers()) {
             for (Player member : party.getOnlineMembers()) {
                 if (!player.equals(member) && member.isValid() && Misc.isNear(player.getLocation(), member.getLocation(), range)) {
                 if (!player.equals(member) && member.isValid() && Misc.isNear(player.getLocation(), member.getLocation(), range)) {
@@ -404,12 +403,10 @@ public class Party {
             return false;
             return false;
         }
         }
 
 
-        if (!(obj instanceof Party)) {
+        if (!(obj instanceof Party other)) {
             return false;
             return false;
         }
         }
 
 
-        Party other = (Party) obj;
-
         if ((this.getName() == null) || (other.getName() == null)) {
         if ((this.getName() == null) || (other.getName() == null)) {
             return false;
             return false;
         }
         }

Some files were not shown because too many files changed in this diff