Compare commits

...
140 Commits
Author SHA1 Message Date
Ben Woo 72ee0d5bed Merge pull request #3402 from Multiverse/fix/on-change-node
Always fire onChange for node even if old and new values may be equal
2025-12-12 12:01:31 +08:00
Ben Woo 1afecd40ea Merge pull request #3401 from Multiverse/chore/1.21.11
Update platforms supported version for 1.21.11
2025-12-12 11:57:24 +08:00
Ben Woo 00944ed3b2 Always fire onChange for node even if old and new values may be equal 2025-12-12 11:56:50 +08:00
Ben Woo 5f659a80d8 Update platforms supported version for 1.21.11 2025-12-11 14:24:39 +08:00
Ben Woo 5f9b0af8d1 Merge pull request #3400 from Multiverse/feat/notify-restart
Add restart message for config options that only applies after a server restart
2025-12-10 10:44:34 +08:00
Ben Woo 5c44c05f6f Add restart message for config options that only applies after a server restart 2025-12-09 20:48:04 +08:00
Ben Woo 1699a47011 Merge pull request #3396 from Multiverse/feat/unset-entity-rate
Default spawn rate and limit to "unset" instead of server wide's default
2025-12-09 17:16:27 +08:00
Ben Woo 96deee719b Merge pull request #3397 from Multiverse/fix/config-reset
Remove config saving on plugin disable
2025-12-09 17:15:33 +08:00
Ben Woo 8a60761098 Merge pull request #3398 from Multiverse/fix/arg-handling
Add maxArgForAware flag to gamerule commands for improved argument handling
2025-12-09 17:14:35 +08:00
Ben Woo a38b4e03b4 Merge pull request #3399 from Multiverse/fix/localise-replacements
Properly localise message replacement even if message itself is not localised
2025-12-09 17:14:18 +08:00
Ben Woo db9b725e10 Properly localise message replacement even if message itself is not localised 2025-12-09 16:17:43 +08:00
Ben Woo d43b23e7ea Add maxArgForAware flag to gamerule commands for improved argument handling 2025-12-09 15:59:57 +08:00
Ben Woo 86d77ae796 Remove config saving on plugin disable 2025-12-09 15:50:38 +08:00
Ben Woo 86bf785c84 Improve config saving error checking 2025-12-09 15:50:19 +08:00
Ben Woo 626537290d Default spawn rate and limit to "unset" instead of server wide's default 2025-12-09 14:41:01 +08:00
Ben Woo 9fa47727f4 Merge pull request #3395 from Multiverse/feat/node-load-change
Refactor configuration node on set callbacks to split up load and change events
2025-12-09 12:34:12 +08:00
Ben Woo ae3bc6d201 Rename NodeLoadCallback to NodeValueCallback to extend it potential use case 2025-12-09 11:28:02 +08:00
Ben Woo b1d126397e Refactor configuration node on set callbacks to split up load and change events 2025-12-08 21:18:10 +08:00
Ben Woo 8ab9993e97 Merge pull request #3394 from Multiverse/feat/case-insensitive
Make world name matching case-insensitive to follow papermc behvaiour
2025-12-08 21:03:28 +08:00
Ben Woo 13b949c692 Make world name matching case-insensitive to follow papermc behvaiour 2025-12-08 17:39:10 +08:00
Ben Woo 2b41c1367e Merge pull request #3393 from Multiverse/feat/more-patterns
Add more useful common regex patterns
2025-12-08 17:21:25 +08:00
Ben Woo fde3f388bd Add more useful common regex patterns 2025-12-07 20:56:00 +08:00
Ben Woo eb3968b7d6 Merge pull request #3391 from Multiverse/feat/sort-plugin-list
Sort plugin list alphabetically for dumps
2025-12-04 10:20:19 +08:00
Ben Woo fed1fdcbea Sort plugin list alphabetically for dumps 2025-12-03 10:34:05 +08:00
Ben Woo bfb6fea5f5 Merge pull request #3390 from Multiverse/feat/placeholder-playercount
Add player count placeholder
2025-12-02 23:13:13 +08:00
Ben Woo 5b02452a3d Add player count placeholder 2025-12-02 23:03:30 +08:00
Ben Woo 4a2bfc74fa Merge pull request #3361 from Multiverse/v5.4
V5.4
2025-12-01 17:06:01 +08:00
Ben Woo f0539441cf Merge pull request #3388 from Multiverse/chore/purpur-loader
Add purpur as a supported loader tag on modrinth
2025-12-01 17:04:51 +08:00
Ben Woo efe490d8c4 Add purpur as a supported loader tag on modrinth 2025-12-01 16:55:41 +08:00
Ben Woo 3074cfab17 Fix typo in maxArgForAware for who command 2025-12-01 16:51:16 +08:00
Ben Woo 7a5dd44feb Merge pull request #3383 from Multiverse/refactor/using-option
Refactor world manager methods with better use of streams and option
2025-12-01 16:41:21 +08:00
Ben Woo 88a9ee5b6d Merge pull request #3387 from Multiverse/fix/selectors-error-spam
Fix selectors spamming stacktrace and improve invalid format message
2025-12-01 16:41:02 +08:00
Ben Woo 46f67240e8 Merge pull request #3384 from Multiverse/fix/issuer-aware
Fix ambiguous error message with issue aware args is not found by using maxArgForAware
2025-12-01 16:40:45 +08:00
Ben Woo 89e765b2e8 Fix selectors spamming stacktrace and improve invalid format message 2025-11-29 11:15:04 +08:00
Ben Woo b8bdec22d3 Fix ambiguous error message with issue aware args is not found by using maxArgForAware 2025-11-19 15:32:26 +08:00
Ben Woo 792913cff7 Refactor world manager methods with better use of streams and option 2025-11-19 15:00:39 +08:00
Ben Woo 6dcb40fdcb Merge pull request #3380 from Multiverse/fix/issuer-aware-argument
Better error message for teleport command when no players match selectors
2025-11-19 11:25:46 +08:00
Ben Woo 2bb601098a Merge pull request #3382 from Multiverse/refactor/attempt
Refactor Attempt interface to remove default method implementations and enforce contract
2025-11-18 16:40:22 +08:00
Ben Woo 4bcebd51f5 Merge pull request #3381 from Multiverse/chore/whitespace
Remove unnecessary blank line in CloneWorldOptions.java
2025-11-18 16:40:01 +08:00
Ben Woo e207eb5d0d Merge branch 'main' into v5.4 2025-11-18 16:37:44 +08:00
Ben Woo f181bb7ade Better error message for teleport command when no players match selectors. 2025-11-18 16:34:38 +08:00
Ben Woo 7207d9303f Refactor IssuerAwareContextBuilder and add maxArgForAware option 2025-11-18 16:34:00 +08:00
Ben Woo 82799cdbe8 Open up PlayerFinder::isSelector method 2025-11-18 16:31:12 +08:00
Ben Woo 3629babdbc Remove unnecessary blank line in CloneWorldOptions.java 2025-11-18 15:34:49 +08:00
Ben Woo eed3353e2b Refactor Attempt interface to remove default method implementations and enforce contract 2025-11-18 15:34:28 +08:00
Ben Woo 147564caab Merge pull request #3372 from Multiverse/fix/perm-register
Register permission with its wildcards explicitly
2025-11-13 10:52:04 +08:00
Ben Woo 30e50866bd Merge pull request #3376 from Multiverse/fix/java-17-issue
Fix compatibility with Java 17
2025-11-05 17:29:46 +08:00
Ben Woo 5dde7608f2 Merge pull request #3377 from Multiverse/fix/checkstyle-action
Bump checkstyle action version
2025-11-05 17:29:11 +08:00
Ben Woo 2f9915c62e Bump checkstyle action version 2025-11-05 17:28:36 +08:00
Ben Woo e62e37831b Fix compatibility with Java 17 2025-11-05 10:09:28 +08:00
Ben Woo ef9934f3c6 Merge pull request #3373 from riku6460/fix-set-spawn
Fix spawn location inconsistency
2025-11-04 11:20:19 +08:00
Ben Woo 691bcd0ffa Merge pull request #3375 from BenTechDev/patch-1
✏️ fix typo in UnloadWorldOptions
2025-11-04 11:10:00 +08:00
BenTech 148644970d ✏️ fix typo in UnloadWorldOptions 2025-11-03 18:34:42 +00:00
riku6460 ad20ec94c5 Fix spawn location inconsistency
CraftWorld.setSpawnLocation(Location) requires the world in location.
https://github.com/PaperMC/Paper/blob/e8c6ba5068287049c890688534963041208c4213/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java#L338

See #3321
2025-11-03 03:33:06 +09:00
Ben Woo f7d86884b4 Merge pull request #3369 from Multiverse/feat/simplified-destination
Add simplified destination tab completion configuration option
2025-10-30 11:30:12 +08:00
Ben Woo 0e0e4e0434 Register permission with its wildcards explicitly 2025-10-30 11:29:12 +08:00
Ben Woo 8e878b1ed0 Merge pull request #3370 from Multiverse/fix/passenger-mode
Update default passenger mode description for papermc 1.21.10 changes
2025-10-26 23:21:33 +08:00
Ben Woo e1c6af2f38 Update default passenger mode description for papermc 1.21.10 changes 2025-10-26 13:37:58 +08:00
Ben Woo b52d65757a Fix config test 2025-10-26 13:27:55 +08:00
Ben Woo ff2135c775 Add simplified destination tab completion configuration option 2025-10-26 12:09:06 +08:00
Ben Woo c4e4e311e2 Merge pull request #3368 from Multiverse/perf/suggest-destination
Minor perf improvement to suggestDestinationsWithPerms
2025-10-26 11:59:39 +08:00
Ben Woo 3e4ded52e3 Merge branch 'main' into v5.4 2025-10-26 11:45:23 +08:00
Ben Woo 4ef8390ea3 Minor perf improvement to suggestDestinationsWithPerms 2025-10-26 11:44:57 +08:00
Ben Woo bca71e1acc Merge pull request #3367 from Multiverse/fix/tab-complete-quotes
Quote multi-word strings for all tab-completion
2025-10-24 14:43:14 +08:00
Ben Woo 343da8c301 Quote multi-word strings for all tab-completion 2025-10-24 14:29:07 +08:00
Ben Woo d34f3adccc Merge pull request #3366 from Multiverse/fix/coordinate-scale
Fix spigot compatibility with getCoordinateScale
2025-10-20 11:33:59 +08:00
Ben Woo b3862faf88 Fix spigot compatibility with getCoordinateScale 2025-10-20 10:56:31 +08:00
Ben Woo 8bffffdc65 Merge pull request #3364 from Multiverse/feat/silent-teleport
Add a silent flag to turn off teleport output message
2025-10-16 11:21:03 +08:00
Ben Woo 79a71fa58f Add a silent flag to turn off teleport output message 2025-10-15 22:43:22 +08:00
Ben Woo a41476de89 Merge pull request #3363 from Multiverse/fix/warning-spam
Revert throw an error when trying to set spawn-rate or spawn-limit if…
2025-10-15 22:30:32 +08:00
Ben Woo fe75e497e4 Revert throw an error when trying to set spawn-rate or spawn-limit if disabled by config 2025-10-15 22:26:31 +08:00
Ben Woo fdd9257e2c Merge pull request #3359 from Multiverse/fix/async-online-players
Implement ConcurrentPlayerWorldTracker to allow for async online player names and worlds lookup
2025-10-13 17:41:37 +08:00
Ben Woo 55d1347d4f Merge pull request #3360 from Multiverse/feat/destination-message
Implement display message for destination instances to improve teleport command output
2025-10-13 17:38:45 +08:00
Ben Woo 29cc97486b Add @since 5.4 annotations to ConcurrentPlayerWorldTracker methods 2025-10-13 17:37:41 +08:00
Ben Woo 8d06b92edd Implement display message for destination instances to improve teleport command output 2025-10-12 23:27:12 +08:00
Ben Woo ce3b6ab700 Make getOnlinePlayers unmodifiable and getPlayerWorld return option 2025-10-12 23:00:01 +08:00
Ben Woo 58ac0f505c Make tab completion for players a non-async call 2025-10-12 22:58:50 +08:00
Ben Woo 4004554733 Merge pull request #3358 from Multiverse/chore/mockbukkit-dep
Bump mockbukkit and fix test dep
2025-10-12 22:42:15 +08:00
Ben Woo 99037b2af5 Bump mockbukkit and fix test dep 2025-10-12 22:37:30 +08:00
Ben Woo ac46a5f64a Merge pull request #3355 from Multiverse/chore/config-links
Update wiki and add donation link in `config.yml`
2025-10-09 22:08:47 +08:00
Ben Woo 4d805b921b Merge pull request #3356 from Multiverse/chore/copper-update
Add supported mc versions for 1.21.9 and 1.21.10
2025-10-09 21:55:48 +08:00
Ben Woo a612854490 Add supported mc versions for 1.21.9 and 1.21.10 2025-10-09 21:42:20 +08:00
Ben Woo e85c14e319 Update wiki and add donation link in config.yml 2025-10-09 21:39:25 +08:00
Ben Woo 3983af7b4c Merge pull request #3351 from Multiverse/fix/spawn-location
Fix join location on 1.21.9 due to deprecated PlayerSpawnLocationEvent
2025-10-03 23:07:40 +08:00
Ben Woo 8853f88f3a Fix join location on 1.21.9 due to deprecated PlayerSpawnLocationEvent 2025-10-03 23:01:07 +08:00
Ben Woo 08d361e0ec Merge pull request #3350 from Multiverse/chore/mockbukkit-bump
Bump multiverse-plugin and mockbukkit version
2025-10-01 21:51:18 +08:00
Ben Woo f985f2db06 Bump multiverse-plugin and mockbukkit version 2025-10-01 21:46:38 +08:00
Ben Woo c068225950 Implement ConcurrentPlayerWorldTracker to allow for async online player names and worlds lookup 2025-10-01 21:45:27 +08:00
Ben Woo 82acca3ff9 Merge pull request #3348 from Multiverse/feat/properties-bukkit-world
Use bukkit world properties for difficulty, scale, and keep-spawn when create/import
2025-10-01 21:34:05 +08:00
Ben Woo d712c1eb09 Merge pull request #3347 from Multiverse/fix/deprecated-biome
Refactor biome parsing to use NamespacedKey and Registry
2025-10-01 21:33:28 +08:00
Ben Woo 25e9213ee9 Merge pull request #3349 from Multiverse/fix/apply-spawn-disabled
Throw an error when trying to set spawn-rate or spawn-limit if disabled by config
2025-10-01 21:33:02 +08:00
Ben Woo 9e79708fec Refactor biome parsing to use NamespacedKey and Registry 2025-09-29 22:18:42 +08:00
Ben Woo 65d1141289 Throw an error when trying to set spawn-rate or spawn-limit if disabled by config 2025-09-29 22:01:51 +08:00
Ben Woo 63b2ffdf72 Use bukkit world properties for difficulty, scale, and keep-spawn when create/import 2025-09-29 21:34:00 +08:00
Ben Woo 14d8ad7a5c Merge pull request #3346 from Multiverse/fix/locale-lowercase
Fix issues with lower/uppercasing string when server is of non-english locale
2025-09-29 21:18:29 +08:00
Ben Woo 33e0df925f Merge pull request #3345 from Multiverse/chore/non-public-listener
Cleanup listener methods to be non-public
2025-09-29 21:17:45 +08:00
Ben Woo 1883edbb28 Fix issues with lower/uppercasing string when server is of non-english locale 2025-09-29 20:47:08 +08:00
Ben Woo b7a2bd61ca Cleanup listener methods to be non-public 2025-09-29 20:22:40 +08:00
Ben Woo a0673ac109 Merge pull request #3344 from Multiverse/fix/test-generator
Pass empty string instead of null for generator id and improve error message
2025-09-28 17:50:10 +08:00
Ben Woo 7735a33ccf Change test error message to not show full stacktrace 2025-09-28 17:47:05 +08:00
Ben Woo 5f24520685 Pass empty string instead of null for generator type 2025-09-28 17:46:31 +08:00
Ben Woo 608cb8611c Merge pull request #3343 from Multiverse/fix/custom-env
Remove tab completing "custom" environment type
2025-09-28 10:19:03 +08:00
Ben Woo a965b9ce56 Remove tab completing "custom" environment type 2025-09-28 10:15:08 +08:00
Ben Woo 981d368058 Merge pull request #3338 from Multiverse/feat/relative-positioning
Add back support for relative position with ~ in destination coordinates
2025-09-22 20:10:26 +08:00
Ben Woo 7507099a4e Merge pull request #3341 from Multiverse/api/unloadedworldlocation-final
Make UnloadedWorldLocation non-final to allow extending
2025-09-22 20:08:52 +08:00
Ben Woo 29b58b1655 Make UnloadedWorldLocation non-final to allow extending 2025-09-22 19:57:10 +08:00
Ben Woo a1081c1b6b Merge pull request #3340 from Multiverse/feat/generator-test
Improve generator plugin detection and add an option to disable it
2025-09-22 19:55:36 +08:00
Ben Woo 3152b62668 Merge pull request #3339 from Multiverse/fix/worlds-old-exist
Fix issue when trying to backup `worlds.yml.old` and file already exists
2025-09-22 19:51:57 +08:00
Ben Woo 0082cc9852 Improve generator plugin detection and add an option to disable it 2025-09-22 19:51:18 +08:00
Ben Woo f60b656bed Fix issue when trying to backup worlds.yml.old and file already exists 2025-09-22 19:27:58 +08:00
Ben Woo da5f5de5cd Add localization for position parse error messages 2025-09-20 21:33:16 +08:00
Ben Woo e9aacecc2f Add back support for relative position with ~ in destination coordinates 2025-09-20 20:31:13 +08:00
Ben Woo fa3adbe605 Merge pull request #3332 from Multiverse/fix/delete-fail-message
Fix message when delete failed due to invalid world folder
2025-09-06 13:11:39 +08:00
Ben Woo e33b2f7025 Fix message when delete failed due to invalid world folder 2025-09-06 13:07:03 +08:00
Ben Woo a9cc577754 Merge pull request #3330 from ElFrod0/config-server-properties-path
Configurable server.properties path
2025-08-31 10:34:48 +08:00
Ben Woo 02344a31cf Add api status annotation for new server-properties-path config 2025-08-31 10:30:33 +08:00
ElFrod0 2f4871ef71 Add newline for consistency 2025-08-29 19:40:41 +02:00
ElFrod0 9750fad4b7 Update fresh config for tests 2025-08-29 19:35:21 +02:00
ElFrod0 df77372d96 Configurable server.properties file location 2025-08-29 19:33:49 +02:00
Ben Woo 039af3c219 Merge pull request #3327 from Multiverse/5.3
5.3
2025-08-29 10:34:05 +08:00
Ben Woo 09ec4f87ef Merge pull request #3328 from Multiverse/fix/mvapi-not-started
Pass in config object directly into spawn config class instead of using mvapi
2025-08-27 14:06:11 +08:00
Ben Woo f996f22f4b Pass in config object directly into spawn config class instead of using mvapi 2025-08-27 14:02:37 +08:00
Ben Woo b230c5ae9c Merge pull request #3326 from Multiverse/fix/spawning-config-order
Maintain spawn category config order between config saves
2025-08-27 13:50:30 +08:00
Ben Woo 2561107697 Merge pull request #3325 from Multiverse/feat/disable-apply-spawn
Add option to disable mv handling of spawn rate and limit
2025-08-27 13:48:42 +08:00
Ben Woo 70babc8656 Maintain spawn category config order between config saves 2025-08-27 13:46:59 +08:00
Ben Woo bd09d6c950 Merge pull request #3324 from Multiverse/chore/deps
Fix some dependency issues
2025-08-27 13:35:17 +08:00
Ben Woo b94de763ca Depend on latest paper api version 2025-08-27 12:40:52 +08:00
Ben Woo 22ca57543f Change mockbukkit version to fix test issues 2025-08-27 12:40:25 +08:00
Ben Woo 25c70170d9 Add option to disable mv handling of spawn rate and limit 2025-08-27 12:35:09 +08:00
Ben Woo 9926041c2f Merge pull request #3321 from KioProject123/fix/spawn-location-angle
fix spawn location angle
2025-08-18 15:06:10 +08:00
KioProject 1c99898d0d fix 2025-08-18 13:49:01 +08:00
KioProject 41e0261eee fix spawn location angle 2025-08-18 13:13:25 +08:00
Ben Woo 642f3ab1b8 Merge pull request #3320 from MrXiaoM/patch-1
Fix adventure dep in spigot-based server
2025-08-17 22:57:20 +08:00
人間工作 57d1844756 fix NoClassDefFoundError in spigot-based server 2025-08-17 22:09:17 +08:00
Ben Woo 73e1650fc3 Merge pull request #3318 from Multiverse/fix/match-invalid-regex
Properly handle error from invalid regex string in matcher
2025-08-11 13:08:49 +08:00
Ben Woo 800709ca95 Properly annotate parameters for matcher classes 2025-08-11 12:43:38 +08:00
Ben Woo 935cbad21c Properly handle error from invalid wildcard string in matcher 2025-08-11 12:40:05 +08:00
Ben Woo 0e2a27b298 Merge pull request #3317 from Multiverse/fix/remove-logging
Fix unwanted warning logging for matcher
2025-08-11 12:28:59 +08:00
Ben Woo baaa175423 Properly handle error from invalid regex string in matcher 2025-08-11 12:24:36 +08:00
Ben Woo e2b7a31191 Fix unwanted warning logging for matcher 2025-08-11 12:15:16 +08:00
99 changed files with 3158 additions and 815 deletions

No files matched your search

+2 -2
View File
@@ -11,9 +11,9 @@ jobs:
pull-requests: write
continue-on-error: true
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- uses: dbelyaev/action-checkstyle@v0.8.4
- uses: dbelyaev/action-checkstyle@v3
continue-on-error: true
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -107,9 +107,9 @@ jobs:
files: '["${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar"]'
name: ${{ steps.release-info.outputs.tag_name }}
changelog: ${{ steps.release-artifact.outputs.body }}
game_versions: 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
game_versions: 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
version_type: ${{ steps.parse-release-type.outputs.release_type }}
loaders: bukkit, spigot, paper
loaders: bukkit, spigot, paper, purpur
dependencies: ${{ inputs.modrinth_dependencies }}
- name: Upload to DBO
@@ -121,7 +121,7 @@ jobs:
changelog: ${{ steps.release-artifact.outputs.body }}
changelog_type: markdown
display_name: ${{ steps.release-info.outputs.tag_name }}
game_versions: 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
game_versions: 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
release_type: ${{ steps.parse-release-type.outputs.release_type }}
project_relations: ${{ inputs.dbo_project_relations }}
file_path: ${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar
@@ -136,5 +136,5 @@ jobs:
channel: ${{ steps.parse-release-type.outputs.release_type }}
files: '[{"path": "${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar", "platforms": ["PAPER"]}]'
description: ${{ steps.release-artifact.outputs.body }}
platform_dependencies: '{"PAPER": ["1.18.2-1.21.8"]}'
platform_dependencies: '{"PAPER": ["1.18.2-1.21.11"]}'
plugin_dependencies: ${{ inputs.hangar_plugin_dependencies }}
+3 -3
View File
@@ -1,7 +1,7 @@
import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'org.mvplugins.multiverse-plugin' version '1.2.0'
id 'org.mvplugins.multiverse-plugin' version '1.2.2'
}
group = 'org.mvplugins.multiverse.core'
@@ -25,9 +25,9 @@ repositories {
}
configure(apiDependencies) {
serverApiVersion = '1.19.4-R0.1-SNAPSHOT'
serverApiVersion = '1.21.8-R0.1-SNAPSHOT'
mockBukkitServerApiVersion = '1.21'
mockBukkitVersion = '4.72.2'
mockBukkitVersion = '4.84.0'
}
dependencies {
@@ -124,7 +124,6 @@ public class MultiverseCore extends MultiverseModule {
public void onDisable() {
super.onDisable();
MultiverseCoreApi.shutdown();
saveAllConfigs();
shutdownDependencyInjection();
PluginServiceLocatorFactory.get().shutdown();
Logging.shutdown();
@@ -215,11 +214,11 @@ public class MultiverseCore extends MultiverseModule {
*/
private Try<Void> saveAllConfigs() {
return configProvider.get().save()
.flatMap(ignore -> worldManagerProvider.get().saveWorldsConfig())
.flatMap(ignore ->anchorManagerProvider.get().saveAllAnchors())
.onFailure(e -> {
Logging.severe("Failed to save configs, things may not work as expected.");
});
.andThenTry(() -> worldManagerProvider.get().saveWorldsConfig())
.andThenTry(() -> anchorManagerProvider.get().saveAllAnchors())
.onFailure(e ->
Logging.severe("Failed to save all configs, things may not work as expected. %s",
e.getLocalizedMessage()));
}
/**
@@ -1,7 +1,6 @@
package org.mvplugins.multiverse.core;
import com.google.common.collect.Lists;
import io.vavr.control.Option;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
@@ -20,9 +19,8 @@ import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@Service
final class PlaceholderExpansionHook extends PlaceholderExpansion {
@@ -82,7 +80,7 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
return null;
}
final var placeholder = paramsArray.removeFirst();
final var placeholder = paramsArray.remove(0);
String worldName = parseWorldName(offlinePlayer, paramsArray);
if (worldName == null) return null;
@@ -105,9 +103,9 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
}
// Try get from params
String paramWorldName = paramsArray.getLast();
String paramWorldName = paramsArray.get(paramsArray.size() - 1);
if (worldManager.isLoadedWorld(paramWorldName)) {
paramsArray.removeLast();
paramsArray.remove(paramsArray.size() - 1);
return paramWorldName;
}
@@ -123,7 +121,7 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
@NotNull List<String> placeholderParams,
@NotNull LoadedMultiverseWorld world) {
// Switch to find what specific placeholder we want
switch (placeholder.toLowerCase()) {
switch (placeholder.toLowerCase(Locale.ENGLISH)) {
case "alias" -> {
return world.getAliasOrName();
}
@@ -166,6 +164,12 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
case "name" -> {
return world.getName();
}
case "playercount" -> {
return String.valueOf(world.getBukkitWorld()
.map(World::getPlayers)
.map(List::size)
.getOrElse(-1));
}
case "playerlimit" -> {
return String.valueOf(world.getPlayerLimit());
}
@@ -179,7 +183,7 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
return String.valueOf(world.getSeed());
}
case "time" -> {
String timeFormat = !placeholderParams.isEmpty() ? placeholderParams.getFirst() : "";
String timeFormat = !placeholderParams.isEmpty() ? placeholderParams.get(0) : "";
long time = world.getBukkitWorld().map(World::getTime).getOrElse(0L);
switch (timeFormat) {
case "" -> {
@@ -6,6 +6,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
@@ -26,6 +27,7 @@ import org.bukkit.GameRule;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.SpawnCategory;
import org.jetbrains.annotations.NotNull;
@@ -42,9 +44,9 @@ import org.mvplugins.multiverse.core.config.handle.PropertyModifyAction;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.core.WorldDestination;
import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -87,7 +89,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
registerAsyncCompletion("commands", this::suggestCommands);
registerAsyncCompletion("destinations", this::suggestDestinations);
registerStaticCompletion("difficulties", suggestEnums(Difficulty.class));
registerStaticCompletion("environments", suggestEnums(World.Environment.class));
registerStaticCompletion("environments", List.of("normal", "nether", "the_end")); // Don't tab complete the "custom" environment
registerAsyncCompletion("flags", this::suggestFlags);
registerStaticCompletion("gamemodes", suggestEnums(GameMode.class));
registerStaticCompletion("gamerules", this::suggestGamerules);
@@ -99,7 +101,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
registerAsyncCompletion("mvworlds", this::suggestMVWorlds);
registerAsyncCompletion("mvworldpropsname", this::suggestMVWorldPropsName);
registerAsyncCompletion("mvworldpropsvalue", this::suggestMVWorldPropsValue);
registerAsyncCompletion("playersarray", this::suggestPlayersArray);
registerCompletion("playersarray", this::suggestPlayersArray); // getting online players cannot be async
registerStaticCompletion("propsmodifyaction", suggestEnums(PropertyModifyAction.class));
registerStaticCompletion("spawncategories", suggestEnums(SpawnCategory.class));
registerAsyncCompletion("spawncategorypropsname", this::suggestSpawnCategoryPropsName);
@@ -176,7 +178,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
public <T extends Enum<T>> Collection<String> suggestEnums(Class<T> enumClass) {
return EnumSet.allOf(enumClass).stream()
.map(Enum::name)
.map(String::toLowerCase)
.map(name -> name.toLowerCase(Locale.ENGLISH))
.toList();
}
@@ -223,18 +225,22 @@ public class MVCommandCompletions extends PaperCommandCompletions {
// Most likely console did not specify a player
return Collections.<String>emptyList();
}
if (context.hasConfig("othersOnly") && (players.length == 1 && players[0].equals(context.getIssuer().getIssuer()))) {
CommandSender sender = context.getIssuer().getIssuer();
if (context.hasConfig("othersOnly") && (players.length == 1 && players[0].equals(sender))) {
return Collections.<String>emptyList();
}
return suggestDestinationsWithPerms(context.getIssuer().getIssuer(), players, context.getInput());
return suggestDestinationsWithPerms(sender, Arrays.asList(players), context.getInput());
})
.getOrElse(Collections.emptyList());
}
private Collection<String> suggestDestinationsWithPerms(CommandSender teleporter, Player[] players, String deststring) {
private Collection<String> suggestDestinationsWithPerms(CommandSender teleporter, List<Entity> teleportees, String deststring) {
return destinationsProvider.suggestDestinations(teleporter, deststring).stream()
.filter(packet -> !config.getSimplifiedDestinationTabCompletion()
|| packet.destination() instanceof WorldDestination
|| deststring.startsWith(packet.destination().getIdentifier() + ":"))
.filter(packet -> corePermissionsChecker
.checkDestinationPacketPermission(teleporter, Arrays.asList(players), packet))
.checkDestinationPacketPermission(teleporter, teleportees, packet))
.map(DestinationSuggestionPacket::parsableString)
.toList();
}
@@ -278,19 +284,16 @@ public class MVCommandCompletions extends PaperCommandCompletions {
case "both" -> {
return worldManager.getWorlds().stream()
.map(MultiverseWorld::getTabCompleteName)
.map(StringFormatter::quoteMultiWordString)
.toList();
}
case "loaded" -> {
return worldManager.getLoadedWorlds().stream()
.map(MultiverseWorld::getTabCompleteName)
.map(StringFormatter::quoteMultiWordString)
.toList();
}
case "unloaded" -> {
return worldManager.getUnloadedWorlds().stream()
.map(MultiverseWorld::getTabCompleteName)
.map(StringFormatter::quoteMultiWordString)
.toList();
}
case "potential" -> {
@@ -11,6 +11,7 @@ import co.aikar.commands.MinecraftMessageKeys;
import co.aikar.commands.PaperCommandContexts;
import co.aikar.commands.contexts.ContextResolver;
import com.google.common.base.Strings;
import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
@@ -116,7 +117,8 @@ public class MVCommandContexts extends PaperCommandContexts {
}
return destinationsProvider.parseDestination(context.getSender(), destination)
.getOrThrow(failure -> MVInvalidCommandArgument.of(failure.getFailureMessage()));
.getOrThrow(failure ->
new InvalidCommandArgument(failure.getFailureMessage().formatted(context.getIssuer())));
}
private GameRule<?> parseGameRule(BukkitCommandExecutionContext context) {
@@ -271,13 +273,25 @@ public class MVCommandContexts extends PaperCommandContexts {
private IssuerAwareContextBuilder<Player[]> playerArrayContextBuilder() {
return new IssuerAwareContextBuilder<Player[]>()
.fromPlayer((context, player) -> new Player[]{player})
.fromInput((context, input) -> {
Player[] players = PlayerFinder.getMulti(input, context.getSender()).toArray(new Player[0]);
return (players.length == 0) ? null : players;
})
.fromInput((context, input) -> PlayerFinder
.tryGetMulti(input, context.getSender())
.map(list -> list.toArray(new Player[0]))
.map(arr -> (arr.length == 0) ? null : arr)
.getOrElseThrow(failure -> {
if (failure instanceof MVInvalidCommandArgument mvFailure) {
throw mvFailure;
}
throw new InvalidCommandArgument(failure.getLocalizedMessage() + " "
+ Option.of(failure.getCause()).map(Throwable::getLocalizedMessage).getOrElse(""));
}))
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player."))
.issuerAwareInputFailMessage((context, input) -> Message.of("Invalid player: " + input + ". Either specify an online player or use this command as a player."))
.inputOnlyFailMessage((context, input) -> Message.of("Player " + input + " not found."));
.inputOnlyFailMessage((context, input) -> {
if (PlayerFinder.isSelector(input)) {
return Message.of("No player(s) matched selector: " + input + ".");
}
return Message.of("Player(s) " + input + " not found.");
});
}
private PlayerLocation parsePlayerLocation(BukkitCommandExecutionContext context) {
@@ -112,9 +112,9 @@ public class MVCommandIssuer extends BukkitCommandIssuer {
} else {
var formatter = getManager().getFormat(messageType);
if (formatter != null) {
sendMessage(formatter.format(message.formatted()));
sendMessage(formatter.format(message.formatted(getManager().getLocales(), this)));
} else {
sendMessage(message.formatted());
sendMessage(message.formatted(getManager().getLocales(), this));
}
}
}
@@ -22,6 +22,9 @@ public class MVRootCommand extends BukkitRootCommand {
@Override
public List<String> getTabCompletions(CommandIssuer sender, String alias, String[] args, boolean commandsOnly, boolean isAsync) {
String[] quoteFormatedArgs = StringFormatter.parseQuotesInArgs(args).toArray(String[]::new);
return super.getTabCompletions(sender, alias, quoteFormatedArgs, commandsOnly, isAsync);
return super.getTabCompletions(sender, alias, quoteFormatedArgs, commandsOnly, isAsync)
.stream()
.map(StringFormatter::quoteMultiWordString)
.toList();
}
}
@@ -123,48 +123,8 @@ public final class IssuerAwareContextBuilder<T> {
*/
@ApiStatus.AvailableSince("5.1")
public IssuerAwareContextResolver<T, BukkitCommandExecutionContext> generateContext() {
Objects.requireNonNull(fromPlayer);
Objects.requireNonNull(fromInput);
Objects.requireNonNull(issuerOnlyFailMessage);
Objects.requireNonNull(issuerAwarePlayerFailMessage);
Objects.requireNonNull(issuerAwareInputFailMessage);
Objects.requireNonNull(inputOnlyFailMessage);
return context -> {
BukkitCommandIssuer issuer = context.getIssuer();
String resolve = context.getFlagValue("resolve", "");
if (resolve.equals("issuerOnly")) {
if (issuer.isPlayer()) {
T result = fromPlayer.apply(context, issuer.getPlayer());
if (result != null) {
return result;
}
}
throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer));
}
String input = context.getFirstArg();
T result = fromInput.apply(context, input);
if (result != null) {
context.popFirstArg();
return result;
}
if (resolve.equals("issuerAware")) {
if (issuer.isPlayer()) {
Player player = issuer.getPlayer();
result = fromPlayer.apply(context, player);
if (result != null) {
return result;
}
throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer));
}
throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
};
validateRequiredVariables();
return context -> resolveValue(context, GenericIssuerAwareValue::new).value;
}
/**
@@ -178,49 +138,66 @@ public final class IssuerAwareContextBuilder<T> {
*/
@ApiStatus.AvailableSince("5.1")
public <I extends IssuerAwareValue> IssuerAwareContextResolver<I, BukkitCommandExecutionContext> generateContext(BiFunction<Boolean, T, I> createValue) {
// todo: This is a copy and paste from above
validateRequiredVariables();
return context -> resolveValue(context, createValue);
}
private void validateRequiredVariables() {
Objects.requireNonNull(fromPlayer);
Objects.requireNonNull(fromInput);
Objects.requireNonNull(issuerOnlyFailMessage);
Objects.requireNonNull(issuerAwarePlayerFailMessage);
Objects.requireNonNull(issuerAwareInputFailMessage);
Objects.requireNonNull(inputOnlyFailMessage);
}
return context -> {
BukkitCommandIssuer issuer = context.getIssuer();
String resolve = context.getFlagValue("resolve", "");
private <I extends IssuerAwareValue> I resolveValue(BukkitCommandExecutionContext context, BiFunction<Boolean, T, I> createValue) {
BukkitCommandIssuer issuer = context.getIssuer();
String resolve = context.getFlagValue("resolve", "");
if (resolve.equals("issuerOnly")) {
if (issuer.isPlayer()) {
T result = fromPlayer.apply(context, issuer.getPlayer());
if (result != null) {
return createValue.apply(true, result);
}
if (resolve.equals("issuerOnly")) {
if (issuer.isPlayer()) {
T result = fromPlayer.apply(context, issuer.getPlayer());
if (result != null) {
return createValue.apply(true, result);
}
throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer));
}
throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer));
}
String input = context.getFirstArg();
T result = fromInput.apply(context, input);
if (result != null) {
context.popFirstArg();
return createValue.apply(false, result);
}
String input = context.getFirstArg();
T result = fromInput.apply(context, input);
if (result != null) {
context.popFirstArg();
return createValue.apply(false, result);
}
if (resolve.equals("issuerAware")) {
if (issuer.isPlayer()) {
Player player = issuer.getPlayer();
result = fromPlayer.apply(context, player);
if (result != null) {
return createValue.apply(true, result);
}
throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
int maxArgForAware = context.getFlagValue("maxArgForAware", Integer.MAX_VALUE);
long argLengthWithoutFlags = context.getArgs().stream()
.takeWhile(value -> !value.startsWith("-") && !value.isEmpty()) // ignore the flags
.count();
if (resolve.equals("issuerAware") && argLengthWithoutFlags <= maxArgForAware) {
if (issuer.isPlayer()) {
Player player = issuer.getPlayer();
result = fromPlayer.apply(context, player);
if (result != null) {
return createValue.apply(true, result);
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer));
throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer));
}
throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
};
throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
}
private static class GenericIssuerAwareValue<T> extends IssuerAwareValue {
private final T value;
public GenericIssuerAwareValue(boolean byIssuer, T value) {
super(byIssuer);
this.value = value;
}
}
}
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.command.flag;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import co.aikar.commands.InvalidCommandArgument;
@@ -220,7 +221,7 @@ public class CommandValueFlag<T> extends CommandFlag {
private void setEnumContext() {
this.context = (String value) -> {
try {
return Enum.valueOf(type, value.toUpperCase());
return Enum.valueOf(type, value.toUpperCase(Locale.ENGLISH));
} catch (IllegalArgumentException e) {
throw new InvalidCommandArgument("Invalid value for argument " + key + ": " + value);
}
@@ -229,7 +230,7 @@ public class CommandValueFlag<T> extends CommandFlag {
private void setEnumCompletion() {
List<String> types = Arrays.stream(type.getEnumConstants())
.map(typeClass -> typeClass.name().toLowerCase())
.map(typeClass -> typeClass.name().toLowerCase(Locale.ENGLISH))
.toList();
this.completion = (input) -> types;
@@ -67,12 +67,10 @@ class ConfigCommand extends CoreCommand {
private void updateConfigValue(MVCommandIssuer issuer, String name, String value) {
var stringPropertyHandle = config.getStringPropertyHandle();
stringPropertyHandle.setPropertyString(issuer.getIssuer(), name, value)
.onSuccess(ignore -> {
config.save();
issuer.sendMessage(MVCorei18n.CONFIG_SET_SUCCESS,
Replace.NAME.with(name),
Replace.VALUE.with(stringPropertyHandle.getProperty(name).getOrNull()));
})
.andThenTry(config::save)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.CONFIG_SET_SUCCESS,
Replace.NAME.with(name),
Replace.VALUE.with(stringPropertyHandle.getProperty(name).getOrNull())))
.onFailure(e -> issuer.sendMessage(MVCorei18n.CONFIG_SET_ERROR,
Replace.NAME.with(name),
Replace.VALUE.with(value),
@@ -5,6 +5,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
@@ -136,6 +137,9 @@ final class DumpsService {
}
private String getPluginList() {
return " - " + StringUtils.join(plugin.getServer().getPluginManager().getPlugins(), "\n - ");
return " - " + StringUtils.join(Arrays.stream(plugin.getServer().getPluginManager().getPlugins())
.map(Object::toString)
.sorted()
.toList(), "\n - ");
}
}
@@ -52,7 +52,7 @@ final class EntitySpawnConfigCommand extends CoreCommand {
void onInfoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
MultiverseWorld world,
@@ -91,7 +91,7 @@ final class EntitySpawnConfigCommand extends CoreCommand {
void onModifyCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=4")
@Syntax("[world]")
MultiverseWorld world,
@@ -113,11 +113,10 @@ final class EntitySpawnConfigCommand extends CoreCommand {
.getSpawnCategoryConfig(spawnCategory)
.getStringPropertyHandle()
.modifyPropertyString(property, value, action)
.onSuccess(ignore -> issuer.sendMessage("Successfully set " + property + " to " + value
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendInfo("Successfully set " + property + " to " + value
+ " for " + spawnCategory.name() + " in " + world.getName()))
.onFailure(e -> issuer.sendMessage("Unable to set " + property + " to " + value
.onFailure(e -> issuer.sendError("Unable to set " + property + " to " + value
+ " for " + spawnCategory.name() + " in " + world.getName() + ": " + e.getMessage()));
worldManager.saveWorldsConfig();
}
}
@@ -67,7 +67,7 @@ class GameruleCommand extends CoreCommand {
@Description("{@@mv-core.gamerule.set.value.description}")
GameRuleValue gameRuleValue,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=2")
@Syntax("[World or *]")
@Description("{@@mv-core.gamerule.set.world.description}")
LoadedMultiverseWorld[] worlds) {
@@ -114,7 +114,7 @@ class GameruleCommand extends CoreCommand {
@Description("{@@mv-core.gamerule.reset.gamerule.description}")
GameRule gamerule,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[World or *]")
@Description("{@@mv-core.gamerule.reset.world.description}")
LoadedMultiverseWorld[] worlds) {
@@ -152,7 +152,7 @@ class GameruleCommand extends CoreCommand {
void onGameruleListCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("<world>")
@Description("{@@mv-core.gamerule.list.description.world}")
LoadedMultiverseWorld world,
@@ -61,8 +61,8 @@ class InfoCommand extends CoreCommand {
public void onInfoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Syntax("<world>")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
@Description("{@@mv-core.info.description.world}")
LoadedMultiverseWorld world,
@@ -49,12 +49,12 @@ class ModifyCommand extends CoreCommand {
"@propsmodifyaction:notByIssuerForArg=arg1|@mvworldpropsname:byIssuerForArg=arg1 " +
"@mvworldpropsname:notByIssuerForArg=arg1|@mvworldpropsvalue:byIssuerForArg=arg1 " +
"@mvworldpropsvalue:notByIssuerForArg=arg1")
@Syntax("[world] <set|add|remove|reset> <property> <value>")
@Syntax("[world] <set|add|remove|reset> <property> [value]")
@Description("{@@mv-core.modify.description}")
void onModifyCommand(// SUPPRESS CHECKSTYLE: ParameterNumber
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=3")
@Syntax("[world]")
@Description("{@@mv-core.modify.world.description}")
@NotNull MultiverseWorldValue worldValue,
@@ -94,28 +94,28 @@ class ModifyCommand extends CoreCommand {
}
StringPropertyHandle worldPropertyHandle = world.getStringPropertyHandle();
worldPropertyHandle.modifyPropertyString(issuer.getIssuer(), propertyName, propertyValue, action).onSuccess(ignore -> {
issuer.sendMessage(MVCorei18n.MODIFY_SUCCESS,
worldPropertyHandle.modifyPropertyString(issuer.getIssuer(), propertyName, propertyValue, action)
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.MODIFY_SUCCESS,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(worldPropertyHandle.getProperty(propertyName).getOrNull()),
Replace.WORLD.with(world.getName()));
worldManager.saveWorldsConfig();
}).onFailure(exception -> {
if (propertyValue == null) {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE_NOVALUE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
} else {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(propertyValue),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
}
Replace.WORLD.with(world.getName())))
.onFailure(exception -> {
if (propertyValue == null) {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE_NOVALUE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
} else {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(propertyValue),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
}
});
}
@@ -33,7 +33,7 @@ final class PurgeAllEntitiesCommand extends CoreCommand {
void onPurgeAllEntitiesCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[world]")
LoadedMultiverseWorld world,
@@ -28,7 +28,7 @@ final class PurgeEntitiesCommand extends CoreCommand {
void onPurgeEntityCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -63,7 +63,7 @@ class SetSpawnCommand extends CoreCommand {
worldManager.getLoadedWorld(location.getWorld())
.peek(mvWorld -> mvWorld.setSpawnLocation(location)
.flatMap(ignore -> worldManager.saveWorldsConfig())
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.SETSPAWN_SUCCESS,
Replace.WORLD.with(mvWorld.getName()),
Replace.LOCATION.with(prettyLocation(location))))
@@ -119,7 +119,7 @@ final class SpawnCommand extends CoreCommand {
? Message.of(MVCorei18n.GENERIC_YOU)
: Message.of(entity.getName())),
Replace.WORLD.with(mvWorld.getName()),
Replace.REASON.with(failure.getFirst().getFailureMessage())));
Replace.REASON.with(failure.get(0).getFailureMessage())));
}
private void handleMultiTeleport(MVCommandIssuer issuer, LoadedMultiverseWorld mvWorld,
@@ -19,7 +19,10 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.context.issueraware.PlayerArrayValue;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.PageFilterFlags;
import org.mvplugins.multiverse.core.command.flags.UnsafeFlags;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
@@ -35,14 +38,14 @@ final class TeleportCommand extends CoreCommand {
private final CoreConfig config;
private final CorePermissionsChecker permissionsChecker;
private final AsyncSafetyTeleporter safetyTeleporter;
private final UnsafeFlags flags;
private final Flags flags;
@Inject
TeleportCommand(
@NotNull CoreConfig config,
@NotNull CorePermissionsChecker permissionsChecker,
@NotNull AsyncSafetyTeleporter safetyTeleporter,
@NotNull UnsafeFlags flags
@NotNull Flags flags
) {
this.config = config;
this.permissionsChecker = permissionsChecker;
@@ -54,14 +57,14 @@ final class TeleportCommand extends CoreCommand {
@Subcommand("teleport|tp")
@CommandPermission("@mvteleport")
@CommandCompletion("@playersarray:checkPermissions=@mvteleportother|@destinations:byIssuerForArg=arg1 "
+ "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + UnsafeFlags.NAME + " "
+ "@flags:notByIssuerForArg=arg1,groupName=" + UnsafeFlags.NAME)
+ "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + Flags.NAME + " "
+ "@flags:notByIssuerForArg=arg1,groupName=" + Flags.NAME)
@Syntax("[player] <destination> [--unsafe]")
@Description("{@@mv-core.teleport.description}")
void onTeleportCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@co.aikar.commands.annotation.Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[player]")
@Description("{@@mv-core.teleport.player.description}")
PlayerArrayValue playersValue,
@@ -103,16 +106,21 @@ final class TeleportCommand extends CoreCommand {
.checkSafety(!parsedFlags.hasFlag(flags.unsafe) && destination.checkTeleportSafety())
.passengerMode(config.getPassengerMode())
.teleportSingle(player)
.onSuccess(() -> issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(getYouOrName(issuer, player)),
Replace.DESTINATION.with(destination.toString())))
.onSuccess(() -> {
if (parsedFlags.hasFlag(flags.silent)) {
return;
}
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(getYouOrName(issuer, player)),
Replace.DESTINATION.with(destination.getDisplayMessage()));
})
.onFailureCount(reasonsCountMap -> {
for (var entry : reasonsCountMap.entrySet()) {
Logging.finer("Failed to teleport %s players to %s: %s",
entry.getValue(), destination, entry.getKey());
issuer.sendError(MVCorei18n.TELEPORT_FAILED,
Replace.PLAYER.with(player.getName()),
Replace.DESTINATION.with(destination.toString()),
Replace.DESTINATION.with(destination.getDisplayMessage()),
Replace.REASON.with(Message.of(entry.getKey())));
}
});
@@ -136,18 +144,38 @@ final class TeleportCommand extends CoreCommand {
.checkSafety(!parsedFlags.hasFlag(flags.unsafe) && destination.checkTeleportSafety())
.passengerMode(config.getPassengerMode())
.teleport(List.of(players))
.onSuccessCount(successCount -> issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(successCount + " players"),
Replace.DESTINATION.with(destination.toString())))
.onSuccessCount(successCount -> {
if (parsedFlags.hasFlag(flags.silent)) {
return;
}
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(successCount + " players"),
Replace.DESTINATION.with(destination.getDisplayMessage()));
})
.onFailureCount(reasonsCountMap -> {
for (var entry : reasonsCountMap.entrySet()) {
Logging.finer("Failed to teleport %s players to %s: %s",
entry.getValue(), destination, entry.getKey());
issuer.sendError(MVCorei18n.TELEPORT_FAILED,
Replace.PLAYER.with(entry.getValue() + " players"),
Replace.DESTINATION.with(destination.toString()),
Replace.DESTINATION.with(destination.getDisplayMessage()),
Replace.REASON.with(Message.of(entry.getKey())));
}
});
}
@Service
private static final class Flags extends UnsafeFlags {
private static final String NAME = "mvteleport";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}
private final CommandFlag silent = flag(CommandFlag.builder("--silent")
.addAlias("-s")
.build());
}
}
@@ -74,12 +74,12 @@ class WhoCommand extends CoreCommand {
@Subcommand("who")
@CommandPermission("multiverse.core.list.who")
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + PageFilterFlags.NAME)
@Syntax("<world> [--page <page>] [--filter <filter>]")
@Syntax("[world] [--page <page>] [--filter <filter>]")
@Description("{@@mv-core.who.description}")
void onWhoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Syntax("<world>")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
@Description("{@@mv-core.who.world.description}")
LoadedMultiverseWorld inputtedWorld,
@@ -35,7 +35,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("[time]")
int time,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -54,7 +54,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("[z]")
double z,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -79,7 +79,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<damage>")
double damage,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -103,7 +103,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<distance>")
double distance,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -124,7 +124,7 @@ final class WorldBorderCommand extends CoreCommand {
void onWorldBorderGet(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -147,7 +147,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("[time]")
int time,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -179,7 +179,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<distance>")
int distance,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -203,7 +203,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<time>")
int time,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -8,16 +8,13 @@ import java.util.Locale;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.event.EventPriority;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.queue.ConfirmMode;
import org.mvplugins.multiverse.core.config.handle.CommentedConfigurationHandle;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
@@ -28,7 +25,6 @@ import org.mvplugins.multiverse.core.config.migration.action.InvertBoolMigratorA
import org.mvplugins.multiverse.core.config.migration.action.MoveMigratorAction;
import org.mvplugins.multiverse.core.config.migration.VersionMigrator;
import org.mvplugins.multiverse.core.config.migration.action.SetMigratorAction;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.teleportation.PassengerMode;
import org.mvplugins.multiverse.core.teleportation.PassengerModes;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat;
@@ -190,6 +186,26 @@ public final class CoreConfig {
return configHandle.get(configNodes.enforceFlight);
}
@ApiStatus.AvailableSince("5.3")
public Try<Void> setApplyEntitySpawnRate(boolean applyEntitySpawnRate) {
return configHandle.set(configNodes.applyEntitySpawnRate, applyEntitySpawnRate);
}
@ApiStatus.AvailableSince("5.3")
public boolean getApplyEntitySpawnRate() {
return configHandle.get(configNodes.applyEntitySpawnRate);
}
@ApiStatus.AvailableSince("5.3")
public Try<Void> setApplyEntitySpawnLimit(boolean applyEntitySpawnLimit) {
return configHandle.set(configNodes.applyEntitySpawnLimit, applyEntitySpawnLimit);
}
@ApiStatus.AvailableSince("5.3")
public boolean getApplyEntitySpawnLimit() {
return configHandle.get(configNodes.applyEntitySpawnLimit);
}
/**
* {@inheritDoc}
*/
@@ -519,6 +535,16 @@ public final class CoreConfig {
return configHandle.get(configNodes.resolveAliasName);
}
@ApiStatus.AvailableSince("5.4")
public Try<Void> setSimplifiedDestinationTabCompletion(boolean simplifiedDestinationTabCompletion) {
return configHandle.set(configNodes.simplifiedDestinationTabCompletion, simplifiedDestinationTabCompletion);
}
@ApiStatus.AvailableSince("5.4")
public boolean getSimplifiedDestinationTabCompletion() {
return configHandle.get(configNodes.simplifiedDestinationTabCompletion);
}
/**
* {@inheritDoc}
*/
@@ -603,6 +629,26 @@ public final class CoreConfig {
return configHandle.get(configNodes.bukkitYmlPath);
}
@ApiStatus.AvailableSince("5.3")
public Try<Void> setServerPropertiesPath(String serverPropertiesPath) {
return configHandle.set(configNodes.serverPropertiesPath, serverPropertiesPath);
}
@ApiStatus.AvailableSince("5.3")
public String getServerPropertiesPath() {
return configHandle.get(configNodes.serverPropertiesPath);
}
@ApiStatus.AvailableSince("5.3")
public Try<Void> setAutoDetectGeneratorPlugins(boolean autoDetectGeneratorPlugins) {
return configHandle.set(configNodes.autoDetectGeneratorPlugins, autoDetectGeneratorPlugins);
}
@ApiStatus.AvailableSince("5.3")
public boolean getAutoDetectGeneratorPlugins() {
return configHandle.get(configNodes.autoDetectGeneratorPlugins);
}
/**
* {@inheritDoc}
*/
@@ -4,6 +4,7 @@ import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.event.EventPriority;
import org.bukkit.plugin.PluginManager;
@@ -76,13 +77,10 @@ final class CoreConfigNodes {
.comment("# █░▀░█ █▄█ █▄▄ ░█░ █ ▀▄▀ ██▄ █▀▄ ▄█ ██▄   █▄▄ █▄█ █▀▄ ██▄ #")
.comment("# #")
.comment("# #")
.comment("# WIKI: https://github.com/Multiverse/Multiverse-Core/wiki #")
.comment("# WIKI: https://mvplugins.org/core/ #")
.comment("# DISCORD: https://discord.gg/NZtfKky #")
.comment("# BUG REPORTS: https://github.com/Multiverse/Multiverse-Core/issues #")
.comment("# #")
.comment("# #")
.comment("# Each option in this file is documented and explained here: #")
.comment("# ==> https://github.com/Multiverse/Multiverse-Core/wiki/config.yml #")
.comment("# DONATE: https://github.com/sponsors/Multiverse #")
.comment("# #")
.comment("# #")
.comment("# New options are added to this file automatically. If you manually made changes #")
@@ -137,6 +135,32 @@ final class CoreConfigNodes {
.name("enforce-flight")
.build());
final ConfigNode<Boolean> applyEntitySpawnRate = node(ConfigNode.builder("world.apply-entity-spawn-rate", Boolean.class)
.comment("")
.comment("Sets whether Multiverse will apply the world's entity `tick-rate` config in worlds.yml.")
.comment("If disabled, the `tick-rate` config in worlds.yml will be ignored.")
.comment("Disable this if you want paper-world.yml or another plugin to handle entity spawn rate per world.")
.comment("!!!NOTE: This will only apply after a server restart!")
.defaultValue(true)
.name("apply-entity-spawn-rate")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'apply-entity-spawn-rate' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<Boolean> applyEntitySpawnLimit = node(ConfigNode.builder("world.apply-entity-spawn-limit", Boolean.class)
.comment("")
.comment("Sets whether Multiverse will apply the world's entity `spawn-limit` config when a world is loaded.")
.comment("If disabled, the `spawn-limit` config in worlds.yml will be ignored.")
.comment("Disable this if you want paper-world.yml or another plugin to handle entity limits per world.")
.comment("!!!NOTE: This will only apply after a server restart!")
.defaultValue(true)
.name("apply-entity-spawn-limit")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'apply-entity-spawn-limit' config option will only take" +
" full effect after a server restart.")))
.build());
final ConfigNode<Boolean> autoPurgeEntities = node(ConfigNode.builder("world.auto-purge-entities", Boolean.class)
.comment("")
.comment("Sets whether Multiverse will purge entities on world load based world's entity spawn config.")
@@ -187,7 +211,8 @@ final class CoreConfigNodes {
final ConfigNode<PassengerModes> passengerMode = node(ConfigNode.builder("teleport.passenger-mode", PassengerModes.class)
.comment("")
.comment("Configures how passengers and vehicles are handled when an entity is teleported.")
.comment(" default: Server will handle passengers and vehicles, this usually means entities will not be teleported to a different world if they have passengers.")
.comment(" default: Server will handle passengers and vehicles. On papermc 1.21.10+, all will be retained by default.")
.comment(" On older versions, all will be dismounted if world changes.")
.comment(" dismount_passengers: Passengers will be removed from the parent entity before the teleport.")
.comment(" dismount_vehicle: Vehicle will be removed and from the parent entity before the teleport.")
.comment(" dismount_all: All passengers and vehicles will be removed from the parent entity before the teleport.")
@@ -370,7 +395,7 @@ final class CoreConfigNodes {
.comment("This config option defines the default language Multiverse should use.")
.defaultValue(Locale.ENGLISH)
.name("default-locale")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
commandManager.get().getLocales().setDefaultLocale(newValue))
.build());
@@ -380,9 +405,8 @@ final class CoreConfigNodes {
.comment("If the player's language does not have a translation, it will use the default language set above instead.")
.defaultValue(true)
.name("per-player-locale")
.onSetValue((oldValue, newValue) -> {
commandManager.get().usePerIssuerLocale(newValue);
})
.onLoadAndChange((oldValue, newValue) ->
commandManager.get().usePerIssuerLocale(newValue))
.build());
private final ConfigHeaderNode commandHeader = node(ConfigHeaderNode.builder("command")
@@ -398,6 +422,15 @@ final class CoreConfigNodes {
.name("resolve-alias-name")
.build());
final ConfigNode<Boolean> simplifiedDestinationTabCompletion = node(ConfigNode.builder("command.simplified-destination-tab-completion", Boolean.class)
.comment("")
.comment("If this is set to true, Multiverse will only suggest simple destination formats in tab completion.")
.comment("This means only world names will be suggested without other destination type such as `e:worldname:x,y,z` or `p:playername`.")
.comment("Note: This DOES NOT prevent players from using the destinations, as that is controlled by permissions.")
.defaultValue(false)
.name("simplified-destination-tab-completion")
.build());
final ConfigNode<ConfirmMode> confirmMode = node(ConfigNode.builder("command.confirm-mode", ConfirmMode.class)
.comment("")
.comment("This config option defines whether `/mv confirm` is needed before running a DANGEROUS action.")
@@ -452,8 +485,11 @@ final class CoreConfigNodes {
.comment("")
.comment("This config option defines the priority for the PlayerPortalEvent.")
.name("event-priority-player-portal")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-portal", newValue))
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-portal' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerRespawn = node(ConfigNode.builder("event-priority.player-respawn", EventPriority.class)
@@ -461,16 +497,23 @@ final class CoreConfigNodes {
.name("event-priority-player-respawn")
.comment("")
.comment("This config option defines the priority for the PlayerRespawnEvent.")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-respawn", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-respawn' config option will only take " +
"full effect after a server restart."))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerSpawnLocation = node(ConfigNode.builder("event-priority.player-spawn-location", EventPriority.class)
.defaultValue(EventPriority.NORMAL)
.comment("")
.comment("This config option defines the priority for the PlayerSpawnLocationEvent.")
.name("event-priority-player-spawn-location").onSetValue((oldValue, newValue) ->
.name("event-priority-player-spawn-location")
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-spawn-location", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-spawn-location' config option will only take " +
"full effect after a server restart."))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerTeleport = node(ConfigNode.builder("event-priority.player-teleport", EventPriority.class)
@@ -478,8 +521,11 @@ final class CoreConfigNodes {
.name("event-priority-player-teleport")
.comment("")
.comment("This config option defines the priority for the PlayerTeleportEvent.")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-teleport", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-teleport' config option will only take " +
"full effect after a server restart."))
.build());
private final ConfigHeaderNode miscHeader = node(ConfigHeaderNode.builder("misc")
@@ -492,6 +538,29 @@ final class CoreConfigNodes {
.comment("Note: this config option needs a server restart to take effect.")
.defaultValue("bukkit.yml")
.name("bukkit-yml-path")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'bukkit-yml-path' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<String> serverPropertiesPath = node(ConfigNode.builder("misc.server-properties-path", String.class)
.comment("")
.comment("Change this if you use a custom path for the server.properties file with `--config` startup flag.")
.comment("Note: this config option needs a server restart to take effect.")
.defaultValue("server.properties")
.name("server-properties-path")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'server-properties-path' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<Boolean> autoDetectGeneratorPlugins = node(ConfigNode.builder("misc.auto-detect-generator-plugins", Boolean.class)
.comment("")
.comment("When enabled, Multiverse will attempt to automatically detect world generator plugins installed on your server.")
.comment("This option only affects tab-completion within `/mv create` command and output of `/mv generators` command.")
.comment("Disabling this will not have any affect on the usages of world generator plugins itself.")
.defaultValue(true)
.name("auto-detect-generator-plugins")
.build());
final ConfigNode<Integer> globalDebug = node(ConfigNode.builder("misc.global-debug", Integer.class)
@@ -509,7 +578,7 @@ final class CoreConfigNodes {
.validator(value -> (value < 0 || value > 3)
? Try.failure(new MultiverseException("Debug level must be between 0 and 3."))
: Try.success(null))
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (newValue != Logging.getDebugLevel()) {
Logging.setDebugLevel(newValue);
pluginManager.callEvent(new MVDebugModeEvent(newValue));
@@ -523,7 +592,7 @@ final class CoreConfigNodes {
.comment("This will only work if the above 'global-debug' is set to 1 or more.")
.defaultValue(false)
.name("debug-permissions")
.onSetValue((oldValue, newValue) -> PermissionUtils.setDebugPermissions(newValue))
.onLoadAndChange((oldValue, newValue) -> PermissionUtils.setDebugPermissions(newValue))
.build());
final ConfigNode<Boolean> silentStart = node(ConfigNode.builder("misc.silent-start", Boolean.class)
@@ -531,12 +600,13 @@ final class CoreConfigNodes {
.comment("If true, the startup console messages will no longer show.")
.defaultValue(false)
.name("silent-start")
.onSetValue((oldValue, newValue) -> Logging.setShowingConfig(!newValue))
.onLoadAndChange((oldValue, newValue) -> Logging.setShowingConfig(!newValue))
.build());
final ConfigNode<Boolean> showDonationMessage = node(ConfigNode.builder("misc.show-donation-message", Boolean.class)
.comment("")
.comment("If you don't want to donate, you can set this to false and Multiverse will stop nagging you.")
.comment("But if you want to donate, here is our Github Sponsors page: https://github.com/sponsors/Multiverse")
.defaultValue(true)
.name("show-donation-message")
.build());
@@ -3,19 +3,22 @@ package org.mvplugins.multiverse.core.config.handle;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Logger;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.migration.ConfigMigrator;
import org.mvplugins.multiverse.core.config.node.ListValueNode;
import org.mvplugins.multiverse.core.config.node.Node;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.config.node.ValueNode;
@@ -84,7 +87,8 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
});
nodeValueMap.forEach((valueNode, value) -> {
valueNode.onSetValue(value, value);
valueNode.onLoad(value);
valueNode.onLoadAndChange(Bukkit.getConsoleSender(), null, value);
});
}
@@ -156,10 +160,27 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
* @return Empty try if the value was set, try containing an error otherwise.
*/
public <T> Try<Void> set(@NotNull ValueNode<T> node, T value) {
return set(Bukkit.getConsoleSender(), node, value);
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param sender The sender who triggered the change.
* @param node The node to set the value of.
* @param value The value to set.
* @param <T> The type of the node value.
* @return Empty try if the value was set, try containing an error otherwise.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public <T> Try<Void> set(@NotNull CommandSender sender, @NotNull ValueNode<T> node, T value) {
return node.validate(value).map(ignore -> {
T oldValue = get(node);
nodeValueMap.put(node, value);
node.onSetValue(oldValue, get(node));
node.onLoadAndChange(sender, oldValue, value);
node.onChange(sender, oldValue, value);
return null;
});
}
@@ -226,7 +226,7 @@ public class StringPropertyHandle {
public Try<Void> setPropertyString(@NotNull CommandSender sender, @Nullable String name, @Nullable String value) {
return findNode(name, ValueNode.class)
.flatMap(node -> node.parseFromString(sender, value)
.flatMap(parsedValue -> handle.set(node, parsedValue)));
.flatMap(parsedValue -> handle.set(sender, node, parsedValue)));
}
/**
@@ -14,6 +14,9 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.node.functions.NodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeValueCallback;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeStringParser;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeSuggester;
import org.mvplugins.multiverse.core.config.node.serializer.DefaultSerializerProvider;
@@ -53,7 +56,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected @Nullable NodeStringParser<T> stringParser;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
protected @Nullable NodeValueCallback<T> onLoad;
protected @Nullable NodeChangeCallback<T> onLoadAndChange;
protected @Nullable NodeChangeCallback<T> onChange;
protected ConfigNode(
@NotNull String path,
@@ -66,7 +71,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@Nullable NodeStringParser<T> stringParser,
@Nullable NodeSerializer<T> serializer,
@Nullable Function<T, Try<Void>> validator,
@Nullable BiConsumer<T, T> onSetValue) {
@Nullable NodeValueCallback<T> onLoad,
@Nullable NodeChangeCallback<T> onLoadAndChange,
@Nullable NodeChangeCallback<T> onChange) {
super(path, comments);
this.name = name;
this.type = type;
@@ -82,7 +89,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
? serializer
: DefaultSerializerProvider.getDefaultSerializer(type);
this.validator = validator;
this.onSetValue = onSetValue;
this.onLoad = onLoad;
this.onLoadAndChange = onLoadAndChange;
this.onChange = onChange;
}
/**
@@ -186,9 +195,29 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
* {@inheritDoc}
*/
@Override
public void onSetValue(@Nullable T oldValue, @Nullable T newValue) {
if (onSetValue != null) {
onSetValue.accept(oldValue, newValue);
public void onLoad(@Nullable T value) {
if (onLoad != null) {
onLoad.run(value);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLoadAndChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue) {
if (onLoadAndChange != null) {
onLoadAndChange.run(sender, oldValue, newValue);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue) {
if (onChange != null) {
onChange.run(sender, oldValue, newValue);
}
}
@@ -208,7 +237,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected @Nullable NodeStringParser<T> stringParser;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
protected @Nullable NodeValueCallback<T> onLoad;
protected @Nullable NodeChangeCallback<T> onLoadAndChange;
protected @Nullable NodeChangeCallback<T> onChange;
/**
* Creates a new builder.
@@ -368,15 +399,83 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return self();
}
/**
* Sets the action to be performed when the value is loaded.
*
* @param onLoad The action to be performed.
* @return This builder.
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onLoad(@NotNull NodeValueCallback<T> onLoad) {
this.onLoad = this.onLoad == null ? onLoad : this.onLoad.then(onLoad);
return self();
}
/**
* Sets the action to be performed when the value is loaded and changed.
*
* @param onLoadAndChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onLoadAndChange(@NotNull NodeChangeCallback<T> onLoadAndChange) {
this.onLoadAndChange = this.onLoadAndChange == null ? onLoadAndChange : this.onLoadAndChange.then(onLoadAndChange);
return self();
}
/**
* Sets the action to be performed when the value is loaded and changed.
*
* @param onLoadAndChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onLoadAndChange(@NotNull SenderNodeChangeCallback<T> onLoadAndChange) {
return onLoadAndChange((NodeChangeCallback<T>) onLoadAndChange);
}
/**
* Sets the action to be performed when the value is changed.
*
* @param onChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onChange(@NotNull NodeChangeCallback<T> onChange) {
this.onChange = this.onChange == null ? onChange : this.onChange.then(onChange);
return self();
}
/**
* Sets the action to be performed when the value is changed.
*
* @param onChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onChange(@NotNull SenderNodeChangeCallback<T> onChange) {
return onChange((NodeChangeCallback<T>) onChange);
}
/**
* Sets the action to be performed when the value is set.
*
* @param onSetValue The action to be performed.
* @return This builder.
*
* @deprecated Use {@link #onLoadAndChange(NodeChangeCallback)} instead.
*/
@Deprecated(since = "5.4", forRemoval = true)
public @NotNull B onSetValue(@NotNull BiConsumer<T, T> onSetValue) {
this.onSetValue = this.onSetValue == null ? onSetValue : this.onSetValue.andThen(onSetValue);
return self();
return onLoadAndChange(onSetValue::accept);
}
/**
@@ -385,7 +484,8 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@Override
public @NotNull ConfigNode<T> build() {
return new ConfigNode<>(path, comments.toArray(new String[0]),
name, type, aliases, defaultValue, suggester, stringParser, serializer, validator, onSetValue);
name, type, aliases, defaultValue, suggester, stringParser, serializer, validator,
onLoad, onLoadAndChange, onChange);
}
}
}
@@ -19,6 +19,8 @@ import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.node.functions.DefaultStringParserProvider;
import org.mvplugins.multiverse.core.config.node.functions.DefaultSuggesterProvider;
import org.mvplugins.multiverse.core.config.node.functions.NodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeValueCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeStringParser;
import org.mvplugins.multiverse.core.config.node.functions.NodeSuggester;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeStringParser;
@@ -67,7 +69,9 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
@Nullable NodeStringParser<List<I>> stringParser,
@Nullable NodeSerializer<List<I>> serializer,
@Nullable Function<List<I>, Try<Void>> validator,
@Nullable BiConsumer<List<I>, List<I>> onSetValue,
@Nullable NodeValueCallback<List<I>> onLoad,
@Nullable NodeChangeCallback<List<I>> onLoadAndChange,
@Nullable NodeChangeCallback<List<I>> onChange,
@NotNull Class<I> itemType,
@Nullable NodeSuggester itemSuggester,
@Nullable NodeStringParser<I> itemStringParser,
@@ -75,7 +79,7 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
@Nullable Function<I, Try<Void>> itemValidator,
@Nullable BiConsumer<I, I> onSetItemValue) {
super(path, comments, name, type, aliases, defaultValueSupplier, suggester, stringParser, serializer,
validator, onSetValue);
validator, onLoad, onLoadAndChange, onChange);
this.itemType = itemType;
this.itemSuggester = itemSuggester != null
? itemSuggester
@@ -105,8 +109,8 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
if (this.itemSerializer != null && this.serializer == null) {
setDefaultSerialiser();
}
if (this.onSetItemValue != null && this.onSetValue == null) {
setDefaultOnSetValue();
if (this.onSetItemValue != null && this.onLoadAndChange == null) {
setDefaultOnLoadAndChange();
}
if (this.defaultValue == null) {
this.defaultValue = ArrayList::new;
@@ -168,8 +172,8 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
};
}
private void setDefaultOnSetValue() {
this.onSetValue = (oldValue, newValue) -> {
private void setDefaultOnLoadAndChange() {
this.onLoadAndChange = (oldValue, newValue) -> {
if (oldValue != null) {
oldValue.stream()
.filter(value -> !newValue.contains(value))
@@ -381,7 +385,9 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
stringParser,
serializer,
validator,
onSetValue,
onLoad,
onLoadAndChange,
onChange,
itemType,
itemSuggester,
itemStringParser,
@@ -5,11 +5,13 @@ import java.util.Collection;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.apache.logging.log4j.util.Strings;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.handle.BaseConfigurationHandle;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
public interface ValueNode<T> extends Node {
@@ -108,11 +110,51 @@ public interface ValueNode<T> extends Node {
*/
Try<Void> validate(@Nullable T value);
/**
* Called when the value of this node is loaded by {@link BaseConfigurationHandle#load()}.
*
* @param value The loaded value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void onLoad(@Nullable T value);
/**
* Called when the value of this node is loaded or changed.
*
* @param sender The sender who triggered the change. If triggered by loading or no target sender is specified,
* it will be the console sender.
* @param oldValue The old value, will always be null on load.
* @param newValue The new value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void onLoadAndChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue);
/**
* Called when the value of this node is changed by {@link BaseConfigurationHandle#set(ValueNode, Object)}.
*
* @param sender The sender who changed the value, or console sender if no target sender specified.
* @param oldValue The old value.
* @param newValue The new value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void onChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue);
/**
* Called when the value of this node is set.
*
* @param oldValue The old value.
* @param newValue The new value.
*
* @deprecated Use {@link #onLoadAndChange(CommandSender, Object, Object)} instead.
*/
void onSetValue(@Nullable T oldValue, @Nullable T newValue);
@Deprecated(since = "5.4", forRemoval = true)
default void onSetValue(@Nullable T oldValue, @Nullable T newValue) {
onLoadAndChange(Bukkit.getConsoleSender(), oldValue, newValue);
}
}
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.config.node.functions;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import co.aikar.commands.ACFUtil;
@@ -41,13 +42,13 @@ public final class DefaultStringParserProvider {
}
private static final NodeStringParser<Enum> ENUM_STRING_PARSER = (input, type) -> Try.of(
() -> Enum.valueOf(type, input.toUpperCase()));
() -> Enum.valueOf(type, input.toUpperCase(Locale.ENGLISH)));
private static final NodeStringParser<String> STRING_STRING_PARSER = (input, type) -> Try.of(
() -> input);
private static final NodeStringParser<Boolean> BOOLEAN_STRING_PARSER = (input, type) -> Try.of(
() -> switch (String.valueOf(input).toLowerCase()) {
() -> switch (String.valueOf(input).toLowerCase(Locale.ENGLISH)) {
case "t", "true", "on", "y", "yes", "1", "allow" -> true;
case "f", "false", "off", "n", "no", "0", "deny" -> false;
default -> throw new MultiverseException("Unable to convert '" + input + "' to boolean. Please use 'true' or 'false'");
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.config.node.functions;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -43,7 +44,7 @@ public final class DefaultSuggesterProvider {
private static NodeSuggester enumSuggester(Class<?> clazz) {
return input -> Arrays.stream(clazz.getEnumConstants())
.map(Object::toString)
.map(String::toLowerCase)
.map(name -> name.toLowerCase(Locale.ENGLISH))
.collect(Collectors.toList());
}
@@ -0,0 +1,65 @@
package org.mvplugins.multiverse.core.config.node.functions;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
/**
* A callback interface for handling changes to configuration node values.
*
* @param <T> The type of the node value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@FunctionalInterface
public interface NodeChangeCallback<T> {
/**
* Called when the value of a node changes.
*
* @param oldValue The old value of the node.
* @param newValue The new value of the node.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void run(T oldValue, T newValue);
/**
* Called when the value of a node changes, with the sender of the change.
*
* @param sender The sender of the change.
* @param oldValue The old value of the node.
* @param newValue The new value of the node.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
default void run(CommandSender sender, T oldValue, T newValue) {
run(oldValue, newValue);
}
/**
* Chains another {@link NodeChangeCallback} to be executed after this one.
*
* @param after The callback to execute after this one.
* @return A new {@link NodeChangeCallback} that executes both callbacks in order.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
default NodeChangeCallback<T> then(NodeChangeCallback<T> after) {
return new SenderNodeChangeCallback<T>() {
@Override
public void run(CommandSender sender, T oldValue, T newValue) {
NodeChangeCallback.this.run(sender, oldValue, newValue);
after.run(sender, oldValue, newValue);
}
@Override
public void run(T oldValue, T newValue) {
NodeChangeCallback.this.run(oldValue, newValue);
after.run(oldValue, newValue);
}
};
}
}
@@ -0,0 +1,38 @@
package org.mvplugins.multiverse.core.config.node.functions;
import org.jetbrains.annotations.ApiStatus;
/**
* Handler called when an action is performed on a node such as loading.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@FunctionalInterface
public interface NodeValueCallback<T> {
/**
* Called when an action is performed on a node.
*
* @param value The value of the node.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void run(T value);
/**
* Chains another {@link NodeValueCallback} to be executed after this one.
*
* @param after The callback to execute after this one.
* @return A new {@link NodeValueCallback} that executes both callbacks in order.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
default NodeValueCallback<T> then(NodeValueCallback<T> after) {
return (T value) -> {
this.run(value);
after.run(value);
};
}
}
@@ -0,0 +1,31 @@
package org.mvplugins.multiverse.core.config.node.functions;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
/**
* A callback interface for handling changes to configuration node values with contextual information from the sender.
*
* @param <T> The type of the node value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@FunctionalInterface
public interface SenderNodeChangeCallback<T> extends NodeChangeCallback<T> {
/**
* {@inheritDoc}
*/
@ApiStatus.AvailableSince("5.4")
void run(CommandSender sender, T oldValue, T newValue);
/**
* {@inheritDoc}
*/
@ApiStatus.AvailableSince("5.4")
@Override
default void run(T oldValue, T newValue) {
run(Bukkit.getConsoleSender(), oldValue, newValue);
}
}
@@ -11,8 +11,11 @@ import org.jetbrains.annotations.Nullable;
* A function that parses a string into a node value object of type {@link T} with contextual information from the sender.
*
* @param <T> The type of the object to parse.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
@FunctionalInterface
public interface SenderNodeStringParser<T> extends NodeStringParser<T> {
/**
* Parses a string into a node value object of type {@link T} with contextual information from the sender.
@@ -48,12 +48,12 @@ public final class DefaultSerializerProvider {
if (type.isInstance(object)) {
return (Enum<?>) object;
}
return Enum.valueOf(type, String.valueOf(object).toUpperCase());
return Enum.valueOf(type, String.valueOf(object).toUpperCase(Locale.ENGLISH));
}
@Override
public Object serialize(Enum object, Class<Enum> type) {
return object.name().toLowerCase();
return object.name().toLowerCase(Locale.ENGLISH);
}
};
@@ -80,7 +80,7 @@ public final class DefaultSerializerProvider {
}
String input = String.valueOf(object);
//todo: this is a copy from string parser
return switch (input.toLowerCase()) {
return switch (input.toLowerCase(Locale.ENGLISH)) {
case "t", "true", "on", "y", "yes", "1", "allow" -> true;
case "f", "false", "off", "n", "no", "0", "deny" -> false;
default -> throw new RuntimeException("Unable to convert '" + input + "' to boolean.");
@@ -4,8 +4,9 @@ import io.vavr.control.Option;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
/**
@@ -79,6 +80,20 @@ public abstract class DestinationInstance<I extends DestinationInstance<I, T>,
*/
public abstract @NotNull Option<String> getFinerPermissionSuffix();
/**
* Gets a user-friendly display text for this destination instance. This is used when displaying the destination
* to the player. By default, this returns the same as {@link #toString()}. Override this method to provide a more
* user-friendly display text with colors, formatting and localization support.
*
* @return The display message.
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@NotNull
public Message getDisplayMessage() {
return Message.of(this.toString());
}
/**
* Serialises the destination instance to a savable string.
*
@@ -7,6 +7,7 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
/**
* Destination instance implementation for the {@link AnchorDestination}.
@@ -63,6 +64,15 @@ public final class AnchorDestinationInstance extends DestinationInstance<AnchorD
return Option.of(anchorName);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
//TODO Localize
return Message.of("anchor '" + anchorName + "'");
}
/**
* {@inheritDoc}
*/
@@ -6,7 +6,7 @@ import java.util.stream.Collectors;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.bukkit.Bukkit;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@@ -20,8 +20,7 @@ import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker;
/**
* {@link Destination} implementation for beds.
@@ -30,7 +29,11 @@ import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.re
public final class BedDestination implements Destination<BedDestination, BedDestinationInstance, BedDestination.InstanceFailureReason> {
static final String OWN_BED_STRING = "playerbed";
BedDestination() {
private final ConcurrentPlayerWorldTracker worldTracker;
@Inject
BedDestination(@NotNull ConcurrentPlayerWorldTracker worldTracker) {
this.worldTracker = worldTracker;
}
/**
@@ -62,8 +65,8 @@ public final class BedDestination implements Destination<BedDestination, BedDest
@Override
public @NotNull Collection<DestinationSuggestionPacket> suggestDestinations(
@NotNull CommandSender sender, @Nullable String destinationParams) {
List<DestinationSuggestionPacket> collect = Bukkit.getOnlinePlayers().stream()
.map(player -> new DestinationSuggestionPacket(this, player.getName(), player.getName()))
List<DestinationSuggestionPacket> collect = worldTracker.getOnlinePlayers().stream()
.map(player -> new DestinationSuggestionPacket(this, player, player))
.collect(Collectors.toList());
if (sender instanceof Player) {
collect.add(new DestinationSuggestionPacket(this, OWN_BED_STRING, OWN_BED_STRING));
@@ -9,12 +9,13 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
/**
* Destination instance implementation for the {@link BedDestination}.
*/
public final class BedDestinationInstance extends DestinationInstance<BedDestinationInstance, BedDestination> {
private final Player player;
private final @Nullable Player player;
/**
* Constructor.
@@ -64,6 +65,15 @@ public final class BedDestinationInstance extends DestinationInstance<BedDestina
return Option.of(player != null ? player.getName() : BedDestination.OWN_BED_STRING);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
//TODO Localize
return Message.of(player == null ? "your bed/respawn point" : player.getName() + "'s bed/respawn point");
}
/**
* {@inheritDoc}
*/
@@ -19,14 +19,15 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.entrycheck.WorldEntryCheckerProvider;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.*;
@@ -62,7 +63,12 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
* @return A new {@link ExactDestinationInstance}
*/
public @NotNull ExactDestinationInstance fromLocation(@NotNull Location location) {
return new ExactDestinationInstance(this, new UnloadedWorldLocation(location));
return new ExactDestinationInstance(
this,
worldManager,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
);
}
/**
@@ -73,12 +79,17 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
@NotNull CommandSender sender,
@NotNull String destinationParams
) {
String[] items = REPatterns.COLON.split(destinationParams);
String[] items = REPatterns.COLON.split(destinationParams, 2);
if (items.length < 2) {
if (items[0].equals("@here")) {
return getLocationFromSender(sender)
.map(location -> Attempt.<ExactDestinationInstance, InstanceFailureReason>success(
new ExactDestinationInstance(this, new UnloadedWorldLocation(location))
new ExactDestinationInstance(
this,
worldManager,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
)
))
.getOrElse(() -> Attempt.failure(InstanceFailureReason.INVALID_COORDINATES_FORMAT)); // todo: specific failure reason for this case
}
@@ -86,41 +97,21 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
}
String worldName = items[0];
String coordinates = items[1];
String[] coordinatesParams = REPatterns.COMMA.split(coordinates);
if (coordinatesParams.length != 3) {
return Attempt.failure(InstanceFailureReason.INVALID_COORDINATES_FORMAT);
}
String positionStr = items[1];
World world = getLoadedMultiverseWorld(worldName).flatMap(LoadedMultiverseWorld::getBukkitWorld).getOrNull();
if (world == null) {
return Attempt.failure(InstanceFailureReason.WORLD_NOT_FOUND, Replace.WORLD.with(worldName));
}
UnloadedWorldLocation location;
EntityPosition position;
try {
location = new UnloadedWorldLocation(
world,
Double.parseDouble(coordinatesParams[0]),
Double.parseDouble(coordinatesParams[1]),
Double.parseDouble(coordinatesParams[2])
);
} catch (NumberFormatException e) {
position = EntityPosition.fromString(positionStr);
} catch (PositionParseException e) {
return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e));
}
if (items.length == 4) {
String pitch = items[2];
String yaw = items[3];
try {
location.setPitch(Float.parseFloat(pitch));
location.setYaw(Float.parseFloat(yaw));
} catch (NumberFormatException e) {
return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e));
}
}
return Attempt.success(new ExactDestinationInstance(this, location));
return Attempt.success(new ExactDestinationInstance(this, worldManager, worldName, position));
}
//TODO: Extract to a world finder class
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.destination.core;
import io.vavr.control.Option;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
@@ -8,22 +9,34 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
/**
* Destination instance implementation for the {@link ExactDestination}.
*/
public final class ExactDestinationInstance extends DestinationInstance<ExactDestinationInstance, ExactDestination> {
private final UnloadedWorldLocation location;
private final WorldManager worldManager;
private final String worldName;
private final EntityPosition position;
/**
* Constructor.
*
* @param location The location to teleport to.
* @param destination The parent destination.
* @param worldName The name of the world.
* @param position The position in the world.
*/
ExactDestinationInstance(@NotNull ExactDestination destination, @NotNull UnloadedWorldLocation location) {
ExactDestinationInstance(@NotNull ExactDestination destination,
@NotNull WorldManager worldManager,
@NotNull String worldName,
@NotNull EntityPosition position) {
super(destination);
this.location = location;
this.worldManager = worldManager;
this.worldName = worldName;
this.position = position;
}
/**
@@ -31,10 +44,13 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
*/
@Override
public @NotNull Option<Location> getLocation(@NotNull Entity teleportee) {
if (location.getWorld() == null) {
World world = Bukkit.getWorld(worldName);
if (world == null) {
return Option.none();
}
return Option.of(location.toBukkitLocation());
Location destinationLocation = position.toBukkitLocation(teleportee.getLocation());
destinationLocation.setWorld(world);
return Option.of(destinationLocation);
}
/**
@@ -58,7 +74,18 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
*/
@Override
public @NotNull Option<String> getFinerPermissionSuffix() {
return Option.of(location.getWorld()).map(World::getName);
return Option.of(worldName);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
String displayWorldName = worldManager.getWorld(worldName)
.map(MultiverseWorld::getAliasOrName)
.getOrElse(worldName);
return Message.of(displayWorldName + " at " + position.toString());
}
/**
@@ -66,7 +93,6 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
*/
@Override
public @NotNull String serialise() {
return location.getWorldName() + ":" + location.getX() + "," + location.getY()
+ "," + location.getZ() + ":" + location.getPitch() + ":" + location.getYaw();
return worldName + ":" + position.toString();
}
}
@@ -4,7 +4,7 @@ import java.util.Collection;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.bukkit.Bukkit;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@@ -14,21 +14,26 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker;
/**
* {@link Destination} implementation for players.s
*/
@Service
public final class PlayerDestination implements Destination<PlayerDestination, PlayerDestinationInstance, PlayerDestination.InstanceFailureReason> {
private final ConcurrentPlayerWorldTracker playerWorldTracker;
/**
* Creates a new instance of the PlayerDestination.
*/
PlayerDestination() {
@Inject
PlayerDestination(@NotNull ConcurrentPlayerWorldTracker playerWorldTracker) {
this.playerWorldTracker = playerWorldTracker;
}
/**
@@ -60,8 +65,8 @@ public final class PlayerDestination implements Destination<PlayerDestination, P
@Override
public @NotNull Collection<DestinationSuggestionPacket> suggestDestinations(
@NotNull CommandSender sender, @Nullable String destinationParams) {
return Bukkit.getOnlinePlayers().stream()
.map(p -> new DestinationSuggestionPacket(this, p.getName(), p.getName()))
return playerWorldTracker.getOnlinePlayers().stream()
.map(player -> new DestinationSuggestionPacket(this, player, player))
.toList();
}
@@ -9,6 +9,7 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
import java.util.UUID;
@@ -66,6 +67,14 @@ public final class PlayerDestinationInstance extends DestinationInstance<PlayerD
return Option.of(playerName);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
return Message.of(playerName);
}
/**
* {@inheritDoc}
*/
@@ -8,12 +8,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
/**
* Destination instance implementation for the {@link WorldDestination}.
*/
@@ -87,6 +84,14 @@ public final class WorldDestinationInstance extends DestinationInstance<WorldDes
return Option.of(world.getName());
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
return Message.of(world.getAliasOrName());
}
/**
* {@inheritDoc}
*/
@@ -0,0 +1,23 @@
package org.mvplugins.multiverse.core.exceptions.utils.position;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.locale.message.Message;
public class PositionParseException extends MultiverseException {
public PositionParseException(String message) {
super(message);
}
public PositionParseException(@Nullable Message message) {
super(message);
}
public PositionParseException(@Nullable String message, @Nullable Throwable cause) {
super(message, cause);
}
public PositionParseException(@Nullable Message message, @Nullable Throwable cause) {
super(message, cause);
}
}
@@ -16,6 +16,7 @@ import org.mvplugins.multiverse.core.dynamiclistener.annotations.IgnoreIfCancell
import org.mvplugins.multiverse.core.dynamiclistener.annotations.SkipIfEventExist;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker;
/**
* Multiverse's Listener for players.
@@ -24,16 +25,17 @@ import org.mvplugins.multiverse.core.world.WorldManager;
final class MVChatListener implements CoreListener {
private final CoreConfig config;
private final WorldManager worldManager;
private final MVPlayerListener playerListener;
private final ConcurrentPlayerWorldTracker playerWorldTracker;
@Inject
MVChatListener(
CoreConfig config,
WorldManager worldManager,
MVPlayerListener playerListener) {
ConcurrentPlayerWorldTracker playerWorldTracker
) {
this.config = config;
this.worldManager = worldManager;
this.playerListener = playerListener;
this.playerWorldTracker = playerWorldTracker;
}
@EventClass("io.papermc.paper.event.player.AsyncChatEvent")
@@ -94,12 +96,9 @@ final class MVChatListener implements CoreListener {
}
private String getWorldName(Player player) {
String world = playerListener.getPlayerWorld().get(player.getName());
if (world == null) {
world = player.getWorld().getName();
playerListener.getPlayerWorld().put(player.getName(), world);
}
return this.worldManager.getLoadedWorld(world)
String worldName = playerWorldTracker.getPlayerWorld(player.getName())
.getOrElse(() -> player.getWorld().getName());
return this.worldManager.getLoadedWorld(worldName)
.map(mvworld -> mvworld.isHidden() ? "" : mvworld.getAliasOrName())
.getOrElse("");
}
@@ -11,7 +11,6 @@ import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
@@ -43,7 +42,7 @@ final class MVEntityListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
public void foodLevelChange(FoodLevelChangeEvent event) {
void foodLevelChange(FoodLevelChangeEvent event) {
if (event.isCancelled()) {
return;
}
@@ -65,7 +64,7 @@ final class MVEntityListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
public void entityRegainHealth(EntityRegainHealthEvent event) {
void entityRegainHealth(EntityRegainHealthEvent event) {
if (event.isCancelled() || event.getRegainReason() != RegainReason.REGEN) {
return;
}
@@ -84,7 +83,7 @@ final class MVEntityListener implements CoreListener {
* @param event The event.
*/
@EventMethod
public void creatureSpawn(CreatureSpawnEvent event) {
void creatureSpawn(CreatureSpawnEvent event) {
if (event.isCancelled()) {
return;
}
@@ -110,7 +109,7 @@ final class MVEntityListener implements CoreListener {
* Handle Spawn Category settings for non-creature entities.
*/
@EventMethod
public void entitySpawn(EntitySpawnEvent event) {
void entitySpawn(EntitySpawnEvent event) {
if (event.getEntity() instanceof LivingEntity) {
// Handled by CreatureSpawnEvent
return;
@@ -23,6 +23,7 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventPriority;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerPortalEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
@@ -32,16 +33,16 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.dynamiclistener.EventRunnable;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventClass;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventPriorityKey;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.event.MVRespawnEvent;
import org.mvplugins.multiverse.core.locale.PluginLocales;
import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker;
import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter;
import org.mvplugins.multiverse.core.teleportation.BlockSafety;
import org.mvplugins.multiverse.core.teleportation.TeleportQueue;
import org.mvplugins.multiverse.core.utils.result.ResultChain;
@@ -52,7 +53,6 @@ import org.mvplugins.multiverse.core.world.entrycheck.EntryFeeResult;
import org.mvplugins.multiverse.core.world.entrycheck.WorldEntryCheckerProvider;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder;
import org.mvplugins.multiverse.core.world.helpers.EnforcementHandler;
import org.spigotmc.event.player.PlayerSpawnLocationEvent;
/**
* Multiverse's Listener for players.
@@ -72,8 +72,7 @@ final class MVPlayerListener implements CoreListener {
private final EnforcementHandler enforcementHandler;
private final DimensionFinder dimensionFinder;
private final CorePermissionsChecker corePermissionsChecker;
private final Map<String, String> playerWorld = new ConcurrentHashMap<>();
private final AsyncSafetyTeleporter asyncSafetyTeleporter;
@Inject
MVPlayerListener(
@@ -89,7 +88,8 @@ final class MVPlayerListener implements CoreListener {
DestinationsProvider destinationsProvider,
EnforcementHandler enforcementHandler,
DimensionFinder dimensionFinder,
CorePermissionsChecker corePermissionsChecker) {
CorePermissionsChecker corePermissionsChecker,
AsyncSafetyTeleporter asyncSafetyTeleporter) {
this.plugin = plugin;
this.config = config;
this.worldManagerProvider = worldManagerProvider;
@@ -103,6 +103,7 @@ final class MVPlayerListener implements CoreListener {
this.enforcementHandler = enforcementHandler;
this.dimensionFinder = dimensionFinder;
this.corePermissionsChecker = corePermissionsChecker;
this.asyncSafetyTeleporter = asyncSafetyTeleporter;
}
private WorldManager getWorldManager() {
@@ -113,19 +114,10 @@ final class MVPlayerListener implements CoreListener {
return commandManagerProvider.get();
}
public PluginLocales getLocales() {
private PluginLocales getLocales() {
return getCommandManager().getLocales();
}
/**
* Gets the map of player and the world name they are in.
*
* @return the playerWorld-map
*/
public Map<String, String> getPlayerWorld() {
return playerWorld;
}
/**
* This method is called when a player respawns.
*
@@ -134,7 +126,7 @@ final class MVPlayerListener implements CoreListener {
@EventMethod
@EventPriorityKey("mvcore-player-respawn")
@DefaultEventPriority(EventPriority.LOW)
public void playerRespawn(PlayerRespawnEvent event) {
void playerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer();
LoadedMultiverseWorld mvWorld = getWorldManager().getLoadedWorld(player.getWorld()).getOrNull();
if (mvWorld == null) {
@@ -201,68 +193,63 @@ final class MVPlayerListener implements CoreListener {
return Option.of(mvWorld.getSpawnLocation());
}
@EventClass("org.spigotmc.event.player.PlayerSpawnLocationEvent")
@EventMethod
// TODO: Consider if this key is needed anymore, need to remove from config.yml as well
@EventPriorityKey("mvcore-player-spawn-location")
EventRunnable playerSpawnLocation() {
return new EventRunnable<PlayerSpawnLocationEvent>() {
@Override
public void onEvent(PlayerSpawnLocationEvent event) {
Player player = event.getPlayer();
MultiverseWorld world = getWorldManager().getLoadedWorld(player.getWorld()).getOrNull();
if (world == null) {
Logging.finer("Player joined in a world that is not managed by Multiverse.");
return;
}
if (!player.hasPlayedBefore()) {
handleFirstSpawn(event);
} else {
handleJoinLocation(event);
}
handleGameModeAndFlight(player, event.getSpawnLocation().getWorld());
}
void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
if (player.hasPlayedBefore()) {
handleJoinLocation(player);
} else {
handleFirstSpawn(player);
}
handleGameModeAndFlight(player, player.getWorld());
}
private void handleFirstSpawn(PlayerSpawnLocationEvent event) {
if (!config.getFirstSpawnOverride()) {
Logging.finer("FirstSpawnOverride is disabled");
// User has disabled the feature in config
return;
}
Logging.fine("Moving NEW player to(firstspawnoverride): %s", config.getFirstSpawnLocation());
destinationsProvider.parseDestination(config.getFirstSpawnLocation())
.map(destination -> destination.getLocation(event.getPlayer())
.peek(event::setSpawnLocation)
.onEmpty(() -> Logging.warning("The destination in FirstSpawnLocation in config is invalid")))
.onFailure(failure -> {
Logging.warning("Invalid destination in FirstSpawnLocation in config: %s");
Logging.warning(failure.getFailureMessage().formatted(getLocales()));
});
}
private void handleFirstSpawn(Player player) {
if (!config.getFirstSpawnOverride()) {
Logging.finer("FirstSpawnOverride is disabled");
// User has disabled the feature in config
return;
}
Logging.fine("Moving NEW player to(firstspawnoverride): %s", config.getFirstSpawnLocation());
destinationsProvider.parseDestination(config.getFirstSpawnLocation())
.peek(destination -> teleportToDestinationOnJoin(player, destination))
.onFailure(failure -> {
Logging.warning("Invalid destination in FirstSpawnLocation in config: %s");
Logging.warning(failure.getFailureMessage().formatted(getLocales()));
});
}
private void handleJoinLocation(PlayerSpawnLocationEvent event) {
if (!config.getEnableJoinDestination()) {
Logging.finer("JoinDestination is disabled");
// User has disabled the feature in config
return;
}
if (config.getJoinDestination().isBlank()) {
Logging.warning("Joindestination is enabled but no destination has been specified in config!");
return;
}
if (corePermissionsChecker.hasJoinLocationBypassPermission(event.getPlayer())) {
Logging.finer("Player %s has bypass permission for JoinDestination", event.getPlayer().getName());
return;
}
Logging.finer("JoinDestination is " + config.getJoinDestination());
destinationsProvider.parseDestination(config.getJoinDestination())
.map(destination -> destination.getLocation(event.getPlayer())
.peek(event::setSpawnLocation)
.onEmpty(() -> Logging.warning("The destination in JoinDestination in config is invalid")))
.onFailure(failure -> {
Logging.warning("Invalid destination in JoinDestination in config: %s");
Logging.warning(failure.getFailureMessage().formatted(getLocales()));
});
}
};
private void handleJoinLocation(Player player) {
if (!config.getEnableJoinDestination()) {
Logging.finer("JoinDestination is disabled");
// User has disabled the feature in config
return;
}
if (config.getJoinDestination().isBlank()) {
Logging.warning("Joindestination is enabled but no destination has been specified in config!");
return;
}
if (corePermissionsChecker.hasJoinLocationBypassPermission(player)) {
Logging.finer("Player %s has bypass permission for JoinDestination", player.getName());
return;
}
Logging.finer("JoinDestination is " + config.getJoinDestination());
destinationsProvider.parseDestination(config.getJoinDestination())
.peek(destination -> teleportToDestinationOnJoin(player, destination))
.onFailure(failure -> {
Logging.warning("Invalid destination in JoinDestination in config: %s");
Logging.warning(failure.getFailureMessage().formatted(getLocales()));
});
}
private void teleportToDestinationOnJoin(Player player, DestinationInstance<?, ?> destination) {
asyncSafetyTeleporter.to(destination).teleportSingle(player)
.onSuccess(result -> Logging.fine("Player %s has been teleported on join",
player.getName()))
.onFailure(failure -> Logging.warning("Failed to teleport player %s on join: %s",
player.getName(), failure.get(0)));
}
/**
@@ -271,10 +258,9 @@ final class MVPlayerListener implements CoreListener {
*/
@EventMethod
@DefaultEventPriority(EventPriority.MONITOR)
public void playerChangedWorld(PlayerChangedWorldEvent event) {
void playerChangedWorld(PlayerChangedWorldEvent event) {
// Permissions now determine whether or not to handle a gamemode.
this.handleGameModeAndFlight(event.getPlayer(), event.getPlayer().getWorld());
playerWorld.put(event.getPlayer().getName(), event.getPlayer().getWorld().getName());
}
/**
@@ -284,7 +270,7 @@ final class MVPlayerListener implements CoreListener {
@EventMethod
@EventPriorityKey("mvcore-player-teleport")
@DefaultEventPriority(EventPriority.HIGHEST)
public void playerTeleport(PlayerTeleportEvent event) {
void playerTeleport(PlayerTeleportEvent event) {
Logging.finer("Got teleport event for player '"
+ event.getPlayer().getName() + "' with cause '" + event.getCause() + "'");
if (event.isCancelled()) {
@@ -355,7 +341,7 @@ final class MVPlayerListener implements CoreListener {
*/
@EventMethod
@DefaultEventPriority(EventPriority.LOWEST)
public void playerPortalCheck(PlayerPortalEvent event) {
void playerPortalCheck(PlayerPortalEvent event) {
if (event.isCancelled()) {
return;
}
@@ -381,7 +367,7 @@ final class MVPlayerListener implements CoreListener {
@EventMethod
@EventPriorityKey("mvcore-player-portal")
@DefaultEventPriority(EventPriority.HIGH)
public void playerPortal(PlayerPortalEvent event) {
void playerPortal(PlayerPortalEvent event) {
if (event.isCancelled()) {
return;
}
@@ -12,7 +12,6 @@ import jakarta.inject.Inject;
import org.bukkit.Material;
import org.bukkit.PortalType;
import org.bukkit.block.BlockState;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.Action;
import org.bukkit.event.entity.EntityPortalEvent;
import org.bukkit.event.player.PlayerInteractEvent;
@@ -48,7 +47,7 @@ final class MVPortalListener implements CoreListener {
*/
@EventMethod
@IgnoreIfCancelled
public void portalCreate(PortalCreateEvent event) {
void portalCreate(PortalCreateEvent event) {
Logging.fine("Attempting to create portal at '%s' with reason: %s",
event.getWorld().getName(), event.getReason());
@@ -93,7 +92,7 @@ final class MVPortalListener implements CoreListener {
*/
@EventMethod
@IgnoreIfCancelled
public void playerInteract(PlayerInteractEvent event) {
void playerInteract(PlayerInteractEvent event) {
if (isCreateEndPortalInteraction(event)) {
return;
}
@@ -122,7 +121,7 @@ final class MVPortalListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
public void entityPortal(EntityPortalEvent event) {
void entityPortal(EntityPortalEvent event) {
if (event.isCancelled() || event.getTo() == null) {
return;
}
@@ -9,7 +9,6 @@ package org.mvplugins.multiverse.core.listeners;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.bukkit.event.EventHandler;
import org.bukkit.event.weather.ThunderChangeEvent;
import org.bukkit.event.weather.WeatherChangeEvent;
import org.jvnet.hk2.annotations.Service;
@@ -35,7 +34,7 @@ final class MVWeatherListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
public void weatherChange(WeatherChangeEvent event) {
void weatherChange(WeatherChangeEvent event) {
if (event.isCancelled() || !event.toWeatherState()) {
return;
}
@@ -54,7 +53,7 @@ final class MVWeatherListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
public void thunderChange(ThunderChangeEvent event) {
void thunderChange(ThunderChangeEvent event) {
if (event.isCancelled() || !event.toThunderState()) {
return;
}
@@ -346,6 +346,11 @@ public enum MVCorei18n implements MessageKeyProvider {
EXCEPTION_MULTIVERSEWORLD_UNLOADPLAYERSINWORLD,
EXCEPTION_MULTIVERSEWORLD_UNLOADERROR,
// multiverse position parse exception
EXCEPTION_POSITIONPARSE_INVALIDDIRECTION,
EXCEPTION_POSITIONPARSE_INVALIDCOORDINATES,
EXCEPTION_POSITIONPARSE_INVALIDNUMBER,
// generic
GENERIC_SUCCESS,
GENERIC_FAILURE,
@@ -27,39 +27,12 @@ public final class LocalizedMessage extends Message implements MessageKeyProvide
return messageKeyProvider.getMessageKey();
}
@Override
public @NotNull String[] getReplacements(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
return toReplacementsArray(locales, commandIssuer, replacements);
}
@Override
public @NotNull String formatted(@NotNull CommandIssuer commandIssuer) {
return formatted(commandIssuer.getManager().getLocales(), commandIssuer);
}
@Override
public @NotNull String formatted(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
Objects.requireNonNull(locales, "locales must not be null");
String[] parsedReplacements = getReplacements(locales, commandIssuer);
if (parsedReplacements.length == 0) {
return locales.getMessage(commandIssuer, getMessageKey());
}
return ACFUtil.replaceStrings(locales.getMessage(commandIssuer, getMessageKey()), parsedReplacements);
}
private static String[] toReplacementsArray(
@NotNull Locales locales,
@Nullable CommandIssuer commandIssuer,
@NotNull MessageReplacement... replacements) {
String[] replacementsArray = new String[replacements.length * 2];
int i = 0;
for (MessageReplacement replacement : replacements) {
replacementsArray[i++] = replacement.getKey();
replacementsArray[i++] = replacement.getReplacement().fold(
str -> str,
message -> message.formatted(locales, commandIssuer));
}
return replacementsArray;
}
}
@@ -103,7 +103,7 @@ public sealed class Message permits LocalizedMessage {
* @return The replacements
*/
public @NotNull String[] getReplacements(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
return getReplacements();
return toReplacementsArray(locales, commandIssuer, replacements);
}
/**
@@ -152,7 +152,7 @@ public sealed class Message permits LocalizedMessage {
* @return The formatted, localized message
*/
public @NotNull String formatted(@NotNull CommandIssuer commandIssuer) {
return formatted();
return formatted(commandIssuer.getManager().getLocales(), commandIssuer);
}
/**
@@ -166,7 +166,11 @@ public sealed class Message permits LocalizedMessage {
* @return The formatted, localized message
*/
public @NotNull String formatted(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
return formatted();
String[] parsedReplacements = getReplacements(locales, commandIssuer);
if (parsedReplacements.length == 0) {
return raw();
}
return ACFUtil.replaceStrings(message, parsedReplacements);
}
private static String[] toReplacementsArray(@NotNull MessageReplacement... replacements) {
@@ -178,4 +182,19 @@ public sealed class Message permits LocalizedMessage {
}
return replacementsArray;
}
private static String[] toReplacementsArray(
@NotNull Locales locales,
@Nullable CommandIssuer commandIssuer,
@NotNull MessageReplacement... replacements) {
String[] replacementsArray = new String[replacements.length * 2];
int i = 0;
for (MessageReplacement replacement : replacements) {
replacementsArray[i++] = replacement.getKey();
replacementsArray[i++] = replacement.getReplacement().fold(
str -> str,
message -> message.formatted(locales, commandIssuer));
}
return replacementsArray;
}
}
@@ -10,18 +10,16 @@ import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.ApiStatus;
import org.jvnet.hk2.annotations.Contract;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.MultiverseCoreApi;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.MultiverseCommand;
import org.mvplugins.multiverse.core.commands.CoreCommand;
import org.mvplugins.multiverse.core.dynamiclistener.DynamicListener;
import org.mvplugins.multiverse.core.dynamiclistener.DynamicListenerRegistration;
import org.mvplugins.multiverse.core.inject.PluginServiceLocator;
import org.mvplugins.multiverse.core.inject.PluginServiceLocatorFactory;
import org.mvplugins.multiverse.core.inject.binder.PluginBinder;
import org.mvplugins.multiverse.core.listeners.CoreListener;
import org.mvplugins.multiverse.core.utils.REPatterns;
import java.util.Locale;
/**
* Common plugin class for all Multiverse plugins.
*/
@@ -151,7 +149,7 @@ public abstract class MultiverseModule extends JavaPlugin {
.andThen(pluginLocales -> {
pluginLocales.addFileResClassLoader(this);
pluginLocales.addBundleClassLoader(this.getClassLoader());
pluginLocales.addMessageBundles(this.getDescription().getName().toLowerCase());
pluginLocales.addMessageBundles(this.getName().toLowerCase(Locale.ENGLISH));
})
.onFailure(e -> {
Logging.severe("Failed to register locales");
@@ -13,6 +13,7 @@ import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import static org.mvplugins.multiverse.core.permissions.PermissionUtils.concatPermission;
import static org.mvplugins.multiverse.core.permissions.PermissionUtils.registerPermissionWithWildcards;
@Service
public final class CorePermissions {
@@ -60,28 +61,34 @@ public final class CorePermissions {
@PostConstruct
void registerBasePermissions() {
pluginManager.addPermission(new Permission(JOINLOCATION_BYPASS, PermissionDefault.FALSE));
Logging.fine("Successfully registered base permissions");
Try.run(() -> {
registerPermissionWithWildcards(new Permission(JOINLOCATION_BYPASS, PermissionDefault.FALSE));
}).onSuccess(ignore -> {
Logging.fine("Successfully registered base permissions");
}).onFailure(e -> {
Logging.fine("Failed to register base permissions: %s", e.getMessage());
});
}
public Try<Void> addWorldPermissions(@NotNull MultiverseWorld world) {
return Try.run(() -> {
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(WORLD_ACCESS, world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(WORLD_EXEMPT, world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(GAMEMODE_BYPASS, world.getName()), PermissionDefault.FALSE));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(PLAYERLIMIT_BYPASS, world.getName()), PermissionDefault.FALSE));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(SPAWN, world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(SPAWN, "self", world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(SPAWN, "other", world.getName()), PermissionDefault.OP));
Logging.fine("Successfully registered permissions for world %s", world.getName());
});
}).onFailure(e -> Logging.fine("Failed to register permissions for world %s: %s",
world.getName(), e.getMessage()));
}
public Try<Void> removeWorldPermissions(@NotNull MultiverseWorld world) {
@@ -90,20 +97,23 @@ public final class CorePermissions {
pluginManager.removePermission(concatPermission(WORLD_EXEMPT, world.getName()));
pluginManager.removePermission(concatPermission(GAMEMODE_BYPASS, world.getName()));
pluginManager.removePermission(concatPermission(PLAYERLIMIT_BYPASS, world.getName()));
pluginManager.removePermission(concatPermission(SPAWN, world.getName()));
pluginManager.removePermission(concatPermission(SPAWN, "self", world.getName()));
pluginManager.removePermission(concatPermission(SPAWN, "other", world.getName()));
Logging.fine("Successfully removed permissions for world %s", world.getName());
});
}).onFailure(e -> Logging.fine("Failed to remove permissions for world %s: %s",
world.getName(), e.getMessage()));
}
public Try<Void> addDestinationPermissions(@NotNull Destination destination) {
return Try.run(() -> {
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(TELEPORT, "self", destination.getIdentifier()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(TELEPORT, "other", destination.getIdentifier()), PermissionDefault.OP));
Logging.fine("Successfully registered permissions for destination %s", destination.getIdentifier());
});
}).onFailure(e -> Logging.fine("Failed to register permissions for destination %s: %s",
destination.getIdentifier(), e.getMessage()));
}
public Try<Void> removeDestinationPermissions(@NotNull Destination destination) {
@@ -111,6 +121,7 @@ public final class CorePermissions {
pluginManager.removePermission(concatPermission(TELEPORT, "self", destination.getIdentifier()));
pluginManager.removePermission(concatPermission(TELEPORT, "other", destination.getIdentifier()));
Logging.fine("Successfully removed permissions for destination %s", destination.getIdentifier());
});
}).onFailure(e -> Logging.fine("Failed to remove permissions for destination %s: %s",
destination.getIdentifier(), e.getMessage()));
}
}
@@ -1,8 +1,14 @@
package org.mvplugins.multiverse.core.permissions;
import com.dumptruckman.minecraft.util.Logging;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.jetbrains.annotations.ApiStatus;
import java.util.Arrays;
public final class PermissionUtils {
@@ -20,6 +26,37 @@ public final class PermissionUtils {
PermissionUtils.debugPermissions = debugPermissions;
}
/**
* Registers a permission along with all its wildcard parents.
* <br />
* For example, registering "mv.bypass.joinlocation" will also register "mv.*" and "mv.bypass.*" as parents.
*
* @param permission The permission to register.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static void registerPermissionWithWildcards(Permission permission) {
Bukkit.getServer().getPluginManager().addPermission(permission);
String[] split = permission.getName().split("\\.");
StringBuilder prefix = new StringBuilder();
// Skip the last element since it's the actual permission
Arrays.stream(Arrays.copyOfRange(split, 0, split.length - 1)).forEach(s -> {
prefix.append(s).append(".");
Permission perm = getOrAddPermission(prefix + "*");
permission.addParent(perm, true);
});
}
private static Permission getOrAddPermission(String permission) {
Permission perm = Bukkit.getServer().getPluginManager().getPermission(permission);
if (perm == null) {
perm = new Permission(permission, PermissionDefault.FALSE);
Bukkit.getServer().getPluginManager().addPermission(perm);
}
return perm;
}
/**
* Joins permissions with a dot.
*
@@ -228,8 +228,8 @@ public final class LocationManipulation {
if (orientation == null) {
return 0;
}
if (ORIENTATION_INTS.containsKey(orientation.toLowerCase())) {
return ORIENTATION_INTS.get(orientation.toLowerCase());
if (ORIENTATION_INTS.containsKey(orientation.toLowerCase(Locale.ENGLISH))) {
return ORIENTATION_INTS.get(orientation.toLowerCase(Locale.ENGLISH));
}
return 0;
}
@@ -77,7 +77,7 @@ public final class FileUtils {
*/
public @Nullable File getServerProperties() {
if (this.serverProperties == null) {
this.serverProperties = findFileFromServerDirectory("server.properties");
this.serverProperties = findFileFromServerDirectory(config.getServerPropertiesPath());
Logging.finer("server.properties: %s", this.serverProperties);
}
return this.serverProperties;
@@ -1,16 +1,18 @@
package org.mvplugins.multiverse.core.utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.dumptruckman.minecraft.util.Logging;
import com.google.common.base.Strings;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -76,22 +78,33 @@ public final class PlayerFinder {
public static @NotNull List<Player> getMulti(@Nullable String playerIdentifiers,
@NotNull CommandSender sender
) {
List<Player> playerResults = new ArrayList<>();
return tryGetMulti(playerIdentifiers, sender)
.getOrElse(Collections.emptyList());
}
/**
* Get multiple {@link Player} based on many identifiers of name UUID or selector.
*
* @param playerIdentifiers An identifier of multiple names, UUIDs or selectors, separated by comma.
* @param sender Target sender for selector.
* @return A list of all the {@link Player} found.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static @NotNull Try<@NotNull List<Player>> tryGetMulti(@Nullable String playerIdentifiers,
@NotNull CommandSender sender
) {
if (playerIdentifiers == null || Strings.isNullOrEmpty(playerIdentifiers)) {
return playerResults;
return Try.success(Collections.emptyList());
}
// TODO: Currently just assume entire string is a selector. Add support for comma seperated mixture of names, uuids and selectors
if (isSelector(playerIdentifiers)) {
Logging.finer("Using selector: %s", playerIdentifiers);
List<Player> targetPlayers = getMultiBySelector(playerIdentifiers, sender);
if (targetPlayers != null) {
playerResults.addAll(targetPlayers);
}
return playerResults;
return tryGetMultiBySelector(playerIdentifiers, sender);
}
List<Player> playerResults = new ArrayList<>();
String[] playerIdentifierArray = REPatterns.COMMA.split(playerIdentifiers);
for (String playerIdentifier : playerIdentifierArray) {
Player targetPlayer = getByName(playerIdentifier);
@@ -104,15 +117,25 @@ public final class PlayerFinder {
playerResults.add(targetPlayer);
continue;
}
List<Player> targetPlayers = getMultiBySelector(playerIdentifier, sender);
if (targetPlayers != null) {
playerResults.addAll(targetPlayers);
Try<@NotNull List<Player>> selectorParseResult = tryGetMultiBySelector(playerIdentifier, sender);
if (selectorParseResult.isFailure()) {
return Try.failure(selectorParseResult.getCause());
}
playerResults.addAll(selectorParseResult.getOrElse(Collections.emptyList()));
}
return playerResults;
return Try.success(playerResults);
}
private static boolean isSelector(@NotNull String playerIdentifier) {
/**
* Check if the player identifier is a selector.
*
* @param playerIdentifier An identifier of name, UUID or selector.
* @return True if the identifier is a selector, else false.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static boolean isSelector(@NotNull String playerIdentifier) {
return VANILLA_SELECTORS.stream().anyMatch(playerIdentifier::startsWith);
}
@@ -167,7 +190,7 @@ public final class PlayerFinder {
@NotNull CommandSender sender
) {
List<Player> matchedPlayers = getMultiBySelector(playerSelector, sender);
if (matchedPlayers == null || matchedPlayers.isEmpty()) {
if (matchedPlayers.isEmpty()) {
Logging.fine("No player found with selector '%s' for %s.", playerSelector, sender.getName());
return null;
}
@@ -186,22 +209,36 @@ public final class PlayerFinder {
* @param sender Target sender for selector.
* @return A list of all the {@link Player} found.
*/
public static @Nullable List<Player> getMultiBySelector(@NotNull String playerSelector,
public static @NotNull List<Player> getMultiBySelector(@NotNull String playerSelector,
@NotNull CommandSender sender
) {
return tryGetMultiBySelector(playerSelector, sender)
.onFailure(throwable -> Logging.warning(
"Error selecting entities with selector '%s' for %s: %s",
playerSelector, sender.getName(), throwable.getMessage()
))
.getOrElse(Collections::emptyList);
}
/**
* Get multiple {@link Player} based on <a href="https://minecraft.gamepedia.com/Commands#Target_selectors">vanilla selectors</a>.
*
* @param playerSelector A target selector, usually starts with an '@'.
* @param sender Target sender for selector.
* @return A list of all the {@link Player} found.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static @NotNull Try<@NotNull List<Player>> tryGetMultiBySelector(@NotNull String playerSelector,
@NotNull CommandSender sender
) {
if (playerSelector.charAt(0) != '@') {
return null;
}
try {
return Bukkit.selectEntities(sender, playerSelector).stream()
.filter(e -> e instanceof Player)
.map(e -> ((Player) e))
.collect(Collectors.toList());
} catch (IllegalArgumentException e) {
Logging.warning("An error occurred while parsing selector '%s' for %s. Is it is the correct format?",
playerSelector, sender.getName());
e.printStackTrace();
return null;
return Try.success(Collections.emptyList());
}
return Try.of(() -> Bukkit.selectEntities(sender, playerSelector).stream()
.filter(e -> e instanceof Player)
.map(e -> ((Player) e))
.collect(Collectors.toList()));
}
}
@@ -1,5 +1,7 @@
package org.mvplugins.multiverse.core.utils;
import org.jetbrains.annotations.ApiStatus;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
@@ -16,7 +18,17 @@ public class REPatterns {
public static final Pattern COMMA = get(",");
public static final Pattern DOT = get("\\.");
public static final Pattern EQUALS = get("=");
@ApiStatus.AvailableSince("5.4")
public static final Pattern HYPHEN = get("-");
@ApiStatus.AvailableSince("5.4")
public static final Pattern NAMESPACE = get("^[a-z0-9_.-]+$");
@ApiStatus.AvailableSince("5.4")
public static final Pattern NAMESPACE_KEY = get("^[a-z0-9/_.-]+$");
public static final Pattern SEMICOLON = get(";");
@ApiStatus.AvailableSince("5.4")
public static final Pattern SPACE = get(" ");
public static final Pattern UNDERSCORE = get("_");
public static final Pattern UUID = get("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
@ApiStatus.AvailableSince("5.4")
public static final Pattern YAML_KEY = get("^[a-zA-Z0-9\\[\\]_-]+$");
}
@@ -1,6 +1,8 @@
package org.mvplugins.multiverse.core.utils.matcher;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashSet;
@@ -34,7 +36,7 @@ public class ExactStringMatcher implements StringMatcher {
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public ExactStringMatcher(String exactMatch) {
public ExactStringMatcher(@NotNull String exactMatch) {
this.exactMatches = new HashSet<>();
this.exactMatches.add(exactMatch);
}
@@ -47,7 +49,7 @@ public class ExactStringMatcher implements StringMatcher {
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public ExactStringMatcher(Collection<String> exactMatches) {
public ExactStringMatcher(@NotNull Collection<String> exactMatches) {
this.exactMatches = new HashSet<>(exactMatches);
}
@@ -59,7 +61,7 @@ public class ExactStringMatcher implements StringMatcher {
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public void addExactMatch(String value) {
public void addExactMatch(@NotNull String value) {
this.exactMatches.add(value);
}
@@ -67,7 +69,7 @@ public class ExactStringMatcher implements StringMatcher {
* {@inheritDoc}
*/
@Override
public boolean matches(String value) {
public boolean matches(@Nullable String value) {
return exactMatches.contains(value);
}
}
@@ -2,6 +2,8 @@ package org.mvplugins.multiverse.core.utils.matcher;
import com.dumptruckman.minecraft.util.Logging;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
@@ -38,7 +40,7 @@ public class MatcherGroup implements StringMatcher {
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public MatcherGroup(Collection<String> matchStrings) {
public MatcherGroup(@NotNull Collection<String> matchStrings) {
this();
for (String matchString : matchStrings) {
addMatcher(matchString);
@@ -53,16 +55,18 @@ public class MatcherGroup implements StringMatcher {
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public void addMatcher(String matchString) {
public void addMatcher(@Nullable String matchString) {
if (matchString == null || matchString.isEmpty()) {
return;
}
if (isExact(matchString)) {
Logging.warning("Exact: " + matchString);
exactMatcher.addExactMatch(matchString);
} else {
stringMatchers.add(StringMatcher.fromString(matchString));
}
}
private boolean isExact(String matcherString) {
private boolean isExact(@NotNull String matcherString) {
return !matcherString.contains("*") && !matcherString.startsWith("r=");
}
@@ -74,7 +78,7 @@ public class MatcherGroup implements StringMatcher {
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public void addMatcher(StringMatcher matcher) {
public void addMatcher(@NotNull StringMatcher matcher) {
stringMatchers.add(matcher);
}
@@ -82,7 +86,7 @@ public class MatcherGroup implements StringMatcher {
* {@inheritDoc}
*/
@Override
public boolean matches(String value) {
public boolean matches(@Nullable String value) {
if (exactMatcher.matches(value)) {
return true;
}
@@ -1,6 +1,10 @@
package org.mvplugins.multiverse.core.utils.matcher;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.regex.Pattern;
@@ -12,8 +16,8 @@ import java.util.regex.Pattern;
*/
@ApiStatus.AvailableSince("5.2")
public class RegexStringMatcher implements StringMatcher {
private final String regexString;
private final Pattern regexPattern;
private final @NotNull String regexString;
private final @Nullable Pattern regexPattern;
/**
* Creates a new RegexStringMatcher with a regex string. 'r=' prefix will be stripped if present.
@@ -23,7 +27,7 @@ public class RegexStringMatcher implements StringMatcher {
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public RegexStringMatcher(String regexString) {
public RegexStringMatcher(@NotNull String regexString) {
this.regexString = regexString;
this.regexPattern = compileRegex(regexString);
}
@@ -32,14 +36,22 @@ public class RegexStringMatcher implements StringMatcher {
if (regexString.startsWith("r=")) {
regexString = regexString.substring(2);
}
return Pattern.compile(regexString);
String finalRegexString = regexString;
return Try.of(() -> Pattern.compile(finalRegexString))
.onFailure(ex -> Logging.warning("Failed to compile regex '%s': %s",
finalRegexString, ex.getMessage()))
.getOrNull();
}
/**
* {@inheritDoc}
*/
@Override
public boolean matches(String value) {
public boolean matches(@Nullable String value) {
if (regexPattern == null || value == null) {
return false;
}
return regexPattern.matcher(value).matches();
}
}
@@ -1,6 +1,8 @@
package org.mvplugins.multiverse.core.utils.matcher;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.stream.Collectors;
@@ -26,7 +28,7 @@ public interface StringMatcher {
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
static StringMatcher fromString(String matcherString) {
static @NotNull StringMatcher fromString(@NotNull String matcherString) {
if (matcherString.startsWith("r=")) {
return new RegexStringMatcher(matcherString);
} else if (matcherString.contains("*")) {
@@ -45,7 +47,7 @@ public interface StringMatcher {
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
boolean matches(String value);
boolean matches(@Nullable String value);
/**
* Filters a list of strings, returning only those that match the pattern defined by this StringMatcher.
@@ -57,7 +59,7 @@ public interface StringMatcher {
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
default List<String> filter(List<String> values) {
default @NotNull List<String> filter(@NotNull List<String> values) {
return values.stream()
.filter(this::matches)
.collect(Collectors.toList());
@@ -1,6 +1,10 @@
package org.mvplugins.multiverse.core.utils.matcher;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.regex.Pattern;
@@ -25,16 +29,22 @@ public class WildcardStringMatcher implements StringMatcher {
* @param wildcard the wildcard string to match against.
*/
@ApiStatus.AvailableSince("5.2")
public WildcardStringMatcher(String wildcard) {
public WildcardStringMatcher(@NotNull String wildcard) {
this.wildcard = wildcard;
this.pattern = Pattern.compile(("\\Q" + wildcard + "\\E").replace("*", "\\E.*\\Q"));
this.pattern = Try.of(() -> Pattern.compile(("\\Q" + wildcard + "\\E").replace("*", "\\E.*\\Q")))
.onFailure(ex -> Logging.warning("Failed to compile wildcard '%s': %s",
wildcard, ex.getMessage()))
.getOrNull();
}
/**
* {@inheritDoc}
*/
@Override
public boolean matches(String value) {
public boolean matches(@Nullable String value) {
if (pattern == null || value == null) {
return false;
}
return pattern.matcher(value).matches();
}
}
@@ -0,0 +1,150 @@
package org.mvplugins.multiverse.core.utils.position;
import org.bukkit.Location;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.utils.REPatterns;
/**
* Represents a position for an entity in 3D space, including both coordinates and facing direction.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public class EntityPosition {
/**
* Creates an EntityPosition with absolute coordinates and direction.
*
* @param x The absolute X coordinate.
* @param y The absolute Y coordinate.
* @param z The absolute Z coordinate.
* @param pitch The absolute pitch (vertical angle).
* @param yaw The absolute yaw (horizontal angle).
* @return A new EntityPosition instance with the specified absolute coordinates and direction.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static EntityPosition ofAbsolute(double x, double y, double z, double pitch, double yaw) {
return new EntityPosition(
VectorPosition.ofAbsolute(x, y, z),
FaceDirection.ofAbsolute(pitch, yaw)
);
}
/**
* Creates an EntityPosition from a Bukkit Location with absolute coordinates and direction.
*
* @param location The Bukkit Location to convert.
* @return A new EntityPosition instance representing the given location.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static EntityPosition ofLocation(Location location) {
return new EntityPosition(VectorPosition.ofLocation(location), FaceDirection.ofLocation(location));
}
/**
* Parses an EntityPosition from a string representation.
* The expected format is "&lt;x&gt;,&lt;y&gt;,&lt;z&gt;:&lt;pitch&gt;:&lt;yaw&gt;" for absolute coordinates and direction,
* or "&lt;x&gt;,&lt;y&gt;,&lt;z&gt;" for absolute coordinates with default direction (0 pitch, 0 yaw).
* <br>
* Relative coordinates and direction can be specified using the '~' prefix, e.g., "~10,~,~-10:0:90".
*
* @param positionStr The string representation of the position.
* @return A new EntityPosition instance parsed from the string.
* @throws PositionParseException If the string format is invalid.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static EntityPosition fromString(String positionStr) throws PositionParseException {
String[] parts = REPatterns.COLON.split(positionStr, 2);
return parts.length == 2
? new EntityPosition(VectorPosition.fromString(parts[0]), FaceDirection.fromString(parts[1]))
: new EntityPosition(VectorPosition.fromString(parts[0]), FaceDirection.ofAbsolute(0, 0));
}
private final VectorPosition vector;
private final FaceDirection direction;
/**
* Creates a new EntityPosition with the specified vector and direction.
*
* @param vector The vector position (coordinates).
* @param direction The facing direction (pitch and yaw).
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public EntityPosition(VectorPosition vector, FaceDirection direction) {
this.vector = vector;
this.direction = direction;
}
/**
* Gets the vector position (coordinates).
*
* @return The vector position.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public VectorPosition getVector() {
return vector;
}
/**
* Gets the facing direction (pitch and yaw).
*
* @return The facing direction.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public FaceDirection getDirection() {
return direction;
}
/**
* Augments a given Bukkit Location by applying the relative components of this EntityPosition.
* This modifies the provided Location in place.
*
* @param base The base Bukkit Location to augment and offset for relative positioning as required.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public void augmentBukkitLocation(Location base) {
vector.augmentBukkitLocation(base);
direction.augmentBukkitLocation(base);
}
/**
* Converts this EntityPosition to a new Bukkit Location based on a given base Location.
* This does not modify the base Location, but returns a new Location instance.
*
* @param base The base Bukkit Location to use as a reference for relative positioning as required.
* @return A new Bukkit Location representing this EntityPosition.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public Location toBukkitLocation(Location base) {
return new Location(
base.getWorld(),
vector.getX().getValue(base.getX()),
vector.getY().getValue(base.getY()),
vector.getZ().getValue(base.getZ()),
(float) direction.getYaw().getValue(base.getYaw()),
(float) direction.getPitch().getValue(base.getPitch())
);
}
@Override
public String toString() {
return vector + ":" + direction;
}
}
@@ -0,0 +1,138 @@
package org.mvplugins.multiverse.core.utils.position;
import org.bukkit.Location;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.REPatterns;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Represents a direction to face, defined by pitch and yaw.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public class FaceDirection {
/**
* Creates a FaceDirection with absolute pitch and yaw.
*
* @param pitch The absolute pitch (vertical angle).
* @param yaw The absolute yaw (horizontal angle).
* @return A new FaceDirection instance with the specified absolute pitch and yaw.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static FaceDirection ofAbsolute(double pitch, double yaw) {
return new FaceDirection(
PositionNumber.ofAbsolute(pitch),
PositionNumber.ofAbsolute(yaw)
);
}
/**
* Gets the pitch and yaw from a Bukkit Location to create an FaceDirection with absolute values.
*
* @param location The Bukkit Location to convert.
* @return A new FaceDirection instance representing the direction of the given location.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static FaceDirection ofLocation(org.bukkit.Location location) {
return new FaceDirection(
PositionNumber.ofAbsolute(location.getPitch()),
PositionNumber.ofAbsolute(location.getYaw())
);
}
/**
* Parses a FaceDirection from a string representation.
* The expected format is "&lt;pitch&gt;:&lt;yaw&gt;" for absolute pitch and yaw.
* <br>
* Relative pitch and yaw can be specified using the '~' prefix, e.g., "~:~5".
*
* @param directionStr The string representation of the direction.
* @return A new FaceDirection instance parsed from the string.
* @throws PositionParseException If the string format is invalid.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static FaceDirection fromString(String directionStr) throws PositionParseException {
String[] parts = REPatterns.COLON.split(directionStr, 2);
if (parts.length != 2) {
throw new PositionParseException(Message.of(MVCorei18n.EXCEPTION_POSITIONPARSE_INVALIDDIRECTION,
replace("{format}").with(directionStr)));
}
//TODO: Add support for compass directions (N, S, E, W, NE, NW, SE, SW) for yaw
PositionNumber pitch = PositionNumber.fromString(parts[0]);
PositionNumber yaw = PositionNumber.fromString(parts[1]);
return new FaceDirection(pitch, yaw);
}
private final PositionNumber pitch;
private final PositionNumber yaw;
/**
* Creates a new FaceDirection with the specified pitch and yaw.
*
* @param pitch The pitch (vertical angle).
* @param yaw The yaw (horizontal angle).
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public FaceDirection(PositionNumber pitch, PositionNumber yaw) {
this.pitch = pitch;
this.yaw = yaw;
}
/**
* Gets the pitch component of this FaceDirection.
*
* @return The pitch number representation.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getPitch() {
return pitch;
}
/**
* Gets the yaw component of this FaceDirection.
*
* @return The yaw number representation.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getYaw() {
return yaw;
}
/**
* Applies this FaceDirection to a given Bukkit Location, modifying its pitch and yaw.
* <br>
* Relative pitch and yaw will be offset based on the current values in the Location.
*
* @param base The Bukkit Location to modify.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public void augmentBukkitLocation(Location base) {
base.setPitch((float) pitch.getValue(base.getPitch()));
base.setYaw((float) yaw.getValue(base.getYaw()));
}
@Override
public String toString() {
return pitch + ":" + yaw;
}
}
@@ -0,0 +1,187 @@
package org.mvplugins.multiverse.core.utils.position;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Represents a number that can be either absolute or relative (prefixed with '~').
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public sealed interface PositionNumber permits PositionNumber.Relative, PositionNumber.Absolute {
/**
* Creates an absolute PositionNumber. Absolute numbers represent fixed coordinates that will not be adjusted
* based on any base value.
*
* @param value the absolute value
* @return a PositionNumber representing the absolute value
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
static PositionNumber ofAbsolute(double value) {
return new Absolute(value);
}
/**
* Creates a relative PositionNumber. Relative numbers are prefixed with '~' and represent an offset
* from a base value. For example, a relative PositionNumber of '~5' means 5 units more than the base value,
* while '~ -3' means 3 units less than the base value.
*
* @param value the relative offset value
* @return a PositionNumber representing the relative offset
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
static PositionNumber ofRelative(double value) {
return new Relative(value);
}
/**
* Parses a PositionNumber from a string. The string can represent either an absolute number (e.g., "10.5")
* or a relative number prefixed with '~' (e.g., "~5" or "~ -3"). If the string is "~" with no number,
* it is treated as a relative value of 0.
*
* @param string the string to parse
* @return a PositionNumber representing the parsed value.
* @throws PositionParseException If the string format is invalid.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
static PositionNumber fromString(String string) throws PositionParseException {
if (string.startsWith("~")) {
if (string.length() == 1) {
return new Relative(0);
}
return new Relative(tryParseDouble(string.substring(1)));
}
return new Absolute(tryParseDouble(string));
}
private static double tryParseDouble(String str) throws PositionParseException {
return Try.of(() -> Double.parseDouble(str))
.getOrElseThrow(throwable -> new PositionParseException(
Message.of(MVCorei18n.EXCEPTION_POSITIONPARSE_INVALIDNUMBER,
replace("{number}").with(str))));
}
/**
* Calculates the effective value based on the base value.
* For absolute PositionNumbers, this returns the stored value.
* For relative PositionNumbers, this returns base + stored value.
*
* @param base the base value to use for relative calculations
* @return the calculated effective value
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
double getValue(double base);
/**
* Checks if this PositionNumber is relative.
*
* @return true if this is a relative PositionNumber, false if absolute
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
boolean isRelative();
/**
* Checks if this PositionNumber is absolute.
*
* @return true if this is an absolute PositionNumber, false if relative
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
boolean isAbsolute();
/**
* Gets the raw stored value without any base adjustment.
*
* @return the raw value (for relative, this is the offset; for absolute, this is the fixed value)
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
double getRawValue();
final class Relative implements PositionNumber {
private final double value;
Relative(double value) {
this.value = value;
}
@Override
public double getValue(double base) {
return base + value;
}
@Override
public boolean isRelative() {
return true;
}
@Override
public boolean isAbsolute() {
return false;
}
@Override
public double getRawValue() {
return value;
}
@Override
public String toString() {
return "~" + value;
}
}
final class Absolute implements PositionNumber {
private final double value;
Absolute(double value) {
this.value = value;
}
@Override
public double getValue(double base) {
return value;
}
@Override
public boolean isRelative() {
return false;
}
@Override
public boolean isAbsolute() {
return true;
}
@Override
public double getRawValue() {
return value;
}
@Override
public String toString() {
return Double.toString(value);
}
}
}
@@ -0,0 +1,227 @@
package org.mvplugins.multiverse.core.utils.position;
import org.bukkit.Location;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.REPatterns;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Represents an x, y, z position in 3D space, with support for absolute and relative coordinates.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public class VectorPosition {
/**
* Creates a VectorPosition with absolute coordinates.
* @param x The absolute x coordinate.
* @param y The absolute y coordinate.
* @param z The absolute z coordinate.
* @return A new VectorPosition instance with the specified absolute coordinates.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static VectorPosition ofAbsolute(double x, double y, double z) {
return new VectorPosition(
PositionNumber.ofAbsolute(x),
PositionNumber.ofAbsolute(y),
PositionNumber.ofAbsolute(z)
);
}
/**
* Creates a VectorPosition from a Bukkit Vector, using absolute coordinates.
*
* @param vector The Bukkit Vector to convert.
* @return A new VectorPosition instance representing the given vector.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static VectorPosition ofVector(Vector vector) {
return new VectorPosition(
PositionNumber.ofAbsolute(vector.getX()),
PositionNumber.ofAbsolute(vector.getY()),
PositionNumber.ofAbsolute(vector.getZ())
);
}
/**
* Creates a VectorPosition from a Bukkit Location, using absolute coordinates.
*
* @param location The Bukkit Location to convert.
* @return A new VectorPosition instance representing the given location.
*
* @since 5.3
*/
public static VectorPosition ofLocation(Location location) {
return new VectorPosition(
PositionNumber.ofAbsolute(location.getX()),
PositionNumber.ofAbsolute(location.getY()),
PositionNumber.ofAbsolute(location.getZ())
);
}
/**
* Parses a VectorPosition from a string representation.
* The expected format is "&lt;x&gt;,&lt;y&gt;,&lt;z&gt;" for absolute or relative coordinates.
* <br>
* Relative coordinates can be specified using the '~' prefix, e.g., "~10,~,~-10".
*
* @param coordStr The string representation of the coordinates.
* @return A new VectorPosition instance parsed from the string.
* @throws PositionParseException If the string format is invalid.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static VectorPosition fromString(String coordStr) throws PositionParseException {
String[] parts = REPatterns.COMMA.split(coordStr);
if (parts.length != 3) {
throw new PositionParseException(Message.of(MVCorei18n.EXCEPTION_POSITIONPARSE_INVALIDCOORDINATES,
replace("{format}").with(coordStr)));
}
return new VectorPosition(
PositionNumber.fromString(parts[0]),
PositionNumber.fromString(parts[1]),
PositionNumber.fromString(parts[2])
);
}
private final PositionNumber x;
private final PositionNumber y;
private final PositionNumber z;
/**
* Creates a new VectorPosition with the specified PositionNumbers for x, y, and z.
*
* @param x The PositionNumber for the x coordinate.
* @param y The PositionNumber for the y coordinate.
* @param z The PositionNumber for the z coordinate.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public VectorPosition(PositionNumber x, PositionNumber y, PositionNumber z) {
this.x = x;
this.y = y;
this.z = z;
}
/**
* Gets the PositionNumber for the x coordinate.
*
* @return The PositionNumber representing the x coordinate.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getX() {
return x;
}
/**
* Gets the PositionNumber for the y coordinate.
*
* @return The PositionNumber representing the y coordinate.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getY() {
return y;
}
/**
* Gets the PositionNumber for the z coordinate.
*
* @return The PositionNumber representing the z coordinate.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getZ() {
return z;
}
/**
* Augments the given Bukkit Vector in place by applying this VectorPosition's coordinates.
* Relative coordinates will adjust the existing values, while absolute coordinates will set them directly.
*
* @param base The Bukkit Vector to augment.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public void augmentBukkitVector(Vector base) {
base.setX(x.getValue(base.getX()));
base.setY(y.getValue(base.getY()));
base.setZ(z.getValue(base.getZ()));
}
/**
* Augments the given Bukkit Location in place by applying this VectorPosition's coordinates.
* Relative coordinates will adjust the existing values, while absolute coordinates will set them directly.
*
* @param location The Bukkit Location to augment.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public void augmentBukkitLocation(Location location) {
location.setX(x.getValue(location.getX()));
location.setY(y.getValue(location.getY()));
location.setZ(z.getValue(location.getZ()));
}
/**
* Converts this VectorPosition to a new Bukkit Vector based on a given base Vector.
* This does not modify the base Vector, but returns a new Vector instance.
*
* @param base The base Bukkit Vector to use as a reference for relative positioning as required.
* @return A new Bukkit Vector representing this VectorPosition.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public Vector toBukkitVector(Vector base) {
return new Vector(
x.getValue(base.getX()),
y.getValue(base.getY()),
z.getValue(base.getZ())
);
}
/**
* Converts this VectorPosition to a new Bukkit Location based on a given base Location.
* This does not modify the base Location, but returns a new Location instance.
*
* @param base The base Bukkit Location to use as a reference for relative positioning as required.
* @return A new Bukkit Location representing this VectorPosition.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public Location toBukkitLocation(Location base) {
return new Location(
base.getWorld(),
x.getValue(base.getX()),
y.getValue(base.getY()),
z.getValue(base.getZ()),
base.getYaw(),
base.getPitch()
);
}
@Override
public String toString() {
return x + "," + y + "," + z;
}
}
@@ -107,18 +107,14 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
*
* @return Whether this attempt is a success.
*/
default boolean isSuccess() {
return this instanceof Success;
}
boolean isSuccess();
/**
* Returns whether this attempt is a failure.
*
* @return Whether this attempt is a failure.
*/
default boolean isFailure() {
return this instanceof Failure;
}
boolean isFailure();
/**
* Converts this {@link Attempt} instance to an equivalent {@link Try} representation. Defaults to a
@@ -143,19 +139,25 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
@ApiStatus.AvailableSince("5.1")
Try<T> toTry(Function<Failure<T, F>, Throwable> throwableFunction);
/**
* Runs the given runnable after this attempt regardless of success or failure.
*
* @param runnable The runnable.
* @return This attempt.
*/
default Attempt<T, F> thenRun(Runnable runnable) {
runnable.run();
return this;
}
default Attempt<T, F> thenAccept(Consumer<Either<T, F>> consumer) {
if (this instanceof Success) {
consumer.accept(Either.left(get()));
} else {
consumer.accept(Either.right(getFailureReason()));
}
return this;
}
/**
* Accepts either the value or the failure reason depending on the result type.
* This will run regardless of success or failure.
*
* @param consumer The consumer with either the value or the failure reason.
* @return This attempt.
*/
Attempt<T, F> thenAccept(Consumer<Either<T, F>> consumer);
/**
* Peeks at the value if this is a success attempt.
@@ -163,12 +165,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param consumer The consumer with the value.
* @return This attempt.
*/
default Attempt<T, F> peek(Consumer<T> consumer) {
if (this instanceof Success) {
consumer.accept(get());
}
return this;
}
Attempt<T, F> peek(Consumer<T> consumer);
/**
* Maps the value to another value if this is a success attempt.
@@ -177,13 +174,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <U> The type of the new value.
* @return The new attempt.
*/
default <U> Attempt<U, F> map(Function<? super T, ? extends U> mapper) {
if (this instanceof Success) {
return new Success<>(mapper.apply(get()));
} else {
return new Failure<>((Failure<T, F>) this);
}
}
<U> Attempt<U, F> map(Function<? super T, ? extends U> mapper);
/**
* Maps the value to another attempt if this is a success attempt.
@@ -192,13 +183,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <U> The type of the new value.
* @return The new attempt.
*/
default <U> Attempt<U, F> map(Supplier<? extends U> mapper) {
if (this instanceof Success) {
return new Success<>(mapper.get());
} else {
return new Failure<>((Failure<T, F>) this);
}
}
<U> Attempt<U, F> map(Supplier<? extends U> mapper);
/**
* Maps the value to another attempt with the same fail reason if this is a success attempt.
@@ -207,13 +192,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <U> The type of the new value.
* @return The new attempt.
*/
default <U> Attempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper) {
if (this instanceof Success) {
return mapper.apply(get());
} else {
return new Failure<>((Failure<T, F>) this);
}
}
<U> Attempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper);
/**
* Maps the value to another attempt with the same fail reason if this is a success attempt.
@@ -222,13 +201,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <U> The type of the new value.
* @return The new attempt.
*/
default <U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper) {
if (this instanceof Success) {
return mapper.get();
} else {
return new Failure<>((Failure<T, F>) this);
}
}
<U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper);
/**
* Maps to another attempt with a different fail reason.
@@ -237,13 +210,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <UF> The type of the new fail reason.
* @return The new attempt.
*/
default <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
if (this instanceof Success) {
return new Success<>(get());
} else {
return new Failure<>(failureReason, getFailureMessage(), (Failure<T, F>) this);
}
}
<UF extends FailureReason> Attempt<T, UF> transform(UF failureReason);
/**
* Maps attempt result to another value.
@@ -256,13 +223,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
default <U> U transform(Function<T, U> successMapper, Function<F, U> failureMapper) {
if (this instanceof Success) {
return successMapper.apply(get());
} else {
return failureMapper.apply(getFailureReason());
}
}
<U> U transform(Function<T, U> successMapper, Function<F, U> failureMapper);
/**
* Calls either the failure or success function depending on the result type.
@@ -272,13 +233,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <N> The type of the new value.
* @return The result of the function.
*/
default <N> N fold(Function<Failure<T, F>, N> failureMapper, Function<T, N> successMapper) {
if (this instanceof Success) {
return successMapper.apply(get());
} else {
return failureMapper.apply((Failure<T, F>) this);
}
}
<N> N fold(Function<Failure<T, F>, N> failureMapper, Function<T, N> successMapper);
/**
* Calls the given runnable if this is a success attempt.
@@ -286,12 +241,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param runnable The runnable.
* @return This attempt.
*/
default Attempt<T, F> onSuccess(Runnable runnable) {
if (this instanceof Success) {
runnable.run();
}
return this;
}
Attempt<T, F> onSuccess(Runnable runnable);
/**
* Calls the given consumer if this is a success attempt.
@@ -299,12 +249,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param consumer The consumer with the value.
* @return This attempt.
*/
default Attempt<T, F> onSuccess(Consumer<T> consumer) {
if (this instanceof Success) {
consumer.accept(get());
}
return this;
}
Attempt<T, F> onSuccess(Consumer<T> consumer);
/**
* Calls the given consumer if this is a failure attempt.
@@ -312,12 +257,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param runnable The runnable.
* @return This attempt.
*/
default Attempt<T, F> onFailure(Runnable runnable) {
if (this instanceof Failure) {
runnable.run();
}
return this;
}
Attempt<T, F> onFailure(Runnable runnable);
/**
* Calls the given consumer if this is a failure attempt.
@@ -325,12 +265,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param consumer The consumer with the failure instance.
* @return This attempt.
*/
default Attempt<T, F> onFailure(Consumer<Failure<T, F>> consumer) {
if (this instanceof Failure) {
consumer.accept((Failure<T, F>) this);
}
return this;
}
Attempt<T, F> onFailure(Consumer<Failure<T, F>> consumer);
/**
* Calls the given runnable if this is a failure attempt.
@@ -338,12 +273,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param consumer The consumer with the failure reason.
* @return This attempt.
*/
default Attempt<T, F> onFailureReason(Consumer<F> consumer) {
if (this instanceof Failure) {
consumer.accept(getFailureReason());
}
return this;
}
Attempt<T, F> onFailureReason(Consumer<F> consumer);
/**
* Represents a successful attempt with a value.
@@ -378,6 +308,26 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
return value;
}
@Override
public F getFailureReason() {
throw new UnsupportedOperationException("No failure reason as attempt is a success");
}
@Override
public Message getFailureMessage() {
throw new UnsupportedOperationException("No failure message as attempt is a success");
}
@Override
public boolean isSuccess() {
return true;
}
@Override
public boolean isFailure() {
return false;
}
@Override
public Try<T> toTry() {
return Try.success(value);
@@ -389,13 +339,83 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
}
@Override
public F getFailureReason() {
throw new UnsupportedOperationException("No failure reason as attempt is a success");
public Attempt<T, F> thenAccept(Consumer<Either<T, F>> consumer) {
consumer.accept(Either.left(value));
return this;
}
@Override
public Message getFailureMessage() {
throw new UnsupportedOperationException("No failure message as attempt is a success");
public Attempt<T, F> peek(Consumer<T> consumer) {
consumer.accept(value);
return this;
}
@Override
public <U> Attempt<U, F> map(Function<? super T, ? extends U> mapper) {
return new Success<>(mapper.apply(value));
}
@Override
public <U> Attempt<U, F> map(Supplier<? extends U> mapper) {
return new Success<>(mapper.get());
}
@Override
public <U> Attempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper) {
return mapper.apply(value);
}
@Override
public <U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper) {
return mapper.get();
}
@Override
public <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
return changeFailureType();
}
@Override
public <U> U transform(Function<T, U> successMapper, Function<F, U> failureMapper) {
return successMapper.apply(value);
}
@Override
public <N> N fold(Function<Failure<T, F>, N> failureMapper, Function<T, N> successMapper) {
return successMapper.apply(value);
}
@Override
public Attempt<T, F> onSuccess(Runnable runnable) {
runnable.run();
return this;
}
@Override
public Attempt<T, F> onSuccess(Consumer<T> consumer) {
consumer.accept(value);
return this;
}
@Override
public Attempt<T, F> onFailure(Runnable runnable) {
return this;
}
@Override
public Attempt<T, F> onFailure(Consumer<Failure<T, F>> consumer) {
return this;
}
@Override
public Attempt<T, F> onFailureReason(Consumer<F> consumer) {
return this;
}
private <UF extends FailureReason> Attempt<T, UF> changeFailureType() {
@SuppressWarnings("unchecked")
Attempt<T, UF> mappedSuccess = (Attempt<T, UF>) this;
return mappedSuccess;
}
@Override
@@ -451,6 +471,26 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
throw exceptionSupplier.apply(this);
}
@Override
public F getFailureReason() {
return failureReason;
}
@Override
public Message getFailureMessage() {
return message;
}
@Override
public boolean isSuccess() {
return false;
}
@Override
public boolean isFailure() {
return true;
}
@Override
public Try<T> toTry() {
return Try.failure(new MultiverseException(message));
@@ -462,13 +502,83 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
}
@Override
public F getFailureReason() {
return failureReason;
public Attempt<T, F> thenAccept(Consumer<Either<T, F>> consumer) {
consumer.accept(Either.right(failureReason));
return this;
}
@Override
public Message getFailureMessage() {
return message;
public Attempt<T, F> peek(Consumer<T> consumer) {
return this;
}
@Override
public <U> Attempt<U, F> map(Function<? super T, ? extends U> mapper) {
return changeValueType();
}
@Override
public <U> Attempt<U, F> map(Supplier<? extends U> mapper) {
return changeValueType();
}
@Override
public <U> Attempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper) {
return changeValueType();
}
@Override
public <U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper) {
return changeValueType();
}
@Override
public <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
return new Failure<>(failureReason, getFailureMessage(), this);
}
@Override
public <U> U transform(Function<T, U> successMapper, Function<F, U> failureMapper) {
return failureMapper.apply(failureReason);
}
@Override
public <N> N fold(Function<Failure<T, F>, N> failureMapper, Function<T, N> successMapper) {
return failureMapper.apply(this);
}
@Override
public Attempt<T, F> onSuccess(Runnable runnable) {
return this;
}
@Override
public Attempt<T, F> onSuccess(Consumer<T> consumer) {
return this;
}
@Override
public Attempt<T, F> onFailure(Runnable runnable) {
runnable.run();
return this;
}
@Override
public Attempt<T, F> onFailure(Consumer<Failure<T, F>> consumer) {
consumer.accept(this);
return this;
}
@Override
public Attempt<T, F> onFailureReason(Consumer<F> consumer) {
consumer.accept(failureReason);
return this;
}
private <U> Attempt<U, F> changeValueType() {
@SuppressWarnings("unchecked")
Attempt<U, F> mappedFailure = (Attempt<U, F>) this;
return mappedFailure;
}
@Override
@@ -18,7 +18,8 @@ public final class ChatTextFormatter {
private static final TextFormatter wrapper;
static {
if (ReflectHelper.hasClass("net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer")) {
if (ReflectHelper.hasClass("net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer")
&& ReflectHelper.hasClass("net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer")) {
wrapper = new AdventureTextFormatter();
} else {
wrapper = new ChatColorTextFormatter();
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.world;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
import com.dumptruckman.minecraft.util.Logging;
@@ -129,7 +130,9 @@ final class WorldConfig {
}
Try<Void> save() {
return configHandle.save();
return configHandle.save().onFailure(ex ->
Logging.warning("Failed to save world config for world '%s'. %s",
worldName, ex.getLocalizedMessage()));
}
ConfigurationSection getConfigurationSection() {
@@ -417,7 +420,7 @@ final class WorldConfig {
if (alias.get().isEmpty()) return;
Try.of(() -> Enum.valueOf(EnglishChatColor.class, color.toUpperCase()))
Try.of(() -> Enum.valueOf(EnglishChatColor.class, color.toUpperCase(Locale.ENGLISH)))
.map(c -> c.color)
.onSuccess(c -> {
if (c != ChatColor.WHITE) {
@@ -425,7 +428,7 @@ final class WorldConfig {
}
});
Try.of(() -> Enum.valueOf(EnglishChatStyle.class, style.toUpperCase()))
Try.of(() -> Enum.valueOf(EnglishChatStyle.class, style.toUpperCase(Locale.ENGLISH)))
.map(c -> c.color)
.onSuccess(s -> {
if (s != null) {
@@ -16,6 +16,7 @@ import org.bukkit.configuration.MemoryConfiguration;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
import org.mvplugins.multiverse.core.event.world.MVWorldPropertyChangedEvent;
import org.mvplugins.multiverse.core.config.node.ConfigNode;
@@ -37,11 +38,13 @@ final class WorldConfigNodes {
private final NodeGroup nodes = new NodeGroup();
private WorldManager worldManager;
private EnforcementHandler enforcementHandler;
private CoreConfig config;
private MultiverseWorld world = null;
WorldConfigNodes(@NotNull MultiverseCore multiverseCore) {
this.worldManager = multiverseCore.getServiceLocator().getService(WorldManager.class);
this.enforcementHandler = multiverseCore.getServiceLocator().getService(EnforcementHandler.class);
this.config = multiverseCore.getServiceLocator().getService(CoreConfig.class);
}
MultiverseWorld getWorld() {
@@ -57,11 +60,15 @@ final class WorldConfigNodes {
}
private <T> ConfigNode<T> node(ConfigNode.Builder<T, ?> nodeBuilder) {
nodeBuilder.onSetValue((oldValue, newValue) -> {
nodeBuilder.onChange((oldValue, newValue) -> {
if (Objects.equals(oldValue, newValue)) return;
if (world == null) return;
MVWorldPropertyChangedEvent<?> mvWorldPropertyChangeEvent = new MVWorldPropertyChangedEvent<>(
world, Option.of(nodeBuilder.name()).getOrElse(nodeBuilder.path()), oldValue, newValue);
world,
Option.of(nodeBuilder.name()).getOrElse(nodeBuilder.path()),
oldValue,
newValue
);
Bukkit.getPluginManager().callEvent(mvWorldPropertyChangeEvent);
Logging.finer("MVWorldPropertyChangeEvent fired for world '%s' with name '%s' and value '%s'",
world.getName(), nodeBuilder.path(), newValue);
@@ -80,7 +87,7 @@ final class WorldConfigNodes {
final ConfigNode<String> alias = node(ConfigNode.builder("alias", String.class)
.defaultValue("")
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (world == null) return;
world.updateColourlessAlias();
}));
@@ -90,14 +97,14 @@ final class WorldConfigNodes {
final ConfigNode<Boolean> allowFlight = node(ConfigNode.builder("allow-flight", Boolean.class)
.defaultValue(false)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
enforcementHandler.handleAllFlightEnforcement(loadedWorld);
}));
final ConfigNode<Boolean> allowWeather = node(ConfigNode.builder("allow-weather", Boolean.class)
.defaultValue(true)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(world -> {
if (!world.isClearWeather() && !newValue) {
@@ -125,7 +132,7 @@ final class WorldConfigNodes {
final ConfigNode<Difficulty> difficulty = node(ConfigNode.builder("difficulty", Difficulty.class)
.defaultValue(Difficulty.NORMAL)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.setDifficulty(newValue));
}));
@@ -172,7 +179,7 @@ final class WorldConfigNodes {
final ConfigNode<GameMode> gamemode = node(ConfigNode.builder("gamemode", GameMode.class)
.defaultValue(GameMode.SURVIVAL)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
enforcementHandler.handleAllGameModeEnforcement(loadedWorld);
}));
@@ -191,7 +198,7 @@ final class WorldConfigNodes {
final ConfigNode<Boolean> keepSpawnInMemory = node(ConfigNode
.builder("keep-spawn-in-memory", Boolean.class)
.defaultValue(true)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.setKeepSpawnInMemory(newValue));
}));
@@ -205,7 +212,7 @@ final class WorldConfigNodes {
final ConfigNode<Boolean> pvp = node(ConfigNode.builder("pvp", Boolean.class)
.defaultValue(true)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.setPVP(newValue));
}));
@@ -234,17 +241,17 @@ final class WorldConfigNodes {
final ConfigNode<SpawnLocation> spawnLocation = node(ConfigNode.builder("spawn-location", SpawnLocation.class)
.defaultValue(NullSpawnLocation.get())
.hidden()
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
if (newValue == null || newValue instanceof NullSpawnLocation) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> {
bukkitWorld.setSpawnLocation(newValue.getBlockX(), newValue.getBlockY(), newValue.getBlockZ());
newValue.setWorld(bukkitWorld);
bukkitWorld.setSpawnLocation(newValue);
});
}));
final ConfigNode<EntitySpawnConfig> enititySpawnConfig = node(ConfigNode.builder("spawning", EntitySpawnConfig.class)
.defaultValue(() -> EntitySpawnConfig.fromSection(new MemoryConfiguration()))
.defaultValue(() -> EntitySpawnConfig.fromSection(config, new MemoryConfiguration()))
.hidden()
.serializer(new NodeSerializer<>() {
@Override
@@ -252,7 +259,7 @@ final class WorldConfigNodes {
ConfigurationSection spawnSection = (object instanceof ConfigurationSection section)
? section
: new MemoryConfiguration();
return EntitySpawnConfig.fromSection(spawnSection);
return EntitySpawnConfig.fromSection(config, spawnSection);
}
@Override
@@ -260,7 +267,7 @@ final class WorldConfigNodes {
return object.toSection();
}
})
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
newValue.setWorldRef(world);
newValue.applyConfigToWorld();
}));
@@ -8,6 +8,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
@@ -16,6 +17,7 @@ import java.util.stream.Collectors;
import com.dumptruckman.minecraft.util.Logging;
import com.google.common.base.Strings;
import io.papermc.lib.PaperLib;
import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
@@ -173,12 +175,12 @@ public final class WorldManager {
private void removeWorldsNotInConfigs(Collection<String> removedWorlds) {
removedWorlds.forEach(worldName -> getWorld(worldName)
.fold(
() -> Attempt.failure(FailureReason.GENERIC, Message.of("world already removed")),
world -> removeWorld(RemoveWorldOptions.world(world))
)
.onFailure(failure -> Logging.severe("Failed to unload world %s: %s", worldName, failure))
.onSuccess(success -> Logging.fine("Unloaded world %s as it was removed from config", worldName)));
.map(world -> removeWorld(RemoveWorldOptions.world(world)))
.getOrElse(() -> worldActionResult(RemoveFailureReason.WORLD_NON_EXISTENT, worldName))
.onFailure(failure ->
Logging.severe("Failed to unload world %s: %s", worldName, failure))
.onSuccess(success ->
Logging.fine("Unloaded world %s as it was removed from config", worldName)));
}
/**
@@ -207,30 +209,29 @@ public final class WorldManager {
}
private void importExistingBukkitWorld(World bukkitWorld) {
if (bukkitWorld == null || isWorld(bukkitWorld.getName())) {
return;
}
importWorld(ImportWorldOptions.worldName(bukkitWorld.getName())
.environment(bukkitWorld.getEnvironment())
.generator(generatorProvider.getDefaultGeneratorForWorld(bukkitWorld.getName())))
.onFailure(failure ->
Logging.severe("Failed to import world %s: %s", bukkitWorld.getName(), failure))
.onSuccess(success ->
Logging.fine("Imported existing world %s", bukkitWorld.getName()));
Option.of(bukkitWorld)
.filter(world -> !isWorld(world.getName()))
.map(world -> importWorld(
ImportWorldOptions.worldName(world.getName())
.environment(world.getEnvironment())
.generator(generatorProvider.getDefaultGeneratorForWorld(world.getName())))
.onFailure(failure ->
Logging.severe("Failed to import world %s: %s", world.getName(), failure))
.onSuccess(newMVWorld ->
Logging.fine("Imported existing world %s", newMVWorld.getName())));
}
/**
* Loads all worlds that are set to autoload.
*/
private void autoLoadWorlds() {
getWorlds().forEach(world -> {
if (isLoadedWorld(world) || !world.isAutoLoad()) {
return;
}
loadWorld(LoadWorldOptions.world(world))
.onFailure(failure ->
Logging.severe("Failed to load world %s: %s", world.getName(), failure));
});
getWorlds().stream()
.filter(world -> !isLoadedWorld(world) && world.isAutoLoad())
.forEach(world -> loadWorld(LoadWorldOptions.world(world))
.onFailure(failure ->
Logging.severe("Failed to autoload world %s: %s", world.getName(), failure))
.onSuccess(newMVWorld ->
Logging.fine("Autoloaded world %s", newMVWorld.getName())));
}
/**
@@ -271,15 +272,12 @@ public final class WorldManager {
.mapAttempt(creator -> addGeneratorToCreator(creator, generatorString))
.mapAttempt(this::createBukkitWorld)
.transform(CreateFailureReason.WORLD_CREATOR_FAILED)
.map(bukkitWorld -> {
LoadedMultiverseWorld loadedWorld = newLoadedMultiverseWorld(
.map(bukkitWorld -> newLoadedMultiverseWorld(
bukkitWorld,
generatorString,
options.biome(),
options.useSpawnAdjust());
pluginManager.callEvent(new MVWorldCreatedEvent(loadedWorld));
return loadedWorld;
});
options.useSpawnAdjust()))
.peek(loadedWorld -> pluginManager.callEvent(new MVWorldCreatedEvent(loadedWorld)));
}
/**
@@ -295,14 +293,9 @@ public final class WorldManager {
} else if (isWorld(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_EXIST_UNLOADED, worldName);
}
World bukkitWorld = Bukkit.getWorld(worldName);
if (bukkitWorld != null) {
// World is already loaded, maybe by another plugin
return doImportBukkitWorld(options, bukkitWorld);
}
return validateImportWorldOptions(options).mapAttempt(this::doImportWorld);
return Option.of(Bukkit.getWorld(worldName))
.map(bukkitWorld -> doImportBukkitWorld(options, bukkitWorld))
.getOrElse(() -> validateImportWorldOptions(options).mapAttempt(this::doImportWorld));
}
private Attempt<ImportWorldOptions, ImportFailureReason> validateImportWorldOptions(ImportWorldOptions options) {
@@ -325,15 +318,12 @@ public final class WorldManager {
.mapAttempt(creator -> addGeneratorToCreator(creator, generatorString))
.mapAttempt(this::createBukkitWorld)
.transform(ImportFailureReason.WORLD_CREATOR_FAILED)
.map(bukkitWorld -> {
LoadedMultiverseWorld loadedWorld = newLoadedMultiverseWorld(
bukkitWorld,
generatorString,
options.biome(),
options.useSpawnAdjust());
pluginManager.callEvent(new MVWorldImportedEvent(loadedWorld));
return loadedWorld;
});
.map(bukkitWorld -> newLoadedMultiverseWorld(
bukkitWorld,
generatorString,
options.biome(),
options.useSpawnAdjust()))
.peek(loadedWorld -> pluginManager.callEvent(new MVWorldImportedEvent(loadedWorld)));
}
private Attempt<LoadedMultiverseWorld, ImportFailureReason> doImportBukkitWorld(ImportWorldOptions options, World bukkitWorld) {
@@ -355,7 +345,7 @@ public final class WorldManager {
private MultiverseWorld newMultiverseWorld(String worldName, WorldConfig worldConfig) {
MultiverseWorld mvWorld = new MultiverseWorld(worldName, worldConfig, config);
worldsMap.put(mvWorld.getName(), mvWorld);
worldsMap.put(mvWorld.getName().toLowerCase(Locale.ENGLISH), mvWorld);
corePermissions.addWorldPermissions(mvWorld);
return mvWorld;
}
@@ -370,12 +360,20 @@ public final class WorldManager {
private LoadedMultiverseWorld newLoadedMultiverseWorld(
@NotNull World world, @Nullable String generator, @Nullable String biome, boolean adjustSpawn) {
WorldConfig worldConfig = worldsConfigManager.addWorldConfig(world.getName());
// Properties from multiverse input
worldConfig.setAdjustSpawn(adjustSpawn);
worldConfig.setGenerator(generator == null ? "" : generator);
worldConfig.setBiome(biome == null ? "" : biome);
// Properties from the bukkit world
worldConfig.setDifficulty(world.getDifficulty());
worldConfig.setKeepSpawnInMemory(world.getKeepSpawnInMemory());
worldConfig.setScale(getCoordinateScale(world));
worldConfig.save();
MultiverseWorld mvWorld = newMultiverseWorld(world.getName(), worldConfig);
newMultiverseWorld(world.getName(), worldConfig);
LoadedMultiverseWorld loadedWorld = new LoadedMultiverseWorld(
world,
worldConfig,
@@ -384,20 +382,21 @@ public final class WorldManager {
locationManipulation,
entityPurger
);
setDefaultEnvironmentScale(mvWorld);
loadedWorldsMap.put(loadedWorld.getName(), loadedWorld);
loadedWorldsMap.put(loadedWorld.getName().toLowerCase(Locale.ENGLISH), loadedWorld);
saveWorldsConfig();
pluginManager.callEvent(new MVWorldLoadedEvent(loadedWorld));
return loadedWorld;
}
private void setDefaultEnvironmentScale(MultiverseWorld world) {
double scale = switch (world.getEnvironment()) {
case NETHER -> 8.0;
case THE_END -> 16.0;
default -> 1.0;
private double getCoordinateScale(World world) {
if (PaperLib.isPaper()) {
return world.getCoordinateScale();
}
return switch (world.getEnvironment()) {
case NORMAL -> 1;
case NETHER -> 8;
default -> 1;
};
world.setScale(scale);
}
/**
@@ -502,7 +501,7 @@ public final class WorldManager {
locationManipulation,
entityPurger
);
loadedWorldsMap.put(loadedWorld.getName(), loadedWorld);
loadedWorldsMap.put(loadedWorld.getName().toLowerCase(Locale.ENGLISH), loadedWorld);
saveWorldsConfig();
pluginManager.callEvent(new MVWorldLoadedEvent(loadedWorld));
return Attempt.success(loadedWorld);
@@ -649,12 +648,18 @@ public final class WorldManager {
*/
public Attempt<String, DeleteFailureReason> deleteWorld(@NotNull DeleteWorldOptions options) {
return getLoadedWorld(options.world()).fold(
() -> loadWorld(LoadWorldOptions.world(options.world()))
.transform(DeleteFailureReason.LOAD_FAILED)
.mapAttempt(world -> doDeleteWorld(world, options)),
() -> loadThenDeleteWorld(options),
world -> doDeleteWorld(world, options));
}
private Attempt<String, DeleteFailureReason> loadThenDeleteWorld(@NotNull DeleteWorldOptions options) {
return loadWorld(LoadWorldOptions.world(options.world()))
.fold(
ignore -> worldActionResult(DeleteFailureReason.LOAD_FAILED, options.world().getName()),
loadedWorld -> doDeleteWorld(loadedWorld, options)
);
}
/**
* Deletes an existing multiverse world entirely. Warning: This will delete all world files.
*
@@ -685,12 +690,13 @@ public final class WorldManager {
private Attempt<File, DeleteFailureReason> validateWorldToDelete(
@NotNull LoadedMultiverseWorld world) {
File worldFolder = world.getBukkitWorld().map(World::getWorldFolder).getOrNull();
if (worldFolder == null || !worldNameChecker.isValidWorldFolder(worldFolder)) {
Logging.severe("Failed to get world folder for world: " + world.getName());
return worldActionResult(DeleteFailureReason.WORLD_FOLDER_NOT_FOUND, world.getName());
}
return worldActionResult(worldFolder);
return world.getBukkitWorld().map(World::getWorldFolder)
.filter(worldNameChecker::isValidWorldFolder)
.map(this::<File, DeleteFailureReason>worldActionResult)
.getOrElse(() -> {
Logging.severe("World folder does not exist for world: " + world.getName());
return worldActionResult(DeleteFailureReason.WORLD_FOLDER_NOT_FOUND, world.getName());
});
}
/**
@@ -962,7 +968,9 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getWorld(@Nullable String worldName) {
return getLoadedWorld(worldName).fold(() -> getUnloadedWorld(worldName), Option::of);
return getLoadedWorld(worldName)
.map(world -> (MultiverseWorld) world)
.orElse(() -> getUnloadedWorld(worldName));
}
/**
@@ -974,7 +982,8 @@ public final class WorldManager {
*/
public Option<MultiverseWorld> getWorldByNameOrAlias(@Nullable String worldNameOrAlias) {
return getLoadedWorldByNameOrAlias(worldNameOrAlias)
.fold(() -> getUnloadedWorldByNameOrAlias(worldNameOrAlias), Option::of);
.map(world -> (MultiverseWorld) world)
.orElse(() -> getUnloadedWorldByNameOrAlias(worldNameOrAlias));
}
/**
@@ -989,7 +998,8 @@ public final class WorldManager {
public Collection<MultiverseWorld> getWorlds() {
return worldsMap.values().stream()
.map(world -> getLoadedWorld(world)
.fold(() -> world, loadedWorld -> loadedWorld))
.map(loadedWorld -> (MultiverseWorld) loadedWorld)
.getOrElse(world))
.toList();
}
@@ -1000,7 +1010,7 @@ public final class WorldManager {
* @return True if the world is a world is known to multiverse, but may or may not be loaded.
*/
public boolean isWorld(@Nullable String worldName) {
return worldsMap.containsKey(worldName);
return worldName != null && worldsMap.containsKey(worldName.toLowerCase(Locale.ENGLISH));
}
/**
@@ -1010,7 +1020,9 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getUnloadedWorld(@Nullable String worldName) {
return isLoadedWorld(worldName) ? Option.none() : Option.of(worldsMap.get(worldName));
return isLoadedWorld(worldName)
? Option.none()
: Option.of(worldName).flatMap(name -> Option.of(worldsMap.get(name.toLowerCase(Locale.ENGLISH))));
}
/**
@@ -1062,7 +1074,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable World world) {
return world == null ? Option.none() : Option.of(loadedWorldsMap.get(world.getName()));
return Option.of(world).flatMap(notNullWorld -> getLoadedWorld(notNullWorld.getName()));
}
/**
@@ -1072,7 +1084,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable MultiverseWorld world) {
return world == null ? Option.none() : Option.of(loadedWorldsMap.get(world.getName()));
return Option.of(world).flatMap(notNullWorld -> getLoadedWorld(notNullWorld.getName()));
}
/**
@@ -1082,7 +1094,8 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable String worldName) {
return Option.of(loadedWorldsMap.get(worldName));
return Option.of(worldName)
.flatMap(name -> Option.of(loadedWorldsMap.get(name.toLowerCase(Locale.ENGLISH))));
}
/**
@@ -1143,7 +1156,7 @@ public final class WorldManager {
* @return True if the world is a multiverse world that is loaded.
*/
public boolean isLoadedWorld(@Nullable String worldName) {
return loadedWorldsMap.containsKey(worldName);
return worldName != null && loadedWorldsMap.containsKey(worldName.toLowerCase(Locale.ENGLISH));
}
/**
@@ -19,6 +19,7 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
/**
* Manages the worlds.yml file.
@@ -81,8 +82,9 @@ final class WorldsConfigManager {
}
// Copy old config file to `worlds.yml.old`
Path oldWorldConfig = worldConfigFile.toPath().getParent().resolve(CONFIG_FILENAME + ".old");
Files.copy(worldConfigFile.toPath(), oldWorldConfig, COPY_ATTRIBUTES);
Path oldWorldConfig = worldConfigFile.toPath().getParent()
.resolve(CONFIG_FILENAME + ".old." + System.currentTimeMillis());
Files.copy(worldConfigFile.toPath(), oldWorldConfig, COPY_ATTRIBUTES, REPLACE_EXISTING);
return configData.replace("==: MVWorld", "w@: world")
.replace("==: MVSpawnSettings", "")
@@ -1,5 +1,7 @@
package org.mvplugins.multiverse.core.world.biomeprovider;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.block.Biome;
import org.bukkit.generator.BiomeProvider;
import org.jetbrains.annotations.NotNull;
@@ -7,6 +9,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
/**
* A parser for {@link SingleBiomeProvider}
@@ -17,13 +20,16 @@ final class SingleBiomeProviderParser implements BiomeProviderParser {
@Override
public BiomeProvider parseBiomeProvider(@NotNull String worldName, @NotNull String params) {
return new SingleBiomeProvider(Biome.valueOf(params.toUpperCase()));
NamespacedKey biomeKey = NamespacedKey.fromString(params.toLowerCase(Locale.ENGLISH));
return new SingleBiomeProvider(Registry.BIOME.get(biomeKey));
}
@Override
public Collection<String> suggestParams(@NotNull String currentInput) {
if (biomes == null) {
biomes = Arrays.stream(Biome.values()).map(biome -> biome.toString().toLowerCase()).toList();
biomes = Registry.BIOME.stream()
.map(biome -> biome.getKey().getKey().toLowerCase(Locale.ENGLISH))
.toList();
}
return biomes;
}
@@ -1,29 +1,36 @@
package org.mvplugins.multiverse.core.world.entity;
import com.dumptruckman.minecraft.util.Logging;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.MemoryConfiguration;
import org.bukkit.entity.Entity;
import org.bukkit.entity.SpawnCategory;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
public final class EntitySpawnConfig {
private final CoreConfig config;
private final Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig;
EntitySpawnConfig(Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig) {
EntitySpawnConfig(CoreConfig config, Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig) {
this.config = config;
this.spawnCategoriesConfig = spawnCategoriesConfig;
}
public SpawnCategoryConfig getSpawnCategoryConfig(SpawnCategory spawnCategory) {
return spawnCategoriesConfig.computeIfAbsent(spawnCategory,
computeSpawnCategory -> new SpawnCategoryConfig(computeSpawnCategory, new MemoryConfiguration()));
computeSpawnCategory -> new SpawnCategoryConfig(
config,
computeSpawnCategory,
new MemoryConfiguration()
));
}
public boolean shouldAllowSpawn(Entity entity) {
@@ -31,8 +38,9 @@ public final class EntitySpawnConfig {
}
public void applyConfigToWorld() {
spawnCategoriesConfig.values()
.forEach(SpawnCategoryConfig::applyConfigToWorld);
for (SpawnCategory category : SpawnCategory.values()) {
getSpawnCategoryConfig(category).applyConfigToWorld();
}
}
@Override
@@ -46,23 +54,27 @@ public final class EntitySpawnConfig {
public ConfigurationSection toSection() {
MemoryConfiguration section = new MemoryConfiguration();
spawnCategoriesConfig.forEach((spawnCategory, spawnCategoryConfig) -> {
section.set(spawnCategory.toString().toLowerCase(), spawnCategoryConfig.saveSection());
section.set(spawnCategory.toString().toLowerCase(Locale.ENGLISH), spawnCategoryConfig.saveSection());
});
return section;
}
@ApiStatus.Internal
public static EntitySpawnConfig fromSection(ConfigurationSection section) {
Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig = new HashMap<>();
section.getValues(false).forEach((key, value) -> {
if (!(value instanceof ConfigurationSection sectionPart)) {
Logging.warning("Invalid spawn category config for " + key + ": " + value);
return;
public static EntitySpawnConfig fromSection(CoreConfig config, ConfigurationSection section) {
Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig = new LinkedHashMap<>();
Map<String, Object> existingCategories = section.getValues(false);
for (SpawnCategory category : SpawnCategory.values()) {
Object value = existingCategories.get(category.toString().toLowerCase(Locale.ENGLISH));
if (!(value instanceof ConfigurationSection)) {
if (value != null) {
Logging.warning("Invalid spawn category config for " + category + ": " + value);
}
value = section.createSection(category.toString());
}
SpawnCategory spawnCategory = SpawnCategory.valueOf(key.toUpperCase());
spawnCategoriesConfig.put(spawnCategory, new SpawnCategoryConfig(spawnCategory, sectionPart));
});
return new EntitySpawnConfig(spawnCategoriesConfig);
ConfigurationSection sectionPart = (ConfigurationSection) value;
spawnCategoriesConfig.put(category, new SpawnCategoryConfig(config, category, sectionPart));
}
return new EntitySpawnConfig(config, spawnCategoriesConfig);
}
@ApiStatus.Internal
@@ -2,17 +2,20 @@ package org.mvplugins.multiverse.core.world.entity;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.bukkit.ChatColor;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.SpawnCategory;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.config.handle.MemoryConfigurationHandle;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.config.node.ConfigNode;
import org.mvplugins.multiverse.core.config.node.ListConfigNode;
import org.mvplugins.multiverse.core.config.node.Node;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
@@ -21,6 +24,7 @@ import java.util.List;
public final class SpawnCategoryConfig {
private final CoreConfig config;
private final SpawnCategory spawnCategory;
private final MemoryConfigurationHandle handle;
private final StringPropertyHandle stringPropertyHandle;
@@ -28,7 +32,8 @@ public final class SpawnCategoryConfig {
private MultiverseWorld world;
SpawnCategoryConfig(SpawnCategory spawnCategory, ConfigurationSection section) {
SpawnCategoryConfig(CoreConfig config, SpawnCategory spawnCategory, ConfigurationSection section) {
this.config = config;
this.spawnCategory = spawnCategory;
this.nodes = new Nodes();
this.handle = MemoryConfigurationHandle.builder(section, nodes.nodes)
@@ -57,23 +62,42 @@ public final class SpawnCategoryConfig {
return;
}
loadedWorld.getBukkitWorld().peek(bukkitWorld -> {
if (!isSpawn()) {
if (getExceptions().isEmpty()) {
Logging.finer("World %s %s setTicksPerSpawns: 0", world.getName(), spawnCategory);
bukkitWorld.setTicksPerSpawns(spawnCategory, 0);
} else {
Logging.finer("World %s %s setTicksPerSpawns: -1", world.getName(), spawnCategory);
bukkitWorld.setTicksPerSpawns(spawnCategory, -1);
}
} else {
Logging.finer("World %s %s setTicksPerSpawns: %d", world.getName(), spawnCategory, getTickRate());
bukkitWorld.setTicksPerSpawns(spawnCategory, getTickRate());
}
Logging.finer("World %s %s setSpawnLimit: %d", world.getName(), spawnCategory, getSpawnLimit());
bukkitWorld.setSpawnLimit(spawnCategory, getSpawnLimit());
applyTickPerSpawns(bukkitWorld);
applySpawnLimit(bukkitWorld);
});
}
private void applyTickPerSpawns(World bukkitWorld) {
if (!config.getApplyEntitySpawnLimit()) {
Logging.finer("World %s %s skipping setTicksPerSpawns due to core config", world.getName(), spawnCategory);
return;
}
if (!isSpawn() && getExceptions().isEmpty()) {
Logging.finer("World %s %s setTicksPerSpawns: 0", world.getName(), spawnCategory);
bukkitWorld.setTicksPerSpawns(spawnCategory, 0);
return;
}
if (getTickRate() == -2) {
Logging.finer("World %s %s skipping setTicksPerSpawns as tick-rate is UNSET", world.getName(), spawnCategory);
return;
}
Logging.finer("World %s %s setTicksPerSpawns: %d", world.getName(), spawnCategory, getTickRate());
bukkitWorld.setTicksPerSpawns(spawnCategory, getTickRate());
}
private void applySpawnLimit(World bukkitWorld) {
if (!config.getApplyEntitySpawnLimit()) {
Logging.finer("Skipping World %s %s setSpawnLimit due to core config", world.getName(), spawnCategory);
return;
}
if (getSpawnLimit() == -2) {
Logging.finer("World %s %s skipping setSpawnLimit as spawn-limit is UNSET", world.getName(), spawnCategory);
return;
}
Logging.finer("World %s %s setSpawnLimit: %d", world.getName(), spawnCategory, getSpawnLimit());
bukkitWorld.setSpawnLimit(spawnCategory, getSpawnLimit());
}
public StringPropertyHandle getStringPropertyHandle() {
return stringPropertyHandle;
}
@@ -142,19 +166,39 @@ public final class SpawnCategoryConfig {
final ConfigNode<Boolean> spawn = node(ConfigNode.builder("spawn", Boolean.class)
.defaultValue(true)
.onSetValue((oldValue, newValue) -> applyConfigToWorld())
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.build());
final ConfigNode<Integer> tickRate = node(ConfigNode.builder("tick-rate", Integer.class)
.defaultValue(-1)
.suggester(input -> List.of("-1", "10", "100", "400", "1000"))
.onSetValue((oldValue, newValue) -> applyConfigToWorld())
.defaultValue(-2)
.suggester(input -> List.of("@unset", "@bukkit", "10", "100", "400", "1000"))
.serializer(SpawnValueSerializer.INSTANCE)
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.onChange((sender, oldValue, newValue) -> {
if (!config.getApplyEntitySpawnRate()) {
sender.sendMessage(ChatColor.RED + "Warning: Changing tick rates has no effect because " +
"'apply-entity-spawn-rate' is disabled in the core config.");
} else if (newValue == -2) {
sender.sendMessage(ChatColor.YELLOW + "Note: Setting tick-rate to '@unset' may not reset to " +
"the server default until the world is reloaded or server is restarted.");
}
})
.build());
final ConfigNode<Integer> spawnLimit = node(ConfigNode.builder("spawn-limit", Integer.class)
.defaultValue(-1)
.suggester(input -> List.of("-1", "10", "100", "400", "1000"))
.onSetValue((oldValue, newValue) -> applyConfigToWorld())
.defaultValue(-2)
.suggester(input -> List.of("@unset", "@bukkit", "10", "100", "400", "1000"))
.serializer(SpawnValueSerializer.INSTANCE)
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.onChange((sender, oldValue, newValue) -> {
if (!config.getApplyEntitySpawnLimit()) {
sender.sendMessage(ChatColor.RED + "Warning: Changing spawn limits has no effect because " +
"'apply-entity-spawn-limit' is disabled in the core config.");
} else if (newValue == -2) {
sender.sendMessage(ChatColor.YELLOW + "Note: Setting spawn-limit to '@unset' may not reset to " +
"the server default until the world is reloaded or server is restarted.");
}
})
.build());
final ListConfigNode<EntityType> exceptions = node(ListConfigNode.listBuilder("exceptions", EntityType.class)
@@ -162,7 +206,32 @@ public final class SpawnCategoryConfig {
.itemSuggester(input -> SpawnCategoryMapper.getEntityTypes(spawnCategory).stream()
.map(EntityType::name)
.toList())
.onSetValue((oldValue, newValue) -> applyConfigToWorld())
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.build());
}
private static class SpawnValueSerializer implements NodeSerializer<Integer> {
private static final SpawnValueSerializer INSTANCE = new SpawnValueSerializer();
@Override
public Integer deserialize(Object object, Class<Integer> type) {
String str = String.valueOf(object);
if (str.equalsIgnoreCase("@unset")) {
return -2;
}
if (str.equalsIgnoreCase("@bukkit")) {
return -1;
}
return Integer.parseInt(str);
}
@Override
public Object serialize(Integer object, Class<Integer> type) {
return switch (object) {
case -2 -> "@unset";
case -1 -> "@bukkit";
default -> object;
};
}
}
}
@@ -27,6 +27,7 @@ import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.utils.FileUtils;
import org.mvplugins.multiverse.core.utils.REPatterns;
@@ -38,15 +39,17 @@ import org.mvplugins.multiverse.core.utils.REPatterns;
public final class GeneratorProvider implements Listener {
private final Map<String, String> defaultGenerators;
private final Map<String, GeneratorPlugin> generatorPlugins;
private final CoreConfig coreConfig;
private final FileUtils fileUtils;
@Inject
GeneratorProvider(@NotNull MultiverseCore multiverseCore, @NotNull FileUtils fileUtils) {
GeneratorProvider(@NotNull MultiverseCore plugin, @NotNull CoreConfig coreConfig, @NotNull FileUtils fileUtils) {
this.coreConfig = coreConfig;
this.fileUtils = fileUtils;
defaultGenerators = new HashMap<>();
generatorPlugins = new HashMap<>();
Bukkit.getPluginManager().registerEvents(this, multiverseCore);
Bukkit.getPluginManager().registerEvents(this, plugin);
loadDefaultWorldGenerators();
loadPluginGenerators();
}
@@ -73,11 +76,14 @@ public final class GeneratorProvider implements Listener {
* Find generator plugins from plugins loaded and register them.
*/
private void loadPluginGenerators() {
Arrays.stream(Bukkit.getPluginManager().getPlugins()).forEach(plugin -> {
if (!coreConfig.getAutoDetectGeneratorPlugins()) {
return;
}
for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) {
if (testIsGeneratorPlugin(plugin)) {
registerGeneratorPlugin(new SimpleGeneratorPlugin(plugin.getName()));
}
});
}
}
/**
@@ -88,14 +94,23 @@ public final class GeneratorProvider implements Listener {
*/
private boolean testIsGeneratorPlugin(Plugin plugin) {
String worldName = Bukkit.getWorlds().stream().findFirst().map(World::getName).orElse("world");
return Try.of(() -> plugin.getDefaultWorldGenerator(worldName, "") != null)
.recover(IllegalArgumentException.class, true)
.recover(throwable -> {
Logging.warning("Plugin %s threw an exception when testing if it is a generator plugin!",
plugin.getName());
throwable.printStackTrace();
return false;
}).getOrElse(false);
try {
return plugin.getDefaultWorldGenerator(worldName, "") != null;
} catch (UnsupportedOperationException e) {
// Some plugins throw this if they don't support world generation
return false;
} catch (Throwable t) {
Logging.warning("Plugin %s threw an exception when testing if it is a generator plugin! ",
plugin.getName());
Logging.warning("Error by plugin %s: %s", plugin.getName(), t.getMessage());
Logging.warning("This is NOT a bug in Multiverse. Do NOT report this to Multiverse support. " +
"Enable debug mode with `/mv debug 1` to see the full stack trace.");
if (coreConfig.getGlobalDebug() >= 1) {
t.printStackTrace();
}
// Assume it's a generator plugin since it tried to do something, most likely the id is wrong.
return true;
}
}
/**
@@ -206,6 +221,9 @@ public final class GeneratorProvider implements Listener {
*/
@EventHandler
private void onPluginEnable(PluginEnableEvent event) {
if (!coreConfig.getAutoDetectGeneratorPlugins()) {
return;
}
if (!testIsGeneratorPlugin(event.getPlugin())) {
Logging.finest("Plugin %s is not a generator plugin.", event.getPlugin().getName());
return;
@@ -0,0 +1,90 @@
package org.mvplugins.multiverse.core.world.helpers;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnmodifiableView;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Tracks which players are in which worlds, using a thread-safe map.
* This allows async access to online players list and the world they are in.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@Service
public final class ConcurrentPlayerWorldTracker implements Listener {
private final Map<String, String> playerWorldMap;
@Inject
ConcurrentPlayerWorldTracker(@NotNull MultiverseCore plugin) {
this.playerWorldMap = new ConcurrentHashMap<>();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
/**
* Get an unmodifiable collection of all online player names on the server.
*
* @return Unmodifiable collection of online player names.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@NotNull
@UnmodifiableView
public Collection<String> getOnlinePlayers() {
return Collections.unmodifiableCollection(playerWorldMap.keySet());
}
/**
* Get the world name a player is currently in.
*
* @param playerName Name of the player.
* @return World name the player is in, or null if the player is not online.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@NotNull
public Option<String> getPlayerWorld(String playerName) {
return Option.of(playerWorldMap.get(playerName));
}
@EventHandler(priority = EventPriority.LOWEST)
private void onPlayerJoin(PlayerJoinEvent event) {
setPlayerWorld(event.getPlayer());
}
@EventHandler(priority = EventPriority.LOWEST)
private void onPlayerChangedWorld(@NotNull PlayerChangedWorldEvent event) {
setPlayerWorld(event.getPlayer());
}
private void setPlayerWorld(Player player) {
String playerName = player.getName();
String worldName = player.getWorld().getName();
playerWorldMap.put(playerName, worldName);
}
@EventHandler
private void onPlayerQuit(@NotNull PlayerQuitEvent event) {
String playerName = event.getPlayer().getName();
playerWorldMap.remove(playerName);
}
}
@@ -1,14 +1,15 @@
package org.mvplugins.multiverse.core.world.helpers;
import java.io.File;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import io.vavr.control.Option;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.utils.REPatterns;
/**
* <p>Utility class in helping to check the status of a world name and it's associated world folder.</p>
@@ -19,11 +20,11 @@ import org.jvnet.hk2.annotations.Service;
@Service
public final class WorldNameChecker {
private static final Pattern WORLD_NAME_PATTERN = Pattern.compile("[a-zA-Z0-9/._-]+");
private static final Set<String> BLACKLIST_NAMES = Set.of(
"cache",
"config",
"crash-reports",
"libraries",
"logs",
"plugins",
"versions");
@@ -46,18 +47,21 @@ public final class WorldNameChecker {
*/
@NotNull
public NameStatus checkName(@Nullable String worldName) {
return Option.of(worldName).map(name -> {
if (name.isEmpty()) {
return NameStatus.EMPTY;
}
if (BLACKLIST_NAMES.contains(name)) {
return NameStatus.BLACKLISTED;
}
if (!WORLD_NAME_PATTERN.matcher(name).matches()) {
return NameStatus.INVALID_CHARS;
}
return NameStatus.VALID;
}).getOrElse(NameStatus.EMPTY);
return Option.of(worldName)
.map(name -> name.toLowerCase(Locale.ENGLISH))
.map(name -> {
if (name.isEmpty()) {
return NameStatus.EMPTY;
}
if (BLACKLIST_NAMES.contains(name)) {
return NameStatus.BLACKLISTED;
}
if (!REPatterns.NAMESPACE_KEY.matcher(name).matches()) {
return NameStatus.INVALID_CHARS;
}
return NameStatus.VALID;
})
.getOrElse(NameStatus.EMPTY);
}
/**
@@ -130,7 +134,7 @@ public final class WorldNameChecker {
* @return True if it looks like a world, else false.
*/
private boolean folderHasDat(@NotNull File worldFolder) {
File[] files = worldFolder.listFiles((file, name) -> name.toLowerCase().endsWith(".dat"));
File[] files = worldFolder.listFiles((file, name) -> name.toLowerCase(Locale.ENGLISH).endsWith(".dat"));
return files != null && files.length > 0;
}
@@ -22,13 +22,13 @@ import java.util.Objects;
* This is useful to store location with world that may not be loaded yet or have been unloaded at some point.
*/
@SerializableAs("UnloadedWorldLocation")
public final class UnloadedWorldLocation extends Location {
public class UnloadedWorldLocation extends Location {
public static UnloadedWorldLocation fromLocation(@NotNull Location location) {
return new UnloadedWorldLocation(location.getWorld(), location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
}
private String worldName;
private @Nullable String worldName;
public UnloadedWorldLocation(@Nullable String worldName, double x, double y, double z) {
super(null, x, y, z);
@@ -67,7 +67,7 @@ public final class UnloadedWorldLocation extends Location {
this.worldName = worldName;
}
public String getWorldName() {
public @Nullable String getWorldName() {
return worldName;
}
@@ -160,10 +160,10 @@ public final class UnloadedWorldLocation extends Location {
@Override
public int hashCode() {
int hash = 3;
hash = 19 * hash + worldName.hashCode();
hash = 19 * hash + (int) (Double.doubleToLongBits(this.getX()) ^ (Double.doubleToLongBits(this.getX()) >>> 32));
hash = 19 * hash + (int) (Double.doubleToLongBits(this.getY()) ^ (Double.doubleToLongBits(this.getY()) >>> 32));
hash = 19 * hash + (int) (Double.doubleToLongBits(this.getZ()) ^ (Double.doubleToLongBits(this.getZ()) >>> 32));
hash = 19 * hash + String.valueOf(worldName).hashCode();
hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.getX()));
hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.getY()));
hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.getZ()));
hash = 19 * hash + Float.floatToIntBits(this.getPitch());
hash = 19 * hash + Float.floatToIntBits(this.getYaw());
return hash;
@@ -171,7 +171,7 @@ public final class UnloadedWorldLocation extends Location {
@Override
public String toString() {
return "Location{" +
return "UnloadedWorldLocation{" +
"world=" + worldName +
",x=" + getX() +
",y=" + getY() +
@@ -94,7 +94,6 @@ public final class CloneWorldOptions implements KeepWorldSettingsOptions {
return keepWorldConfig;
}
/**
* Sets whether to keep the world border of the world during cloning.
*
@@ -38,7 +38,7 @@ public final class UnloadWorldOptions {
* Sets whether to unload the bukkit world.
* <br />
* By setting this to false, multiverse will essentially untrack the world, but the world itself is still loaded.
* This should be only used in edge cases where the world is used by other plugins byt you don't want multiverse
* This should be only used in edge cases where the world is used by other plugins but you don't want multiverse
* to handle it.
*
* @param unloadBukkitWorldInput Whether to unload the bukkit world.
@@ -255,7 +255,7 @@ mv-core.destination.cannon.failurereason.invalidformat=&cCannon destination form
mv-core.destination.exact.failurereason.invalidformat=&cExact destination format is: &6e:worldname:x,y,z:pitch:yaw
mv-core.destination.player.failurereason.playernotfound=&cPlayer '&6{player}&c' does not exist or is not online!
mv-core.destination.shared.failurereason.invalidcoordinatesformat=You must specify a valid coordinate in this format: &6{x},{y},{z}
mv-core.destination.shared.failurereason.invalidnumberformat=Value is not a valid number. {error}.
mv-core.destination.shared.failurereason.invalidnumberformat=&cInvalid destination input. {error}
mv-core.destination.shared.failurereason.worldnotfound=&cWorld '&6{world}&c' does not exist or is not loaded!
mv-core.destination.parse.failurereason.invaliddestinationid=Invalid destination id: {id}. Valid destination ids are: {ids}
@@ -281,8 +281,8 @@ mv-core.createworld.worldexistunloaded=World '{world}' already exists, but it's
mv-core.createworld.worldexistloaded=World '{world}' already exists!
mv-core.deleteworld.worldnonexistent=World '{world}' not found!
mv-core.deleteworld.loadfailed=Unable to load world '{world}', does the world folder exist?&f You can run '&a/mv remove {world}&f' to remove it from Multiverse, or attempt to load again with '&a/mv load {world}&f'.
mv-core.deleteworld.worldfoldernotfound=World '{world}' folder not found!
mv-core.deleteworld.loadfailed=Unable to load world '{world}', is the world folder deleted or corrupted?&f You can run '&a/mv remove {world}&f' to remove it from Multiverse, or attempt to load again with '&a/mv load {world}&f'.
mv-core.deleteworld.worldfoldernotfound=&cUnable to locate '{world}' folder! You may '&a/mv remove {world}&c' to remove it from Multiverse, and manually delete the folder if it exists.
mv-core.deleteworld.failedtodeletefolder=Failed to delete world folder '{world}': {error}\n&fSee console for more details.
mv-core.importworld.invalidworldname=World '{world}' contains invalid characters!
@@ -327,6 +327,11 @@ mv-core.exception.multiverseworld.unloaddefaultworld=&cYou can't unload the defa
mv-core.exception.multiverseworld.unloadplayersinworld=&cThere are still &6{count}&c player(s) in the world! Use '&6--remove-players&c' flag to your command to teleport all players out of the world.
mv-core.exception.multiverseworld.unloaderror=&cAn unknown error occurred while unloading world: &6{world}&c.\n&cSee console for more details.
# multiverse position parse exception
mv-core.exception.positionparse.invaliddirection=&cInvalid direction string format: {format}. Expected format: <pitch>:<yaw>
mv-core.exception.positionparse.invalidcoordinates=&cInvalid coordinates format: {format}. Expected format: <x>,<y>,<z>
mv-core.exception.positionparse.invalidnumber=&cInvalid number format: {number}. Expects a numeric value.
# generic
mv-core.generic.success=Success!
mv-core.generic.failure=Failed!
@@ -69,6 +69,22 @@ class DestinationTest : TestWithMockBukkit() {
assertEquals("e:world:1.2,2.0,3.0:10.5:9.5", destination.toString())
}
@Test
fun `Exact destination instance with relative position`() {
val destination = destinationsProvider.parseDestination("e:world:~1.5,2,~-3:10.5:~9.5").orNull
assertTrue(destination is ExactDestinationInstance)
val expectedLocation = Location(
world.bukkitWorld.orNull,
player.location.x + 1.5,
2.0,
player.location.z - 3.0,
player.location.yaw + 9.5F,
10.5F
)
assertLocationEquals(expectedLocation, destination.getLocation(player).orNull)
assertEquals("e:world:~1.5,2.0,~-3.0:10.5:~9.5", destination.toString())
}
@Test
fun `Exact destination instance from location`() {
val exactDestination = serviceLocator.getActiveService(ExactDestination::class.java).takeIf { it != null } ?: run {
@@ -123,6 +139,7 @@ class DestinationTest : TestWithMockBukkit() {
assertTrue(destinationsProvider.parseDestination("b:invalid-bed").isFailure)
assertTrue(destinationsProvider.parseDestination("ca:invalid-cannon").isFailure)
assertTrue(destinationsProvider.parseDestination("e:world:1,2,x").isFailure)
assertTrue(destinationsProvider.parseDestination("e:world:1,2,3:").isFailure)
assertTrue(destinationsProvider.parseDestination("pl:invalid-player").isFailure)
assertTrue(destinationsProvider.parseDestination("w:invalid-world").isFailure)
// todo: should we make invalid yaw for WorldDestination fail?
@@ -5,6 +5,7 @@ import org.bukkit.World
import org.bukkit.WorldType
import org.hamcrest.MatcherAssert.assertThat
import org.mockbukkit.mockbukkit.matcher.plugin.PluginManagerFiredEventClassMatcher.hasFiredEventInstance
import org.mockbukkit.mockbukkit.matcher.plugin.PluginManagerFiredEventClassMatcher.hasNotFiredEventInstance
import org.mvplugins.multiverse.core.TestWithMockBukkit
import org.mvplugins.multiverse.core.event.world.*
import org.mvplugins.multiverse.core.world.options.CloneWorldOptions
@@ -43,7 +44,7 @@ class WorldManagerTest : TestWithMockBukkit() {
@Test
fun `Create world with custom options`() {
assertTrue(worldManager.createWorld(
CreateWorldOptions.worldName("world_nether")
CreateWorldOptions.worldName("Section/my-nether_world")
.environment(World.Environment.NETHER)
.generateStructures(false)
.seed(1234L)
@@ -51,11 +52,11 @@ class WorldManagerTest : TestWithMockBukkit() {
.worldType(WorldType.FLAT)
).isSuccess)
val getWorld = worldManager.getLoadedWorld("world_nether")
val getWorld = worldManager.getLoadedWorld("Section/my-nether_world")
assertTrue(getWorld.isDefined)
val world = getWorld.get()
assertNotNull(world)
assertEquals("world_nether", world.name)
assertEquals("Section/my-nether_world", world.name)
assertEquals(World.Environment.NETHER, world.environment)
assertFalse(world.canGenerateStructures().get())
assertEquals(1234L, world.seed)
@@ -84,6 +85,18 @@ class WorldManagerTest : TestWithMockBukkit() {
)
}
@Test
fun `Create world failed - world exists and loaded (case insensitive)`() {
assertEquals(
CreateFailureReason.WORLD_EXIST_LOADED,
worldManager.createWorld(CreateWorldOptions.worldName("WoRld2")).failureReason
)
assertEquals(
CreateFailureReason.WORLD_EXIST_LOADED,
worldManager.createWorld(CreateWorldOptions.worldName("WORLD2")).failureReason
)
}
@Test
fun `Create world failed - world exists but unloaded`() {
assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world)).isSuccess)
@@ -255,4 +268,60 @@ class WorldManagerTest : TestWithMockBukkit() {
assertEquals(world, worldManager.getWorldByNameOrAlias("testalias").orNull)
assertNull(worldManager.getUnloadedWorldByNameOrAlias("testalias").orNull)
}
@Test
fun `Get world`() {
assertEquals(world, worldManager.getLoadedWorld("world").orNull)
assertNull(worldManager.getUnloadedWorld("world").orNull)
assertEquals(world, worldManager.getWorld("world").orNull)
assertTrue(worldManager.isLoadedWorld("world"))
assertFalse(worldManager.isUnloadedWorld("world"))
assertTrue(worldManager.isWorld("world"))
val unloadedWorld = worldManager.unloadWorld(UnloadWorldOptions.world(world)).get()
assertNull(worldManager.getLoadedWorld("world").orNull)
assertEquals(unloadedWorld, worldManager.getUnloadedWorld("world").orNull)
assertEquals(unloadedWorld, worldManager.getWorld("world").orNull)
assertFalse(worldManager.isLoadedWorld("world"))
assertTrue(worldManager.isUnloadedWorld("world"))
assertTrue(worldManager.isWorld("world"))
}
@Test
fun `Get world is case insensitive`() {
assertEquals(world2, worldManager.getLoadedWorld("WORLD2").orNull)
assertNull(worldManager.getUnloadedWorld("wOrld2").orNull)
assertEquals(world2, worldManager.getWorld("wOrld2").orNull)
assertTrue(worldManager.isLoadedWorld("WoRlD2"))
assertFalse(worldManager.isUnloadedWorld("WoRlD2"))
assertTrue(worldManager.isWorld("wORLD2"))
val unloadedWorld2 = worldManager.unloadWorld(UnloadWorldOptions.world(world2)).get()
assertNull(worldManager.getLoadedWorld("wOrld2").orNull)
assertEquals(unloadedWorld2, worldManager.getUnloadedWorld("wOrld2").orNull)
assertEquals(unloadedWorld2, worldManager.getWorld("wORLD2").orNull)
assertFalse(worldManager.isLoadedWorld("WoRlD2"))
assertTrue(worldManager.isUnloadedWorld("WoRlD2"))
assertTrue(worldManager.isWorld("wORLD2"))
}
@Test
fun `Set different value - MVWorldPropertyChangedEvent event is fired`() {
server.pluginManager.clearEvents()
world.scale = 8.0
assertThat(server.pluginManager, hasFiredEventInstance(MVWorldPropertyChangedEvent::class.java))
}
@Test
fun `Set equal value - MVWorldPropertyChangedEvent event is not fired`() {
server.pluginManager.clearEvents()
world.scale = 1.0
assertThat(server.pluginManager, hasNotFiredEventInstance(MVWorldPropertyChangedEvent::class.java))
}
}
@@ -38,6 +38,7 @@ class WorldNameCheckerTest : TestWithMockBukkit() {
fun `Blacklisted world name`() {
assertEquals(WorldNameChecker.NameStatus.BLACKLISTED, worldNameChecker.checkName("logs"))
assertEquals(WorldNameChecker.NameStatus.BLACKLISTED, worldNameChecker.checkName("plugins"))
assertEquals(WorldNameChecker.NameStatus.BLACKLISTED, worldNameChecker.checkName("CACHE"))
}
@Test
@@ -4,6 +4,8 @@ world:
enforce-access: false
enforce-gamemode: true
enforce-flight: true
apply-entity-spawn-rate: true
apply-entity-spawn-limit: true
auto-purge-entities: false
world-name-format:
nether: '%overworld%_nether'
@@ -39,6 +41,7 @@ messaging:
command:
resolve-alias-name: true
simplified-destination-tab-completion: false
confirm-mode: enable
use-confirm-otp: true
confirm-timeout: 30
@@ -52,6 +55,8 @@ event-priority:
misc:
bukkit-yml-path: bukkit.yml
server-properties-path: server.properties
auto-detect-generator-plugins: true
global-debug: 0
debug-permissions: false
silent-start: false
+82 -2
View File
@@ -33,7 +33,47 @@ world:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
world_nether:
@@ -71,6 +111,46 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
+41 -1
View File
@@ -33,6 +33,46 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
+78 -3
View File
@@ -34,11 +34,46 @@ world:
pitch: 0.0
yaw: 0.0
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
spawn: falSe
tick-rate: 123
spawn-limit: -1
exceptions: [cow]
spawn-limit: '@unset'
exceptions: [ cow ]
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: # should be parsed as list of string
- a
- 1
@@ -78,6 +113,46 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
+111 -21
View File
@@ -34,16 +34,46 @@ world_the_end:
pitch: 0.0
yaw: 0.0
spawning:
animal:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
monster:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
spawn-limit: '@unset'
tick-rate: '@bukkit'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@bukkit'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
world[dot]a[dot]b:
@@ -82,16 +112,46 @@ world[dot]a[dot]b:
pitch: 0.0
yaw: 0.0
spawning:
animal:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
monster:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
spawn-limit: '@unset'
tick-rate: '@bukkit'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@bukkit'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist:
- test
version: 1.2
@@ -131,16 +191,46 @@ world[dot]a[dot]c:
pitch: 0.0
yaw: 0.0
spawning:
animal:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
monster:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
spawn-limit: '@unset'
tick-rate: '@bukkit'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@bukkit'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist:
- test
version: 1.2
+123 -3
View File
@@ -33,7 +33,47 @@ world:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
world_nether:
@@ -71,7 +111,47 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
new[dot]world:
@@ -104,6 +184,46 @@ new[dot]world:
seed: -9223372036854775808
spawn-location:
==: MVNullLocation (It's a bug if you see this in your config file)
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
@@ -33,7 +33,47 @@ world:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
world_nether:
@@ -66,6 +106,46 @@ world_nether:
seed: -9223372036854775808
spawn-location:
==: MVNullLocation (It's a bug if you see this in your config file)
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
+74 -4
View File
@@ -34,17 +34,47 @@ world:
pitch: 0.0
yaw: 0.0
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
spawn: true
tick-rate: 111
spawn-limit: -1
spawn-limit: '@unset'
exceptions:
- cow
- pig
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
spawn: false
tick-rate: -1
spawn-limit: -1
tick-rate: '@unset'
spawn-limit: '@unset'
exceptions: [ ]
world-blacklist: []
version: 1.2
@@ -83,6 +113,46 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2