Compare commits

...

99 Commits

Author SHA1 Message Date
d4feda78ae Changes version back to snapshot
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-08-07 15:08:15 +02:00
81e65810e1 Bumps version for release
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-08-07 14:09:42 +02:00
0dab832bfd Adds a note about sticks being an NPC selector
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-08-07 13:24:06 +02:00
1510960089 Fixes missing cost message for extended salvage
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-08-03 17:05:28 +02:00
cdb0f267a3 Renames an event method
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-08-03 16:44:15 +02:00
81c15b6600 Adds information about the appropriate crafting station in ActionStartEvent
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-08-03 16:32:44 +02:00
5efba6687c Adds a separate message for clicking an NPC with an empty hand
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-08-03 13:09:09 +02:00
d0df4800f0 Changes back to SNAPSHOT
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-07-29 19:35:10 +02:00
d73fcfd778 Bumps version for release
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-07-29 19:23:14 +02:00
0993fbe15f Improves console logging
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-07-29 18:32:08 +02:00
f3372a13a5 Rewrites a lot of scrapper code to fix actual and potential bugs
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-07-29 17:53:07 +02:00
cf702c0e48 Cleans up salvage code a bit 2024-07-29 13:23:59 +02:00
afb608b609 Makes ActionStartEvent give duration in ticks instead of end time
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-07-29 01:40:08 +02:00
523e4cb47a Changes pitch for working sound
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-07-28 17:20:51 +02:00
384893b01a Randomizes the working sound somewhat 2024-07-28 17:15:25 +02:00
904adf46f0 Adds some events that trigger when NPC actions start or end, and when sounds are played 2024-07-28 17:12:26 +02:00
81dda85405 Adds sound effects to Blacksmiths and Scrappers 2024-07-28 12:48:45 +02:00
92c1b96fba Adds missing comments for BLACKSMITH filters
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-07 17:01:20 +02:00
62e58ba291 Adds missing scrapper notice
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-07 15:24:09 +02:00
21b860b5c8 Updates README header
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-07 14:57:24 +02:00
51d79c21c2 Changes version back to SNAPSHOT
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-07 14:50:34 +02:00
ce518ef338 Bumps server version for release
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-07 14:35:10 +02:00
4993c972e9 Updates plugin.yml
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-07 13:55:03 +02:00
75744ccdd0 Makes edit commands display the default value if no value is provided 2024-05-07 13:54:45 +02:00
cb477cafb5 Adds missing info about non-standard functionality
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-07 03:27:36 +02:00
33ef557771 Fixes some savage fail problems
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
Fixes item not being returned when salvage fails
Removes some redundancy when giving back items
Makes extended salvage return 50% of the salvage (no items if only one item would be returned)
Adds a better message when failing to salvage extended salvage items
Fixes a bug in the caching of smith presets
2024-05-07 01:45:48 +02:00
1d7e8a0732 Implements netherite salvaging #24
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-06 21:26:21 +02:00
e6047f3866 Implements armor trim salvage #24
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-06 14:48:48 +02:00
3ed3c99c15 Speeds up smith presets by using caching and sets
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-06 12:56:22 +02:00
3dd8467a58 Updates some incorrect README information
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-06 12:17:22 +02:00
d28d67f1bc Adds default configuration values README.md
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-06 00:11:35 +02:00
d405c0dcff Implements enchantment salvage toggle #23
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-05 23:45:59 +02:00
d7bac6d08f Fixes missing config values not loading properly until a reload
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-05 18:38:46 +02:00
21d55563b7 Changes ignored salvage into trash salvage according to #22 2024-05-05 18:02:12 +02:00
c71d664a79 Fixes a NullPointerException and the default value of successSalvagedMessage
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-05 15:51:46 +02:00
2612f4f7d8 Improves messages when trying to salvage an item
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
Implements unimplemented too damaged for salvage message
Adds two new messages explaining whether an item will produce full or partial salvage
2024-05-05 15:37:38 +02:00
e956c7dda7 Fixes some problems with getting scrapper data
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-04 22:12:46 +02:00
757fcdf139 Improves configuration migration, and fixes some issues
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
Fixes incorrect dropItems instead of dropItem key in config.yml
Adds some missing dropper messages to config.yml
Adds proper migration keys for all known configuration options
Adds a modified version of Stargate's StargateYamlConfiguration to allow retaining comments during configuration migration.
Fixes the annoying "[]" is not a valid repairable item
Removes the use of data keys for the global configuration
2024-05-04 17:40:21 +02:00
8b8890c408 Improves tab-completions for salvage-able and reforge-able items settings 2024-05-04 14:25:08 +02:00
f9463f58d1 Fixes some bugs
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
Fixes some config commands not being registered
Bumps KnarLib version to include bug-fix
Fixes and improves tab-completion for enchantment block-list
2024-05-04 14:11:43 +02:00
7e5525bd00 Updates Spigot, and fixes depreciated code
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-04 02:46:46 +02:00
44f8bb36b0 Bumps version
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-05-04 01:03:20 +02:00
4012e532da Finishes the scrapper implementation
Some checks failed
EpicKnarvik97/Blacksmith/pipeline/head There was a failure building this commit
2024-05-04 01:01:56 +02:00
455db78988 Updates some KnarLib code
Some checks failed
EpicKnarvik97/Blacksmith/pipeline/head There was a failure building this commit
2024-01-25 16:57:10 +01:00
c532160fb5 Gets some smith preset items from tags instead of name matching
Some checks failed
EpicKnarvik97/Blacksmith/pipeline/head There was a failure building this commit
2024-01-13 00:55:41 +01:00
dfae68050e Implements some necessary code for the scrapper
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
Implements ignored salvage
Implements salvage-able items
Adds some TODOs
Implements salvage fail chance
Moves several methods to ItemHelper
Adds option for allowing extended salvage
2023-12-30 13:56:33 +01:00
6872dadca8 Changes some configuration keys
Some checks failed
EpicKnarvik97/Blacksmith/pipeline/head There was a failure building this commit
2023-11-20 13:20:11 +01:00
11d8c74a26 Adds comments to every option in strings.yml 2023-11-16 22:51:20 +01:00
4282cd8a2f Displays descriptions for each setting when displaying the value 2023-11-16 19:44:35 +01:00
ac6fc430ff Adds code for migrating old configuration values 2023-11-16 17:34:37 +01:00
72d33ed7a2 Implements the scrapper edit command 2023-11-16 13:06:24 +01:00
4f885135e3 Implements the scrapper global edit command
Changes setting quite a bit to avoid code duplication
2023-11-16 01:17:27 +01:00
f3f3f66c38 Writes a lot of code necessary for the scrapper
Adds the classes necessary for the new scrapper
Partly implements some scrapper functionality
Restructures some classes to reduce code duplication
Moves some classes to make some classes easier to find
Adds a bunch of TODOs where things have an unfinished implementation
2023-11-14 16:04:48 +01:00
1938a690c6 Merge branch 'dev' into scrapper 2023-11-12 19:41:06 +01:00
e39e70b690 Merge branch 'master' into dev
# Conflicts:
#	src/main/java/net/knarcraft/blacksmith/BlacksmithPlugin.java
2023-11-12 19:40:51 +01:00
f0b9a82d7e Bumps version
Some checks failed
EpicKnarvik97/Blacksmith/pipeline/head There was a failure building this commit
2023-11-12 19:08:27 +01:00
92585c4dee Adds a missing null check 2023-11-12 19:08:06 +01:00
3e3a35d02a Starts on the Scrapper implementation 2023-11-12 19:02:11 +01:00
72ea5600fe Fixes some warnings, and updates dependencies
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-09-23 19:15:11 +02:00
191f548f5a Fixes order of arguments when reloading the language
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-07-08 03:29:18 +02:00
be0b16e80a Fixes the order of selected language VS fallback language
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-04-12 02:36:35 +02:00
cb70874093 Adds an option for toggling whether reforging removing enchantments
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-30 12:34:34 +01:00
faff982585 Fixes a collision between the test enchantments
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-28 09:20:27 +01:00
c84c2cf30e Adds tests for TypeValidationHelper
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-28 09:13:02 +01:00
a79f2e273a Adds missing tests for ItemHelper
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-17 05:28:46 +01:00
347b69b2a8 Removes disableMaterialLimitation
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
Removes the disableMaterialLimitation option
Replaces EnchantmentTarget.BREAKABLE.includes(item) with getMaxDurability(item) > 0 as it seems more generic
2023-01-16 19:45:41 +01:00
e5cb3b4a30 A lot of tests, and some improvements
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
Adds a lot more tests for some utility classes
Adds a new option to disable the hard-coded limitation "EnchantmentTarget.BREAKABLE" which can be useful for servers with custom items or similar.
Makes enchantment name matching case-insensitive.
Prevents an exception is enchantment name contains invalid characters.
Makes invalid objects supplied to asStringList return null instead of throwing an exception.
Uses isBlank instead of trim.isEmpty in the isEmpty method
Re-uses the random generator if calculating salvage several times
2023-01-16 17:42:54 +01:00
7d468115e0 Makes the npc talking format configurable #19 2023-01-16 03:02:13 +01:00
bbb93bb0eb Fixes a bug in multi-item salvage
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-14 17:46:29 +01:00
7cc2aef9d4 Adds some tests for the salvage helper
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-14 15:10:40 +01:00
8e5d4c7a61 Adds a helper class for calculating random item salvage #18
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-13 20:34:34 +01:00
89cebb85c3 Changes version to SNAPSHOT
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-11 14:56:30 +01:00
0d077ccdbb Bumps version to 1.0.4 for release
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-11 14:38:35 +01:00
1bc03d7670 Improves some README descriptions 2023-01-11 14:29:59 +01:00
f192a5a2b5 Fixes a bug in the preset prefix logic
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-11 02:46:30 +01:00
bd00a59d08 Makes it possible to edit reforgeAbleItems globally #17 2023-01-11 01:54:52 +01:00
8423eabc57 Fixes handling of preset replacement 2023-01-11 01:32:48 +01:00
488d4c7589 Makes some settings inherit properly #16 2023-01-11 00:51:05 +01:00
753c7c6275 Adds optional ability to reforge anvils #15
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-09 23:47:17 +01:00
a856aa03e0 Implements material wildcards for costs #14
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-09 16:53:52 +01:00
913cc5736e Prevents filters from matching non-reforge-able materials 2023-01-09 15:25:05 +01:00
a5ae3cb295 Fixes a bug caused by "-" being consumed
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-09 15:13:55 +01:00
7d940ee334 Updates dependencies
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-09 05:06:33 +01:00
ea54492ccf Adds negation for material names and presets #13 2023-01-09 05:03:28 +01:00
30b8507b9f Improves the tab-completion values for reforgeAbleItems
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2023-01-06 14:52:30 +01:00
c5ffa2b0c4 Improves setup information 2023-01-06 00:03:33 +01:00
a9cfea2d32 Fixes a repo name
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2022-11-26 15:37:14 +01:00
1bab972c13 Improves Jenkinsfile 2022-11-26 15:14:53 +01:00
d1c549fd20 Adds auto deploy
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2022-11-26 14:51:52 +01:00
c9ff3af6ac Adds distribution management and Jenkinsfile
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2022-11-26 04:13:41 +01:00
5089a721a0 Fixes some minor issues
Fixes some repositories using http instead of https
Fixes formatting for tables
2022-11-14 02:44:18 +01:00
a036c39dc3 Updates code to account for KnarLib changes 2022-11-07 22:22:23 +01:00
a501a3cbb4 Changes things for the non-static Translator 2022-11-07 15:20:30 +01:00
7875e9a705 Adds missing time unit strings 2022-11-07 01:59:49 +01:00
ac5f032ce9 Uses a placeholder for the plugin version 2022-11-07 00:22:03 +01:00
7784ee46a5 Adds an alert if an update is available 2022-11-07 00:18:13 +01:00
3c805ee284 Uses KnarLib for common tasks 2022-11-07 00:07:32 +01:00
3c4394d6fa Bumps version to 1.0.3 2022-11-05 04:40:09 +01:00
d0f4ff11b7 Adds missing null checks before asStringList 2022-11-05 04:39:18 +01:00
82 changed files with 7769 additions and 2579 deletions

33
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,33 @@
pipeline {
agent any
tools {
jdk 'JDK17'
}
stages {
stage('Build') {
steps {
echo 'Building...'
sh 'mvn clean & mvn validate & mvn compile'
}
}
stage('Test') {
steps {
echo 'Testing...'
sh 'mvn test'
}
}
stage('Verify') {
steps {
echo 'Verifying...'
sh 'mvn verify -Dmaven.test.skip=true'
}
}
stage('Deploy') {
steps {
echo 'Deploying...'
sh 'mvn deploy -Dmaven.install.skip=true -Dmaven.test.skip=true'
archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true
}
}
}
}

313
README.md
View File

@ -1,7 +1,8 @@
# Blacksmith
The blacksmith plugin adds a new blacksmith trait to Citizens NPCs. NPCs are able to repair a player's held item for a
fee. Costs are highly customizable.
The blacksmith plugin adds a blacksmith trait and a scrapper to Citizens NPCs. Blacksmith NPCs are able to repair a
player's held item for a fee. Costs are highly customizable. Scrapper NPCs can break down items into their crafting
recipe.
### Important changes from the original fork
@ -10,12 +11,12 @@ fee. Costs are highly customizable.
- By default, natural cost is used. The original fork made it cheaper the more damaged an item is, but natural cost
makes the cost increase the more damaged the item is.
- EnchantmentTarget is used instead of a hard-coded list of repairable items
- All settings (except default reforge-able-items), both global and for each blacksmith, can be changed using commands,
- All settings, both global and for each blacksmith / scrapper, can be changed using commands,
and support tab-completion.
- This plugin is not directly compatible with the original. If you are using the old one, you will need to set it up
again!
### Dependencies
### Required dependencies
- Citizens2
- Vault
@ -23,42 +24,86 @@ fee. Costs are highly customizable.
## Basic usage
To create a new blacksmith, simply add the blacksmith trait to an NPC. Right-clicking the NPC will tell you if the
currently held item is repairable by the blacksmith. If it is, the blacksmith should give a price quote. Right-clicking
again starts the repair. The item should be given back or dropped after a random delay according to the set limits.
To create a new blacksmith, simply add the blacksmith trait to an NPC by selecting it with `/npc select`, and then using
`/trait add Blacksmith` (See Citizens' documentation for more details).
While costs are always set globally (no blacksmith can do the same job for cheaper), the blacksmith's messages,
cool-down between each reforge, whether the item is dropped or given, the chance of failing or adding an enchantment,
the maximum number of added enchantments, max and min delays, the length of the cool-down and which items the blacksmith
is able to reforge can be changed individually.
To create a new scrapper, simply add the scrapper trait to an NPC by selecting it with `/npc select`, and then using
`/trait add Scrapper` (See Citizens' documentation for more details).
Right-clicking the NPC will tell you if the currently held item is repairable by the blacksmith / salvageable by the
scrapper. If it is, the blacksmith / scrapper should give a price quote. Right-clicking again starts the repair /
salvage. The item should be given back or dropped after a random delay according to the set limits.
While costs are always set globally (no blacksmith or scrapper can do the same job for cheaper), the blacksmith /
scrapper's messages, cool-down between each reforge / salvage, whether the item is dropped or given, the chance of
failing or adding an enchantment, the maximum number of added enchantments, max and min delays, the length of the
cool-down and which items the blacksmith is able to reforge / salvage can be changed individually.
Also note: As a change from the original plugin, unless a value for an NPC has been explicitly set for that NPC, it will
mirror the values set in config.yml. This also means that configuration values aren't populated automatically in
citizen's NPC save file. While you can manually set the values using the same keys as in config.yml, you should use the
/blacksmith command when possible.
`/blacksmith` or `/scrapper` command when possible.
### Non-standard functionality
There is some non-standard optional functionality for blacksmiths and scrappers that goes beyond repairing things with
durability. They can all be enabled or disabled using configuration values.
- Blacksmiths can repair anvils if the option `reforgeAnvils` is enabled
- Scrappers can remove armor trim from items if `salvageArmorTrims` is enabled. This will always be done before
salvaging the item itself. If not enabled, the item is considered un-salvageable.
- Scrappers can remove netherite from items if `salvageNetherite` is enabled. This will be done if no armor trim exists,
before normal salvage. If not enabled, the item is considered un-salvageable.
- Scrappers can salvage enchanted items, and return some XP based on the enchantment set if `salvageEnchanted` is
enabled. If not enabled, the item is considered un-salvageable.
- Scrappers can salvage just about anything with a crafting table recipe if `extendedSalvageEnabled` is enabled. As long
as you have the amount of items produced by a crafting recipe, you can undo the recipe. Four planks can be salvaged
into a log as an example.
### Special behavior
In addition to just being able to repair items, blacksmiths have some random features which can be cool or annoying:
In addition to just being able to repair items, blacksmiths / scrappers have some random features which can be cool or
annoying:
- There is a chance that blacksmiths fail to repair an item, leaving it at about the same durability as before. Use
- There is a chance that blacksmiths / scrappers fail to repair an item, leaving it at about the same durability as
before (scrappers with `extendedSalvageEnabled` turned on might cause items in a stack to be lost). Use
failReforgeChance to control the chance. Set it to 0 to remove the feature.
- There is a chance a blacksmith may add an enchantment to a reforged item. You can control the probability using
extraEnchantmentChance, and set the maximum number of enchantments using maxEnchantments
extraEnchantmentChance, and set the maximum number of enchantments using maxEnchantments. EnchantmentBlockList can be
used to block any enchantments you don't want to randomly grant.
### Scrapper basics
A scrapper will produce salvage for a damage-able item by calculating the amount of items returned based on items in the
recipe, and the percentage of durability left on the item. To avoid returning relatively worthless items instead of
valuable items, `trashSalvage` can be configured. Trash salvage will only be returned if all non-trash items are
returned as well. A scrapper will by default only salvage damage-able items (same as blacksmiths), but enabling
`extendedSalvageEnabled` for a scrapper will allow it to salvage any crafting table recipe. Note that to salvage for
example planks into wood, four wood will be taken.
When an item is salvaged, EXP will be returned based on enchantments on the item.
Note that sticks are valid items to be handled by Scrappers if extended salvage is enabled. The item is also the default
item for selecting NPCs in Citizens. If you find the NPC selection message annoying, and don't normally use sticks for
selection, you can change the item in Citizens' config and replace it with STRUCTURE_VOID or some other unobtainable
item.
## Commands
| Command | Arguments | Description |
| --- | --- | --- |
|-------------------|-------------------------------|----------------------------------------------------------------------------------------------|
| /blacksmith | \<option> \[new-value] | Changes a configuration option for the selected blacksmith (use Citizens' /npc select first) |
| /blacksmithconfig | \<reload/option> \[new-value] | Changes a default/global configuration value |
| /blacksmithConfig | \<reload/option> \[new-value] | Changes a default/global blacksmith configuration value |
| /scrapper | \<option> \[new-value] | Changes a configuration option for the selected scrapper (use Citizens' /npc select first) |
| /scrapperConfig | \<reload/option> \[new-value] | Changes a default/global scrapper configuration value |
| /preset | \<preset>\[:filter] | Displays all materials included in the given preset, after applying the filter if set |
For /blacksmith and /blacksmithconfig, if a new value isn't specified, the current value is displayed instead.
For /blacksmith, /blacksmithConfig, /scrapper and /scrapperConfig, if a new value isn't specified, a description of the
configuration option, and the current value, is displayed instead.
For /blacksmith, using -1 or null as the value will clear a custom value, making the NPC use the default value set in
the config instead. Additionally, every value overridden for an NPC will display a marker (default: \[NPC]) when
displaying the current value.
For /blacksmith or /scrapper, using -1 or null as the value will clear a custom value, making the NPC use the default
value set in the config instead. Additionally, every value overridden for an NPC will display a marker (default: \[NPC])
when displaying the current value.
Note: basePrice, pricePerDurabilityPoint and enchantmentCost can be set like: `/blacksmithconfig option 4` or
like `/blacksmithconfig option material/enchantment 4` depending on whether you are setting the default or an override
@ -66,108 +111,186 @@ for a specific material/enchantment. For /blacksmithconfig <basePrice/pricePerDu
material/enchantment>, using -1 or null as the value will clear the cost for the specified material/enchantment, using
the default one instead.
basePrice and pricePerDurabilityPoint support using a wildcard "*" to set the price for multiple groups of materials at
once. `/blacksmithconfig basePrice golden* 43` would set the price of any material starting with "GOLDEN" to 43. You can
also set `netherite*: 34` directly in the config file to set the price of all netherite materials to 34.
### Presets and filters:
Note: All of these can be used when specifying reforge-able items, such as
**Presets are a nice way to make specialized blacksmiths / scrappers by specifying categories of materials, instead of
manually listing every material manually. They can only be used for the `reforgeAbleItems` and `salvageAbleItems`
options.**
Note: All of these can be used when specifying reforge-able and salvage-able items, such as
"preset:weapon-smith:bow,preset:armor_smith:gold,PRESET:TOOL_SMITH,shield"
The format of reforge-able items requires each preset/material to be separated by a comma. If specifying a preset
instead of a material, start by typing "preset:". Then you can type WEAPON_SMITH, ARMOR_SMITH or TOOL_SMITH. If you
want, you can add another colon and one of the filters supported by the preset. For example: "preset:WEAPON_SMITH:BOW"
to make the blacksmith only repair bows. You can use the same preset several times with different filters. For
example: "preset:ARMOR_SMITH:DIAMOND,preset:ARMOR_SMITH:NETHERITE" would allow the blacksmith to repair all diamond and
netherite armor.
instead of a material, start by typing "preset:". Then you can type BLACKSMITH, WEAPON_SMITH, ARMOR_SMITH or TOOL_SMITH.
If you want, you can add another colon and one of the filters supported by the preset.
For example: "preset:WEAPON_SMITH:BOW" to make the blacksmith only repair bows and crossbows. You can use the same
preset several times with different filters. For example: "preset:ARMOR_SMITH:DIAMOND,preset:ARMOR_SMITH:NETHERITE"
would allow the blacksmith to repair all diamond and netherite armor.
Presets and filters can also be negated by applying a "-" character in front of the material name. For example,
"-SHIELD" would make shields unrepairable, even if included in a listed preset. "preset:WEAPON_SMITH,-SHIELD"
would allow the blacksmith to repair any weapon included in the WEAPON_SMITH preset except shields.
"preset:BLACKSMITH,-ELYTRA"would allow the blacksmith to repair any repairable item, except elytra. You can also negate
entire presets or filters. For example: "preset:blacksmith,-preset:armor-smith:diamond" would allow a blacksmith to
repair all items, except diamond armor.
All currently supported presets, and available filters for each preset:
- WEAPON_SMITH:
- BOW
- SWORD
- RANGED
- ARMOR_SMITH:
- LEATHER
- IRON
- CHAINMAIL
- GOLD
- DIAMOND
- NETHERITE
- HELMET
- BOOTS
- LEGGINGS
- CHESTPLATE
- TOOL_SMITH
- WOOD
- STONE
- IRON
- GOLD
- DIAMOND
- NETHERITE
- PICKAXE
- AXE
- HOE
- SHOVEL
- MISC
- BLACKSMITH (WEAPON_SMITH + ARMOR_SMITH + TOOL_SMITH)
- GOLD (all gold armor, tools and weapons)
- IRON (all iron armor, tools and weapons)
- DIAMOND (all diamond armor, tools and weapons)
- NETHERITE (all netherite armor, tools and weapons)
- WEAPON_SMITH: (RANGED + SWORD + SHIELD)
- BOW (bows and crossbows)
- SWORD (swords)
- RANGED (bows, crossbows and tridents)
- ARMOR_SMITH: (HELMET + BOOTS + LEGGINGS + CHESTPLATE + ELYTRA)
- LEATHER (all pieces of leather armor)
- IRON (all pieces of iron armor)
- CHAINMAIL (all pieces of chainmail armor)
- GOLD (all pieces of gold armor)
- DIAMOND (all pieces of diamond armor)
- NETHERITE (all pieces of netherite armor)
- HELMET (all helmets)
- BOOTS (all boots)
- LEGGINGS (all leggings)
- CHESTPLATE (all chest-plates)
- TOOL_SMITH: (PICKAXE + AXE + HOE + SHOVEL + MISC)
- WOOD (all wood tools)
- STONE (all stone tools)
- IRON (all iron tools)
- GOLD (all gold tools)
- DIAMOND (all diamond tools)
- NETHERITE (all netherite tools)
- PICKAXE (all pickaxes)
- AXE (all axes)
- HOE (all hoes)
- SHOVEL (all shovels)
- MISC (FISHING_ROD + SHEARS + FLINT_AND_STEEL)
## Permissions
| Permission node | Description |
| --- | --- |
| blacksmith.admin | Allows overall blacksmith configuration |
| blacksmith.edit | Allows changing settings for the selected blacksmith NPC |
| blacksmith.use | Allows the player to repair items using blacksmiths |
|------------------|----------------------------------------------------------------------------------------|
| blacksmith.admin | Allows overall blacksmith and scrapper configuration. |
| blacksmith.edit | Allows changing settings for the selected blacksmith or scrapper NPC. |
| blacksmith.use | Allows the player to repair items using blacksmiths and salvage items using scrappers. |
## Configuration options
### Plugin Options
| Key | Value type | Description |
| --- | --- | --- |
|----------|------------|----------------------------------------------------------------------------------------------|
| language | string | The language used for this plugin. Only "en" is supported, unless you add a custom language. |
### Global-only options
### Blacksmith global-only options
| Key | Value type | Description |
| --- | --- | --- |
| basePrice | positive decimal number | The base price which has to be paid regardless of the durability remaining for an item. Setting this without specifying a material sets the basePrice for any item the basePrice has not been set for. |
| pricePerDurabilityPoint | positive decimal number | The price added for each durability point present/missing (depends on whether natural cost is set to true or false). Setting this without specifying a material sets the pricePerDurabilityPoint for any item the pricePerDurabilityPoint has not been set for. |
| enchantmentCost | positive decimal number | The added cost for each level of an enchantment present on the item. The cost can be set for specific enchantments. Not specifying an enchantment sets the value for all enchantments without a set value.
| useNaturalCost | true/false | If true, each missing durability will add to the cost (price = basePrice + missingDurability * pricePerDurabilityPoint + enchantmentCost). If false, durability will be used to calculate the cost instead of missingDurability (this was the behavior before natural cost was added). |
| showExactTime | true/false | If true, blacksmiths will display exact time remaining in minutes and seconds, instead of vague expressions |
| Key | Value type | Default | Description |
|---------------------------|-------------------------|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| basePrice | positive decimal number | default: 10.0 | The base price which has to be paid regardless of the durability remaining for an item. Setting this without specifying a material sets the basePrice for any item the basePrice has not been set for. You can use for example "netherite*: 10" to set the value for any material beginning with "netherite". |
| pricePerDurabilityPoint | positive decimal number | default: 0.005 | The price added for each durability point present/missing (depends on whether natural cost is set to true or false). Setting this without specifying a material sets the pricePerDurabilityPoint for any item the pricePerDurabilityPoint has not been set for. You can use for example "netherite*: 10" to set the value for any material beginning with "netherite". |
| enchantmentCost | positive decimal number | default: 5 | The added cost for each level of an enchantment present on the item. The cost can be set for specific enchantments. Not specifying an enchantment sets the value for all enchantments without a set value. |
| useNaturalCost | true/false | true | If true, each missing durability will add to the cost (price = basePrice + missingDurability * pricePerDurabilityPoint + enchantmentCost). If false, durability will be used to calculate the cost instead of missingDurability (this was the behavior before natural cost was added). |
| showExactTime | true/false | false | If true, blacksmiths will display exact time remaining in minutes and seconds, instead of vague expressions. |
| chippedAnvilReforgingCost | positive decimal number | 10.0 | The price for reforging a chipped anvil (slightly damaged). No other costs apply! |
| damagedAnvilReforgingCost | positive decimal number | 20.0 | The price for reforging a damaged anvil (very damaged). No other costs apply! |
### Per-npc (with default values set in config.yml)
### Blacksmith per-npc (with default values set in config.yml)
#### Configuration values
| Key | Value type | Description |
| --- | --- | --- |
| dropItem | true/false | Whether the blacksmith should drop the repaired item on the ground (instead of putting it into the player's inventory). |
| disableCoolDown | true/false | Whether to completely disable the cool-down between repairs. |
| disableDelay | true/false | Whether to completely disable the delay required to reforge an item. |
| failReforgeChance | 0-100 | The chance of the blacksmith failing to repair an item. |
| extraEnchantmentChance | 0-100 | The chance of the blacksmith adding an enchantment to an item. |
| maxEnchantments | 0-10 | The maximum number of different enchantments a blacksmith can add. |
| maxReforgeDelay | 0-3600 | The maximum number of seconds a player needs to wait for an item to be repaired. |
| minReforgeDelay | 0-3600 | The minimum number of seconds a player needs to wait for an item to be repaired. |
| reforgeCoolDown | 0-3600 | The cool-down, in seconds, a player has to wait between each time they use one specific blacksmith. |
| reforgeAbleItems | DIAMOND_LEGGINGS,GOLD-pickaxe,bow, etc. | Specifies which items this blacksmith is able to reforge. If set to "" or null, all normally repairable items can be repaired. If set to a list of items, only the items specified can be repaired. Some presets have been included for ease of use. Use a preset by specifying "preset:sword-smith" instead of a material such as "gold-pickaxe". |
| blacksmithTitle | text string | The title displayed as part of the message explaining that a blacksmith doesn't recognize a player's held item |
| enchantmentBlocklist | string list | A string list of all enchantments a blacksmith should not be allowed to add to items. |
| Key | Value type | Default | Description |
|--------------------------------|-----------------------------------------|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| dropItem | true/false | true | Whether the blacksmith should drop the repaired item on the ground (instead of putting it into the player's inventory). |
| failReforgeChance | 0-100 | 10 | The chance of the blacksmith failing to repair an item (further damaging it, or just repairing it a bit), and either removing or downgrading all enchantments (if failReforgeRemovesEnchantments is true). |
| failReforgeRemovesEnchantments | true/false | false | Whether a failed reforge should remove or downgrade all enchantments on the item. |
| extraEnchantmentChance | 0-100 | 5 | The chance of the blacksmith adding an enchantment to an item. |
| maxEnchantments | 0-10 | 3 | The maximum number of different enchantments a blacksmith can add. |
| maxReforgeWaitTimeSeconds | 0-3600 | 30 | The maximum number of seconds a player needs to wait for an item to be repaired. |
| minReforgeWaitTimeSeconds | 0-3600 | 5 | The minimum number of seconds a player needs to wait for an item to be repaired. |
| reforgeCoolDownSeconds | 0-3600 | 60 | The cool-down, in seconds, a player has to wait between each time they use one specific blacksmith. |
| reforgeAbleItems | DIAMOND_LEGGINGS,GOLD-pickaxe,bow, etc. | [ ] | Specifies which items this blacksmith is able to reforge. If set to "" or null, all normally repairable items can be repaired. If set to a list of items, only the items specified can be repaired. Some presets have been included for ease of use. Use a preset by specifying "preset:sword-smith" instead of a material such as "gold-pickaxe". |
| blacksmithTitle | text string | blacksmith | The title displayed as part of the message explaining that a blacksmith doesn't recognize a player's held item |
| enchantmentBlockList | string list | binding_curse,mending,vanishing_curse | A string list of all enchantments a blacksmith should not be allowed to add to items. |
| reforgeAnvils | true/false | false | Whether to allow the blacksmith to reforge anvils. If enabled, chipped and damaged anvils will be replaced with a normal anvil. |
#### Messages
| Message Key | Explanation |
| --- | --- |
| busyPlayerMessage | The message displayed when the blacksmith is serving another player |
| busyReforgeMessage | The message displayed when the blacksmith is busy reforging an item |
| coolDownUnexpiredMessage | The message displayed when the player has to wait for the cool-down to expire before using the blacksmith again |
| costMessage | The message displayed when telling a player about the cost of repairing an item |
| failReforgeMessage | The message displayed when a blacksmith fails to reforge an item |
| insufficientFundsMessage | The message displayed when a player is unable to pay for reforging an item |
| invalidItemMessage | The message displayed when a blacksmith is presented an item which it cannot repair |
| itemChangedMessage | The message displayed when a player changes their item after being shown the repair cost |
| startReforgeMessage | The message displayed when a blacksmith starts reforging an item |
| successMessage | The message displayed when a blacksmith successfully repairs an item |
| notDamagedMessage | The message displayed if a player tries to reforge an item with full durability |
| Message Key | Default | Explanation |
|--------------------------|-------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| busyPlayerMessage | &cI'm busy at the moment. Come back later! | The message displayed when the blacksmith is serving another player. |
| busyReforgeMessage | &cI'm working on it. Be patient! I'll finish {time}! | The message displayed when the blacksmith is busy reforging an item. |
| coolDownUnexpiredMessage | &cYou've already had your chance! Give me a break! I'll be ready {time}! | The message displayed when the player has to wait for the cool-down to expire before using the blacksmith again. |
| costMessage | &eIt will cost &a{cost}&e to reforge that &a{item}&e! Click again to reforge! | The message displayed when telling a player about the cost of repairing an item. |
| failReforgeMessage | &cWhoops! Didn't mean to do that! Maybe next time? | The message displayed when a blacksmith fails to reforge an item. |
| insufficientFundsMessage | &cYou don't have enough money to reforge that item! | The message displayed when a player is unable to pay for reforging an item. |
| invalidItemMessage | &cI'm sorry, but I'm a/an {title}, I don't know how to reforge that! | The message displayed when a blacksmith is presented an item which it cannot repair. |
| itemChangedMessage | &cThat's not the item you wanted to reforge before! | The message displayed when a player changes their item after being shown the repair cost. |
| startReforgeMessage | &eOk, let's see what I can do... | The message displayed when a blacksmith starts reforging an item. |
| successMessage | There you go! All better! | The message displayed when a blacksmith successfully repairs an item. |
| notDamagedMessage | &cThat item is not in need of repair | The message displayed if a player tries to reforge an item with full durability. |
| noItemMessage | Please present the item you want to reforge | The message displayed when a blacksmith is clicked with an empty hand |
### Scrapper global-only options
| Key | Value type | Default | Description |
|-----------------------|------------------------------------------------------------|---------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| salvagePrice | positive decimal number | 0 | The cost of using a scrapper to salvage an item. |
| armorTrimSalvagePrice | positive decimal number | 5 | The cost of using a scrapper to remove armor trim from an item. |
| netheriteSalvagePrice | positive decimal number | 15 | The cost of using a scrapper to remove netherite from an item. |
| showExactTime | true/false | false | If true, scrappers will display exact time remaining in minutes and seconds, instead of vague expressions. |
| giveExperience | true/false | true | If true, each enchantment level on the salvaged item will give one EXP level as salvage. |
| trashSalvage | TARGET_MATERIAL:IGNORED_MATERIAL\[,\*_TARGET2:\*_IGNORED2] | `"*_SHOVEL;*_PICKAXE;*_AXE;*_HOE;*_SWORD;SHIELD;*_BOW:STICK"` | The items that should be deferred when calculating partial salvage. Because receiving just the sticks when salvaging a diamond pickaxe is kind of sad, this allows specifying for example: `*_SHOVEL;*_PICKAXE;*_AXE;*_HOE;*_SWORD;SHIELD;*_BOW:STICK` (the default) for deferring sticks in salvage for shovels, pickaxes, axes, hoes and swords. A `:` character splits selected items and the trash salvage. Different item specifications are split by a `;` character. Use `,` to split separate trash salvages when using commands, like: `SHIELD:STICK,BOW_STRING`. |
### Scrapper per-npc (with default values set in config.yml)
#### Configuration values
| Key | Value type | Default | Description |
|---------------------------|-----------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| dropItem | true/false | true | Whether the scrapper should drop the repaired item on the ground (instead of putting it into the player's inventory). |
| failSalvageChance | 0-100 | 0 | The chance of the scrapper failing to salvage an item, either further damaging the item, partly repairing the item or causing some items to disappear. |
| salvageAbleItems | DIAMOND_LEGGINGS,GOLD-pickaxe,bow, etc. | [] | Specifies which items this scrapper is able to salvage. If set to "" or null, all normally repairable items can be salvaged. If set to a list of items, only the items specified can be salvaged. Some presets have been included for ease of use. Use a preset by specifying "preset:sword-smith" instead of a material such as "gold-pickaxe". |
| maxSalvageWaitTimeSeconds | 0-3600 | 5 | The maximum number of seconds a player needs to wait for an item to be salvaged. |
| minSalvageWaitTimeSeconds | 0-3600 | 30 | The minimum number of seconds a player needs to wait for an item to be salvaged. |
| salvageCoolDownSeconds | 0-3600 | 60 | The cool-down, in seconds, a player has to wait between each time they use one specific scrapper. |
| scrapperTitle | text string | scrapper | The title displayed as part of the message explaining that a scrapper doesn't recognize a player's held item. |
| extendedSalvageEnabled | true/false | false | Whether to enable the extended salvage behavior for this scrapper. As long as it is allowed by salvageAbleItems and it can be crafted in a crafting table, it can be salvaged. This includes things like four planks salvaged into wood. |
| salvageEnchanted | true/false | false | Whether the scrapper is able to salvage enchanted items. This is disabled by default as it's very easy to accidentally salvage heavily enchanted items if one is not careful. |
| salvageArmorTrims | true/false | true | Whether the scrapper is able to salvage armor trims from items. |
| salvageNetherite | true/false | true | Whether the scrapper is able to salvage netherite items into diamond items. |
#### Messages
| Message Key | Default | Explanation |
|---------------------------------|-----------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| busyPlayerMessage | &cI'm busy at the moment. Come back later! | The message displayed when the scrapper is serving another player. |
| busySalvageMessage | &cI'm working on it. Be patient! I'll finish {time}! | The message displayed when the scrapper is busy salvaging an item. |
| coolDownUnexpiredMessage | &cYou've already had your chance! Give me a break! I'll be ready {time}! | The message displayed when the player has to wait for the cool-down to expire before using the scrapper again. |
| invalidItemMessage | &cI'm sorry, but I'm a/an {title}, I don't know how to salvage that! | The message displayed when a scrapper is presented an item which it cannot salvage. |
| tooDamagedForSalvageMessage | &cThat item is too damaged to be salvaged into anything useful | The message displayed when a scrapper is presented with an item too damaged to produce salvage. |
| successSalvagedMessage | There you go! | The message displayed when a scrapper successfully repairs an item. |
| failSalvageMessage | &cWhoops! The item broke! Maybe next time? | The message displayed when a scrapper fails to salvage an item. |
| failExtendedSalvageMessage | &cWhoops! I was unable to extract much, if any, salvage! Maybe next time? | The message displayed when a scrapper fails to salvage an item without durability (only relevant is extended salvage is enabled). |
| itemChangedMessage | &cThat's not the item you wanted to salvage before! | The message displayed when a player changes their item after being shown the salvage cost. |
| startSalvageMessage | &eOk, let's see what I can do... | The message displayed when a scrapper starts salvaging an item. |
| insufficientFundsMessage | &cYou don't have enough money to salvage an item! | The message displayed when a player is unable to pay for salvaging an item. |
| costMessage | &eIt will cost &a{cost}&e to salvage that &a{item}&e! {yield} &eClick again to salvage! | The message displayed when telling a player about the cost of salvaging an item. |
| costMessageArmorTrim | &eIt will cost &a{cost}&e to salvage that &a{item}&e's armor trim! | The message displayed when telling a player about the cost of salvaging an item's armor trim. |
| costMessageNetherite | &eIt will cost &a{cost}&e to salvage that &a{item}&e into diamond! | The message displayed when telling a player about the cost of salvaging a netherite item into a diamond item. |
| fullSalvageMessage | &aI should be able to extract all components from that pristine item.&r | The message displayed as part of costMessage's yield placeholder if all components will be returned. |
| partialSalvageMessage | &cI cannot extract all components from that damaged item.&r | The message displayed as part of costMessage's yield placeholder if only some components will be returned. |
| cannotSalvageEnchantedMessage | &cI'm sorry, but I'm unable to salvage enchanted items! | The message displayed when telling a player that the scrapper cannot salvage enchanted items. |
| cannotSalvageArmorTrimMessage | &cI'm sorry, but I'm unable to salvage armor trims! | The message displayed when telling a player that the scrapper cannot salvage items with armor trim. |
| armorTrimSalvageNotFoundMessage | &cI'm sorry, but I don't know how to salvage that armor trim! | The message displayed when telling a player that the scrapper cannot find the correct items to return as armor trim salvage. This will happen if more armor trim materials are added, or the Spigot API is changed. |
| cannotSalvageNetheriteMessage | &cI'm sorry, but I'm unable to salvage netherite items! | The message displayed when telling a player that the scrapper cannot salvage netherite items. |
| noItemMessage | Please present the item you want to salvage | The message displayed when a scrapper is clicked with an empty hand |
## Language customization

111
pom.xml
View File

@ -6,12 +6,13 @@
<groupId>net.knarcraft</groupId>
<artifactId>blacksmith</artifactId>
<version>1.0.2-SNAPSHOT</version>
<version>1.1.3-SNAPSHOT</version>
<name>Blacksmith</name>
<description>Blacksmith Character for the CitizensAPI</description>
<description>Blacksmith NPC for the Citizens API</description>
<!-- Properties -->
<properties>
<java.version>16</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<build.number>Unknown</build.number>
</properties>
@ -24,20 +25,34 @@
</repository>
<repository>
<id>citizens-repo</id>
<url>http://repo.citizensnpcs.co/</url>
<url>https://repo.citizensnpcs.co/</url>
</repository>
<repository>
<id>vault-repo</id>
<url>http://nexus.hc.to/content/repositories/pub_releases</url>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<repository>
<id>knarcraft-repo</id>
<url>https://git.knarcraft.net/api/packages/EpicKnarvik97/maven</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>knarcraft-repo</id>
<url>https://git.knarcraft.net/api/packages/EpicKnarvik97/maven</url>
</repository>
<snapshotRepository>
<id>knarcraft-repo</id>
<url>https://git.knarcraft.net/api/packages/EpicKnarvik97/maven</url>
</snapshotRepository>
</distributionManagement>
<!-- Dependencies -->
<dependencies>
<dependency>
<groupId>net.citizensnpcs</groupId>
<artifactId>citizens-main</artifactId>
<version>2.0.30-SNAPSHOT</version>
<version>2.0.33-SNAPSHOT</version>
<type>jar</type>
<scope>provided</scope>
<exclusions>
@ -50,46 +65,94 @@
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.19.2-R0.1-SNAPSHOT</version>
<version>1.20.6-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.milkbowl.vault</groupId>
<artifactId>Vault</artifactId>
<version>1.7.3</version>
<groupId>com.github.MilkBowl</groupId>
<artifactId>VaultAPI</artifactId>
<version>1.7.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>23.0.0</version>
<version>24.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.knarcraft</groupId>
<artifactId>knarlib</artifactId>
<version>1.2.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- Build information -->
<build>
<defaultGoal>clean package install</defaultGoal>
<resources>
<resource>
<filtering>true</filtering>
<directory>${basedir}/src/main/resources</directory>
<includes>
<include>*.yml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<version>3.8.1</version>
<configuration>
<source>16</source>
<target>16</target>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<relocations>
<relocation>
<pattern>net.knarcraft.knarlib</pattern>
<shadedPattern>net.knarcraft.blacksmith.lib.knarlib</shadedPattern>
</relocation>
<relocation>
<pattern>org.jetbrains.annotations</pattern>
<shadedPattern>net.knarcraft.blacksmith.lib.annotations</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>net.knarcraft:knarlib</artifact>
<includes>
<include>net/knarcraft/knarlib/**</include>
</includes>
</filter>
<filter>
<artifact>org.jetbrains:annotations</artifact>
<includes>
<include>org/jetbrains/annotations/**</include>
</includes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>

View File

@ -1,23 +1,47 @@
package net.knarcraft.blacksmith;
import net.citizensnpcs.api.CitizensAPI;
import net.knarcraft.blacksmith.command.BlackSmithConfigCommand;
import net.knarcraft.blacksmith.command.BlackSmithConfigTabCompleter;
import net.knarcraft.blacksmith.command.BlackSmithEditCommand;
import net.knarcraft.blacksmith.command.BlackSmithEditTabCompleter;
import net.citizensnpcs.api.trait.TraitFactory;
import net.citizensnpcs.api.trait.TraitInfo;
import net.knarcraft.blacksmith.command.PresetCommand;
import net.knarcraft.blacksmith.command.PresetTabCompleter;
import net.knarcraft.blacksmith.config.GlobalSettings;
import net.knarcraft.blacksmith.formatting.Translator;
import net.knarcraft.blacksmith.command.blacksmith.BlackSmithConfigCommand;
import net.knarcraft.blacksmith.command.blacksmith.BlackSmithConfigTabCompleter;
import net.knarcraft.blacksmith.command.blacksmith.BlackSmithEditCommand;
import net.knarcraft.blacksmith.command.blacksmith.BlackSmithEditTabCompleter;
import net.knarcraft.blacksmith.command.scrapper.ScrapperConfigCommand;
import net.knarcraft.blacksmith.command.scrapper.ScrapperConfigTabCompleter;
import net.knarcraft.blacksmith.command.scrapper.ScrapperEditCommand;
import net.knarcraft.blacksmith.command.scrapper.ScrapperEditTabCompleter;
import net.knarcraft.blacksmith.config.StargateYamlConfiguration;
import net.knarcraft.blacksmith.config.blacksmith.GlobalBlacksmithSettings;
import net.knarcraft.blacksmith.config.scrapper.GlobalScrapperSettings;
import net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage;
import net.knarcraft.blacksmith.listener.NPCClickListener;
import net.knarcraft.blacksmith.listener.PlayerListener;
import net.knarcraft.blacksmith.manager.EconomyManager;
import net.knarcraft.blacksmith.trait.BlacksmithTrait;
import net.knarcraft.blacksmith.trait.ScrapperTrait;
import net.knarcraft.blacksmith.util.ConfigHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.formatting.TranslatableTimeUnit;
import net.knarcraft.knarlib.formatting.Translator;
import net.knarcraft.knarlib.util.UpdateChecker;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.event.Event;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.plugin.java.JavaPluginLoader;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
/**
@ -25,57 +49,119 @@ import java.util.logging.Level;
*/
public class BlacksmithPlugin extends JavaPlugin {
private static final String CONFIG_FILE_NAME = "config.yml";
private static BlacksmithPlugin instance;
private GlobalSettings config;
private GlobalBlacksmithSettings blacksmithConfig;
private GlobalScrapperSettings scrapperConfig;
private static Translator translator;
private static StringFormatter stringFormatter;
private FileConfiguration configuration;
/**
* Constructor required for MockBukkit
*/
@SuppressWarnings("unused")
public BlacksmithPlugin() {
super();
}
/**
* Constructor required for MockBukkit
*/
@SuppressWarnings("unused")
protected BlacksmithPlugin(@NotNull JavaPluginLoader loader, @NotNull PluginDescriptionFile descriptionFile,
@NotNull File dataFolder, @NotNull File file) {
super(loader, descriptionFile, dataFolder, file);
}
/**
* Gets an instance of the Blacksmith plugin
*
* @return <p>An instance of the blacksmith plugin</p>
*/
public static BlacksmithPlugin getInstance() {
public static @NotNull BlacksmithPlugin getInstance() {
return instance;
}
/**
* Gets settings for the blacksmith plugin
* Gets settings for the blacksmith plugin's blacksmiths
*
* @return <p>Settings for the blacksmith plugin</p>
* @return <p>Settings for the blacksmith plugin's blacksmith</p>
*/
public GlobalSettings getSettings() {
return config;
public @NotNull GlobalBlacksmithSettings getGlobalBlacksmithSettings() {
return blacksmithConfig;
}
/**
* Gets settings for the blacksmith plugin's scrapper
*
* @return <p>Settings for the blacksmith plugin's scrapper</p>
*/
public @NotNull GlobalScrapperSettings getGlobalScrapperSettings() {
return scrapperConfig;
}
/**
* Reloads the configuration file from disk
*/
public void reload() {
config.load();
this.reloadConfig();
Translator.loadLanguages(this.getConfig().getString("language", "en"));
blacksmithConfig.load();
scrapperConfig.load();
translator.loadLanguages(this.getDataFolder(), "en", this.getConfiguration().getString(
"language", "en"));
}
/**
* Gets the raw configuration
*
* @return <p>The raw configuration</p>
*/
@NotNull
public FileConfiguration getConfiguration() {
return this.configuration;
}
/**
* Gets the string formatter to use for formatting
*
* @return <p>The string formatter to use</p>
*/
public static @NotNull StringFormatter getStringFormatter() {
return BlacksmithPlugin.stringFormatter;
}
/**
* Gets the translator to use for translation
*
* @return <p>The translator to use</p>
*/
public static @NotNull Translator getTranslator() {
return BlacksmithPlugin.translator;
}
@Override
public void onDisable() {
getLogger().log(Level.INFO, " v" + getDescription().getVersion() + " disabled.");
log(" v" + getDescription().getVersion() + " disabled.");
}
@Override
public void onEnable() {
instance = this;
//Copy default config to disk
FileConfiguration fileConfiguration = this.getConfig();
//Copy default config to disk, and add missing configuration values
this.saveDefaultConfig();
this.getConfig().options().copyDefaults(true);
this.reloadConfig();
fileConfiguration.options().copyDefaults(true);
this.saveConfig();
//Load settings
config = new GlobalSettings(this);
config.load();
Translator.loadLanguages(fileConfiguration.getString("language", "en"));
//Migrate from an earlier configuration file syntax if necessary
if (getConfiguration().getString("scrapper.defaults.dropItem") == null) {
ConfigHelper.migrateConfig(this.getDataFolder().getPath().replaceAll("\\\\", "/"),
getConfiguration());
this.reloadConfig();
}
initializeConfigurations(getConfiguration());
//Set up Vault integration
if (!setUpVault()) {
@ -83,15 +169,118 @@ public class BlacksmithPlugin extends JavaPlugin {
}
//Register the blacksmith trait with Citizens
CitizensAPI.getTraitFactory().registerTrait(
net.citizensnpcs.api.trait.TraitInfo.create(BlacksmithTrait.class).withName("blacksmith"));
TraitFactory traitFactory = CitizensAPI.getTraitFactory();
traitFactory.registerTrait(TraitInfo.create(BlacksmithTrait.class).withName("blacksmith"));
traitFactory.registerTrait(TraitInfo.create(ScrapperTrait.class).withName("scrapper"));
//Register all commands
registerCommands();
//Register all listeners
registerListeners();
getLogger().log(Level.INFO, " v" + getDescription().getVersion() + " enabled.");
log(" v" + getDescription().getVersion() + " enabled.");
//Alert about an update in the console
UpdateChecker.checkForUpdate(this, "https://api.spigotmc.org/legacy/update.php?resource=105938",
() -> this.getDescription().getVersion(), null);
}
@Override
public void reloadConfig() {
super.reloadConfig();
this.configuration = new StargateYamlConfiguration();
this.configuration.options().copyDefaults(true);
try {
this.configuration.load(new File(getDataFolder(), CONFIG_FILE_NAME));
} catch (IOException | InvalidConfigurationException exception) {
error("Unable to load the configuration! Message: " + exception.getMessage());
}
}
@Override
public void saveConfig() {
super.saveConfig();
try {
this.configuration.save(new File(getDataFolder(), CONFIG_FILE_NAME));
} catch (IOException exception) {
error("Unable to save the configuration! Message: " + exception.getMessage());
}
}
/**
* Calls an event
*
* @param event <p>The event to call</p>
*/
public void callEvent(@NotNull Event event) {
this.getServer().getPluginManager().callEvent(event);
}
/**
* Prints an info message to the console
*
* @param message <p>The message to print</p>
*/
public static void log(@NotNull String message) {
BlacksmithPlugin.getInstance().getLogger().log(Level.INFO, message);
}
/**
* Prints a warning message to the console
*
* @param message <p>The message to print</p>
*/
public static void warn(@NotNull String message) {
BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING, message);
}
/**
* Prints an error message to the console
*
* @param message <p>The message to print</p>
*/
public static void error(@NotNull String message) {
BlacksmithPlugin.getInstance().getLogger().log(Level.SEVERE, message);
}
/**
* Prints a debug message to the console
*
* @param message <p>The message to print</p>
*/
public static void debug(@NotNull String message) {
BlacksmithPlugin.getInstance().getLogger().log(Level.FINE, message);
}
/**
* Initializes custom configuration and translation
*
* @param fileConfiguration <p>The configuration file to get values from</p>
*/
private void initializeConfigurations(@NotNull FileConfiguration fileConfiguration) {
//Load settings
blacksmithConfig = new GlobalBlacksmithSettings(this);
blacksmithConfig.load();
scrapperConfig = new GlobalScrapperSettings(this);
scrapperConfig.load();
//Prepare the translator
translator = new Translator();
translator.registerMessageCategory(TranslatableTimeUnit.UNIT_SECOND);
translator.registerMessageCategory(BlacksmithTranslatableMessage.ITEM_TYPE_ENCHANTMENT);
translator.loadLanguages(this.getDataFolder(), "en",
fileConfiguration.getString("language", "en"));
PluginDescriptionFile description = this.getDescription();
String prefix;
if (description.getPrefix() == null) {
prefix = "Blacksmith";
} else {
prefix = description.getPrefix();
}
BlacksmithPlugin.stringFormatter = new StringFormatter(prefix, translator);
// This reload is necessary to get values just added to the configuration for some reason
this.reload();
}
/**
@ -100,10 +289,10 @@ public class BlacksmithPlugin extends JavaPlugin {
* @return <p>True if Vault setup/integration succeeded</p>
*/
private boolean setUpVault() {
getLogger().log(Level.INFO, "Setting Up Vault now....");
boolean canLoad = EconomyManager.setUp(getServer().getServicesManager(), getLogger());
log("Setting Up Vault now....");
boolean canLoad = EconomyManager.setUp(getServer().getServicesManager());
if (!canLoad) {
getLogger().log(Level.SEVERE, "Vault Integration Failed....");
error("Vault Integration Failed....");
getServer().getPluginManager().disablePlugin(this);
return false;
}
@ -123,24 +312,28 @@ public class BlacksmithPlugin extends JavaPlugin {
* Registers all commands used by this plugin
*/
private void registerCommands() {
//Register the blacksmith NPC edit main-command
PluginCommand blacksmithCommand = this.getCommand("blacksmith");
if (blacksmithCommand != null) {
blacksmithCommand.setExecutor(new BlackSmithEditCommand());
blacksmithCommand.setTabCompleter(new BlackSmithEditTabCompleter());
registerCommand("blacksmith", new BlackSmithEditCommand(), new BlackSmithEditTabCompleter());
registerCommand("blacksmithConfig", new BlackSmithConfigCommand(), new BlackSmithConfigTabCompleter());
registerCommand("scrapper", new ScrapperEditCommand(), new ScrapperEditTabCompleter());
registerCommand("scrapperConfig", new ScrapperConfigCommand(), new ScrapperConfigTabCompleter());
registerCommand("preset", new PresetCommand(), new PresetTabCompleter());
}
//Register the global config edit command
PluginCommand blacksmithConfigCommand = this.getCommand("blacksmithConfig");
if (blacksmithConfigCommand != null) {
blacksmithConfigCommand.setExecutor(new BlackSmithConfigCommand());
blacksmithConfigCommand.setTabCompleter(new BlackSmithConfigTabCompleter());
/**
* Registers a command
*
* @param commandName <p>The name of the command</p>
* @param executor <p>The executor to bind to the command</p>
* @param tabCompleter <p>The tab completer to bind to the command, or null</p>
*/
private void registerCommand(@NotNull String commandName, @NotNull CommandExecutor executor,
@Nullable TabCompleter tabCompleter) {
PluginCommand command = this.getCommand(commandName);
if (command != null) {
command.setExecutor(executor);
if (tabCompleter != null) {
command.setTabCompleter(tabCompleter);
}
PluginCommand presetCommand = this.getCommand("preset");
if (presetCommand != null) {
presetCommand.setExecutor(new PresetCommand());
presetCommand.setTabCompleter(new PresetTabCompleter());
}
}

View File

@ -1,265 +0,0 @@
package net.knarcraft.blacksmith.command;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.GlobalSetting;
import net.knarcraft.blacksmith.config.GlobalSettings;
import net.knarcraft.blacksmith.config.NPCSetting;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.formatting.ItemType;
import net.knarcraft.blacksmith.formatting.TranslatableMessage;
import net.knarcraft.blacksmith.util.InputParsingHelper;
import net.knarcraft.blacksmith.util.TypeValidationHelper;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.enchantments.Enchantment;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import static net.knarcraft.blacksmith.formatting.StringFormatter.displayErrorMessage;
import static net.knarcraft.blacksmith.formatting.StringFormatter.displaySuccessMessage;
import static net.knarcraft.blacksmith.formatting.TranslatableMessage.getValueChangedMessage;
/**
* The command used for changing global configuration options
*/
public class BlackSmithConfigCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
if (args.length == 0) {
return false;
}
String commandName = args[0];
if (commandName.equalsIgnoreCase("reload")) {
return new ReloadCommand().onCommand(sender, command, label, args);
}
GlobalSettings settings = BlacksmithPlugin.getInstance().getSettings();
//Changing reforge-able items' default isn't recommended
if (commandName.equalsIgnoreCase(NPCSetting.REFORGE_ABLE_ITEMS.getCommandName())) {
displayErrorMessage(sender, TranslatableMessage.DEFAULT_REFORGE_ABLE_ITEMS_UNCHANGEABLE);
return false;
}
//Find which global setting the user has specified, if any
GlobalSetting detectedGlobalSetting = null;
for (GlobalSetting globalSetting : GlobalSetting.values()) {
if (commandName.equalsIgnoreCase(globalSetting.getCommandName())) {
detectedGlobalSetting = globalSetting;
break;
}
}
//Find which npc setting the user has specified, if any
NPCSetting detectedNPCSetting = null;
for (NPCSetting npcSetting : NPCSetting.values()) {
if (commandName.equalsIgnoreCase(npcSetting.getCommandName())) {
detectedNPCSetting = npcSetting;
break;
}
}
//Display the current value of a setting
if (args.length == 1) {
return displayCurrentValue(detectedGlobalSetting, detectedNPCSetting, settings, sender);
} else if (args.length == 2 && isSpecialCase(commandName)) {
if (displaySpecialCaseValue(args[1], sender, detectedGlobalSetting, settings)) {
return true;
}
}
//Update the value of a special-case setting
if (args.length == 3 && updateSpecialCase(settings, detectedGlobalSetting, args, sender)) {
return true;
}
//Change the value of the specified setting
if ((detectedGlobalSetting != null &&
TypeValidationHelper.isValid(detectedGlobalSetting.getValueType(), args[1], sender)) ||
(detectedNPCSetting != null &&
TypeValidationHelper.isValid(detectedNPCSetting.getValueType(), args[1], sender))) {
return changeValue(args, detectedGlobalSetting, detectedNPCSetting, settings, sender);
} else {
return false;
}
}
/**
* Changes the value of the setting defined in the user's input
*
* @param args <p>The arguments given by the user</p>
* @param detectedGlobalSetting <p>The global setting recognized from the input, if any</p>
* @param detectedNPCSetting <p>The NPC setting recognized from the input, if any</p>
* @param settings <p>The global settings object to get settings from</p>
* @param sender <p>The command sender to display any output to</p>
* @return <p>True if the value was successfully changed</p>
*/
private boolean changeValue(String[] args, GlobalSetting detectedGlobalSetting, NPCSetting detectedNPCSetting,
GlobalSettings settings, CommandSender sender) {
String newValue = args[1];
if (detectedGlobalSetting != null) {
settings.changeValue(detectedGlobalSetting, newValue);
displaySuccessMessage(sender, getValueChangedMessage(detectedGlobalSetting.getCommandName(), newValue));
return true;
} else if (detectedNPCSetting != null) {
//This makes sure all arguments are treated as a sentence
if (detectedNPCSetting.getValueType() == SettingValueType.STRING) {
newValue = String.join(" ", Arrays.asList(args).subList(1, args.length));
}
settings.changeValue(detectedNPCSetting, newValue);
displaySuccessMessage(sender, getValueChangedMessage(detectedNPCSetting.getCommandName(), newValue));
return true;
} else {
return false;
}
}
/**
* Displays the current value of the selected setting
*
* @param detectedGlobalSetting <p>The global setting recognized from the input, if any</p>
* @param detectedNPCSetting <p>The NPC setting recognized from the input, if any</p>
* @param settings <p>The global settings object to get settings from</p>
* @param sender <p>The command sender to display any output to</p>
* @return <p>True if a settings was successfully displayed</p>
*/
private boolean displayCurrentValue(GlobalSetting detectedGlobalSetting, NPCSetting detectedNPCSetting,
GlobalSettings settings, CommandSender sender) {
String settingValue;
String correctCommandName;
boolean printRawValue = false;
//Find the value of the specified setting
//TODO: See if there's a way to remove this duplication
if (detectedGlobalSetting != null) {
settingValue = String.valueOf(settings.getRawValue(detectedGlobalSetting));
correctCommandName = detectedGlobalSetting.getCommandName();
} else if (detectedNPCSetting != null) {
settingValue = String.valueOf(settings.getRawValue(detectedNPCSetting));
correctCommandName = detectedNPCSetting.getCommandName();
//For messages, print an additional raw value showing which color codes are used
if (detectedNPCSetting.getPath().startsWith("defaults.messages")) {
printRawValue = true;
}
} else {
return false;
}
//Display the current value of the setting
displaySuccessMessage(sender, TranslatableMessage.getCurrentValueMessage(correctCommandName, settingValue));
//Print the value with any colors displayed as &a-f0-9
if (printRawValue) {
sender.sendMessage(TranslatableMessage.getRawValueMessage(
settingValue.replace(ChatColor.COLOR_CHAR, '&')));
}
return true;
}
/**
* Displays the current value of a special-case configuration value
*
* @param selector <p>The selector specifying which material/enchantment to display information about</p>
* @param sender <p>The sender to display the value to</p>
* @param setting <p>The setting to display</p>
* @param settings <p>The settings object to query</p>
* @return <p>True if the value was successfully displayed</p>
*/
private boolean displaySpecialCaseValue(String selector, CommandSender sender, GlobalSetting setting,
GlobalSettings settings) {
if (setting == GlobalSetting.BASE_PRICE || setting == GlobalSetting.PRICE_PER_DURABILITY_POINT) {
Material material = InputParsingHelper.matchMaterial(selector);
if (material == null) {
return false;
}
String currentValue;
if (setting == GlobalSetting.BASE_PRICE) {
currentValue = String.valueOf(settings.getBasePrice(material));
} else {
currentValue = String.valueOf(settings.getPricePerDurabilityPoint(material));
}
displaySuccessMessage(sender, TranslatableMessage.getItemCurrentValueMessage(setting.getCommandName(),
ItemType.MATERIAL, material.name(), currentValue));
return true;
} else if (setting == GlobalSetting.ENCHANTMENT_COST) {
Enchantment enchantment = InputParsingHelper.matchEnchantment(selector);
if (enchantment == null) {
return false;
}
displaySuccessMessage(sender, TranslatableMessage.getItemCurrentValueMessage(setting.getCommandName(),
ItemType.ENCHANTMENT, enchantment.getKey().getKey(),
String.valueOf(settings.getEnchantmentCost(enchantment))));
return true;
} else {
return false;
}
}
/**
* Gets whether a command name matches a special case command
*
* @param commandName <p>The command specified</p>
* @return <p>True if the command is a special case</p>
*/
private boolean isSpecialCase(String commandName) {
return commandName.equalsIgnoreCase(GlobalSetting.BASE_PRICE.getCommandName()) ||
commandName.equalsIgnoreCase(GlobalSetting.PRICE_PER_DURABILITY_POINT.getCommandName()) ||
commandName.equalsIgnoreCase(GlobalSetting.ENCHANTMENT_COST.getCommandName());
}
/**
* Updates a special-case configuration value if a special-case is encountered
*
* @param settings <p>The settings to modify</p>
* @param detectedGlobalSetting <p>The global setting specified</p>
* @param args <p>All arguments given</p>
* @param sender <p>The command sender to notify if successful</p>
* @return <p>True if already handled as a special case</p>
*/
private boolean updateSpecialCase(GlobalSettings settings, GlobalSetting detectedGlobalSetting, String[] args,
CommandSender sender) {
if (InputParsingHelper.isEmpty(args[2])) {
args[2] = "-1";
} else if (!TypeValidationHelper.isValid(SettingValueType.POSITIVE_DOUBLE, args[2], sender)) {
return true;
}
double newPrice = Double.parseDouble(args[2]);
String itemChanged;
ItemType itemType;
String newValue = String.valueOf(newPrice);
if (detectedGlobalSetting == GlobalSetting.BASE_PRICE ||
detectedGlobalSetting == GlobalSetting.PRICE_PER_DURABILITY_POINT) {
Material material = InputParsingHelper.matchMaterial(args[1]);
if (material == null) {
return false;
}
itemType = ItemType.MATERIAL;
itemChanged = material.name();
if (detectedGlobalSetting == GlobalSetting.BASE_PRICE) {
settings.setBasePrice(material, newPrice);
} else {
settings.setPricePerDurabilityPoint(material, newPrice);
}
} else if (detectedGlobalSetting == GlobalSetting.ENCHANTMENT_COST) {
Enchantment enchantment = InputParsingHelper.matchEnchantment(args[1]);
if (enchantment == null) {
return false;
}
itemType = ItemType.ENCHANTMENT;
itemChanged = enchantment.getKey().getKey();
settings.setEnchantmentCost(enchantment, newPrice);
} else {
return false;
}
displaySuccessMessage(sender, TranslatableMessage.getItemValueChangedMessage(detectedGlobalSetting.getCommandName(),
itemType, itemChanged, newValue));
return true;
}
}

View File

@ -1,119 +0,0 @@
package net.knarcraft.blacksmith.command;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.NPCSetting;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.formatting.StringFormatter;
import net.knarcraft.blacksmith.formatting.TranslatableMessage;
import net.knarcraft.blacksmith.formatting.Translator;
import net.knarcraft.blacksmith.trait.BlacksmithTrait;
import net.knarcraft.blacksmith.util.InputParsingHelper;
import net.knarcraft.blacksmith.util.TypeValidationHelper;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import static net.knarcraft.blacksmith.formatting.StringFormatter.displaySuccessMessage;
import static net.knarcraft.blacksmith.formatting.TranslatableMessage.getCurrentValueMessage;
import static net.knarcraft.blacksmith.formatting.TranslatableMessage.getValueChangedMessage;
/**
* The main command used for blacksmith editing
*/
public class BlackSmithEditCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
NPC npc = CitizensAPI.getDefaultNPCSelector().getSelected(sender);
if (npc == null || !npc.hasTrait(BlacksmithTrait.class)) {
StringFormatter.displayErrorMessage(sender, TranslatableMessage.NO_NPC_SELECTED);
return true;
}
if (args.length < 1) {
return false;
}
BlacksmithTrait blacksmithTrait = npc.getTraitNullable(BlacksmithTrait.class);
for (NPCSetting npcSetting : NPCSetting.values()) {
String commandName = npcSetting.getCommandName();
if (commandName.equalsIgnoreCase(args[0])) {
String newValue = args.length < 2 ? null : args[1];
//This makes sure all arguments are treated as a sentence
if (npcSetting.getValueType() == SettingValueType.STRING && args.length > 2) {
newValue = String.join(" ", Arrays.asList(args).subList(1, args.length));
}
return displayOrChangeNPCSetting(blacksmithTrait, npcSetting, newValue, sender);
}
}
return false;
}
/**
* Changes the given NPC setting, or displays the current value if a new value isn't specified
*
* @param blacksmithTrait <p>The blacksmith trait belonging to the selected NPC</p>
* @param npcSetting <p>The NPC setting to change</p>
* @param newValue <p>The value to change the setting to</p>
* @param sender <p>The command sender to notify about results</p>
* @return <p>True if everything went successfully</p>
*/
private boolean displayOrChangeNPCSetting(BlacksmithTrait blacksmithTrait, NPCSetting npcSetting, String newValue,
CommandSender sender) {
if (newValue == null) {
//Display the current value of the setting
displayNPCSetting(blacksmithTrait, npcSetting, sender);
} else {
//If an empty value or null, clear the value instead of changing it
if (InputParsingHelper.isEmpty(newValue)) {
newValue = null;
} else {
//Abort if an invalid value is given
boolean isValidType = TypeValidationHelper.isValid(npcSetting.getValueType(), newValue, sender);
if (!isValidType) {
return false;
}
newValue = ChatColor.translateAlternateColorCodes('&', newValue);
}
//Change the setting
blacksmithTrait.getSettings().changeSetting(npcSetting, newValue);
displaySuccessMessage(sender, getValueChangedMessage(npcSetting.getCommandName(), String.valueOf(newValue)));
//Save the changes immediately to prevent data loss on server crash
CitizensAPI.getNPCRegistry().saveToStore();
}
return true;
}
/**
* Displays the current value of the given NPC setting
*
* @param blacksmithTrait <p>The blacksmith trait of the NPC to get the value from</p>
* @param npcSetting <p>The NPC setting to see the value of</p>
* @param sender <p>The command sender to display the value to</p>
*/
private void displayNPCSetting(BlacksmithTrait blacksmithTrait, NPCSetting npcSetting, CommandSender sender) {
String rawValue = String.valueOf(blacksmithTrait.getSettings().getRawValue(npcSetting));
if (InputParsingHelper.isEmpty(rawValue)) {
//Display the default value, if no custom value has been specified
rawValue = String.valueOf(BlacksmithPlugin.getInstance().getSettings().getRawValue(npcSetting));
displaySuccessMessage(sender, getCurrentValueMessage(npcSetting.getCommandName(), rawValue));
} else {
//Add a marker if the value has been customized
String marker = Translator.getTranslatedMessage(TranslatableMessage.SETTING_OVERRIDDEN_MARKER);
displaySuccessMessage(sender, getCurrentValueMessage(npcSetting.getCommandName(), rawValue) + marker);
}
if (npcSetting.getPath().startsWith("defaults.messages")) {
sender.sendMessage(TranslatableMessage.getRawValueMessage(rawValue.replace(ChatColor.COLOR_CHAR, '&')));
}
}
}

View File

@ -0,0 +1,191 @@
package net.knarcraft.blacksmith.command;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.Setting;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.Settings;
import net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage;
import net.knarcraft.blacksmith.trait.CustomTrait;
import net.knarcraft.blacksmith.util.ConfigCommandHelper;
import net.knarcraft.blacksmith.util.InputParsingHelper;
import net.knarcraft.blacksmith.util.TypeValidationHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import static net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage.getCurrentValueMessage;
import static net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage.getDefaultValueMessage;
import static net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage.getValueChangedMessage;
/**
* A generic implementation of the edit command
*
* @param <K> <p>The type of trait to edit</p>
* @param <L> <p>The type of setting the trait uses</p>
*/
public abstract class EditCommand<K extends CustomTrait<L>, L extends Setting> implements CommandExecutor {
protected final Class<K> traitClass;
/**
* Instantiates a new edit command
*
* @param traitClass <p>The type of trait this command edits</p>
*/
public EditCommand(Class<K> traitClass) {
this.traitClass = traitClass;
}
/**
* Gets the setting corresponding to the given input
*
* @param input <p>The input given by the user</p>
* @return <p>The corresponding setting, or null if not recognized</p>
*/
public abstract L getSetting(String input);
/**
* Gets the global settings to use
*
* @return <p>The global settings</p>
*/
public abstract @NotNull Settings<L> getGlobalSettings();
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String s,
@NotNull String[] args) {
NPC npc = CitizensAPI.getDefaultNPCSelector().getSelected(sender);
if (npc == null || !npc.hasTrait(traitClass)) {
BlacksmithPlugin.getStringFormatter().displayErrorMessage(sender,
BlacksmithTranslatableMessage.NO_NPC_SELECTED);
return true;
}
if (args.length < 1) {
return false;
}
K trait = npc.getTraitNullable(traitClass);
if (trait == null) {
return false;
}
L setting = getSetting(args[0]);
if (setting != null) {
String newValue = args.length < 2 ? null : args[1];
//This makes sure all arguments are treated as a sentence
if (setting.getValueType() == SettingValueType.STRING && args.length > 2) {
newValue = String.join(" ", Arrays.asList(args).subList(1, args.length));
}
Settings<L> settings = getGlobalSettings();
return displayOrChangeNPCSetting(trait, setting, settings, newValue, sender);
} else {
return false;
}
}
/**
* Changes the given NPC setting, or displays the current value if a new value isn't specified
*
* @param trait <p>The trait belonging to the selected NPC</p>
* @param setting <p>The NPC setting to change</p>
* @param globalSettings <p>The global settings to get default values from</p>
* @param newValue <p>The value to change the setting to</p>
* @param sender <p>The command sender to notify about results</p>
* @return <p>True if everything went successfully</p>
*/
private boolean displayOrChangeNPCSetting(@NotNull CustomTrait<L> trait,
@NotNull L setting,
@NotNull Settings<L> globalSettings,
@Nullable String newValue,
@NotNull CommandSender sender) {
if (newValue == null) {
//Display the current value of the setting
displayNPCSetting(trait, setting, globalSettings, sender);
} else {
//If an empty value or null, clear the value instead of changing it
if (InputParsingHelper.isEmpty(newValue)) {
newValue = null;
} else {
//Abort if an invalid value is given
boolean isValidType = TypeValidationHelper.isValid(setting.getValueType(), newValue, sender);
if (!isValidType) {
return false;
}
newValue = ChatColor.translateAlternateColorCodes('&', newValue);
}
//Change the setting
Settings<L> settings = trait.getTraitSettings();
if (settings == null) {
BlacksmithPlugin.error("Settings for a CustomTrait has not been initialized! Please inform the developer!");
return false;
}
settings.changeValue(setting, newValue);
BlacksmithPlugin.getStringFormatter().displaySuccessMessage(sender,
getValueChangedMessage(setting.getCommandName(), String.valueOf(newValue)));
//Save the changes immediately to prevent data loss on server crash
CitizensAPI.getNPCRegistry().saveToStore();
}
return true;
}
/**
* Displays the current value of the given NPC setting
*
* @param trait <p>The trait of the NPC to get the value from</p>
* @param setting <p>The NPC setting to see the value of</p>
* @param globalSettings <p>The global settings to get default values from</p>
* @param sender <p>The command sender to display the value to</p>
*/
private void displayNPCSetting(@NotNull CustomTrait<L> trait,
@NotNull L setting,
@NotNull Settings<L> globalSettings,
@NotNull CommandSender sender) {
Settings<L> settings = trait.getTraitSettings();
if (settings == null) {
BlacksmithPlugin.error("Settings for a CustomTrait has not been initialized! Please inform the developer!");
return;
}
StringFormatter formatter = BlacksmithPlugin.getStringFormatter();
//Find the value of the specified setting
String commandName = setting.getCommandName();
String currentValue = String.valueOf(settings.getRawValue(setting));
String defaultValue = String.valueOf(setting.getDefaultValue());
String marker = formatter.getUnFormattedMessage(BlacksmithTranslatableMessage.SETTING_OVERRIDDEN_MARKER);
boolean isMessage = setting.isMessage();
boolean isSet = !InputParsingHelper.isEmpty(currentValue);
// Display the description for how this setting is used
formatter.displaySuccessMessage(sender, setting.getDescription());
// Display the default value
formatter.displayNeutralMessage(sender, getDefaultValueMessage(commandName, defaultValue));
if (isMessage) {
ConfigCommandHelper.displayRaw(sender, defaultValue);
}
if (!isSet) {
//Display the global value, if no custom value has been specified
currentValue = String.valueOf(globalSettings.getRawValue(setting));
}
// Display the value with a marker if it's customized
formatter.displayNeutralMessage(sender, getCurrentValueMessage(commandName, currentValue) + (isSet ? marker : ""));
if (isMessage) {
ConfigCommandHelper.displayRaw(sender, currentValue);
}
}
}

View File

@ -1,10 +1,9 @@
package net.knarcraft.blacksmith.command;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.SmithPreset;
import net.knarcraft.blacksmith.config.SmithPresetFilter;
import net.knarcraft.blacksmith.formatting.StringFormatter;
import net.knarcraft.blacksmith.formatting.TranslatableMessage;
import net.knarcraft.blacksmith.formatting.Translator;
import net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@ -13,9 +12,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.blacksmith.formatting.StringFormatter.displayErrorMessage;
import static net.knarcraft.blacksmith.formatting.StringFormatter.displaySuccessMessage;
import java.util.Set;
/**
* The command for displaying which materials are contained in a preset
@ -30,7 +27,7 @@ public class PresetCommand implements CommandExecutor {
}
String presetName = args[0].toUpperCase().replace('-', '_');
List<Material> includedMaterials;
Set<Material> includedMaterials;
try {
//Display the preset with the filter applied
@ -40,7 +37,8 @@ public class PresetCommand implements CommandExecutor {
SmithPresetFilter filter = SmithPresetFilter.valueOf(parts[1]);
if (!smithPreset.supportsFilter(filter)) {
displayErrorMessage(sender, TranslatableMessage.INVALID_FILTER_FOR_PRESET);
BlacksmithPlugin.getStringFormatter().displayErrorMessage(sender,
BlacksmithTranslatableMessage.INVALID_FILTER_FOR_PRESET);
return false;
}
includedMaterials = smithPreset.getFilteredMaterials(filter);
@ -48,7 +46,8 @@ public class PresetCommand implements CommandExecutor {
includedMaterials = SmithPreset.valueOf(presetName).getMaterials();
}
} catch (IllegalArgumentException exception) {
displayErrorMessage(sender, TranslatableMessage.INVALID_PRESET_OR_FILTER);
BlacksmithPlugin.getStringFormatter().displayErrorMessage(sender,
BlacksmithTranslatableMessage.INVALID_PRESET_OR_FILTER);
return false;
}
@ -57,8 +56,9 @@ public class PresetCommand implements CommandExecutor {
for (Material material : includedMaterials) {
materialNames.add(material.name());
}
displaySuccessMessage(sender, StringFormatter.replacePlaceholder(Translator.getTranslatedMessage(
TranslatableMessage.PRESET_MATERIALS), "{materials}", String.join(", ", materialNames)));
BlacksmithPlugin.getStringFormatter().displaySuccessMessage(sender,
BlacksmithPlugin.getStringFormatter().replacePlaceholder(BlacksmithTranslatableMessage.PRESET_MATERIALS,
"{materials}", String.join(", ", materialNames)));
return true;
}

View File

@ -2,7 +2,7 @@ package net.knarcraft.blacksmith.command;
import net.knarcraft.blacksmith.config.SmithPreset;
import net.knarcraft.blacksmith.config.SmithPresetFilter;
import net.knarcraft.blacksmith.util.TabCompletionHelper;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;

View File

@ -1,7 +1,7 @@
package net.knarcraft.blacksmith.command;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.formatting.TranslatableMessage;
import net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@ -11,8 +11,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.blacksmith.formatting.StringFormatter.displaySuccessMessage;
/**
* The command for re-loading the plugin
*/
@ -22,14 +20,13 @@ public class ReloadCommand implements TabExecutor {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
BlacksmithPlugin.getInstance().reload();
displaySuccessMessage(sender, TranslatableMessage.PLUGIN_RELOADED);
BlacksmithPlugin.getStringFormatter().displaySuccessMessage(sender, BlacksmithTranslatableMessage.PLUGIN_RELOADED);
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
return new ArrayList<>();
}

View File

@ -0,0 +1,228 @@
package net.knarcraft.blacksmith.command.blacksmith;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.command.ReloadCommand;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.blacksmith.BlacksmithSetting;
import net.knarcraft.blacksmith.config.blacksmith.GlobalBlacksmithSettings;
import net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage;
import net.knarcraft.blacksmith.formatting.ItemType;
import net.knarcraft.blacksmith.util.ConfigCommandHelper;
import net.knarcraft.blacksmith.util.InputParsingHelper;
import net.knarcraft.blacksmith.util.ItemHelper;
import net.knarcraft.blacksmith.util.TypeValidationHelper;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.enchantments.Enchantment;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
/**
* The command used for changing global blacksmith configuration options
*/
public class BlackSmithConfigCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
if (args.length == 0) {
return false;
}
String commandName = args[0];
if (commandName.equalsIgnoreCase("reload")) {
return new ReloadCommand().onCommand(sender, command, label, args);
}
GlobalBlacksmithSettings settings = BlacksmithPlugin.getInstance().getGlobalBlacksmithSettings();
//Find which setting the user has specified, if any
BlacksmithSetting detectedSetting = BlacksmithSetting.getSetting(commandName);
if (detectedSetting == null) {
return false;
}
//Display the current value of a setting
if (args.length == 1) {
ConfigCommandHelper.displayCurrentValue(detectedSetting, settings, sender);
return true;
} else if (args.length == 2 && isSpecialCase(commandName)) {
if (displaySpecialCaseValue(args[1], sender, detectedSetting, settings)) {
return true;
}
}
//Update the value of a special-case setting
if (args.length == 3 && updateSpecialCase(settings, detectedSetting, args, sender)) {
return true;
}
//Change the value of the specified setting
if (TypeValidationHelper.isValid(detectedSetting.getValueType(), args[1], sender)) {
ConfigCommandHelper.changeValue(args, detectedSetting, settings, sender);
return true;
} else {
return false;
}
}
/**
* Displays the current value of a special-case configuration value
*
* @param selector <p>The selector specifying which material/enchantment to display information about</p>
* @param sender <p>The sender to display the value to</p>
* @param setting <p>The setting to display</p>
* @param settings <p>The settings object to query</p>
* @return <p>True if the value was successfully displayed</p>
*/
private boolean displaySpecialCaseValue(@NotNull String selector, @NotNull CommandSender sender,
@Nullable BlacksmithSetting setting,
@NotNull GlobalBlacksmithSettings settings) {
if (setting == BlacksmithSetting.BASE_PRICE || setting == BlacksmithSetting.PRICE_PER_DURABILITY_POINT) {
Material material = InputParsingHelper.matchMaterial(selector);
if (material == null) {
return false;
}
String currentValue;
if (setting == BlacksmithSetting.BASE_PRICE) {
currentValue = String.valueOf(settings.getBasePrice(material));
} else {
currentValue = String.valueOf(settings.getPricePerDurabilityPoint(material));
}
BlacksmithPlugin.getStringFormatter().displaySuccessMessage(sender,
BlacksmithTranslatableMessage.getItemCurrentValueMessage(setting.getCommandName(),
ItemType.MATERIAL, material.name(), currentValue));
return true;
} else if (setting == BlacksmithSetting.ENCHANTMENT_COST) {
Enchantment enchantment = InputParsingHelper.matchEnchantment(selector);
if (enchantment == null) {
return false;
}
BlacksmithPlugin.getStringFormatter().displaySuccessMessage(sender,
BlacksmithTranslatableMessage.getItemCurrentValueMessage(setting.getCommandName(),
ItemType.ENCHANTMENT, enchantment.getKey().getKey(),
String.valueOf(settings.getEnchantmentCost(enchantment))));
return true;
} else {
return false;
}
}
/**
* Gets whether a command name matches a special case command
*
* @param commandName <p>The command specified</p>
* @return <p>True if the command is a special case</p>
*/
private boolean isSpecialCase(@NotNull String commandName) {
return commandName.equalsIgnoreCase(BlacksmithSetting.BASE_PRICE.getCommandName()) ||
commandName.equalsIgnoreCase(BlacksmithSetting.PRICE_PER_DURABILITY_POINT.getCommandName()) ||
commandName.equalsIgnoreCase(BlacksmithSetting.ENCHANTMENT_COST.getCommandName());
}
/**
* Updates a special-case configuration value if a special-case is encountered
*
* @param settings <p>The settings to modify</p>
* @param blacksmithSetting <p>The setting specified</p>
* @param args <p>All arguments given</p>
* @param sender <p>The command sender to notify if successful</p>
* @return <p>True if already handled as a special case</p>
*/
private boolean updateSpecialCase(@NotNull GlobalBlacksmithSettings settings,
@Nullable BlacksmithSetting blacksmithSetting,
@NotNull String[] args,
CommandSender sender) {
if (InputParsingHelper.isEmpty(args[2])) {
args[2] = "-1";
} else if (!TypeValidationHelper.isValid(SettingValueType.POSITIVE_DOUBLE, args[2], sender)) {
return true;
}
double newPrice = Double.parseDouble(args[2]);
String newValue = String.valueOf(newPrice);
if (blacksmithSetting == BlacksmithSetting.BASE_PRICE ||
blacksmithSetting == BlacksmithSetting.PRICE_PER_DURABILITY_POINT) {
return updatePriceSpecialCase(settings, blacksmithSetting, args[1], newPrice, sender);
} else if (blacksmithSetting == BlacksmithSetting.ENCHANTMENT_COST) {
//Update enchantment cost for an item
Enchantment enchantment = InputParsingHelper.matchEnchantment(args[1]);
if (enchantment == null) {
return false;
}
ItemType itemType = ItemType.ENCHANTMENT;
String itemChanged = enchantment.getKey().getKey();
settings.setEnchantmentCost(enchantment, newPrice);
BlacksmithPlugin.getStringFormatter().displaySuccessMessage(sender,
BlacksmithTranslatableMessage.getItemValueChangedMessage(blacksmithSetting.getCommandName(),
itemType, itemChanged, newValue));
return true;
} else {
return false;
}
}
/**
* Updates a special case price configuration value if a special case is encountered
*
* @param settings <p>The settings to modify</p>
* @param blacksmithSetting <p>The setting specified</p>
* @param materialName <p>The material name to update the price for</p>
* @param newPrice <p>The new price to update to</p>
* @param sender <p>The command sender to respond to</p>
* @return <p>True if the input was valid, and the item(s) was/were updated</p>
*/
private boolean updatePriceSpecialCase(@NotNull GlobalBlacksmithSettings settings,
@NotNull BlacksmithSetting blacksmithSetting,
@NotNull String materialName, double newPrice,
@NotNull CommandSender sender) {
ItemType itemType = ItemType.MATERIAL;
String itemChanged;
//Update base price or price per durability point for an item
if (materialName.contains("*")) {
itemChanged = materialName;
updateAllMatchedPrices(settings, blacksmithSetting, materialName, newPrice);
} else {
Material material = InputParsingHelper.matchMaterial(materialName);
if (material == null) {
return false;
}
itemChanged = material.name();
if (blacksmithSetting == BlacksmithSetting.BASE_PRICE) {
settings.setBasePrice(material, newPrice);
} else {
settings.setPricePerDurabilityPoint(material, newPrice);
}
}
BlacksmithPlugin.getStringFormatter().displaySuccessMessage(sender,
BlacksmithTranslatableMessage.getItemValueChangedMessage(blacksmithSetting.getCommandName(),
itemType, itemChanged, String.valueOf(newPrice)));
return true;
}
/**
* Updates all materials matching the material name wildcard
*
* @param settings <p>The settings to modify</p>
* @param blacksmithSetting <p>The setting specified</p>
* @param materialName <p>The wildcard material name to update cost for</p>
* @param newPrice <p>The new cost for the matched items</p>
*/
private void updateAllMatchedPrices(@NotNull GlobalBlacksmithSettings settings,
@NotNull BlacksmithSetting blacksmithSetting,
@NotNull String materialName, double newPrice) {
Set<Material> materials = ItemHelper.getWildcardMatch(materialName, false);
for (Material material : materials) {
if (blacksmithSetting == BlacksmithSetting.BASE_PRICE) {
settings.setBasePrice(material, newPrice);
} else {
settings.setPricePerDurabilityPoint(material, newPrice);
}
}
}
}

View File

@ -1,9 +1,9 @@
package net.knarcraft.blacksmith.command;
package net.knarcraft.blacksmith.command.blacksmith;
import net.knarcraft.blacksmith.config.GlobalSetting;
import net.knarcraft.blacksmith.config.NPCSetting;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.blacksmith.BlacksmithSetting;
import net.knarcraft.blacksmith.util.InputParsingHelper;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
@ -14,10 +14,10 @@ import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.blacksmith.util.TabCompleteValuesHelper.getTabCompletions;
import static net.knarcraft.blacksmith.util.TabCompletionHelper.filterMatchingContains;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
/**
* The tab completer for the command used for changing global configuration options
* The tab completer for the command used for changing global blacksmith configuration options
*/
public class BlackSmithConfigTabCompleter implements TabCompleter {
@ -32,71 +32,61 @@ public class BlackSmithConfigTabCompleter implements TabCompleter {
//Arguments: <setting> [new value/material or enchantment] []
//Prevent tab-completion when typing messages with spaces
if (skipCompletionForSpacedMessage(args) != null) {
if (args.length > 2 && skipCompletionForSpacedMessage(args[0]) != null) {
return new ArrayList<>();
}
if (args.length == 1) {
List<String> availableCommands = new ArrayList<>();
availableCommands.add("reload");
for (NPCSetting setting : NPCSetting.values()) {
for (BlacksmithSetting setting : BlacksmithSetting.values()) {
availableCommands.add(setting.getCommandName());
}
for (GlobalSetting globalSetting : GlobalSetting.values()) {
availableCommands.add(globalSetting.getCommandName());
}
return filterMatchingContains(availableCommands, args[0]);
} else if (args.length == 2) {
return tabCompleteCommandValues(args[0], args[1]);
} else if (args.length == 3) {
//Get per-material tab completions, or return nothing if an invalid setting was specified
for (GlobalSetting globalSetting : GlobalSetting.values()) {
if (globalSetting.getCommandName().equalsIgnoreCase(args[0])) {
return getPerTypeTabCompletions(globalSetting, args);
BlacksmithSetting blacksmithSetting = BlacksmithSetting.getSetting(args[0]);
if (blacksmithSetting != null) {
return getPerTypeTabCompletions(blacksmithSetting, args);
} else {
return new ArrayList<>();
}
}
return new ArrayList<>();
}
return null;
}
/**
* Checks whether to tab-complete nothing because a message containing spaces is being written
*
* @param args <p>The arguments given by the user</p>
* @param command <p>The command specified by the user</p>
* @return <p>Null if not writing a spaced message</p>
*/
private List<String> skipCompletionForSpacedMessage(String[] args) {
if (args.length > 2) {
NPCSetting npcSetting = null;
for (NPCSetting setting : NPCSetting.values()) {
if (setting.getCommandName().equalsIgnoreCase(args[0])) {
npcSetting = setting;
break;
}
}
if (npcSetting != null && npcSetting.getPath().startsWith("defaults.messages")) {
private List<String> skipCompletionForSpacedMessage(@NotNull String command) {
BlacksmithSetting blacksmithSetting = BlacksmithSetting.getSetting(command);
if (blacksmithSetting != null && blacksmithSetting.isMessage()) {
return new ArrayList<>();
}
}
return null;
}
/**
* Gets tab-completions for a selected material or enchantment
*
* @param globalSetting <p>The global setting to get tab-completions for</p>
* @param blacksmithSetting <p>The global setting to get tab-completions for</p>
* @param args <p>The arguments given by the user</p>
* @return <p>The tab-completions to show to the user</p>
*/
private List<String> getPerTypeTabCompletions(GlobalSetting globalSetting, String[] args) {
private List<String> getPerTypeTabCompletions(@NotNull BlacksmithSetting blacksmithSetting,
@NotNull String[] args) {
//Display possible tab-completions only if a valid enchantment or material is provided
if (((globalSetting == GlobalSetting.BASE_PRICE ||
globalSetting == GlobalSetting.PRICE_PER_DURABILITY_POINT) &&
if (((blacksmithSetting == BlacksmithSetting.BASE_PRICE ||
blacksmithSetting == BlacksmithSetting.PRICE_PER_DURABILITY_POINT) &&
InputParsingHelper.matchMaterial(args[1]) != null) ||
(globalSetting == GlobalSetting.ENCHANTMENT_COST &&
(blacksmithSetting == BlacksmithSetting.ENCHANTMENT_COST &&
InputParsingHelper.matchEnchantment(args[1]) != null)) {
return filterMatchingContains(getTabCompletions(globalSetting.getValueType()), args[2]);
return filterMatchingContains(getTabCompletions(blacksmithSetting.getValueType()), args[2]);
} else {
return new ArrayList<>();
}
@ -109,36 +99,39 @@ public class BlackSmithConfigTabCompleter implements TabCompleter {
* @param commandValue <p>The command value used to filter tab-completions</p>
* @return <p>Some valid options for the command's argument</p>
*/
private List<String> tabCompleteCommandValues(String commandName, String commandValue) {
private List<String> tabCompleteCommandValues(@NotNull String commandName, @NotNull String commandValue) {
if (commandName.equalsIgnoreCase("reload")) {
return new ArrayList<>();
}
for (GlobalSetting globalSetting : GlobalSetting.values()) {
if (globalSetting.getCommandName().equalsIgnoreCase(commandName)) {
return getCompletions(globalSetting, commandValue);
}
}
for (NPCSetting npcSetting : NPCSetting.values()) {
if (npcSetting.getCommandName().equalsIgnoreCase(commandName)) {
return filterMatchingContains(getTabCompletions(
npcSetting.getValueType()), commandValue);
}
}
BlacksmithSetting setting = BlacksmithSetting.getSetting(commandName);
if (setting != null) {
return getCompletions(setting, commandValue);
} else {
return null;
}
}
/**
* Gets tab-completions for the given global setting and filters on the command value
*
* @param globalSetting <p>The global setting to get tab-completions for</p>
* @param blacksmithSetting <p>The global setting to get tab-completions for</p>
* @param commandValue <p>The command value used to filter between available tab-completions</p>
* @return <p>The available tab-completions</p>
*/
private List<String> getCompletions(GlobalSetting globalSetting, String commandValue) {
List<String> returnValues = filterMatchingContains(getTabCompletions(globalSetting.getValueType()), commandValue);
if (globalSetting == GlobalSetting.BASE_PRICE || globalSetting == GlobalSetting.PRICE_PER_DURABILITY_POINT) {
private List<String> getCompletions(@NotNull BlacksmithSetting blacksmithSetting,
@NotNull String commandValue) {
if (blacksmithSetting == BlacksmithSetting.ENCHANTMENT_BLOCK_LIST ||
blacksmithSetting == BlacksmithSetting.REFORGE_ABLE_ITEMS) {
return TabCompletionHelper.getStringList(getTabCompletions(blacksmithSetting.getValueType()),
commandValue, TabCompletionHelper::filterMatchingContains);
}
List<String> returnValues = filterMatchingContains(
getTabCompletions(blacksmithSetting.getValueType()), commandValue);
if (blacksmithSetting == BlacksmithSetting.BASE_PRICE ||
blacksmithSetting == BlacksmithSetting.PRICE_PER_DURABILITY_POINT) {
returnValues.addAll(filterMatchingContains(getTabCompletions(SettingValueType.MATERIAL), commandValue));
} else if (globalSetting == GlobalSetting.ENCHANTMENT_COST) {
} else if (blacksmithSetting == BlacksmithSetting.ENCHANTMENT_COST) {
returnValues.addAll(filterMatchingContains(getTabCompletions(SettingValueType.ENCHANTMENT), commandValue));
}
return returnValues;

View File

@ -0,0 +1,32 @@
package net.knarcraft.blacksmith.command.blacksmith;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.command.EditCommand;
import net.knarcraft.blacksmith.config.Settings;
import net.knarcraft.blacksmith.config.blacksmith.BlacksmithSetting;
import net.knarcraft.blacksmith.trait.BlacksmithTrait;
import org.jetbrains.annotations.NotNull;
/**
* The main command used for blacksmith editing
*/
public class BlackSmithEditCommand extends EditCommand<BlacksmithTrait, BlacksmithSetting> {
/**
* Instantiates a new blacksmith edit command
*/
public BlackSmithEditCommand() {
super(BlacksmithTrait.class);
}
@Override
public BlacksmithSetting getSetting(String input) {
return BlacksmithSetting.getSetting(input);
}
@Override
public @NotNull Settings<BlacksmithSetting> getGlobalSettings() {
return BlacksmithPlugin.getInstance().getGlobalBlacksmithSettings();
}
}

View File

@ -1,8 +1,8 @@
package net.knarcraft.blacksmith.command;
package net.knarcraft.blacksmith.command.blacksmith;
import net.knarcraft.blacksmith.config.NPCSetting;
import net.knarcraft.blacksmith.config.blacksmith.BlacksmithSetting;
import net.knarcraft.blacksmith.util.TabCompleteValuesHelper;
import net.knarcraft.blacksmith.util.TabCompletionHelper;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
@ -17,23 +17,24 @@ import java.util.List;
*/
public class BlackSmithEditTabCompleter implements TabCompleter {
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("blacksmith.edit")) {
return new ArrayList<>();
}
List<String> npcSettings = new ArrayList<>();
for (NPCSetting setting : NPCSetting.values()) {
for (BlacksmithSetting setting : BlacksmithSetting.values()) {
if (setting.isPerNPC()) {
npcSettings.add(setting.getCommandName());
}
}
if (args.length == 1) {
return TabCompletionHelper.filterMatchingContains(npcSettings, args[0]);
} else {
if (npcSettings.contains(args[0]) && args.length <= 2) {
if (npcSettings.contains(args[0]) && args.length == 2) {
return tabCompleteCommandValues(args[0], args[1]);
} else {
return new ArrayList<>();
@ -48,14 +49,14 @@ public class BlackSmithEditTabCompleter implements TabCompleter {
* @param commandValue <p>The command value used to filter tab-completions</p>
* @return <p>Some valid options for the command's argument</p>
*/
private List<String> tabCompleteCommandValues(String commandName, String commandValue) {
for (NPCSetting npcSetting : NPCSetting.values()) {
if (npcSetting.getCommandName().equalsIgnoreCase(commandName)) {
private @Nullable List<String> tabCompleteCommandValues(@NotNull String commandName, @NotNull String commandValue) {
BlacksmithSetting setting = BlacksmithSetting.getSetting(commandName);
if (setting != null) {
return TabCompletionHelper.filterMatchingContains(TabCompleteValuesHelper.getTabCompletions(
npcSetting.getValueType()), commandValue);
}
}
setting.getValueType()), commandValue);
} else {
return null;
}
}
}

View File

@ -0,0 +1,53 @@
package net.knarcraft.blacksmith.command.scrapper;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.command.ReloadCommand;
import net.knarcraft.blacksmith.config.scrapper.GlobalScrapperSettings;
import net.knarcraft.blacksmith.config.scrapper.ScrapperSetting;
import net.knarcraft.blacksmith.util.ConfigCommandHelper;
import net.knarcraft.blacksmith.util.TypeValidationHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
/**
* The command used for changing global scrapper configuration options
*/
public class ScrapperConfigCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
if (args.length == 0) {
return false;
}
String commandName = args[0];
if (commandName.equalsIgnoreCase("reload")) {
return new ReloadCommand().onCommand(sender, command, label, args);
}
GlobalScrapperSettings settings = BlacksmithPlugin.getInstance().getGlobalScrapperSettings();
//Find which setting the user has specified, if any
ScrapperSetting detectedSetting = ScrapperSetting.getSetting(commandName);
if (detectedSetting == null) {
return false;
}
//Display the current value of a setting
if (args.length == 1) {
ConfigCommandHelper.displayCurrentValue(detectedSetting, settings, sender);
return true;
}
//Change the value of the specified setting
if (TypeValidationHelper.isValid(detectedSetting.getValueType(), args[1], sender)) {
ConfigCommandHelper.changeValue(args, detectedSetting, settings, sender);
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,85 @@
package net.knarcraft.blacksmith.command.scrapper;
import net.knarcraft.blacksmith.config.Setting;
import net.knarcraft.blacksmith.config.scrapper.ScrapperSetting;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.blacksmith.util.TabCompleteValuesHelper.getTabCompletions;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
/**
* The tab completer for the command used for changing global scrapper configuration options
*/
public class ScrapperConfigTabCompleter implements TabCompleter {
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String s, @NotNull String[] args) {
if (!sender.hasPermission("blacksmith.admin")) {
return new ArrayList<>();
}
//Arguments: <setting> [new value/material or enchantment] []
//Prevent tab-completion when typing messages with spaces
if (args.length > 2 && skipCompletionForSpacedMessage(args[0])) {
return new ArrayList<>();
}
if (args.length == 1) {
List<String> availableCommands = new ArrayList<>();
availableCommands.add("reload");
for (Setting setting : ScrapperSetting.values()) {
availableCommands.add(setting.getCommandName());
}
return filterMatchingContains(availableCommands, args[0]);
} else if (args.length == 2) {
return tabCompleteCommandValues(args[0], args[1]);
}
return null;
}
/**
* Checks whether to tab-complete nothing because a message containing spaces is being written
*
* @param command <p>The command specified by the user</p>
* @return <p>Null if not writing a spaced message</p>
*/
private boolean skipCompletionForSpacedMessage(@NotNull String command) {
Setting scrapperSetting = ScrapperSetting.getSetting(command);
return scrapperSetting != null && scrapperSetting.isMessage();
}
/**
* Tab completes the values available for the given command
*
* @param commandName <p>The name of the used command</p>
* @param commandValue <p>The command value used to filter tab-completions</p>
* @return <p>Some valid options for the command's argument</p>
*/
private List<String> tabCompleteCommandValues(@NotNull String commandName, @NotNull String commandValue) {
if (commandName.equalsIgnoreCase("reload")) {
return new ArrayList<>();
}
Setting scrapperSetting = ScrapperSetting.getSetting(commandName);
if (scrapperSetting != null) {
if (scrapperSetting == ScrapperSetting.SALVAGE_ABLE_ITEMS) {
return TabCompletionHelper.getStringList(getTabCompletions(scrapperSetting.getValueType()),
commandValue, TabCompletionHelper::filterMatchingContains);
}
return filterMatchingContains(getTabCompletions(scrapperSetting.getValueType()), commandValue);
} else {
return new ArrayList<>();
}
}
}

View File

@ -0,0 +1,32 @@
package net.knarcraft.blacksmith.command.scrapper;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.command.EditCommand;
import net.knarcraft.blacksmith.config.Settings;
import net.knarcraft.blacksmith.config.scrapper.ScrapperSetting;
import net.knarcraft.blacksmith.trait.ScrapperTrait;
import org.jetbrains.annotations.NotNull;
/**
* The main command used for scrapper editing
*/
public class ScrapperEditCommand extends EditCommand<ScrapperTrait, ScrapperSetting> {
/**
* Instantiates a new scrapper edit command
*/
public ScrapperEditCommand() {
super(ScrapperTrait.class);
}
@Override
public ScrapperSetting getSetting(String input) {
return ScrapperSetting.getSetting(input);
}
@Override
public @NotNull Settings<ScrapperSetting> getGlobalSettings() {
return BlacksmithPlugin.getInstance().getGlobalScrapperSettings();
}
}

View File

@ -0,0 +1,62 @@
package net.knarcraft.blacksmith.command.scrapper;
import net.knarcraft.blacksmith.config.scrapper.ScrapperSetting;
import net.knarcraft.blacksmith.util.TabCompleteValuesHelper;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The tab completer for the scrapper editing command
*/
public class ScrapperEditTabCompleter implements TabCompleter {
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String s, @NotNull String[] args) {
if (!sender.hasPermission("blacksmith.edit")) {
return new ArrayList<>();
}
List<String> npcSettings = new ArrayList<>();
for (ScrapperSetting setting : ScrapperSetting.values()) {
if (setting.isPerNPC()) {
npcSettings.add(setting.getCommandName());
}
}
if (args.length == 1) {
return TabCompletionHelper.filterMatchingContains(npcSettings, args[0]);
} else {
if (npcSettings.contains(args[0]) && args.length == 2) {
return tabCompleteCommandValues(args[0], args[1]);
} else {
return new ArrayList<>();
}
}
}
/**
* Tab completes the values available for the given command
*
* @param commandName <p>The name of the used command</p>
* @param commandValue <p>The command value used to filter tab-completions</p>
* @return <p>Some valid options for the command's argument</p>
*/
private @Nullable List<String> tabCompleteCommandValues(@NotNull String commandName, @NotNull String commandValue) {
ScrapperSetting setting = ScrapperSetting.getSetting(commandName);
if (setting != null) {
return TabCompletionHelper.filterMatchingContains(TabCompleteValuesHelper.getTabCompletions(
setting.getValueType()), commandValue);
} else {
return null;
}
}
}

View File

@ -1,109 +0,0 @@
package net.knarcraft.blacksmith.config;
import java.util.Arrays;
public enum GlobalSetting {
/**
* The base price for repairing, regardless of durability
*
* <p>This allows specifying a price for each item, by setting basePrice.item_name.</p>
*/
BASE_PRICE("global.basePrice.default", SettingValueType.POSITIVE_DOUBLE, 10.0, "basePrice"),
/**
* The base price for each durability point
*
* <p>If natural cost, this is the cost each missing durability point will add to the cost. If not natural cost,
* this is the cost each present durability point will add to the cost. This allows specifying a price per
* durability point value for each item, by setting pricePerDurabilityPoint.item_name</p>
*/
PRICE_PER_DURABILITY_POINT("global.pricePerDurabilityPoint.default", SettingValueType.POSITIVE_DOUBLE,
0.005, "pricePerDurabilityPoint"),
/**
* The price increase for each level of each present enchantment
*
* <p>This can be specified for each possible enchantment by setting enchantment-cost.enchantment_name</p>
*/
ENCHANTMENT_COST("global.enchantmentCost.default", SettingValueType.POSITIVE_DOUBLE, 5.0, "enchantmentCost"),
/**
* Whether the cost should increase for damage taken, as opposed to increase for durability present
*/
NATURAL_COST("global.useNaturalCost", SettingValueType.BOOLEAN, true, "useNaturalCost"),
/**
* Whether to show exact time when displaying the wait time for a reforging or the cool-down
*/
SHOW_EXACT_TIME("global.showExactTime", SettingValueType.BOOLEAN, false, "showExactTime");
private final String path;
private final String parent;
private final String commandName;
private final Object value;
private final SettingValueType valueType;
/**
* Instantiates a new setting
*
* @param path <p>The full config path for this setting</p>
* @param valueType <p>The type of value used by this setting</p>
* @param value <p>The default value of this setting</p>
* @param commandName <p>The name of the command used to change this setting</p>
*/
GlobalSetting(String path, SettingValueType valueType, Object value, String commandName) {
this.path = path;
this.value = value;
this.commandName = commandName;
this.valueType = valueType;
String[] pathParts = path.split("\\.");
this.parent = String.join(".", Arrays.copyOfRange(pathParts, 0, pathParts.length - 1));
}
/**
* Gets the full config path for this setting
*
* @return <p>The full config path for this setting</p>
*/
public String getPath() {
return path;
}
/**
* Gets the parent item of the defined path
*
* @return <p>The parent node</p>
*/
public String getParent() {
return parent;
}
/**
* Gets the value of this setting
*
* @return <p>The value of this setting</p>
*/
public Object getDefaultValue() {
return value;
}
/**
* The name of the command used to change this setting
*
* @return <p>The name of this setting's command</p>
*/
public String getCommandName() {
return commandName;
}
/**
* Gets the value type for this setting
*
* @return <p>The value type for this setting</p>
*/
public SettingValueType getValueType() {
return this.valueType;
}
}

View File

@ -1,436 +0,0 @@
package net.knarcraft.blacksmith.config;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.api.util.YamlStorage;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.util.ConfigHelper;
import net.knarcraft.blacksmith.util.InputParsingHelper;
import org.bukkit.Material;
import org.bukkit.enchantments.Enchantment;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
/**
* A class which keeps track of all default NPC settings and all global settings
*/
public class GlobalSettings {
private final Map<Material, Double> materialBasePrices = new HashMap<>();
private final Map<Material, Double> materialPricePerDurabilityPoints = new HashMap<>();
private final Map<Enchantment, Double> enchantmentCosts = new HashMap<>();
private final Map<NPCSetting, Object> defaultNPCSettings = new HashMap<>();
private final Map<GlobalSetting, Object> globalSettings = new HashMap<>();
private final YamlStorage defaultConfig;
/**
* Instantiates a new "Settings"
*
* @param plugin <p>A reference to the blacksmith plugin</p>
*/
public GlobalSettings(BlacksmithPlugin plugin) {
defaultConfig = new YamlStorage(new File(plugin.getDataFolder() + File.separator + "config.yml"),
"Blacksmith Configuration\nWarning: The values under defaults are the values set for a " +
"blacksmith upon creation. To change any values for existing NPCs, edit the citizens NPC file.");
}
/**
* Loads all configuration values from the config file
*/
public void load() {
//Load the config from disk
defaultConfig.load();
DataKey root = defaultConfig.getKey("");
//Just in case, clear existing values
defaultNPCSettings.clear();
globalSettings.clear();
materialBasePrices.clear();
materialPricePerDurabilityPoints.clear();
enchantmentCosts.clear();
//Load/Save NPC default settings
loadDefaultNPCSettings(root);
//Load/Save global settings
loadGlobalSettings(root);
//Save any modified values to disk
defaultConfig.save();
}
/**
* Changes the value of the given setting
*
* @param globalSetting <p>The global setting to change</p>
* @param newValue <p>The new value of the setting</p>
*/
public void changeValue(GlobalSetting globalSetting, Object newValue) {
globalSettings.put(globalSetting, newValue);
save();
}
/**
* Changes the value of the given setting
*
* @param npcSetting <p>The default NPC setting to change</p>
* @param newValue <p>The new value for the setting</p>
*/
public void changeValue(NPCSetting npcSetting, Object newValue) {
if (npcSetting.getValueType() == SettingValueType.STRING_LIST) {
//Workaround to make sure it's treated as the correct type
defaultNPCSettings.put(npcSetting, ConfigHelper.asStringList(newValue));
} else {
defaultNPCSettings.put(npcSetting, newValue);
}
save();
}
/**
* Gets the current raw value of the given global setting
*
* @param globalSetting <p>The setting to get</p>
* @return <p>The current raw setting value</p>
*/
public Object getRawValue(GlobalSetting globalSetting) {
return globalSettings.get(globalSetting);
}
/**
* Gets the current raw value of the given default NPC setting
*
* @param npcSetting <p>The setting to get</p>
* @return <p>The current raw setting value</p>
*/
public Object getRawValue(NPCSetting npcSetting) {
return defaultNPCSettings.get(npcSetting);
}
/**
* Sets the enchantment cost for the given enchantment
*
* @param enchantment <p>The enchantment to set the enchantment cost for</p>
* @param newEnchantmentCost <p>The new enchantment cost</p>
*/
public void setEnchantmentCost(Enchantment enchantment, double newEnchantmentCost) {
if (enchantment == null) {
if (newEnchantmentCost < 0) {
throw new IllegalArgumentException("Enchantment cost cannot be negative!");
}
globalSettings.put(GlobalSetting.ENCHANTMENT_COST, newEnchantmentCost);
} else {
if (newEnchantmentCost < 0) {
enchantmentCosts.put(enchantment, null);
} else {
enchantmentCosts.put(enchantment, newEnchantmentCost);
}
}
save();
}
/**
* Sets the price per durability point for the given material
*
* @param material <p>The material to set the price per durability point price for</p>
* @param newPrice <p>The new price per durability point price</p>
*/
public void setPricePerDurabilityPoint(Material material, double newPrice) {
if (material == null) {
if (newPrice < 0) {
throw new IllegalArgumentException("Price per durability point cannot be negative!");
}
globalSettings.put(GlobalSetting.PRICE_PER_DURABILITY_POINT, newPrice);
} else {
//Use a negative price to unset the per-item value
if (newPrice < 0) {
materialPricePerDurabilityPoints.put(material, null);
} else {
materialPricePerDurabilityPoints.put(material, newPrice);
}
}
save();
}
/**
* Sets the base price for the given material
*
* @param material <p>The material to set the base price for</p>
* @param newBasePrice <p>The new base price</p>
*/
public void setBasePrice(Material material, double newBasePrice) {
if (material == null) {
if (newBasePrice < 0) {
throw new IllegalArgumentException("Base price cannot be negative!");
}
globalSettings.put(GlobalSetting.BASE_PRICE, newBasePrice);
} else {
//Use a negative price to unset the per-item value
if (newBasePrice < 0) {
materialBasePrices.put(material, null);
} else {
materialBasePrices.put(material, newBasePrice);
}
}
save();
}
/**
* Gets the current value of the default NPC settings
*
* @return <p>The current value of the default NPC settings</p>
*/
public Map<NPCSetting, Object> getDefaultNPCSettings() {
return new HashMap<>(this.defaultNPCSettings);
}
/**
* Gets whether to use natural cost for cost calculation
*
* <p>Natural cost makes it more costly the more damage is dealt to an item. The alternative is the legacy behavior
* where the amount of durability points remaining increases the cost.</p>
*
* @return <p>Whether to use natural cost</p>
*/
public boolean getUseNaturalCost() {
return asBoolean(GlobalSetting.NATURAL_COST);
}
/**
* Gets whether to show exact time for reforging wait-time, and for wait-time between sessions
*
* @return <p>Whether to show exact time</p>
*/
public boolean getShowExactTime() {
return asBoolean(GlobalSetting.SHOW_EXACT_TIME);
}
/**
* Gets the base price for the given material
*
* @param material <p>The material to get the base price for</p>
* @return <p>The base price for the material</p>
*/
public double getBasePrice(Material material) {
if (materialBasePrices.containsKey(material) && materialBasePrices.get(material) != null) {
return materialBasePrices.get(material);
} else {
return asDouble(GlobalSetting.BASE_PRICE);
}
}
/**
* Gets the price per durability point for the given material
*
* @param material <p>The material to get the durability point price for</p>
* @return <p>The durability point price for the material</p>
*/
public double getPricePerDurabilityPoint(Material material) {
if (materialPricePerDurabilityPoints.containsKey(material) &&
materialPricePerDurabilityPoints.get(material) != null) {
return materialPricePerDurabilityPoints.get(material);
} else {
return asDouble(GlobalSetting.PRICE_PER_DURABILITY_POINT);
}
}
/**
* Gets the cost to be added for each level of the given enchantment
*
* @param enchantment <p>The enchantment to get the cost for</p>
* @return <p>The cost of each enchantment level</p>
*/
public double getEnchantmentCost(Enchantment enchantment) {
if (enchantmentCosts.containsKey(enchantment) && enchantmentCosts.get(enchantment) != null) {
return enchantmentCosts.get(enchantment);
} else {
return asDouble(GlobalSetting.ENCHANTMENT_COST);
}
}
/**
* Gets the given value as a boolean
*
* <p>This will throw an exception if used for a non-boolean value</p>
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as a boolean</p>
*/
public boolean asBoolean(GlobalSetting setting) {
return ConfigHelper.asBoolean(getValue(setting));
}
/**
* Gets the given value as a double
*
* <p>This will throw an exception if used for a non-double setting</p>
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as a double</p>
*/
public double asDouble(GlobalSetting setting) {
return ConfigHelper.asDouble(getValue(setting));
}
/**
* Gets the value of a setting, using the default if not set
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The current value</p>
*/
private Object getValue(GlobalSetting setting) {
Object value = globalSettings.get(setting);
//If not set in config.yml, use the default value from the enum
if (value == null) {
value = setting.getDefaultValue();
}
return value;
}
/**
* Loads all global settings
*
* @param root <p>The root node of all global settings</p>
*/
private void loadGlobalSettings(DataKey root) {
for (GlobalSetting globalSetting : GlobalSetting.values()) {
if (!root.keyExists(globalSetting.getPath())) {
//If the setting does not exist in the config file, add it
root.setRaw(globalSetting.getPath(), globalSetting.getDefaultValue());
} else {
//Set the setting to the value found in the path
globalSettings.put(globalSetting, root.getRaw(globalSetting.getPath()));
}
}
//Load all base prices
DataKey basePriceNode = root.getRelative(GlobalSetting.BASE_PRICE.getParent());
Map<String, String> relevantKeys = getRelevantKeys(basePriceNode);
for (String key : relevantKeys.keySet()) {
String materialName = relevantKeys.get(key);
Material material = InputParsingHelper.matchMaterial(materialName);
if (material != null) {
materialBasePrices.put(material, basePriceNode.getDouble(key));
} else {
BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING,
"Unable to find a material matching " + materialName);
}
}
//Load all per-durability-point prices
DataKey basePerDurabilityPriceNode = root.getRelative(GlobalSetting.PRICE_PER_DURABILITY_POINT.getParent());
relevantKeys = getRelevantKeys(basePerDurabilityPriceNode);
for (String key : relevantKeys.keySet()) {
String materialName = relevantKeys.get(key);
Material material = InputParsingHelper.matchMaterial(materialName);
if (material != null) {
materialPricePerDurabilityPoints.put(material, basePerDurabilityPriceNode.getDouble(key));
} else {
BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING,
"Unable to find a material matching " + materialName);
}
}
//Load all enchantment prices
DataKey enchantmentCostNode = root.getRelative(GlobalSetting.ENCHANTMENT_COST.getParent());
relevantKeys = getRelevantKeys(basePerDurabilityPriceNode);
for (String key : relevantKeys.keySet()) {
String enchantmentName = relevantKeys.get(key);
Enchantment enchantment = InputParsingHelper.matchEnchantment(enchantmentName);
if (enchantment != null) {
enchantmentCosts.put(enchantment, enchantmentCostNode.getDouble(key));
} else {
BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING,
"Unable to find an enchantment matching " + enchantmentName);
}
}
}
/**
* Gets a map between relevant keys and their normalized name
*
* @param rootKey <p>The root data key containing sub-keys</p>
* @return <p>Any sub-keys found that aren't the default</p>
*/
private Map<String, String> getRelevantKeys(DataKey rootKey) {
Map<String, String> relevant = new HashMap<>();
for (DataKey dataKey : rootKey.getSubKeys()) {
String keyName = dataKey.name();
//Skip the default value
if (keyName.equals("default")) {
continue;
}
String normalizedName = keyName.toUpperCase().replace("-", "_");
relevant.put(keyName, normalizedName);
}
return relevant;
}
/**
* Converts a normalized material name to the format used in the config file
*
* @param normalizedName <p>The normalized name to un-normalize</p>
* @return <p>The un-normalized name</p>
*/
private String unNormalizeName(String normalizedName) {
return normalizedName.toLowerCase().replace("_", "-");
}
/**
* Loads all default NPC settings
*
* @param root <p>The root node of all default NPC settings</p>
*/
private void loadDefaultNPCSettings(DataKey root) {
for (NPCSetting setting : NPCSetting.values()) {
if (!root.keyExists(setting.getPath())) {
//If the setting does not exist in the config file, add it
root.setRaw(setting.getPath(), setting.getDefaultValue());
} else {
//Set the setting to the value found in the path
defaultNPCSettings.put(setting, root.getRaw(setting.getPath()));
}
}
}
/**
* Saves all current settings to the config file
*/
private void save() {
DataKey root = defaultConfig.getKey("");
//Save all default NPC settings
for (NPCSetting setting : NPCSetting.values()) {
root.setRaw(setting.getPath(), defaultNPCSettings.get(setting));
}
//Save all normal global settings
for (GlobalSetting globalSetting : GlobalSetting.values()) {
root.setRaw(globalSetting.getPath(), globalSettings.get(globalSetting));
}
//Save all base prices
DataKey basePriceNode = root.getRelative(GlobalSetting.BASE_PRICE.getParent());
for (Material material : materialBasePrices.keySet()) {
basePriceNode.setRaw(unNormalizeName(material.name()), materialBasePrices.get(material));
}
//Save all per-durability-point prices
DataKey basePerDurabilityPriceNode = root.getRelative(GlobalSetting.PRICE_PER_DURABILITY_POINT.getParent());
for (Material material : materialPricePerDurabilityPoints.keySet()) {
basePerDurabilityPriceNode.setRaw(unNormalizeName(material.name()), materialPricePerDurabilityPoints.get(material));
}
//Load all enchantment prices
DataKey enchantmentCostNode = root.getRelative(GlobalSetting.ENCHANTMENT_COST.getParent());
for (Enchantment enchantment : enchantmentCosts.keySet()) {
enchantmentCostNode.setRaw(unNormalizeName(enchantment.getKey().getKey()), enchantmentCosts.get(enchantment));
}
//Perform the actual save to disk
defaultConfig.save();
}
}

View File

@ -1,203 +0,0 @@
package net.knarcraft.blacksmith.config;
/**
* An enum representing all of Blacksmith's settings
*/
public enum NPCSetting {
/**
* The setting for whether the NPC should drop an item to the ground when finished
*
* <p>If set to false, the item will be directly put in the player's inventory instead</p>
*/
DROP_ITEM("dropItem", SettingValueType.BOOLEAN, true, "dropItem"),
/**
* The setting for the chance of a reforging to fail
*/
FAIL_CHANCE("failReforgeChance", SettingValueType.PERCENTAGE, 10, "failReforgeChance"),
/**
* The setting for the chance of an additional enchantment being added
*/
EXTRA_ENCHANTMENT_CHANCE("extraEnchantmentChance", SettingValueType.PERCENTAGE, 5,
"extraEnchantmentChance"),
/**
* The setting for the maximum amount of enchantments that can be added to an item
*/
MAX_ENCHANTMENTS("maxEnchantments", SettingValueType.POSITIVE_INTEGER, 3, "maxEnchantments"),
/**
* The maximum amount of seconds a player may need to wait for the reforging to finish
*/
MAX_REFORGE_DELAY("delaysInSeconds.maximum", SettingValueType.POSITIVE_INTEGER, 30, "maxReforgeDelay"),
/**
* The minimum amount of seconds a player may need to wait for the reforging to finish
*/
MIN_REFORGE_DELAY("delaysInSeconds.minimum", SettingValueType.POSITIVE_INTEGER, 5, "minReforgeDelay"),
/**
* The setting for number of seconds a player has to wait between each usage of the blacksmith
*/
REFORGE_COOL_DOWN("delaysInSeconds.reforgeCoolDown", SettingValueType.POSITIVE_INTEGER, 60, "reforgeCoolDown"),
/**
* The setting for which items the blacksmith is able to reforge
*/
REFORGE_ABLE_ITEMS("reforgeAbleItems", SettingValueType.REFORGE_ABLE_ITEMS, "", "reforgeAbleItems"),
/**
* The setting for the title used to display which kind of blacksmith the NPC is
*
* <p>While this should be entirely configurable, values such as armor-smith, sword-smith and similar, which
* describe the blacksmith's specialization, and thus the range of reforge-able items, is expected.</p>
*/
BLACKSMITH_TITLE("blacksmithTitle", SettingValueType.STRING, "blacksmith", "blacksmithTitle"),
/**
* The setting for the enchantments a blacksmith cannot apply to items
*/
ENCHANTMENT_BLOCKLIST("enchantmentBlocklist", SettingValueType.STRING_LIST, new String[]{"binding_curse",
"mending", "vanishing_curse"}, "enchantmentBlocklist"),
/*-----------
| Messages |
-----------*/
/**
* The message displayed when the blacksmith is busy with another player
*/
BUSY_WITH_PLAYER_MESSAGE("messages.busyPlayerMessage", SettingValueType.STRING,
"&cI'm busy at the moment. Come back later!", "busyPlayerMessage"),
/**
* The message displayed when the blacksmith is already reforging something for the player
*/
BUSY_WITH_REFORGE_MESSAGE("messages.busyReforgeMessage", SettingValueType.STRING,
"&cI'm working on it. Be patient! I'll finish {time}!", "busyReforgeMessage"),
/**
* The message displayed if the player has to wait for the cool-down to expire
*/
COOL_DOWN_UNEXPIRED_MESSAGE("messages.coolDownUnexpiredMessage", SettingValueType.STRING,
"&cYou've already had your chance! Give me a break! I'll be ready {time}!",
"coolDownUnexpiredMessage"),
/**
* The message displayed when displaying the cost of reforging the held item to the player
*/
COST_MESSAGE("messages.costMessage", SettingValueType.STRING,
"&eIt will cost &a{cost}&e to reforge that &a{item}&e! Click again to reforge!", "costMessage"),
/**
* The message displayed if the blacksmith fails reforging an item
*/
FAIL_MESSAGE("messages.failReforgeMessage", SettingValueType.STRING,
"&cWhoops! Didn't mean to do that! Maybe next time?", "failReforgeMessage"),
/**
* The message displayed if a player is unable to pay the blacksmith
*/
INSUFFICIENT_FUNDS_MESSAGE("messages.insufficientFundsMessage", SettingValueType.STRING,
"&cYou don't have enough money to reforge that item!", "insufficientFundsMessage"),
/**
* The message displayed if the blacksmith encounters an item they cannot reforge
*/
INVALID_ITEM_MESSAGE("messages.invalidItemMessage", SettingValueType.STRING,
"&cI'm sorry, but I'm a/an {title}, I don't know how to reforge that!", "invalidItemMessage"),
/**
* The message displayed if a player presents a different item after seeing the price to reforge an item
*/
ITEM_UNEXPECTEDLY_CHANGED_MESSAGE("messages.itemChangedMessage", SettingValueType.STRING,
"&cThat's not the item you wanted to reforge before!", "itemChangedMessage"),
/**
* The message displayed when the blacksmith starts reforging an item
*/
START_REFORGE_MESSAGE("messages.startReforgeMessage", SettingValueType.STRING,
"&eOk, let's see what I can do...", "startReforgeMessage"),
/**
* The message displayed when the blacksmith successfully finishes reforging an item
*/
SUCCESS_MESSAGE("messages.successMessage", SettingValueType.STRING,
"There you go! All better!", "successMessage"),
/**
* The message displayed when trying to reforge an item with full durability
*/
NOT_DAMAGED_MESSAGE("messages.notDamagedMessage", SettingValueType.STRING,
"&cThat item is not in need of repair", "notDamagedMessage");
private final String path;
private final String childPath;
private final Object value;
private final String commandName;
private final SettingValueType valueType;
/**
* Instantiates a new setting
*
* @param path <p>The full config path for this setting</p>
* @param valueType <p>The type of value used by this setting</p>
* @param value <p>The default value of this setting</p>
* @param commandName <p>The name of the command used to change this setting</p>
*/
NPCSetting(String path, SettingValueType valueType, Object value, String commandName) {
this.path = "defaults." + path;
this.value = value;
this.valueType = valueType;
this.childPath = path;
this.commandName = commandName;
}
/**
* Gets the full config path for this setting
*
* @return <p>The full config path for this setting</p>
*/
public String getPath() {
return path;
}
/**
* Gets the config path without the root node
*
* @return <p>The config path without the root node</p>
*/
public String getChildPath() {
return childPath;
}
/**
* Gets the value of this setting
*
* @return <p>The value of this setting</p>
*/
public Object getDefaultValue() {
return value;
}
/**
* The name of the command used to change this setting
*
* @return <p>The name of this setting's command</p>
*/
public String getCommandName() {
return commandName;
}
/**
* Gets the value type for this setting
*
* @return <p>The value type for this setting</p>
*/
public SettingValueType getValueType() {
return this.valueType;
}
}

View File

@ -0,0 +1,75 @@
package net.knarcraft.blacksmith.config;
import org.jetbrains.annotations.NotNull;
/**
* An interface describing a setting
*/
@SuppressWarnings("unused")
public interface Setting {
/**
* Gets the full config path for this setting
*
* @return <p>The full config path for this setting</p>
*/
@NotNull
String getPath();
/**
* Gets the config path without the root node
*
* @return <p>The config path without the root node</p>
*/
@NotNull
String getChildPath();
/**
* Gets the value of this setting
*
* @return <p>The value of this setting</p>
*/
@NotNull
Object getDefaultValue();
/**
* The name of the command used to change this setting
*
* @return <p>The name of this setting's command</p>
*/
@NotNull
String getCommandName();
/**
* Gets the value type for this setting
*
* @return <p>The value type for this setting</p>
*/
@NotNull
SettingValueType getValueType();
/**
* Gets the description explaining the usage of this setting
*
* @return <p>This setting's description</p>
*/
@NotNull
String getDescription();
/**
* Gets whether this setting can be set per-NPC, or if it's set globally
*
* @return <p>True if this setting is set per-NPC</p>
*/
boolean isPerNPC();
/**
* Gets whether this setting is a customizable message
*
* <p>Messages are a special case, as you generally want to see the raw formatting, not just the result.</p>
*
* @return <p>True if this setting is a customizable message</p>
*/
boolean isMessage();
}

View File

@ -50,4 +50,9 @@ public enum SettingValueType {
*/
REFORGE_ABLE_ITEMS,
/**
* A list of enchantments
*/
ENCHANTMENT_LIST,
}

View File

@ -0,0 +1,30 @@
package net.knarcraft.blacksmith.config;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* An interface describing an object for managing settings
*
* @param <K> <p>The type of setting managed</p>
*/
public interface Settings<K extends Setting> {
/**
* Changes the value of the given setting
*
* @param setting <p>The setting to change</p>
* @param newValue <p>The new value of the setting</p>
*/
void changeValue(@NotNull K setting, @Nullable Object newValue);
/**
* Gets the current raw value of the given global setting
*
* @param setting <p>The setting to get</p>
* @return <p>The current raw setting value</p>
*/
@NotNull
Object getRawValue(@NotNull K setting);
}

View File

@ -1,17 +1,29 @@
package net.knarcraft.blacksmith.config;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.util.ItemHelper;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.Map;
import java.util.Set;
/**
* A representation of the presets for different kinds of smiths
*/
public enum SmithPreset {
/**
* A blacksmith capable of re-forging everything
*/
BLACKSMITH(new SmithPresetFilter[]{SmithPresetFilter.GOLD, SmithPresetFilter.IRON, SmithPresetFilter.DIAMOND,
SmithPresetFilter.NETHERITE}),
/**
* A blacksmith capable of re-forging all weapons (including shields)
*/
@ -32,13 +44,19 @@ public enum SmithPreset {
SmithPresetFilter.AXE, SmithPresetFilter.HOE, SmithPresetFilter.SHOVEL, SmithPresetFilter.MISC});
private final SmithPresetFilter[] filters;
private static final Map<SmithPreset, Set<String>> presetMaterialNames = new HashMap<>();
private static final Map<SmithPreset, Map<SmithPresetFilter, Set<String>>> filterMaterialNames = new HashMap<>();
private static Set<Material> armor = null;
private static Set<Material> ranged = null;
private static Set<Material> weapons = null;
private static Set<Material> tools = null;
/**
* Instantiates a new smith preset
*
* @param filters <p>The filters applicable to this preset</p>
*/
SmithPreset(SmithPresetFilter[] filters) {
SmithPreset(@NotNull SmithPresetFilter[] filters) {
this.filters = filters;
}
@ -48,7 +66,7 @@ public enum SmithPreset {
* @param filter <p>The filter to check</p>
* @return <p>True if the filter is supported</p>
*/
public boolean supportsFilter(SmithPresetFilter filter) {
public boolean supportsFilter(@NotNull SmithPresetFilter filter) {
return List.of(filters).contains(filter);
}
@ -57,6 +75,7 @@ public enum SmithPreset {
*
* @return <p>The filters supported by this preset</p>
*/
@NotNull
public List<SmithPresetFilter> getSupportedFilters() {
return List.of(filters);
}
@ -66,6 +85,7 @@ public enum SmithPreset {
*
* @return <p>All available smith presets</p>
*/
@NotNull
public static List<String> getPresetNames() {
List<String> presetNames = new ArrayList<>();
for (SmithPreset preset : SmithPreset.values()) {
@ -80,12 +100,25 @@ public enum SmithPreset {
* @param possiblePreset <p>The string that might be a preset</p>
* @return <p>The string, possibly with the preset replaced</p>
*/
public static String replacePreset(String possiblePreset) {
@NotNull
public static String replacePreset(@NotNull String possiblePreset) {
boolean negated = false;
String upperCasedPreset = possiblePreset.replace('-', '_').toUpperCase();
if (!upperCasedPreset.startsWith("PRESET:")) {
if (possiblePreset.startsWith("-")) {
negated = true;
}
if ((negated && !upperCasedPreset.startsWith("_PRESET:")) ||
(!negated && !upperCasedPreset.startsWith("PRESET:"))) {
return possiblePreset;
}
//Strip the "-" here to prevent stripping for material names
if (negated) {
upperCasedPreset = upperCasedPreset.substring(1);
}
//Parse the input
SmithPresetFilter filter = null;
SmithPreset preset;
@ -99,17 +132,38 @@ public enum SmithPreset {
} catch (IllegalArgumentException exception) {
/* This case means that either the preset or the filter given is invalid, and thus the preset string should
be ignored to prevent any problems. */
BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING, String.format("The smith preset %s is " +
"invalid, and will be ignored. Please fix it!", possiblePreset));
BlacksmithPlugin.warn(String.format("The smith preset %s is invalid, and will be ignored. Please fix it!",
possiblePreset));
return "";
}
//Return the list of materials included in the preset
if (filter != null) {
return String.join(",", preset.getMaterialNames(filter));
} else {
return String.join(",", preset.getMaterialNames());
// Check if the preset result has been stored
Set<String> materialNames = null;
if (filter == null) {
if (presetMaterialNames.containsKey(preset)) {
materialNames = presetMaterialNames.get(preset);
}
} else {
if (filterMaterialNames.containsKey(preset) && filterMaterialNames.get(preset).containsKey(filter)) {
materialNames = filterMaterialNames.get(preset).get(filter);
}
}
//Return the list of materials included in the preset
if (materialNames == null) {
if (filter != null) {
materialNames = preset.getMaterialNames(filter);
filterMaterialNames.putIfAbsent(preset, new HashMap<>());
filterMaterialNames.get(preset).put(filter, materialNames);
} else {
materialNames = preset.getMaterialNames();
presetMaterialNames.put(preset, materialNames);
}
}
if (negated) {
materialNames = negateMaterials(materialNames);
}
return String.join(",", materialNames);
}
/**
@ -118,8 +172,9 @@ public enum SmithPreset {
* @param filter <p>The filter to use for filtering</p>
* @return <p>The materials included in this preset, filtered using the given filter</p>
*/
public List<Material> getFilteredMaterials(SmithPresetFilter filter) {
List<Material> materials = new ArrayList<>(this.getMaterials());
@NotNull
public Set<Material> getFilteredMaterials(SmithPresetFilter filter) {
Set<Material> materials = new HashSet<>(this.getMaterials());
materials.removeIf((item) -> !filter.isIncluded(item));
return materials;
}
@ -129,8 +184,10 @@ public enum SmithPreset {
*
* @return <p>All materials in this preset</p>
*/
public List<Material> getMaterials() {
@NotNull
public Set<Material> getMaterials() {
return switch (this) {
case BLACKSMITH -> ItemHelper.getAllReforgeAbleMaterials();
case WEAPON_SMITH -> getWeapons();
case ARMOR_SMITH -> getArmor();
case TOOL_SMITH -> getTools();
@ -142,11 +199,14 @@ public enum SmithPreset {
*
* @return <p>All ranged weapon materials</p>
*/
private List<Material> getRanged() {
List<Material> ranged = new ArrayList<>();
@NotNull
private Set<Material> getRanged() {
if (ranged == null) {
ranged = new HashSet<>();
ranged.add(Material.TRIDENT);
ranged.add(Material.BOW);
ranged.add(Material.CROSSBOW);
}
return ranged;
}
@ -155,15 +215,18 @@ public enum SmithPreset {
*
* @return <p>All tool materials</p>
*/
private List<Material> getTools() {
List<Material> tools = new ArrayList<>();
tools.addAll(getMaterialsEndingWith("_HOE"));
tools.addAll(getMaterialsEndingWith("_SHOVEL"));
tools.addAll(getMaterialsEndingWith("_AXE"));
tools.addAll(getMaterialsEndingWith("_PICKAXE"));
@NotNull
private Set<Material> getTools() {
if (tools == null) {
tools = new HashSet<>();
tools.addAll(Tag.ITEMS_HOES.getValues());
tools.addAll(Tag.ITEMS_SHOVELS.getValues());
tools.addAll(Tag.ITEMS_AXES.getValues());
tools.addAll(Tag.ITEMS_PICKAXES.getValues());
tools.add(Material.FLINT_AND_STEEL);
tools.add(Material.FISHING_ROD);
tools.add(Material.SHEARS);
}
return tools;
}
@ -172,10 +235,13 @@ public enum SmithPreset {
*
* @return <p>All weapon materials</p>
*/
private List<Material> getWeapons() {
List<Material> weapons = new ArrayList<>(getSwords());
@NotNull
private Set<Material> getWeapons() {
if (weapons == null) {
weapons = new HashSet<>(getSwords());
weapons.addAll(getRanged());
weapons.add(Material.SHIELD);
}
return weapons;
}
@ -184,17 +250,26 @@ public enum SmithPreset {
*
* @return <p>All sword materials</p>
*/
private List<Material> getSwords() {
return getMaterialsEndingWith("_SWORD");
@NotNull
private Set<Material> getSwords() {
return Tag.ITEMS_SWORDS.getValues();
}
private List<Material> getArmor() {
List<Material> armor = new ArrayList<>();
/**
* Gets all types of armor
*
* @return <p>All armor types</p>
*/
@NotNull
private Set<Material> getArmor() {
if (armor == null) {
armor = new HashSet<>();
armor.addAll(getMaterialsEndingWith("HELMET"));
armor.addAll(getMaterialsEndingWith("CHESTPLATE"));
armor.addAll(getMaterialsEndingWith("LEGGINGS"));
armor.addAll(getMaterialsEndingWith("BOOTS"));
armor.add(Material.ELYTRA);
}
return armor;
}
@ -204,22 +279,24 @@ public enum SmithPreset {
* @param end <p>The string to look for</p>
* @return <p>The resulting materials</p>
*/
private List<Material> getMaterialsEndingWith(String end) {
List<Material> swords = new ArrayList<>();
for (Material material : Material.values()) {
@NotNull
private Set<Material> getMaterialsEndingWith(@NotNull String end) {
Set<Material> matchedMaterials = new HashSet<>();
for (Material material : ItemHelper.getAllReforgeAbleMaterials()) {
if (!material.name().startsWith("LEGACY") && material.name().endsWith(end)) {
swords.add(material);
matchedMaterials.add(material);
}
}
return swords;
return matchedMaterials;
}
/**
* Gets material names of all materials reforge-able by this smith
* Gets material names of all materials reforge-able by this smith preset
*
* @return <p>All material names for this smith</p>
* @return <p>All material names for this smith preset</p>
*/
private List<String> getMaterialNames() {
@NotNull
private Set<String> getMaterialNames() {
return getNames(this.getMaterials());
}
@ -229,7 +306,8 @@ public enum SmithPreset {
* @param filter <p>The filter used for filtering materials</p>
* @return <p>All material names for this smith</p>
*/
private List<String> getMaterialNames(SmithPresetFilter filter) {
@NotNull
private Set<String> getMaterialNames(@NotNull SmithPresetFilter filter) {
return getNames(this.getFilteredMaterials(filter));
}
@ -239,12 +317,30 @@ public enum SmithPreset {
* @param materials <p>The materials to get the names of</p>
* @return <p>The names of the materials</p>
*/
private List<String> getNames(List<Material> materials) {
List<String> items = new ArrayList<>();
@NotNull
private Set<String> getNames(@NotNull Set<Material> materials) {
Set<String> items = new HashSet<>();
for (Material material : materials) {
items.add(material.name().toLowerCase().replace("_", "-"));
}
return items;
}
/**
* Negates the given material names
*
* @param materials <p>The material names to negate</p>
* @return <p>The negated material names</p>
*/
@NotNull
private static Set<String> negateMaterials(@NotNull Set<String> materials) {
Set<String> negatedMaterials = new HashSet<>(materials.size());
materials.forEach((material) -> {
if (material != null && !material.isBlank()) {
negatedMaterials.add("-" + material);
}
});
return negatedMaterials;
}
}

View File

@ -93,7 +93,7 @@ public enum SmithPresetFilter {
HELMET(false, "_HELMET"),
/**
* Filters to only include chestplates
* Filters to only include chest-plates
*/
CHESTPLATE(false, "_CHESTPLATE"),

View File

@ -0,0 +1,246 @@
package net.knarcraft.blacksmith.config;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A YAML configuration which retains all comments
*
* <p>This configuration converts all comments to YAML values when loaded, which all start with comment_. When saved,
* those YAML values are converted to normal text comments. This ensures that the comments aren't removed by the
* YamlConfiguration during its parsing.</p>
*
* <p>Note: When retrieving a configuration section, that will include comments that start with "comment_". You should
* filter those before parsing input.</p>
*
* @author Kristian Knarvik
* @author Thorin
*/
public class StargateYamlConfiguration extends YamlConfiguration {
private static final String START_OF_COMMENT_LINE = "[HASHTAG]";
private static final String END_OF_COMMENT = "_endOfComment_";
private static final String START_OF_COMMENT = "comment_";
@Override
@NotNull
@SuppressWarnings("deprecation")
protected String buildHeader() {
return "";
}
@Override
@NotNull
public String saveToString() {
// Convert YAML comments to normal comments
return this.convertYAMLMappingsToComments(super.saveToString());
}
@Override
public void loadFromString(@NotNull String contents) throws InvalidConfigurationException {
// Convert normal comments to YAML comments to prevent them from disappearing
super.loadFromString(this.convertCommentsToYAMLMappings(contents));
}
/**
* Gets a configuration section's keys, without any comment entries
*
* @param configurationSection <p>The configuration section to get keys for</p>
* @param deep <p>Whether to get keys for child elements as well</p>
* @return <p>The configuration section's keys, with comment entries removed</p>
*/
public static Set<String> getKeysWithoutComments(@NotNull ConfigurationSection configurationSection, boolean deep) {
Set<String> keys = new HashSet<>(configurationSection.getKeys(deep));
keys.removeIf(key -> key.matches(START_OF_COMMENT + "[0-9]+"));
return keys;
}
/**
* Reads a file with comments, and recreates them into yaml mappings
*
* <p>A mapping follows this format: comment_{CommentNumber}: "The comment"
* This needs to be done as comments otherwise get removed using
* the {@link FileConfiguration#save(File)} method. The config
* needs to be saved if a config value has changed.</p>
*/
@NotNull
private String convertCommentsToYAMLMappings(@NotNull String configString) {
StringBuilder yamlBuilder = new StringBuilder();
List<String> currentComment = new ArrayList<>();
int commentId = 0;
int previousIndentation = 0;
for (String line : configString.split("\n")) {
String trimmed = line.trim();
if (trimmed.startsWith("#")) {
// Store the indentation of the block
if (currentComment.isEmpty()) {
previousIndentation = getIndentation(line);
}
//Temporarily store the comment line
addComment(currentComment, trimmed);
} else {
addYamlString(yamlBuilder, currentComment, line, previousIndentation, commentId);
commentId++;
previousIndentation = 0;
}
}
return yamlBuilder.toString();
}
/**
* Adds a YAML string to the given string builder
*
* @param yamlBuilder <p>The string builder used for building YAML</p>
* @param currentComment <p>The comment to add as a YAML string</p>
* @param line <p>The current line</p>
* @param previousIndentation <p>The indentation of the current block comment</p>
* @param commentId <p>The id of the comment</p>
*/
private void addYamlString(@NotNull StringBuilder yamlBuilder, @NotNull List<String> currentComment,
@NotNull String line, int previousIndentation, int commentId) {
String trimmed = line.trim();
//Write the full formatted comment to the StringBuilder
if (!currentComment.isEmpty()) {
int indentation = trimmed.isEmpty() ? previousIndentation : getIndentation(line);
generateCommentYAML(yamlBuilder, currentComment, commentId, indentation);
currentComment.clear();
}
//Add the non-comment line assuming it isn't empty
if (!trimmed.isEmpty()) {
yamlBuilder.append(line).append("\n");
}
}
/**
* Adds the given comment to the given list
*
* @param commentParts <p>The list to add to</p>
* @param comment <p>The comment to add</p>
*/
private void addComment(@NotNull List<String> commentParts, @NotNull String comment) {
if (comment.startsWith("# ")) {
commentParts.add(comment.replaceFirst("# ", START_OF_COMMENT_LINE));
} else {
commentParts.add(comment.replaceFirst("#", START_OF_COMMENT_LINE));
}
}
/**
* Generates a YAML-compatible string for one comment block
*
* @param yamlBuilder <p>The string builder to add the generated YAML to</p>
* @param commentLines <p>The lines of the comment to convert into YAML</p>
* @param commentId <p>The unique id of the comment</p>
* @param indentation <p>The indentation to add to every line</p>
*/
private void generateCommentYAML(@NotNull StringBuilder yamlBuilder, @NotNull List<String> commentLines,
int commentId, int indentation) {
String subIndentation = this.addIndentation(indentation + 2);
//Add the comment start marker
yamlBuilder.append(this.addIndentation(indentation)).append(START_OF_COMMENT).append(commentId).append(": |\n");
for (String commentLine : commentLines) {
//Add each comment line with the proper indentation
yamlBuilder.append(subIndentation).append(commentLine).append("\n");
}
//Add the comment end marker
yamlBuilder.append(subIndentation).append(subIndentation).append(END_OF_COMMENT).append("\n");
}
/**
* Converts the internal YAML mapping format to a readable config file
*
* <p>The internal YAML structure is converted to a string with the same format as a standard configuration file.
* The internal structure has comments in the format: START_OF_COMMENT + id + multi-line YAML string +
* END_OF_COMMENT.</p>
*
* @param yamlString <p>A string using the YAML format</p>
* @return <p>The corresponding comment string</p>
*/
@NotNull
private String convertYAMLMappingsToComments(@NotNull String yamlString) {
StringBuilder finalText = new StringBuilder();
String[] lines = yamlString.split("\n");
for (int currentIndex = 0; currentIndex < lines.length; currentIndex++) {
String line = lines[currentIndex];
String possibleComment = line.trim();
if (possibleComment.startsWith(START_OF_COMMENT)) {
//Add an empty line before every comment block
finalText.append("\n");
currentIndex = readComment(finalText, lines, currentIndex + 1, getIndentation(line));
} else {
//Output the configuration key
finalText.append(line).append("\n");
}
}
return finalText.toString().trim();
}
/**
* Fully reads a comment
*
* @param builder <p>The string builder to write to</p>
* @param lines <p>The lines to read from</p>
* @param startIndex <p>The index to start reading from</p>
* @param commentIndentation <p>The indentation of the read comment</p>
* @return <p>The index containing the next non-comment line</p>
*/
private int readComment(@NotNull StringBuilder builder, @NotNull String[] lines, int startIndex,
int commentIndentation) {
for (int currentIndex = startIndex; currentIndex < lines.length; currentIndex++) {
String line = lines[currentIndex];
String possibleComment = line.trim();
if (!line.contains(END_OF_COMMENT)) {
possibleComment = possibleComment.replace(START_OF_COMMENT_LINE, "");
builder.append(addIndentation(commentIndentation)).append("# ").append(possibleComment).append("\n");
} else {
return currentIndex;
}
}
return startIndex;
}
/**
* Gets a string containing the given indentation
*
* @param indentationSpaces <p>The number spaces to use for indentation</p>
* @return <p>A string containing the number of spaces specified</p>
*/
@NotNull
private String addIndentation(int indentationSpaces) {
return " ".repeat(Math.max(0, indentationSpaces));
}
/**
* Gets the indentation (number of spaces) of the given line
*
* @param line <p>The line to get indentation of</p>
* @return <p>The number of spaces in the line's indentation</p>
*/
private int getIndentation(@NotNull String line) {
int spacesFound = 0;
for (char aCharacter : line.toCharArray()) {
if (aCharacter == ' ') {
spacesFound++;
} else {
break;
}
}
return spacesFound;
}
}

View File

@ -0,0 +1,40 @@
package net.knarcraft.blacksmith.config;
public interface TraitSettings<K extends Setting> extends Settings<K> {
/**
* Gets whether to disable the action cool-down
*
* @return <p>Whether to disable the action cool-down</p>
*/
boolean getDisableCoolDown();
/**
* Gets the message to display when a blacksmith or scrapper is still affected by a cool-down
*
* @return <p>The cool down unexpired message</p>
*/
String getCoolDownUnexpiredMessage();
/**
* Gets the message to display when the blacksmith is busy with another player
*
* @return <p>The busy with player message</p>
*/
String getBusyWithPlayerMessage();
/**
* Gets the message to display when the blacksmith is busy with reforging or salvaging an item
*
* @return <p>The busy working message</p>
*/
String getBusyWorkingMessage();
/**
* Gets the message to display when a blacksmith starts reforging or salvaging an item
*
* @return <p>The start working message</p>
*/
String getStartWorkingMessage();
}

View File

@ -1,35 +1,37 @@
package net.knarcraft.blacksmith.config;
package net.knarcraft.blacksmith.config.blacksmith;
import net.citizensnpcs.api.util.DataKey;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.trait.BlacksmithTrait;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.TraitSettings;
import net.knarcraft.blacksmith.util.ConfigHelper;
import net.knarcraft.blacksmith.util.InputParsingHelper;
import net.knarcraft.blacksmith.util.ItemHelper;
import org.bukkit.Material;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
/**
* A class which keeps track of all Blacksmith settings/config values for one NPC
*/
public class NPCSettings {
public class BlacksmithNPCSettings implements TraitSettings<BlacksmithSetting> {
private final List<Material> reforgeAbleItems = new ArrayList<>();
private final List<Enchantment> enchantmentBlocklist = new ArrayList<>();
private final Map<NPCSetting, Object> currentValues = new HashMap<>();
private final GlobalSettings globalSettings;
private final List<Enchantment> enchantmentBlockList = new ArrayList<>();
private final Map<BlacksmithSetting, Object> currentValues = new HashMap<>();
private final GlobalBlacksmithSettings globalBlacksmithSettings;
/**
* Instantiates a new "Settings" object
*/
public NPCSettings(GlobalSettings globalSettings) {
this.globalSettings = globalSettings;
public BlacksmithNPCSettings(GlobalBlacksmithSettings globalBlacksmithSettings) {
this.globalBlacksmithSettings = globalBlacksmithSettings;
}
/**
@ -38,14 +40,14 @@ public class NPCSettings {
* @param key <p>The data key to load variables from</p>
*/
public void loadVariables(DataKey key) {
for (NPCSetting setting : NPCSetting.values()) {
for (BlacksmithSetting setting : BlacksmithSetting.values()) {
if (key.keyExists(setting.getChildPath())) {
currentValues.put(setting, key.getRaw(setting.getChildPath()));
}
}
//Updates the list of reforge-able items/materials
updateReforgeAbleItems();
updateEnchantmentBlocklist();
updateEnchantmentBlockList();
}
/**
@ -54,29 +56,25 @@ public class NPCSettings {
* @param key <p>The data key to save variables to</p>
*/
public void saveVariables(DataKey key) {
for (NPCSetting setting : NPCSetting.values()) {
for (BlacksmithSetting setting : BlacksmithSetting.values()) {
key.setRaw(setting.getChildPath(), currentValues.get(setting));
}
}
/**
* Changes one setting to the given value
*
* @param setting <p>The setting to change</p>
* @param newValue <p>The new value of the setting</p>
*/
public void changeSetting(NPCSetting setting, Object newValue) {
if (setting.getValueType() == SettingValueType.STRING_LIST) {
@Override
public void changeValue(@NotNull BlacksmithSetting setting, @Nullable Object newValue) {
if (setting.getValueType() == SettingValueType.ENCHANTMENT_LIST ||
setting.getValueType() == SettingValueType.REFORGE_ABLE_ITEMS) {
//Workaround to make sure it's treated as the correct type
currentValues.put(setting, ConfigHelper.asStringList(newValue));
currentValues.put(setting, newValue == null ? null : ConfigHelper.asStringList(newValue));
} else {
currentValues.put(setting, newValue);
}
if (setting == NPCSetting.REFORGE_ABLE_ITEMS) {
if (setting == BlacksmithSetting.REFORGE_ABLE_ITEMS) {
updateReforgeAbleItems();
}
if (setting == NPCSetting.ENCHANTMENT_BLOCKLIST) {
updateEnchantmentBlocklist();
if (setting == BlacksmithSetting.ENCHANTMENT_BLOCK_LIST) {
updateEnchantmentBlockList();
}
}
@ -86,17 +84,13 @@ public class NPCSettings {
* @param setting <p>The setting to get the value of</p>
* @return <p>The current value of the setting</p>
*/
public Object getRawValue(NPCSetting setting) {
public @NotNull Object getRawValue(@NotNull BlacksmithSetting setting) {
return currentValues.get(setting);
}
/**
* Gets the message to display when the blacksmith is busy with another player
*
* @return <p>The busy with player message</p>
*/
@Override
public String getBusyWithPlayerMessage() {
return asString(NPCSetting.BUSY_WITH_PLAYER_MESSAGE);
return asString(BlacksmithSetting.BUSY_WITH_PLAYER_MESSAGE);
}
/**
@ -104,8 +98,8 @@ public class NPCSettings {
*
* @return <p>The busy reforging message</p>
*/
public String getBusyReforgingMessage() {
return asString(NPCSetting.BUSY_WITH_REFORGE_MESSAGE);
public String getBusyWorkingMessage() {
return asString(BlacksmithSetting.BUSY_WITH_REFORGE_MESSAGE);
}
/**
@ -114,7 +108,7 @@ public class NPCSettings {
* @return <p>The message to use for displaying item cost</p>
*/
public String getCostMessage() {
return asString(NPCSetting.COST_MESSAGE);
return asString(BlacksmithSetting.COST_MESSAGE);
}
/**
@ -123,7 +117,7 @@ public class NPCSettings {
* @return <p>The invalid item message</p>
*/
public String getInvalidItemMessage() {
return asString(NPCSetting.INVALID_ITEM_MESSAGE);
return asString(BlacksmithSetting.INVALID_ITEM_MESSAGE);
}
/**
@ -132,16 +126,12 @@ public class NPCSettings {
* @return <p>The not damaged message</p>
*/
public String getNotDamagedMessage() {
return asString(NPCSetting.NOT_DAMAGED_MESSAGE);
return asString(BlacksmithSetting.NOT_DAMAGED_MESSAGE);
}
/**
* Gets the message to display when a blacksmith starts reforging an item
*
* @return <p>The start reforge message</p>
*/
public String getStartReforgeMessage() {
return asString(NPCSetting.START_REFORGE_MESSAGE);
@Override
public String getStartWorkingMessage() {
return asString(BlacksmithSetting.START_REFORGE_MESSAGE);
}
/**
@ -150,7 +140,7 @@ public class NPCSettings {
* @return <p>The reforge success message</p>
*/
public String getSuccessMessage() {
return asString(NPCSetting.SUCCESS_MESSAGE);
return asString(BlacksmithSetting.SUCCESS_MESSAGE);
}
/**
@ -159,7 +149,7 @@ public class NPCSettings {
* @return <p>The reforge fail message</p>
*/
public String getFailMessage() {
return asString(NPCSetting.FAIL_MESSAGE);
return asString(BlacksmithSetting.FAIL_MESSAGE);
}
/**
@ -168,16 +158,12 @@ public class NPCSettings {
* @return <p>The insufficient funds message</p>
*/
public String getInsufficientFundsMessage() {
return asString(NPCSetting.INSUFFICIENT_FUNDS_MESSAGE);
return asString(BlacksmithSetting.INSUFFICIENT_FUNDS_MESSAGE);
}
/**
* Gets the message to display when a blacksmith is still affected by a cool-down
*
* @return <p>The cool down unexpired message</p>
*/
@Override
public String getCoolDownUnexpiredMessage() {
return asString(NPCSetting.COOL_DOWN_UNEXPIRED_MESSAGE);
return asString(BlacksmithSetting.COOL_DOWN_UNEXPIRED_MESSAGE);
}
/**
@ -186,7 +172,17 @@ public class NPCSettings {
* @return <p>The item changed message</p>
*/
public String getItemChangedMessage() {
return asString(NPCSetting.ITEM_UNEXPECTEDLY_CHANGED_MESSAGE);
return asString(BlacksmithSetting.ITEM_UNEXPECTEDLY_CHANGED_MESSAGE);
}
/**
* Gets the message displayed if a player presents the blacksmith with an empty hand
*
* @return <p>The no item message</p>
*/
@NotNull
public String getNoItemMessage() {
return asString(BlacksmithSetting.NO_ITEM_MESSAGE);
}
/**
@ -197,16 +193,26 @@ public class NPCSettings {
* @return <p>All items reforge-able by this NPC</p>
*/
public List<Material> getReforgeAbleItems() {
Object currentValue = currentValues.get(BlacksmithSetting.REFORGE_ABLE_ITEMS);
if (currentValue == null || String.valueOf(currentValue).isEmpty()) {
return globalBlacksmithSettings.getReforgeAbleItems();
} else {
return new ArrayList<>(this.reforgeAbleItems);
}
}
/**
* Gets the list of blocked enchantments
*
* @return <p>The list of blocked enchantments</p>
*/
public List<Enchantment> getEnchantmentBlocklist() {
return new ArrayList<>(this.enchantmentBlocklist);
public List<Enchantment> getEnchantmentBlockList() {
Object currentValue = currentValues.get(BlacksmithSetting.ENCHANTMENT_BLOCK_LIST);
if (currentValue == null || String.valueOf(currentValue).isEmpty()) {
return globalBlacksmithSettings.getEnchantmentBlockList();
} else {
return new ArrayList<>(this.enchantmentBlockList);
}
}
/**
@ -215,7 +221,7 @@ public class NPCSettings {
* @return <p>The minimum reforge delay</p>
*/
public int getMinReforgeDelay() {
return asInt(NPCSetting.MIN_REFORGE_DELAY);
return asInt(BlacksmithSetting.MIN_REFORGE_DELAY);
}
/**
@ -224,7 +230,7 @@ public class NPCSettings {
* @return <p>The maximum reforge delay</p>
*/
public int getMaxReforgeDelay() {
return asInt(NPCSetting.MAX_REFORGE_DELAY);
return asInt(BlacksmithSetting.MAX_REFORGE_DELAY);
}
/**
@ -233,7 +239,7 @@ public class NPCSettings {
* @return <p>The reforge cool-down</p>
*/
public int getReforgeCoolDown() {
return asInt(NPCSetting.REFORGE_COOL_DOWN);
return asInt(BlacksmithSetting.REFORGE_COOL_DOWN);
}
/**
@ -242,7 +248,16 @@ public class NPCSettings {
* @return <p>The fail chance</p>
*/
public int getFailChance() {
return asInt(NPCSetting.FAIL_CHANCE);
return asInt(BlacksmithSetting.FAIL_CHANCE);
}
/**
* Gets whether a failed reforging should remove/downgrade enchantments
*
* @return <p>Whether enchantments should be removed</p>
*/
public boolean getFailRemovesEnchantments() {
return asBoolean(BlacksmithSetting.FAIL_REMOVE_ENCHANTMENTS);
}
/**
@ -251,7 +266,7 @@ public class NPCSettings {
* @return <p>The extra enchantment chance</p>
*/
public int getExtraEnchantmentChance() {
return asInt(NPCSetting.EXTRA_ENCHANTMENT_CHANCE);
return asInt(BlacksmithSetting.EXTRA_ENCHANTMENT_CHANCE);
}
/**
@ -260,7 +275,7 @@ public class NPCSettings {
* @return <p>The maximum enchantments</p>
*/
public int getMaxEnchantments() {
return asInt(NPCSetting.MAX_ENCHANTMENTS);
return asInt(BlacksmithSetting.MAX_ENCHANTMENTS);
}
/**
@ -269,7 +284,7 @@ public class NPCSettings {
* @return <p>Whether to drop reforged items on the ground</p>
*/
public boolean getDropItem() {
return ConfigHelper.asBoolean(getValue(NPCSetting.DROP_ITEM));
return asBoolean(BlacksmithSetting.DROP_ITEM);
}
/**
@ -278,25 +293,21 @@ public class NPCSettings {
* @return <p>The title of the blacksmith</p>
*/
public String getBlacksmithTitle() {
return asString(NPCSetting.BLACKSMITH_TITLE);
return asString(BlacksmithSetting.BLACKSMITH_TITLE);
}
/**
* Gets whether to disable the reforge-cool-down
*
* @return <p>Whether to disable the reforge-cool-down</p>
*/
@Override
public boolean getDisableCoolDown() {
return asInt(NPCSetting.REFORGE_COOL_DOWN) <= 0;
return asInt(BlacksmithSetting.REFORGE_COOL_DOWN) <= 0;
}
/**
* Gets whether to disable the delay between starting reforging and the reforging finishing
* Gets whether this blacksmith is able to repair anvils
*
* @return <p>Whether to disable the reforge delay</p>
* @return <p>True if this blacksmith is able to repair anvils</p>
*/
public boolean getDisableDelay() {
return asInt(NPCSetting.MAX_REFORGE_DELAY) <= 0;
public boolean getRepairAnvils() {
return asBoolean(BlacksmithSetting.REPAIR_ANVILS);
}
/**
@ -307,7 +318,7 @@ public class NPCSettings {
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as an integer</p>
*/
private int asInt(NPCSetting setting) {
private int asInt(BlacksmithSetting setting) {
return ConfigHelper.asInt(getValue(setting));
}
@ -317,21 +328,31 @@ public class NPCSettings {
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as a string</p>
*/
private String asString(NPCSetting setting) {
private String asString(BlacksmithSetting setting) {
return getValue(setting).toString();
}
/**
* Gets the boolean value of the given setting
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as a boolean</p>
*/
private boolean asBoolean(BlacksmithSetting setting) {
return ConfigHelper.asBoolean(getValue(setting));
}
/**
* Gets the value of a setting, using the default if not set
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The current value</p>
*/
private Object getValue(NPCSetting setting) {
private Object getValue(BlacksmithSetting setting) {
Object value = currentValues.get(setting);
//If not set, use the default value from the config.yml file
if (value == null) {
Map<NPCSetting, Object> defaultNPCSettings = globalSettings.getDefaultNPCSettings();
Map<BlacksmithSetting, Object> defaultNPCSettings = globalBlacksmithSettings.getDefaultNPCSettings();
if (defaultNPCSettings.containsKey(setting)) {
value = defaultNPCSettings.get(setting);
}
@ -344,51 +365,38 @@ public class NPCSettings {
}
/**
* Replaces placeholders in the given reforge-able value
*
* @param value <p>The value specified by a user</p>
* @return <p>The value with placeholders replaced</p>
* Updates the list of blocked enchantments
*/
private Object replaceReforgeAblePresets(Object value) {
if (value instanceof String string) {
String[] list = string.split(",");
List<String> replaced = new ArrayList<>(list.length);
for (String item : list) {
replaced.add(SmithPreset.replacePreset(item));
}
return String.join(",", replaced);
} else if (value instanceof String[] stringList) {
List<String> replaced = new ArrayList<>(stringList.length);
for (String item : stringList) {
replaced.add(SmithPreset.replacePreset(item));
}
return replaced.toArray();
} else {
throw new IllegalArgumentException("Unexpected object type encountered!");
private void updateEnchantmentBlockList() {
this.enchantmentBlockList.clear();
List<String> enchantments = ConfigHelper.asStringList(getValue(BlacksmithSetting.ENCHANTMENT_BLOCK_LIST));
if (enchantments != null) {
this.enchantmentBlockList.addAll(getEnchantmentBlockList(enchantments));
}
}
/**
* Updates the list of blocked enchantments
* Gets the list of enchantments listed in the given string list
*
* @param enchantments <p>The enchantment names to block</p>
* @return <p>The enchantments to be blocked</p>
*/
private void updateEnchantmentBlocklist() {
this.enchantmentBlocklist.clear();
List<String> newEnchantmentBlocklist = ConfigHelper.asStringList(getValue(NPCSetting.ENCHANTMENT_BLOCKLIST));
for (String item : newEnchantmentBlocklist) {
public static List<Enchantment> getEnchantmentBlockList(List<String> enchantments) {
List<Enchantment> enchantmentBlockList = new ArrayList<>();
for (String item : enchantments) {
if (InputParsingHelper.isEmpty(item)) {
continue;
}
Enchantment enchantment = InputParsingHelper.matchEnchantment(item);
if (enchantment != null) {
this.enchantmentBlocklist.add(enchantment);
enchantmentBlockList.add(enchantment);
} else {
BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING, "Unable to verify " + item +
" as a valid enchantment");
BlacksmithPlugin.warn("Unable to verify " + item + " as a valid enchantment");
}
}
return enchantmentBlockList;
}
/**
@ -396,25 +404,9 @@ public class NPCSettings {
*/
private void updateReforgeAbleItems() {
this.reforgeAbleItems.clear();
String newReforgeAbleItems = (String) currentValues.get(NPCSetting.REFORGE_ABLE_ITEMS);
if (newReforgeAbleItems == null) {
return;
}
//Convert any presets with a list of materials
newReforgeAbleItems = (String) replaceReforgeAblePresets(newReforgeAbleItems);
for (String item : newReforgeAbleItems.split(",")) {
if (InputParsingHelper.isEmpty(item)) {
continue;
}
Material material = InputParsingHelper.matchMaterial(item);
if (material != null && BlacksmithTrait.isRepairable(new ItemStack(material, 1))) {
this.reforgeAbleItems.add(material);
} else {
BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING, "Unable to verify " + item +
" as a valid reforge-able item");
}
List<String> materialStrings = ConfigHelper.asStringList(getValue(BlacksmithSetting.REFORGE_ABLE_ITEMS));
if (materialStrings != null) {
this.reforgeAbleItems.addAll(ItemHelper.getItems(materialStrings, true));
}
}

View File

@ -0,0 +1,352 @@
package net.knarcraft.blacksmith.config.blacksmith;
import net.knarcraft.blacksmith.config.Setting;
import net.knarcraft.blacksmith.config.SettingValueType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* An enum representing all blacksmith-related settings
*/
public enum BlacksmithSetting implements Setting {
/**
* The setting for whether the NPC should drop an item to the ground when finished
*
* <p>If set to false, the item will be directly put in the player's inventory instead</p>
*/
DROP_ITEM("dropItem", SettingValueType.BOOLEAN, true, "Whether the " +
"item will drop a reforged item on the ground, instead of putting it into the user's inventory",
true, false),
/**
* The setting for the chance of a reforging to fail
*/
FAIL_CHANCE("failReforgeChance", SettingValueType.PERCENTAGE, 10,
"The chance to fail reforging an item, which only repairs the item a tiny bit or not at all (0-100)",
true, false),
/**
* The setting for whether failing a reforging should downgrade/remove enchantments as well
*/
FAIL_REMOVE_ENCHANTMENTS("failReforgeRemovesEnchantments", SettingValueType.BOOLEAN, false,
"Whether failed reforging should remove or " +
"downgrade the item's enchantments", true, false),
/**
* The setting for the chance of an additional enchantment being added
*/
EXTRA_ENCHANTMENT_CHANCE("extraEnchantmentChance", SettingValueType.PERCENTAGE, 5,
"The chance that an enchantment will be added to the " +
"reforged item (0-100)", true, false),
/**
* The setting for the maximum amount of enchantments that can be added to an item
*/
MAX_ENCHANTMENTS("maxEnchantments", SettingValueType.POSITIVE_INTEGER, 3,
"The maximum number of enchantments the blacksmith will try to" +
" add", true, false),
/**
* The maximum amount of seconds a player may need to wait for the reforging to finish
*/
MAX_REFORGE_DELAY("maxReforgeWaitTimeSeconds", SettingValueType.POSITIVE_INTEGER, 30,
"The maximum time for a reforging to finish",
true, false),
/**
* The minimum amount of seconds a player may need to wait for the reforging to finish
*/
MIN_REFORGE_DELAY("minReforgeWaitTimeSeconds", SettingValueType.POSITIVE_INTEGER, 5,
"The minimum time for a reforging to finish",
true, false),
/**
* The setting for number of seconds a player has to wait between each usage of the blacksmith
*/
REFORGE_COOL_DOWN("reforgeCoolDownSeconds", SettingValueType.POSITIVE_INTEGER, 60,
"The cool-down period between each reforge",
true, false),
/**
* The setting for which items the blacksmith is able to reforge
*/
REFORGE_ABLE_ITEMS("reforgeAbleItems", SettingValueType.REFORGE_ABLE_ITEMS, "",
"The items a blacksmith is able to reforge. Setting this only " +
"allows NPCs to repair the listed items. This should be set for each individual NPC.",
true, false),
/**
* The setting for the title used to display which kind of blacksmith the NPC is
*
* <p>While this should be entirely configurable, values such as armor-smith, sword-smith and similar, which
* describe the blacksmith's specialization, and thus the range of reforge-able items, is expected.</p>
*/
BLACKSMITH_TITLE("blacksmithTitle", SettingValueType.STRING, "blacksmith",
"The title describing the blacksmith's usage/speciality", true, false),
/**
* The setting for the enchantments a blacksmith cannot apply to items
*/
ENCHANTMENT_BLOCK_LIST("enchantmentBlockList", SettingValueType.ENCHANTMENT_LIST, List.of("binding_curse",
"mending", "vanishing_curse"), "The enchantments a " +
"blacksmith is denied from applying to an item. Disable anything you find too op or annoying.",
true, false),
/**
* Whether to allow this blacksmith to repair anvils
*/
REPAIR_ANVILS("reforgeAnvils", SettingValueType.BOOLEAN, false,
"Whether the blacksmith will reforge anvils as a special case", true, false),
/*-----------
| Messages |
-----------*/
/**
* The message displayed when the blacksmith is busy with another player
*/
BUSY_WITH_PLAYER_MESSAGE("busyPlayerMessage", SettingValueType.STRING,
"&cI'm busy at the moment. Come back later!",
"The message to display when another player is using the blacksmith", true, true),
/**
* The message displayed when the blacksmith is already reforging something for the player
*/
BUSY_WITH_REFORGE_MESSAGE("busyReforgeMessage", SettingValueType.STRING,
"&cI'm working on it. Be patient! I'll finish {time}!",
"The message to display when the blacksmith is working on the reforging", true, true),
/**
* The message displayed if the player has to wait for the cool-down to expire
*/
COOL_DOWN_UNEXPIRED_MESSAGE("coolDownUnexpiredMessage", SettingValueType.STRING,
"&cYou've already had your chance! Give me a break! I'll be ready {time}!",
"The message to display when the blacksmith is still on" +
" a cool-down from the previous re-forging", true, true),
/**
* The message displayed when displaying the cost of reforging the held item to the player
*/
COST_MESSAGE("costMessage", SettingValueType.STRING,
"&eIt will cost &a{cost}&e to reforge that &a{item}&e! Click again to reforge!",
"The message to display when informing a player about the reforging" +
" cost", true, true),
/**
* The message displayed if the blacksmith fails reforging an item
*/
FAIL_MESSAGE("failReforgeMessage", SettingValueType.STRING,
"&cWhoops! Didn't mean to do that! Maybe next time?",
"The message to display when the blacksmith fails to reforge an item", true, true),
/**
* The message displayed if a player is unable to pay the blacksmith
*/
INSUFFICIENT_FUNDS_MESSAGE("insufficientFundsMessage", SettingValueType.STRING,
"&cYou don't have enough money to reforge that item!",
"The message to display when a player cannot pay for the reforging", true, true),
/**
* The message displayed if the blacksmith encounters an item they cannot reforge
*/
INVALID_ITEM_MESSAGE("invalidItemMessage", SettingValueType.STRING,
"&cI'm sorry, but I'm a/an {title}, I don't know how to reforge that!",
"The message to display when holding an item the blacksmith " +
"is unable to reforge", true, true),
/**
* The message displayed if a player presents a different item after seeing the price to reforge an item
*/
ITEM_UNEXPECTEDLY_CHANGED_MESSAGE("itemChangedMessage", SettingValueType.STRING,
"&cThat's not the item you wanted to reforge before!",
"The message to display when presenting a different item than the one just evaluated",
true, true),
/**
* The message displayed when the blacksmith starts reforging an item
*/
START_REFORGE_MESSAGE("startReforgeMessage", SettingValueType.STRING,
"&eOk, let's see what I can do...", "The message to " +
"display once the blacksmith starts re-forging", true, true),
/**
* The message displayed when the blacksmith successfully finishes reforging an item
*/
SUCCESS_MESSAGE("successMessage", SettingValueType.STRING,
"There you go! All better!", "The message to display once " +
"the reforging has successfully finished", true, true),
/**
* The message displayed when trying to reforge an item with full durability
*/
NOT_DAMAGED_MESSAGE("notDamagedMessage", SettingValueType.STRING, "&cThat item is not in need of repair",
"The message to display if a player is trying to reforge an item with full durability",
true, true),
/**
* The message displayed when clicking a blacksmith with an empty hand
*/
NO_ITEM_MESSAGE("noItemMessage", SettingValueType.STRING, "Please present the item you want to reforge",
"The message to display when a blacksmith is clicked with an empty hand", true, true),
/*------------------
| Global settings |
------------------*/
/**
* The base price for repairing, regardless of durability
*
* <p>This allows specifying a price for each item, by setting basePrice.item_name.</p>
*/
BASE_PRICE("basePrice.default", SettingValueType.POSITIVE_DOUBLE, 10.0, "The minimum price of " +
"each cost", false, false),
/**
* The base price for each durability point
*
* <p>If natural cost, this is the cost each missing durability point will add to the cost. If not natural cost,
* this is the cost each present durability point will add to the cost. This allows specifying a price per
* durability point value for each item, by setting pricePerDurabilityPoint.item_name</p>
*/
PRICE_PER_DURABILITY_POINT("pricePerDurabilityPoint.default", SettingValueType.POSITIVE_DOUBLE, 0.005,
"The additional cost for each durability point missing (natural cost) or present (not natural " +
"cost)", false, false),
/**
* The price increase for each level of each present enchantment
*
* <p>This can be specified for each possible enchantment by setting enchantment-cost.enchantment_name</p>
*/
ENCHANTMENT_COST("enchantmentCost.default", SettingValueType.POSITIVE_DOUBLE, 5.0,
"The additional cost for each enchantment level present on an item", false, false),
/**
* Whether the cost should increase for damage taken, as opposed to increase for durability present
*/
NATURAL_COST("useNaturalCost", SettingValueType.BOOLEAN, true, "Natural cost makes re-forging " +
"more expensive the more damaged the item is. Disabling this will enable the legacy blacksmith behavior " +
"instead", false, false),
/**
* Whether to show exact time when displaying the wait time for a reforging or the cool-down
*/
SHOW_EXACT_TIME("showExactTime", SettingValueType.BOOLEAN, false, "Exact time displays the " +
"exact number of seconds and minutes remaining as part of the reforging cool-down and reforging delay " +
"messages, instead of just vaguely hinting at the remaining time.", false, false),
/**
* The cost for repairing a chipped anvil
*/
ANVIL_CHIPPED_COST("chippedAnvilReforgingCost", SettingValueType.POSITIVE_DOUBLE, 10.0,
"The cost of fully repairing a chipped anvil", false, false),
/**
* The cost for repairing a damaged anvil
*/
ANVIL_DAMAGED_COST("damagedAnvilReforgingCost", SettingValueType.POSITIVE_DOUBLE, 20.0,
"The cost of fully repairing a damaged anvil", false, false),
;
private final String path;
private final String childPath;
private final Object value;
private final String commandName;
private final SettingValueType valueType;
private final String description;
private final boolean isPerNPC;
private final boolean isMessage;
/**
* Instantiates a new setting
*
* @param key <p>The configuration key for this setting</p>
* @param valueType <p>The type of value used by this setting</p>
* @param value <p>The default value of this setting</p>
* @param description <p>The description describing this setting</p>
* @param isPerNPC <p>Whether this setting is per-NPC or global</p>
* @param isMessage <p>Whether this option is for an NPC message</p>
*/
BlacksmithSetting(@NotNull String key, @NotNull SettingValueType valueType, @Nullable Object value,
@NotNull String description, boolean isPerNPC, boolean isMessage) {
if (isPerNPC) {
if (isMessage) {
this.path = "blacksmith.defaults.messages." + key;
} else {
this.path = "blacksmith.defaults." + key;
}
} else {
this.path = "blacksmith.global." + key;
}
this.value = value;
this.valueType = valueType;
this.childPath = key;
if (key.contains(".")) {
String[] pathParts = key.split("\\.");
this.commandName = pathParts[0];
} else {
this.commandName = key;
}
this.description = description;
this.isPerNPC = isPerNPC;
this.isMessage = isMessage;
}
@Override
public @NotNull String getPath() {
return path;
}
@Override
public @NotNull String getChildPath() {
return childPath;
}
@Override
public @NotNull Object getDefaultValue() {
return value;
}
@Override
public @NotNull String getCommandName() {
return commandName;
}
@Override
public @NotNull SettingValueType getValueType() {
return this.valueType;
}
@Override
public @NotNull String getDescription() {
return this.description;
}
@Override
public boolean isPerNPC() {
return this.isPerNPC;
}
@Override
public boolean isMessage() {
return this.isMessage;
}
/**
* Gets the blacksmith setting specified by the input string
*
* @param input <p>The input to check</p>
* @return <p>The matching blacksmith setting, or null if not found</p>
*/
public static @Nullable BlacksmithSetting getSetting(@NotNull String input) {
for (BlacksmithSetting blacksmithSetting : BlacksmithSetting.values()) {
if (input.equalsIgnoreCase(blacksmithSetting.commandName)) {
return blacksmithSetting;
}
}
return null;
}
}

View File

@ -0,0 +1,566 @@
package net.knarcraft.blacksmith.config.blacksmith;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.Settings;
import net.knarcraft.blacksmith.config.StargateYamlConfiguration;
import net.knarcraft.blacksmith.util.ConfigHelper;
import net.knarcraft.blacksmith.util.InputParsingHelper;
import net.knarcraft.blacksmith.util.ItemHelper;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.enchantments.Enchantment;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A class which keeps track of all default blacksmith NPC settings and all global blacksmith settings
*/
public class GlobalBlacksmithSettings implements Settings<BlacksmithSetting> {
private final Map<Material, Double> materialBasePrices = new HashMap<>();
private final Map<Material, Double> materialPricePerDurabilityPoints = new HashMap<>();
private final Map<Enchantment, Double> enchantmentCosts = new HashMap<>();
private final Map<BlacksmithSetting, Object> settings = new HashMap<>();
private final List<Material> defaultReforgeAbleMaterials = new ArrayList<>();
private final List<Enchantment> defaultEnchantmentBlockList = new ArrayList<>();
private final BlacksmithPlugin instance;
/**
* Instantiates a new "Global Blacksmith Settings"
*
* @param instance <p>The blacksmith plugin to load and save configurations for</p>
*/
public GlobalBlacksmithSettings(BlacksmithPlugin instance) {
this.instance = instance;
}
/**
* Loads all configuration values from the config file
*/
public void load() {
// Just in case, clear existing values
this.settings.clear();
this.materialBasePrices.clear();
this.materialPricePerDurabilityPoints.clear();
this.enchantmentCosts.clear();
// Load/Save settings
loadSettings();
// Save any modified values to disk
instance.saveConfig();
}
/**
* Changes the value of the given setting
*
* @param blacksmithSetting <p>The default NPC setting to change</p>
* @param newValue <p>The new value for the setting</p>
*/
public void changeValue(@NotNull BlacksmithSetting blacksmithSetting, @Nullable Object newValue) {
if (blacksmithSetting.getValueType() == SettingValueType.ENCHANTMENT_LIST ||
blacksmithSetting.getValueType() == SettingValueType.REFORGE_ABLE_ITEMS) {
//Workaround to make sure it's treated as the correct type
this.settings.put(blacksmithSetting, newValue == null ? null : ConfigHelper.asStringList(newValue));
} else {
this.settings.put(blacksmithSetting, newValue);
}
save();
if (blacksmithSetting == BlacksmithSetting.REFORGE_ABLE_ITEMS) {
loadReforgeAbleItems();
} else if (blacksmithSetting == BlacksmithSetting.ENCHANTMENT_BLOCK_LIST) {
loadEnchantmentBlockList();
}
}
/**
* Gets the current raw value of the given default NPC setting
*
* @param blacksmithSetting <p>The setting to get</p>
* @return <p>The current raw setting value</p>
*/
public @NotNull Object getRawValue(@NotNull BlacksmithSetting blacksmithSetting) {
return this.settings.get(blacksmithSetting);
}
/**
* Sets the enchantment cost for the given enchantment
*
* @param enchantment <p>The enchantment to set the enchantment cost for</p>
* @param newEnchantmentCost <p>The new enchantment cost</p>
*/
public void setEnchantmentCost(Enchantment enchantment, double newEnchantmentCost) {
if (enchantment == null) {
if (newEnchantmentCost < 0) {
throw new IllegalArgumentException("Enchantment cost cannot be negative!");
}
this.settings.put(BlacksmithSetting.ENCHANTMENT_COST, newEnchantmentCost);
} else {
if (newEnchantmentCost < 0) {
this.enchantmentCosts.put(enchantment, null);
} else {
this.enchantmentCosts.put(enchantment, newEnchantmentCost);
}
}
save();
}
/**
* Sets the price per durability point for the given material
*
* @param material <p>The material to set the price per durability point price for</p>
* @param newPrice <p>The new price per durability point price</p>
*/
public void setPricePerDurabilityPoint(Material material, double newPrice) {
if (material == null) {
if (newPrice < 0) {
throw new IllegalArgumentException("Price per durability point cannot be negative!");
}
this.settings.put(BlacksmithSetting.PRICE_PER_DURABILITY_POINT, newPrice);
} else {
//Use a negative price to unset the per-item value
if (newPrice < 0) {
this.materialPricePerDurabilityPoints.put(material, null);
} else {
this.materialPricePerDurabilityPoints.put(material, newPrice);
}
}
save();
}
/**
* Sets the base price for the given material
*
* @param material <p>The material to set the base price for</p>
* @param newBasePrice <p>The new base price</p>
*/
public void setBasePrice(Material material, double newBasePrice) {
if (material == null) {
if (newBasePrice < 0) {
throw new IllegalArgumentException("Base price cannot be negative!");
}
settings.put(BlacksmithSetting.BASE_PRICE, newBasePrice);
} else {
//Use a negative price to unset the per-item value
if (newBasePrice < 0) {
this.materialBasePrices.put(material, null);
} else {
this.materialBasePrices.put(material, newBasePrice);
}
}
save();
}
/**
* Gets the current value of the default NPC settings
*
* @return <p>The current value of the default NPC settings</p>
*/
public Map<BlacksmithSetting, Object> getDefaultNPCSettings() {
return new HashMap<>(this.settings);
}
/**
* Gets whether to use natural cost for cost calculation
*
* <p>Natural cost makes it more costly the more damage is dealt to an item. The alternative is the legacy behavior
* where the amount of durability points remaining increases the cost.</p>
*
* @return <p>Whether to use natural cost</p>
*/
public boolean getUseNaturalCost() {
return asBoolean(BlacksmithSetting.NATURAL_COST);
}
/**
* Gets whether to show exact time for reforging wait-time, and for wait-time between sessions
*
* @return <p>Whether to show exact time</p>
*/
public boolean getShowExactTime() {
return asBoolean(BlacksmithSetting.SHOW_EXACT_TIME);
}
/**
* Gets the base price for the given material
*
* @param material <p>The material to get the base price for</p>
* @return <p>The base price for the material</p>
*/
public double getBasePrice(Material material) {
if (this.materialBasePrices.containsKey(material) && this.materialBasePrices.get(material) != null) {
return this.materialBasePrices.get(material);
} else {
return asDouble(BlacksmithSetting.BASE_PRICE);
}
}
/**
* Gets the price per durability point for the given material
*
* @param material <p>The material to get the durability point price for</p>
* @return <p>The durability point price for the material</p>
*/
public double getPricePerDurabilityPoint(Material material) {
if (this.materialPricePerDurabilityPoints.containsKey(material) &&
this.materialPricePerDurabilityPoints.get(material) != null) {
return this.materialPricePerDurabilityPoints.get(material);
} else {
return asDouble(BlacksmithSetting.PRICE_PER_DURABILITY_POINT);
}
}
/**
* Gets the cost to be added for each level of the given enchantment
*
* @param enchantment <p>The enchantment to get the cost for</p>
* @return <p>The cost of each enchantment level</p>
*/
public double getEnchantmentCost(Enchantment enchantment) {
if (this.enchantmentCosts.containsKey(enchantment) && this.enchantmentCosts.get(enchantment) != null) {
return this.enchantmentCosts.get(enchantment);
} else {
return asDouble(BlacksmithSetting.ENCHANTMENT_COST);
}
}
/**
* Gets the value of reforgeAbleItems
*
* @return <p>The value of reforgeAbleItems</p>
*/
public @NotNull List<Material> getReforgeAbleItems() {
return this.defaultReforgeAbleMaterials;
}
/**
* Gets the value of enchantmentBlockList
*
* @return <p>The value of enchantmentBlockList</p>
*/
public @NotNull List<Enchantment> getEnchantmentBlockList() {
return this.defaultEnchantmentBlockList;
}
/**
* Gets the cost for repairing the given type of anvil
*
* @param material <p>The anvil material to repair</p>
* @return <p>The cost of repairing the anvil</p>
*/
public double getAnvilCost(Material material) {
if (material == Material.CHIPPED_ANVIL) {
return asDouble(BlacksmithSetting.ANVIL_CHIPPED_COST);
} else if (material == Material.DAMAGED_ANVIL) {
return asDouble(BlacksmithSetting.ANVIL_DAMAGED_COST);
} else {
throw new IllegalArgumentException("An unexpected item was encountered!");
}
}
/**
* Gets the given value as a boolean
*
* <p>This will throw an exception if used for a non-boolean value</p>
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as a boolean</p>
*/
public boolean asBoolean(BlacksmithSetting setting) {
return ConfigHelper.asBoolean(getValue(setting));
}
/**
* Gets the given value as a double
*
* <p>This will throw an exception if used for a non-double setting</p>
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as a double</p>
*/
public double asDouble(BlacksmithSetting setting) {
return ConfigHelper.asDouble(getValue(setting));
}
/**
* Gets the value of a setting, using the default if not set
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The current value</p>
*/
private Object getValue(BlacksmithSetting setting) {
Object value = this.settings.get(setting);
//If not set in config.yml, use the default value from the enum
if (value == null) {
value = setting.getDefaultValue();
}
return value;
}
/**
* Loads all global settings
*/
private void loadSettings() {
instance.reloadConfig();
FileConfiguration configuration = instance.getConfiguration();
for (BlacksmithSetting blacksmithSetting : BlacksmithSetting.values()) {
if (!configuration.contains(blacksmithSetting.getPath())) {
//If the setting does not exist in the config file, add it
configuration.set(blacksmithSetting.getPath(), blacksmithSetting.getDefaultValue());
} else {
//Set the setting to the value found in the path
settings.put(blacksmithSetting, configuration.get(blacksmithSetting.getPath()));
}
}
loadReforgeAbleItems();
loadEnchantmentBlockList();
//Load all base prices
loadBasePrices(configuration);
//Load all per-durability-point prices
loadPricesPerDurabilityPoint(configuration);
//Load all enchantment prices
loadEnchantmentPrices(configuration);
}
/**
* Loads all prices for enchantments
*
* @param fileConfiguration <p>The configuration to read</p>
*/
private void loadEnchantmentPrices(@NotNull FileConfiguration fileConfiguration) {
ConfigurationSection enchantmentCostNode = fileConfiguration.getConfigurationSection(
getBase(BlacksmithSetting.ENCHANTMENT_COST.getPath()));
if (enchantmentCostNode == null) {
BlacksmithPlugin.warn("Could not load enchantment prices. because the configuration section doesn't exist");
return;
}
Map<String, String> relevantKeys = getRelevantKeys(enchantmentCostNode);
for (String key : relevantKeys.keySet()) {
String enchantmentName = relevantKeys.get(key);
Enchantment enchantment = InputParsingHelper.matchEnchantment(enchantmentName);
BlacksmithPlugin.warn("loadEnchantmentPrices " + enchantmentName);
setItemPrice(this.enchantmentCosts, enchantmentName, enchantment, enchantmentCostNode.getDouble(key));
}
}
/**
* Loads all prices per durability point for all materials
*
* @param fileConfiguration <p>The configuration to read</p>
*/
private void loadPricesPerDurabilityPoint(@NotNull FileConfiguration fileConfiguration) {
ConfigurationSection basePerDurabilityPriceNode = fileConfiguration.getConfigurationSection(
getBase(BlacksmithSetting.PRICE_PER_DURABILITY_POINT.getPath()));
if (basePerDurabilityPriceNode == null) {
BlacksmithPlugin.warn("Could not load per durability prices. because the configuration section doesn't exist");
return;
}
Map<String, String> relevantKeys = getRelevantKeys(basePerDurabilityPriceNode);
for (String key : relevantKeys.keySet()) {
String materialName = relevantKeys.get(key);
double price = basePerDurabilityPriceNode.getDouble(key);
if (materialName.contains("*")) {
//Treat *CHESTPLATE as a regular expression to match all chest-plates
setMatchedMaterialPrices(this.materialPricePerDurabilityPoints, materialName, price);
} else {
Material material = InputParsingHelper.matchMaterial(materialName);
setItemPrice(this.materialPricePerDurabilityPoints, materialName, material, price);
}
}
}
/**
* Loads base prices for all materials
*
* @param fileConfiguration <p>The configuration to read</p>
*/
private void loadBasePrices(@NotNull FileConfiguration fileConfiguration) {
ConfigurationSection basePriceNode = fileConfiguration.getConfigurationSection(
getBase(BlacksmithSetting.BASE_PRICE.getPath()));
if (basePriceNode == null) {
BlacksmithPlugin.warn("Could not load base prices, because the configuration section doesn't exist");
return;
}
Map<String, String> relevantKeys = getRelevantKeys(basePriceNode);
for (String key : relevantKeys.keySet()) {
String materialName = relevantKeys.get(key);
double price = basePriceNode.getDouble(key);
if (materialName.contains("*")) {
//Treat *CHESTPLATE as a regular expression to match all chest-plates
setMatchedMaterialPrices(this.materialBasePrices, materialName, price);
} else {
Material material = InputParsingHelper.matchMaterial(materialName);
setItemPrice(this.materialBasePrices, materialName, material, price);
}
}
}
/**
* Sets the price for any materials matching the given wildcard material name
*
* @param prices <p>The map to store the prices in</p>
* @param materialName <p>The material name to match</p>
* @param price <p>The price to set for the matched materials</p>
*/
private void setMatchedMaterialPrices(@NotNull Map<Material, Double> prices, @NotNull String materialName, double price) {
String search = InputParsingHelper.regExIfy(materialName);
for (Material material : ItemHelper.getAllReforgeAbleMaterials()) {
if (material.name().matches(search)) {
setItemPrice(prices, material.name(), material, price);
}
}
}
/**
* Sets the price for the given material
*
* @param prices <p>The map to store the price in</p>
* @param itemName <p>The name of the material to add a price for</p>
* @param item <p>The material parsed from the name</p>
* @param price <p>The price to set</p>
*/
private <K> void setItemPrice(@NotNull Map<K, Double> prices, @NotNull String itemName, @Nullable K item,
double price) {
if (item != null) {
prices.put(item, price);
} else {
BlacksmithPlugin.warn("Unable to find a material/enchantment matching " + itemName);
}
}
/**
* Gets a map between relevant keys and their normalized name
*
* @param configurationSection <p>The configuration section to search</p>
* @return <p>Any sub-keys found that aren't the default</p>
*/
@NotNull
private Map<String, String> getRelevantKeys(@NotNull ConfigurationSection configurationSection) {
Map<String, String> relevant = new HashMap<>();
for (String dataKey : StargateYamlConfiguration.getKeysWithoutComments(configurationSection, false)) {
//Skip the default value
if (dataKey.equals("default")) {
continue;
}
String normalizedName = dataKey.toUpperCase().replace("-", "_");
relevant.put(dataKey, normalizedName);
}
return relevant;
}
/**
* Converts a normalized material name to the format used in the config file
*
* @param normalizedName <p>The normalized name to un-normalize</p>
* @return <p>The un-normalized name</p>
*/
@NotNull
private String unNormalizeName(@NotNull String normalizedName) {
return normalizedName.toLowerCase().replace("_", "-");
}
/**
* Loads reforgeAble items from the current value
*/
private void loadReforgeAbleItems() {
this.defaultReforgeAbleMaterials.clear();
List<String> materialNames = ConfigHelper.asStringList(settings.get(
BlacksmithSetting.REFORGE_ABLE_ITEMS));
if (materialNames != null) {
this.defaultReforgeAbleMaterials.addAll(ItemHelper.getItems(materialNames, true));
}
}
/**
* Loads the enchantment block list from the current value
*/
private void loadEnchantmentBlockList() {
this.defaultEnchantmentBlockList.clear();
List<String> enchantmentNames = ConfigHelper.asStringList(settings.get(
BlacksmithSetting.ENCHANTMENT_BLOCK_LIST));
if (enchantmentNames != null) {
this.defaultEnchantmentBlockList.addAll(BlacksmithNPCSettings.getEnchantmentBlockList(enchantmentNames));
}
}
/**
* Saves all current settings to the config file
*/
private void save() {
FileConfiguration fileConfiguration = instance.getConfiguration();
//Save all default settings
for (BlacksmithSetting setting : BlacksmithSetting.values()) {
fileConfiguration.set(setting.getPath(), this.settings.get(setting));
}
//Save all base prices
ConfigurationSection basePriceNode = getAndCreateSection(fileConfiguration, getBase(BlacksmithSetting.BASE_PRICE.getPath()));
for (Material material : this.materialBasePrices.keySet()) {
basePriceNode.set(unNormalizeName(material.name()), this.materialBasePrices.get(material));
}
//Save all per-durability-point prices
ConfigurationSection basePerDurabilityPriceNode = getAndCreateSection(fileConfiguration, getBase(BlacksmithSetting.PRICE_PER_DURABILITY_POINT.getPath()));
for (Material material : this.materialPricePerDurabilityPoints.keySet()) {
basePerDurabilityPriceNode.set(unNormalizeName(material.name()), this.materialPricePerDurabilityPoints.get(material));
}
//Load all enchantment prices
ConfigurationSection enchantmentCostNode = getAndCreateSection(fileConfiguration, getBase(BlacksmithSetting.ENCHANTMENT_COST.getPath()));
for (Enchantment enchantment : this.enchantmentCosts.keySet()) {
enchantmentCostNode.set(unNormalizeName(enchantment.getKey().getKey()), this.enchantmentCosts.get(enchantment));
}
//Perform the actual save to disk
instance.saveConfig();
}
/**
* Gets a configuration section, creating it if necessary
*
* @param fileConfiguration <p>The file configuration to read</p>
* @param sectionPath <p>The path to the configuration section to get</p>
* @return <p>The configuration section</p>
*/
@NotNull
private ConfigurationSection getAndCreateSection(@NotNull FileConfiguration fileConfiguration, @NotNull String sectionPath) {
ConfigurationSection configurationSection = fileConfiguration.getConfigurationSection(sectionPath);
if (configurationSection == null) {
fileConfiguration.createSection(sectionPath);
configurationSection = fileConfiguration.getConfigurationSection(sectionPath);
}
if (configurationSection == null) {
throw new RuntimeException("Unable to create configuration section!");
}
return configurationSection;
}
/**
* Gets the base path of a configuration path pointing to a specific value
*
* @param configurationPath <p>The configuration path to get the base of</p>
* @return <p>The base configuration path</p>
*/
@NotNull
private String getBase(@NotNull String configurationPath) {
String[] parts = configurationPath.split("\\.");
String[] base = Arrays.copyOfRange(parts, 0, parts.length - 1);
return String.join(".", base);
}
}

View File

@ -0,0 +1,278 @@
package net.knarcraft.blacksmith.config.scrapper;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.Settings;
import net.knarcraft.blacksmith.util.ConfigHelper;
import net.knarcraft.blacksmith.util.ItemHelper;
import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A class which keeps track of all default scrapper NPC settings and all global scrapper settings
*/
public class GlobalScrapperSettings implements Settings<ScrapperSetting> {
private final Map<ScrapperSetting, Object> settings = new HashMap<>();
private final List<Material> defaultSalvageableMaterials = new ArrayList<>();
private final Map<Material, Set<Material>> trashSalvage = new HashMap<>();
private final BlacksmithPlugin instance;
/**
* Instantiates a new "Settings"
*
* @param instance <p>A reference to the blacksmith plugin</p>
*/
public GlobalScrapperSettings(@NotNull BlacksmithPlugin instance) {
this.instance = instance;
}
/**
* Loads all configuration values from the config file
*/
public void load() {
//Just in case, clear existing values
this.settings.clear();
//Load/Save global settings
loadSettings();
//Save any modified values to disk
instance.saveConfig();
}
/**
* Changes the value of the given setting
*
* @param scrapperSetting <p>The default NPC setting to change</p>
* @param newValue <p>The new value for the setting</p>
*/
public void changeValue(@NotNull ScrapperSetting scrapperSetting, @Nullable Object newValue) {
if (scrapperSetting.getValueType() == SettingValueType.STRING_LIST ||
scrapperSetting.getValueType() == SettingValueType.REFORGE_ABLE_ITEMS) {
//Workaround to make sure it's treated as the correct type
this.settings.put(scrapperSetting, newValue == null ? null : ConfigHelper.asStringList(newValue));
} else {
this.settings.put(scrapperSetting, newValue);
}
if (scrapperSetting == ScrapperSetting.SALVAGE_ABLE_ITEMS) {
loadSalvageAbleItems();
}
save();
}
/**
* Gets the current raw value of the given global setting
*
* @param scrapperSetting <p>The setting to get</p>
* @return <p>The current raw setting value</p>
*/
@NotNull
public Object getRawValue(@NotNull ScrapperSetting scrapperSetting) {
return this.settings.get(scrapperSetting);
}
/**
* Gets the current value of the default NPC settings
*
* @return <p>The current value of the default NPC settings</p>
*/
@NotNull
public Map<ScrapperSetting, Object> getDefaultNPCSettings() {
return new HashMap<>(this.settings);
}
/**
* Gets whether to show exact time for reforging wait-time, and for wait-time between sessions
*
* @return <p>Whether to show exact time</p>
*/
public boolean showExactTime() {
return asBoolean(ScrapperSetting.SHOW_EXACT_TIME);
}
/**
* Gets the given value as a boolean
*
* <p>This will throw an exception if used for a non-boolean value</p>
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as a boolean</p>
*/
public boolean asBoolean(@NotNull ScrapperSetting setting) {
return ConfigHelper.asBoolean(getValue(setting));
}
/**
* Gets the given value as a double
*
* <p>This will throw an exception if used for a non-double setting</p>
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as a double</p>
*/
public double asDouble(@NotNull ScrapperSetting setting) {
return ConfigHelper.asDouble(getValue(setting));
}
/**
* Gets the value of a setting, using the default if not set
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The current value</p>
*/
private Object getValue(ScrapperSetting setting) {
Object value = this.settings.get(setting);
//If not set in config.yml, use the default value from the enum
if (value == null) {
value = setting.getDefaultValue();
}
return value;
}
/**
* Loads all global settings
*/
private void loadSettings() {
instance.reloadConfig();
FileConfiguration configuration = instance.getConfiguration();
for (ScrapperSetting setting : ScrapperSetting.values()) {
if (!configuration.contains(setting.getPath())) {
//If the setting does not exist in the config file, add it
configuration.set(setting.getPath(), setting.getDefaultValue());
} else {
//Set the setting to the value found in the path
this.settings.put(setting, configuration.get(setting.getPath()));
}
}
loadSalvageAbleItems();
loadTrashSalvage();
}
/**
* Saves all current settings to the config file
*/
private void save() {
FileConfiguration fileConfiguration = instance.getConfiguration();
//Save all default settings
for (ScrapperSetting setting : ScrapperSetting.values()) {
fileConfiguration.set(setting.getPath(), this.settings.get(setting));
}
//Perform the actual save to disk
instance.saveConfig();
}
/**
* Gets the value of salvageAbleItems
*
* @return <p>The value of salvageAbleItems</p>
*/
@NotNull
public List<Material> getSalvageAbleItems() {
return this.defaultSalvageableMaterials;
}
/**
* Gets the cost of using a scrapper to salvage an item
*
* @return <p>The cost of using a scrapper to salvage an item</p>
*/
public double getSalvageCost() {
return asDouble(ScrapperSetting.SALVAGE_COST);
}
/**
* Gets the cost of using a scrapper to remove armor trim
*
* @return <p>The cost of using a scrapper to remove armor trim</p>
*/
public double getArmorTrimSalvageCost() {
return asDouble(ScrapperSetting.ARMOR_TRIM_SALVAGE_COST);
}
/**
* Gets the cost of using a scrapper to remove netherite from an item
*
* @return <p>The cost of using a scrapper to remove netherite from an item</p>
*/
public double getNetheriteSalvageCost() {
return asDouble(ScrapperSetting.NETHERITE_SALVAGE_COST);
}
/**
* Gets trash salvage for the given material
*
* <p>The trash salvage should be deferred when calculating item salvage, in order to increase the probability of
* getting the more expensive materials back.</p>
*
* @param material <p>The material to get trash salvage for</p>
* @return <p>The trash salvage</p>
*/
@Nullable
public Set<Material> getTrashSalvage(@NotNull Material material) {
return this.trashSalvage.get(material);
}
/**
* Loads reforgeAble items from the current value
*/
private void loadSalvageAbleItems() {
this.defaultSalvageableMaterials.clear();
List<String> materialNames = ConfigHelper.asStringList(this.settings.get(ScrapperSetting.SALVAGE_ABLE_ITEMS));
if (materialNames != null) {
this.defaultSalvageableMaterials.addAll(ItemHelper.getItems(materialNames, true));
}
}
/**
* Loads all trash salvage from the configuration file
*/
private void loadTrashSalvage() {
this.trashSalvage.clear();
List<String> allTrashSalvage = ConfigHelper.asStringList(this.settings.get(ScrapperSetting.IGNORED_SALVAGE));
if (allTrashSalvage == null) {
return;
}
for (String trashSalvageInfo : allTrashSalvage) {
// Ignore invalid lines
if (!trashSalvageInfo.contains(":")) {
BlacksmithPlugin.warn(String.format("The trash salvage configuration line %s is invalid", trashSalvageInfo));
continue;
}
// Parse all material names
String[] data = trashSalvageInfo.split(":");
String[] materialStrings = data[0].split(";");
List<Material> materials = new ArrayList<>();
for (String materialString : materialStrings) {
materials.addAll(ItemHelper.getWildcardMatch(materialString, true));
}
String[] trashSalvageStrings = data[1].split(";");
List<Material> ignored = new ArrayList<>();
for (String trashSalvageString : trashSalvageStrings) {
ignored.addAll(ItemHelper.getWildcardMatch(trashSalvageString, true));
}
// Add the trash salvage to all the matched materials
for (Material material : materials) {
trashSalvage.computeIfAbsent(material, k -> new HashSet<>());
trashSalvage.get(material).addAll(ignored);
}
}
}
}

View File

@ -0,0 +1,469 @@
package net.knarcraft.blacksmith.config.scrapper;
import net.citizensnpcs.api.util.DataKey;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.TraitSettings;
import net.knarcraft.blacksmith.util.ConfigHelper;
import net.knarcraft.blacksmith.util.ItemHelper;
import org.bukkit.Material;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ScrapperNPCSettings implements TraitSettings<ScrapperSetting> {
private final Map<ScrapperSetting, Object> currentValues = new HashMap<>();
private final GlobalScrapperSettings globalScrapperSettings;
private final List<Material> salvageAbleItems = new ArrayList<>();
/**
* Instantiates a new scrapper NPC settings object
*
* @param globalScrapperSettings <p>The global settings object to get default values from</p>
*/
public ScrapperNPCSettings(GlobalScrapperSettings globalScrapperSettings) {
this.globalScrapperSettings = globalScrapperSettings;
}
/**
* Loads variables from the given data key
*
* @param key <p>The data key to load variables from</p>
*/
public void loadVariables(DataKey key) {
for (ScrapperSetting setting : ScrapperSetting.values()) {
if (key.keyExists(setting.getChildPath())) {
currentValues.put(setting, key.getRaw(setting.getChildPath()));
}
}
}
/**
* Saves variables to the given data key
*
* @param key <p>The data key to save variables to</p>
*/
public void saveVariables(DataKey key) {
for (ScrapperSetting setting : ScrapperSetting.values()) {
key.setRaw(setting.getChildPath(), currentValues.get(setting));
}
}
/**
* Changes one setting to the given value
*
* @param setting <p>The setting to change</p>
* @param newValue <p>The new value of the setting</p>
*/
public void changeValue(@NotNull ScrapperSetting setting, @Nullable Object newValue) {
if (setting.getValueType() == SettingValueType.STRING_LIST ||
setting.getValueType() == SettingValueType.REFORGE_ABLE_ITEMS) {
//Workaround to make sure it's treated as the correct type
currentValues.put(setting, newValue == null ? null : ConfigHelper.asStringList(newValue));
} else {
currentValues.put(setting, newValue);
}
if (setting == ScrapperSetting.SALVAGE_ABLE_ITEMS) {
updateSalvageAbleItems();
}
}
/**
* Updates the reforge-able items according to the current value of the setting
*/
private void updateSalvageAbleItems() {
this.salvageAbleItems.clear();
List<String> materialStrings = ConfigHelper.asStringList(getValue(ScrapperSetting.SALVAGE_ABLE_ITEMS));
if (materialStrings != null) {
this.salvageAbleItems.addAll(ItemHelper.getItems(materialStrings, !extendedSalvageEnabled()));
}
}
/**
* Gets the raw current value of a setting
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The current value of the setting</p>
*/
public @NotNull Object getRawValue(@NotNull ScrapperSetting setting) {
return currentValues.get(setting);
}
@Override
@NotNull
public String getBusyWithPlayerMessage() {
return asString(ScrapperSetting.BUSY_WITH_PLAYER_MESSAGE);
}
@Override
@NotNull
public String getBusyWorkingMessage() {
return asString(ScrapperSetting.BUSY_WITH_SALVAGE_MESSAGE);
}
@Override
@NotNull
public String getStartWorkingMessage() {
return asString(ScrapperSetting.START_SALVAGE_MESSAGE);
}
/**
* Gets the message to display when a scrapper has successfully scrapped an item
*
* @return <p>The salvage success message</p>
*/
@NotNull
public String getSuccessMessage() {
return asString(ScrapperSetting.SUCCESS_SALVAGE_MESSAGE);
}
/**
* Gets the message to display when a scrapper fails to salvage an item
*
* @return <p>The salvage fail message</p>
*/
@NotNull
public String getFailSalvageMessage() {
return asString(ScrapperSetting.FAIL_SALVAGE_MESSAGE);
}
/**
* Gets the message to display when a scrapper fails to salvage an item without durability
*
* @return <p>The extended salvage fail message</p>
*/
@NotNull
public String getFailExtendedSalvageMessage() {
return asString(ScrapperSetting.FAIL_SALVAGE_EXTENDED_MESSAGE);
}
/**
* Gets the message to use for displaying salvage cost
*
* @return <p>The message to use for displaying salvage cost</p>
*/
@NotNull
public String getCostMessage() {
return asString(ScrapperSetting.COST_MESSAGE);
}
/**
* Gets the message to use for displaying armor trim salvage cost
*
* @return <p>The message to use for displaying armor trim salvage cost</p>
*/
@NotNull
public String getArmorTrimCostMessage() {
return asString(ScrapperSetting.COST_MESSAGE_ARMOR_TRIM);
}
/**
* Gets the message to use for displaying netherite salvage cost
*
* @return <p>The message to use for displaying netherite salvage cost</p>
*/
@NotNull
public String getNetheriteCostMessage() {
return asString(ScrapperSetting.COST_MESSAGE_NETHERITE);
}
@Override
@NotNull
public String getCoolDownUnexpiredMessage() {
return asString(ScrapperSetting.COOL_DOWN_UNEXPIRED_MESSAGE);
}
/**
* Gets the message displayed if a player presents a different item after seeing the price to salvage an item
*
* @return <p>The item changed message</p>
*/
@NotNull
public String getItemChangedMessage() {
return asString(ScrapperSetting.ITEM_UNEXPECTEDLY_CHANGED_MESSAGE);
}
/**
* Gets the message displayed if a player presents the scrapper with an empty hand
*
* @return <p>The no item message</p>
*/
@NotNull
public String getNoItemMessage() {
return asString(ScrapperSetting.NO_ITEM_MESSAGE);
}
/**
* Gets the minimum delay used to wait for a salvaging to finish.
*
* @return <p>The minimum salvage delay</p>
*/
public int getMinSalvageDelay() {
return asInt(ScrapperSetting.MIN_SALVAGE_DELAY);
}
/**
* Gets the maximum delay used to wait for a salvaging to finish
*
* @return <p>The maximum salvage delay</p>
*/
public int getMaxSalvageDelay() {
return asInt(ScrapperSetting.MAX_SALVAGE_DELAY);
}
/**
* Gets the cool-down between each salvaging
*
* @return <p>The salvage cool-down</p>
*/
public int getSalvageCoolDown() {
return asInt(ScrapperSetting.SALVAGE_COOL_DOWN);
}
/**
* Gets whether an item should be dropped on the ground instead of being given to the player
*
* @return <p>Whether to drop reforged items on the ground</p>
*/
public boolean getDropItem() {
return asBoolean(ScrapperSetting.DROP_ITEM);
}
@Override
public boolean getDisableCoolDown() {
return asInt(ScrapperSetting.SALVAGE_COOL_DOWN) <= 0;
}
/**
* Gets the chance of a salvaging to fail
*
* @return <p>The chance of a salvaging to fail</p>
*/
public int getFailChance() {
return asInt(ScrapperSetting.FAIL_SALVAGE_CHANCE);
}
/**
* Gets the given value as an integer
*
* <p>This will throw an exception if used for a non-integer setting</p>
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as an integer</p>
*/
private int asInt(@NotNull ScrapperSetting setting) {
return ConfigHelper.asInt(getValue(setting));
}
/**
* Gets the string value of the given setting
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as a string</p>
*/
@NotNull
private String asString(@NotNull ScrapperSetting setting) {
return getValue(setting).toString();
}
/**
* Gets the boolean value of the given setting
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as a boolean</p>
*/
private boolean asBoolean(@NotNull ScrapperSetting setting) {
return ConfigHelper.asBoolean(getValue(setting));
}
/**
* Gets the value of a setting, using the default if not set
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The current value</p>
*/
@NotNull
private Object getValue(@NotNull ScrapperSetting setting) {
Object value = currentValues.get(setting);
//If not set, use the default value from the config.yml file
if (value == null) {
Map<ScrapperSetting, Object> defaultNPCSettings = globalScrapperSettings.getDefaultNPCSettings();
if (defaultNPCSettings.containsKey(setting)) {
value = defaultNPCSettings.get(setting);
}
}
//If not set in config.yml, use the default value from the enum
if (value == null) {
value = setting.getDefaultValue();
}
return value;
}
/**
* Gets all item types this NPC is able to salvage
*
* @return <p>All salvageable items</p>
*/
@NotNull
public List<Material> getSalvageAbleItems() {
Object currentValue = currentValues.get(ScrapperSetting.SALVAGE_ABLE_ITEMS);
if (currentValue == null || String.valueOf(currentValue).isEmpty()) {
return globalScrapperSettings.getSalvageAbleItems();
} else {
return new ArrayList<>(this.salvageAbleItems);
}
}
/**
* Whether extended salvaging is enabled
*
* <p>When not extended, only repairable items can be salvaged. When extended, any item produced using a crafting
* recipe can be salvaged. This does not include smelting or similar.</p>
*
* @return <p>True if extended salvaging is enabled</p>
*/
public boolean extendedSalvageEnabled() {
return asBoolean(ScrapperSetting.EXTENDED_SALVAGE_ENABLED);
}
/**
* Whether salvage of enchanted items is enabled
*
* @return <p>True of this scrapper can salvage enchanted items</p>
*/
public boolean salvageEnchanted() {
return asBoolean(ScrapperSetting.SALVAGE_ENCHANTED);
}
/**
* Whether salvage of armor trims is enabled
*
* @return <p>True if this scrapper can salvage armor trims</p>
*/
public boolean salvageArmorTrims() {
return asBoolean(ScrapperSetting.SALVAGE_ARMOR_TRIMS);
}
/**
* Whether salvage of netherite items is enabled
*
* @return <p>True if this scrapper can salvage netherite items</p>
*/
public boolean salvageNetherite() {
return asBoolean(ScrapperSetting.SALVAGE_NETHERITE);
}
/**
* Gets the title of this scrapper NPC
*
* <p>The title is used to specify scrapper sub-types in order to make it clear which items a scrapper can salvage.
* For example, an armor-scrapper would only be able to salvage armor pieces.</p>
*
* @return <p>The title of this scrapper NPC</p>
*/
@NotNull
public String getScrapperTitle() {
return asString(ScrapperSetting.SCRAPPER_TITLE);
}
/**
* Gets the message to display if this NPC is given an item it cannot salvage
*
* @return <p>The message to display</p>
*/
@NotNull
public String getInvalidItemMessage() {
return asString(ScrapperSetting.INVALID_ITEM_MESSAGE);
}
/**
* Gets the message to display if the player is unable to afford an item salvage
*
* @return <p>The message to display</p>
*/
@NotNull
public String getInsufficientFundsMessage() {
return asString(ScrapperSetting.INSUFFICIENT_FUNDS_MESSAGE);
}
/**
* Gets the message to display if an item is too damaged to produce any salvage
*
* @return <p>The no salvage message</p>
*/
@NotNull
public String getTooDamagedMessage() {
return asString(ScrapperSetting.TOO_DAMAGED_FOR_SALVAGE_MESSAGE);
}
/**
* Gets the message to display when explaining that full salvage will be given for an item
*
* @return <p>The full salvage message</p>
*/
@NotNull
public String getFullSalvageMessage() {
return asString(ScrapperSetting.FULL_SALVAGE_MESSAGE);
}
/**
* Gets the message to display when explaining that only partial salvage will be given for an item
*
* @return <p>The partial salvage message</p>
*/
@NotNull
public String getPartialSalvageMessage() {
return asString(ScrapperSetting.PARTIAL_SALVAGE_MESSAGE);
}
/**
* Gets the message to display when explaining that this scrapper is unable to salvage enchanted items
*
* @return <p>The cannot salvage enchanted message</p>
*/
@NotNull
public String getCannotSalvageEnchantedMessage() {
return asString(ScrapperSetting.CANNOT_SALVAGE_ENCHANTED_MESSAGE);
}
/**
* Gets the message to display when explaining that this scrapper is unable to salvage armor trims
*
* @return <p>The cannot salvage armor trim message</p>
*/
@NotNull
public String getCannotSalvageArmorTrimMessage() {
return asString(ScrapperSetting.CANNOT_SALVAGE_ARMOR_TRIM_MESSAGE);
}
/**
* Gets the message to display when explaining that this scrapper is unable to find salvage for the armor trim
*
* <p>Because there is no direct way (that I have found) to convert TrimMaterial and TrimPattern to Material, armor
* trim salvaging relies on material string and a hard-coded map. As those are prone to breaking because of API
* changes, there is a high likelihood that cases will arise when the scrapper is unable to find the correct
* materials.</p>
*
* @return <p>The cannot find armor trim salvage message</p>
*/
@NotNull
public String getArmorTrimSalvageNotFoundMessage() {
return asString(ScrapperSetting.ARMOR_TRIM_SALVAGE_NOT_FOUND_MESSAGE);
}
/**
* Gets the message to display when explaining that this scrapper is unable to salvage netherite items
*
* @return <p>The message to display when explaining that this scrapper is unable to salvage netherite items</p>
*/
@NotNull
public String getCannotSalvageNetheriteMessage() {
return asString(ScrapperSetting.CANNOT_SALVAGE_NETHERITE_MESSAGE);
}
}

View File

@ -0,0 +1,397 @@
package net.knarcraft.blacksmith.config.scrapper;
import net.knarcraft.blacksmith.config.Setting;
import net.knarcraft.blacksmith.config.SettingValueType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* An enum representing all scrapper-related settings
*/
public enum ScrapperSetting implements Setting {
/**
* The setting for whether the NPC should drop an item to the ground when finished
*
* <p>If set to false, the item will be directly put in the player's inventory instead</p>
*/
DROP_ITEM("dropItem", SettingValueType.BOOLEAN, true, "Whether the " +
"item will drop materials resulting from salvaging on the ground, instead of putting them into the user's" +
" inventory", true, false),
/**
* The chance of a scrapper returning no salvage, regardless of item condition
*/
FAIL_SALVAGE_CHANCE("failSalvageChance", SettingValueType.POSITIVE_DOUBLE, 0,
"The chance to fail a salvage, thus destroying the item (0-100)",
true, false),
/**
* The setting for which items a scrapper is able to salvage
*/
SALVAGE_ABLE_ITEMS("salvageAbleItems", SettingValueType.REFORGE_ABLE_ITEMS, "",
"The items a blacksmith is able to salvage. Setting this only " +
"allows NPCs to repair the listed items. This should be set for each individual NPC",
true, false),
/**
* The maximum amount of seconds a player may need to wait for the reforging to finish
*/
MAX_SALVAGE_DELAY("maxSalvageWaitTimeSeconds", SettingValueType.POSITIVE_INTEGER, 30,
"The maximum time for a salvaging to finish",
true, false),
/**
* The minimum amount of seconds a player may need to wait for the reforging to finish
*/
MIN_SALVAGE_DELAY("minSalvageWaitTimeSeconds", SettingValueType.POSITIVE_INTEGER, 5,
"The minimum time for a salvaging to finish",
true, false),
/**
* The setting for number of seconds a player has to wait between each usage of the blacksmith
*/
SALVAGE_COOL_DOWN("salvageCoolDownSeconds", SettingValueType.POSITIVE_INTEGER, 60,
"The cool-down period between each salvage",
true, false),
/**
* The setting for the title used to display which kind of scrapper the NPC is
*
* <p>While this should be entirely configurable, values such as armor-scrapper, sword-scrapper and similar, which
* describe the scrapper's specialization, and thus the range of salvageable items, is expected.</p>
*/
SCRAPPER_TITLE("scrapperTitle", SettingValueType.STRING, "scrapper",
"The title describing the scrapper's usage/speciality", true, false),
/**
* The setting for whether the NPC should allow salvaging of any craft-able item instead of just repairable items
*/
EXTENDED_SALVAGE_ENABLED("extendedSalvageEnabled", SettingValueType.BOOLEAN, false,
"Whether to enable salvaging of non-repairable items, such as planks", true, false),
/**
* The setting for whether the NPC should allow salvaging of enchanted items
*/
SALVAGE_ENCHANTED("salvageEnchanted", SettingValueType.BOOLEAN, false,
"Whether to enable salvaging of enchanted items. This is disabled by default because it's " +
"possible to accidentally salvage items with very good enchantments.", true, false),
/**
* The setting for whether the NPC should allow salvaging of armor trims
*/
SALVAGE_ARMOR_TRIMS("salvageArmorTrims", SettingValueType.BOOLEAN, true,
"Whether to enable salvaging of armor trims.", true, false),
/**
* The setting for whether the NPC should allow salvaging netherite items to diamond items
*/
SALVAGE_NETHERITE("salvageNetherite", SettingValueType.BOOLEAN, true,
"Whether to enable salvaging of netherite items", true, false),
/*-----------
| Messages |
-----------*/
/**
* The message displayed when the scrapper is busy with another player
*/
BUSY_WITH_PLAYER_MESSAGE("busyPlayerMessage", SettingValueType.STRING, "&cI'm busy at the moment. Come " +
"back later!", "The message to display when another player is using the scrapper",
true, true),
/**
* The message displayed when the scrapper is busy salvaging the player's item
*/
BUSY_WITH_SALVAGE_MESSAGE("busySalvageMessage", SettingValueType.STRING, "&cI'm working on it. Be " +
"patient! I'll finish {time}!", "The message to display when the blacksmith is working on the " +
"salvaging", true, true),
/**
* The message displayed if the player needs to wait for the cool-down to expire
*/
COOL_DOWN_UNEXPIRED_MESSAGE("coolDownUnexpiredMessage", SettingValueType.STRING,
"&cYou've already had your chance! Give me a break! I'll be ready {time}!",
"The message to display when the blacksmith is still " +
"on a cool-down from the previous re-forging", true, true),
/**
* The message displayed if the scrapper encounters an item they cannot salvage
*/
INVALID_ITEM_MESSAGE("invalidItemMessage", SettingValueType.STRING,
"&cI'm sorry, but I'm a/an {title}, I don't know how to salvage that!",
"The message to display if the player tries to salvage" +
" an item the scrapper cannot salvage", true, true),
/**
* The message displayed if salvaging an item would return in no items
*/
TOO_DAMAGED_FOR_SALVAGE_MESSAGE("tooDamagedForSalvageMessage", SettingValueType.STRING,
"&cThat item is too damaged to be salvaged into anything useful", "The message to display " +
"if salvaging the player's item would result in no salvage", true, true),
/**
* The message displayed if a salvage is successful
*/
SUCCESS_SALVAGE_MESSAGE("successSalvagedMessage", SettingValueType.STRING, "There you go!",
"The message to display when an item is successfully salvaged", true, true),
/**
* The message displayed if a salvage is unsuccessful
*/
FAIL_SALVAGE_MESSAGE("failSalvageMessage", SettingValueType.STRING, "&cWhoops! The item broke! Maybe " +
"next time?", "The message to display when a scrapper fails to salvage an item because of the " +
"fail chance.", true, true),
/**
* The message displayed if a salvage of an item without durability is unsuccessful
*/
FAIL_SALVAGE_EXTENDED_MESSAGE("failExtendedSalvageMessage", SettingValueType.STRING,
"&cWhoops! I was unable to extract much, if any, salvage! Maybe next time?",
"The message to display when the scrapper fails to salvage an item without durability", true, true),
/**
* The message displayed if a player presents a different item after seeing the price to reforge an item
*/
ITEM_UNEXPECTEDLY_CHANGED_MESSAGE("itemChangedMessage", SettingValueType.STRING, "&cThat's not the item" +
" you wanted to salvage before!", "The message to display when presenting a different item than" +
" the one just evaluated", true, true),
/**
* The message displayed when the scrapper starts salvaging an item
*/
START_SALVAGE_MESSAGE("startSalvageMessage", SettingValueType.STRING, "&eOk, let's see what I can do...",
"The message to display once the blacksmith starts re-forging", true, true),
/**
* The message displayed if a player is unable to pay the blacksmith
*/
INSUFFICIENT_FUNDS_MESSAGE("insufficientFundsMessage", SettingValueType.STRING,
"&cYou don't have enough money to salvage an item!",
"The message to display when a player cannot pay for the salvaging", true, true),
/**
* The message displayed when displaying the cost of salvaging the held item to the player
*/
COST_MESSAGE("costMessage", SettingValueType.STRING,
"&eIt will cost &a{cost}&e to salvage that &a{item}&e! {yield} &eClick again to salvage!",
"The message to display when informing a player about the salvaging cost", true, true),
/**
* The message displayed when displaying the cost of salvaging the armor trim of the held item to the player
*/
COST_MESSAGE_ARMOR_TRIM("costMessageArmorTrim", SettingValueType.STRING,
"&eIt will cost &a{cost}&e to salvage that &a{item}&e's armor trim!",
"The message to display when explaining the shown item's armor trim's salvage cost", true, true),
/**
* The message displayed when displaying the cost of salvaging the netherite of the held item to the player
*/
COST_MESSAGE_NETHERITE("costMessageNetherite", SettingValueType.STRING,
"&eIt will cost &a{cost}&e to salvage that &a{item}&e into diamond!",
"The message to display when explaining the shown item's netherite salvage cost", true, true),
/**
* The message displayed when explaining that all items will be returned as salvage
*/
FULL_SALVAGE_MESSAGE("fullSalvageMessage", SettingValueType.STRING,
"&aI should be able to extract all components from that pristine item.&r",
"The message to display when explaining expected full yield as part of the cost message", true, true),
/**
* The message displayed when explaining that only some items will be returned as salvage
*/
PARTIAL_SALVAGE_MESSAGE("partialSalvageMessage", SettingValueType.STRING,
"&cI cannot extract all components from that damaged item.&r",
"The message to display when explaining expected partial yield as part of the cost message", true, true),
/**
* The message displayed when explaining that enchanted item salvage is disabled
*/
CANNOT_SALVAGE_ENCHANTED_MESSAGE("cannotSalvageEnchantedMessage", SettingValueType.STRING,
"&cI'm sorry, but I'm unable to salvage enchanted items!",
"The message to display when asked to salvage an enchanted item, and that option is disabled", true, true),
/**
* The message displayed when explaining that armor trim salvage is disabled
*/
CANNOT_SALVAGE_ARMOR_TRIM_MESSAGE("cannotSalvageArmorTrimMessage", SettingValueType.STRING,
"&cI'm sorry, but I'm unable to salvage armor trims!",
"The message to display when asked to salvage an armor trim, and that option is disabled", true, true),
/**
* The message displayed when explaining that the items to return as armor trim salvage are unknown
*/
ARMOR_TRIM_SALVAGE_NOT_FOUND_MESSAGE("armorTrimSalvageNotFoundMessage", SettingValueType.STRING,
"&cI'm sorry, but I don't know how to salvage that armor trim!",
"The message to display if the correct materials to return for the armor trim are unknown", true, true),
/**
* The message displayed when explaining that netherite salvage is disabled
*/
CANNOT_SALVAGE_NETHERITE_MESSAGE("cannotSalvageNetheriteMessage", SettingValueType.STRING,
"&cI'm sorry, but I'm unable to salvage netherite items!",
"The message to display when asked to salvage netherite items, and that option is disabled", true, true),
/**
* The message displayed when clicking a scrapper with an empty hand
*/
NO_ITEM_MESSAGE("noItemMessage", SettingValueType.STRING, "Please present the item you want to salvage",
"The message to display when a scrapper is clicked with an empty hand", true, true),
/*------------------
| Global settings |
------------------*/
/**
* The setting for the salvage cost
*/
SALVAGE_COST("salvagePrice", SettingValueType.POSITIVE_DOUBLE, 0,
"The cost of using a scrapper to salvage an item", false, false),
/**
* The setting for the armor trim salvage cost
*/
ARMOR_TRIM_SALVAGE_COST("armorTrimSalvagePrice", SettingValueType.POSITIVE_DOUBLE, 5,
"The cost of using the scrapper to remove armor trim", false, false),
/**
* The setting for the netherite salvage cost
*/
NETHERITE_SALVAGE_COST("netheriteSalvagePrice", SettingValueType.POSITIVE_DOUBLE, 15,
"The cost of using the scrapper to remove netherite from an item", false, false),
/**
* Whether to display exact time in minutes and seconds when displaying a remaining cool-down
*/
SHOW_EXACT_TIME("showExactTime", SettingValueType.BOOLEAN, "false", "Exact time displays the " +
"exact number of seconds and minutes remaining as part of the salvaging cool-down and salvaging delay " +
"messages, instead of just vaguely hinting at the remaining time.", false, false),
/**
* Whether to give experience back when salvaging an enchanted item
*/
GIVE_EXPERIENCE("giveExperience", SettingValueType.BOOLEAN, "true", "Whether enchanted " +
"salvaged items should return some amount of exp upon salvage", false, false),
/**
* Which items are ignored when calculating salvage for a given material
*/
IGNORED_SALVAGE("trashSalvage", SettingValueType.STRING_LIST,
new ArrayList<>(List.of("*_SHOVEL;*_PICKAXE;*_AXE;*_HOE;*_SWORD;SHIELD;*_BOW:STICK")),
"Items treated as trash during salvage calculation. This follows the format: " +
"\"MATERIAL[;MATERIAL2][;MATERIAL3]:TRASH_MATERIAL[;TRASH_MATERIAL2]\", so the material or " +
"materials listed will treat the material specified after the \":\" as trash when calculating " +
"salvage unless all non-trash items can be given as well(* matches any character).",
false, false),
;
private final String path;
private final String childPath;
private final Object value;
private final String commandName;
private final SettingValueType valueType;
private final String description;
private final boolean isPerNPC;
private final boolean isMessage;
/**
* Instantiates a new setting
*
* @param key <p>The configuration key for this setting</p>
* @param valueType <p>The type of value used by this setting</p>
* @param value <p>The default value of this setting</p>
* @param description <p>The description describing this setting</p>
* @param isPerNPC <p>Whether this setting is per-NPC or global</p>
* @param isMessage <p>Whether this option is for an NPC message</p>
*/
ScrapperSetting(@NotNull String key, @NotNull SettingValueType valueType, @Nullable Object value,
@NotNull String description, boolean isPerNPC, boolean isMessage) {
if (isPerNPC) {
if (isMessage) {
this.path = "scrapper.defaults.messages." + key;
} else {
this.path = "scrapper.defaults." + key;
}
} else {
this.path = "scrapper.global." + key;
}
this.value = value;
this.valueType = valueType;
this.childPath = key;
if (key.contains(".")) {
String[] pathParts = key.split("\\.");
this.commandName = pathParts[0];
} else {
this.commandName = key;
}
this.description = description;
this.isPerNPC = isPerNPC;
this.isMessage = isMessage;
}
@Override
@NotNull
public String getPath() {
return this.path;
}
@Override
@NotNull
public String getChildPath() {
return this.childPath;
}
@Override
@NotNull
public Object getDefaultValue() {
return this.value;
}
@Override
@NotNull
public String getCommandName() {
return this.commandName;
}
@Override
@NotNull
public SettingValueType getValueType() {
return this.valueType;
}
@Override
@NotNull
public String getDescription() {
return this.description;
}
@Override
public boolean isPerNPC() {
return this.isPerNPC;
}
@Override
public boolean isMessage() {
return this.isMessage;
}
/**
* Gets the scrapper setting specified by the input string
*
* @param input <p>The input to check</p>
* @return <p>The matching scrapper setting, or null if not found</p>
*/
public static @Nullable ScrapperSetting getSetting(@NotNull String input) {
for (ScrapperSetting scrapperSetting : ScrapperSetting.values()) {
if (input.equalsIgnoreCase(scrapperSetting.getCommandName())) {
return scrapperSetting;
}
}
return null;
}
}

View File

@ -0,0 +1,16 @@
package net.knarcraft.blacksmith.container;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Recipe;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* The result of a recipe's salvage
*
* @param recipe <p>The selected recipe</p>
* @param salvage <p>The resulting salvage</p>
*/
public record RecipeResult(@NotNull Recipe recipe, @NotNull List<ItemStack> salvage) {
}

View File

@ -0,0 +1,20 @@
package net.knarcraft.blacksmith.container;
import net.knarcraft.blacksmith.property.SalvageMethod;
import net.knarcraft.blacksmith.property.SalvageState;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* The result of an attempted salvage
*
* @param salvageMethod <p>The salvage method used</p>
* @param salvage <p>The produced salvage</p>
* @param salvageState <p>The state of the salvage result</p>
* @param requiredAmount <p>The amount of items required for the salvage</p>
*/
public record SalvageResult(@NotNull SalvageMethod salvageMethod, @NotNull List<ItemStack> salvage,
@NotNull SalvageState salvageState, int requiredAmount) {
}

View File

@ -0,0 +1,40 @@
package net.knarcraft.blacksmith.event;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.jetbrains.annotations.NotNull;
/**
* The base class for all blacksmith plugin events
*/
@SuppressWarnings("unused")
public abstract class AbstractBlacksmithPluginEvent extends Event implements BlacksmithPluginEvent {
protected final NPC npc;
protected final Player player;
/**
* Instantiates a new blacksmith plugin event
*
* @param npc <p>The NPC involved in the event</p>
* @param player <p>The player involved in the event</p>
*/
public AbstractBlacksmithPluginEvent(@NotNull NPC npc, @NotNull Player player) {
this.npc = npc;
this.player = player;
}
@Override
@NotNull
public NPC getNpc() {
return this.npc;
}
@Override
@NotNull
public Player getPlayer() {
return this.player;
}
}

View File

@ -0,0 +1,25 @@
package net.knarcraft.blacksmith.event;
import org.bukkit.Material;
/**
* An event triggered when a blacksmith or scrapper starts reforging or salvaging an item
*/
@SuppressWarnings("unused")
public interface ActionStartEvent extends BlacksmithPluginEvent {
/**
* Gets the amount of ticks this action will take
*
* @return <p>The duration in ticks</p>
*/
long getActionDurationTicks();
/**
* Gets the appropriate crafting station for this action
*
* @return <p>The appropriate crafting station</p>
*/
Material getCraftingStation();
}

View File

@ -0,0 +1,29 @@
package net.knarcraft.blacksmith.event;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* An event triggered by the Blacksmith plugin
*/
@SuppressWarnings("unused")
public interface BlacksmithPluginEvent {
/**
* Gets the NPC involved in the event
*
* @return <p>The NPC</p>
*/
@NotNull
NPC getNpc();
/**
* Gets the player involved in the event
*
* @return <p>The player</p>
*/
@NotNull
Player getPlayer();
}

View File

@ -0,0 +1,41 @@
package net.knarcraft.blacksmith.event;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* The event triggered when a blacksmith reforging fails
*/
public class BlacksmithReforgeFailEvent extends AbstractBlacksmithPluginEvent implements ReforgeEndEvent {
private static final HandlerList handlers = new HandlerList();
/**
* Instantiates a new blacksmith reforge fail event
*
* @param npc <p>The NPC involved in the event</p>
*/
public BlacksmithReforgeFailEvent(@NotNull NPC npc, @NotNull Player player) {
super(npc, player);
}
/**
* Gets a handler-list containing all event handlers
*
* @return <p>A handler-list with all event handlers</p>
*/
@NotNull
@SuppressWarnings("unused")
public static HandlerList getHandlerList() {
return handlers;
}
@Override
@NotNull
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -0,0 +1,60 @@
package net.knarcraft.blacksmith.event;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* The event triggered when a blacksmith reforging starts
*/
public class BlacksmithReforgeStartEvent extends AbstractBlacksmithPluginEvent implements ActionStartEvent {
private static final HandlerList handlers = new HandlerList();
private final long durationTicks;
private final Material craftingStation;
/**
* Instantiates a new blacksmith reforge start event
*
* @param npc <p>The NPC involved in the event</p>
* @param player <p>The player involved in the event</p>
* @param durationTicks <p>The duration of the reforge</p>
* @param craftingStation <p>The appropriate crafting station for this reforging</p>
*/
public BlacksmithReforgeStartEvent(@NotNull NPC npc, @NotNull Player player, long durationTicks,
@NotNull Material craftingStation) {
super(npc, player);
this.durationTicks = durationTicks;
this.craftingStation = craftingStation;
}
@Override
public long getActionDurationTicks() {
return durationTicks;
}
@Override
public Material getCraftingStation() {
return craftingStation;
}
/**
* Gets a handler-list containing all event handlers
*
* @return <p>A handler-list with all event handlers</p>
*/
@NotNull
@SuppressWarnings("unused")
public static HandlerList getHandlerList() {
return handlers;
}
@Override
@NotNull
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -0,0 +1,42 @@
package net.knarcraft.blacksmith.event;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* The event triggered when a blacksmith reforging succeeds
*/
public class BlacksmithReforgeSucceedEvent extends AbstractBlacksmithPluginEvent implements ReforgeEndEvent {
private static final HandlerList handlers = new HandlerList();
/**
* Instantiates a new blacksmith reforge succeed event
*
* @param npc <p>The NPC involved in the event</p>
* @param player <p>The player involved in the event</p>
*/
public BlacksmithReforgeSucceedEvent(@NotNull NPC npc, @NotNull Player player) {
super(npc, player);
}
/**
* Gets a handler-list containing all event handlers
*
* @return <p>A handler-list with all event handlers</p>
*/
@SuppressWarnings("unused")
@NotNull
public static HandlerList getHandlerList() {
return handlers;
}
@Override
@NotNull
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -0,0 +1,142 @@
package net.knarcraft.blacksmith.event;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* An event triggered when an NPC plays a sound indicating an action
*/
@SuppressWarnings("unused")
public class NPCSoundEvent extends AbstractBlacksmithPluginEvent implements Cancellable {
private static final HandlerList handlers = new HandlerList();
private boolean cancelled;
private float pitch;
private float volume;
private SoundCategory soundCategory;
private Sound sound;
/**
* Instantiates a new NPC sound event
*
* @param npc <p>The NPC playing the sound</p>
* @param player <p>The player whose interaction triggered the sound</p>
* @param soundCategory <p>The category the sound is to play in</p>
* @param sound <p>The sound to play</p>
* @param volume <p>The volume of the played sound</p>
* @param pitch <p>The pitch of the played sound</p>
*/
public NPCSoundEvent(@NotNull NPC npc, @NotNull Player player, @NotNull SoundCategory soundCategory,
@NotNull Sound sound, float volume, float pitch) {
super(npc, player);
this.soundCategory = soundCategory;
this.sound = sound;
this.volume = volume;
this.pitch = pitch;
}
/**
* Gets the pitch of the played sound
*
* @return <p>The pitch of the played sound</p>
*/
public float getPitch() {
return this.pitch;
}
/**
* Gets the volume of the played sound
*
* @return <p>The volume of the played sound</p>
*/
public float getVolume() {
return this.volume;
}
/**
* Gets the category the sound is played in
*
* @return <p>The sound category used</p>
*/
public @NotNull SoundCategory getSoundCategory() {
return this.soundCategory;
}
/**
* Gets the sound to play
*
* @return <p>The sound to play</p>
*/
public @NotNull Sound getSound() {
return this.sound;
}
/**
* Sets the pitch of the played sound
*
* @param pitch <p>The new pitch</p>
*/
public void setPitch(float pitch) {
this.pitch = pitch;
}
/**
* Sets the volume of the played sound
*
* @param volume <p>The new volume</p>
*/
public void setVolume(float volume) {
this.volume = volume;
}
/**
* Sets the sound to play
*
* @param sound <p>The new sound to play</p>
*/
public void setSound(@NotNull Sound sound) {
this.sound = sound;
}
/**
* Sets the category the sound is played in
*
* @param soundCategory <p>The new sound category</p>
*/
public void setSoundCategory(@NotNull SoundCategory soundCategory) {
this.soundCategory = soundCategory;
}
@Override
public boolean isCancelled() {
return this.cancelled;
}
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
/**
* Gets a handler-list containing all event handlers
*
* @return <p>A handler-list with all event handlers</p>
*/
@NotNull
@SuppressWarnings("unused")
public static HandlerList getHandlerList() {
return handlers;
}
@Override
@NotNull
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -0,0 +1,7 @@
package net.knarcraft.blacksmith.event;
/**
* An event triggered when a reforge finishes
*/
public interface ReforgeEndEvent extends BlacksmithPluginEvent {
}

View File

@ -0,0 +1,7 @@
package net.knarcraft.blacksmith.event;
/**
* An event triggered when a salvaging finishes
*/
public interface SalvageEndEvent extends BlacksmithPluginEvent {
}

View File

@ -0,0 +1,42 @@
package net.knarcraft.blacksmith.event;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* The event triggered when a scrapper salvaging fails
*/
public class ScrapperSalvageFailEvent extends AbstractBlacksmithPluginEvent implements SalvageEndEvent {
private static final HandlerList handlers = new HandlerList();
/**
* Instantiates a new scrapper salvage fail event
*
* @param npc <p>The NPC involved in the event</p>
* @param player <p>The player involved in the event</p>
*/
public ScrapperSalvageFailEvent(@NotNull NPC npc, @NotNull Player player) {
super(npc, player);
}
/**
* Gets a handler-list containing all event handlers
*
* @return <p>A handler-list with all event handlers</p>
*/
@SuppressWarnings("unused")
@NotNull
public static HandlerList getHandlerList() {
return handlers;
}
@Override
@NotNull
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -0,0 +1,61 @@
package net.knarcraft.blacksmith.event;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* The event triggered when a scrapper salvaging starts
*/
@SuppressWarnings("unused")
public class ScrapperSalvageStartEvent extends AbstractBlacksmithPluginEvent implements ActionStartEvent {
private static final HandlerList handlers = new HandlerList();
private final long durationTicks;
private final Material craftingStation;
/**
* Instantiates a new scrapper salvage start event
*
* @param npc <p>The NPC involved in the event</p>
* @param player <p>The player involved in the event</p>
* @param durationTicks <p>The duration of the salvage</p>
* @param craftingStation <p>The appropriate crafting station for this salvaging</p>
*/
public ScrapperSalvageStartEvent(@NotNull NPC npc, @NotNull Player player, long durationTicks,
@NotNull Material craftingStation) {
super(npc, player);
this.durationTicks = durationTicks;
this.craftingStation = craftingStation;
}
@Override
public long getActionDurationTicks() {
return this.durationTicks;
}
@Override
public Material getCraftingStation() {
return this.craftingStation;
}
/**
* Gets a handler-list containing all event handlers
*
* @return <p>A handler-list with all event handlers</p>
*/
@SuppressWarnings("unused")
@NotNull
public static HandlerList getHandlerList() {
return handlers;
}
@Override
@NotNull
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -0,0 +1,42 @@
package net.knarcraft.blacksmith.event;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* The event triggered when a scrapper salvaging succeeds
*/
public class ScrapperSalvageSucceedEvent extends AbstractBlacksmithPluginEvent implements SalvageEndEvent {
private static final HandlerList handlers = new HandlerList();
/**
* Instantiates a new scrapper salvage succeed event
*
* @param npc <p>The NPC involved in the event</p>
* @param player <p>The player involved in the event</p>
*/
public ScrapperSalvageSucceedEvent(@NotNull NPC npc, @NotNull Player player) {
super(npc, player);
}
/**
* Gets a handler-list containing all event handlers
*
* @return <p>A handler-list with all event handlers</p>
*/
@SuppressWarnings("unused")
@NotNull
public static HandlerList getHandlerList() {
return handlers;
}
@Override
@NotNull
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -0,0 +1,33 @@
package net.knarcraft.blacksmith.formatting;
import net.citizensnpcs.api.npc.NPC;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.knarlib.util.ColorHelper;
import org.bukkit.entity.Player;
import java.util.List;
/**
* A formatter for formatting displayed messages
*/
public final class BlacksmithStringFormatter {
private BlacksmithStringFormatter() {
}
/**
* Sends a message from a blacksmith NPC to a player
*
* @param npc <p>The NPC sending the message</p>
* @param player <p>The player to send the message to</p>
* @param message <p>The message to send</p>
*/
public static void sendNPCMessage(NPC npc, Player player, String message) {
player.sendMessage(BlacksmithPlugin.getStringFormatter().replacePlaceholders(
BlacksmithTranslatableMessage.NPC_TALK_FORMAT, List.of("{npc}", "{message}"),
List.of(npc.getName(), ColorHelper.translateColorCodes(message, ColorConversion.RGB))));
}
}

View File

@ -1,6 +1,11 @@
package net.knarcraft.blacksmith.formatting;
import static net.knarcraft.blacksmith.formatting.StringFormatter.replacePlaceholders;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.knarlib.formatting.StringReplacer;
import net.knarcraft.knarlib.formatting.TranslatableMessage;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* An enum containing all translatable global messages
@ -8,7 +13,7 @@ import static net.knarcraft.blacksmith.formatting.StringFormatter.replacePlaceho
* <p>This does not include NPC messages as they are configurable per-npc, and can be translated by changing the
* default message values in the main config file.</p>
*/
public enum TranslatableMessage {
public enum BlacksmithTranslatableMessage implements TranslatableMessage {
/**
* The message displayed when a configuration value has been successfully changed
@ -25,6 +30,11 @@ public enum TranslatableMessage {
*/
CURRENT_VALUE,
/**
* The message displayed when showing the default value of a configuration option
*/
DEFAULT_VALUE,
/**
* The message displayed when showing the current value of a configuration option for one material or enchantment
*/
@ -50,11 +60,6 @@ public enum TranslatableMessage {
*/
NO_NPC_SELECTED,
/**
* The message displayed if trying to change the default value of reforge-able item using commands
*/
DEFAULT_REFORGE_ABLE_ITEMS_UNCHANGEABLE,
/**
* The message displayed if a string list is required, but something else is given
*/
@ -105,36 +110,6 @@ public enum TranslatableMessage {
*/
PRESET_MATERIALS,
/**
* The format for displaying the exact duration of a blacksmith's cool-down or delay
*/
DURATION_FORMAT,
/**
* The text to display for 0 seconds
*/
UNIT_NOW,
/**
* The text to display for 1 second
*/
UNIT_SECOND,
/**
* The text to display for a number of seconds
*/
UNIT_SECONDS,
/**
* The text to display for 1 minute
*/
UNIT_MINUTE,
/**
* The text to display for a number of minutes
*/
UNIT_MINUTES,
/**
* The text to display when describing less than 10 seconds remaining
*/
@ -163,7 +138,12 @@ public enum TranslatableMessage {
/**
* The marker used for displaying that a given setting has been overridden for the selected NPC
*/
SETTING_OVERRIDDEN_MARKER;
SETTING_OVERRIDDEN_MARKER,
/**
* The format to use for formatting any message spoken by a blacksmith NPC
*/
NPC_TALK_FORMAT;
/**
* Gets the message to display when displaying the raw value of messages
@ -171,8 +151,9 @@ public enum TranslatableMessage {
* @param rawValue <p>The raw value to display</p>
* @return <p>The message to display</p>
*/
public static String getRawValueMessage(String rawValue) {
return StringFormatter.replacePlaceholder(Translator.getTranslatedMessage(TranslatableMessage.RAW_VALUE),
@NotNull
public static String getRawValueMessage(@NotNull String rawValue) {
return BlacksmithPlugin.getStringFormatter().replacePlaceholder(BlacksmithTranslatableMessage.RAW_VALUE,
"{rawValue}", rawValue);
}
@ -183,9 +164,10 @@ public enum TranslatableMessage {
* @param newValue <p>The new value of the setting</p>
* @return <p>The string to display to a user</p>
*/
public static String getValueChangedMessage(String setting, String newValue) {
return replacePlaceholders(Translator.getTranslatedMessage(TranslatableMessage.VALUE_CHANGED),
new String[]{"{setting}", "{newValue}"}, new String[]{setting, newValue});
@NotNull
public static String getValueChangedMessage(@NotNull String setting, @NotNull String newValue) {
return BlacksmithPlugin.getStringFormatter().replacePlaceholders(BlacksmithTranslatableMessage.VALUE_CHANGED,
List.of("{setting}", "{newValue}"), List.of(setting, newValue));
}
/**
@ -197,10 +179,30 @@ public enum TranslatableMessage {
* @param newValue <p>The new value of the setting</p>
* @return <p>The string to display to a user</p>
*/
public static String getItemValueChangedMessage(String setting, ItemType itemType, String item, String newValue) {
return replacePlaceholders(Translator.getTranslatedMessage(TranslatableMessage.VALUE_FOR_ITEM_CHANGED),
new String[]{"{setting}", "{itemType}", "{item}", "{newValue}"},
new String[]{setting, itemType.getItemTypeName(), item, newValue});
@NotNull
public static String getItemValueChangedMessage(@NotNull String setting, @NotNull ItemType itemType,
@NotNull String item, @NotNull String newValue) {
StringReplacer stringReplacer = new StringReplacer(BlacksmithPlugin.getStringFormatter().getUnFormattedMessage(
BlacksmithTranslatableMessage.VALUE_FOR_ITEM_CHANGED));
stringReplacer.add("{setting}", setting);
stringReplacer.add("{itemType}", itemType.getItemTypeName());
stringReplacer.add("{item}", item);
stringReplacer.add("{newValue}", newValue);
return stringReplacer.replace();
}
/**
* Gets the message to display when displaying a setting's default value
*
* @param setting <p>The setting whose value is shown</p>
* @param defaultValue <p>The default value of the setting</p>
* @return <p>The string to display to a user</p>
*/
@NotNull
public static String getDefaultValueMessage(@NotNull String setting, @NotNull String defaultValue) {
return BlacksmithPlugin.getStringFormatter().replacePlaceholders(BlacksmithTranslatableMessage.DEFAULT_VALUE,
List.of("{setting}", "{defaultValue}"),
List.of(setting, defaultValue));
}
/**
@ -210,9 +212,11 @@ public enum TranslatableMessage {
* @param currentValue <p>The current value of the setting</p>
* @return <p>The string to display to a user</p>
*/
public static String getCurrentValueMessage(String setting, String currentValue) {
return replacePlaceholders(Translator.getTranslatedMessage(TranslatableMessage.CURRENT_VALUE),
new String[]{"{setting}", "{currentValue}"}, new String[]{setting, currentValue});
@NotNull
public static String getCurrentValueMessage(@NotNull String setting, @NotNull String currentValue) {
return BlacksmithPlugin.getStringFormatter().replacePlaceholders(BlacksmithTranslatableMessage.CURRENT_VALUE,
List.of("{setting}", "{currentValue}"),
List.of(setting, currentValue));
}
/**
@ -224,10 +228,22 @@ public enum TranslatableMessage {
* @param currentValue <p>The current value of the setting</p>
* @return <p>The string to display to a user</p>
*/
public static String getItemCurrentValueMessage(String setting, ItemType itemType, String item, String currentValue) {
return replacePlaceholders(Translator.getTranslatedMessage(TranslatableMessage.CURRENT_VALUE_FOR_ITEM),
new String[]{"{setting}", "{itemType}", "{item}", "{currentValue}"},
new String[]{setting, itemType.getItemTypeName(), item, currentValue});
@NotNull
public static String getItemCurrentValueMessage(@NotNull String setting, @NotNull ItemType itemType,
@NotNull String item, @NotNull String currentValue) {
StringReplacer stringReplacer = new StringReplacer(BlacksmithPlugin.getStringFormatter().getUnFormattedMessage(
BlacksmithTranslatableMessage.CURRENT_VALUE_FOR_ITEM));
stringReplacer.add("{setting}", setting);
stringReplacer.add("{itemType}", itemType.getItemTypeName());
stringReplacer.add("{item}", item);
stringReplacer.add("{currentValue}", currentValue);
return stringReplacer.replace();
}
@Override
@NotNull
public TranslatableMessage[] getAllMessages() {
return BlacksmithTranslatableMessage.values();
}
}

View File

@ -1,5 +1,8 @@
package net.knarcraft.blacksmith.formatting;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.knarlib.formatting.StringFormatter;
/**
* An enum representing all item types used in messages
*/
@ -14,9 +17,11 @@ public enum ItemType {
* @return <p>The name of this item type</p>
*/
public String getItemTypeName() {
StringFormatter stringFormatter = BlacksmithPlugin.getStringFormatter();
return switch (this) {
case MATERIAL -> Translator.getTranslatedMessage(TranslatableMessage.ITEM_TYPE_MATERIAL);
case ENCHANTMENT -> Translator.getTranslatedMessage(TranslatableMessage.ITEM_TYPE_ENCHANTMENT);
case MATERIAL -> stringFormatter.getUnFormattedMessage(BlacksmithTranslatableMessage.ITEM_TYPE_MATERIAL);
case ENCHANTMENT ->
stringFormatter.getUnFormattedMessage(BlacksmithTranslatableMessage.ITEM_TYPE_ENCHANTMENT);
};
}

View File

@ -1,128 +0,0 @@
package net.knarcraft.blacksmith.formatting;
import net.citizensnpcs.api.npc.NPC;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A formatter for formatting displayed messages
*/
public final class StringFormatter {
private final static String pluginName = BlacksmithPlugin.getInstance().getDescription().getName();
private StringFormatter() {
}
/**
* Sends a message from a blacksmith NPC to a player
*
* @param npc <p>The NPC sending the message</p>
* @param player <p>The player to send the message to</p>
* @param message <p>The message to send</p>
*/
public static void sendNPCMessage(NPC npc, Player player, String message) {
player.sendMessage(ChatColor.GREEN + "[" + npc.getName() + "] -> You:" + ChatColor.RESET + " " +
translateColors(message));
}
/**
* Displays a message signifying a successful action
*
* @param sender <p>The command sender to display the message to</p>
* @param message <p>The translatable message to display</p>
*/
public static void displaySuccessMessage(CommandSender sender, TranslatableMessage message) {
sender.sendMessage(ChatColor.GREEN + getFormattedMessage(Translator.getTranslatedMessage(message)));
}
/**
* Displays a message signifying a successful action
*
* @param sender <p>The command sender to display the message to</p>
* @param message <p>The raw message to display</p>
*/
public static void displaySuccessMessage(CommandSender sender, String message) {
sender.sendMessage(ChatColor.GREEN + getFormattedMessage(message));
}
/**
* Displays a message signifying an unsuccessful action
*
* @param sender <p>The command sender to display the message to</p>
* @param message <p>The translatable message to display</p>
*/
public static void displayErrorMessage(CommandSender sender, TranslatableMessage message) {
sender.sendMessage(ChatColor.DARK_RED + getFormattedMessage(Translator.getTranslatedMessage(message)));
}
/**
* Gets the formatted version of any chat message
*
* @param message <p>The message to format</p>
* @return <p>The formatted message</p>
*/
private static String getFormattedMessage(String message) {
return "[" + pluginName + "] " + ChatColor.RESET + translateColors(message);
}
/**
* Translates & color codes to proper colors
*
* @param input <p>The input string to translate colors for</p>
* @return <p>The input with color codes translated</p>
*/
private static String translateColors(String input) {
return ChatColor.translateAlternateColorCodes('&', input);
}
/**
* Replaces a placeholder in a string
*
* @param input <p>The input string to replace in</p>
* @param placeholder <p>The placeholder to replace</p>
* @param replacement <p>The replacement value</p>
* @return <p>The input string with the placeholder replaced</p>
*/
public static String replacePlaceholder(String input, String placeholder, String replacement) {
return input.replace(placeholder, replacement);
}
/**
* Replaces placeholders in a string
*
* @param input <p>The input string to replace in</p>
* @param placeholders <p>The placeholders to replace</p>
* @param replacements <p>The replacement values</p>
* @return <p>The input string with placeholders replaced</p>
*/
public static String replacePlaceholders(String input, String[] placeholders, String[] replacements) {
for (int i = 0; i < Math.min(placeholders.length, replacements.length); i++) {
input = replacePlaceholder(input, placeholders[i], replacements[i]);
}
return input;
}
/**
* Translates all found color codes to formatting in a string
*
* @param message <p>The string to search for color codes</p>
* @return <p>The message with color codes translated</p>
*/
public static String translateAllColorCodes(String message) {
message = ChatColor.translateAlternateColorCodes('&', message);
Pattern pattern = Pattern.compile("(#[a-fA-F0-9]{6})");
Matcher matcher = pattern.matcher(message);
while (matcher.find()) {
message = message.replace(matcher.group(), "" + ChatColor.of(matcher.group()));
}
return message;
}
}

View File

@ -1,23 +1,16 @@
package net.knarcraft.blacksmith.formatting;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import static net.knarcraft.blacksmith.formatting.StringFormatter.replacePlaceholder;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
/**
* A utility for formatting a string specifying an amount of time
*/
public final class TimeFormatter {
private static Map<Double, TranslatableMessage[]> timeUnits;
private static List<Double> sortedUnits;
private TimeFormatter() {
}
@ -31,7 +24,7 @@ public final class TimeFormatter {
*/
public static String formatTime(boolean exact, int seconds) {
if (exact) {
return getDurationString(seconds);
return net.knarcraft.knarlib.formatting.TimeFormatter.getDurationString(BlacksmithPlugin.getTranslator(), seconds);
} else {
return formatUnclearTime(seconds);
}
@ -60,10 +53,11 @@ public final class TimeFormatter {
* @return <p>Text describing the time interval</p>
*/
private static String getMessageFromInterval(TimeInterval interval) {
String text = Translator.getTranslatedMessage(TranslatableMessage.valueOf(interval.name()));
String text = BlacksmithPlugin.getStringFormatter().getUnFormattedMessage(
BlacksmithTranslatableMessage.valueOf(interval.name()));
//Choose a random entry if a comma-separated list is provided
if (text != null && text.contains(",")) {
if (text.contains(",")) {
String[] parts = text.split(",");
String randomPart = parts[new Random().nextInt(parts.length)];
if (randomPart != null) {
@ -72,75 +66,11 @@ public final class TimeFormatter {
}
//Use the set message, or use the default
if (text != null && !text.trim().isEmpty()) {
if (!text.trim().isEmpty()) {
return text;
} else {
return interval.getDefaultText();
}
}
/**
* Gets the string used for displaying this sign's duration
*
* @return <p>The string used for displaying this sign's duration</p>
*/
public static String getDurationString(int duration) {
if (duration == 0) {
return Translator.getTranslatedMessage(TranslatableMessage.UNIT_NOW);
} else {
if (sortedUnits == null) {
initializeUnits();
}
for (Double unit : sortedUnits) {
if (duration / unit >= 1) {
double units = round(duration / unit);
return formatDurationString(units, timeUnits.get(unit)[units == 1 ? 0 : 1],
(units * 10) % 10 == 0);
}
}
return formatDurationString(duration, TranslatableMessage.UNIT_SECONDS, false);
}
}
/**
* Rounds a number to its last two digits
*
* @param number <p>The number to round</p>
* @return <p>The rounded number</p>
*/
private static double round(double number) {
return Math.round(number * 100.0) / 100.0;
}
/**
* Formats a duration string
*
* @param duration <p>The duration to display</p>
* @param translatableMessage <p>The time unit to display</p>
* @param castToInt <p>Whether to cast the duration to an int</p>
* @return <p>The formatted duration string</p>
*/
private static String formatDurationString(double duration, TranslatableMessage translatableMessage, boolean castToInt) {
String durationFormat = Translator.getTranslatedMessage(TranslatableMessage.DURATION_FORMAT);
durationFormat = replacePlaceholder(durationFormat, "{unit}",
Translator.getTranslatedMessage(translatableMessage));
return replacePlaceholder(durationFormat, "{time}", castToInt ? String.valueOf((int) duration) :
String.valueOf(duration));
}
/**
* Initializes the mapping of available time units for formatting permission sign duration
*/
private static void initializeUnits() {
double minute = 60;
timeUnits = new HashMap<>();
timeUnits.put(minute, new TranslatableMessage[]{TranslatableMessage.UNIT_MINUTE, TranslatableMessage.UNIT_MINUTES});
timeUnits.put(1D, new TranslatableMessage[]{TranslatableMessage.UNIT_SECOND, TranslatableMessage.UNIT_SECONDS});
sortedUnits = new ArrayList<>(timeUnits.keySet());
Collections.sort(sortedUnits);
Collections.reverse(sortedUnits);
}
}

View File

@ -1,120 +0,0 @@
package net.knarcraft.blacksmith.formatting;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.util.FileHelper;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
/**
* A tool to get strings translated to the correct language
*/
public final class Translator {
private static Map<TranslatableMessage, String> translatedMessages;
private static Map<TranslatableMessage, String> backupTranslatedMessages;
private Translator() {
}
/**
* Loads the languages used by this translator
*/
public static void loadLanguages(String selectedLanguage) {
backupTranslatedMessages = loadTranslatedMessages("en");
translatedMessages = loadCustomTranslatedMessages(selectedLanguage);
if (translatedMessages == null) {
translatedMessages = loadTranslatedMessages(selectedLanguage);
}
}
/**
* Gets a translated version of the given translatable message
*
* @param translatableMessage <p>The message to translate</p>
* @return <p>The translated message</p>
*/
public static String getTranslatedMessage(TranslatableMessage translatableMessage) {
if (translatedMessages == null) {
return "Translated strings not loaded";
}
String translatedMessage;
if (translatedMessages.containsKey(translatableMessage)) {
translatedMessage = translatedMessages.get(translatableMessage);
} else if (backupTranslatedMessages.containsKey(translatableMessage)) {
translatedMessage = backupTranslatedMessages.get(translatableMessage);
} else {
translatedMessage = translatableMessage.toString();
}
return StringFormatter.translateAllColorCodes(translatedMessage);
}
/**
* Loads all translated messages for the given language
*
* @param language <p>The language chosen by the user</p>
* @return <p>A mapping of all strings for the given language</p>
*/
public static Map<TranslatableMessage, String> loadTranslatedMessages(String language) {
try {
BufferedReader reader = FileHelper.getBufferedReaderForInternalFile("/strings.yml");
return loadTranslatableMessages(language, reader);
} catch (FileNotFoundException e) {
BlacksmithPlugin.getInstance().getLogger().log(Level.SEVERE, "Unable to load translated messages");
return null;
}
}
/**
* Tries to load translated messages from a custom strings.yml file
*
* @param language <p>The selected language</p>
* @return <p>The loaded translated strings, or null if no custom language file exists</p>
*/
public static Map<TranslatableMessage, String> loadCustomTranslatedMessages(String language) {
BlacksmithPlugin instance = BlacksmithPlugin.getInstance();
File strings = new File(instance.getDataFolder(), "strings.yml");
if (!strings.exists()) {
instance.getLogger().log(Level.FINEST, "Strings file not found");
return null;
}
try {
instance.getLogger().log(Level.INFO, "Loading custom strings...");
return loadTranslatableMessages(language, new BufferedReader(new InputStreamReader(new FileInputStream(strings))));
} catch (FileNotFoundException e) {
instance.getLogger().log(Level.WARNING, "Unable to load custom messages");
return null;
}
}
/**
* Loads translatable messages from the given reader
*
* @param language <p>The selected language</p>
* @param reader <p>The buffered reader to read from</p>
* @return <p>The loaded translated strings</p>
*/
private static Map<TranslatableMessage, String> loadTranslatableMessages(String language, BufferedReader reader) {
Map<TranslatableMessage, String> translatedMessages = new HashMap<>();
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(reader);
for (TranslatableMessage message : TranslatableMessage.values()) {
String translated = configuration.getString(language + "." + message.toString());
if (translated != null) {
translatedMessages.put(message, translated);
}
}
return translatedMessages;
}
}

View File

@ -1,11 +1,16 @@
package net.knarcraft.blacksmith.listener;
import net.knarcraft.blacksmith.formatting.StringFormatter;
import net.knarcraft.blacksmith.formatting.TranslatableMessage;
import net.citizensnpcs.api.event.NPCRightClickEvent;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage;
import net.knarcraft.blacksmith.trait.BlacksmithTrait;
import net.knarcraft.blacksmith.trait.CustomTrait;
import net.knarcraft.blacksmith.trait.ScrapperTrait;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A listener for detecting and handling a Blacksmith being right-clicked
@ -13,32 +18,47 @@ import org.bukkit.event.Listener;
public class NPCClickListener implements Listener {
@EventHandler
public void onRightClick(net.citizensnpcs.api.event.NPCRightClickEvent event) {
//We only care about blacksmiths
if (!event.getNPC().hasTrait(BlacksmithTrait.class)) {
public void onRightClick(@NotNull NPCRightClickEvent event) {
//We only care about blacksmiths and scrappers
if (event.getNPC().hasTrait(BlacksmithTrait.class)) {
handleNPCClick(event, event.getNPC().getTraitNullable(BlacksmithTrait.class));
} else if (event.getNPC().hasTrait(ScrapperTrait.class)) {
handleNPCClick(event, event.getNPC().getTraitNullable(ScrapperTrait.class));
}
}
/**
* Handles a player clicking on a blacksmith or a scrapper NPC
*
* @param event <p>The NPC click event performed</p>
* @param customTrait <p>The NPC's custom trait</p>
*/
private void handleNPCClick(@NotNull NPCRightClickEvent event, @Nullable CustomTrait<?> customTrait) {
if (customTrait == null) {
BlacksmithPlugin.warn("Could not get trait from NPC!");
return;
}
BlacksmithTrait blacksmithTrait = event.getNPC().getTraitNullable(BlacksmithTrait.class);
//Perform any necessary pre-session work
Player player = event.getClicker();
//Permission check
if (!player.hasPermission("blacksmith.use")) {
StringFormatter.displayErrorMessage(player, TranslatableMessage.PERMISSION_DENIED);
BlacksmithPlugin.getStringFormatter().displayErrorMessage(player,
BlacksmithTranslatableMessage.PERMISSION_DENIED);
return;
}
if (!blacksmithTrait.prepareForSession(player)) {
if (!customTrait.prepareForSession(player)) {
return;
}
if (blacksmithTrait.hasSession()) {
if (customTrait.hasSession()) {
//Continue the existing session
blacksmithTrait.continueSession(player);
customTrait.continueSession(player);
} else {
//Start a new session
blacksmithTrait.startSession(player);
customTrait.startSession(player);
}
}

View File

@ -1,7 +1,9 @@
package net.knarcraft.blacksmith.listener;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPCRegistry;
import net.knarcraft.blacksmith.trait.BlacksmithTrait;
import net.knarcraft.blacksmith.trait.ScrapperTrait;
import org.bukkit.Material;
import org.bukkit.enchantments.EnchantmentTarget;
import org.bukkit.entity.Entity;
@ -13,6 +15,8 @@ import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PlayerListener implements Listener {
@ -22,23 +26,24 @@ public class PlayerListener implements Listener {
* @param event <p>The triggered event</p>
*/
@EventHandler(priority = EventPriority.HIGHEST)
public void onClick(PlayerInteractEvent event) {
public void onClick(@NotNull PlayerInteractEvent event) {
if (event.getHand() == null || (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() !=
Action.RIGHT_CLICK_BLOCK)) {
return;
}
//If the player is not looking at a blacksmith, there is no need to do anything
//If the player is not looking at a blacksmith or scrapper, there is no need to do anything
Entity target = getTarget(event.getPlayer(), event.getPlayer().getNearbyEntities(15, 10, 15));
if (!CitizensAPI.getNPCRegistry().isNPC(target) ||
!CitizensAPI.getNPCRegistry().getNPC(target).hasTrait(BlacksmithTrait.class)) {
NPCRegistry registry = CitizensAPI.getNPCRegistry();
if (!registry.isNPC(target) ||
(!registry.getNPC(target).hasTrait(BlacksmithTrait.class) &&
!registry.getNPC(target).hasTrait(ScrapperTrait.class))) {
return;
}
//Block armor equip if interacting with a blacksmith
//Block armor equip if interacting with a blacksmith or a scrapper
ItemStack usedItem = event.getPlayer().getInventory().getItem(event.getHand());
if (usedItem != null && isArmor(usedItem)) {
event.setUseItemInHand(Event.Result.DENY);
event.getPlayer().updateInventory();
}
}
@ -48,7 +53,7 @@ public class PlayerListener implements Listener {
* @param item <p>The item to check if is armor or not</p>
* @return <p>True if the given item is a type of armor</p>
*/
public static boolean isArmor(ItemStack item) {
public static boolean isArmor(@NotNull ItemStack item) {
return EnchantmentTarget.WEARABLE.includes(item) || item.getType() == Material.ELYTRA;
}
@ -60,7 +65,8 @@ public class PlayerListener implements Listener {
* @param <T> <p>The type of entity the player is looking at</p>
* @return <p>The entity the player is looking at, or null if no such entity exists</p>
*/
private static <T extends Entity> T getTarget(final Entity entity, final Iterable<T> entities) {
@Nullable
private static <T extends Entity> T getTarget(final @Nullable Entity entity, final @NotNull Iterable<T> entities) {
if (entity == null) {
return null;
}
@ -68,8 +74,10 @@ public class PlayerListener implements Listener {
final double threshold = 1;
for (final T other : entities) {
final Vector n = other.getLocation().toVector().subtract(entity.getLocation().toVector());
if (entity.getLocation().getDirection().normalize().crossProduct(n).lengthSquared() < threshold &&
n.normalize().dot(entity.getLocation().getDirection().normalize()) >= 0) {
// Replace if the other entity is nearer, or no target has been found
if (target == null ||
target.getLocation().distanceSquared(entity.getLocation()) >
other.getLocation().distanceSquared(entity.getLocation())) {

View File

@ -1,7 +1,9 @@
package net.knarcraft.blacksmith.manager;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.GlobalSettings;
import net.knarcraft.blacksmith.config.blacksmith.GlobalBlacksmithSettings;
import net.knarcraft.blacksmith.config.scrapper.GlobalScrapperSettings;
import net.knarcraft.blacksmith.property.SalvageMethod;
import net.knarcraft.blacksmith.util.ItemHelper;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.Material;
@ -10,9 +12,7 @@ import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.ServicesManager;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jetbrains.annotations.NotNull;
/**
* A class which deals with everything economy
@ -29,25 +29,35 @@ public class EconomyManager {
* Sets up Vault economy support
*
* @param servicesManager <p>The services manager to use for finding a Vault provider</p>
* @param logger <p>The logger to use for logging</p>
* @return <p>True if Vault was successfully set up</p>
*/
public static boolean setUp(ServicesManager servicesManager, Logger logger) {
public static boolean setUp(@NotNull ServicesManager servicesManager) {
//If already set up, there is nothing to do
if (economy != null) {
return true;
}
return setupVault(servicesManager, logger);
return setupVault(servicesManager);
}
/**
* Gets whether the given player can pay for re-forging their held item
* Gets whether the given player cannot pay for re-forging their held item
*
* @param player <p>The player holding an item</p>
* @return <p>Whether the player can pay for the reforge</p>
* @return <p>Whether the player cannot pay for the reforge</p>
*/
public static boolean canPay(Player player) {
return economy.getBalance(player) - getHeldItemCost(player) >= 0;
public static boolean cannotPayForHeldItemReforge(@NotNull Player player) {
return economy.getBalance(player) - getHeldItemCost(player) < 0;
}
/**
* Gets whether the given player cannot pay for salvaging an item
*
* @param player <p>The player holding an item</p>
* @param salvageMethod <p>The salvage method to check</p>
* @return <p>Whether the player cannot pay for the salvage</p>
*/
public static boolean cannotPayForSalvage(@NotNull Player player, @NotNull SalvageMethod salvageMethod) {
return economy.getBalance(player) - getSalvageCost(salvageMethod) < 0;
}
/**
@ -56,11 +66,38 @@ public class EconomyManager {
* @param player <p>The player holding an item</p>
* @return <p>The formatted cost</p>
*/
public static String formatCost(Player player) {
@NotNull
public static String formatBlacksmithCost(@NotNull Player player) {
double cost = getHeldItemCost(player);
return economy.format(cost);
}
/**
* Gets the human-readable cost of salvaging an item
*
* @param salvageMethod <p>The salvage method to get the cost for</p>
* @return <p>The formatted cost</p>
*/
@NotNull
public static String formatSalvageCost(@NotNull SalvageMethod salvageMethod) {
return economy.format(getSalvageCost(salvageMethod));
}
/**
* Gets the cost of salvaging using the specified method
*
* @param salvageMethod <p>The salvage method to get cost for</p>
* @return <p>The salvage cost</p>
*/
private static double getSalvageCost(@NotNull SalvageMethod salvageMethod) {
GlobalScrapperSettings settings = BlacksmithPlugin.getInstance().getGlobalScrapperSettings();
return switch (salvageMethod) {
case SALVAGE, EXTENDED_SALVAGE -> settings.getSalvageCost();
case NETHERITE -> settings.getNetheriteSalvageCost();
case ARMOR_TRIM -> settings.getArmorTrimSalvageCost();
};
}
/**
* Withdraws the reforging cost from the given player
*
@ -68,8 +105,24 @@ public class EconomyManager {
*
* @param player <p>The player to withdraw from</p>
*/
public static void withdraw(Player player) {
economy.withdrawPlayer(player, getHeldItemCost(player));
public static void withdrawBlacksmith(@NotNull Player player) {
double cost = getHeldItemCost(player);
if (cost > 0) {
economy.withdrawPlayer(player, cost);
}
}
/**
* Withdraws the salvaging cost from the given player
*
* @param player <p>The player to withdraw from</p>
* @param salvageMethod <p>The salvage method to withdraw for</p>
*/
public static void withdrawScrapper(@NotNull Player player, @NotNull SalvageMethod salvageMethod) {
double cost = getSalvageCost(salvageMethod);
if (cost > 0) {
economy.withdrawPlayer(player, cost);
}
}
/**
@ -78,7 +131,7 @@ public class EconomyManager {
* @param player <p>The player to calculate the cost for</p>
* @return <p>The calculated cost</p>
*/
private static double getHeldItemCost(Player player) {
private static double getHeldItemCost(@NotNull Player player) {
return getCost(player.getInventory().getItemInMainHand());
}
@ -88,15 +141,16 @@ public class EconomyManager {
* @param item <p>The item to be repaired</p>
* @return <p>The cost of the repair</p>
*/
private static double getCost(ItemStack item) {
GlobalSettings globalSettings = BlacksmithPlugin.getInstance().getSettings();
private static double getCost(@NotNull ItemStack item) {
GlobalBlacksmithSettings globalBlacksmithSettings = BlacksmithPlugin.getInstance().getGlobalBlacksmithSettings();
Material material = item.getType();
//Calculate the base price
double price = globalSettings.getBasePrice(material);
double price = globalBlacksmithSettings.getBasePrice(material);
// Adjust price based on durability
double pricePerDurabilityPoint = globalSettings.getPricePerDurabilityPoint(material);
if (globalSettings.getUseNaturalCost()) {
double pricePerDurabilityPoint = globalBlacksmithSettings.getPricePerDurabilityPoint(material);
if (globalBlacksmithSettings.getUseNaturalCost()) {
//Cost increases with damage
price += ((double) ItemHelper.getDamage(item)) * pricePerDurabilityPoint;
} else {
@ -106,6 +160,11 @@ public class EconomyManager {
//Increase price for any enchantments
price += getEnchantmentCost(item);
//Override the cost for anvils
if (ItemHelper.isAnvil(material, true)) {
price = globalBlacksmithSettings.getAnvilCost(material);
}
return price;
}
@ -115,8 +174,8 @@ public class EconomyManager {
* @param item <p>The item to calculate enchantment cost for</p>
* @return <p>The resulting enchantment cost</p>
*/
private static double getEnchantmentCost(ItemStack item) {
GlobalSettings settings = BlacksmithPlugin.getInstance().getSettings();
private static double getEnchantmentCost(@NotNull ItemStack item) {
GlobalBlacksmithSettings settings = BlacksmithPlugin.getInstance().getGlobalBlacksmithSettings();
double price = 0;
for (Enchantment enchantment : item.getEnchantments().keySet()) {
price += settings.getEnchantmentCost(enchantment) * item.getEnchantmentLevel(enchantment);
@ -128,10 +187,9 @@ public class EconomyManager {
* Sets up Vault for economy
*
* @param servicesManager <p>The services manager to use for finding a Vault provider</p>
* @param logger <p>The logger to use for logging</p>
* @return <p>True if Vault was successfully set up</p>
*/
private static boolean setupVault(ServicesManager servicesManager, Logger logger) {
private static boolean setupVault(@NotNull ServicesManager servicesManager) {
// Setup Vault
RegisteredServiceProvider<Economy> economyProvider = servicesManager.getRegistration(Economy.class);
if (economyProvider != null) {
@ -139,7 +197,7 @@ public class EconomyManager {
return true;
} else {
// Disable if no economy plugin was found
logger.log(Level.SEVERE, "Failed to load an economy plugin. Disabling...");
BlacksmithPlugin.error("Failed to load an economy plugin. Disabling...");
return false;
}
}

View File

@ -0,0 +1,28 @@
package net.knarcraft.blacksmith.property;
/**
* A representation of the different ways an item can be salvaged
*/
public enum SalvageMethod {
/**
* Salvaging the item normally by returning the item's crafting recipe
*/
SALVAGE,
/**
* Salvaging unrepairable items normally by returning the item's crafting recipe, but with unrestricted
*/
EXTENDED_SALVAGE,
/**
* Removing the armor trim from an item
*/
ARMOR_TRIM,
/**
* Un-doing netherite application for an item
*/
NETHERITE,
}

View File

@ -0,0 +1,23 @@
package net.knarcraft.blacksmith.property;
/**
* The state of trying to find salvage for an item
*/
public enum SalvageState {
/**
* Found useful salvage that can be given to a player
*/
FOUND_SALVAGE,
/**
* The item cannot be salvaged using the used method
*/
INCORRECT_METHOD,
/**
* While the method was correct, no useful salvage was created
*/
NO_SALVAGE,
}

View File

@ -1,40 +1,28 @@
package net.knarcraft.blacksmith.trait;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.util.DataKey;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.NPCSettings;
import net.knarcraft.blacksmith.formatting.TimeFormatter;
import net.knarcraft.blacksmith.config.blacksmith.BlacksmithNPCSettings;
import net.knarcraft.blacksmith.config.blacksmith.BlacksmithSetting;
import net.knarcraft.blacksmith.manager.EconomyManager;
import net.knarcraft.blacksmith.util.ItemHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.enchantments.EnchantmentTarget;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.jetbrains.annotations.NotNull;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import static net.knarcraft.blacksmith.formatting.StringFormatter.replacePlaceholder;
import static net.knarcraft.blacksmith.formatting.StringFormatter.sendNPCMessage;
import static net.knarcraft.blacksmith.formatting.BlacksmithStringFormatter.sendNPCMessage;
/**
* The class representing the Blacksmith NPC trait
*/
public class BlacksmithTrait extends Trait {
public class BlacksmithTrait extends CustomTrait<BlacksmithSetting> {
private final Map<UUID, Calendar> coolDowns = new HashMap<>();
private ReforgeSession session;
private final NPCSettings config;
private long _sessionStart = System.currentTimeMillis();
private final BlacksmithNPCSettings config;
/**
* Instantiates a new blacksmith trait
@ -43,7 +31,8 @@ public class BlacksmithTrait extends Trait {
super("blacksmith");
//This should crash if the blacksmith plugin hasn't been properly registered
Bukkit.getServer().getPluginManager().getPlugin("Blacksmith");
this.config = new NPCSettings(BlacksmithPlugin.getInstance().getSettings());
this.config = new BlacksmithNPCSettings(BlacksmithPlugin.getInstance().getGlobalBlacksmithSettings());
super.setTraitSettings(this.config);
}
/**
@ -51,43 +40,18 @@ public class BlacksmithTrait extends Trait {
*
* @return <p>The current settings for this NPC</p>
*/
public NPCSettings getSettings() {
@NotNull
public BlacksmithNPCSettings getSettings() {
return config;
}
/**
* Gets whether this blacksmith is already in a reforging session
*
* @return <p>Whether already in a reforge session</p>
*/
public boolean hasSession() {
return this.session != null;
}
/**
* Adds a cool-down for the given player's next blacksmith reforge
*
* @param playerUUID <p>The ID of the player to add the cool-down for</p>
* @param waitUntil <p>The time when the player can interact again</p>
*/
public void addCoolDown(UUID playerUUID, Calendar waitUntil) {
coolDowns.put(playerUUID, waitUntil);
}
/**
* Unsets the session of this blacksmith, making it ready for another round
*/
public void unsetSession() {
this.session = null;
}
/**
* Loads all config values stored in citizens' config file for this NPC
*
* @param key <p>The data key used for the config root</p>
*/
@Override
public void load(DataKey key) {
public void load(@NotNull DataKey key) {
config.loadVariables(key);
}
@ -97,141 +61,51 @@ public class BlacksmithTrait extends Trait {
* @param key <p>The data key used for the config root</p>
*/
@Override
public void save(DataKey key) {
public void save(@NotNull DataKey key) {
config.saveVariables(key);
}
/**
* Performs necessary work before the session is started or continued
*
* @param player <p>The player to prepare the session for</p>
* @return <p>True if preparations were successful. False if a session shouldn't be started</p>
*/
public boolean prepareForSession(Player player) {
UUID playerId = player.getUniqueId();
//If cool-down has been disabled after it was set for this player, remove the cool-down
if (config.getDisableCoolDown() && coolDowns.get(playerId) != null) {
coolDowns.remove(playerId);
}
//Deny if permission is missing
if (!player.hasPermission("blacksmith.reforge")) {
return false;
}
//Deny if on cool-down, or remove cool-down if expired
if (coolDowns.get(playerId) != null) {
Calendar calendar = Calendar.getInstance();
if (!calendar.after(coolDowns.get(playerId))) {
int secondDifference = (int) (coolDowns.get(playerId).getTimeInMillis() - calendar.getTimeInMillis()) / 1000;
boolean exactTime = BlacksmithPlugin.getInstance().getSettings().getShowExactTime();
sendNPCMessage(this.npc, player, replacePlaceholder(config.getCoolDownUnexpiredMessage(),
"{time}", TimeFormatter.formatTime(exactTime, secondDifference)));
return false;
}
coolDowns.remove(playerId);
}
//If already in a session, but the player has failed to interact, and left the blacksmith, allow a new session
if (session != null) {
if (!session.isRunning() && (System.currentTimeMillis() > _sessionStart + 10 * 1000 ||
this.npc.getEntity().getLocation().distance(session.getPlayer().getLocation()) > 20)) {
session = null;
}
}
return true;
}
/**
* Tries to continue the session for the given player
*
* @param player <p>The player to continue the session for</p>
*/
public void continueSession(Player player) {
//Another player is using the blacksmith
if (!session.isInSession(player)) {
sendNPCMessage(this.npc, player, config.getBusyWithPlayerMessage());
return;
}
//The blacksmith is already reforging for the player
if (session.isRunning()) {
int timeRemaining = (int) ((session.getFinishTime() - System.currentTimeMillis()) / 1000);
boolean showExactTime = BlacksmithPlugin.getInstance().getSettings().getShowExactTime();
sendNPCMessage(this.npc, player, replacePlaceholder(config.getBusyReforgingMessage(), "{time}",
TimeFormatter.formatTime(showExactTime, timeRemaining)));
return;
}
if (session.endSession()) {
//Quit if the player cannot afford, or has changed their item
session = null;
} else {
//Start reforging for the player
reforge(npc, player);
}
}
/**
* Starts a new session, and prepares to repair the player's item
*
* @param player <p>The player to start the session for</p>
*/
public void startSession(Player player) {
@Override
public void startSession(@NotNull Player player) {
ItemStack hand = player.getInventory().getItemInMainHand();
if (hand.getType().isAir()) {
sendNPCMessage(this.npc, player, config.getNoItemMessage());
return;
}
//Refuse if not repairable, or if reforge-able items is set, but doesn't include the held item
List<Material> reforgeAbleItems = config.getReforgeAbleItems();
if (!isRepairable(hand) || (!reforgeAbleItems.isEmpty() && !reforgeAbleItems.contains(hand.getType()))) {
String invalidMessage = replacePlaceholder(config.getInvalidItemMessage(),
boolean notHoldingAnvil = !this.config.getRepairAnvils() || !ItemHelper.isAnvil(hand.getType(), false);
boolean notHoldingRepairable = !ItemHelper.isRepairable(hand) ||
(!reforgeAbleItems.isEmpty() && !reforgeAbleItems.contains(hand.getType()));
if (notHoldingAnvil && notHoldingRepairable) {
String invalidMessage = StringFormatter.replacePlaceholder(config.getInvalidItemMessage(),
"{title}", config.getBlacksmithTitle());
sendNPCMessage(this.npc, player, invalidMessage);
return;
}
if (ItemHelper.getDamage(hand) == 0) {
if (ItemHelper.getDamage(hand) == 0 && !ItemHelper.isAnvil(hand.getType(), true)) {
sendNPCMessage(this.npc, player, config.getNotDamagedMessage());
return;
}
//Start a new reforge session for the player
_sessionStart = System.currentTimeMillis();
currentSessionStartTime = System.currentTimeMillis();
session = new ReforgeSession(this, player, npc, config);
//Tell the player the cost of repairing the item
String cost = EconomyManager.formatCost(player);
String cost = EconomyManager.formatBlacksmithCost(player);
String itemName = hand.getType().name().toLowerCase().replace('_', ' ');
sendNPCMessage(this.npc, player, config.getCostMessage().replace("{cost}", cost).replace("{item}",
itemName));
}
/**
* Starts reforging the player's item
*
* @param npc <p>The NPC performing the reforge</p>
* @param player <p>The player that initiated the reforge</p>
*/
private void reforge(NPC npc, Player player) {
sendNPCMessage(this.npc, player, config.getStartReforgeMessage());
EconomyManager.withdraw(player);
session.beginReforge();
ItemStack heldItem = player.getInventory().getItemInMainHand();
//Display the item in the NPC's hand
if (npc.getEntity() instanceof Player) {
((Player) npc.getEntity()).getInventory().setItemInMainHand(heldItem);
} else {
Objects.requireNonNull(((LivingEntity) npc.getEntity()).getEquipment()).setItemInMainHand(heldItem);
}
//Remove the item from the player's inventory
player.getInventory().setItemInMainHand(null);
}
/**
* Gets whether the given item is repairable
*
* @param item <p>The item to check</p>
* @return <p>True if the item is repairable</p>
*/
public static boolean isRepairable(ItemStack item) {
return item.getItemMeta() instanceof Damageable && EnchantmentTarget.BREAKABLE.includes(item);
@Override
protected boolean showExactTime() {
return BlacksmithPlugin.getInstance().getGlobalBlacksmithSettings().getShowExactTime();
}
}

View File

@ -0,0 +1,210 @@
package net.knarcraft.blacksmith.trait;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.trait.Trait;
import net.knarcraft.blacksmith.config.Setting;
import net.knarcraft.blacksmith.config.Settings;
import net.knarcraft.blacksmith.config.TraitSettings;
import net.knarcraft.blacksmith.formatting.TimeFormatter;
import net.knarcraft.blacksmith.manager.EconomyManager;
import net.knarcraft.knarlib.formatting.StringFormatter;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import static net.knarcraft.blacksmith.formatting.BlacksmithStringFormatter.sendNPCMessage;
/**
* A custom trait that utilizes sessions
*/
public abstract class CustomTrait<K extends Setting> extends Trait {
protected Session session;
protected TraitSettings<K> config;
protected final Map<UUID, Calendar> coolDowns = new HashMap<>();
protected long currentSessionStartTime = System.currentTimeMillis();
/**
* Instantiates a new custom trait
*
* @param name <p>The name of the new trait</p>
*/
protected CustomTrait(String name) {
super(name);
}
/**
* Sets the trait settings object containing this trait's configuration
*
* @param config <p>The trait settings to use</p>
*/
protected void setTraitSettings(TraitSettings<K> config) {
this.config = config;
}
/**
* Gets the settings used by this trait
*
* @return <p>The settings used by this trait</p>
*/
public @Nullable Settings<K> getTraitSettings() {
return this.config;
}
/**
* Starts a new session, and prepares to repair the player's item
*
* @param player <p>The player to start the session for</p>
*/
public abstract void startSession(@NotNull Player player);
/**
* Tries to continue the session for the given player
*
* @param player <p>The player to continue the session for</p>
*/
public void continueSession(@NotNull Player player) {
//Another player is using the blacksmith
if (session.isNotInSession(player)) {
sendNPCMessage(this.npc, player, config.getBusyWithPlayerMessage());
return;
}
//The blacksmith is already reforging for the player
if (session.isRunning()) {
int timeRemaining = (int) ((session.getFinishTime() - System.currentTimeMillis()) / 1000);
boolean showExactTime = showExactTime();
sendNPCMessage(this.npc, player, StringFormatter.replacePlaceholder(config.getBusyWorkingMessage(),
"{time}", TimeFormatter.formatTime(showExactTime, timeRemaining)));
return;
}
if (session.isSessionInvalid()) {
//Quit if the player cannot afford, or has changed their item
session = null;
} else {
//Start reforging for the player
startMainAction(npc, player);
}
}
/**
* Gets whether this blacksmith is already in a reforging session
*
* @return <p>Whether already in a salvage session</p>
*/
public boolean hasSession() {
return this.session != null;
}
/**
* Adds a cool-down for the given player's next blacksmith reforge
*
* @param playerUUID <p>The ID of the player to add the cool-down for</p>
* @param waitUntil <p>The time when the player can interact again</p>
*/
public void addCoolDown(@NotNull UUID playerUUID, @NotNull Calendar waitUntil) {
coolDowns.put(playerUUID, waitUntil);
}
/**
* Unsets the session of this scrapper, making it ready for another round
*/
public void unsetSession() {
this.session = null;
}
/**
* Performs necessary work before the session is started or continued
*
* @param player <p>The player to prepare the session for</p>
* @return <p>True if preparations were successful. False if a session shouldn't be started</p>
*/
public boolean prepareForSession(@NotNull Player player) {
UUID playerId = player.getUniqueId();
//If cool-down has been disabled after it was set for this player, remove the cool-down
if (config.getDisableCoolDown() && coolDowns.get(playerId) != null) {
coolDowns.remove(playerId);
}
//Deny if permission is missing
if (!player.hasPermission("blacksmith.use")) {
return false;
}
//Deny if on cool-down, or remove cool-down if expired
if (coolDowns.get(playerId) != null) {
Calendar calendar = Calendar.getInstance();
if (!calendar.after(coolDowns.get(playerId))) {
int secondDifference = (int) (coolDowns.get(playerId).getTimeInMillis() - calendar.getTimeInMillis()) / 1000;
boolean exactTime = showExactTime();
sendNPCMessage(this.npc, player, StringFormatter.replacePlaceholder(config.getCoolDownUnexpiredMessage(),
"{time}", TimeFormatter.formatTime(exactTime, secondDifference)));
return false;
}
coolDowns.remove(playerId);
}
//If already in a session, but the player has failed to interact, and left the blacksmith, allow a new session
if (session != null && !session.isRunning() && (System.currentTimeMillis() > currentSessionStartTime + 10 * 1000 ||
this.npc.getEntity().getLocation().distance(session.getPlayer().getLocation()) > 20)) {
session = null;
}
return true;
}
/**
* Gets whether to show exact time when displaying the remaining time left
*
* @return <p>Whether to show exact time, or vague words</p>
*/
protected abstract boolean showExactTime();
/**
* Starts reforging the player's item
*
* @param npc <p>The NPC performing the reforge</p>
* @param player <p>The player that initiated the reforge</p>
*/
private void startMainAction(@NotNull NPC npc, @NotNull Player player) {
sendNPCMessage(this.npc, player, config.getStartWorkingMessage());
boolean isBlacksmith = this instanceof BlacksmithTrait;
if (isBlacksmith) {
EconomyManager.withdrawBlacksmith(player);
} else if (this.session instanceof SalvageSession salvageSession) {
EconomyManager.withdrawScrapper(player, salvageSession.salvageMethod);
}
session.scheduleAction();
PlayerInventory playerInventory = player.getInventory();
ItemStack heldItem = player.getInventory().getItemInMainHand();
//Display the item in the NPC's hand
if (npc.getEntity() instanceof Player) {
((Player) npc.getEntity()).getInventory().setItemInMainHand(heldItem);
} else {
Objects.requireNonNull(((LivingEntity) npc.getEntity()).getEquipment()).setItemInMainHand(heldItem);
}
//Remove the item from the player's inventory
if (this.session instanceof SalvageSession salvageSession) {
// For scrappers, just reduce the amounts of items, unless the remaining stack is salvaged
int amount = salvageSession.getItemsConsumed();
if (amount != heldItem.getAmount()) {
heldItem.setAmount(heldItem.getAmount() - amount);
playerInventory.setItemInMainHand(heldItem);
return;
}
}
playerInventory.setItemInMainHand(null);
}
}

View File

@ -2,41 +2,39 @@ package net.knarcraft.blacksmith.trait;
import net.citizensnpcs.api.npc.NPC;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.NPCSettings;
import net.knarcraft.blacksmith.config.blacksmith.BlacksmithNPCSettings;
import net.knarcraft.blacksmith.event.BlacksmithReforgeFailEvent;
import net.knarcraft.blacksmith.event.BlacksmithReforgeSucceedEvent;
import net.knarcraft.blacksmith.manager.EconomyManager;
import net.knarcraft.blacksmith.util.InputParsingHelper;
import net.knarcraft.blacksmith.util.ItemHelper;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Registry;
import org.bukkit.Sound;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.scheduler.BukkitScheduler;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.logging.Level;
import static net.knarcraft.blacksmith.formatting.StringFormatter.sendNPCMessage;
import static net.knarcraft.blacksmith.formatting.BlacksmithStringFormatter.sendNPCMessage;
/**
* A representation of the session between a player and a blacksmith
*/
public class ReforgeSession implements Runnable {
public class ReforgeSession extends Session implements Runnable {
private final BlacksmithTrait blacksmithTrait;
private final Player player;
private final NPC npc;
private final ItemStack itemToReforge;
private int taskId;
private long finishTime = 0;
private final NPCSettings config;
private static final String[] enchantments = new String[Enchantment.values().length];
private static final Random random = new Random();
private final BlacksmithNPCSettings config;
private static List<String> enchantments = null;
/**
* Instantiates a new session
@ -46,29 +44,57 @@ public class ReforgeSession implements Runnable {
* @param npc <p>The Blacksmith NPC involved in the session</p>
* @param config <p>The config to use for the session</p>
*/
ReforgeSession(BlacksmithTrait blacksmithTrait, Player player, NPC npc, NPCSettings config) {
ReforgeSession(@NotNull BlacksmithTrait blacksmithTrait, @NotNull Player player, @NotNull NPC npc,
@NotNull BlacksmithNPCSettings config) {
super(player, npc);
this.blacksmithTrait = blacksmithTrait;
this.player = player;
this.npc = npc;
this.itemToReforge = player.getInventory().getItemInMainHand();
this.config = config;
//Populate enchantments the first time this is run
if (enchantments[0] == null) {
int i = 0;
for (Enchantment enchantment : Enchantment.values()) {
enchantments[i++] = enchantment.getKey().getKey();
if (enchantments == null) {
Registry<Enchantment> enchantmentRegistry = Bukkit.getRegistry(Enchantment.class);
if (enchantmentRegistry == null) {
throw new RuntimeException("Unable to get enchantment registry");
}
enchantments = new ArrayList<>();
for (Enchantment enchantment : enchantmentRegistry) {
enchantments.add(enchantment.getKey().getKey());
}
}
}
/**
* Gets the time in milliseconds when this reforging session will finish
*
* @return <p>The time the reforging will finish</p>
*/
public long getFinishTime() {
return this.finishTime;
@Override
public boolean isSessionInvalid() {
// Prevent player from switching items during session
ItemStack itemInHand = this.player.getInventory().getItemInMainHand();
if (!itemToReforge.equals(itemInHand)) {
sendNPCMessage(this.npc, this.player, this.config.getItemChangedMessage());
return true;
}
// The player is unable to pay
if (EconomyManager.cannotPayForHeldItemReforge(this.player)) {
sendNPCMessage(this.npc, this.player, this.config.getInsufficientFundsMessage());
return true;
}
return false;
}
@Override
protected int getActionDelay() {
if (this.config.getMaxReforgeDelay() > 0) {
//Finish the reforging after a random delay between the max and min
return new Random().nextInt(this.config.getMaxReforgeDelay()) + this.config.getMinReforgeDelay();
} else {
//Finish the salvaging as soon as possible
return 0;
}
}
@Override
protected @NotNull Material getCraftingStation() {
return Material.ANVIL;
}
/**
@ -76,35 +102,35 @@ public class ReforgeSession implements Runnable {
*/
@Override
public void run() {
sendNPCMessage(this.npc, player, reforgeItem() ? config.getSuccessMessage() : config.getFailMessage());
boolean success = reforgeItem();
sendNPCMessage(this.npc, this.player, success ? this.config.getSuccessMessage() : this.config.getFailMessage());
playSound(success ? Sound.BLOCK_ANVIL_USE : Sound.ENTITY_VILLAGER_NO);
//Stop the reforged item from displaying in the blacksmith's hand
if (npc.getEntity() instanceof Player) {
((Player) npc.getEntity()).getInventory().setItemInMainHand(null);
if (this.npc.getEntity() instanceof Player) {
((Player) this.npc.getEntity()).getInventory().setItemInMainHand(null);
} else {
Objects.requireNonNull(((LivingEntity) npc.getEntity()).getEquipment()).setItemInMainHand(null);
Objects.requireNonNull(((LivingEntity) this.npc.getEntity()).getEquipment()).setItemInMainHand(null);
}
//Give the item back to the player
if (!config.getDisableDelay()) {
//If the player isn't online, or the player cannot fit the item, drop the item to prevent it from disappearing
if (config.getDropItem() || !player.isOnline() || player.getInventory().firstEmpty() == -1) {
player.getWorld().dropItemNaturally(npc.getEntity().getLocation(), itemToReforge);
} else {
player.getInventory().addItem(itemToReforge);
}
} else {
//It can be assumed as this happens instantly, that the player still has the item's previous slot selected
player.getInventory().setItemInMainHand(itemToReforge);
}
giveReforgedItem();
//Mark this blacksmith as available
blacksmithTrait.unsetSession();
this.blacksmithTrait.unsetSession();
// Start cool-down
Calendar wait = Calendar.getInstance();
wait.add(Calendar.SECOND, config.getReforgeCoolDown());
blacksmithTrait.addCoolDown(player.getUniqueId(), wait);
wait.add(Calendar.SECOND, this.config.getReforgeCoolDown());
this.blacksmithTrait.addCoolDown(this.player.getUniqueId(), wait);
}
/**
* Gives the reforged item back to the player
*/
private void giveReforgedItem() {
giveResultingItem(this.config.getMaxReforgeDelay() > 0, this.config.getDropItem(), this.npc,
this.itemToReforge);
}
/**
@ -113,11 +139,13 @@ public class ReforgeSession implements Runnable {
* @return <p>Whether the reforge was successful. False if the blacksmith failed to fully repair the item.</p>
*/
private boolean reforgeItem() {
if (random.nextInt(100) < config.getFailChance()) {
if (random.nextInt(100) < this.config.getFailChance()) {
failReforge();
BlacksmithPlugin.getInstance().callEvent(new BlacksmithReforgeFailEvent(this.npc, this.player));
return false;
} else {
succeedReforge();
BlacksmithPlugin.getInstance().callEvent(new BlacksmithReforgeSucceedEvent(this.npc, this.player));
return true;
}
}
@ -127,26 +155,38 @@ public class ReforgeSession implements Runnable {
*/
private void succeedReforge() {
// Remove any damage done to the item
updateDamage(itemToReforge, 0);
// Add random enchantments
int roll = random.nextInt(100);
if (!(roll < config.getExtraEnchantmentChance() &&
itemToReforge.getEnchantments().keySet().size() < config.getMaxEnchantments())) {
// Abort if randomness isn't on our side, or if max enchantments has been reached
return;
if (ItemHelper.updateDamage(this.itemToReforge, 0)) {
BlacksmithPlugin.warn("Unable to update damage for " + this.itemToReforge);
}
//Replace damaged anvils with a normal anvil
if (ItemHelper.isAnvil(this.itemToReforge.getType(), true)) {
this.itemToReforge.setType(Material.ANVIL);
}
//See if a random roll (0-99) is less than extraEnchantmentChance, and add a random enchantment
int roll = random.nextInt(100);
if (roll < this.config.getExtraEnchantmentChance() &&
this.itemToReforge.getEnchantments().keySet().size() < this.config.getMaxEnchantments() &&
!ItemHelper.isAnvil(this.itemToReforge.getType(), false)) {
addRandomEnchantment();
}
}
/**
* Adds a random enchantment to the currently reforged item
*/
private void addRandomEnchantment() {
//Find usable enchantments first
List<Enchantment> usableEnchantments = new ArrayList<>();
for (String enchantmentName : enchantments) {
Enchantment enchantment = InputParsingHelper.matchEnchantment(enchantmentName);
if (enchantment != null && enchantment.canEnchantItem(itemToReforge)) {
if (enchantment != null && enchantment.canEnchantItem(this.itemToReforge)) {
usableEnchantments.add(enchantment);
}
}
//Remove any enchantments in the block list
usableEnchantments.removeAll(blacksmithTrait.getSettings().getEnchantmentBlocklist());
usableEnchantments.removeAll(this.blacksmithTrait.getSettings().getEnchantmentBlockList());
//In case all usable enchantments have been blocked, abort
if (usableEnchantments.isEmpty()) {
@ -159,10 +199,10 @@ public class ReforgeSession implements Runnable {
int randomBound = randomEnchantment.getMaxLevel() + 1;
//A workaround for the random method's bound sometimes being negative
if (randomBound >= 0) {
int existingLevel = itemToReforge.getEnchantmentLevel(randomEnchantment);
int existingLevel = this.itemToReforge.getEnchantmentLevel(randomEnchantment);
/* Add a random enchantment whose level is no lower than the start level, and no lower than the
existing level (to prevent making the item worse) */
itemToReforge.addEnchantment(randomEnchantment, Math.max(Math.max(random.nextInt(randomBound),
this.itemToReforge.addEnchantment(randomEnchantment, Math.max(Math.max(random.nextInt(randomBound),
randomEnchantment.getStartLevel()), existingLevel));
}
}
@ -172,112 +212,28 @@ public class ReforgeSession implements Runnable {
* The method to run when a blacksmith fails re-forging an item
*/
private void failReforge() {
// Remove or downgrade existing enchantments
for (Enchantment enchantment : itemToReforge.getEnchantments().keySet()) {
if (random.nextBoolean()) {
itemToReforge.removeEnchantment(enchantment);
} else {
if (itemToReforge.getEnchantmentLevel(enchantment) > 1) {
itemToReforge.removeEnchantment(enchantment);
itemToReforge.addEnchantment(enchantment, 1);
}
}
if (this.config.getFailRemovesEnchantments()) {
removeOrDowngradeEnchantments();
}
//Damage the item
short currentItemDurability = ItemHelper.getDurability(itemToReforge);
short newDurability = (short) (currentItemDurability + (currentItemDurability * random.nextInt(8)));
short maxDurability = itemToReforge.getType().getMaxDurability();
if (newDurability <= 0) {
newDurability = (short) (maxDurability / 3);
} else if (currentItemDurability + newDurability > maxDurability) {
newDurability = (short) (maxDurability - random.nextInt(maxDurability - 25));
}
updateDamage(itemToReforge, maxDurability - newDurability);
damageItemRandomly(this.itemToReforge);
}
/**
* Updates the damage done to an item
*
* @param item <p>The item to update damage for</p>
* @param newDamage <p>The new damage done</p>
* Removes or downgrades all enchantments for the currently reforged item
*/
private void updateDamage(ItemStack item, int newDamage) {
ItemMeta meta = item.getItemMeta();
Damageable damageable = (Damageable) meta;
if (damageable != null) {
damageable.setDamage(newDamage);
} else {
BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING, "Unable to change damage of " + item);
private void removeOrDowngradeEnchantments() {
//Remove or downgrade existing enchantments
for (Enchantment enchantment : this.itemToReforge.getEnchantments().keySet()) {
//Completely remove the enchantment, downgrade it, or keep it if lucky and already level 1
if (random.nextBoolean()) {
this.itemToReforge.removeEnchantment(enchantment);
} else if (this.itemToReforge.getEnchantmentLevel(enchantment) > 1) {
this.itemToReforge.removeEnchantment(enchantment);
this.itemToReforge.addEnchantment(enchantment, 1);
}
item.setItemMeta(meta);
}
/**
* Gets whether to end the current session
*
* <p>If the player has switched their item, or the player cannot pay, this returns true.</p>
*
* @return <p>True if the current session should end</p>
*/
public boolean endSession() {
// Prevent player from switching items during session
ItemStack itemInHand = player.getInventory().getItemInMainHand();
if (!itemToReforge.equals(itemInHand)) {
sendNPCMessage(this.npc, player, config.getItemChangedMessage());
return true;
}
// The player is unable to pay
if (!EconomyManager.canPay(player)) {
sendNPCMessage(this.npc, player, config.getInsufficientFundsMessage());
return true;
}
return false;
}
/**
* Gets whether the current session is still running
*
* @return <p>True if the current session is still running</p>
*/
public boolean isRunning() {
return BlacksmithPlugin.getInstance().getServer().getScheduler().isQueued(taskId);
}
/**
* Gets whether the given player is currently in a reforging session
*
* @param other <p>The player to check if is in session</p>
* @return <p>True if the given player is in a reforge session</p>
*/
public boolean isInSession(Player other) {
return player.getName().equals(other.getName());
}
/**
* Begins the actual item reforging
*/
public void beginReforge() {
BukkitScheduler scheduler = BlacksmithPlugin.getInstance().getServer().getScheduler();
int reforgeDelay;
if (!config.getDisableCoolDown()) {
//Finish the reforging after a random delay between the max and min
reforgeDelay = new Random().nextInt(config.getMaxReforgeDelay()) + config.getMinReforgeDelay();
} else {
//Finish the reforging as soon as possible
reforgeDelay = 0;
}
this.finishTime = System.currentTimeMillis() + (reforgeDelay * 1000L);
taskId = scheduler.scheduleSyncDelayedTask(BlacksmithPlugin.getInstance(), this, reforgeDelay * 20L);
}
/**
* Gets the player currently in this reforge session
*
* @return <p>The player currently in this reforge session</p>
*/
public Player getPlayer() {
return player;
}
}

View File

@ -0,0 +1,184 @@
package net.knarcraft.blacksmith.trait;
import net.citizensnpcs.api.npc.NPC;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.scrapper.ScrapperNPCSettings;
import net.knarcraft.blacksmith.event.ScrapperSalvageFailEvent;
import net.knarcraft.blacksmith.event.ScrapperSalvageSucceedEvent;
import net.knarcraft.blacksmith.manager.EconomyManager;
import net.knarcraft.blacksmith.property.SalvageMethod;
import net.knarcraft.blacksmith.util.ItemHelper;
import net.knarcraft.blacksmith.util.SalvageHelper;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import static net.knarcraft.blacksmith.formatting.BlacksmithStringFormatter.sendNPCMessage;
/**
* A representation of the session between a player and a scrapper
*/
public class SalvageSession extends Session implements Runnable {
private final ScrapperTrait scrapperTrait;
private final ItemStack itemToSalvage;
private final ScrapperNPCSettings config;
private final List<ItemStack> salvage;
private final int itemsConsumed;
private final int enchantmentLevels;
protected final SalvageMethod salvageMethod;
private static final Random random = new Random();
/**
* Instantiates a new session
*
* @param scrapperTrait <p>A reference to the scrapper trait</p>
* @param player <p>The player initiating the session</p>
* @param npc <p>The scrapper NPC involved in the session</p>
* @param config <p>The config to use for the session</p>
* @param salvageMethod <p>The salvage method performed in this session</p>
* @param itemsConsumed <p>The number of items actually consumed, in case a salvaging fails</p>
*/
public SalvageSession(@NotNull ScrapperTrait scrapperTrait, @NotNull Player player, @NotNull NPC npc,
@NotNull ScrapperNPCSettings config, @NotNull List<ItemStack> salvage,
@NotNull SalvageMethod salvageMethod, int itemsConsumed) {
super(player, npc);
this.scrapperTrait = scrapperTrait;
this.itemToSalvage = player.getInventory().getItemInMainHand().clone();
this.config = config;
this.salvage = salvage;
this.enchantmentLevels = SalvageHelper.getTotalEnchantmentLevels(this.itemToSalvage);
this.salvageMethod = salvageMethod;
this.itemsConsumed = itemsConsumed;
}
@Override
public boolean isSessionInvalid() {
// Prevent player from switching items during session
ItemStack itemInHand = this.player.getInventory().getItemInMainHand();
if (!itemToSalvage.equals(itemInHand)) {
sendNPCMessage(this.npc, this.player, this.config.getItemChangedMessage());
return true;
}
if (EconomyManager.cannotPayForSalvage(this.player, this.salvageMethod)) {
sendNPCMessage(this.npc, this.player, this.config.getInsufficientFundsMessage());
return true;
}
return false;
}
@Override
protected int getActionDelay() {
if (this.config.getMaxSalvageDelay() > 0) {
//Finish the salvaging after a random delay between the max and min
return new Random().nextInt(this.config.getMaxSalvageDelay()) + this.config.getMinSalvageDelay();
} else {
//Finish the salvaging as soon as possible
return 0;
}
}
@Override
protected @NotNull Material getCraftingStation() {
return switch (this.salvageMethod) {
case EXTENDED_SALVAGE -> Material.CRAFTING_TABLE;
case SALVAGE -> Material.ANVIL;
case NETHERITE, ARMOR_TRIM -> Material.SMITHING_TABLE;
};
}
/**
* Runs the actual salvage which fixes the item and gives it back to the player
*/
@Override
public void run() {
salvageItem();
//Stop the reforged item from displaying in the scrapper's hand
if (this.npc.getEntity() instanceof Player) {
((Player) this.npc.getEntity()).getInventory().setItemInMainHand(null);
} else {
Objects.requireNonNull(((LivingEntity) this.npc.getEntity()).getEquipment()).setItemInMainHand(null);
}
//Mark this scrapper as available
this.scrapperTrait.unsetSession();
// Start cool-down
Calendar wait = Calendar.getInstance();
wait.add(Calendar.SECOND, this.config.getSalvageCoolDown());
this.scrapperTrait.addCoolDown(this.player.getUniqueId(), wait);
}
/**
* Gets the number of items consumed in order to perform the salvage
*
* @return <p>The number of items consumed as part of this salvage</p>
*/
public int getItemsConsumed() {
return this.itemsConsumed;
}
/**
* Trues to salvage an item, and gives the return item
*/
private void salvageItem() {
if (random.nextInt(100) < this.config.getFailChance()) {
playSound(Sound.ENTITY_VILLAGER_NO);
failSalvage();
BlacksmithPlugin.getInstance().callEvent(new ScrapperSalvageFailEvent(this.npc, this.player));
} else {
playSound(Sound.BLOCK_ANVIL_USE);
giveSalvage();
BlacksmithPlugin.getInstance().callEvent(new ScrapperSalvageSucceedEvent(this.npc, this.player));
sendNPCMessage(this.npc, this.player, this.config.getSuccessMessage());
}
}
/**
* The method to run when a crapper fails salvaging an item
*/
private void failSalvage() {
// Make sure the given amount is the same amount taken for salvage to avoid duplication
this.itemToSalvage.setAmount(itemsConsumed);
if (ItemHelper.getMaxDurability(this.itemToSalvage) > 0) {
//Damage the item if possible
damageItemRandomly(this.itemToSalvage);
giveResultingItem(this.config.getMaxSalvageDelay() > 0, this.config.getDropItem(), this.npc,
this.itemToSalvage);
sendNPCMessage(this.npc, this.player, this.config.getFailSalvageMessage());
} else {
// Give half the salvage
List<ItemStack> halfSalvage = SalvageHelper.pickRandomSalvage(this.salvage, new ArrayList<>(), 0.5);
for (ItemStack item : halfSalvage) {
giveResultingItem(this.config.getMaxSalvageDelay() > 0, this.config.getDropItem(), this.npc, item);
}
sendNPCMessage(this.npc, this.player, this.config.getFailExtendedSalvageMessage());
}
}
/**
* Gives the player the calculated salvage
*/
private void giveSalvage() {
// TODO: Find a better calculation than 1 enchantment level = 1 exp level
// Gives the player back some of the EXP used on an item
this.player.giveExpLevels(this.enchantmentLevels);
BlacksmithPlugin.debug("Giving salvage " + this.salvage);
for (ItemStack item : this.salvage) {
giveResultingItem(this.config.getMaxSalvageDelay() > 0, this.config.getDropItem(), this.npc, item);
}
}
}

View File

@ -0,0 +1,313 @@
package net.knarcraft.blacksmith.trait;
import net.citizensnpcs.api.util.DataKey;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.SmithPreset;
import net.knarcraft.blacksmith.config.SmithPresetFilter;
import net.knarcraft.blacksmith.config.scrapper.ScrapperNPCSettings;
import net.knarcraft.blacksmith.config.scrapper.ScrapperSetting;
import net.knarcraft.blacksmith.container.RecipeResult;
import net.knarcraft.blacksmith.container.SalvageResult;
import net.knarcraft.blacksmith.manager.EconomyManager;
import net.knarcraft.blacksmith.property.SalvageMethod;
import net.knarcraft.blacksmith.property.SalvageState;
import net.knarcraft.blacksmith.util.ItemHelper;
import net.knarcraft.blacksmith.util.SalvageHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.formatting.StringReplacer;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ArmorMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static net.knarcraft.blacksmith.formatting.BlacksmithStringFormatter.sendNPCMessage;
/**
* The class representing a scrapper NPC trait
*/
public class ScrapperTrait extends CustomTrait<ScrapperSetting> {
private final ScrapperNPCSettings config;
/**
* Instantiates a new blacksmith trait
*/
public ScrapperTrait() {
super("scrapper");
//This should crash if the blacksmith plugin hasn't been properly registered
Bukkit.getServer().getPluginManager().getPlugin("Blacksmith");
this.config = new ScrapperNPCSettings(BlacksmithPlugin.getInstance().getGlobalScrapperSettings());
super.setTraitSettings(this.config);
}
/**
* Gets the current settings for this NPC
*
* @return <p>The current settings for this NPC</p>
*/
@NotNull
public ScrapperNPCSettings getSettings() {
return config;
}
/**
* Loads all config values stored in citizens' config file for this NPC
*
* @param key <p>The data key used for the config root</p>
*/
@Override
public void load(@NotNull DataKey key) {
getSettings().loadVariables(key);
}
/**
* Saves all config values for this NPC
*
* @param key <p>The data key used for the config root</p>
*/
@Override
public void save(@NotNull DataKey key) {
getSettings().saveVariables(key);
}
/**
* Starts a new session, and prepares to repair the player's item
*
* @param player <p>The player to start the session for</p>
*/
public void startSession(@NotNull Player player) {
ItemStack itemInHand = player.getInventory().getItemInMainHand().clone();
if (itemInHand.getType().isAir()) {
sendNPCMessage(this.npc, player, config.getNoItemMessage());
return;
}
List<Material> salvageAbleItems = getSettings().getSalvageAbleItems();
boolean extended = getSettings().extendedSalvageEnabled();
// Check if the item can be salvaged
if (!canBeSalvaged(itemInHand, salvageAbleItems, extended)) {
sendNPCMessage(this.npc, player, StringFormatter.replacePlaceholder(getSettings().getInvalidItemMessage(),
"{title}", getSettings().getScrapperTitle()));
BlacksmithPlugin.debug("Cannot salvage provided item: " + itemInHand);
return;
}
SalvageResult result = getBestResult(player, itemInHand, extended);
if (result == null || result.salvage().isEmpty()) {
return;
}
//Start a new scrapper session for the player
currentSessionStartTime = System.currentTimeMillis();
session = new SalvageSession(this, player, npc, getSettings(), result.salvage(),
result.salvageMethod(), result.requiredAmount());
// Print the cost to the player
printCostMessage(player, itemInHand, EconomyManager.formatSalvageCost(result.salvageMethod()),
result.salvageMethod());
}
/**
* Gets the best available salvage result
*
* @param player <p>The player attempting to salvage an item</p>
* @param itemInHand <p>The item the player is attempting to salvage</p>
* @param extended <p>Whether extended salvage is enabled</p>
* @return <p>The best result, or null if no valid result exists</p>
*/
@Nullable
private SalvageResult getBestResult(@NotNull Player player, @NotNull ItemStack itemInHand, boolean extended) {
SalvageResult result = isArmorTrimSalvage(player, itemInHand);
if (result.salvageState() == SalvageState.NO_SALVAGE) {
return null;
} else if (result.salvageState() == SalvageState.FOUND_SALVAGE) {
return result;
}
result = isNetheriteSalvage(player, itemInHand);
if (result.salvageState() == SalvageState.NO_SALVAGE) {
return null;
} else if (result.salvageState() == SalvageState.FOUND_SALVAGE) {
return result;
}
result = isNormalSalvage(player, itemInHand, extended);
if (result.salvageState() == SalvageState.NO_SALVAGE) {
return null;
} else if (result.salvageState() == SalvageState.FOUND_SALVAGE) {
return result;
}
return null;
}
/**
* Gets the result of trying to salvage the item normally
*
* @param player <p>The player trying to salvage the item</p>
* @param itemInHand <p>The item to be salvaged</p>
* @param extended <p>Whether extended salvage is enabled</p>
* @return <p>The result of attempting the salvage</p>
*/
@NotNull
private SalvageResult isNormalSalvage(@NotNull Player player, @NotNull ItemStack itemInHand, boolean extended) {
// As there is no recipe for netherite items, the check needs to be after the netherite salvage check
if (!SalvageHelper.isSalvageable(player.getServer(), itemInHand)) {
sendNPCMessage(this.npc, player, StringFormatter.replacePlaceholder(getSettings().getInvalidItemMessage(),
"{title}", getSettings().getScrapperTitle()));
BlacksmithPlugin.debug("Provided with non-salvage-able item " + itemInHand);
return new SalvageResult(SalvageMethod.SALVAGE, new ArrayList<>(), SalvageState.NO_SALVAGE, 0);
}
// Check if the item is enchanted, and whether this blacksmith can salvage it
if (!itemInHand.getEnchantments().isEmpty() && !getSettings().salvageEnchanted()) {
sendNPCMessage(this.npc, player, getSettings().getCannotSalvageEnchantedMessage());
return new SalvageResult(SalvageMethod.SALVAGE, new ArrayList<>(), SalvageState.NO_SALVAGE, 0);
}
// Check if any salvage will be produced
RecipeResult recipeResult = getSalvage(itemInHand, extended);
SalvageMethod method = ItemHelper.isRepairable(itemInHand) ? SalvageMethod.SALVAGE : SalvageMethod.EXTENDED_SALVAGE;
boolean noUsefulSalvage = recipeResult == null || recipeResult.salvage().isEmpty();
if (noUsefulSalvage) {
sendNPCMessage(this.npc, player, getSettings().getTooDamagedMessage());
return new SalvageResult(method, new ArrayList<>(), SalvageState.NO_SALVAGE, 0);
} else {
return new SalvageResult(method, recipeResult.salvage(), SalvageState.FOUND_SALVAGE,
recipeResult.recipe().getResult().getAmount());
}
}
/**
* Gets the result of trying to salvage the item as a netherite applied item
*
* @param player <p>The player trying to salvage the item</p>
* @param itemInHand <p>The item to be salvaged</p>
* @return <p>The result of attempting the salvage</p>
*/
@NotNull
private SalvageResult isNetheriteSalvage(@NotNull Player player, @NotNull ItemStack itemInHand) {
if (!SmithPreset.BLACKSMITH.getFilteredMaterials(SmithPresetFilter.NETHERITE).contains(itemInHand.getType())) {
return new SalvageResult(SalvageMethod.NETHERITE, new ArrayList<>(), SalvageState.INCORRECT_METHOD, 0);
}
if (!getSettings().salvageNetherite()) {
sendNPCMessage(this.npc, player, getSettings().getCannotSalvageNetheriteMessage());
return new SalvageResult(SalvageMethod.NETHERITE, new ArrayList<>(), SalvageState.NO_SALVAGE, 0);
}
List<ItemStack> salvage = SalvageHelper.getNetheriteSalvage(itemInHand);
if (salvage == null || salvage.isEmpty()) {
return new SalvageResult(SalvageMethod.NETHERITE, new ArrayList<>(), SalvageState.NO_SALVAGE, 0);
}
return new SalvageResult(SalvageMethod.NETHERITE, salvage, SalvageState.FOUND_SALVAGE, 1);
}
/**
* Gets the result of trying to salvage the item as an armor trim
*
* @param player <p>The player trying to salvage the item</p>
* @param itemInHand <p>The item to be salvaged</p>
* @return <p>The result of attempting the salvage</p>
*/
@NotNull
private SalvageResult isArmorTrimSalvage(@NotNull Player player, @NotNull ItemStack itemInHand) {
if (!(itemInHand.getItemMeta() instanceof ArmorMeta armorMeta) || !armorMeta.hasTrim()) {
return new SalvageResult(SalvageMethod.ARMOR_TRIM, new ArrayList<>(), SalvageState.INCORRECT_METHOD, 0);
}
if (!getSettings().salvageArmorTrims()) {
sendNPCMessage(this.npc, player, getSettings().getCannotSalvageArmorTrimMessage());
return new SalvageResult(SalvageMethod.ARMOR_TRIM, new ArrayList<>(), SalvageState.NO_SALVAGE, 0);
}
List<ItemStack> salvage = SalvageHelper.getArmorTrimSalvage(itemInHand, armorMeta);
if (salvage == null || salvage.isEmpty()) {
sendNPCMessage(this.npc, player, getSettings().getArmorTrimSalvageNotFoundMessage());
return new SalvageResult(SalvageMethod.ARMOR_TRIM, new ArrayList<>(), SalvageState.NO_SALVAGE, 0);
}
return new SalvageResult(SalvageMethod.ARMOR_TRIM, salvage, SalvageState.FOUND_SALVAGE, 1);
}
/**
* Prints a message to the given player, explaining the cost of salvaging the held item
*
* @param player <p>The player that interacted with the scrapper</p>
* @param itemInHand <p>The item the player wants to salvage</p>
* @param cost <p>The cost of salvaging the item</p>
* @param salvageMethod <p>The type of salvage performed</p>
*/
private void printCostMessage(@NotNull Player player, @NotNull ItemStack itemInHand, @NotNull String cost,
@NotNull SalvageMethod salvageMethod) {
StringReplacer replacer = new StringReplacer();
replacer.add("{cost}", cost);
replacer.add("{item}", itemInHand.getType().name().toLowerCase().replace('_', ' '));
if (salvageMethod == SalvageMethod.ARMOR_TRIM) {
sendNPCMessage(this.npc, player, replacer.replace(getSettings().getArmorTrimCostMessage()));
} else if (salvageMethod == SalvageMethod.SALVAGE || salvageMethod == SalvageMethod.EXTENDED_SALVAGE) {
String expectedYield;
if (ItemHelper.getDamage(itemInHand) <= 0) {
expectedYield = getSettings().getFullSalvageMessage();
} else {
expectedYield = getSettings().getPartialSalvageMessage();
}
replacer.add("{yield}", expectedYield);
sendNPCMessage(this.npc, player, replacer.replace(getSettings().getCostMessage()));
} else if (salvageMethod == SalvageMethod.NETHERITE) {
sendNPCMessage(this.npc, player, replacer.replace(getSettings().getNetheriteCostMessage()));
} else {
BlacksmithPlugin.error("Unrecognized salvage method " + salvageMethod);
}
}
@Override
protected boolean showExactTime() {
return BlacksmithPlugin.getInstance().getGlobalScrapperSettings().showExactTime();
}
/**
* Gets whether this scrapper can salvage the given item
*
* @param item <p>The item to check</p>
* @param salvageAbleItems <p>The items this scrapper can salvage</p>
* @param extended <p>Whether extended salvage is enabled</p>
* @return <p>True if the item can be theoretically salvaged</p>
*/
private boolean canBeSalvaged(@NotNull ItemStack item, @NotNull List<Material> salvageAbleItems, boolean extended) {
return (extended || ItemHelper.isRepairable(item)) &&
(salvageAbleItems.isEmpty() || salvageAbleItems.contains(item.getType()));
}
/**
* Gets salvage for an item, if it's salvage-able
*
* @param item <p>The item to calculate salvage for</p>
* @param extended <p>Whether extended salvage is enabled</p>
* @return <p>The possible salvage, or null if not salvage-able</p>
*/
@Nullable
private RecipeResult getSalvage(@NotNull ItemStack item, boolean extended) {
// Get the salvage, for the item, but ignore some materials if set, and the item isn't at full durability
Set<Material> trashSalvage = BlacksmithPlugin.getInstance().getGlobalScrapperSettings().getTrashSalvage(
item.getType());
// Don't ignore salvage for fully repaired items
if (trashSalvage == null || ItemHelper.getDamage(item) == 0) {
trashSalvage = new HashSet<>();
}
return SalvageHelper.getSalvage(BlacksmithPlugin.getInstance().getServer(), item, trashSalvage, extended);
}
}

View File

@ -0,0 +1,200 @@
package net.knarcraft.blacksmith.trait;
import net.citizensnpcs.api.npc.NPC;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.event.BlacksmithReforgeStartEvent;
import net.knarcraft.blacksmith.event.NPCSoundEvent;
import net.knarcraft.blacksmith.event.ScrapperSalvageStartEvent;
import net.knarcraft.blacksmith.util.ItemHelper;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitScheduler;
import org.jetbrains.annotations.NotNull;
import java.util.Random;
/**
* A runnable session for performing a reforging/salvage task
*/
public abstract class Session implements Runnable {
protected static final Random random = new Random();
protected final Player player;
protected final NPC npc;
protected long finishTime;
protected int taskId;
/**
* Instantiates a new session
*
* @param player <p>The player the session belongs to</p>
* @param npc <p>The NPC involved in the session</p>
*/
public Session(@NotNull Player player, @NotNull NPC npc) {
this.player = player;
this.npc = npc;
}
/**
* Gets whether the given player is currently in a reforging session
*
* @param other <p>The player to check if is in session</p>
* @return <p>False if the given player is in a reforge session</p>
*/
public boolean isNotInSession(@NotNull Player other) {
return !player.getUniqueId().equals(other.getUniqueId());
}
/**
* Gets the player currently in this reforge session
*
* @return <p>The player currently in this reforge session</p>
*/
public @NotNull Player getPlayer() {
return player;
}
/**
* Gets whether the current session is invalid, and should be ended
*
* <p>If the player has switched their item, the player cannot pay, or some other condition fails,
* this returns true.</p>
*
* @return <p>True if the current session should end</p>
*/
public abstract boolean isSessionInvalid();
/**
* Gets whether the current session is still running
*
* @return <p>True if the current session is still running</p>
*/
public boolean isRunning() {
return BlacksmithPlugin.getInstance().getServer().getScheduler().isQueued(taskId);
}
/**
* Gets the time in milliseconds when this session's action will finish
*
* <p>This returns 0 if the action hasn't been run yet.</p>
*
* @return <p>The timestamp for when the action will finish</p>
*/
public long getFinishTime() {
return this.finishTime;
}
/**
* Schedules this session's main task for after the action delay has passed
*/
public void scheduleAction() {
if (isRunning()) {
throw new IllegalStateException("Session action tried to run twice!");
}
BukkitScheduler scheduler = BlacksmithPlugin.getInstance().getServer().getScheduler();
int actionDelay = getActionDelay();
this.finishTime = System.currentTimeMillis() + (actionDelay * 1000L);
long actionDelayTicks = actionDelay * 20L;
BlacksmithPlugin instance = BlacksmithPlugin.getInstance();
if (this instanceof ReforgeSession) {
instance.callEvent(new BlacksmithReforgeStartEvent(npc, player, actionDelayTicks, getCraftingStation()));
} else if (this instanceof SalvageSession) {
instance.callEvent(new ScrapperSalvageStartEvent(npc, player, actionDelayTicks, getCraftingStation()));
}
taskId = scheduler.scheduleSyncDelayedTask(BlacksmithPlugin.getInstance(), this, actionDelayTicks);
}
/**
* Gives the resulting item to the player
*
* @param hasDelay <p>Whether the session was delayed, or if the item was processed instantly</p>
* @param dropItem <p>Whether the item should be dropped on the ground</p>
* @param npc <p>The NPC holding the item</p>
* @param itemToReturn <p>The item to return to the player</p>
*/
protected void giveResultingItem(boolean hasDelay, boolean dropItem, @NotNull NPC npc, @NotNull ItemStack itemToReturn) {
if (hasDelay) {
//If the player isn't online, or the player cannot fit the item, drop the item to prevent it from disappearing
if (dropItem || !this.player.isOnline() || !ItemHelper.canFitItem(this.player.getInventory(), itemToReturn)) {
this.player.getWorld().dropItemNaturally(npc.getEntity().getLocation(), itemToReturn);
} else {
this.player.getInventory().addItem(itemToReturn);
}
} else {
//It can be assumed as this happens instantly, that the player still has the item's previous slot selected
this.player.getInventory().setItemInMainHand(itemToReturn);
}
}
/**
* Randomly damages the given item
*
* @param item <p>The item to damage</p>
*/
protected void damageItemRandomly(@NotNull ItemStack item) {
short currentItemDurability = ItemHelper.getDurability(item);
short newDurability = (short) (currentItemDurability + (currentItemDurability * random.nextInt(8)));
short maxDurability = item.getType().getMaxDurability();
if (newDurability <= 0) {
newDurability = (short) (maxDurability / 3);
} else if (currentItemDurability + newDurability > maxDurability) {
newDurability = (short) (maxDurability - random.nextInt(maxDurability - 25));
}
if (ItemHelper.updateDamage(item, maxDurability - newDurability)) {
BlacksmithPlugin.warn("Unable to update damage for " + item);
}
}
/**
* Gets the delay for this session's action to finish
*
* @return <p>The delay to wait for this session's action to finish</p>
*/
protected abstract int getActionDelay();
/**
* Gets the appropriate crafting station for this session
*
* @return <p>The appropriate crafting station</p>
*/
protected abstract @NotNull Material getCraftingStation();
/**
* Plays an NPC sound
*
* @param sound <p>The sound to play</p>
*/
protected void playSound(Sound sound) {
playSound(this.npc.getEntity(), sound);
}
/**
* Plays a npc sound using a cancellable event
*
* @param entity <p>The entity that should play the sound</p>
* @param sound <p>The sound to play</p>
*/
private void playSound(@NotNull Entity entity, @NotNull Sound sound) {
World world = entity.getLocation().getWorld();
if (world == null) {
return;
}
NPCSoundEvent event = new NPCSoundEvent(this.npc, this.player, SoundCategory.AMBIENT, sound, 0.5f, 1.0f);
BlacksmithPlugin.getInstance().callEvent(event);
if (event.isCancelled()) {
return;
}
world.playSound(event.getNpc().getEntity(), event.getSound(), event.getSoundCategory(), event.getVolume(),
event.getPitch());
}
}

View File

@ -0,0 +1,99 @@
package net.knarcraft.blacksmith.util;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.Setting;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.Settings;
import net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import static net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage.getCurrentValueMessage;
import static net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage.getDefaultValueMessage;
import static net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage.getValueChangedMessage;
/**
* A helper class for the configuration command
*/
public final class ConfigCommandHelper {
private ConfigCommandHelper() {
}
/**
* Changes the value of the setting defined in the user's input
*
* @param args <p>The arguments given by the user</p>
* @param detectedSetting <p>The setting recognized from the input, if any</p>
* @param settings <p>The global settings object to get settings from</p>
* @param sender <p>The command sender to display any output to</p>
*/
public static <K extends Setting> void changeValue(@NotNull String[] args,
@NotNull K detectedSetting,
@NotNull Settings<K> settings,
@NotNull CommandSender sender) {
String newValue = args[1];
//This makes sure all arguments are treated as a sentence
if (detectedSetting.getValueType() == SettingValueType.STRING) {
newValue = String.join(" ", Arrays.asList(args).subList(1, args.length));
}
settings.changeValue(detectedSetting, newValue);
BlacksmithPlugin.getStringFormatter().displaySuccessMessage(sender,
getValueChangedMessage(detectedSetting.getCommandName(), newValue));
}
/**
* Displays the current value of the selected setting
*
* @param setting <p>The global setting recognized from the input</p>
* @param settings <p>The global settings object to get settings from</p>
* @param sender <p>The command sender to display any output to</p>
*/
public static <K extends Setting> void displayCurrentValue(@NotNull K setting,
@NotNull Settings<K> settings,
@NotNull CommandSender sender) {
StringFormatter formatter = BlacksmithPlugin.getStringFormatter();
boolean printRawValue = false;
//Find the value of the specified setting
String currentValue = String.valueOf(settings.getRawValue(setting));
String defaultValue = String.valueOf(setting.getDefaultValue());
//For messages, print an additional raw value showing which color codes are used
if (setting.isPerNPC() && setting.isMessage()) {
printRawValue = true;
}
// Display the description of the setting
formatter.displaySuccessMessage(sender, setting.getDescription());
// Display the setting's default value
formatter.displayNeutralMessage(sender, getDefaultValueMessage(setting.getCommandName(), defaultValue));
if (printRawValue) {
displayRaw(sender, defaultValue);
}
// Display the current value of the setting
formatter.displayNeutralMessage(sender, getCurrentValueMessage(setting.getCommandName(), currentValue));
if (printRawValue) {
displayRaw(sender, currentValue);
}
}
/**
* Displays a message with formatting codes shown
*
* @param sender <p>The sender to display the raw value to</p>
* @param value <p>The value to display raw</p>
*/
public static void displayRaw(@NotNull CommandSender sender, @NotNull String value) {
sender.sendMessage(BlacksmithTranslatableMessage.getRawValueMessage(
value.replace(ChatColor.COLOR_CHAR, '&')));
}
}

View File

@ -1,7 +1,22 @@
package net.knarcraft.blacksmith.util;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.StargateYamlConfiguration;
import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.knarlib.util.FileHelper;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.MemorySection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* A helper class for getting an object value as the correct type
@ -16,18 +31,22 @@ public final class ConfigHelper {
* Gets the given value as a string list
*
* @param value <p>The raw string list value</p>
* @return <p>The value as a string list</p>
* @return <p>The value as a string list, or null if not compatible</p>
*/
public static List<String> asStringList(Object value) {
if (value instanceof String) {
return List.of(((String) value).split(","));
} else {
public static @Nullable List<String> asStringList(@Nullable Object value) {
if (value == null) {
return new ArrayList<>();
}
if (value instanceof String string) {
return List.of((string).split(","));
} else if (value instanceof List<?> list) {
List<String> strings = new ArrayList<>();
List<?> list = (List<?>) value;
for (Object object : list) {
strings.add(String.valueOf(object));
}
return strings;
} else {
return null;
}
}
@ -38,8 +57,10 @@ public final class ConfigHelper {
*
* @param value <p>The object value to get</p>
* @return <p>The value of the given object as a double</p>
* @throws ClassCastException <p>If a non-double is given as input</p>
* @throws NumberFormatException <p>If a non-double is given as input</p>
*/
public static double asDouble(Object value) {
public static double asDouble(@NotNull Object value) throws ClassCastException, NumberFormatException {
if (value instanceof String) {
return Double.parseDouble((String) value);
} else if (value instanceof Integer) {
@ -56,12 +77,15 @@ public final class ConfigHelper {
*
* @param value <p>The object value to get</p>
* @return <p>The value of the given object as a boolean</p>
* @throws ClassCastException <p>If the given value is not a boolean</p>
*/
public static boolean asBoolean(Object value) {
if (value instanceof String) {
public static boolean asBoolean(@NotNull Object value) throws ClassCastException {
if (value instanceof Boolean booleanValue) {
return booleanValue;
} else if (value instanceof String) {
return Boolean.parseBoolean((String) value);
} else {
return (Boolean) value;
throw new ClassCastException();
}
}
@ -72,8 +96,9 @@ public final class ConfigHelper {
*
* @param value <p>The object value to get</p>
* @return <p>The value of the given object as an integer</p>
* @throws ClassCastException <p>If the given value is not an integer</p>
*/
public static int asInt(Object value) {
public static int asInt(@NotNull Object value) throws ClassCastException {
if (value instanceof String) {
return Integer.parseInt((String) value);
} else {
@ -81,4 +106,103 @@ public final class ConfigHelper {
}
}
/**
* Changes all configuration values from the old name to the new name
*
* @param dataFolderPath <p>The path to this plugin's data</p>
* @param currentConfiguration <p>The current config to back up</p>
*/
public static void migrateConfig(@NotNull String dataFolderPath, @NotNull FileConfiguration currentConfiguration) {
BlacksmithPlugin instance = BlacksmithPlugin.getInstance();
//Save the old config just in case something goes wrong
try {
currentConfiguration.save(new File(dataFolderPath, "config.yml.old"));
} catch (IOException exception) {
BlacksmithPlugin.warn("Unable to save old backup and do migration");
return;
}
//Load old and new configuration
instance.reloadConfig();
FileConfiguration oldConfiguration = instance.getConfig();
InputStream configStream = FileHelper.getInputStreamForInternalFile("/config.yml");
if (configStream == null) {
BlacksmithPlugin.error("Could not migrate the configuration, as the internal configuration could not be read!");
return;
}
YamlConfiguration newConfiguration = StargateYamlConfiguration.loadConfiguration(
FileHelper.getBufferedReaderFromInputStream(configStream));
//Read all available config migrations
Map<String, String> migrationFields;
try {
InputStream migrationStream = FileHelper.getInputStreamForInternalFile("/config-migrations.txt");
if (migrationStream == null) {
BlacksmithPlugin.error("Could not migrate the configuration, as the internal migration paths could not be read!");
return;
}
migrationFields = FileHelper.readKeyValuePairs(FileHelper.getBufferedReaderFromInputStream(migrationStream),
"=", ColorConversion.NORMAL);
} catch (IOException exception) {
BlacksmithPlugin.warn("Unable to load config migration file");
return;
}
//Replace old config names with the new ones
for (String key : migrationFields.keySet()) {
if (oldConfiguration.contains(key)) {
migrateProperty(migrationFields, key, oldConfiguration);
}
}
// Copy all keys to the new config
for (String key : StargateYamlConfiguration.getKeysWithoutComments(oldConfiguration, true)) {
if (oldConfiguration.get(key) instanceof MemorySection) {
continue;
}
newConfiguration.set(key, oldConfiguration.get(key));
}
try {
newConfiguration.save(new File(dataFolderPath, "config.yml"));
} catch (IOException exception) {
BlacksmithPlugin.warn("Unable to save migrated config");
}
instance.reloadConfig();
}
/**
* Migrates one configuration property
*
* @param migrationFields <p>The configuration fields to be migrated</p>
* @param key <p>The key/path of the property to migrate</p>
* @param oldConfiguration <p>The original pre-migration configuration</p>
*/
private static void migrateProperty(@NotNull Map<String, String> migrationFields, @NotNull String key,
@NotNull FileConfiguration oldConfiguration) {
String newPath = migrationFields.get(key);
Object oldValue = oldConfiguration.get(key);
if (!newPath.trim().isEmpty()) {
if (oldConfiguration.isConfigurationSection(key)) {
// Copy each value of a configuration section
ConfigurationSection sourceSection = oldConfiguration.getConfigurationSection(key);
ConfigurationSection destinationSection = oldConfiguration.createSection(newPath);
if (sourceSection == null) {
return;
}
for (String path : StargateYamlConfiguration.getKeysWithoutComments(sourceSection, true)) {
destinationSection.set(path, sourceSection.get(path));
}
} else {
// Copy the value to the new path
oldConfiguration.set(newPath, oldValue);
}
}
// Remove the old path's value
oldConfiguration.set(key, null);
}
}

View File

@ -1,32 +0,0 @@
package net.knarcraft.blacksmith.util;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* A helper class for dealing with files
*/
public final class FileHelper {
private FileHelper() {
}
/**
* Gets a buffered reader for
*
* @return <p>A buffered read for reading the file</p>
* @throws FileNotFoundException <p>If unable to get an input stream for the given file</p>
*/
public static BufferedReader getBufferedReaderForInternalFile(String file) throws FileNotFoundException {
InputStream inputStream = FileHelper.class.getResourceAsStream(file);
if (inputStream == null) {
throw new FileNotFoundException("Unable to read the given file");
}
return new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
}
}

View File

@ -1,8 +1,12 @@
package net.knarcraft.blacksmith.util;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.enchantments.Enchantment;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A helper class for parsing input into proper object types
@ -19,9 +23,9 @@ public final class InputParsingHelper {
* @param input <p>The input to check</p>
* @return <p>True if the value is empty</p>
*/
public static boolean isEmpty(String input) {
public static boolean isEmpty(@Nullable String input) {
return input == null || input.equalsIgnoreCase("null") || input.equals("\"\"") ||
input.trim().isEmpty() || input.equals("-1");
input.isBlank() || input.equals("-1");
}
/**
@ -30,7 +34,7 @@ public final class InputParsingHelper {
* @param input <p>The string to match to a material</p>
* @return <p>The material matching the string, or null if not found</p>
*/
public static Material matchMaterial(String input) {
public static @Nullable Material matchMaterial(@NotNull String input) {
return Material.matchMaterial(input.replace("-", "_"));
}
@ -40,8 +44,28 @@ public final class InputParsingHelper {
* @param input <p>The string to match to an enchantment</p>
* @return <p>The enchantment matching the string, or null if not found</p>
*/
public static Enchantment matchEnchantment(String input) {
return Enchantment.getByKey(NamespacedKey.minecraft(input.replace("-", "_")));
public static @Nullable Enchantment matchEnchantment(@NotNull String input) {
try {
Registry<Enchantment> enchantments = Bukkit.getRegistry(Enchantment.class);
if (enchantments == null) {
throw new RuntimeException("Unable to get enchantment registry");
}
return enchantments.get(NamespacedKey.minecraft(
input.replace("-", "_").replace(" ", "_").toLowerCase()));
} catch (IllegalArgumentException exception) {
//Invalid characters, such as : will normally throw an illegal argument exception
return null;
}
}
/**
* Converts a material name like "*helmet" into a regular expression ".*HELMET"
*
* @param input <p>The input to RegExIfy</p>
* @return <p>The converted input</p>
*/
public static @NotNull String regExIfy(@NotNull String input) {
return input.replace("*", ".*").toUpperCase().replace("-", "_");
}
}

View File

@ -1,27 +1,65 @@
package net.knarcraft.blacksmith.util;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.SmithPreset;
import org.bukkit.Material;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A helper class for getting information about items
*/
public final class ItemHelper {
private ItemHelper() {
}
/**
* Gets whether the given item is repairable
*
* @param item <p>The item to check</p>
* @return <p>True if the item is repairable</p>
*/
public static boolean isRepairable(@NotNull ItemStack item) {
return item.getItemMeta() instanceof Damageable && getMaxDurability(item) > 0;
}
/**
* Gets the max durability of an item
*
* @param itemStack <p>The item to get the durability of</p>
* @return <p>The max durability of the item</p>
*/
public static short getMaxDurability(@NotNull ItemStack itemStack) {
if (itemStack.getItemMeta() instanceof Damageable) {
return itemStack.getType().getMaxDurability();
} else {
return 0;
}
}
/**
* Gets the current durability of the given item
*
* @param itemStack <p>The item to get the durability of</p>
* @return <p>The durability of the item</p>
*/
public static short getDurability(ItemStack itemStack) {
Damageable damageable = (Damageable) itemStack.getItemMeta();
int maxDurability = itemStack.getType().getMaxDurability();
if (damageable != null) {
public static short getDurability(@NotNull ItemStack itemStack) {
if (itemStack.getItemMeta() instanceof Damageable damageable) {
int maxDurability = getMaxDurability(itemStack);
return (short) (maxDurability - damageable.getDamage());
} else {
return (short) maxDurability;
return 0;
}
}
@ -31,13 +69,195 @@ public final class ItemHelper {
* @param itemStack <p>The damage done to the item</p>
* @return <p>The damage done to the item</p>
*/
public static short getDamage(ItemStack itemStack) {
Damageable damageable = (Damageable) itemStack.getItemMeta();
if (damageable != null) {
public static short getDamage(@NotNull ItemStack itemStack) {
if (itemStack.getItemMeta() instanceof Damageable damageable) {
return (short) damageable.getDamage();
} else {
return 0;
}
}
/**
* Updates the damage done to an item
*
* @param item <p>The item to update damage for</p>
* @param newDamage <p>The new damage done</p>
* @return <p>False if the damage was updated. False if not damageable.</p>
*/
public static boolean updateDamage(@NotNull ItemStack item, int newDamage) {
ItemMeta meta = item.getItemMeta();
if (!(meta instanceof Damageable damageable)) {
return true;
}
damageable.setDamage(newDamage);
item.setItemMeta(meta);
return false;
}
/**
* Gets a complete list of all reforge-able materials
*
* <p>Note: As this loops through all materials, the result should be cached</p>
*
* @return <p>A complete list of reforge-able materials</p>
*/
public static @NotNull Set<Material> getAllReforgeAbleMaterials() {
Set<Material> reforgeAbleMaterials = new HashSet<>();
for (Material material : Material.values()) {
if (!material.isItem()) {
continue;
}
ItemStack item = new ItemStack(material);
if (isRepairable(item)) {
reforgeAbleMaterials.add(material);
}
}
return reforgeAbleMaterials;
}
/**
* Checks whether the given material is an anvil
*
* @param material <p>The material to check</p>
* @param requireDamaged <p>Whether only a damaged anvil should count</p>
* @return <p>True if the given material is an anvil</p>
*/
public static boolean isAnvil(@NotNull Material material, boolean requireDamaged) {
boolean isDamagedAnvil = material == Material.CHIPPED_ANVIL || material == Material.DAMAGED_ANVIL;
boolean isAnvil = isDamagedAnvil || material == Material.ANVIL;
return (requireDamaged && isDamagedAnvil) || (!requireDamaged && isAnvil);
}
/**
* Checks whether the given inventory is able to fit the given item
*
* @param inventory <p>The inventory to check</p>
* @param item <p>The item to check</p>
* @return <p>True if the inventory can fit the item</p>
*/
public static boolean canFitItem(@NotNull Inventory inventory, @NotNull ItemStack item) {
// If a slot is available, there is no problem
if (inventory.firstEmpty() != -1) {
return true;
}
// If the inventory doesn't contain the correct type of item, stacking is impossible
if (!inventory.contains(item.getType())) {
return false;
}
// Check if the item stack can fit in the inventory if stacked with existing items
int availableSlots = 0;
for (ItemStack itemStack : inventory.getStorageContents()) {
ItemMeta itemMeta = itemStack.getItemMeta();
ItemMeta targetMeta = item.getItemMeta();
// Skip items of a different type, or with metadata that would prevent stacking
if (itemStack.getType() != item.getType() ||
(itemMeta != null && targetMeta != null && !itemMeta.equals(targetMeta))) {
continue;
}
availableSlots += itemStack.getMaxStackSize() - itemStack.getAmount();
if (availableSlots < item.getAmount()) {
return true;
}
}
return false;
}
/**
* Gets all materials matching the given material wildcard
*
* @param materialName <p>The material name or material wildcard to match</p>
* @param extended <p>Whether to use an extended match, allowing any material</p>
* @return <p>The matched material(s)</p>
*/
public static @NotNull Set<Material> getWildcardMatch(@NotNull String materialName, boolean extended) {
String search = InputParsingHelper.regExIfy(materialName);
Set<Material> materials = new HashSet<>();
Set<Material> all;
if (extended) {
all = Set.of(Material.values());
} else {
all = ItemHelper.getAllReforgeAbleMaterials();
}
for (Material material : all) {
if (material.name().matches(search)) {
materials.add(material);
}
}
return materials;
}
/**
* Gets a list of the items described in the given item list
*
* @param itemList <p>The list of items defined by the user</p>
* @param requireRepairable <p>Whether a material must be repairable to be valid.</p>
* @return <p>The materials contained in the item list</p>
*/
public static List<Material> getItems(@Nullable List<String> itemList, boolean requireRepairable) {
List<Material> items = new ArrayList<>();
if (itemList == null) {
return null;
}
//Convert any presets with a list of materials
itemList = replacePresets(itemList);
Set<Material> blacklisted = new HashSet<>();
//Parse every material, and add to reforgeAble items
for (String item : itemList) {
//Ignore ",,"
if (InputParsingHelper.isEmpty(item) || item.matches("\\[]")) {
continue;
}
boolean blacklist = false;
if (item.startsWith("-")) {
blacklist = true;
item = item.substring(1);
}
Material material = InputParsingHelper.matchMaterial(item);
if (material != null && (!requireRepairable || ItemHelper.isRepairable(new ItemStack(material, 1)))) {
if (!blacklist) {
items.add(material);
} else {
blacklisted.add(material);
}
} else {
BlacksmithPlugin.warn("Unable to verify " + item + " as a valid repairable item");
}
}
//Remove any blacklisted materials at the end to make sure order of arguments won't matter
items.removeAll(blacklisted);
return items;
}
/**
* Replaces smith presets in the given list of strings
*
* @param stringList <p>The value specified by a user</p>
* @return <p>The value with smith presets replaced</p>
*/
private static @NotNull List<String> replacePresets(@NotNull List<String> stringList) {
List<String> newStrings = new ArrayList<>();
for (String item : stringList) {
if (item == null) {
continue;
}
String replaced = SmithPreset.replacePreset(item);
if (!replaced.equals(item)) {
newStrings.addAll(List.of(replaced.split(",")));
} else {
newStrings.add(item);
}
}
return newStrings;
}
}

View File

@ -0,0 +1,394 @@
package net.knarcraft.blacksmith.util;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.container.RecipeResult;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Recipe;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.inventory.ShapelessRecipe;
import org.bukkit.inventory.meta.ArmorMeta;
import org.bukkit.inventory.meta.trim.ArmorTrim;
import org.bukkit.inventory.meta.trim.TrimMaterial;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* A helper class for deciding the salvage returned if salvaging an item
*/
public final class SalvageHelper {
private static final Random random = new Random();
private static final Map<TrimMaterial, Material> trimMaterialToMaterial;
static {
trimMaterialToMaterial = new HashMap<>();
trimMaterialToMaterial.put(TrimMaterial.AMETHYST, Material.AMETHYST_SHARD);
trimMaterialToMaterial.put(TrimMaterial.COPPER, Material.COPPER_INGOT);
trimMaterialToMaterial.put(TrimMaterial.DIAMOND, Material.DIAMOND);
trimMaterialToMaterial.put(TrimMaterial.EMERALD, Material.EMERALD);
trimMaterialToMaterial.put(TrimMaterial.GOLD, Material.GOLD_INGOT);
trimMaterialToMaterial.put(TrimMaterial.IRON, Material.IRON_INGOT);
trimMaterialToMaterial.put(TrimMaterial.LAPIS, Material.LAPIS_LAZULI);
trimMaterialToMaterial.put(TrimMaterial.NETHERITE, Material.NETHERITE_INGOT);
trimMaterialToMaterial.put(TrimMaterial.QUARTZ, Material.QUARTZ);
trimMaterialToMaterial.put(TrimMaterial.REDSTONE, Material.REDSTONE);
}
/**
* Gets salvage for a given netherite item
*
* @param item <p>The netherite armor/tool to salvage</p>
* @return <p></p>
*/
@Nullable
public static List<ItemStack> getNetheriteSalvage(@NotNull ItemStack item) {
Material newMaterial = Material.matchMaterial(item.getType().name().replace("NETHERITE", "DIAMOND"));
if (newMaterial == null) {
return null;
}
ItemStack clone = item.clone();
clone.setType(newMaterial);
List<ItemStack> salvage = new ArrayList<>();
salvage.add(clone);
salvage.add(new ItemStack(Material.NETHERITE_INGOT, 1));
salvage.add(new ItemStack(Material.NETHERITE_UPGRADE_SMITHING_TEMPLATE, 1));
return salvage;
}
/**
* Gets salvage for the given armor trim
*
* @param item <p>The item to have its armor trim salvaged</p>
* @param armorMeta <p>The armor meta of the item to salvage</p>
* @return <p>The salvage, or null if salvage could not be calculated</p>
*/
@Nullable
public static List<ItemStack> getArmorTrimSalvage(@NotNull ItemStack item, @NotNull ArmorMeta armorMeta) {
ArmorTrim armorTrim = armorMeta.getTrim();
if (armorTrim == null) {
return null;
}
List<ItemStack> result = new ArrayList<>();
Material trimMaterial = trimMaterialToMaterial.get(armorTrim.getMaterial());
if (trimMaterial != null) {
result.add(new ItemStack(trimMaterial, 1));
} else {
return null;
}
Material patternMaterial = Material.matchMaterial(armorTrim.getPattern().getKey().getKey() +
"_ARMOR_TRIM_SMITHING_TEMPLATE");
if (patternMaterial != null) {
result.add(new ItemStack(patternMaterial, 1));
} else {
return null;
}
// Return a clone of the input item, with the armor trim removed
ItemStack strippedItem = item.clone();
armorMeta.setTrim(null);
strippedItem.setItemMeta(armorMeta);
result.add(strippedItem);
return result;
}
/**
* Checks whether the given item can be salvaged, assuming no restrictions apply
*
* @param server <p>The server to get recipes from</p>
* @param item <p>The item to check</p>
* @return <p>True if the item can be salvaged</p>
*/
public static boolean isSalvageable(@NotNull Server server, @NotNull ItemStack item) {
List<Recipe> recipes = server.getRecipesFor(new ItemStack(item.getType(), item.getAmount()));
for (Recipe recipe : recipes) {
// Only crafting recipes are allowed.
if ((recipe instanceof ShapedRecipe || recipe instanceof ShapelessRecipe) &&
item.getAmount() >= recipe.getResult().getAmount()) {
return true;
}
}
return false;
}
/**
* Gets the sum of all enchantment levels for the given item
*
* @param item <p>The item to check</p>
* @return <p>The total amount of enchantment levels on the item</p>
*/
public static int getTotalEnchantmentLevels(@NotNull ItemStack item) {
int expLevels = 0;
for (Enchantment enchantment : item.getEnchantments().keySet()) {
expLevels += item.getEnchantmentLevel(enchantment);
}
return expLevels;
}
/**
* Gets the items to return if salvaging the given item stack
*
* <p>Note: Only items craft-able in a crafting table are salvageable. Netherite gear is therefore not salvageable.</p>
*
* @param server <p>The server to get recipes from</p>
* @param salvagedItem <p>The item stack to salvage</p>
* @param trashSalvage <p>Any material treated as trash salvage</p>
* @param extended <p>Whether to enable extended salvage, ignoring the repairable restriction</p>
* @return <p>The items to return to the user, or null if not salvageable</p>
*/
public static @Nullable RecipeResult getSalvage(@NotNull Server server, @Nullable ItemStack salvagedItem,
@NotNull Collection<Material> trashSalvage, boolean extended) {
if (salvagedItem == null || salvagedItem.getAmount() < 1 ||
(!extended && !ItemHelper.isRepairable(salvagedItem))) {
return null;
}
for (Recipe recipe : server.getRecipesFor(new ItemStack(salvagedItem.getType(), salvagedItem.getAmount()))) {
BlacksmithPlugin.debug("Considering recipe: " + recipe.getResult() + " -> " + getRawRecipeSalvage(recipe));
// Only consider crafting table recipes
if (!(recipe instanceof ShapedRecipe) && !(recipe instanceof ShapelessRecipe)) {
BlacksmithPlugin.debug("Recipe had invalid type");
continue;
}
// Make sure the player has enough items
if (salvagedItem.getAmount() < recipe.getResult().getAmount()) {
BlacksmithPlugin.debug("Too few items for recipe");
continue;
}
// Get actual salvage, as long as any can be produced
List<ItemStack> salvage = getRecipeSalvage(recipe, salvagedItem, trashSalvage);
if (salvage != null && !salvage.isEmpty()) {
BlacksmithPlugin.debug("Valid recipe: " + recipe.getResult() + " -> " + getRawRecipeSalvage(recipe));
BlacksmithPlugin.debug("Actual salvage: " + salvage);
return new RecipeResult(recipe, salvage);
}
}
return null;
}
/**
* Gets the salvage resulting from the given recipe and the given item
*
* @param recipe <p>The recipe to get salvage for</p>
* @param salvagedItem <p>The item to be salvaged</p>
* @param trashSalvage <p>Any material treated as trash salvage</p>
* @return <p>A list of items, or null if not a valid type of recipe</p>
*/
private static @Nullable List<ItemStack> getRecipeSalvage(@NotNull Recipe recipe, @NotNull ItemStack salvagedItem,
@NotNull Collection<Material> trashSalvage) {
List<ItemStack> ingredients = getRawRecipeSalvage(recipe);
if (ingredients == null) {
return null;
}
List<ItemStack> copy = copyItems(ingredients);
BlacksmithPlugin.debug("Copied salvage: " + copy);
List<ItemStack> salvage = getSalvage(copy, salvagedItem, trashSalvage);
BlacksmithPlugin.debug("Combining salvage: " + salvage);
List<ItemStack> combined = combineStacks(salvage);
BlacksmithPlugin.debug("Combined : " + combined);
return combined;
}
/**
* Copies a list of items
*
* <p>Note: This does not copy any metadata. It only copies the item type and the amount.</p>
*
* @param itemsToCopy <p>The items to make a copy of</p>
* @return <p>A copy of the given items</p>
*/
private static @NotNull List<ItemStack> copyItems(@NotNull List<ItemStack> itemsToCopy) {
List<ItemStack> copies = new ArrayList<>(itemsToCopy.size());
for (ItemStack itemStack : itemsToCopy) {
copies.add(new ItemStack(itemStack.getType(), itemStack.getAmount()));
}
return copies;
}
/**
* Gets the salvage resulting from the given item, and the given recipe items
*
* @param recipeItems <p>All items required for crafting the item to salvage</p>
* @param salvagedItem <p>The item to be salvaged</p>
* @param trashSalvage <p>The types of materials considered trash for this recipe</p>
* @return <p>The items to be returned to the user as salvage</p>
*/
@NotNull
private static List<ItemStack> getSalvage(@NotNull List<ItemStack> recipeItems,
@NotNull ItemStack salvagedItem,
@NotNull Collection<Material> trashSalvage) {
int durability = ItemHelper.getDurability(salvagedItem);
int maxDurability = ItemHelper.getMaxDurability(salvagedItem);
// Prevent divide by zero for items that don't have a set max durability
if (maxDurability <= 0) {
maxDurability = 1;
durability = 1;
}
BlacksmithPlugin.debug("Durability: " + durability + "/" + maxDurability);
double percentageRemaining = (double) durability / maxDurability;
BlacksmithPlugin.debug("Remaining: " + percentageRemaining);
return pickRandomSalvage(recipeItems, trashSalvage, percentageRemaining);
}
/**
* Picks random salvage from the given items
*
* @param itemsToChooseFrom <p>The salvage to choose from</p>
* @param trashSalvage <p>The salvage considered trash</p>
* @param percentageRemaining <p>The percentage remaining for the salvaged item (0-1)</p>
* @return <p>Random salvage</p>
*/
@NotNull
public static List<ItemStack> pickRandomSalvage(@NotNull List<ItemStack> itemsToChooseFrom,
@NotNull Collection<Material> trashSalvage,
double percentageRemaining) {
// Avoid an infinite loop
if (percentageRemaining > 1 || percentageRemaining < 0) {
percentageRemaining = 1;
}
// If not damaged, just give everything
if (percentageRemaining == 1) {
BlacksmithPlugin.debug("100% Remaining. Copying " + itemsToChooseFrom);
return copyItems(itemsToChooseFrom);
}
// Split into good items and trash items
List<ItemStack> goodItems = copyItems(itemsToChooseFrom);
goodItems.removeIf((item) -> trashSalvage.contains(item.getType()));
List<ItemStack> trashItems = copyItems(itemsToChooseFrom);
trashItems.removeIf((item) -> !trashSalvage.contains(item.getType()));
int itemsToReturn = (int) Math.floor(totalItemAmount(itemsToChooseFrom) * percentageRemaining);
int goodItemAmount = totalItemAmount(goodItems);
int pickedItems = 0;
List<ItemStack> salvage = new ArrayList<>(itemsToReturn);
while (pickedItems < itemsToReturn && pickedItems < goodItemAmount) {
int index = SalvageHelper.random.nextInt(goodItems.size());
ItemStack itemStack = goodItems.get(index);
if (itemStack.getType().isAir() || itemStack.getAmount() == 0) {
continue;
}
salvage.add(new ItemStack(itemStack.getType(), 1));
itemStack.setAmount(itemStack.getAmount() - 1);
pickedItems++;
}
while (pickedItems < itemsToReturn) {
int index = SalvageHelper.random.nextInt(trashItems.size());
ItemStack itemStack = trashItems.get(index);
if (itemStack.getType().isAir() || itemStack.getAmount() == 0) {
continue;
}
salvage.add(new ItemStack(itemStack.getType(), 1));
itemStack.setAmount(itemStack.getAmount() - 1);
pickedItems++;
}
return salvage;
}
/**
* Gets the total sum of items in the given list of items
*
* @param items <p>The items to get the sum of</p>
* @return <p>The total number of items</p>
*/
public static int totalItemAmount(@NotNull List<ItemStack> items) {
int sum = 0;
for (ItemStack itemStack : items) {
sum += itemStack.getAmount();
}
return sum;
}
/**
* Combines all items of the same type in the given list
*
* <p>Basically, if the input is two item stacks containing one diamond each, the output will be an item stack with
* two diamonds instead.</p>
*
* @param items <p>The items to combine</p>
* @return <p>The given items, but combined</p>
*/
private static @NotNull List<ItemStack> combineStacks(@NotNull List<ItemStack> items) {
Map<Material, Integer> itemAmounts = new HashMap<>();
for (ItemStack item : items) {
Material itemType = item.getType();
itemAmounts.put(itemType, itemAmounts.getOrDefault(itemType, 0) + item.getAmount());
}
List<ItemStack> combined = new ArrayList<>();
for (Material material : itemAmounts.keySet()) {
combined.add(new ItemStack(material, itemAmounts.get(material)));
}
return combined;
}
/**
* Gets the raw salvage of a recipe, assuming full salvage would be possible
*
* @param recipe <p>The recipe to get salvage for</p>
* @return <p>The salvage resulting from the recipe</p>
*/
private static List<ItemStack> getRawRecipeSalvage(@NotNull Recipe recipe) {
List<ItemStack> ingredients;
if (recipe instanceof ShapedRecipe shapedRecipe) {
ingredients = getIngredients(shapedRecipe);
} else if (recipe instanceof ShapelessRecipe shapelessRecipe) {
ingredients = shapelessRecipe.getIngredientList();
} else {
//Recipes other than crafting shouldn't be considered for salvaging
return null;
}
//Make things easier by eliminating identical stacks
return combineStacks(ingredients);
}
/**
* Gets all ingredients contained in the given shaped recipe
*
* @param shapedRecipe <p>The shaped recipe to get ingredients for</p>
* @return <p>The items contained in the recipe</p>
*/
@NotNull
private static List<ItemStack> getIngredients(@NotNull ShapedRecipe shapedRecipe) {
List<ItemStack> ingredients = new ArrayList<>();
Map<Character, ItemStack> ingredientMap = shapedRecipe.getIngredientMap();
//The shape is a list of the three rows' strings. Each string contains 3 characters.
String[] shape = shapedRecipe.getShape();
for (String row : shape) {
for (int column = 0; column < row.length(); column++) {
ItemStack item = ingredientMap.get(row.charAt(column));
if (item != null && item.getType() != Material.AIR && item.getAmount() > 0) {
ingredients.add(item);
}
}
}
return ingredients;
}
}

View File

@ -1,11 +1,13 @@
package net.knarcraft.blacksmith.util;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.SmithPreset;
import net.knarcraft.blacksmith.config.SmithPresetFilter;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Registry;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.enchantments.EnchantmentTarget;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
@ -25,7 +27,7 @@ public final class TabCompleteValuesHelper {
* @param valueType <p>The value type to get possible values for</p>
* @return <p>The values to show the user</p>
*/
public static List<String> getTabCompletions(SettingValueType valueType) {
public static @NotNull List<String> getTabCompletions(@NotNull SettingValueType valueType) {
return switch (valueType) {
case POSITIVE_INTEGER -> getPositiveIntegers();
case BOOLEAN -> getBooleans();
@ -33,39 +35,32 @@ public final class TabCompleteValuesHelper {
case STRING -> getStrings();
case PERCENTAGE -> getPercentages();
case REFORGE_ABLE_ITEMS -> getReforgeAbleMaterials();
case MATERIAL -> getAllReforgeAbleMaterials();
case ENCHANTMENT -> getAllEnchantments();
case STRING_LIST -> getExampleEnchantmentBlockLists();
case MATERIAL -> getAllReforgeAbleMaterialNames();
case ENCHANTMENT, ENCHANTMENT_LIST -> getAllEnchantments();
case STRING_LIST -> List.of("*_SHOVEL,*_PICKAXE,*_AXE,*_HOE,*_SWORD:STICK,SMITHING_TABLE:*_PLANKS");
};
}
/**
* Gets example enchantment block lists
*
* @return <p>Some example enchantment block lists</p>
*/
private static List<String> getExampleEnchantmentBlockLists() {
List<String> exampleBlockLists = new ArrayList<>();
exampleBlockLists.add(Enchantment.VANISHING_CURSE.getKey().getKey() + "," +
Enchantment.BINDING_CURSE.getKey().getKey() + "," + Enchantment.MENDING.getKey().getKey());
exampleBlockLists.add(Enchantment.VANISHING_CURSE.getKey().getKey() + "," +
Enchantment.BINDING_CURSE.getKey().getKey());
return exampleBlockLists;
}
/**
* Gets a complete list of all reforge-able materials
* Gets a complete list of all reforge-able material names
*
* @return <p>A complete list of reforge-able materials</p>
* @return <p>A complete list of reforge-able material names</p>
*/
private static List<String> getAllReforgeAbleMaterials() {
private static @NotNull List<String> getAllReforgeAbleMaterialNames() {
List<String> reforgeAbleMaterials = new ArrayList<>();
for (Material material : Material.values()) {
ItemStack item = new ItemStack(material);
if (item.getItemMeta() instanceof Damageable && EnchantmentTarget.BREAKABLE.includes(item)) {
for (Material material : ItemHelper.getAllReforgeAbleMaterials()) {
reforgeAbleMaterials.add(material.name());
}
}
reforgeAbleMaterials.add("NETHERITE_*");
reforgeAbleMaterials.add("DIAMOND_*");
reforgeAbleMaterials.add("GOLDEN_*");
reforgeAbleMaterials.add("IRON_*");
reforgeAbleMaterials.add("CHAINMAIL_*");
reforgeAbleMaterials.add("STONE_*");
reforgeAbleMaterials.add("*BOW");
reforgeAbleMaterials.add("LEATHER_*");
reforgeAbleMaterials.add("WOODEN_*");
return reforgeAbleMaterials;
}
@ -74,9 +69,14 @@ public final class TabCompleteValuesHelper {
*
* @return <p>A complete list of enchantments</p>
*/
private static List<String> getAllEnchantments() {
private static @NotNull List<String> getAllEnchantments() {
Registry<Enchantment> enchantmentRegistry = Bukkit.getRegistry(Enchantment.class);
if (enchantmentRegistry == null) {
throw new RuntimeException("Unable to get the enchantment registry");
}
List<String> enchantments = new ArrayList<>();
for (Enchantment enchantment : Enchantment.values()) {
for (Enchantment enchantment : enchantmentRegistry) {
enchantments.add(enchantment.getKey().getKey());
}
return enchantments;
@ -87,14 +87,19 @@ public final class TabCompleteValuesHelper {
*
* @return <p>Some example possible values for reforge-able materials</p>
*/
private static List<String> getReforgeAbleMaterials() {
private static @NotNull List<String> getReforgeAbleMaterials() {
List<String> stringLists = new ArrayList<>();
stringLists.add("preset:sword-smith");
stringLists.add("preset:weapon-smith");
stringLists.add("preset:armor-smith");
stringLists.add("preset:tool-smith");
stringLists.add("preset:ranged-smith");
stringLists.add("bow,crossbow,elytra");
for (SmithPreset preset : SmithPreset.values()) {
stringLists.add("preset:" + preset.name());
for (SmithPresetFilter filter : preset.getSupportedFilters()) {
stringLists.add("preset:" + preset.name() + ":" + filter.name());
}
}
stringLists.add("preset:WEAPON_SMITH:RANGED,SHIELD");
stringLists.add("preset:WEAPON_SMITH,preset:ARMOR_SMITH");
stringLists.add("preset:WEAPON_SMITH,preset:TOOL_SMITH");
stringLists.add("preset:ARMOR_SMITH,preset:TOOL_SMITH");
stringLists.add("BOW,CROSSBOW,ELYTRA");
return stringLists;
}
@ -103,7 +108,7 @@ public final class TabCompleteValuesHelper {
*
* @return <p>Some example string values</p>
*/
private static List<String> getStrings() {
private static @NotNull List<String> getStrings() {
List<String> strings = new ArrayList<>(1);
strings.add("&aExample message. Use & for color tags.");
return strings;
@ -114,7 +119,7 @@ public final class TabCompleteValuesHelper {
*
* @return <p>Some example percentage values</p>
*/
private static List<String> getPercentages() {
private static @NotNull List<String> getPercentages() {
List<String> percentages = new ArrayList<>(6);
percentages.add("0");
percentages.add("10");
@ -130,7 +135,7 @@ public final class TabCompleteValuesHelper {
*
* @return <p>Some possible positive doubles</p>
*/
private static List<String> getPositiveDoubles() {
private static @NotNull List<String> getPositiveDoubles() {
List<String> positiveDoubles = new ArrayList<>(4);
positiveDoubles.add("0.0");
positiveDoubles.add("0.0001");
@ -145,7 +150,7 @@ public final class TabCompleteValuesHelper {
*
* @return <p>Some example positive integers</p>
*/
private static List<String> getPositiveIntegers() {
private static @NotNull List<String> getPositiveIntegers() {
List<String> positiveIntegers = new ArrayList<>(6);
positiveIntegers.add("0");
positiveIntegers.add("5");
@ -162,7 +167,7 @@ public final class TabCompleteValuesHelper {
*
* @return <p>The possible boolean values</p>
*/
private static List<String> getBooleans() {
private static @NotNull List<String> getBooleans() {
List<String> booleans = new ArrayList<>(2);
booleans.add("True");
booleans.add("False");

View File

@ -1,32 +0,0 @@
package net.knarcraft.blacksmith.util;
import java.util.ArrayList;
import java.util.List;
/**
* A helper class for tab-completion
*/
public final class TabCompletionHelper {
private TabCompletionHelper() {
}
/**
* Finds tab complete values that contain the typed text
*
* @param values <p>The values to filter</p>
* @param typedText <p>The text the player has started typing</p>
* @return <p>The given string values that contain the player's typed text</p>
*/
public static List<String> filterMatchingContains(List<String> values, String typedText) {
List<String> configValues = new ArrayList<>();
for (String value : values) {
if (value.toLowerCase().contains(typedText.toLowerCase())) {
configValues.add(value);
}
}
return configValues;
}
}

View File

@ -1,13 +1,16 @@
package net.knarcraft.blacksmith.util;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.formatting.TranslatableMessage;
import net.knarcraft.blacksmith.formatting.BlacksmithTranslatableMessage;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.enchantments.Enchantment;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import static net.knarcraft.blacksmith.formatting.StringFormatter.displayErrorMessage;
/**
* A helper class for validating a value's type
*/
@ -25,7 +28,8 @@ public final class TypeValidationHelper {
* @param sender <p>The command sender to use for printing error messages</p>
* @return <p>True if the value is valid</p>
*/
public static boolean isValid(SettingValueType valueType, Object value, CommandSender sender) {
public static boolean isValid(@NotNull SettingValueType valueType, @Nullable Object value,
@Nullable CommandSender sender) {
try {
return switch (valueType) {
case POSITIVE_DOUBLE -> isPositiveDouble(value, sender);
@ -33,8 +37,10 @@ public final class TypeValidationHelper {
case POSITIVE_INTEGER -> isPositiveInteger(value, sender);
case PERCENTAGE -> isPercentage(value, sender);
case BOOLEAN -> true;
case STRING_LIST, REFORGE_ABLE_ITEMS -> isStringList(value, sender);
case MATERIAL, ENCHANTMENT -> false;
case REFORGE_ABLE_ITEMS, STRING_LIST -> isStringList(value, sender);
case MATERIAL -> isMaterial(value, sender);
case ENCHANTMENT -> isEnchantment(value, sender);
case ENCHANTMENT_LIST -> isEnchantmentList(value, sender);
};
} catch (ClassCastException exception) {
//This error signifies that an object is not a string, and of the wrong class
@ -42,6 +48,68 @@ public final class TypeValidationHelper {
}
}
/**
* Checks whether the given value is a material
*
* @param value <p>The value to check</p>
* @param sender <p>The command sender to use for printing error messages</p>
* @return <p>True if the value is a material</p>
*/
private static boolean isMaterial(@Nullable Object value, @Nullable CommandSender sender) {
boolean isMaterial = value instanceof Material || (value instanceof String string &&
InputParsingHelper.matchMaterial(string) != null);
if (!isMaterial && sender != null) {
BlacksmithPlugin.getStringFormatter().displayErrorMessage(sender,
BlacksmithTranslatableMessage.ITEM_TYPE_MATERIAL);
}
return isMaterial;
}
/**
* Checks whether the given value is a list of enchantments
*
* @param value <p>The value to check</p>
* @param sender <p>The command sender to use for printing error messages</p>
* @return <p>True if the value is an enchantment list</p>
*/
private static boolean isEnchantmentList(@Nullable Object value, @Nullable CommandSender sender) {
// Check whether a string list is given
if (!isStringList(value, sender)) {
return false;
}
// Make sure the input can be converted to a string list
List<String> strings = ConfigHelper.asStringList(value);
if (strings == null) {
return false;
}
// Make sure each value is an enchantment
for (String string : strings) {
if (!isEnchantment(string, sender)) {
return false;
}
}
return true;
}
/**
* Checks whether the given value is an enchantment
*
* @param value <p>The value to check</p>
* @param sender <p>The command sender to use for printing error messages</p>
* @return <p>True if the value is an enchantment</p>
*/
private static boolean isEnchantment(@Nullable Object value, @Nullable CommandSender sender) {
boolean isEnchantment = value instanceof Enchantment || (value instanceof String string &&
InputParsingHelper.matchEnchantment(string) != null);
if (!isEnchantment && sender != null) {
BlacksmithPlugin.getStringFormatter().displayErrorMessage(sender,
BlacksmithTranslatableMessage.ITEM_TYPE_ENCHANTMENT);
}
return isEnchantment;
}
/**
* Checks whether the given value is a string list
*
@ -49,10 +117,11 @@ public final class TypeValidationHelper {
* @param sender <p>The command sender to use for printing error messages</p>
* @return <p>True if the value is a string list</p>
*/
private static boolean isStringList(Object value, CommandSender sender) {
private static boolean isStringList(@Nullable Object value, @Nullable CommandSender sender) {
boolean isStringList = value instanceof String[] || value instanceof List<?> || value instanceof String;
if (!isStringList && sender != null) {
displayErrorMessage(sender, TranslatableMessage.INPUT_STRING_LIST_REQUIRED);
BlacksmithPlugin.getStringFormatter().displayErrorMessage(sender,
BlacksmithTranslatableMessage.INPUT_STRING_LIST_REQUIRED);
}
return isStringList;
}
@ -64,16 +133,14 @@ public final class TypeValidationHelper {
* @param sender <p>The command sender to use for printing error messages</p>
* @return <p>True if the value is a percentage</p>
*/
private static boolean isPercentage(Object value, CommandSender sender) {
try {
int intValue = ConfigHelper.asInt(value);
return intValue >= 0 && intValue <= 100;
} catch (NumberFormatException | NullPointerException exception) {
if (sender != null) {
displayErrorMessage(sender, TranslatableMessage.INPUT_PERCENTAGE_REQUIRED);
}
return false;
private static boolean isPercentage(@Nullable Object value, @Nullable CommandSender sender) {
boolean isPercentage = value != null && isPositiveInteger(value, null) &&
ConfigHelper.asInt(value) >= 0 && ConfigHelper.asInt(value) <= 100;
if (!isPercentage && sender != null) {
BlacksmithPlugin.getStringFormatter().displayErrorMessage(sender,
BlacksmithTranslatableMessage.INPUT_PERCENTAGE_REQUIRED);
}
return isPercentage;
}
/**
@ -83,10 +150,11 @@ public final class TypeValidationHelper {
* @param sender <p>The command sender to use for printing error messages</p>
* @return <p>True if the value is a non-empty string</p>
*/
private static boolean isNonEmptyString(Object value, CommandSender sender) {
boolean isString = value instanceof String string && !string.strip().isEmpty();
private static boolean isNonEmptyString(@Nullable Object value, @Nullable CommandSender sender) {
boolean isString = value instanceof String string && !string.isBlank();
if (!isString && sender != null) {
displayErrorMessage(sender, TranslatableMessage.INPUT_STRING_REQUIRED);
BlacksmithPlugin.getStringFormatter().displayErrorMessage(sender,
BlacksmithTranslatableMessage.INPUT_STRING_REQUIRED);
}
return isString;
}
@ -98,12 +166,13 @@ public final class TypeValidationHelper {
* @param sender <p>The command sender to use for printing error messages</p>
* @return <p>True if the value is a positive double</p>
*/
private static boolean isPositiveDouble(Object value, CommandSender sender) {
private static boolean isPositiveDouble(@Nullable Object value, @Nullable CommandSender sender) {
try {
return ConfigHelper.asDouble(value) >= 0.0;
return value != null && ConfigHelper.asDouble(value) >= 0.0;
} catch (NumberFormatException | NullPointerException exception) {
if (sender != null) {
displayErrorMessage(sender, TranslatableMessage.INPUT_POSITIVE_DOUBLE_REQUIRED);
BlacksmithPlugin.getStringFormatter().displayErrorMessage(sender,
BlacksmithTranslatableMessage.INPUT_POSITIVE_DOUBLE_REQUIRED);
}
return false;
}
@ -116,12 +185,13 @@ public final class TypeValidationHelper {
* @param sender <p>The command sender to use for printing error messages</p>
* @return <p>True if the value is a positive integer</p>
*/
private static boolean isPositiveInteger(Object value, CommandSender sender) {
private static boolean isPositiveInteger(@Nullable Object value, @Nullable CommandSender sender) {
try {
return ConfigHelper.asInt(value) >= 0;
return value != null && ConfigHelper.asInt(value) >= 0;
} catch (NumberFormatException | NullPointerException exception) {
if (sender != null) {
displayErrorMessage(sender, TranslatableMessage.INPUT_POSITIVE_INTEGER_REQUIRED);
BlacksmithPlugin.getStringFormatter().displayErrorMessage(sender,
BlacksmithTranslatableMessage.INPUT_POSITIVE_INTEGER_REQUIRED);
}
return false;
}

View File

@ -0,0 +1,38 @@
global.basePrice=blacksmith.global.basePrice
global.pricePerDurabilityPoint=blacksmith.global.pricePerDurabilityPoint
global.enchantmentCost=blacksmith.global.enchantmentCost
global.useNaturalCost=blacksmith.global.useNaturalCost
global.showExactTime=blacksmith.global.showExactTime
global.chippedAnvilRepairCost=blacksmith.global.chippedAnvilReforgingCost
global.damagedAnvilRepairCost=blacksmith.global.damagedAnvilReforgingCost
global.chippedAnvilReforgingCost=blacksmith.global.chippedAnvilReforgingCost
global.damagedAnvilReforgingCost=blacksmith.global.damagedAnvilReforgingCost
global.disableMaterialLimitation=
defaults.dropItem=blacksmith.defaults.dropItem
defaults.reforgeAbleItems=blacksmith.defaults.reforgeAbleItems
defaults.disableCoolDown=
defaults.disableDelay=
defaults.failReforgeChance=blacksmith.defaults.failReforgeChance
defaults.extraEnchantmentChance=blacksmith.defaults.extraEnchantmentChance
defaults.maxEnchantments=blacksmith.defaults.maxEnchantments
defaults.delaysInSeconds.maximum=blacksmith.defaults.maxReforgeWaitTimeSeconds
defaults.delaysInSeconds.minimum=blacksmith.defaults.minReforgeWaitTimeSeconds
defaults.delaysInSeconds.reforgeCoolDown=blacksmith.defaults.reforgeCoolDownSeconds
defaults.delaysInSeconds.blacksmithTitle=
defaults.messages.busyPlayerMessage=blacksmith.defaults.messages.busyPlayerMessage
defaults.messages.busyReforgeMessage=blacksmith.defaults.messages.busyReforgeMessage
defaults.messages.coolDownUnexpiredMessage=blacksmith.defaults.messages.coolDownUnexpiredMessage
defaults.messages.costMessage=blacksmith.defaults.messages.costMessage
defaults.messages.failReforgeMessage=blacksmith.defaults.messages.failReforgeMessage
defaults.messages.insufficientFundsMessage=blacksmith.defaults.messages.insufficientFundsMessage
defaults.messages.invalidItemMessage=blacksmith.defaults.messages.invalidItemMessage
defaults.messages.itemChangedMessage=blacksmith.defaults.messages.itemChangedMessage
defaults.messages.startReforgeMessage=blacksmith.defaults.messages.startReforgeMessage
defaults.messages.successMessage=blacksmith.defaults.messages.successMessage
defaults.messages.notDamagedMessage=blacksmith.defaults.messages.notDamagedMessage
defaults.blacksmithTitle=blacksmith.defaults.blacksmithTitle
defaults.enchantmentBlocklist=blacksmith.defaults.enchantmentBlockList
defaults.repairAnvils=blacksmith.defaults.reforgeAnvils
defaults.repairAnvils=
defaults.reforgeAnvils=blacksmith.defaults.reforgeAnvils
defaults.failReforgeRemovesEnchantments=blacksmith.defaults.failReforgeRemovesEnchantments

View File

@ -3,6 +3,8 @@
# The language used for messages. Only "en" is supported
language: en
# Settings for the blacksmith trait
blacksmith:
# The settings which apply to all Blacksmith NPCs. These can also be changed using the /blacksmithconfig command
global:
# The minimum price of each cost
@ -28,6 +30,12 @@ global:
# reforging delay messages, instead of just vaguely hinting at the remaining time.
showExactTime: false
# The cost of fully repairing a chipped anvil
chippedAnvilReforgingCost: 10.0
# The cost of fully repairing a damaged anvil
damagedAnvilReforgingCost: 20.0
# The settings which are set to any new NPC. To change any of these settings for an existing NPC, you must change the
# Citizens NPC file, or use the /blacksmith command
defaults:
@ -40,29 +48,33 @@ defaults:
reforgeAbleItems: [ ]
# The enchantments a blacksmith is denied from applying to an item. Disable anything you find too op or annoying.
enchantmentBlocklist: [ "binding_curse", "mending", "vanishing_curse" ]
enchantmentBlockList: [ "binding_curse", "mending", "vanishing_curse" ]
# The chance to fail reforging an item, which only repairs the item a tiny bit or not at all (0-100)
failReforgeChance: 10 # Default = 10%
# Whether failed reforging should remove or downgrade the item's enchantments
failReforgeRemovesEnchantments: false # Default = false
# The chance that an enchantment will be added to the reforged item (0-100)
extraEnchantmentChance: 5 # Default = 5%
# The maximum number of enchantments the blacksmith will try to add
maxEnchantments: 3
# All settable delays
delaysInSeconds:
# The maximum time for a reforging to finish
maximum: 30
# Whether the blacksmith will reforge anvils as a special case
reforgeAnvils: false
# The minimum time for a reforging to finish
minimum: 5
minReforgeWaitTimeSeconds: 5
# The maximum time for a reforging to finish
maxReforgeWaitTimeSeconds: 30
# The cool-down period between each reforge
reforgeCoolDown: 60
reforgeCoolDownSeconds: 60
# The title describing the blacksmith's usage/speciality
# The title describing the blacksmith's usage/speciality (e.x: armor-smith, tool-smith, weapon-smith)
blacksmithTitle: "blacksmith"
# All messages used by the NPC
@ -99,3 +111,138 @@ defaults:
# The message to display if a player is trying to reforge an item with full durability
notDamagedMessage: "&cThat item is not in need of repair"
# The message to display when a blacksmith is clicked with an empty hand
noItemMessage: "Please present the item you want to reforge"
# Settings for the scrapper trait
scrapper:
# The settings which apply to all Scrapper NPCs. These can also be changed using the /scrapperConfig command
global:
# Exact time displays the exact number of seconds and minutes remaining as part of the salvaging cool-down and
# salvaging delay messages, instead of just vaguely hinting at the remaining time.
showExactTime: false
# Whether enchanted salvaged items should return some amount of exp upon salvage
giveExperience: true
# Items treated as trash during salvage calculation. This follows the format:
# "MATERIAL[;MATERIAL2][;MATERIAL3]:TRASH_MATERIAL[;TRASH_MATERIAL2]", so the material or materials listed will
# treat the material specified after the ":" as trash when calculating salvage unless all non-trash items can be
# given as well(* matches any character).
trashSalvage:
- "*_SHOVEL;*_PICKAXE;*_AXE;*_HOE;*_SWORD;SHIELD;*_BOW:STICK"
# The cost of using a scrapper to salvage an item
salvagePrice: 0
# The cost of using the scrapper to remove armor trim
armorTrimSalvagePrice: 5
# The cost of using the scrapper to remove netherite from an item
netheriteSalvagePrice: 15
# The settings which are set to any new scrapper NPC. To change any of these settings for an existing NPC, you must
# change the Citizens NPC file, or use the /scrapper command
defaults:
# Whether the item will drop materials resulting from salvaging on the ground, instead of putting them into the user's inventory
dropItem: true
# The chance to fail a salvage, thus destroying the item (0-100)
failSalvageChance: 0
# The items a blacksmith is able to salvage. Setting this only allows NPCs to repair the listed items. This should be
# set for each individual NPC, and should not be set here, unless you want to restrict NPCs which have not been set
# up yet.
salvageAbleItems: [ ]
# The minimum time for a salvaging to finish
minSalvageWaitTimeSeconds: 5
# The maximum time for a salvaging to finish
maxSalvageWaitTimeSeconds: 30
# The cool-down period between each salvage
salvageCoolDownSeconds: 60
# The title describing the scrapper's usage/speciality (e.x armor-scrapper, tool-scrapper, weapon-scrapper)
scrapperTitle: "scrapper"
# Whether to enable salvaging of non-repairable items, such as planks
extendedSalvageEnabled: false
# Whether to enable salvaging of enchanted items. This is disabled by default because it's possible to accidentally
# salvage items with very good enchantments.
salvageEnchanted: false
# Whether to enable salvaging of armor trims
salvageArmorTrims: true
# Whether to enable salvaging of netherite items
salvageNetherite: true
# Default values for messages used by NPCs
messages:
# The message to display when another player is using the scrapper
busyPlayerMessage: "&cI'm busy at the moment. Come back later!"
# The message to display when the blacksmith is working on the salvaging
busySalvageMessage: "&cI'm working on it. Be patient! I'll finish {time}!"
# The message to display when the scrapper is still on a cool-down from the previous salvaging
coolDownUnexpiredMessage: "&cYou've already had your chance! Give me a break! I'll be ready {time}!"
# The message to display when holding an item the blacksmith is unable to reforge
invalidItemMessage: "&cI'm sorry, but I'm a/an {title}, I don't know how to salvage that!"
# The message to display if salvaging the player's item would result in no salvage
tooDamagedForSalvageMessage: "&cThat item is too damaged to be salvaged into anything useful"
# The message to display when an item is successfully salvaged
successSalvagedMessage: "There you go!"
# The message to display when the scrapper fails to salvage an item
failSalvageMessage: "&cWhoops! The item broke! Maybe next time?"
# The message to display when the scrapper fails to salvage an item without durability
failExtendedSalvageMessage: "&cWhoops! I was unable to extract much, if any, salvage! Maybe next time?"
# The message to display when presenting a different item than the one just evaluated
itemChangedMessage: "&cThat's not the item you wanted to salvage before!"
# The message to display once the scrapper starts salvaging
startSalvageMessage: "&eOk, let's see what I can do..."
# The message to display if the player is unable to pay the scrapper's fee
insufficientFundsMessage: "&cYou don't have enough money to salvage an item!"
# The message to display when explaining the shown item's salvage cost
costMessage: "&eIt will cost &a{cost}&e to salvage that &a{item}&e! {yield} &eClick again to salvage!"
# The message to display when explaining the shown item's armor trim's salvage cost
costMessageArmorTrim: "&eIt will cost &a{cost}&e to salvage that &a{item}&e's armor trim!"
# The message to display when explaining the shown item's netherite salvage cost
costMessageNetherite: "&eIt will cost &a{cost}&e to salvage that &a{item}&e into diamond!"
# The yield message to display if trying to salvage a non-damaged item
fullSalvageMessage: "&aI should be able to extract all components from that pristine item.&r"
# The yield message to display if trying to salvage a damaged item
partialSalvageMessage: "&cI cannot extract all components from that damaged item.&r"
# The message to display when asked to salvage an enchanted item, and that option is disabled
cannotSalvageEnchantedMessage: "&cI'm sorry, but I'm unable to salvage enchanted items!"
# The message to display when asked to salvage an armor trim, and that option is disabled
cannotSalvageArmorTrimMessage: "&cI'm sorry, but I'm unable to salvage armor trims!"
# The message to display if the correct materials to return for the armor trim are unknown
armorTrimSalvageNotFoundMessage: "&cI'm sorry, but I don't know how to salvage that armor trim!"
# The message to display when asked to salvage netherite items, and that option is disabled
cannotSalvageNetheriteMessage: "&cI'm sorry, but I'm unable to salvage netherite items!"
# The message to display when a scrapper is clicked with an empty hand
noItemMessage: "Please present the item you want to salvage"

View File

@ -1,39 +1,50 @@
name: Blacksmith
author: EpicKnarvik97, aPunch, jrbudda, HurricanKai
authors: [ EpicKnarvik97, aPunch, jrbudda, HurricanKai ]
version: 1.0.2
version: '${project.version}'
main: net.knarcraft.blacksmith.BlacksmithPlugin
depend: [ Citizens, Vault ]
softdepend: [ ProtocolLib ]
prefix: "Blacksmith"
description: "A plugin that adds Blacksmith and Scrapper traits compatible with Citizens, allowing players to repair and salvage items"
website: "https://www.spigotmc.org/resources/blacksmith.105938/"
api-version: 1.19
api-version: 1.20
commands:
blacksmith:
permission: blacksmith.edit
usage: /<command> <option> [new value]
description: Used for configuring the selected blacksmith NPC
blacksmithconfig:
description: Used for configuring the selected blacksmith NPC.
blacksmithConfig:
permission: blacksmith.admin
usage: /<command> <option/reload> [new value]
description: Used for configuring default blacksmith settings, or global settings
description: Used for configuring default blacksmith settings, or global blacksmith settings.
scrapper:
permission: blacksmith.edit
usage: /<command> <option> [new value]
description: Used for configuring the selected scrapper NPC.
scrapperConfig:
permission: blacksmith.admin
usage: /<command> <option/reload> [new value]
description: Used for configuring default scrapper settings, or global scrapper settings.
preset:
permission: blacksmith.preset
usage: /<command> <preset>[:filter]
description: Used to display which materials are part of a given preset. If a filter, such as diamond is used, the result of applying the filter is shown.
permissions:
blacksmith.admin:
description: Allows overall blacksmith configuration
description: Allows overall blacksmith and scrapper configuration.
default: op
children:
blacksmith.edit: true
blacksmith.use: true
blacksmith.preset: true
blacksmith.edit:
description: Allows changing settings for the selected blacksmith NPC
description: Allows changing settings for the selected blacksmith or scrapper NPC.
default: op
blacksmith.use:
description: Allows the player to repair items using blacksmiths
description: Allows the player to repair items using blacksmiths and salvage items using scrappers.
default: true
blacksmith.preset:
description: Allows the player to use the /preset command
description: Allows the player to use the /preset command.
default: op

View File

@ -1,32 +1,92 @@
# The english translation of internal strings. To add your own language, copy everything below, and change "en" to your
# own language's code, and change each string. Afterward, copy this file to Blacksmith's plugin folder, and change the
# language in config.yml to your language's language code.
en:
# The format used to display when a setting's value has been changed
VALUE_CHANGED: "&7{setting} set to &6{newValue}"
# The format used to display when a setting's value has been changed for a specific material or enchantment
VALUE_FOR_ITEM_CHANGED: "&7{setting} for {itemType} {item} set to &6{newValue}"
# The format used to display the current value of a setting
CURRENT_VALUE: "&7Current value of {setting}:&r {currentValue}"
# The format used to display the default value of a setting
DEFAULT_VALUE: "&7Default value of {setting}:&r {defaultValue}"
# The format used to display the current value of a setting for a specific material or enchantment
CURRENT_VALUE_FOR_ITEM: "&7Current value of {setting} for {itemType} {item}:&r {currentValue}"
# Translation of the enchantment item type (used for VALUE_FOR_ITEM_CHANGED, CURRENT_VALUE_FOR_ITEM)
ITEM_TYPE_ENCHANTMENT: "enchantment"
# Translation of the material item type (used for VALUE_FOR_ITEM_CHANGED, CURRENT_VALUE_FOR_ITEM)
ITEM_TYPE_MATERIAL: "material"
# The format used when displaying the raw formatting of messages
RAW_VALUE: "Raw value: {rawValue}"
NO_NPC_SELECTED: "You must select an NPC before running this command"
DEFAULT_REFORGE_ABLE_ITEMS_UNCHANGEABLE: "Changing reforge-able items globally will make every new blacksmith unable to reforge anything not in the list, unless it's changed for the individual NPC. If you really want to change this, change it manually."
# The translation of the message displayed when the player uses /blacksmith or /scrapper without selecting an NPC first
NO_NPC_SELECTED: "You must select an NPC with the correct trait before running this command (/npc select)"
# The translation of the message displayed when a comma-separated list of strings is expected by a setting, but not given
INPUT_STRING_LIST_REQUIRED: "A string list is required!"
# The translation of the message displayed when a percentage is expected, but not given
INPUT_PERCENTAGE_REQUIRED: "You specified a value which isn't between 0 and 100!"
# The translation of the message displayed when a non-empty text string is expected, but not given
INPUT_STRING_REQUIRED: "A non-empty string is required!"
# The translation of the message displayed when a positive double (comma number) is expected, but not given
INPUT_POSITIVE_DOUBLE_REQUIRED: "You specified a value which isn't a positive double!"
# The translation of the message displayed when a positive integer is expected, but not given
INPUT_POSITIVE_INTEGER_REQUIRED: "You specified a value which isn't a positive integer!"
# The translation of the message displayed when a player is missing the necessary permission for an action
PERMISSION_DENIED: "You lack the necessary permission"
# The translation of the message displayed when the configuration is reloaded
PLUGIN_RELOADED: "Blacksmith config reloaded!"
# The translation of the message displayed when an invalid filter is used for a smith preset
INVALID_FILTER_FOR_PRESET: "The specified filter is not valid for that preset"
# The translation of the message displayed when an invalid filter or preset is specified
INVALID_PRESET_OR_FILTER: "You specified an invalid preset or an invalid filter"
# The translation of the message displayed before listing all materials in a preset for the /preset command
PRESET_MATERIALS: "Materials in preset: {materials}"
# The format used when displaying any duration remaining
DURATION_FORMAT: "in {time} {unit}"
# The format used when NPCs talk to players ({npc} = The NPC's name, {message} is the actual message contents)
NPC_TALK_FORMAT: "&a[{npc}] -> You:&r {message}"
# Translation of the duration of less than a second
UNIT_NOW: "imminently"
# Translation of seconds in singular form
UNIT_SECOND: "second"
# Translation of seconds in plural form
UNIT_SECONDS: "seconds"
# Translation of minutes in singular form
UNIT_MINUTE: "minute"
# Translation of minutes in plural form
UNIT_MINUTES: "minutes"
# Translation of hour in singular form
UNIT_HOUR: "hour"
# Translation of hour in plural form
UNIT_HOURS: "hours"
# Translation of days in singular form
UNIT_DAY: "day"
# Translation of days in plural form
UNIT_DAYS: "days"
# Translation of weeks in singular form
UNIT_WEEK: "week"
# Translation of weeks in plural form
UNIT_WEEKS: "weeks"
# Translation of months in singular form
UNIT_MONTH: "month"
# Translation of months in plural form
UNIT_MONTHS: "months"
# Translation of years in singular form
UNIT_YEAR: "year"
# Translation of years in plural form
UNIT_YEARS: "years"
# Translation of decades in singular form
UNIT_DECADE: "decade"
# Translation of decades in plural form
UNIT_DECADES: "decades"
# The text shown if the player has less than 10 seconds left of waiting
INTERVAL_LESS_THAN_10_SECONDS: "in just a moment"
# The text shown if the player has to wait between 10 and 30 seconds
INTERVAL_LESS_THAN_30_SECONDS: "in a little while"
# The text shown if the player has to wait for between 30 seconds and 1 minute
INTERVAL_LESS_THAN_1_MINUTE: "in a while"
# The text shown if the player has to wait for less than 5 minutes, but more than 1 minute
INTERVAL_LESS_THAN_5_MINUTES: "after some time"
# The text shown if the player has to wait for more than 5 minutes
INTERVAL_MORE_THAN_5_MINUTES: "in quite a while"
# The marker shown when displaying values overridden for the selected NPC (not shown if the NPC inherits the default value)
SETTING_OVERRIDDEN_MARKER: " [NPC]"

View File

@ -0,0 +1,130 @@
package net.knarcraft.blacksmith.util;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* A test class to test the config helper
*/
public class ConfigHelperTest {
private static List<String> expectedStringList;
@BeforeAll
public static void setUp() {
expectedStringList = new ArrayList<>();
for (int i = 1; i < 8; i++) {
expectedStringList.add(String.valueOf(i));
}
}
@Test
public void asStringListStringTest() {
assertEquals(expectedStringList, ConfigHelper.asStringList("1,2,3,4,5,6,7"));
}
@Test
public void asStringListStringListTest() {
List<String> testValues = new ArrayList<>();
for (int i = 1; i < 8; i++) {
testValues.add(String.valueOf(i));
}
assertEquals(expectedStringList, ConfigHelper.asStringList(testValues));
}
@Test
public void asStringListIntListTest() {
List<Integer> testValues = new ArrayList<>();
for (int i = 1; i < 8; i++) {
testValues.add(i);
}
assertEquals(expectedStringList, ConfigHelper.asStringList(testValues));
}
@Test
public void asDoubleDoubleTest() {
assertEquals(5.244352345, ConfigHelper.asDouble(5.244352345));
}
@Test
public void asDoubleIntTest() {
assertEquals(554636d, ConfigHelper.asDouble(554636));
}
@Test
public void asDoubleDoubleStringTest() {
assertEquals(54.54642388, ConfigHelper.asDouble("54.54642388"));
}
@Test
public void asDoubleIntStringTest() {
assertEquals(967876554, ConfigHelper.asDouble("967876554"));
}
@Test
public void asDoubleInvalidDoubleStringTest() {
assertThrows(NumberFormatException.class, () -> ConfigHelper.asDouble("Potato"));
}
@Test
public void asDoubleInvalidObjectTest() {
assertThrows(ClassCastException.class, () -> ConfigHelper.asDouble(new ArrayList<>()));
}
@Test
public void asBooleanTrueTest() {
assertTrue(ConfigHelper.asBoolean(true));
}
@Test
public void asBooleanFalseTest() {
assertFalse(ConfigHelper.asBoolean(false));
}
@Test
public void asBooleanTrueStringTest() {
assertTrue(ConfigHelper.asBoolean("trUe"));
}
@Test
public void asBooleanFalseStringTest() {
assertFalse(ConfigHelper.asBoolean("faLse"));
}
@Test
public void asBooleanInvalidStringTest() {
assertFalse(ConfigHelper.asBoolean("potato"));
}
@Test
public void asBooleanInvalidObjectTest() {
assertThrows(ClassCastException.class, () -> {
boolean result = ConfigHelper.asBoolean(new ArrayList<>());
assertFalse(result);
});
}
@Test
public void asIntIntTest() {
assertEquals(5466547, ConfigHelper.asInt(5466547));
}
@Test
public void asIntStringIntTest() {
assertEquals(976586547, ConfigHelper.asInt("976586547"));
}
@Test
public void asIntInvalidTest() {
assertThrows(ClassCastException.class, () -> ConfigHelper.asInt(54675467d));
}
}

View File

@ -0,0 +1,67 @@
package net.knarcraft.blacksmith.util;
import org.bukkit.Material;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* A test class for testing input parsing
*/
public class InputParsingHelperTest {
@Test
public void isEmptyTrueTest() {
assertTrue(InputParsingHelper.isEmpty(null));
assertTrue(InputParsingHelper.isEmpty("-1"));
assertTrue(InputParsingHelper.isEmpty("\"\""));
assertTrue(InputParsingHelper.isEmpty("null"));
assertTrue(InputParsingHelper.isEmpty(" "));
}
@Test
public void isEmptyFalseTest() {
assertFalse(InputParsingHelper.isEmpty("potato"));
assertFalse(InputParsingHelper.isEmpty("67465775"));
assertFalse(InputParsingHelper.isEmpty("657.4655467"));
assertFalse(InputParsingHelper.isEmpty(" a "));
assertFalse(InputParsingHelper.isEmpty("\"fish\""));
assertFalse(InputParsingHelper.isEmpty("-657858"));
assertFalse(InputParsingHelper.isEmpty("-6578.58"));
}
@Test
public void matchMaterialValidTest() {
assertEquals(Material.DIAMOND_PICKAXE, InputParsingHelper.matchMaterial("DIAMOND_PICKAXE"));
assertEquals(Material.DIAMOND_AXE, InputParsingHelper.matchMaterial("DIAMOND_axe"));
assertEquals(Material.IRON_SWORD, InputParsingHelper.matchMaterial("iron-SWORD"));
assertEquals(Material.WOODEN_HOE, InputParsingHelper.matchMaterial("wOoDeN-HoE"));
assertEquals(Material.ACACIA_CHEST_BOAT, InputParsingHelper.matchMaterial("ACACIA-chest_bOaT"));
assertEquals(Material.NETHERITE_PICKAXE, InputParsingHelper.matchMaterial("netherite pickaxe"));
assertEquals(Material.STONE_SHOVEL, InputParsingHelper.matchMaterial("minecraft:stone_shovel"));
assertEquals(Material.GOLDEN_BOOTS, InputParsingHelper.matchMaterial("minecraft:golden BOOTS"));
}
@Test
public void matchMaterialInvalidTest() {
assertNull(InputParsingHelper.matchMaterial("this_item_does_not_exist"));
assertNull(InputParsingHelper.matchMaterial("stone:pickaxe"));
assertNull(InputParsingHelper.matchMaterial("aron sword"));
assertNull(InputParsingHelper.matchMaterial("minecraft : golden BOOTS"));
assertNull(InputParsingHelper.matchMaterial("minecraft: golden BOOTS"));
assertNull(InputParsingHelper.matchMaterial("MINECRAFT:golden-BOOTS"));
assertNull(InputParsingHelper.matchMaterial("minecraft: golden-BOOTS"));
}
@Test
public void regExIfyTest() {
assertEquals("NO REGEX HERE", InputParsingHelper.regExIfy("no regEx here"));
assertEquals("NO_REGEX_HERE", InputParsingHelper.regExIfy("no-regEx-here"));
assertEquals("NETHERITE.*", InputParsingHelper.regExIfy("netherite*"));
assertEquals("GOLDEN_.*", InputParsingHelper.regExIfy("golden-*"));
}
}

View File

@ -0,0 +1,34 @@
package net.knarcraft.blacksmith.util;
import org.bukkit.Material;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests for the ItemHelper class
*/
public class ItemHelperTest {
@Test
public void isAnvilRequiresDamagedTest() {
assertTrue(ItemHelper.isAnvil(Material.DAMAGED_ANVIL, true));
assertTrue(ItemHelper.isAnvil(Material.CHIPPED_ANVIL, true));
assertFalse(ItemHelper.isAnvil(Material.ANVIL, true));
assertFalse(ItemHelper.isAnvil(Material.POTATO, true));
assertFalse(ItemHelper.isAnvil(Material.IRON_HOE, true));
}
@Test
public void isAnvilTest() {
assertTrue(ItemHelper.isAnvil(Material.ANVIL, false));
assertTrue(ItemHelper.isAnvil(Material.DAMAGED_ANVIL, false));
assertTrue(ItemHelper.isAnvil(Material.CHIPPED_ANVIL, false));
assertFalse(ItemHelper.isAnvil(Material.POTATO, false));
assertFalse(ItemHelper.isAnvil(Material.IRON_HOE, false));
}
}

View File

@ -0,0 +1,147 @@
package net.knarcraft.blacksmith.util;
import net.knarcraft.blacksmith.config.SettingValueType;
import org.bukkit.Material;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests for the type validator helper
*/
public class TypeValidatorHelperTest {
@Test
public void isValidPositiveDoubleValidTest() {
SettingValueType type = SettingValueType.POSITIVE_DOUBLE;
assertTrue(TypeValidationHelper.isValid(type, 6578d, null));
assertTrue(TypeValidationHelper.isValid(type, 5346.4534, null));
assertTrue(TypeValidationHelper.isValid(type, "237.4378", null));
assertTrue(TypeValidationHelper.isValid(type, 74567, null));
assertTrue(TypeValidationHelper.isValid(type, "843552", null));
}
@Test
public void isValidPositiveDoubleInvalidTest() {
SettingValueType type = SettingValueType.POSITIVE_DOUBLE;
assertFalse(TypeValidationHelper.isValid(type, "potato", null));
assertFalse(TypeValidationHelper.isValid(type, 354f, null));
assertFalse(TypeValidationHelper.isValid(type, SettingValueType.STRING, null));
assertFalse(TypeValidationHelper.isValid(type, -1, null));
assertFalse(TypeValidationHelper.isValid(type, -0.45654, null));
assertFalse(TypeValidationHelper.isValid(type, null, null));
}
@Test
public void isValidStringValidTest() {
assertTrue(TypeValidationHelper.isValid(SettingValueType.STRING, "potato", null));
}
@Test
public void isValidStringInvalidTest() {
SettingValueType type = SettingValueType.STRING;
assertFalse(TypeValidationHelper.isValid(type, 5467, null));
assertFalse(TypeValidationHelper.isValid(type, SettingValueType.POSITIVE_DOUBLE, null));
assertFalse(TypeValidationHelper.isValid(type, "", null));
assertFalse(TypeValidationHelper.isValid(type, " ", null));
assertFalse(TypeValidationHelper.isValid(type, null, null));
}
@Test
public void isValidPositiveIntegerValidTest() {
SettingValueType type = SettingValueType.POSITIVE_INTEGER;
assertTrue(TypeValidationHelper.isValid(type, 6574567, null));
assertTrue(TypeValidationHelper.isValid(type, "785768", null));
}
@Test
public void isValidPositiveIntegerInvalidTest() {
SettingValueType type = SettingValueType.POSITIVE_INTEGER;
assertFalse(TypeValidationHelper.isValid(type, 0.2345, null));
assertFalse(TypeValidationHelper.isValid(type, -1, null));
assertFalse(TypeValidationHelper.isValid(type, "potato", null));
assertFalse(TypeValidationHelper.isValid(type, null, null));
}
@Test
public void isValidPercentageValidTest() {
SettingValueType type = SettingValueType.PERCENTAGE;
assertTrue(TypeValidationHelper.isValid(type, 33, null));
assertTrue(TypeValidationHelper.isValid(type, 100, null));
assertTrue(TypeValidationHelper.isValid(type, 0, null));
}
@Test
public void isValidPercentageInvalidTest() {
SettingValueType type = SettingValueType.PERCENTAGE;
assertFalse(TypeValidationHelper.isValid(type, -1, null));
assertFalse(TypeValidationHelper.isValid(type, -73, null));
assertFalse(TypeValidationHelper.isValid(type, 101, null));
assertFalse(TypeValidationHelper.isValid(type, 14.4, null));
assertFalse(TypeValidationHelper.isValid(type, "potato", null));
assertFalse(TypeValidationHelper.isValid(type, null, null));
}
@Test
public void isValidBooleanValidTest() {
SettingValueType type = SettingValueType.BOOLEAN;
//Note: As anything invalid will just be parsed to false, anything goes
assertTrue(TypeValidationHelper.isValid(type, null, null));
assertTrue(TypeValidationHelper.isValid(type, "potato", null));
assertTrue(TypeValidationHelper.isValid(type, "True", null));
assertTrue(TypeValidationHelper.isValid(type, "False", null));
assertTrue(TypeValidationHelper.isValid(type, "On", null));
assertTrue(TypeValidationHelper.isValid(type, "Off", null));
}
@Test
public void isValidStringListValidTest() {
SettingValueType type = SettingValueType.STRING_LIST;
List<String> testList = new ArrayList<>();
testList.add("fish");
testList.add("potato");
assertTrue(TypeValidationHelper.isValid(type, "1, 2, 3, 4, 5, 6, 7, 8", null));
assertTrue(TypeValidationHelper.isValid(type, new ArrayList<>(), null));
assertTrue(TypeValidationHelper.isValid(type, testList, null));
assertTrue(TypeValidationHelper.isValid(type, "potato", null));
assertTrue(TypeValidationHelper.isValid(type, "", null));
assertTrue(TypeValidationHelper.isValid(type, new String[]{"a", "b", "c"}, null));
}
@Test
public void isValidStringListInvalidTest() {
SettingValueType type = SettingValueType.STRING_LIST;
assertFalse(TypeValidationHelper.isValid(type, null, null));
assertFalse(TypeValidationHelper.isValid(type, 456, null));
assertFalse(TypeValidationHelper.isValid(type, -2345.4321, null));
}
@Test
public void isValidMaterialValidTest() {
SettingValueType type = SettingValueType.MATERIAL;
assertTrue(TypeValidationHelper.isValid(type, Material.POTATO, null));
assertTrue(TypeValidationHelper.isValid(type, "POTATO", null));
assertTrue(TypeValidationHelper.isValid(type, "minecraft:potato", null));
}
@Test
public void isValidMaterialInvalidTest() {
SettingValueType type = SettingValueType.MATERIAL;
assertFalse(TypeValidationHelper.isValid(type, null, null));
assertFalse(TypeValidationHelper.isValid(type, 1, null));
assertFalse(TypeValidationHelper.isValid(type, "random", null));
assertFalse(TypeValidationHelper.isValid(type, -546.678, null));
assertFalse(TypeValidationHelper.isValid(type, true, null));
}
@Test
public void isValidEnchantmentInvalidTest() {
SettingValueType type = SettingValueType.ENCHANTMENT;
assertFalse(TypeValidationHelper.isValid(type, null, null));
}
}