Compare commits

..
263 Commits
Author SHA1 Message Date
NotAlexNoyle 8a8f65ec82 Exclude CODEOWNERS from language statistics 2026-10-27 08:25:13 -04:00
NotAlexNoyle feb854817f Add CODEOWNERS (one staff approval) 2026-10-27 08:25:13 -04:00
NotAlexNoyle 97a73bf591 Fix jar name 2025-12-13 13:20:22 -05:00
NotAlexNoyle 4e66693b21 Backport 5.3.4 to java 17 2025-12-12 00:04:56 -05:00
NotAlexNoyle f9fc0f1a28 Merge branch 'Multiverse:main' into main 2025-12-12 00:03:29 -05:00
NotAlexNoyle f246094915 Rebase on version 5 2025-12-12 00:01:29 -05:00
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
Ben Woo afa2825bc7 Merge pull request #3315 from Multiverse/feat/load-folder-check
Add folder checking before loading world
2025-07-31 16:32:37 +08:00
Ben Woo 21b4f6b087 Add folder checking before loading world 2025-07-31 15:25:14 +08:00
Ben Woo a771652c8f Merge pull request #3291 from Multiverse/5.2
5.2
2025-07-28 10:11:52 +08:00
Ben Woo e89b4f19ee Merge pull request #3314 from Multiverse/fix/debug-permissions
Fix debug-permission comment spacing
2025-07-27 14:23:34 +08:00
Ben Woo eb8ef32a94 Fix debug-permission comment spacing 2025-07-27 12:38:50 +08:00
Ben Woo f9d783bc5c Merge pull request #3311 from Multiverse/refactor/remove-deprecate
Deprecate other removeWorld methods in favour of using RemoveWorldOpt…
2025-07-27 11:36:20 +08:00
Ben Woo 87dd4168cd Merge pull request #3313 from Multiverse/fix/save-flush
Use newer save method with chunk flushing when possible
2025-07-27 11:35:35 +08:00
Ben Woo 2da6ece606 Merge pull request #3312 from Multiverse/chore/bump-deps
Bump dependencies
2025-07-27 11:35:08 +08:00
Ben Woo cbb3a7a518 Merge pull request #3310 from Multiverse/fix/spawn-command
Fix spawn command using deprecated teleport method
2025-07-27 11:34:32 +08:00
Ben Woo 57796d295b Bump dependencies 2025-07-26 12:26:38 +08:00
Ben Woo 09314e8bce Use newer save method with chunk flushing when possible 2025-07-26 12:08:23 +08:00
Ben Woo 907a6dd5c0 Fix spawn command using deprecated teleport method 2025-07-26 11:50:53 +08:00
Ben Woo a50bd807b6 Deprecate other removeWorld methods in favour of using RemoveWorldOptions 2025-07-26 11:50:02 +08:00
Ben Woo 02081b4654 Merge pull request #3305 from Multiverse/fix/text-format-null
Fix ChatTextFormatter handling null strings
2025-07-26 10:44:56 +08:00
Ben Woo 156ec7911d Merge pull request #3308 from Multiverse/fix/world-name-dot
Fix v4 config migration when world name contains dot
2025-07-26 10:44:10 +08:00
Ben Woo 2b95c62a84 Fix v4 config migration when world name contains dot 2025-07-24 22:56:08 +08:00
Ben Woo 539d5c43d1 Merge pull request #3306 from Multiverse/chore/update-game-version
Update supported game version for 1.21.7 and 1.21.8
2025-07-23 14:19:35 +08:00
Ben Woo 1664690472 Update supported game version for 1.21.7 and 1.21.8 2025-07-23 14:08:42 +08:00
Ben Woo 455396713e Fix ChatTextFormatter handling null strings 2025-07-21 22:44:45 +08:00
Ben Woo 8bc3a593d0 Merge pull request #3303 from Multiverse/feat/remove-options
Add options to remove a world without unloading or saving the bukkit …
2025-07-20 15:05:33 +08:00
Ben Woo 5177f290ae Merge pull request #3304 from Multiverse/refactor/wm-cleanup
Refactor/wm cleanup
2025-07-20 15:05:11 +08:00
Ben Woo 5118864bb8 Use our ChatTextFormatter to remove colour from string 2025-07-20 14:56:18 +08:00
Ben Woo 654da03add Remove unnecessary casting 2025-07-20 14:54:41 +08:00
Ben Woo 61229356ad Mark initAllWorlds as internal api 2025-07-20 14:53:16 +08:00
Ben Woo fa5aa41547 Add options to remove a world without unloading or saving the bukkit world 2025-07-20 14:49:57 +08:00
Ben Woo 04f07657eb Merge pull request #3301 from Multiverse/feat/import-bukkit
Improve support for importing worlds already loaded on the server
2025-07-20 11:44:45 +08:00
Ben Woo eea2af169f Improve support for importing worlds already loaded on the server 2025-07-19 23:33:51 +08:00
Ben Woo 6d393723ff Merge pull request #3299 from Multiverse/feat/skip-folder-check
Add option to skip folder check on import
2025-07-19 23:08:28 +08:00
Ben Woo 9137ef04d2 Merge pull request #3300 from Multiverse/feat/load-env-check
Add an environment check when loading a world that already exist in b…
2025-07-19 23:06:56 +08:00
Ben Woo 878c4bea3d Add option to skip folder check on import 2025-07-19 23:05:17 +08:00
Ben Woo 021d86e21c Add an environment check when loading a world that already exist in bukkit 2025-07-19 22:59:15 +08:00
Ben Woo 3e9425c8f1 Merge pull request #3297 from Multiverse/feat/matcher
Implement StringMatcher api
2025-07-18 15:02:20 +08:00
Ben Woo cc60b9c938 Implement StringMatcher api 2025-07-18 14:46:59 +08:00
Ben Woo d3cc950a97 Merge pull request #3296 from Multiverse/feat/attempt-causeby
Add causeBy chaining for Attempt
2025-07-18 11:09:41 +08:00
Ben Woo 5e97b1169b Merge pull request #3294 from Multiverse/fix/removed-property
Add warning on removed animals and monsters world properties
2025-07-18 11:07:39 +08:00
Ben Woo c3f4f117e5 Merge pull request #3293 from Multiverse/fix/delete-tab-complete
Tab complete to include unloaded worlds for delete command
2025-07-18 11:06:54 +08:00
Ben Woo 814901b1c1 Merge pull request #3292 from Multiverse/fix/spawn-egg
Always allow spawner egg to align with v4 behaviour
2025-07-18 11:06:31 +08:00
Ben Woo 7c6b1d07b0 Add causeBy chaining for Attempt 2025-07-18 11:04:41 +08:00
Ben Woo c389a41cdc Add warning on removed animals and monsters world properties 2025-07-17 12:38:33 +08:00
Ben Woo 2db9ae7c66 Tab complete to include unloaded worlds for delete command 2025-07-17 11:59:24 +08:00
Ben Woo b80aea4c38 Always allow spawner egg to align with v4 behaviour 2025-07-17 11:54:56 +08:00
Ben Woo 35b129701f Merge pull request #3285 from Multiverse/feat/joinlocation-bypass
Add bypass permission for join location
2025-07-15 13:46:45 +08:00
Ben Woo d92fe17c4f Merge branch 'main' into feat/joinlocation-bypass
# Conflicts:
#	src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java
2025-07-15 13:40:51 +08:00
Ben Woo 488e258e3f Merge pull request #3290 from Multiverse/fix/gamerule-tab-complete
Fix gamerule tab complete when legacy alias is enabled
2025-07-14 11:26:11 +08:00
Ben Woo d096d1a941 Fix gamerule command flag group name 2025-07-14 11:22:17 +08:00
Ben Woo 3ef4271e63 Fix gamerule tab complete when legacy alias is enabled 2025-07-14 11:18:25 +08:00
Ben Woo 0a33d35b2b Merge pull request #3288 from Multiverse/fix/bukkit-compatility
Fix PlayerSpawnLocationEvent may not be available on bukkit
2025-07-11 11:13:09 +08:00
Ben Woo b4fedea1db Fix PlayerSpawnLocationEvent may not be available on bukkit 2025-07-11 11:06:54 +08:00
Ben Woo 4d92964dda Merge pull request #3287 from Multiverse/fix/worldkey
Fix missing world placeholder in BUKKIT_CREATION_FAILED message
2025-07-09 18:07:57 +08:00
Ben Woo 3443b175ff Fix missing world placeholder in BUKKIT_CREATION_FAILED message 2025-07-09 17:40:11 +08:00
Ben Woo ad3fe9e114 Merge pull request #3284 from Multiverse/fix/teleport-fail-message
Make teleport failure message more descriptive
2025-07-07 11:05:14 +08:00
Ben Woo d1660d7e81 Add bypass permission for join location 2025-07-07 10:41:38 +08:00
Ben Woo 7750b61103 Make teleport failure message more descriptive 2025-07-07 10:19:26 +08:00
Ben Woo eeb1019bbb Merge pull request #3276 from Multiverse/fix/locale-lowercase
Fix message key lowercase issues if java environment is non-english
2025-06-28 17:05:09 +08:00
Ben Woo 6bbaad8bdb Merge pull request #3277 from Multiverse/fix/config-comment
Fix config.yml comment spacing
2025-06-28 17:04:48 +08:00
Missing_Love 1ef4470979 Update multiverse-core_zh.properties (#3274)
* Update multiverse-core_zh.properties

* Update multiverse-core_zh.properties
2025-06-28 17:03:41 +08:00
Ben Woo 3110f462ac Fix config.yml comment spacing 2025-06-28 17:02:10 +08:00
Ben Woo 170e1ac347 Fix message key lowercase issues if java environment is non-english 2025-06-27 18:09:27 +08:00
Ben Woo 86b95034cc Merge pull request #3275 from Multiverse/chore/dbo-1.21.6
Update dbo supported version list with 1.21.6
2025-06-27 12:39:20 +08:00
Ben Woo 8f6d4d5960 Update dbo supported version list with 1.21.6 2025-06-27 12:28:21 +08:00
Ben Woo a34d2f810b Merge pull request #3273 from Multiverse/docs/donate-links
Update donation links to our github sponsor and open collective
2025-06-26 21:09:50 +08:00
Ben Woo db002ab2b1 Update donation links to our github sponsor and open collective 2025-06-26 21:07:33 +08:00
Ben Woo c9c8a1a9c6 Merge pull request #3272 from Tisment/main
Translation of the plugin to Spanish
2025-06-26 20:00:17 +08:00
Tisment cdc22bc533 Created the file multiverse-core_es.properties 2025-06-24 11:40:02 +02:00
Ben Woo 1efe98ab46 Merge pull request #3271 from Multiverse/fix/setspawn-save
Fix setspawn command not saving the updated location to file immediately
2025-06-23 20:29:20 +08:00
Ben Woo 989242810d Fix setspawn command not saving the updated location to file immediately 2025-06-23 19:08:36 +08:00
Ben Woo 29d3c0c8c1 Merge pull request #3244 from Multiverse/5.1
5.1
2025-06-23 12:02:22 +08:00
Ben Woo 97cc7e53f0 Merge pull request #3266 from Multiverse/chore/1.21.6
Add 1.21.6 as supported version for Modrinth and Hangar
2025-06-23 11:08:10 +08:00
Ben Woo 4c4544340a Merge pull request #3270 from Multiverse/fix/dot-world-name
Fix world not loading when the world name has `.` in it
2025-06-22 17:06:54 +08:00
Ben Woo 755fab05ba Add tests for world name with dot 2025-06-22 17:02:50 +08:00
Ben Woo 5f295fd57b Fix world not loading when the world name has . in it 2025-06-22 16:49:14 +08:00
Ben Woo 86b888a68e Add 1.21.6 as supported version for modrinth and hanger 2025-06-20 22:10:42 +08:00
Ben Woo 4fd8350a96 Merge pull request #3265 from Multiverse/feat/destination-sender
Add support for destination parse with sender context
2025-06-20 15:06:30 +08:00
Ben Woo 5702426ddd Fix config test due to destination parsing 2025-06-20 15:01:01 +08:00
Ben Woo 9c562d09ea Add support for destination parse with sender context 2025-06-20 14:20:50 +08:00
Ben Woo 59d1c44c23 Merge pull request #3264 from Multiverse/feat/sender-string-parse
Add support config string parsing with sender context
2025-06-20 14:03:06 +08:00
Ben Woo 1082f73be7 Add support config string parsing with sender context 2025-06-20 12:13:14 +08:00
Ben Woo 1372626b50 Merge pull request #3263 from Multiverse/feat/config-alias-name
Add support for alias name in config node
2025-06-20 12:01:24 +08:00
Ben Woo c09dacb8ec Add support for alias name in config node 2025-06-20 11:11:07 +08:00
Ben Woo 518d64882c Merge pull request #3262 from Multiverse/feat/api-qol
Add qol methods whenLoaded and isLoaded to MultiverseCoreApi
2025-06-16 19:44:30 +08:00
Ben Woo 4f08b9a446 Add qol methods whenLoaded and isLoaded to MultiverseCoreApi 2025-06-16 11:02:19 +08:00
Ben Woo a3b7dd542d Mark CoreConfig#getConfig() as internal use only 2025-06-16 11:01:33 +08:00
Ben Woo c68936f06d Merge pull request #3261 from Multiverse/refactor/destination-suggestion
Streamline destination suggestions with helper methods to directly parse to string
2025-06-15 22:02:19 +08:00
Ben Woo b91214f958 Streamline destination suggestions with helper methods to directly parse to string 2025-06-15 21:55:01 +08:00
Ben Woo ff26ffd9c5 Merge pull request #3258 from Multiverse/feat/sender-context
Implement sender context for config node suggestions
2025-06-15 21:21:31 +08:00
Ben Woo 058a7c97cb Implement SenderNodeSuggester to builders 2025-06-15 21:19:30 +08:00
Ben Woo a50965509d Implement sender context for config node suggestions 2025-06-10 22:48:20 +08:00
Ben Woo 3356724481 Merge pull request #3256 from Multiverse/fix/generator-tab-complete
Improve generator provider tab-complete logic
2025-06-10 11:30:59 +08:00
Ben Woo 8dcdf68b9f Merge pull request #3257 from Whitebrim/patch-1
lang: fix deposit/withdraw (en)
2025-06-10 11:30:45 +08:00
Ben Woo ae0f25a0f8 Merge pull request #3255 from Whitebrim/main
Translation to Russian
2025-06-10 11:30:30 +08:00
Whitebrim 148f513bc2 lang: fix deposit/withdraw (en) 2025-06-10 03:38:57 +03:00
Whitebrim 29f60bcde5 lang: fix deposit/withdraw 2025-06-10 03:36:45 +03:00
Ben Woo e9ddb24706 Improve generator provider tab-complete logic 2025-06-09 23:16:43 +08:00
Whitebrim 71e815a9bd Create multiverse-core_ru.properties
Translated to Russian
2025-06-07 21:17:47 +03:00
Ben Woo a55ce680df Merge pull request #3254 from Multiverse/fix/world-chart
Fix bstats world chart
2025-06-07 13:42:40 +08:00
Ben Woo 3e47460c3b Fix bstats world chart 2025-06-07 13:36:57 +08:00
Ben Woo ce263b5b1f Merge pull request #3252 from Multiverse/feat/improve-context
Feat/improve context
2025-06-07 00:15:53 +08:00
Ben Woo 4ddcb3f0bf Fix teleport command tab complete for in-game with player arg 2025-06-06 23:48:24 +08:00
Ben Woo d76a6a6ca6 Refactor issuer aware context handling to extract our similar logic 2025-06-06 23:08:20 +08:00
Ben Woo e9a20254b0 Merge pull request #3249 from Multiverse/feat/passengers
Add support for passenger and vehicle teleports
2025-06-06 20:32:24 +08:00
Ben Woo 42a8d33824 Fix some javadocs of the new attempt methods 2025-06-06 20:19:19 +08:00
Ben Woo d89f00a607 Merge pull request #3250 from Multiverse/chore/annotations
Add ApiStatus annotations to new and deprecated api
2025-06-06 20:12:43 +08:00
Ben Woo 41c8c17bc9 Fix config test 2025-06-06 20:07:57 +08:00
Ben Woo a3a621945a Add ApiStatus annotations to new and deprecated api 2025-06-06 20:06:20 +08:00
Ben Woo 71eedd392d Add support for passenger and vehicle teleports 2025-06-06 20:01:30 +08:00
Ben Woo ba78b7da3a Merge pull request #3245 from Multiverse/feat/chat-color
Use kyori adventure whenever possible to support rgb formatting
2025-06-02 13:07:38 +08:00
Ben Woo 38231ef0d4 Use kyori adventure whenever possible to support rgb formatting 2025-06-02 13:01:03 +08:00
Ben Woo 232d18f8da Merge pull request #3237 from Multiverse/feat/setspawn-safety
Add location safety check for setspawn command
2025-06-02 11:07:17 +08:00
Ben Woo 530a8a9c2c Merge pull request #3243 from Multiverse/feat/time-placeholder
Implement time formatting for world time placeholder
2025-06-02 11:07:06 +08:00
Ben Woo 8a96c7b610 Don't show seconds for default 12h and 24h format 2025-06-02 10:47:22 +08:00
Ben Woo bb2da157da Fix minecraft time calculation 2025-06-02 10:47:03 +08:00
Ben Woo 1c521e0f79 Implement time formatting for world time placeholder 2025-06-01 23:39:04 +08:00
Ben Woo aab13c1ec6 Add support for multiple params in placeholder 2025-06-01 23:38:12 +08:00
Ben Woo ae4d33d7b1 Fix PlayerLocation context logic 2025-05-31 16:20:51 +08:00
Ben Woo d5b2cc884b Support localization for setspawn command output 2025-05-31 16:20:34 +08:00
Ben Woo 50f6152a71 Fix sendError message type 2025-05-31 15:48:57 +08:00
Ben Woo c84ae0493c Add location safety check for setspawn command 2025-05-31 15:48:47 +08:00
185 changed files with 6593 additions and 4392 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
.gitea/CODEOWNERS linguist-detectable=false
+3
View File
@@ -0,0 +1,3 @@
# Pull requests request review from the eligible staff team(s).
# One approval from either an Admin or Developer satisfies branch protection.
* @trueog/Admins @trueog/Developers
+2
View File
@@ -0,0 +1,2 @@
github: Multiverse
open_collective: multiverse-plugins
+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.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.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.5"]}'
platform_dependencies: '{"PAPER": ["1.18.2-1.21.11"]}'
plugin_dependencies: ${{ inputs.hangar_plugin_dependencies }}
+4
View File
@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready
+4 -5
View File
@@ -9,10 +9,9 @@
[![Release](https://img.shields.io/github/v/release/multiverse/multiverse-core)](https://github.com/Multiverse/Multiverse-Core/releases/latest)
[![Pre-Release](https://img.shields.io/github/v/release/multiverse/multiverse-core?include_prereleases&label=Pre-release)](https://github.com/Multiverse/Multiverse-Core/releases)
[![Discord](https://img.shields.io/discord/325459248047980545?label=discord&logo=discord)](https://discord.gg/NZtfKky)
[![Support us on Patreon](https://img.shields.io/badge/donate-patreon-white?logo=patreon)](https://patreon.com/dumptruckman)
[![License](https://img.shields.io/github/license/Multiverse/Multiverse-Core?color=blue)](LICENSE.md)
[![Discord](https://img.shields.io/discord/325459248047980545?label=Discord&logo=discord)](https://discord.gg/NZtfKky)
[![Donate on Github Sponsor](https://img.shields.io/badge/Github%20Sponsor-Donate-pink?logo=githubsponsors)](https://github.com/sponsors/Multiverse)
[![Donate on Open Collective](https://img.shields.io/badge/Open%20Collective-Donate-blue?style=flat&logo=opencollective)](https://opencollective.com/multiverse-plugins)
# About
@@ -44,7 +43,7 @@ Simply build the source with Gradle:
* Want others to love Multiverse too? You can join the [Multiverse Discord community](https://discord.gg/NZtfKky) and help others with issues and setup!
* A Multiverse guru? You can update our [Wiki](https://github.com/Multiverse/multiverse-web) with your latest tip, tricks and guides! The wiki open for all to edit and improve.
* Love coding? You could look at ["State: Open to PR"](https://github.com/Multiverse/Multiverse-Core/labels/State%3A%20Open%20to%20PR) and ["Resolution: Accepted"](https://github.com/Multiverse/Multiverse-Core/labels/Resolution%3A%20Accepted) issues. We're always happy to receive bug fixes and feature additions as [pull requests](https://www.freecodecamp.org/news/how-to-make-your-first-pull-request-on-github-3/).
* If you'd like to make a financial contribution to the project, do consider joining our [Patreon](https://www.patreon.com/dumptruckman) or make a one-time donation [here](https://paypal.me/dumptruckman)!
* If you'd like to make a financial contribution to the project, do consider donating to our [Github Sponsors](https://github.com/sponsors/Multiverse) or [Open Collective](https://opencollective.com/multiverse-plugins)!
Additionally, we would like to give a big thanks to everyone that has supported Multiverse over the years, as well as those in the years to come. Thank you!
-117
View File
@@ -1,117 +0,0 @@
import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'org.mvplugins.multiverse-plugin' version '1.2.0'
}
group = 'org.mvplugins.multiverse.core'
description = 'Multiverse-Core'
repositories {
maven {
name = 'aikar repo'
url = uri('https://repo.aikar.co/content/groups/aikar/')
}
maven {
name = 'glaremasters repo'
url = 'https://repo.glaremasters.me/repository/towny/'
}
maven {
name = "helpchatRepoReleases"
url = uri("https://repo.helpch.at/releases/")
}
}
configure(apiDependencies) {
serverApiVersion = '1.19.4-R0.1-SNAPSHOT'
mockBukkitServerApiVersion = '1.21'
mockBukkitVersion = '4.31.1'
}
dependencies {
// Economy
externalPlugin('com.github.MilkBowl:VaultAPI:1.7.1') {
exclude group: 'org.bukkit', module: 'bukkit'
}
// PlaceholderAPI
externalPlugin 'me.clip:placeholderapi:2.11.6'
// Command Framework
shadowed 'co.aikar:acf-paper:0.5.1-SNAPSHOT'
// Config
shadowed('io.github.townyadvanced.commentedconfiguration:CommentedConfiguration:1.0.1') {
exclude group: 'org.spigotmc', module: 'spigot-api'
}
// Utils
shadowed 'io.vavr:vavr:0.10.4'
shadowed 'org.glassfish.hk2:hk2-locator:3.0.3'
shadowed('org.glassfish.hk2:hk2-inhabitant-generator:3.0.3') {
exclude group: 'org.apache.maven', module: 'maven-core'
}
shadowed('com.dumptruckman.minecraft:Logging:1.1.1') {
exclude group: 'junit', module: 'junit'
}
shadowed 'de.themoep.idconverter:mappings:1.2-SNAPSHOT'
shadowed('org.bstats:bstats-bukkit:3.1.0') {
exclude group: 'org.bukkit', module: 'bukkit'
}
shadowed 'net.minidev:json-smart:2.4.9'
shadowed 'org.jetbrains:annotations:22.0.0'
shadowed 'io.papermc:paperlib:1.0.8'
// Tests
testImplementation('com.googlecode.json-simple:json-simple:1.1.1') {
exclude group: 'junit', module: 'junit'
}
testImplementation("org.hamcrest:hamcrest:3.0")
}
ext.bitlyAccessToken = System.getenv('BITLY_ACCESS_TOKEN') ?: 'bitly-access-token'
tasks.register('prepareSource', Sync) {
inputs.property 'bitlyAccessToken', bitlyAccessToken
from sourceSets.main.java
into "$buildDir/src"
filter(ReplaceTokens, tokens: [
'bitly-access-token': bitlyAccessToken,
])
}
shadowJar {
relocate 'co.aikar', 'org.mvplugins.multiverse.external.acf'
relocate 'com.dumptruckman.minecraft.util.Logging', 'org.mvplugins.multiverse.core.utils.CoreLogging'
relocate 'com.dumptruckman.minecraft.util.DebugLog', 'org.mvplugins.multiverse.core.utils.DebugFileLogger'
relocate 'de.themoep.idconverter', 'org.mvplugins.multiverse.external.idconverter'
relocate 'io.github.townyadvanced.commentedconfiguration', 'org.mvplugins.multiverse.external.commentedconfiguration'
relocate 'org.bstats', 'org.mvplugins.multiverse.external.bstats'
relocate 'com.sun', 'org.mvplugins.multiverse.external.sun'
relocate 'net.minidev', 'org.mvplugins.multiverse.external.minidev'
relocate 'org.objectweb', 'org.mvplugins.multiverse.external.objectweb'
relocate 'io.vavr', 'org.mvplugins.multiverse.external.vavr'
relocate 'jakarta', 'org.mvplugins.multiverse.external.jakarta'
relocate 'javassist', 'org.mvplugins.multiverse.external.javassist'
relocate 'org.aopalliance', 'org.mvplugins.multiverse.external.aopalliance'
relocate 'org.glassfish', 'org.mvplugins.multiverse.external.glassfish'
relocate 'org.jvnet', 'org.mvplugins.multiverse.external.jvnet'
relocate 'org.intellij', 'org.mvplugins.multiverse.external.intellij'
relocate 'org.jetbrains', 'org.mvplugins.multiverse.external.jetbrains'
relocate 'io.papermc.lib', 'org.mvplugins.multiverse.external.paperlib'
}
publishing {
repositories {
maven {
name = "GitHubPackages"
url = "https://maven.pkg.github.com/Multiverse/Multiverse-Core"
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
}
+364
View File
@@ -0,0 +1,364 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import groovy.util.Node
import groovy.util.NodeList
import org.apache.tools.ant.filters.ReplaceTokens
import org.gradle.api.artifacts.Configuration
import org.gradle.api.credentials.PasswordCredentials
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.Sync
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.external.javadoc.StandardJavadocDocletOptions
import org.gradle.jvm.tasks.Jar
import org.gradle.language.jvm.tasks.ProcessResources
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
`java-library`
eclipse
`maven-publish`
id("com.gradleup.shadow") version "8.3.9"
kotlin("jvm") version "2.0.21"
}
group = "org.mvplugins.multiverse.core"
version = System.getenv("GITHUB_VERSION") ?: "5.3.4.17"
description = "Multiverse-Core"
val sourceSets = extensions.getByType<SourceSetContainer>()
val serverApi: Configuration by configurations.creating
val externalPlugin: Configuration by configurations.creating
val shadowed: Configuration by configurations.creating
repositories {
mavenLocal()
mavenCentral()
maven {
url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
}
maven {
url = uri("https://repo.onarandombox.com/content/groups/public")
}
maven {
url = uri("https://hub.spigotmc.org/nexus/content/groups/public/")
}
maven {
url = uri("https://jitpack.io")
}
maven {
url = uri("https://repo.minebench.de/")
}
maven {
url = uri("https://repo.maven.apache.org/maven2/")
}
maven {
name = "aikar repo"
url = uri("https://repo.aikar.co/content/groups/aikar/")
}
maven {
name = "glaremasters repo"
url = uri("https://repo.glaremasters.me/repository/towny/")
}
maven {
name = "helpchatRepoReleases"
url = uri("https://repo.helpch.at/releases/")
}
maven {
name = "papermc repo"
url = uri("https://repo.papermc.io/repository/maven-public/")
}
// Purpur API
maven("https://repo.purpurmc.org/snapshots")
}
// Configuration wiring like in the Groovy build
configurations["compileOnly"].extendsFrom(serverApi)
configurations["runtimeClasspath"].extendsFrom(serverApi)
configurations["implementation"].extendsFrom(externalPlugin)
configurations["compileClasspath"].extendsFrom(shadowed)
configurations["testCompileClasspath"].extendsFrom(shadowed)
configurations["testRuntimeClasspath"].extendsFrom(shadowed)
dependencies {
// Server API: Purpur 1.19.4
serverApi("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT")
// Economy
externalPlugin("com.github.MilkBowl:VaultAPI:1.7.1") {
exclude(group = "org.bukkit", module = "bukkit")
}
// PlaceholderAPI
externalPlugin("me.clip:placeholderapi:2.11.6")
// Command Framework
shadowed("co.aikar:acf-paper:0.5.1-SNAPSHOT")
// Config
shadowed("io.github.townyadvanced.commentedconfiguration:CommentedConfiguration:1.0.1") {
exclude(group = "org.spigotmc", module = "spigot-api")
}
// Utils
shadowed("io.vavr:vavr:0.10.4")
shadowed("org.glassfish.hk2:hk2-locator:3.0.3")
shadowed("org.glassfish.hk2:hk2-inhabitant-generator:3.0.3") {
exclude(group = "org.apache.maven", module = "maven-core")
}
shadowed("com.dumptruckman.minecraft:Logging:1.1.1") {
exclude(group = "junit", module = "junit")
}
shadowed("de.themoep.idconverter:mappings:1.2-SNAPSHOT")
shadowed("org.bstats:bstats-bukkit:3.1.0") {
exclude(group = "org.bukkit", module = "bukkit")
}
shadowed("net.minidev:json-smart:2.4.9")
shadowed("org.jetbrains:annotations:22.0.0")
shadowed("io.papermc:paperlib:1.0.8")
// Tests
testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21")
testImplementation("com.github.seeseemelk:MockBukkit-v1.19:3.1.0")
testImplementation("com.googlecode.json-simple:json-simple:1.1.1") {
exclude(group = "junit", module = "junit")
}
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("com.natpryce:hamkrest:1.8.0.1")
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
// Annotation Processors
annotationProcessor("org.glassfish.hk2:hk2-metadata-generator:3.0.3")
testAnnotationProcessor("org.glassfish.hk2:hk2-metadata-generator:3.0.3")
}
java {
// Plugin is compiled for Java 17 (GraalVM 17-friendly)
toolchain.languageVersion.set(JavaLanguageVersion.of(17))
withSourcesJar()
withJavadocJar()
}
val bitlyAccessToken: String = System.getenv("BITLY_ACCESS_TOKEN") ?: "bitly-access-token"
// === Token-filtered source generation (prepareSource) ===
val prepareSource by tasks.registering(Sync::class) {
inputs.property("bitlyAccessToken", bitlyAccessToken)
from(sourceSets["main"].java)
// avoid deprecated buildDir getter
into(layout.buildDirectory.dir("src"))
filter<ReplaceTokens>(
"tokens" to mapOf(
"bitly-access-token" to bitlyAccessToken,
),
)
}
// Use ONLY the filtered sources for compileJava (fixes duplicate classes)
tasks.named<JavaCompile>("compileJava") {
dependsOn(prepareSource)
source = prepareSource.get().outputs.files.asFileTree
sourceCompatibility = JavaVersion.VERSION_17.toString()
targetCompatibility = JavaVersion.VERSION_17.toString()
}
tasks.named<JavaCompile>("compileTestJava") {
sourceCompatibility = JavaVersion.VERSION_17.toString()
targetCompatibility = JavaVersion.VERSION_17.toString()
}
tasks.withType<JavaCompile>().configureEach {
options.encoding = "UTF-8"
options.compilerArgs.add(
"-Aorg.glassfish.hk2.metadata.location=META-INF/hk2-locator/Multiverse-Core",
)
}
tasks.withType<Javadoc>().configureEach {
(options as StandardJavadocDocletOptions).encoding = "UTF-8"
}
tasks.named<KotlinCompile>("compileTestKotlin") {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
javaParameters.set(true)
}
}
// We're not using Kotlin in the plugin itself, just tests!
tasks.named<KotlinCompile>("compileKotlin") {
enabled = false
}
configurations
.filterNot { it.name.startsWith("test") }
.forEach {
it.exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8")
}
tasks.named<ProcessResources>("processResources") {
val props = mapOf("version" to project.version.toString())
inputs.properties(props)
filteringCharset = "UTF-8"
filesMatching("plugin.yml") {
expand(props)
}
// Never up-to-date: tests depend on fresh version expansion
outputs.upToDateWhen { false }
}
tasks.named<Javadoc>("javadoc") {
source = sourceSets["main"].allJava
classpath = configurations["compileClasspath"]
}
// Removed: configurations["api"].isCanBeResolved = true (deprecated in Gradle 8/9)
val jarTask = tasks.named<Jar>("jar")
val shadowJarTask = tasks.named<ShadowJar>("shadowJar")
listOf("apiElements", "runtimeElements").forEach { configurationName ->
configurations.named(configurationName) {
outgoing.artifacts.removeIf { artifact ->
artifact.buildDependencies.getDependencies(null).contains(jarTask.get())
}
outgoing.artifact(shadowJarTask)
}
}
tasks.named<ShadowJar>("shadowJar") {
relocate("co.aikar", "org.mvplugins.multiverse.external.acf")
relocate("com.dumptruckman.minecraft.util.Logging", "org.mvplugins.multiverse.core.utils.CoreLogging")
relocate("com.dumptruckman.minecraft.util.DebugLog", "org.mvplugins.multiverse.core.utils.DebugFileLogger")
relocate("de.themoep.idconverter", "org.mvplugins.multiverse.external.idconverter")
relocate("io.github.townyadvanced.commentedconfiguration", "org.mvplugins.multiverse.external.commentedconfiguration")
relocate("me.main__.util", "org.mvplugins.multiverse.external.serializationconfig")
relocate("org.bstats", "org.mvplugins.multiverse.external.bstats")
relocate("com.sun", "org.mvplugins.multiverse.external.sun")
relocate("net.minidev", "org.mvplugins.multiverse.external.minidev")
relocate("org.objectweb", "org.mvplugins.multiverse.external.objectweb")
relocate("io.vavr", "org.mvplugins.multiverse.external.vavr")
relocate("jakarta", "org.mvplugins.multiverse.external.jakarta")
relocate("javassist", "org.mvplugins.multiverse.external.javassist")
relocate("org.aopalliance", "org.mvplugins.multiverse.external.aopalliance")
relocate("org.glassfish", "org.mvplugins.multiverse.external.glassfish")
relocate("org.jvnet", "org.mvplugins.multiverse.external.jvnet")
relocate("org.intellij", "org.mvplugins.multiverse.external.intellij")
relocate("org.jetbrains", "org.mvplugins.multiverse.external.jetbrains")
relocate("io.papermc.lib", "org.mvplugins.multiverse.external.paperlib")
configurations = listOf(project.configurations["shadowed"])
archiveClassifier.set("")
dependencies {
exclude { dependency ->
dependency.moduleGroup == "org.jetbrains.kotlin"
}
}
}
tasks.named("build") {
dependsOn("shadowJar")
}
tasks.named<Jar>("jar") {
enabled = false
}
tasks.named<Test>("test") {
useJUnitPlatform()
testLogging {
exceptionFormat = TestExceptionFormat.FULL
}
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
pom.withXml {
val pomNode = asNode()
val dependenciesNode = (
(pomNode.get("dependencies") as? NodeList)
?.firstOrNull() as? Node
) ?: (pomNode.appendNode("dependencies") as Node)
val dependencyNodes = dependenciesNode.children().filterIsInstance<Node>().toList()
// Remove Kotlin dependency
dependencyNodes
.filter { dependencyNode ->
val groupIdNodes = dependencyNode.get("groupId") as? NodeList ?: return@filter false
val groupId = (groupIdNodes.firstOrNull() as? Node)?.text()
groupId == "org.jetbrains.kotlin"
}
.forEach { dependencyNode ->
dependenciesNode.remove(dependencyNode)
}
// Switch runtime deps to provided
dependencyNodes.forEach { dependencyNode ->
val scopeNodes = dependencyNode.get("scope") as? NodeList ?: return@forEach
val scopeNode = scopeNodes.firstOrNull() as? Node ?: return@forEach
if (scopeNode.text() == "runtime") {
scopeNode.setValue("provided")
}
}
// Add server API (Purpur) to pom as provided
configurations["serverApi"].allDependencies.forEach { dependency ->
val depNode = dependenciesNode.appendNode("dependency")
depNode.appendNode("groupId", dependency.group)
depNode.appendNode("artifactId", dependency.name)
depNode.appendNode("version", dependency.version)
depNode.appendNode("scope", "provided")
}
}
}
}
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/Multiverse/Multiverse-Core")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
maven {
name = "multiverse"
val releasesRepoUrl = uri("https://repo.dumptruckman.com/multiverse-releases")
val snapshotsRepoUrl = uri("https://repo.dumptruckman.com/multiverse-snapshots")
url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl
credentials(PasswordCredentials::class)
}
maven {
// todo: remove before mv5 release
name = "multiverseBeta"
url = uri("https://repo.c0ding.party/multiverse-beta")
credentials(PasswordCredentials::class)
}
}
}
Binary file not shown.
+2 -1
View File
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+21 -13
View File
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,10 +85,9 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -133,10 +134,13 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
@@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
Vendored
+12 -10
View File
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
-14
View File
@@ -1,14 +0,0 @@
/*
* This file was generated by the Gradle 'init' task.
*/
pluginManagement {
repositories {
gradlePluginPortal()
maven {
url = uri('https://repo.onarandombox.com/multiverse-releases')
}
}
}
rootProject.name = 'multiverse-core'
+11
View File
@@ -0,0 +1,11 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven {
url = uri("https://repo.onarandombox.com/multiverse-releases")
}
}
}
rootProject.name = "Multiverse-Core"
@@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import com.dumptruckman.minecraft.util.Logging;
@@ -10,7 +11,7 @@ import jakarta.inject.Inject;
import org.apache.commons.lang3.text.WordUtils;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.AdvancedPie;
import org.bstats.charts.MultiLineChart;
import org.bstats.charts.SingleLineChart;
import org.bukkit.World;
import org.jvnet.hk2.annotations.Service;
@@ -79,18 +80,15 @@ final class BstatsMetricsConfigurator {
}
private void addWorldCountMetric() {
addMultiLineMetric("world_count", map -> {
map.put("Loaded worlds", worldManager.getLoadedWorlds().size());
map.put("Total number of worlds", worldManager.getWorlds().size());
});
addLineMetric("worlds", () -> worldManager.getWorlds().size());
}
private void addAdvancedPieMetric(String chartId, Consumer<Map<String, Integer>> metricsFunc) {
metrics.addCustomChart(createAdvancedPieChart(chartId, metricsFunc));
}
private void addMultiLineMetric(String chartId, Consumer<Map<String, Integer>> metricsFunc) {
metrics.addCustomChart(createMultiLineChart(chartId, metricsFunc));
private void addLineMetric(String chartId, Callable<Integer> metricsFunc) {
metrics.addCustomChart(createLineChart(chartId, metricsFunc));
}
private void incrementCount(Map<String, Integer> map, String key) {
@@ -104,9 +102,7 @@ final class BstatsMetricsConfigurator {
return new AdvancedPie(chartId, () -> map);
}
private MultiLineChart createMultiLineChart(String chartId, Consumer<Map<String, Integer>> metricsFunc) {
Map<String, Integer> map = new HashMap<>();
metricsFunc.accept(map);
return new MultiLineChart(chartId, () -> map);
private SingleLineChart createLineChart(String chartId, Callable<Integer> metricsFunc) {
return new SingleLineChart(chartId, metricsFunc);
}
}
@@ -15,6 +15,7 @@ import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
@@ -123,7 +124,6 @@ public class MultiverseCore extends MultiverseModule {
public void onDisable() {
super.onDisable();
MultiverseCoreApi.shutdown();
saveAllConfigs();
shutdownDependencyInjection();
PluginServiceLocatorFactory.get().shutdown();
Logging.shutdown();
@@ -214,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()));
}
/**
@@ -229,8 +229,7 @@ public class MultiverseCore extends MultiverseModule {
this.getDescription().getVersion(), getVersionAsNumber(), StringFormatter.joinAnd(getDescription().getAuthors()));
if (configProvider.get().isShowingDonateMessage()) {
Logging.config("\u001B[32mHelp dumptruckman keep this project alive. Become a patron! https://www.patreon.com/dumptruckman\u001B[39m");
Logging.config("\u001B[32mOne time donations are also appreciated: https://www.paypal.me/dumptruckman\u001B[39m");
Logging.config("\u001B[32mLoving Multiverse-Core? Please consider supporting the project with a small donation: https://github.com/sponsors/Multiverse\u001B[39m");
}
}
@@ -238,7 +237,11 @@ public class MultiverseCore extends MultiverseModule {
* Gets the MultiverseCoreApi
*
* @return The MultiverseCoreApi
*
* @deprecated Use {@link MultiverseCoreApi#get()} directly.
*/
@Deprecated(since = "5.1", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public MultiverseCoreApi getApi() {
return MultiverseCoreApi.get();
}
@@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core;
import org.bukkit.Bukkit;
import org.bukkit.plugin.ServicePriority;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
import org.mvplugins.multiverse.core.config.CoreConfig;
@@ -15,7 +16,10 @@ import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.biomeprovider.BiomeProviderFactory;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Provides access to the MultiverseCore API.
@@ -23,6 +27,7 @@ import java.util.Objects;
public class MultiverseCoreApi {
private static MultiverseCoreApi instance;
private static final List<Consumer<MultiverseCoreApi>> whenLoadedCallbacks = new ArrayList<>();
static void init(@NotNull MultiverseCore multiverseCore) {
if (instance != null) {
@@ -30,6 +35,9 @@ public class MultiverseCoreApi {
}
instance = new MultiverseCoreApi(multiverseCore.getServiceLocator());
Bukkit.getServicesManager().register(MultiverseCoreApi.class, instance, multiverseCore, ServicePriority.Normal);
whenLoadedCallbacks.forEach(c -> c.accept(instance));
whenLoadedCallbacks.clear();
}
static void shutdown() {
@@ -37,10 +45,49 @@ public class MultiverseCoreApi {
instance = null;
}
/**
* Hook your plugin into the MultiverseCoreApi here to ensure you only start using the API after it has been initialized.
* Use this if you know your plugin may load before Multiverse-Core is fully initialized.
* <br/>
* This handy method removes the need for you to check with plugin manager or listen to plugin enable event.
* <br/>
* Callback will be called immediately if the MultiverseCoreApi has already been initialized.
*
* @param consumer The callback to execute when the MultiverseCoreApi has been initialized.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public static void whenLoaded(@NotNull Consumer<MultiverseCoreApi> consumer) {
if (instance != null) {
consumer.accept(instance);
} else {
whenLoadedCallbacks.add(consumer);
}
}
/**
* Checks if the MultiverseCoreApi has been initialized.
*
* @return True if the MultiverseCoreApi has been initialized, false otherwise
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public static boolean isLoaded() {
return instance != null;
}
/**
* Gets the MultiverseCoreApi. This will throw an exception if the Multiverse-Core has not been initialized.
* <br/>
* You can check if the MultiverseCoreApi has been initialized with {@link #isLoaded()} before using this method.
* <br/>
* Alternatively, you can use {@link #whenLoaded(Consumer)} to hook into the MultiverseCoreApi if your plugin may
* load before Multiverse-Core is fully initialized.
*
* @return The MultiverseCoreApi
* @throws IllegalStateException if the MultiverseCoreApi has not been initialized
*/
public static @NotNull MultiverseCoreApi get() {
if (instance == null) {
@@ -1,6 +1,6 @@
package org.mvplugins.multiverse.core;
import io.vavr.control.Option;
import com.google.common.collect.Lists;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
@@ -13,11 +13,15 @@ import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.utils.MinecraftTimeFormatter;
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.WorldManager;
import java.util.List;
import java.util.Locale;
@Service
final class PlaceholderExpansionHook extends PlaceholderExpansion {
@@ -68,34 +72,56 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
@Override
public @Nullable String onRequest(OfflinePlayer offlinePlayer, @NotNull String params) {
// Split string in to an Array with underscores
String[] paramsArray = REPatterns.UNDERSCORE.split(params, 2);
List<String> paramsArray = Lists.newArrayList(REPatterns.UNDERSCORE.split(params));
// No placeholder defined
if (paramsArray.length < 1) {
warning("No placeholder defined");
if (paramsArray.isEmpty()) {
warning("No placeholder key defined");
return null;
}
final var placeholder = paramsArray[0];
Option<LoadedMultiverseWorld> targetWorld;
final var placeholder = paramsArray.remove(0);
// If no world is defined, use the player's world
if (paramsArray.length == 1) {
if (!offlinePlayer.isOnline()) {
return null;
}
targetWorld = worldManager.getLoadedWorld(((Player)offlinePlayer).getWorld());
} else {
targetWorld = worldManager.getLoadedWorld(paramsArray[1]);
}
String worldName = parseWorldName(offlinePlayer, paramsArray);
if (worldName == null) return null;
// Fail if world is null
return targetWorld.map(world -> getWorldPlaceHolderValue(placeholder, world)).getOrNull();
return worldManager.getLoadedWorld(worldName)
.onEmpty(() -> warning("Multiverse World not found: " + worldName))
.map(world -> getWorldPlaceHolderValue(placeholder, paramsArray, world))
.getOrNull();
}
private @Nullable String getWorldPlaceHolderValue(@NotNull String placeholder, @NotNull LoadedMultiverseWorld world) {
private @Nullable String parseWorldName(OfflinePlayer offlinePlayer, List<String> paramsArray) {
// No world defined, get from player
if (paramsArray.isEmpty()) {
if (offlinePlayer instanceof Player player) {
return player.getWorld().getName();
} else {
warning("You must specify a world name for non-player placeholders");
return null;
}
}
// Try get from params
String paramWorldName = paramsArray.get(paramsArray.size() - 1);
if (worldManager.isLoadedWorld(paramWorldName)) {
paramsArray.remove(paramsArray.size() - 1);
return paramWorldName;
}
// Param not a world, fallback to player
if (offlinePlayer instanceof Player player) {
return player.getWorld().getName();
}
warning("Multiverse World not found: " + paramWorldName);
return null;
}
private @Nullable String getWorldPlaceHolderValue(@NotNull String placeholder,
@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();
}
@@ -138,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());
}
@@ -151,7 +183,22 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
return String.valueOf(world.getSeed());
}
case "time" -> {
return String.valueOf(world.getBukkitWorld().map(World::getTime).getOrElse(0L));
String timeFormat = !placeholderParams.isEmpty() ? placeholderParams.get(0) : "";
long time = world.getBukkitWorld().map(World::getTime).getOrElse(0L);
switch (timeFormat) {
case "" -> {
return String.valueOf(time);
}
case "12h" -> {
return MinecraftTimeFormatter.format12h(time);
}
case "24h" -> {
return MinecraftTimeFormatter.format24h(time);
}
default -> {
return MinecraftTimeFormatter.formatTime(time, timeFormat);
}
}
}
case "type" -> {
return world.getBukkitWorld().map(World::getWorldType).map(Enum::name).getOrElse("null");
@@ -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;
@@ -33,15 +35,18 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
import org.mvplugins.multiverse.core.anchor.MultiverseAnchor;
import org.mvplugins.multiverse.core.command.context.issueraware.IssuerAwareValue;
import org.mvplugins.multiverse.core.command.context.issueraware.MultiverseWorldValue;
import org.mvplugins.multiverse.core.command.context.issueraware.PlayerArrayValue;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.config.node.functions.DefaultSuggesterProvider;
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;
@@ -84,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);
@@ -96,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);
@@ -129,6 +134,22 @@ public class MVCommandCompletions extends PaperCommandCompletions {
if (context.hasConfig("playerOnly") && !context.getIssuer().isPlayer()) {
return Collections.emptyList();
}
if (context.hasConfig("byIssuerForArg")) {
Boolean byIssuerForArg = Try.of(() -> context.getContextValueByName(IssuerAwareValue.class, context.getConfig("byIssuerForArg")))
.map(IssuerAwareValue::isByIssuer)
.getOrElse(false);
if (!byIssuerForArg) {
return Collections.emptyList();
}
}
if (context.hasConfig("notByIssuerForArg")) {
Boolean byIssuerForArg = Try.of(() -> context.getContextValueByName(IssuerAwareValue.class, context.getConfig("notByIssuerForArg")))
.map(IssuerAwareValue::isByIssuer)
.getOrElse(false);
if (byIssuerForArg) {
return Collections.emptyList();
}
}
if (context.hasConfig("resolveUntil")) {
if (!Try.run(() -> context.getContextValueByName(Object.class, context.getConfig("resolveUntil"))).isSuccess()) {
return Collections.emptyList();
@@ -157,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();
}
@@ -195,27 +216,32 @@ public class MVCommandCompletions extends PaperCommandCompletions {
}
private Collection<String> suggestDestinations(BukkitCommandCompletionContext context) {
return Try.of(() -> context.getContextValue(Player[].class))
return Try.of(() -> context.getContextValue(PlayerArrayValue.class))
.map(PlayerArrayValue::value)
.recover(IllegalStateException.class, e -> context.getContextValue(Player[].class))
.recover(IllegalStateException.class, e -> new Player[]{context.getContextValue(Player.class)})
.map(players -> {
if (players.length == 0) {
// 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))
.map(packet -> packet.destination() instanceof WorldDestination
? packet.destinationString()
: packet.destination().getIdentifier() + ":" + packet.destinationString())
.checkDestinationPacketPermission(teleporter, teleportees, packet))
.map(DestinationSuggestionPacket::parsableString)
.toList();
}
@@ -248,7 +274,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
private Collection<String> suggestMVConfigValues(BukkitCommandCompletionContext context) {
return Try.of(() -> context.getContextValue(String.class))
.map(propertyName -> config.getStringPropertyHandle()
.getSuggestedPropertyValue(propertyName, context.getInput(), PropertyModifyAction.SET))
.getSuggestedPropertyValue(propertyName, context.getInput(), PropertyModifyAction.SET, context.getSender()))
.getOrElse(Collections.emptyList());
}
@@ -258,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" -> {
@@ -283,7 +306,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
private Collection<String> suggestMVWorldPropsName(BukkitCommandCompletionContext context) {
return Try.of(() -> {
MultiverseWorld world = context.getContextValue(MultiverseWorld.class);
MultiverseWorld world = context.getContextValue(MultiverseWorldValue.class).value();
PropertyModifyAction action = context.getContextValue(PropertyModifyAction.class);
return world.getStringPropertyHandle().getModifiablePropertyNames(action);
}).getOrElse(Collections.emptyList());
@@ -291,10 +314,10 @@ public class MVCommandCompletions extends PaperCommandCompletions {
private Collection<String> suggestMVWorldPropsValue(BukkitCommandCompletionContext context) {
return Try.of(() -> {
MultiverseWorld world = context.getContextValue(MultiverseWorld.class);
MultiverseWorld world = context.getContextValue(MultiverseWorldValue.class).value();
PropertyModifyAction action = context.getContextValue(PropertyModifyAction.class);
String propertyName = context.getContextValue(String.class);
return world.getStringPropertyHandle().getSuggestedPropertyValue(propertyName, context.getInput(), action);
return world.getStringPropertyHandle().getSuggestedPropertyValue(propertyName, context.getInput(), action, context.getSender());
}).getOrElse(Collections.emptyList());
}
@@ -335,7 +358,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
return world.getEntitySpawnConfig()
.getSpawnCategoryConfig(spawnCategory)
.getStringPropertyHandle()
.getSuggestedPropertyValue(propertyName, context.getInput(), action);
.getSuggestedPropertyValue(propertyName, context.getInput(), action, context.getSender());
}).getOrElse(Collections.emptyList());
}
}
@@ -7,11 +7,19 @@ import co.aikar.commands.ACFUtil;
import co.aikar.commands.BukkitCommandExecutionContext;
import co.aikar.commands.BukkitCommandIssuer;
import co.aikar.commands.InvalidCommandArgument;
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;
import org.bukkit.GameRule;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.BlockCommandSender;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.entity.SpawnCategory;
import org.jetbrains.annotations.Nullable;
@@ -20,6 +28,10 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
import org.mvplugins.multiverse.core.anchor.MultiverseAnchor;
import org.mvplugins.multiverse.core.command.context.GameRuleValue;
import org.mvplugins.multiverse.core.command.context.issueraware.IssuerAwareContextBuilder;
import org.mvplugins.multiverse.core.command.context.PlayerLocation;
import org.mvplugins.multiverse.core.command.context.issueraware.MultiverseWorldValue;
import org.mvplugins.multiverse.core.command.context.issueraware.PlayerArrayValue;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
@@ -27,6 +39,7 @@ import org.mvplugins.multiverse.core.display.filters.ContentFilter;
import org.mvplugins.multiverse.core.display.filters.DefaultContentFilter;
import org.mvplugins.multiverse.core.display.filters.RegexContentFilter;
import org.mvplugins.multiverse.core.exceptions.command.MVInvalidCommandArgument;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
@@ -69,13 +82,16 @@ public class MVCommandContexts extends PaperCommandContexts {
registerContext(GameRule.class, this::parseGameRule);
registerContext(GameRuleValue.class, this::parseGameRuleValue);
registerContext(GeneratorPlugin.class, this::parseGeneratorPlugin);
registerIssuerAwareContext(LoadedMultiverseWorld.class, this::parseLoadedMultiverseWorld);
registerIssuerAwareContext(LoadedMultiverseWorld[].class, this::parseLoadedMultiverseWorldArray);
registerIssuerAwareContext(MultiverseWorld.class, this::parseMultiverseWorld);
registerIssuerAwareContext(MultiverseWorld[].class, this::parseMultiverseWorldArray);
registerIssuerAwareContext(LoadedMultiverseWorld.class, loadedMultiverseWorldContextBuilder().generateContext());
registerIssuerAwareContext(LoadedMultiverseWorld[].class, loadedMultiverseWorldArrayContextBuilder().generateContext());
registerIssuerAwareContext(MultiverseWorld.class, multiverseWorldContextBuilder().generateContext());
registerIssuerAwareContext(MultiverseWorldValue.class, multiverseWorldContextBuilder().generateContext(MultiverseWorldValue::new));
registerIssuerAwareContext(MultiverseWorld[].class, multiverseWorldArrayContextBuilder().generateContext());
registerContext(MultiverseAnchor.class, this::parseMultiverseAnchor);
registerIssuerAwareContext(Player.class, this::parsePlayer);
registerIssuerAwareContext(Player[].class, this::parsePlayerArray);
registerIssuerAwareContext(Player.class, playerContextBuilder().generateContext());
registerIssuerAwareContext(Player[].class, playerArrayContextBuilder().generateContext());
registerIssuerAwareContext(PlayerArrayValue.class, playerArrayContextBuilder().generateContext(PlayerArrayValue::new));
registerIssuerAwareContext(PlayerLocation.class, this::parsePlayerLocation);
registerContext(SpawnCategory[].class, this::parseSpawnCategories);
}
@@ -100,8 +116,9 @@ public class MVCommandContexts extends PaperCommandContexts {
throw new InvalidCommandArgument("No destination specified.");
}
return destinationsProvider.parseDestination(destination)
.getOrThrow(failure -> MVInvalidCommandArgument.of(failure.getFailureMessage()));
return destinationsProvider.parseDestination(context.getSender(), destination)
.getOrThrow(failure ->
new InvalidCommandArgument(failure.getFailureMessage().formatted(context.getIssuer())));
}
private GameRule<?> parseGameRule(BukkitCommandExecutionContext context) {
@@ -149,107 +166,42 @@ public class MVCommandContexts extends PaperCommandContexts {
return generatorPlugin;
}
private LoadedMultiverseWorld parseLoadedMultiverseWorld(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
// Get world based on sender only
if (resolve.equals("issuerOnly")) {
if (context.getIssuer().isPlayer()) {
return worldManager.getLoadedWorld(context.getIssuer().getPlayer().getWorld()).getOrNull();
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("This command can only be used by a player in a Multiverse World.");
}
String worldName = context.getFirstArg();
LoadedMultiverseWorld world = getLoadedMultiverseWorld(worldName);
// Get world based on input, fallback to sender if input is not a world
if (resolve.equals("issuerAware")) {
if (world != null) {
context.popFirstArg();
return world;
}
if (context.getIssuer().isPlayer()) {
return worldManager.getLoadedWorld(context.getPlayer().getWorld())
.getOrElseThrow(() -> new InvalidCommandArgument("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world."));
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("World '" + worldName + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console.");
}
// Get world based on input only
if (world != null) {
context.popFirstArg();
return world;
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world.");
private IssuerAwareContextBuilder<LoadedMultiverseWorld> loadedMultiverseWorldContextBuilder() {
return new IssuerAwareContextBuilder<LoadedMultiverseWorld>()
.fromPlayer((context, player) -> worldManager.getLoadedWorld(player.getWorld()).getOrNull())
.fromInput((context, input) -> getLoadedMultiverseWorld(input))
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a loaded Multiverse World."))
.issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a loaded multiverse world. Either specify a multiverse world name or use this command in a loaded multiverse world."))
.issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console."))
.inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a loaded multiverse world."));
}
private LoadedMultiverseWorld[] parseLoadedMultiverseWorldArray(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
// Get world based on sender only
if (resolve.equals("issuerOnly")) {
if (context.getIssuer().isPlayer()) {
LoadedMultiverseWorld playerWorld = worldManager.getLoadedWorld(context.getIssuer().getPlayer().getWorld())
.getOrElseThrow(() -> new InvalidCommandArgument("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world."));
return new LoadedMultiverseWorld[]{playerWorld};
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("This command can only be used by a player in a Multiverse World.");
}
String worldStrings = context.getFirstArg();
String[] worldNames = worldStrings == null ? new String[0] : REPatterns.COMMA.split(worldStrings);
Set<LoadedMultiverseWorld> worlds = new HashSet<>(worldNames.length);
for (String worldName : worldNames) {
if ("*".equals(worldName)) {
worlds.addAll(worldManager.getLoadedWorlds());
break;
}
LoadedMultiverseWorld world = getLoadedMultiverseWorld(worldName);
if (world == null) {
throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world.");
}
worlds.add(world);
}
// Get world based on input, fallback to sender if input is not a world
if (resolve.equals("issuerAware")) {
if (!worlds.isEmpty()) {
context.popFirstArg();
return worlds.toArray(new LoadedMultiverseWorld[0]);
}
if (context.getIssuer().isPlayer()) {
LoadedMultiverseWorld playerWorld = worldManager.getLoadedWorld(context.getIssuer().getPlayer().getWorld())
.getOrElseThrow(() -> new InvalidCommandArgument("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world."));
return new LoadedMultiverseWorld[]{playerWorld};
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("Worlds '" + worldStrings + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console.");
}
// Get world based on input only
if (!worlds.isEmpty()) {
context.popFirstArg();
return worlds.toArray(new LoadedMultiverseWorld[0]);
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("World " + worldStrings + " is not a loaded multiverse world.");
private IssuerAwareContextBuilder<LoadedMultiverseWorld[]> loadedMultiverseWorldArrayContextBuilder() {
return new IssuerAwareContextBuilder<LoadedMultiverseWorld[]>()
.fromPlayer((context, player) -> worldManager.getLoadedWorld(player.getWorld())
.map(world -> new LoadedMultiverseWorld[]{world})
.getOrNull())
.fromInput((context, input) -> {
String[] worldNames = input == null ? new String[0] : REPatterns.COMMA.split(input);
Set<LoadedMultiverseWorld> worlds = new HashSet<>(worldNames.length);
for (String worldName : worldNames) {
if ("*".equals(worldName)) {
worlds.addAll(worldManager.getLoadedWorlds());
break;
}
LoadedMultiverseWorld world = getLoadedMultiverseWorld(worldName);
if (world == null) {
throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world.");
}
worlds.add(world);
}
return worlds.isEmpty() ? null : worlds.toArray(new LoadedMultiverseWorld[0]);
})
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a loaded Multiverse World."))
.issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a loaded multiverse world. Either specify a multiverse world name or use this command in a loaded multiverse world."))
.issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console."))
.inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a loaded multiverse world."));
}
@Nullable
@@ -259,107 +211,41 @@ public class MVCommandContexts extends PaperCommandContexts {
: worldManager.getLoadedWorld(worldName).getOrNull();
}
private MultiverseWorld parseMultiverseWorld(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
// Get world based on sender only
if (resolve.equals("issuerOnly")) {
if (context.getIssuer().isPlayer()) {
return worldManager.getWorld(context.getIssuer().getPlayer().getWorld()).getOrNull();
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("This command can only be used by a player in a Multiverse World.");
}
String worldName = context.getFirstArg();
MultiverseWorld world = getMultiverseWorld(worldName);
// Get world based on input, fallback to sender if input is not a world
if (resolve.equals("issuerAware")) {
if (world != null) {
context.popFirstArg();
return world;
}
if (context.getIssuer().isPlayer()) {
return worldManager.getWorld(context.getPlayer().getWorld())
.getOrElseThrow(() -> new InvalidCommandArgument("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world."));
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("World '" + worldName + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console.");
}
// Get world based on input only
if (world != null) {
context.popFirstArg();
return world;
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world.");
private IssuerAwareContextBuilder<MultiverseWorld> multiverseWorldContextBuilder() {
return new IssuerAwareContextBuilder<MultiverseWorld>()
.fromPlayer((context, player) -> worldManager.getWorld(player.getWorld()).getOrNull())
.fromInput((context, input) -> getMultiverseWorld(input))
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a Multiverse World."))
.issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world."))
.issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a multiverse world. Remember to specify the world name when using this command in console."))
.inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a multiverse world."));
}
private MultiverseWorld[] parseMultiverseWorldArray(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
// Get world based on sender only
if (resolve.equals("issuerOnly")) {
if (context.getIssuer().isPlayer()) {
MultiverseWorld playerWorld = worldManager.getWorld(context.getIssuer().getPlayer().getWorld())
.getOrElseThrow(() -> new InvalidCommandArgument("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world."));
return new MultiverseWorld[]{playerWorld};
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("This command can only be used by a player in a Multiverse World.");
}
String worldStrings = context.getFirstArg();
String[] worldNames = worldStrings == null ? new String[0] : REPatterns.COMMA.split(worldStrings);
Set<MultiverseWorld> worlds = new HashSet<>(worldNames.length);
for (String worldName : worldNames) {
if ("*".equals(worldName)) {
worlds.addAll(worldManager.getWorlds());
break;
}
MultiverseWorld world = getMultiverseWorld(worldName);
if (world == null) {
throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world.");
}
worlds.add(world);
}
// Get world based on input, fallback to sender if input is not a world
if (resolve.equals("issuerAware")) {
if (!worlds.isEmpty()) {
context.popFirstArg();
return worlds.toArray(new MultiverseWorld[0]);
}
if (context.getIssuer().isPlayer()) {
MultiverseWorld playerWorld = worldManager.getWorld(context.getIssuer().getPlayer().getWorld())
.getOrElseThrow(() -> new InvalidCommandArgument("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world."));
return new MultiverseWorld[]{playerWorld};
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("Worlds '" + worldStrings + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console.");
}
// Get world based on input only
if (!worlds.isEmpty()) {
context.popFirstArg();
return worlds.toArray(new MultiverseWorld[0]);
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("World " + worldStrings + " is not a loaded multiverse world.");
private IssuerAwareContextBuilder<MultiverseWorld[]> multiverseWorldArrayContextBuilder() {
return new IssuerAwareContextBuilder<MultiverseWorld[]>()
.fromPlayer((context, player) -> worldManager.getWorld(player.getWorld())
.map(world -> new MultiverseWorld[]{world})
.getOrNull())
.fromInput((context, input) -> {
String[] worldNames = input == null ? new String[0] : REPatterns.COMMA.split(input);
Set<MultiverseWorld> worlds = new HashSet<>(worldNames.length);
for (String worldName : worldNames) {
if ("*".equals(worldName)) {
worlds.addAll(worldManager.getWorlds());
break;
}
MultiverseWorld world = getMultiverseWorld(worldName);
if (world == null) {
throw new InvalidCommandArgument("World " + worldName + " is not a multiverse world.");
}
worlds.add(world);
}
return worlds.isEmpty() ? null : worlds.toArray(new MultiverseWorld[0]);
})
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a Multiverse World."))
.issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world."))
.issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a multiverse world. Remember to specify the world name when using this command in console."))
.inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a multiverse world."));
}
@Nullable
@@ -375,92 +261,122 @@ public class MVCommandContexts extends PaperCommandContexts {
.getOrElseThrow(() -> new InvalidCommandArgument("The anchor '" +anchorName + "' does not exist."));
}
private Player parsePlayer(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
// Get player based on sender only
if (resolve.equals("issuerOnly")) {
if (context.getIssuer().isPlayer()) {
return context.getIssuer().getPlayer();
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("This command can only be used by a player.");
}
String playerIdentifier = context.getFirstArg();
Player player = PlayerFinder.get(playerIdentifier, context.getSender());
// Get player based on input, fallback to sender if input is not a player
if (resolve.equals("issuerAware")) {
if (player != null) {
context.popFirstArg();
return player;
}
if (context.getIssuer().isPlayer()) {
return context.getIssuer().getPlayer();
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("Invalid player: " + playerIdentifier
+ ". Either specify an online player or use this command as a player.");
}
// Get player based on input only
if (player != null) {
context.popFirstArg();
return player;
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("Player " + playerIdentifier + " not found.");
private IssuerAwareContextBuilder<Player> playerContextBuilder() {
return new IssuerAwareContextBuilder<Player>()
.fromPlayer((context, player) -> player)
.fromInput((context, input) -> PlayerFinder.get(input, context.getSender()))
.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."));
}
private Player[] parsePlayerArray(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
private IssuerAwareContextBuilder<Player[]> playerArrayContextBuilder() {
return new IssuerAwareContextBuilder<Player[]>()
.fromPlayer((context, player) -> new Player[]{player})
.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) -> {
if (PlayerFinder.isSelector(input)) {
return Message.of("No player(s) matched selector: " + input + ".");
}
return Message.of("Player(s) " + input + " not found.");
});
}
// Get player based on sender only
if (resolve.equals("issuerOnly")) {
if (context.getIssuer().isPlayer()) {
return new Player[]{context.getIssuer().getPlayer()};
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("This command can only be used by a player.");
}
private PlayerLocation parsePlayerLocation(BukkitCommandExecutionContext context) {
Try<Location> location = Try.of(() -> parseLocationFromInput(context.getFirstArg(), context.getSender()));
String playerIdentifier = context.getFirstArg();
Player[] players = PlayerFinder.getMulti(playerIdentifier, context.getSender()).toArray(new Player[0]);
// Get player based on input, fallback to sender if input is not a player
if (resolve.equals("issuerAware")) {
if (players.length > 0) {
context.popFirstArg();
return players;
}
if (context.getIssuer().isPlayer()) {
return new Player[]{context.getIssuer().getPlayer()};
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("Invalid player: " + playerIdentifier
+ ". Either specify an online player or use this command as a player.");
}
// Get player based on input only
if (players.length > 0) {
if (location.isSuccess()) {
context.popFirstArg();
return players;
return new PlayerLocation(location.get());
}
if (context.isOptional()) {
return null;
if (context.getPlayer() != null) {
return new PlayerLocation(context.getPlayer().getLocation());
}
if (context.getFirstArg() == null) {
throw new InvalidCommandArgument("You must specify a world location when using this command in console.");
}
if (location.getCause() instanceof InvalidCommandArgument) {
throw (InvalidCommandArgument)location.getCause();
}
throw new RuntimeException(location.getCause());
}
// copied from ACF
private Location parseLocationFromInput(String input, CommandSender sender) {
String[] split = REPatterns.COLON.split(input, 2);
if (split.length == 0) {
throw new InvalidCommandArgument(true);
} else if (split.length < 2 && !(sender instanceof Player) && !(sender instanceof BlockCommandSender)) {
throw new InvalidCommandArgument(MinecraftMessageKeys.LOCATION_PLEASE_SPECIFY_WORLD, new String[0]);
} else {
Location sourceLoc = null;
String world;
String rest;
if (split.length == 2) {
world = split[0];
rest = split[1];
} else if (sender instanceof Player) {
sourceLoc = ((Player)sender).getLocation();
world = sourceLoc.getWorld().getName();
rest = split[0];
} else {
if (!(sender instanceof BlockCommandSender)) {
throw new InvalidCommandArgument(true);
}
sourceLoc = ((BlockCommandSender)sender).getBlock().getLocation();
world = sourceLoc.getWorld().getName();
rest = split[0];
}
boolean rel = rest.startsWith("~");
split = REPatterns.COMMA.split(rel ? rest.substring(1) : rest);
if (split.length < 3) {
throw new InvalidCommandArgument(MinecraftMessageKeys.LOCATION_PLEASE_SPECIFY_XYZ, new String[0]);
} else {
Double x = ACFUtil.parseDouble(split[0], rel ? (double)0.0F : null);
Double y = ACFUtil.parseDouble(split[1], rel ? (double)0.0F : null);
Double z = ACFUtil.parseDouble(split[2], rel ? (double)0.0F : null);
if (sourceLoc != null && rel) {
x = x + sourceLoc.getX();
y = y + sourceLoc.getY();
z = z + sourceLoc.getZ();
} else if (rel) {
throw new InvalidCommandArgument(MinecraftMessageKeys.LOCATION_CONSOLE_NOT_RELATIVE, new String[0]);
}
if (x != null && y != null && z != null) {
World worldObj = Bukkit.getWorld(world);
if (worldObj == null) {
throw new InvalidCommandArgument(MinecraftMessageKeys.INVALID_WORLD, new String[0]);
} else if (split.length >= 5) {
Float yaw = ACFUtil.parseFloat(split[3]);
Float pitch = ACFUtil.parseFloat(split[4]);
if (pitch != null && yaw != null) {
return new Location(worldObj, x, y, z, yaw, pitch);
} else {
throw new InvalidCommandArgument(MinecraftMessageKeys.LOCATION_PLEASE_SPECIFY_XYZ, new String[0]);
}
} else {
return new Location(worldObj, x, y, z);
}
} else {
throw new InvalidCommandArgument(MinecraftMessageKeys.LOCATION_PLEASE_SPECIFY_XYZ, new String[0]);
}
}
}
throw new InvalidCommandArgument("Player " + playerIdentifier + " not found.");
}
private SpawnCategory[] parseSpawnCategories(BukkitCommandExecutionContext context) {
@@ -1,7 +1,6 @@
package org.mvplugins.multiverse.core.command;
import co.aikar.commands.BukkitCommandIssuer;
import co.aikar.commands.MessageKeys;
import co.aikar.commands.MessageType;
import co.aikar.locales.MessageKeyProvider;
import org.bukkit.command.CommandSender;
@@ -9,6 +8,7 @@ import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
public class MVCommandIssuer extends BukkitCommandIssuer {
@@ -24,8 +24,13 @@ public class MVCommandIssuer extends BukkitCommandIssuer {
return commandManager;
}
@Override
public void sendMessageInternal(String message) {
ChatTextFormatter.sendFormattedMessage(getIssuer(), message);
}
public void sendError(String message, MessageReplacement... replacements) {
sendMessage(MessageType.INFO, message, replacements);
sendMessage(MessageType.ERROR, message, replacements);
}
public void sendSyntax(String message, MessageReplacement... replacements) {
@@ -107,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();
}
}
@@ -0,0 +1,16 @@
package org.mvplugins.multiverse.core.command.context;
import org.bukkit.Location;
import org.jetbrains.annotations.ApiStatus;
/**
* Wrapper for context that get location from player if executed by player,
* else requires user input of location coordinates.
*
* @param value The location
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public record PlayerLocation(Location value) {
}
@@ -0,0 +1,203 @@
package org.mvplugins.multiverse.core.command.context.issueraware;
import co.aikar.commands.BukkitCommandExecutionContext;
import co.aikar.commands.BukkitCommandIssuer;
import co.aikar.commands.InvalidCommandArgument;
import co.aikar.commands.contexts.IssuerAwareContextResolver;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.message.Message;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* Reusable logic of issuer only and issuer aware context resolvers
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public final class IssuerAwareContextBuilder<T> {
private BiFunction<BukkitCommandExecutionContext, Player, T> fromPlayer;
private BiFunction<BukkitCommandExecutionContext, String, T> fromInput;
private Function<BukkitCommandExecutionContext, Message> issuerOnlyFailMessage = context -> Message.of("This command can only be used by a player.");
private BiFunction<BukkitCommandExecutionContext, Player, Message> issuerAwarePlayerFailMessage = (context, player) -> Message.of("Unable to resolve context for player '" + player.getName() + "'.");
private BiFunction<BukkitCommandExecutionContext, String, Message> issuerAwareInputFailMessage = (context, input) -> Message.of("Unable to resolve context for input '" + input + "'.");
private BiFunction<BukkitCommandExecutionContext, String, Message> inputOnlyFailMessage = (context, input) -> Message.of("Unable to resolve context for input '" + input + "'.");
public IssuerAwareContextBuilder() {
}
/**
* Parse value from player itself.
*
* @param fromInput the parsing function
* @return The same builder for chaining
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public IssuerAwareContextBuilder<T> fromPlayer(BiFunction<BukkitCommandExecutionContext, Player, T> fromInput) {
this.fromPlayer = fromInput;
return this;
}
/**
* Parse value from command input string.
*
* @param fromInput the parsing function
* @return The same builder for chaining
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public IssuerAwareContextBuilder<T> fromInput(BiFunction<BukkitCommandExecutionContext, String, T> fromInput) {
this.fromInput = fromInput;
return this;
}
/**
* When getting value fails as issuer is not a player.
*
* @param issuerOnlyFailMessage The message
* @return The same builder for chaining
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public IssuerAwareContextBuilder<T> issuerOnlyFailMessage(Function<BukkitCommandExecutionContext, Message> issuerOnlyFailMessage) {
this.issuerOnlyFailMessage = issuerOnlyFailMessage;
return this;
}
/**
* When getting value fails as player cannot parse the value.
*
* @param issuerAwarePlayerFailMessage The message
* @return The same builder for chaining
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public IssuerAwareContextBuilder<T> issuerAwarePlayerFailMessage(BiFunction<BukkitCommandExecutionContext, Player, Message> issuerAwarePlayerFailMessage) {
this.issuerAwarePlayerFailMessage = issuerAwarePlayerFailMessage;
return this;
}
/**
* When getting value fails as input cannot parse the value.
*
* @param issuerAwareInputFailMessage The message
* @return The same builder for chaining
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public IssuerAwareContextBuilder<T> issuerAwareInputFailMessage(BiFunction<BukkitCommandExecutionContext, String, Message> issuerAwareInputFailMessage) {
this.issuerAwareInputFailMessage = issuerAwareInputFailMessage;
return this;
}
/**
* When getting value fails as input cannot parse the value.
*
* @param inputOnlyFailMessage The message
* @return The same builder for chaining
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public IssuerAwareContextBuilder<T> inputOnlyFailMessage(BiFunction<BukkitCommandExecutionContext, String, Message> inputOnlyFailMessage) {
this.inputOnlyFailMessage = inputOnlyFailMessage;
return this;
}
/**
* Creates the issuer aware context resolver logic.
*
* @return The generated context resolver
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public IssuerAwareContextResolver<T, BukkitCommandExecutionContext> generateContext() {
validateRequiredVariables();
return context -> resolveValue(context, GenericIssuerAwareValue::new).value;
}
/**
* Creates the issuer aware context resolver logic, with marking of whether the value was resolved from player or input.
*
* @param createValue The function to create the {@link IssuerAwareValue} value
* @param <I> The type of the value
* @return The generated context resolver
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public <I extends IssuerAwareValue> IssuerAwareContextResolver<I, BukkitCommandExecutionContext> generateContext(BiFunction<Boolean, T, I> createValue) {
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);
}
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);
}
}
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);
}
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(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.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;
}
}
}
@@ -0,0 +1,35 @@
package org.mvplugins.multiverse.core.command.context.issueraware;
import org.jetbrains.annotations.ApiStatus;
/**
* Base class for issuer aware values
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public abstract class IssuerAwareValue {
protected final boolean byIssuer;
/**
* Constructor to create an issuer aware value
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
protected IssuerAwareValue(boolean byIssuer) {
this.byIssuer = byIssuer;
}
/**
* Gets whether the value is by issuer or input
*
* @return true if by issuer, false if by input
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public boolean isByIssuer() {
return byIssuer;
}
}
@@ -0,0 +1,38 @@
package org.mvplugins.multiverse.core.command.context.issueraware;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
/**
* Issuer aware value wrapper for {@link MultiverseWorld}
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public final class MultiverseWorldValue extends IssuerAwareValue {
private final MultiverseWorld world;
/**
* Constructor for issuer aware value
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public MultiverseWorldValue(boolean byIssuer, MultiverseWorld world) {
super(byIssuer);
this.world = world;
}
/**
* The containing world wrapped
*
* @return wrapped world
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public MultiverseWorld value() {
return world;
}
}
@@ -0,0 +1,36 @@
package org.mvplugins.multiverse.core.command.context.issueraware;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
/**
* Issuer aware value wrapper for {@link Player} array
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public final class PlayerArrayValue extends IssuerAwareValue {
private final Player[] players;
/**
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public PlayerArrayValue(boolean byIssuer, Player[] players) {
super(byIssuer);
this.players = players;
}
/**
* The containing player array wrapped
*
* @return wrapped player array
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public Player[] value() {
return players;
}
}
@@ -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;
@@ -65,13 +65,12 @@ class ConfigCommand extends CoreCommand {
}
private void updateConfigValue(MVCommandIssuer issuer, String name, String value) {
config.getStringPropertyHandle().setPropertyString(name, value)
.onSuccess(ignore -> {
config.save();
issuer.sendMessage(MVCorei18n.CONFIG_SET_SUCCESS,
Replace.NAME.with(name),
Replace.VALUE.with(value));
})
var stringPropertyHandle = config.getStringPropertyHandle();
stringPropertyHandle.setPropertyString(issuer.getIssuer(), name, value)
.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),
@@ -57,7 +57,7 @@ class DeleteCommand extends CoreCommand {
@Subcommand("delete")
@CommandPermission("multiverse.core.delete")
@CommandCompletion("@mvworlds:scope=loaded @flags:groupName=" + RemovePlayerFlags.NAME)
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + RemovePlayerFlags.NAME)
@Syntax("<world>")
@Description("{@@mv-core.delete.description}")
void onDeleteCommand(
@@ -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();
}
}
@@ -42,7 +42,6 @@ import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
@Subcommand("gamerule|rule")
class GameruleCommand extends CoreCommand {
private final PageFilterFlags flags;
@@ -52,7 +51,7 @@ class GameruleCommand extends CoreCommand {
this.flags = flags;
}
@Subcommand("set")
@Subcommand("gamerule|rule set")
@CommandPermission("multiverse.core.gamerule.set")
@CommandCompletion("@gamerules @gamerulesvalues @mvworlds:multiple|*")
@Syntax("<Gamerule> <Gamerule value> [World or *]")
@@ -68,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) {
@@ -103,7 +102,7 @@ class GameruleCommand extends CoreCommand {
}
@SuppressWarnings("rawtypes,unchecked")
@Subcommand("reset")
@Subcommand("gamerule|rule reset")
@CommandPermission("multiverse.core.gamerule.set")
@CommandCompletion("@gamerules @mvworlds:multiple|*")
@Syntax("<Gamerule> [World or *]")
@@ -115,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) {
@@ -145,15 +144,15 @@ class GameruleCommand extends CoreCommand {
}
}
@Subcommand("list")
@Subcommand("gamerule|rule list")
@CommandPermission("multiverse.core.gamerule.list")
@CommandCompletion("@mvworlds|@flags:groupName=mvgamerulecommand @flags:groupName=" + PageFilterFlags.NAME)
@CommandCompletion("@mvworlds|@flags:groupName=" + PageFilterFlags.NAME + " @flags:groupName=" + PageFilterFlags.NAME)
@Syntax("[world] [--page <page>] [--filter <filter>]")
@Description("{@@mv-core.gamerule.list.description}")
void onGameruleListCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("<world>")
@Description("{@@mv-core.gamerule.list.description.world}")
LoadedMultiverseWorld world,
@@ -220,6 +219,11 @@ class GameruleCommand extends CoreCommand {
super.onGameruleSetCommand(issuer, gamerule, gameRuleValue, worlds);
}
@Override
void onGameruleResetCommand(MVCommandIssuer issuer, GameRule gamerule, LoadedMultiverseWorld[] worlds) {
super.onGameruleResetCommand(issuer, gamerule, worlds);
}
@Override
@CommandAlias("mvrules|mvgamerules")
void onGameruleListCommand(MVCommandIssuer issuer, LoadedMultiverseWorld world, String[] flags) {
@@ -68,7 +68,8 @@ class ImportCommand extends CoreCommand {
.biome(parsedFlags.flagValue(flags.biome, ""))
.environment(environment)
.generator(parsedFlags.flagValue(flags.generator, String.class))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn)))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn))
.doFolderCheck(!parsedFlags.hasFlag(flags.skipFolderCheck)))
.onSuccess(newWorld -> {
Logging.fine("World import success: " + newWorld);
issuer.sendInfo(MVCorei18n.IMPORT_SUCCESS, Replace.WORLD.with(newWorld.getName()));
@@ -105,6 +106,10 @@ class ImportCommand extends CoreCommand {
private final CommandValueFlag<String> biome = flag(CommandValueFlag.builder("--biome", String.class)
.addAlias("-b")
.build());
private final CommandFlag skipFolderCheck = flag(CommandFlag.builder("--skip-folder-check")
.addAlias("-f")
.build());
}
@Service
@@ -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,
@@ -3,9 +3,8 @@ package org.mvplugins.multiverse.core.commands;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Conditions;
import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Single;
import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.dumptruckman.minecraft.util.Logging;
@@ -15,19 +14,26 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
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.RemovePlayerFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.options.LoadWorldOptions;
@Service
class LoadCommand extends CoreCommand {
private final WorldManager worldManager;
private final LoadCommand.Flags flags;
@Inject
LoadCommand(@NotNull WorldManager worldManager) {
LoadCommand(@NotNull WorldManager worldManager, @NotNull Flags flags) {
this.worldManager = worldManager;
this.flags = flags;
}
@Subcommand("load")
@@ -38,13 +44,19 @@ class LoadCommand extends CoreCommand {
void onLoadCommand(
MVCommandIssuer issuer,
@Single
@Conditions("worldname:scope=unloaded")
@Syntax("<world>")
@Description("{@@mv-core.load.world.description}")
String worldName) {
issuer.sendInfo(MVCorei18n.LOAD_LOADING, Replace.WORLD.with(worldName));
worldManager.loadWorld(worldName)
MultiverseWorld world,
@Optional
@Syntax("[--remove-players]")
@Description("")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
issuer.sendInfo(MVCorei18n.LOAD_LOADING, Replace.WORLD.with(world.getName()));
worldManager.loadWorld(LoadWorldOptions.world(world)
.doFolderCheck(!parsedFlags.hasFlag(flags.skipFolderCheck)))
.onSuccess(newWorld -> {
Logging.fine("World load success: " + newWorld);
issuer.sendInfo(MVCorei18n.LOAD_SUCCESS, Replace.WORLD.with(newWorld.getName()));
@@ -54,17 +66,32 @@ class LoadCommand extends CoreCommand {
});
}
@Service
private static final class Flags extends RemovePlayerFlags {
private static final String NAME = "mvunload";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}
private final CommandFlag skipFolderCheck = flag(CommandFlag.builder("--skip-folder-check")
.addAlias("-f")
.build());
}
@Service
private static final class LegacyAlias extends LoadCommand implements LegacyAliasCommand {
@Inject
LegacyAlias(@NotNull WorldManager worldManager) {
super(worldManager);
LegacyAlias(@NotNull WorldManager worldManager, @NotNull Flags flags) {
super(worldManager, flags);
}
@Override
@CommandAlias("mvload")
void onLoadCommand(MVCommandIssuer issuer, String worldName) {
super.onLoadCommand(issuer, worldName);
void onLoadCommand(MVCommandIssuer issuer, MultiverseWorld world, String[] flagArray) {
super.onLoadCommand(issuer, world, flagArray);
}
}
}
@@ -10,27 +10,32 @@ import co.aikar.commands.annotation.Single;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import jakarta.inject.Inject;
import org.bukkit.ChatColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.context.issueraware.MultiverseWorldValue;
import org.mvplugins.multiverse.core.config.handle.PropertyModifyAction;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.util.Map;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.*;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
class ModifyCommand extends CoreCommand {
private static final Map<String, String> REMOVED_PROPERTIES = Map.of(
"animals", "/mv entity-spawn-config modify [world] animal set spawn <true|false>",
"monsters", "/mv entity-spawn-config modify [world] monster set spawn <true|false>"
);
private final WorldManager worldManager;
@Inject
@@ -40,16 +45,19 @@ class ModifyCommand extends CoreCommand {
@Subcommand("modify")
@CommandPermission("multiverse.core.modify")
@CommandCompletion("@mvworlds:scope=both @propsmodifyaction @mvworldpropsname @mvworldpropsvalue")
@Syntax("[world] <set|add|remove|reset> <property> <value>")
@CommandCompletion("@mvworlds:scope=both|@propsmodifyaction:byIssuerForArg=arg1 " +
"@propsmodifyaction:notByIssuerForArg=arg1|@mvworldpropsname:byIssuerForArg=arg1 " +
"@mvworldpropsname:notByIssuerForArg=arg1|@mvworldpropsvalue:byIssuerForArg=arg1 " +
"@mvworldpropsvalue:notByIssuerForArg=arg1")
@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 MultiverseWorld world,
@NotNull MultiverseWorldValue worldValue,
@Syntax("<set|add|remove|reset>")
@Description("")
@@ -64,6 +72,15 @@ class ModifyCommand extends CoreCommand {
@Syntax("[value]")
@Description("{@@mv-core.modify.value.description}")
@Nullable String propertyValue) {
if (REMOVED_PROPERTIES.containsKey(propertyName)) {
issuer.sendMessage(MVCorei18n.MODIFY_PROPERTYREMOVED,
replace("{property}").with(propertyName),
replace("{replacement}").with(REMOVED_PROPERTIES.get(propertyName)));
return;
}
MultiverseWorld world = worldValue.value();
if (action.isRequireValue() && propertyValue == null) {
issuer.sendMessage(MVCorei18n.MODIFY_SPECIFYVALUE,
replace("{action}").with(action.name().toLowerCase()),
@@ -77,28 +94,28 @@ class ModifyCommand extends CoreCommand {
}
StringPropertyHandle worldPropertyHandle = world.getStringPropertyHandle();
worldPropertyHandle.modifyPropertyString(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()));
}
});
}
@@ -111,7 +128,7 @@ class ModifyCommand extends CoreCommand {
@Override
@CommandAlias("mvmodify|mvm")
void onModifyCommand(MVCommandIssuer issuer, @NotNull MultiverseWorld world, @NotNull PropertyModifyAction action, @NotNull String propertyName, @Nullable String propertyValue) {
void onModifyCommand(MVCommandIssuer issuer, @NotNull MultiverseWorldValue world, @NotNull PropertyModifyAction action, @NotNull String propertyName, @Nullable String propertyValue) {
super.onModifyCommand(issuer, world, action, propertyName, propertyValue);
}
}
@@ -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
) {
@@ -15,6 +15,8 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
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.RemovePlayerFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
@@ -23,19 +25,20 @@ import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
import org.mvplugins.multiverse.core.world.options.RemoveWorldOptions;
@Service
class RemoveCommand extends CoreCommand {
private final WorldManager worldManager;
private final PlayerWorldTeleporter playerWorldTeleporter;
private final RemovePlayerFlags flags;
private final Flags flags;
@Inject
RemoveCommand(
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter,
@NotNull RemovePlayerFlags flags
@NotNull Flags flags
) {
this.worldManager = worldManager;
this.playerWorldTeleporter = playerWorldTeleporter;
@@ -44,7 +47,7 @@ class RemoveCommand extends CoreCommand {
@Subcommand("remove")
@CommandPermission("multiverse.core.remove")
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + RemovePlayerFlags.NAME)
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + Flags.NAME)
@Syntax("<world>")
@Description("{@@mv-core.remove.description}")
void onRemoveCommand(
@@ -64,12 +67,14 @@ class RemoveCommand extends CoreCommand {
? worldManager.getLoadedWorld(world).map(playerWorldTeleporter::removeFromWorld).getOrElse(AsyncAttemptsAggregate::emptySuccess)
: AsyncAttemptsAggregate.emptySuccess();
future.onSuccess(() -> doWorldRemoving(issuer, world))
future.onSuccess(() -> doWorldRemoving(issuer, world, parsedFlags))
.onFailure(() -> issuer.sendError("Failed to teleport one or more players out of the world!"));
}
private void doWorldRemoving(MVCommandIssuer issuer, MultiverseWorld world) {
worldManager.removeWorld(world)
private void doWorldRemoving(MVCommandIssuer issuer, MultiverseWorld world, ParsedCommandFlags parsedFlags) {
worldManager.removeWorld(RemoveWorldOptions.world(world)
.saveBukkitWorld(!parsedFlags.hasFlag(flags.noSave))
.unloadBukkitWorld(!parsedFlags.hasFlag(flags.noUnloadBukkitWorld)))
.onSuccess(removedWorldName -> {
Logging.fine("World remove success: " + removedWorldName);
issuer.sendInfo(MVCorei18n.REMOVE_SUCCESS, Replace.WORLD.with(removedWorldName));
@@ -79,13 +84,32 @@ class RemoveCommand extends CoreCommand {
});
}
@Service
private static final class Flags extends RemovePlayerFlags {
private static final String NAME = "mvremove";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}
private final CommandFlag noUnloadBukkitWorld = flag(CommandFlag.builder("--no-unload-bukkit-world")
.addAlias("-b")
.build());
private final CommandFlag noSave = flag(CommandFlag.builder("--no-save")
.addAlias("-n")
.build());
}
@Service
private static final class LegacyAlias extends RemoveCommand implements LegacyAliasCommand {
@Inject
LegacyAlias(
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter,
RemovePlayerFlags flags
@NotNull Flags flags
) {
super(worldManager, playerWorldTeleporter, flags);
}
@@ -1,76 +1,95 @@
package org.mvplugins.multiverse.core.commands;
import co.aikar.commands.BukkitCommandIssuer;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.Location;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.context.PlayerLocation;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.UnsafeFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.teleportation.BlockSafety;
import org.mvplugins.multiverse.core.world.WorldManager;
@Service
class SetSpawnCommand extends CoreCommand {
private final WorldManager worldManager;
private final BlockSafety blockSafety;
private final UnsafeFlags flags;
@Inject
SetSpawnCommand(@NotNull WorldManager worldManager) {
SetSpawnCommand(@NotNull WorldManager worldManager, @NotNull BlockSafety blockSafety, @NotNull UnsafeFlags flags) {
this.worldManager = worldManager;
this.blockSafety = blockSafety;
this.flags = flags;
}
@CommandAlias("mvsetspawn")
@Subcommand("setspawn")
@CommandPermission("multiverse.core.spawn.set")
@Syntax("[x],[y],[z],[pitch],[yaw]")
@CommandCompletion("@flags:groupName=" + UnsafeFlags.NAME + " @flags:resolveUntil=arg1,groupName=" + UnsafeFlags.NAME)
@Syntax("[worldname:x,y,z[,pitch,yaw]] [--unsafe]")
@Description("{@@mv-core.setspawn.description}")
void onSetSpawnCommand(
BukkitCommandIssuer issuer,
MVCommandIssuer issuer,
@Syntax("[worldname:x,y,z[,pitch,yaw]]")
@Description("{@@mv-core.setspawn.location.description}")
PlayerLocation playerLocation,
@Optional
@Syntax("<location>")
@Description("{@@mv-core.setspawn.location.description}")
Location location) {
Option.of(location).orElse(() -> {
if (issuer.isPlayer()) {
return Option.of(issuer.getPlayer().getLocation());
}
return Option.none();
}).peek(finalLocation ->
worldManager.getLoadedWorld(finalLocation.getWorld())
.peek(mvWorld -> mvWorld.setSpawnLocation(finalLocation)
.onSuccess(ignore -> issuer.sendMessage(
"Successfully set spawn in " + mvWorld.getName() + " to "
+ prettyLocation(mvWorld.getSpawnLocation())))
.onFailure(e -> issuer.sendMessage(e.getLocalizedMessage())))
.onEmpty(() -> issuer.sendMessage("That world is not loaded or does not exist!")))
.onEmpty(() -> issuer.sendMessage("You must specify a location in the format: worldname:x,y,z"));
@Syntax("[--unsafe]")
@Description("")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
Location location = playerLocation.value();
if (!parsedFlags.hasFlag(flags.unsafe) && !blockSafety.canSpawnAtLocationSafely(location)) {
issuer.sendMessage(MVCorei18n.SETSPAWN_UNSAFE);
return;
}
worldManager.getLoadedWorld(location.getWorld())
.peek(mvWorld -> mvWorld.setSpawnLocation(location)
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.SETSPAWN_SUCCESS,
Replace.WORLD.with(mvWorld.getName()),
Replace.LOCATION.with(prettyLocation(location))))
.onFailure(e -> issuer.sendMessage(MVCorei18n.SETSPAWN_FAILED,
Replace.WORLD.with(mvWorld.getName()),
Replace.ERROR.with(e))))
.onEmpty(() -> issuer.sendMessage(MVCorei18n.SETSPAWN_NOTMVWORLD,
Replace.WORLD.with(location.getWorld().getName())));
}
private String prettyLocation(Location location) {
return location.getX() + ", " + location.getY() + ", " + location.getZ() + ". pitch:" + location.getPitch()
+ ", yaw:" + location.getYaw();
return "%.2f, %.2f, %.2f, pitch:%.2f, yaw:%.2f"
.formatted(location.getX(), location.getY(), location.getZ(), location.getPitch(), location.getYaw());
}
@Service
private static final class LegacyAlias extends SetSpawnCommand implements LegacyAliasCommand {
@Inject
LegacyAlias(@NotNull WorldManager worldManager) {
super(worldManager);
LegacyAlias(@NotNull WorldManager worldManager, @NotNull BlockSafety blockSafety, @NotNull UnsafeFlags flags) {
super(worldManager, blockSafety, flags);
}
@Override
@CommandAlias("mvss")
void onSetSpawnCommand(BukkitCommandIssuer issuer, Location location) {
super.onSetSpawnCommand(issuer, location);
void onSetSpawnCommand(MVCommandIssuer issuer, PlayerLocation location, String[] flagArray) {
super.onSetSpawnCommand(issuer, location, flagArray);
}
}
}
@@ -60,7 +60,7 @@ final class SpawnCommand extends CoreCommand {
@CommandAlias("mvspawn")
@Subcommand("spawn")
@CommandPermission("@mvspawn")
@CommandCompletion("@playersarray:checkPermissions=@mvspawnother|@flags:groupName=" + UnsafeFlags.NAME + ",resolveUntil=arg1"
@CommandCompletion("@playersarray:checkPermissions=@mvspawnother|@flags:resolveUntil=arg1,groupName=" + UnsafeFlags.NAME
+ " @flags:groupName=" + UnsafeFlags.NAME)
@Syntax("[player]")
@Description("{@@mv-core.spawn.description}")
@@ -108,7 +108,7 @@ final class SpawnCommand extends CoreCommand {
safetyTeleporter.to(mvWorld.getSpawnLocation())
.by(issuer)
.checkSafety(checkSafety)
.teleport(entity)
.teleportSingle(entity)
.onSuccess(() -> issuer.sendInfo(MVCorei18n.SPAWN_SUCCESS,
Replace.PLAYER.with(entity.equals(issuer.getPlayer())
? Message.of(MVCorei18n.GENERIC_YOU)
@@ -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.getFailureMessage())));
Replace.REASON.with(failure.get(0).getFailureMessage())));
}
private void handleMultiTeleport(MVCommandIssuer issuer, LoadedMultiverseWorld mvWorld,
@@ -1,9 +1,7 @@
package org.mvplugins.multiverse.core.commands;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
@@ -20,9 +18,11 @@ import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
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;
@@ -31,7 +31,6 @@ import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker;
import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter;
import org.mvplugins.multiverse.core.teleportation.TeleportFailureReason;
@Service
final class TeleportCommand extends CoreCommand {
@@ -39,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;
@@ -57,19 +56,18 @@ final class TeleportCommand extends CoreCommand {
@CommandAlias("mvtp")
@Subcommand("teleport|tp")
@CommandPermission("@mvteleport")
@CommandCompletion(
"@destinations:playerOnly|@playersarray:checkPermissions=@mvteleportother "
+ "@destinations:othersOnly|@flags:groupName=" + UnsafeFlags.NAME + ",resolveUntil=arg2 "
+ "@flags:groupName=" + UnsafeFlags.NAME)
@CommandCompletion("@playersarray:checkPermissions=@mvteleportother|@destinations:byIssuerForArg=arg1 "
+ "@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}")
Player[] players,
PlayerArrayValue playersValue,
@Syntax("<destination>")
@Description("{@@mv-core.teleport.destination.description}")
@@ -79,6 +77,7 @@ final class TeleportCommand extends CoreCommand {
@Syntax("[--unsafe]")
@Description("")
String[] flagArray) {
Player[] players = playersValue.value();
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
if (players.length == 1) {
@@ -105,14 +104,26 @@ final class TeleportCommand extends CoreCommand {
safetyTeleporter.to(destination)
.by(issuer)
.checkSafety(!parsedFlags.hasFlag(flags.unsafe) && destination.checkTeleportSafety())
.teleport(player)
.onSuccess(() -> issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(getYouOrName(issuer, player)),
Replace.DESTINATION.with(destination.toString())))
.onFailure(failure -> issuer.sendError(MVCorei18n.TELEPORT_FAILED,
Replace.PLAYER.with(getYouOrName(issuer, player)),
Replace.DESTINATION.with(destination.toString()),
Replace.REASON.with(failure.getFailureMessage())));
.passengerMode(config.getPassengerMode())
.teleportSingle(player)
.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.getDisplayMessage()),
Replace.REASON.with(Message.of(entry.getKey())));
}
});
}
private Message getYouOrName(MVCommandIssuer issuer, Player player) {
@@ -131,19 +142,40 @@ final class TeleportCommand extends CoreCommand {
safetyTeleporter.to(destination)
.by(issuer)
.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,15 +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;
@@ -27,7 +25,8 @@ 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;
@Service
@@ -187,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}
*/
@@ -231,6 +250,31 @@ public final class CoreConfig {
return configHandle.get(configNodes.useFinerTeleportPermissions);
}
/**
* Sets the passenger mode
*
* @param passengerMode The passenger mode
* @return The set result
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public Try<Void> setPassengerMode(PassengerModes passengerMode) {
return configHandle.set(configNodes.passengerMode, passengerMode);
}
/**
* Gets the passenger mode
*
* @return The passenger mode
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public PassengerMode getPassengerMode() {
return configHandle.get(configNodes.passengerMode);
}
/**
* {@inheritDoc}
*/
@@ -491,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}
*/
@@ -575,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}
*/
@@ -632,10 +706,11 @@ public final class CoreConfig {
}
/**
* Gets the underlying config file object
* Gets the underlying config file object. For internal use only.
*
* @return The config file
*/
@ApiStatus.Internal
public FileConfiguration getConfig() {
return configHandle.getConfig();
}
@@ -4,7 +4,8 @@ import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.event.EventPriority;
import org.bukkit.plugin.PluginManager;
@@ -19,12 +20,13 @@ import org.mvplugins.multiverse.core.config.node.Node;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.config.node.functions.NodeStringParser;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.core.WorldDestination;
import org.mvplugins.multiverse.core.dynamiclistener.EventPriorityMapper;
import org.mvplugins.multiverse.core.event.MVDebugModeEvent;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.permissions.PermissionUtils;
import org.mvplugins.multiverse.core.teleportation.PassengerModes;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat;
import java.util.Collection;
@@ -75,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 #")
@@ -93,7 +92,6 @@ final class CoreConfigNodes {
.build());
final ConfigNode<Boolean> autoImportDefaultWorlds = node(ConfigNode.builder("world.auto-import-default-worlds", Boolean.class)
.comment("")
.comment("When enabled, Multiverse will automatically import default worlds defined in the server.properties")
.comment("`level-name` property when the Multiverse is enabled or reloaded. This will include the nether and ")
.comment("end if the server created them.")
@@ -110,6 +108,7 @@ final class CoreConfigNodes {
.build());
final ConfigNode<Boolean> enforceAccess = node(ConfigNode.builder("world.enforce-access", Boolean.class)
.comment("")
.comment("This setting will prevent players from entering worlds they don't have access to.")
.comment("If this is set to false, players will be able to enter any world they want.")
.comment("If this is set to true, players will only be able to enter worlds they have")
@@ -136,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.")
@@ -183,6 +208,21 @@ final class CoreConfigNodes {
.name("use-finer-teleport-permissions")
.build());
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. 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.")
.comment(" retain_passengers: Passengers will teleport together with the parent entity.")
.comment(" retain_vehicle: Vehicles will teleport together with the parent entity.")
.comment(" retain_all: All passengers and vehicles will teleport together with the parent entity.")
.defaultValue(PassengerModes.DEFAULT)
.name("passenger-mode")
.build());
final ConfigNode<Integer> concurrentTeleportLimit = node(ConfigNode.builder("teleport.concurrent-teleport-limit", Integer.class)
.comment("")
.comment("Sets the maximum number of players allowed to be teleported at once with `/mv teleport` command")
@@ -237,6 +277,7 @@ final class CoreConfigNodes {
.defaultValue("")
.name("first-spawn-location")
.suggester(this::suggestDestinations)
.stringParser(this::parseDestinationString)
.build());
final ConfigNode<Boolean> enableJoinDestination = node(ConfigNode.builder("spawn.enable-join-destination", Boolean.class)
@@ -254,6 +295,7 @@ final class CoreConfigNodes {
.defaultValue("")
.name("join-destination")
.suggester(this::suggestDestinations)
.stringParser(this::parseDestinationString)
.build());
final ConfigNode<Boolean> defaultRespawnInOverworld = node(ConfigNode.builder("spawn.default-respawn-in-overworld", Boolean.class)
@@ -353,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());
@@ -363,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")
@@ -381,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.")
@@ -435,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)
@@ -444,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)
@@ -461,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")
@@ -475,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)
@@ -492,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));
@@ -501,11 +587,12 @@ final class CoreConfigNodes {
.build());
final ConfigNode<Boolean> debugPermissions = node(ConfigNode.builder("misc.debug-permissions", Boolean.class)
.comment("")
.comment("Sets whether console will log every permission check done by all multiverse plugins.")
.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)
@@ -513,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());
@@ -532,15 +620,14 @@ final class CoreConfigNodes {
.hidden()
.build());
// todo: Maybe combine with the similar method in MVCommandCompletion but that has permission checking
private Collection<String> suggestDestinations(String input) {
return destinationsProvider.get().getDestinations().stream()
.flatMap(destination -> destination.suggestDestinations(Bukkit.getConsoleSender(), null)
.stream()
.map(packet -> destination instanceof WorldDestination
? packet.destinationString()
: destination.getIdentifier() + ":" + packet.destinationString()))
.toList();
private Collection<String> suggestDestinations(CommandSender sender, String input) {
return destinationsProvider.get().suggestDestinationStrings(sender, input);
}
private Try<String> parseDestinationString(CommandSender sender, String input, Class<String> type) {
return destinationsProvider.get().parseDestination(sender, input)
.map(DestinationInstance::toString)
.toTry();
}
private static final class DimensionFormatNodeSerializer implements NodeSerializer<DimensionFormat> {
@@ -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;
});
}
@@ -5,6 +5,9 @@ import java.util.Collections;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -40,6 +43,7 @@ public class StringPropertyHandle {
/**
* Get property names that can be modified. ADD and REMOVE actions can only be used on list nodes.
*
* @param action The target action
* @return The property names modifiable for the action.
*/
@@ -71,20 +75,47 @@ public class StringPropertyHandle {
/**
* Suggests property values for command auto-complete based on the input and action type.
*
* @param name The name of the property.
* @param input The input value to suggest based on.
* @param action The modification action being performed.
* @param name The name of the property.
* @param input The input value to suggest based on.
* @param action The modification action being performed.
* @return A collection of suggested values.
*/
public Collection<String> getSuggestedPropertyValue(
@Nullable String name, @Nullable String input, @NotNull PropertyModifyAction action) {
@Nullable String name,
@Nullable String input,
@NotNull PropertyModifyAction action
) {
return getSuggestedPropertyValue(name, input, action, Bukkit.getConsoleSender());
}
/**
* Suggests property values for command auto-complete based on the input and action type.
* <br />
* Providing a sender gives contextual information such as sender name, permissions, or player location
* for better suggestions.
*
* @param name The name of the property.
* @param input The input value to suggest based on.
* @param action The modification action being performed.
* @param sender The sender context to use.
* @return A collection of suggested values
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public Collection<String> getSuggestedPropertyValue(
@Nullable String name,
@Nullable String input,
@NotNull PropertyModifyAction action,
@NotNull CommandSender sender
) {
return switch (action) {
case SET -> findNode(name, ValueNode.class)
.map(node -> node.suggest(input))
.map(node -> node.suggest(sender, input))
.getOrElse(Collections.emptyList());
case ADD -> findNode(name, ListValueNode.class)
.map(node -> node.suggestItem(input))
.map(node -> node.suggestItem(sender, input))
.getOrElse(Collections.emptyList());
case REMOVE -> findNode(name, ListValueNode.class)
@@ -111,7 +142,7 @@ public class StringPropertyHandle {
/**
* Sets the value of the specified property name.
*
* @param name The name of the property.
* @param name The name of the property.
* @param value The value to set.
* @return A Try indicating success or failure.
*/
@@ -122,7 +153,7 @@ public class StringPropertyHandle {
/**
* Adds a value to a list property name.
*
* @param name The name of the property.
* @param name The name of the property.
* @param value The value to add.
* @return A Try indicating success or failure.
*/
@@ -133,7 +164,7 @@ public class StringPropertyHandle {
/**
* Removes a value from a list property name.
*
* @param name The name of the property.
* @param name The name of the property.
* @param value The value to remove.
* @return A Try indicating success or failure.
*/
@@ -154,9 +185,9 @@ public class StringPropertyHandle {
/**
* Modifies a property name based on the given action.
*
* @param name The name of the property.
* @param value The new value (if applicable).
* @param action The modification action.
* @param name The name of the property.
* @param value The new value (if applicable).
* @param action The modification action.
* @return A Try indicating success or failure.
*/
public Try<Void> modifyProperty(
@@ -173,59 +204,128 @@ public class StringPropertyHandle {
/**
* Sets the property value from a string representation.
*
* @param name The name of the property.
* @param name The name of the property.
* @param value The string value to set.
* @return A Try indicating success or failure.
*/
public Try<Void> setPropertyString(@Nullable String name, @Nullable String value) {
return setPropertyString(Bukkit.getConsoleSender(), name, value);
}
/**
* Sets the property value from a string representation with a sender context.
*
* @param sender The sender context.
* @param name The name of the property.
* @param value The string value to set.
* @return A Try indicating success or failure.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public Try<Void> setPropertyString(@NotNull CommandSender sender, @Nullable String name, @Nullable String value) {
return findNode(name, ValueNode.class)
.flatMap(node -> node.parseFromString(value)
.flatMap(parsedValue -> handle.set(node, parsedValue)));
.flatMap(node -> node.parseFromString(sender, value)
.flatMap(parsedValue -> handle.set(sender, node, parsedValue)));
}
/**
* Adds a value to a list property using its string representation.
*
* @param name The name of the property.
* @param name The name of the property.
* @param value The string value to add.
* @return A Try indicating success or failure.
*/
public Try<Void> addPropertyString(@Nullable String name, @Nullable String value) {
return addPropertyString(Bukkit.getConsoleSender(), name, value);
}
/**
* Adds a value to a list property using its string representation with a sender context.
*
* @param sender The sender context.
* @param name The name of the property.
* @param value The string value to add.
* @return A Try indicating success or failure.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public Try<Void> addPropertyString(@NotNull CommandSender sender, @Nullable String name, @Nullable String value) {
return findNode(name, ListValueNode.class)
.flatMap(node -> node.parseItemFromString(value)
.flatMap(node -> node.parseItemFromString(sender, value)
.flatMap(parsedValue -> handle.add(node, parsedValue)));
}
/**
* Removes a value from a list property using its string representation.
*
* @param name The name of the property.
* @param name The name of the property.
* @param value The string value to remove.
* @return A Try indicating success or failure.
*/
public Try<Void> removePropertyString(@Nullable String name, @Nullable String value) {
return removePropertyString(Bukkit.getConsoleSender(), name, value);
}
/**
* Removes a value from a list property using its string representation with a sender context.
*
* @param sender The sender context.
* @param name The name of the property.
* @param value The string value to remove.
* @return A Try indicating success or failure.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public Try<Void> removePropertyString(@NotNull CommandSender sender, @Nullable String name, @Nullable String value) {
return findNode(name, ListValueNode.class)
.flatMap(node -> node.parseItemFromString(value)
.flatMap(node -> node.parseItemFromString(sender, value)
.flatMap(parsedValue -> handle.remove(node, parsedValue)));
}
/**
* Modifies a property using a string value based on the given action.
*
* @param name The name of the property.
* @param value The string value (if applicable).
* @param action The modification action.
* @param name The name of the property.
* @param value The string value (if applicable).
* @param action The modification action.
* @return A Try indicating success or failure.
*/
public Try<Void> modifyPropertyString(
@Nullable String name, @Nullable String value, @NotNull PropertyModifyAction action) {
@Nullable String name,
@Nullable String value,
@NotNull PropertyModifyAction action
) {
return modifyPropertyString(Bukkit.getConsoleSender(), name, value, action);
}
/**
* Modifies a property using a string value based on the given action with a sender context.
*
* @param sender The sender context.
* @param name The name of the property.
* @param value The string value (if applicable).
* @param action The modification action.
* @return A Try indicating success or failure.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public Try<Void> modifyPropertyString(
@NotNull CommandSender sender,
@Nullable String name,
@Nullable String value,
@NotNull PropertyModifyAction action
) {
if (action.isRequireValue() && (value == null)) {
return Try.failure(new IllegalArgumentException("Value is required for PropertyModifyAction: " + action));
}
return switch (action) {
case SET -> setPropertyString(name, value);
case ADD -> addPropertyString(name, value);
case REMOVE -> removePropertyString(name, value);
case SET -> setPropertyString(sender, name, value);
case ADD -> addPropertyString(sender, name, value);
case REMOVE -> removePropertyString(sender, name, value);
case RESET -> resetProperty(name);
default -> Try.failure(new IllegalArgumentException("Unknown action: " + action));
};
@@ -234,9 +334,9 @@ public class StringPropertyHandle {
/**
* Finds a configuration node by name and type.
*
* @param name The name of the node.
* @param type The expected class type of the node.
* @param <T> The type of node.
* @param name The name of the node.
* @param type The expected class type of the node.
* @param <T> The type of node.
* @return A Try containing the found node or a failure if not found.
*/
private <T extends Node> Try<T> findNode(@Nullable String name, @NotNull Class<T> type) {
@@ -8,9 +8,17 @@ import java.util.function.Supplier;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.apache.logging.log4j.util.Strings;
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.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;
import org.mvplugins.multiverse.core.config.node.functions.DefaultStringParserProvider;
import org.mvplugins.multiverse.core.config.node.functions.DefaultSuggesterProvider;
@@ -42,27 +50,34 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected final @Nullable String name;
protected final @NotNull Class<T> type;
protected final @NotNull String[] aliases;
protected @Nullable Supplier<T> defaultValue;
protected @Nullable NodeSuggester suggester;
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,
@NotNull String[] comments,
@Nullable String name,
@NotNull Class<T> type,
@NotNull String[] aliases,
@Nullable Supplier<T> defaultValue,
@Nullable NodeSuggester suggester,
@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;
this.aliases = aliases;
this.defaultValue = defaultValue;
this.suggester = (suggester != null)
? suggester
@@ -74,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;
}
/**
@@ -93,6 +110,14 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return type;
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull String[] getAliases() {
return aliases;
}
/**
* {@inheritDoc}
*/
@@ -115,6 +140,17 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return Collections.emptyList();
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Collection<String> suggest(@NotNull CommandSender sender, @Nullable String input) {
if (suggester != null && suggester instanceof SenderNodeSuggester senderSuggester) {
return senderSuggester.suggest(sender, input);
}
return suggest(input);
}
/**
* {@inheritDoc}
*/
@@ -126,6 +162,17 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return Try.failure(new UnsupportedOperationException("No string parser for type " + type.getName()));
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Try<T> parseFromString(@NotNull CommandSender sender, @Nullable String input) {
if (stringParser != null && stringParser instanceof SenderNodeStringParser<T> senderStringParser) {
return senderStringParser.parse(sender, input, type);
}
return parseFromString(input);
}
/**
* {@inheritDoc}
*/
@@ -148,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);
}
}
@@ -164,12 +231,15 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected @Nullable String name;
protected @NotNull final Class<T> type;
protected @NotNull String[] aliases = Strings.EMPTY_ARRAY;
protected @Nullable Supplier<T> defaultValue;
protected @Nullable NodeSuggester suggester;
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.
@@ -243,6 +313,20 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return name(null);
}
/**
* Sets the aliases for this node. Aliases are alternative identifiers for referencing the node.
*
* @param aliases The aliases to set for this node.
* @return This builder.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public @NotNull B aliases(@NotNull String... aliases) {
this.aliases = aliases;
return self();
}
/**
* Sets the suggester for this node.
*
@@ -254,6 +338,20 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return self();
}
/**
* Sets the suggester for this node with sender context.
*
* @param suggester The suggester for this node.
* @return This builder.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public @NotNull B suggester(@NotNull SenderNodeSuggester suggester) {
this.suggester = suggester;
return self();
}
/**
* Sets the string parser for this node.
*
@@ -265,6 +363,20 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return self();
}
/**
* Sets the string parser for this node with sender context.
*
* @param stringParser The string parser for this node.
* @return This builder.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public @NotNull B stringParser(@NotNull SenderNodeStringParser<T> stringParser) {
this.stringParser = stringParser;
return self();
}
/**
* Sets the serializer for this node.
*
@@ -287,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);
}
/**
@@ -304,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, defaultValue, suggester, stringParser, serializer, validator, onSetValue);
name, type, aliases, defaultValue, suggester, stringParser, serializer, validator,
onLoad, onLoadAndChange, onChange);
}
}
}
@@ -5,7 +5,6 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -13,13 +12,19 @@ import java.util.stream.Collectors;
import io.vavr.Value;
import io.vavr.control.Try;
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.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;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeSuggester;
import org.mvplugins.multiverse.core.config.node.serializer.DefaultSerializerProvider;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
import org.mvplugins.multiverse.core.utils.REPatterns;
@@ -58,20 +63,23 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
@NotNull String[] comments,
@Nullable String name,
@NotNull Class<List<I>> type,
@NotNull String[] aliases,
@Nullable Supplier<List<I>> defaultValueSupplier,
@Nullable NodeSuggester suggester,
@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,
@Nullable NodeSerializer<I> itemSerializer,
@Nullable Function<I, Try<Void>> itemValidator,
@Nullable BiConsumer<I, I> onSetItemValue) {
super(path, comments, name, type, defaultValueSupplier, suggester, stringParser, serializer,
validator, onSetValue);
super(path, comments, name, type, aliases, defaultValueSupplier, suggester, stringParser, serializer,
validator, onLoad, onLoadAndChange, onChange);
this.itemType = itemType;
this.itemSuggester = itemSuggester != null
? itemSuggester
@@ -101,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;
@@ -110,12 +118,12 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
}
private void setDefaultSuggester() {
this.suggester = input -> {
if (input == null) {
return itemSuggester.suggest(null);
}
return StringFormatter.addonToCommaSeperated(input, itemSuggester.suggest(input));
};
if (itemSuggester instanceof SenderNodeSuggester senderItemSuggester) {
this.suggester = (SenderNodeSuggester)(sender, input) ->
StringFormatter.addonToCommaSeperated(input, senderItemSuggester.suggest(sender, input));
} else {
this.suggester = input -> StringFormatter.addonToCommaSeperated(input, itemSuggester.suggest(input));
}
}
private void setDefaultStringParser() {
@@ -164,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))
@@ -194,6 +202,17 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
return Collections.emptyList();
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Collection<String> suggestItem(@NotNull CommandSender sender, @Nullable String input) {
if (itemSuggester != null && itemSuggester instanceof SenderNodeSuggester senderSuggester) {
return senderSuggester.suggest(sender, input);
}
return suggestItem(input);
}
/**
* {@inheritDoc}
*/
@@ -205,6 +224,17 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
return Try.failure(new UnsupportedOperationException("No item string parser for type " + itemType));
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Try<I> parseItemFromString(@NotNull CommandSender sender, @Nullable String input) {
if (itemStringParser != null && itemStringParser instanceof SenderNodeStringParser<I> senderStringParser) {
return senderStringParser.parse(sender, input, itemType);
}
return parseItemFromString(input);
}
/**
* {@inheritDoc}
*/
@@ -267,6 +297,20 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
return self();
}
/**
* Sets the suggester for an individual item in the list with sender context.
*
* @param itemSuggester The suggester.
* @return This builder.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public @NotNull B itemSuggester(@NotNull SenderNodeSuggester itemSuggester) {
this.itemSuggester = itemSuggester;
return self();
}
/**
* Sets the string parser for an individual item in the list.
*
@@ -278,6 +322,20 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
return self();
}
/**
* Sets the string parser for an individual item in the list with sender context.
*
* @param itemStringParser The string parser.
* @return This builder.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public @NotNull B itemStringParser(@NotNull SenderNodeStringParser<I> itemStringParser) {
this.itemStringParser = itemStringParser;
return self();
}
/**
* Sets the serializer for an individual item in the list.
*
@@ -321,12 +379,15 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
comments.toArray(new String[0]),
name,
type,
aliases,
defaultValue,
suggester,
stringParser,
serializer,
validator,
onSetValue,
onLoad,
onLoadAndChange,
onChange,
itemType,
itemSuggester,
itemStringParser,
@@ -4,6 +4,8 @@ import java.util.Collection;
import java.util.List;
import io.vavr.control.Try;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -31,6 +33,21 @@ public interface ListValueNode<I> extends ValueNode<List<I>> {
*/
@NotNull Collection<String> suggestItem(@Nullable String input);
/**
* Suggests possible string values for this node. Use contextural information from the sender such as
* sender name, permissions, or player location for better suggestions.
*
* @param sender The sender context.
* @param input The input string.
* @return A collection of possible string values
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
default @NotNull Collection<String> suggestItem(@NotNull CommandSender sender, @Nullable String input) {
return suggestItem(input);
}
/**
* Parses the given string into a value of type {@link I}. Used for property set by user input.
*
@@ -39,6 +56,20 @@ public interface ListValueNode<I> extends ValueNode<List<I>> {
*/
@NotNull Try<I> parseItemFromString(@Nullable String input);
/**
* Parses the given string into a value of type {@link I}. Used for property set by user input.
*
* @param sender The sender context.
* @param input The string to parse.
* @return The parsed value, or given exception if parsing failed.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
default @NotNull Try<I> parseItemFromString(@NotNull CommandSender sender, @Nullable String input) {
return parseItemFromString(input);
}
/**
* Gets the serializer for this node.
*
@@ -1,9 +1,11 @@
package org.mvplugins.multiverse.core.config.node;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import io.github.townyadvanced.commentedconfiguration.setting.CommentedNode;
@@ -16,6 +18,7 @@ import org.jetbrains.annotations.Nullable;
*/
public class NodeGroup implements Collection<Node> {
private final Collection<Node> nodes;
private final List<String> nodeNames;
private final Map<String, Node> nodesMap;
/**
@@ -23,6 +26,7 @@ public class NodeGroup implements Collection<Node> {
*/
public NodeGroup() {
this.nodes = new ArrayList<>();
this.nodeNames = new ArrayList<>();
this.nodesMap = new HashMap<>();
}
@@ -34,18 +38,27 @@ public class NodeGroup implements Collection<Node> {
public NodeGroup(@NotNull Collection<Node> nodes) {
this.nodes = nodes;
this.nodesMap = new HashMap<>(nodes.size());
this.nodeNames = new ArrayList<>(nodes.size());
nodes.forEach(this::addNodeIndex);
}
private void addNodeIndex(@NotNull Node node) {
if (node instanceof ValueNode) {
((ValueNode<?>) node).getName().peek(name -> nodesMap.put(name, node));
if (node instanceof ValueNode<?> valueNode) {
valueNode.getName().peek(name -> {
nodeNames.add(name);
nodesMap.put(name, node);
Arrays.stream(valueNode.getAliases()).forEach(alias -> nodesMap.put(alias, node));
});
}
}
private void removeNodeIndex(@NotNull Node node) {
if (node instanceof ValueNode) {
((ValueNode<?>) node).getName().peek(nodesMap::remove);
if (node instanceof ValueNode<?> valueNode) {
valueNode.getName().peek(name -> {
nodeNames.remove(name);
nodesMap.remove(name);
Arrays.stream(valueNode.getAliases()).forEach(alias -> nodesMap.remove(alias, node));
});
}
}
@@ -55,7 +68,7 @@ public class NodeGroup implements Collection<Node> {
* @return The names of all nodes in this group.
*/
public @NotNull Collection<String> getNames() {
return nodesMap.keySet();
return nodeNames;
}
/**
@@ -4,15 +4,20 @@ 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 {
/**
* Gets the name of this node. Used for identifying the node from user input.
* Gets the name of this node. Used for identifying the node from user input. This must be unique within a node group.
*
* @return An {@link Option} containing the name of this node, or {@link Option.None} if the node has no name.
*/
@@ -25,6 +30,19 @@ public interface ValueNode<T> extends Node {
*/
@NotNull Class<T> getType();
/**
* Gets the aliases of this node. Serves as shorter or legacy alternatives the {@link #getName()} and must be
* unique within a node group.
*
* @return The aliases of this node.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
default @NotNull String[] getAliases() {
return Strings.EMPTY_ARRAY;
}
/**
* Gets the default value with type {@link T} of the node.
*
@@ -40,6 +58,19 @@ public interface ValueNode<T> extends Node {
*/
@NotNull Collection<String> suggest(@Nullable String input);
/**
* Suggests possible string values for this node. Use contextural information from the sender such as
* sender name, permissions, or player location for better suggestions.
*
* @param sender The sender context.
* @param input The input string.
* @return A collection of possible string values
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
@NotNull Collection<String> suggest(@NotNull CommandSender sender, @Nullable String input);
/**
* Parses the given string into a value of type {@link T}. Used for property set by user input.
*
@@ -48,6 +79,21 @@ public interface ValueNode<T> extends Node {
*/
@NotNull Try<T> parseFromString(@Nullable String input);
/**
* Parses the given string into a value of type {@link T} with context from the sender.
* Used for property set by user input.
*
* @param sender The sender context.
* @param input The string to parse.
* @return The parsed value, or given exception if parsing failed.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
default @NotNull Try<T> parseFromString(@NotNull CommandSender sender, @Nullable String input) {
return parseFromString(input);
}
/**
* Gets the serializer for this node.
*
@@ -64,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);
}
};
}
}
@@ -6,7 +6,8 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A function that suggests possible values for a node value.
* A function that suggests possible values for a node value. These suggestions must be able to be used to parse the
* value from string with {@link NodeStringParser}.
*/
@FunctionalInterface
public interface NodeSuggester {
@@ -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);
}
}
@@ -0,0 +1,38 @@
package org.mvplugins.multiverse.core.config.node.functions;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
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.
* This ties in with {@link SenderNodeSuggester} that provides suggestions based on the sender context.
*
* @param sender The sender context.
* @param string The string to parse.
* @param type The type of the object to parse.
* @return The parsed object, or {@link Try.Failure} if the string could not be parsed.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
@NotNull Try<T> parse(@NotNull CommandSender sender, @Nullable String string, @NotNull Class<T> type);
@Override
default @NotNull Try<T> parse(@Nullable String string, @NotNull Class<T> type) {
return parse(Bukkit.getConsoleSender(), string, type);
}
}
@@ -0,0 +1,36 @@
package org.mvplugins.multiverse.core.config.node.functions;
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 java.util.Collection;
/**
* A function that suggests possible values for a node value with sender contextual information.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
@FunctionalInterface
public interface SenderNodeSuggester extends NodeSuggester {
/**
* Suggests possible values for a node value. Generated based on the current user input and sender contextual information.
*
* @param sender The sender context.
* @param input The current partial user input
* @return The possible values.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
@NotNull Collection<String> suggest(@NotNull CommandSender sender, @Nullable String input);
@Override
default @NotNull Collection<String> suggest(@Nullable String input) {
return suggest(Bukkit.getConsoleSender(), input);
}
}
@@ -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.");
@@ -2,7 +2,9 @@ package org.mvplugins.multiverse.core.destination;
import java.util.Collection;
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.jvnet.hk2.annotations.Contract;
@@ -11,6 +13,9 @@ import org.mvplugins.multiverse.core.utils.result.FailureReason;
/**
* A destination is a location that can be teleported to.
* <br />
* Please ensure you implement at least one of {@link #getDestinationInstance(CommandSender, String)} or
* {@link #getDestinationInstance(String)} to prevent a stack overflow.
*
* @param <D> The type of the destination
* @param <T> The type of the destination instance
@@ -18,13 +23,13 @@ import org.mvplugins.multiverse.core.utils.result.FailureReason;
@Contract
public interface Destination<D extends Destination<D, T, F>, T extends DestinationInstance<T, D>, F extends FailureReason> {
/**
* Returns the identifier or prefix that is required for this destination.
* Returns the identifier or prefix required for this destination.
*
* <p>Portals have a prefix of "p" for example and OpenWarp (third party plugin) uses "ow". This is derived from a
* hash and cannot have duplicate values. Read that as your plugin cannot use 'p' because it's already used.
* hash and cannot have duplicate values. Means that your plugin cannot use 'p' because it's already used.
* Please check the wiki when adding a custom destination!</p>
*
* @return The identifier or prefix that is required for this destination.
* @return The identifier or prefix required for this destination.
*/
@NotNull String getIdentifier();
@@ -33,8 +38,36 @@ public interface Destination<D extends Destination<D, T, F>, T extends Destinati
*
* @param destinationParams The destination parameters. ex: p:MyPortal:nw
* @return The destination instance, or null if the destination parameters are invalid.
*
* @deprecated Use {@link #getDestinationInstance(CommandSender, String)} instead. This method will no longer be
* called by {@link DestinationsProvider}.
*/
@NotNull Attempt<T, F> getDestinationInstance(@NotNull String destinationParams);
@Deprecated(forRemoval = true, since = "5.1")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
default @NotNull Attempt<T, F> getDestinationInstance(@NotNull String destinationParams) {
return getDestinationInstance(Bukkit.getConsoleSender(), destinationParams);
}
/**
* Returns the destination instance for the given destination parameters with sender context. This allows
* for shorthands such as getting location or name from sender. If no sender context is available, use
* {@link Bukkit#getConsoleSender()} will be defaulted by {@link DestinationsProvider}.
* <br />
* Note that the resulting {@link DestinationInstance} should be (de)serializable without the original sender context.
* <br />
* For example, the parsable string with sender context `e:@here` should return a {@link DestinationInstance} that
* is serialized to `e:world:x,y,z:p:y`, which can be deserialized without the original sender context.
*
* @param sender The sender context.
* @param destinationParams The destination parameters. ex: p:MyPortal:nw
* @return The destination instance, or null if the destination parameters are invalid.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
default Attempt<T, F> getDestinationInstance(@NotNull CommandSender sender, @NotNull String destinationParams) {
return getDestinationInstance(destinationParams);
}
/**
* Returns a list of possible destinations for the given destination parameters. This packet's destination
@@ -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.
*
@@ -1,5 +1,8 @@
package org.mvplugins.multiverse.core.destination;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.destination.core.WorldDestination;
/**
* Data of a possible destination for tab completion and permission checking
*
@@ -8,4 +11,19 @@ package org.mvplugins.multiverse.core.destination;
* @param finerPermissionSuffix The finer permission suffix
*/
public record DestinationSuggestionPacket(Destination<?, ?, ?> destination, String destinationString, String finerPermissionSuffix) {
/**
* Gets a parsable string representation of the destination that is most likely valid for
* {@link DestinationsProvider#parseDestination(String)}.
*
* @return The parsable string
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public String parsableString() {
return destination instanceof WorldDestination
? destinationString
: destination.getIdentifier() + ":" + destinationString;
}
}
@@ -7,7 +7,9 @@ import java.util.Map;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import jakarta.inject.Inject;
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.jvnet.hk2.annotations.Service;
@@ -50,8 +52,25 @@ public final class DestinationsProvider {
* @param destinationString The destination string.
* @return The destination object, or null if invalid format.
*/
@SuppressWarnings("unchecked,rawtypes")
public @NotNull Attempt<DestinationInstance<?, ?>, FailureReason> parseDestination(@NotNull String destinationString) {
return this.parseDestination(Bukkit.getConsoleSender(), destinationString);
}
/**
* Converts a destination string to a destination object with sender context.
*
* @param sender The target sender context.
* @param destinationString The destination string.
* @return The destination object, or null if invalid format.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
@SuppressWarnings("unchecked,rawtypes")
public @NotNull Attempt<DestinationInstance<?, ?>, FailureReason> parseDestination(
@NotNull CommandSender sender,
@NotNull String destinationString
) {
String[] items = destinationString.split(SEPARATOR, 2);
String idString = items[0];
@@ -73,7 +92,7 @@ public final class DestinationsProvider {
replace("{ids}").with(String.join(", ", this.destinationMap.keySet())));
}
return destination.getDestinationInstance(destinationParams);
return destination.getDestinationInstance(sender, destinationParams);
}
/**
@@ -95,12 +114,36 @@ public final class DestinationsProvider {
return this.destinationMap.values();
}
/**
* Gets suggestions for possible parsable destinations.
*
* @param sender The target sender context.
* @param destinationParams The current user input.
* @return A collection of destination suggestions.
*/
public @NotNull Collection<DestinationSuggestionPacket> suggestDestinations(@NotNull CommandSender sender, @Nullable String destinationParams) {
return this.getDestinations().stream()
.flatMap(destination -> destination.suggestDestinations(sender, destinationParams).stream())
.toList();
}
/**
* Gets suggestions for possible parsable destinations.
*
* @param sender The target sender context.
* @param destinationParams The current user input.
* @return A collection of destination suggestions in parsable string format.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public @NotNull Collection<String> suggestDestinationStrings(@NotNull CommandSender sender, @Nullable String destinationParams) {
return suggestDestinations(sender, destinationParams)
.stream()
.map(DestinationSuggestionPacket::parsableString)
.toList();
}
public enum ParseFailureReason implements FailureReason {
INVALID_DESTINATION_ID(MVCorei18n.DESTINATION_PARSE_FAILUREREASON_INVALIDDESTINATIONID),
;
@@ -45,7 +45,10 @@ public final class AnchorDestination implements Destination<AnchorDestination, A
* {@inheritDoc}
*/
@Override
public @NotNull Attempt<AnchorDestinationInstance, InstanceFailureReason> getDestinationInstance(@NotNull String destinationParams) {
public @NotNull Attempt<AnchorDestinationInstance, InstanceFailureReason> getDestinationInstance(
@NotNull CommandSender sender,
@NotNull String destinationParams
) {
return this.anchorManager.getAnchor(destinationParams)
.fold(
() -> Attempt.failure(InstanceFailureReason.ANCHOR_NOT_FOUND, replace("{anchor}").with(destinationParams)),
@@ -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;
}
/**
@@ -45,7 +48,10 @@ public final class BedDestination implements Destination<BedDestination, BedDest
* {@inheritDoc}
*/
@Override
public @NotNull Attempt<BedDestinationInstance, InstanceFailureReason> getDestinationInstance(@NotNull String destinationParams) {
public @NotNull Attempt<BedDestinationInstance, InstanceFailureReason> getDestinationInstance(
@NotNull CommandSender sender,
@NotNull String destinationParams
) {
Player player = PlayerFinder.get(destinationParams);
if (player == null && !OWN_BED_STRING.equals(destinationParams)) {
return Attempt.failure(InstanceFailureReason.PLAYER_NOT_FOUND, Replace.PLAYER.with(destinationParams));
@@ -59,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}
*/
@@ -51,7 +51,10 @@ public final class CannonDestination implements Destination<CannonDestination, C
* {@inheritDoc}
*/
@Override
public @NotNull Attempt<CannonDestinationInstance, InstanceFailureReason> getDestinationInstance(@NotNull String destinationParams) {
public @NotNull Attempt<CannonDestinationInstance, InstanceFailureReason> getDestinationInstance(
@NotNull CommandSender sender,
@NotNull String destinationParams
) {
String[] params = REPatterns.COLON.split(destinationParams);
if (params.length != 5) {
return Attempt.failure(InstanceFailureReason.INVALID_FORMAT);
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.destination.core;
import java.util.Collection;
import java.util.stream.Stream;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
@@ -8,7 +9,9 @@ import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.BlockCommandSender;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
@@ -16,15 +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.locale.message.MessageReplacement;
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.*;
@@ -60,55 +63,55 @@ 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)
);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Attempt<ExactDestinationInstance, InstanceFailureReason> getDestinationInstance(@NotNull String destinationParams) {
String[] items = REPatterns.COLON.split(destinationParams);
public @NotNull Attempt<ExactDestinationInstance, InstanceFailureReason> getDestinationInstance(
@NotNull CommandSender sender,
@NotNull String 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,
worldManager,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
)
))
.getOrElse(() -> Attempt.failure(InstanceFailureReason.INVALID_COORDINATES_FORMAT)); // todo: specific failure reason for this case
}
return Attempt.failure(InstanceFailureReason.INVALID_FORMAT);
}
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
@@ -118,19 +121,61 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
: worldManager.getLoadedWorld(worldName);
}
private Option<Location> getLocationFromSender(CommandSender sender) {
if (sender instanceof Entity entity) {
return Option.of(entity.getLocation());
}
if (sender instanceof BlockCommandSender blockSender) {
return Option.of(blockSender.getBlock().getLocation());
}
return Option.none();
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Collection<DestinationSuggestionPacket> suggestDestinations(
@NotNull CommandSender sender, @Nullable String destinationParams) {
return worldManager.getLoadedWorlds().stream()
Stream<DestinationSuggestionPacket> stream = worldManager.getLoadedWorlds().stream()
.filter(world -> worldEntryCheckerProvider.forSender(sender)
.canAccessWorld(world)
.isSuccess())
.map(world ->
new DestinationSuggestionPacket(this, world.getTabCompleteName() + ":", world.getName()))
.toList();
new DestinationSuggestionPacket(this, world.getTabCompleteName() + ":", world.getName()));
Location location = getLocationFromSender(sender).getOrNull();
if (location != null) {
var herePacket = new DestinationSuggestionPacket(
this,
"@here",
location.getWorld().getName()
);
var locationPacket = new DestinationSuggestionPacket(
this,
"%s:%.2f,%.2f,%.2f".formatted(
location.getWorld().getName(),
location.getX(),
location.getY(),
location.getZ()
),
location.getWorld().getName()
);
var locationPacketPW = new DestinationSuggestionPacket(
this,
"%s:%.2f,%.2f,%.2f:%.2f:%.2f".formatted(
location.getWorld().getName(),
location.getX(),
location.getY(),
location.getZ(),
location.getPitch(),
location.getYaw()
),
location.getWorld().getName()
);
stream = Stream.concat(stream, Stream.of(herePacket, locationPacket, locationPacketPW));
}
return stream.toList();
}
public enum InstanceFailureReason implements FailureReason {
@@ -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;
}
/**
@@ -43,7 +48,10 @@ public final class PlayerDestination implements Destination<PlayerDestination, P
* {@inheritDoc}
*/
@Override
public @NotNull Attempt<PlayerDestinationInstance, InstanceFailureReason> getDestinationInstance(@NotNull String destinationParams) {
public @NotNull Attempt<PlayerDestinationInstance, InstanceFailureReason> getDestinationInstance(
@NotNull CommandSender sender,
@NotNull String destinationParams
) {
Player player = PlayerFinder.get(destinationParams);
if (player == null) {
return Attempt.failure(InstanceFailureReason.PLAYER_NOT_FOUND, Replace.PLAYER.with(destinationParams));
@@ -57,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}
*/
@@ -60,7 +60,10 @@ public final class WorldDestination implements Destination<WorldDestination, Wor
* {@inheritDoc}
*/
@Override
public @NotNull Attempt<WorldDestinationInstance, InstanceFailureReason> getDestinationInstance(@NotNull String destinationParams) {
public @NotNull Attempt<WorldDestinationInstance, InstanceFailureReason> getDestinationInstance(
@NotNull CommandSender sender,
@NotNull String destinationParams
) {
String[] items = REPatterns.COLON.split(destinationParams, 3);
String worldName = items[0];
MultiverseWorld world = getMultiverseWorld(worldName);
@@ -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}
*/
@@ -8,6 +8,7 @@ import com.google.common.base.Strings;
import org.bukkit.ChatColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
/**
* Filter content and text based on regex matching.
@@ -68,9 +69,12 @@ public class RegexContentFilter implements ContentFilter {
if (!hasValidRegex()) {
return false;
}
String text = ChatColor.stripColor(String.valueOf(value)).toLowerCase();
String text = ChatTextFormatter.removeColor(String.valueOf(value));
if (text == null) {
return false;
}
try {
return regexPattern.matcher(text).find();
return regexPattern.matcher(text.toLowerCase()).find();
} catch (PatternSyntaxException ignored) {
Logging.warning("Error parsing regex '%s' for input '%s'", regexString, text);
return false;
@@ -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);
}
}
@@ -5,7 +5,6 @@ import io.papermc.paper.event.player.AsyncChatEvent;
import jakarta.inject.Inject;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.jvnet.hk2.annotations.Service;
@@ -15,7 +14,9 @@ import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventClass;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.IgnoreIfCancelled;
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")
@@ -87,18 +89,16 @@ final class MVChatListener implements CoreListener {
String prefixChatFormat = config.getPrefixChatFormat();
prefixChatFormat = prefixChatFormat.replace("%world%", worldName).replace("%chat%", chat);
prefixChatFormat = ChatColor.translateAlternateColorCodes('&', prefixChatFormat);
event.setFormat(prefixChatFormat);
prefixChatFormat = ChatTextFormatter.colorize(prefixChatFormat);
if (prefixChatFormat != null) {
event.setFormat(prefixChatFormat);
}
}
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;
}
@@ -92,7 +91,8 @@ final class MVEntityListener implements CoreListener {
// Always allow custom command and plugins to spawn creatures
if (event.getSpawnReason() == SpawnReason.CUSTOM
|| event.getSpawnReason() == SpawnReason.COMMAND
|| event.getSpawnReason() == SpawnReason.BREEDING) {
|| event.getSpawnReason() == SpawnReason.BREEDING
|| event.getSpawnReason() == SpawnReason.SPAWNER_EGG) {
return;
}
@@ -109,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,6 +33,7 @@ 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.annotations.DefaultEventPriority;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
@@ -39,6 +41,8 @@ import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventPriorityKe
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;
@@ -49,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.
@@ -68,8 +71,8 @@ final class MVPlayerListener implements CoreListener {
private final DestinationsProvider destinationsProvider;
private final EnforcementHandler enforcementHandler;
private final DimensionFinder dimensionFinder;
private final Map<String, String> playerWorld = new ConcurrentHashMap<>();
private final CorePermissionsChecker corePermissionsChecker;
private final AsyncSafetyTeleporter asyncSafetyTeleporter;
@Inject
MVPlayerListener(
@@ -84,7 +87,9 @@ final class MVPlayerListener implements CoreListener {
Provider<MVCommandManager> commandManagerProvider,
DestinationsProvider destinationsProvider,
EnforcementHandler enforcementHandler,
DimensionFinder dimensionFinder) {
DimensionFinder dimensionFinder,
CorePermissionsChecker corePermissionsChecker,
AsyncSafetyTeleporter asyncSafetyTeleporter) {
this.plugin = plugin;
this.config = config;
this.worldManagerProvider = worldManagerProvider;
@@ -97,6 +102,8 @@ final class MVPlayerListener implements CoreListener {
this.destinationsProvider = destinationsProvider;
this.enforcementHandler = enforcementHandler;
this.dimensionFinder = dimensionFinder;
this.corePermissionsChecker = corePermissionsChecker;
this.asyncSafetyTeleporter = asyncSafetyTeleporter;
}
private WorldManager getWorldManager() {
@@ -107,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.
*
@@ -128,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) {
@@ -196,23 +194,19 @@ final class MVPlayerListener implements CoreListener {
}
@EventMethod
// TODO: Consider if this key is needed anymore, need to remove from config.yml as well
@EventPriorityKey("mvcore-player-spawn-location")
void playerSpawnLocation(PlayerSpawnLocationEvent event) {
void onPlayerJoin(PlayerJoinEvent 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);
if (player.hasPlayedBefore()) {
handleJoinLocation(player);
} else {
handleJoinLocation(event);
handleFirstSpawn(player);
}
this.handleGameModeAndFlight(player, event.getSpawnLocation().getWorld());
handleGameModeAndFlight(player, player.getWorld());
}
private void handleFirstSpawn(PlayerSpawnLocationEvent event) {
private void handleFirstSpawn(Player player) {
if (!config.getFirstSpawnOverride()) {
Logging.finer("FirstSpawnOverride is disabled");
// User has disabled the feature in config
@@ -220,16 +214,14 @@ final class MVPlayerListener implements CoreListener {
}
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")))
.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) {
private void handleJoinLocation(Player player) {
if (!config.getEnableJoinDestination()) {
Logging.finer("JoinDestination is disabled");
// User has disabled the feature in config
@@ -239,27 +231,36 @@ final class MVPlayerListener implements CoreListener {
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())
.map(destination -> destination.getLocation(event.getPlayer())
.peek(event::setSpawnLocation)
.onEmpty(() -> Logging.warning("The destination in JoinDestination in config is invalid")))
.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)));
}
/**
* This method is called when a player changes worlds.
* @param event The Event that was fired.
*/
@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());
}
/**
@@ -269,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()) {
@@ -340,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;
}
@@ -366,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;
}
@@ -18,6 +18,7 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.options.LoadWorldOptions;
import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions;
import org.mvplugins.multiverse.core.world.reasons.LoadFailureReason;
import org.mvplugins.multiverse.core.world.reasons.UnloadFailureReason;
@@ -68,7 +69,7 @@ final class MVWorldListener implements CoreListener {
worldManager.getUnloadedWorld(event.getWorld().getName())
.peek(world -> {
Logging.fine("Loading world: " + world.getName());
worldManager.loadWorld(world).onFailure(failure -> {
worldManager.loadWorld(LoadWorldOptions.world(world)).onFailure(failure -> {
if (failure.getFailureReason() != LoadFailureReason.WORLD_ALREADY_LOADING) {
Logging.severe("Failed to load world: " + failure);
}
@@ -8,6 +8,8 @@ import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement;
import java.util.Locale;
/**
* All the translation keys for the core plugin
*/
@@ -149,6 +151,7 @@ public enum MVCorei18n implements MessageKeyProvider {
MODIFY_WORLD_DESCRIPTION,
MODIFY_PROPERTY_DESCRIPTION,
MODIFY_VALUE_DESCRIPTION,
MODIFY_PROPERTYREMOVED,
MODIFY_CANNOTHAVEVALUE,
MODIFY_SPECIFYVALUE,
MODIFY_SUCCESS,
@@ -181,6 +184,10 @@ public enum MVCorei18n implements MessageKeyProvider {
SETSPAWN_DESCRIPTION,
SETSPAWN_LOCATION_DESCRIPTION,
SETSPAWN_WORLD_DESCRIPTION,
SETSPAWN_UNSAFE,
SETSPAWN_SUCCESS,
SETSPAWN_FAILED,
SETSPAWN_NOTMVWORLD,
// /mv spawn
SPAWN_DESCRIPTION,
@@ -301,11 +308,13 @@ public enum MVCorei18n implements MessageKeyProvider {
IMPORTWORLD_WORLDEXISTUNLOADED,
IMPORTWORLD_WORLDEXISTLOADED,
IMPORTWORLD_WORLDFOLDERINVALID,
IMPORTWORLD_BUKKITENVIRONMENTMISMATCH,
LOADWORLD_WORLDALREADYLOADING,
LOADWORLD_WORLDNONEXISTENT,
LOADWORLD_WORLDEXISTFOLDER,
LOADWORLD_WORLDEXISTLOADED,
LOADWORLD_BUKKITENVIRONMENTMISMATCH,
REMOVEWORLD_WORLDNONEXISTENT,
@@ -337,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,
@@ -347,7 +361,8 @@ public enum MVCorei18n implements MessageKeyProvider {
// END CHECKSTYLE-SUPPRESSION: Javadoc
private final MessageKey key = MessageKey.of("mv-core." + this.name().replace('_', '.').toLowerCase());
private final MessageKey key = MessageKey.of("mv-core." + this.name().replace('_', '.')
.toLowerCase(Locale.ENGLISH));
/**
* {@inheritDoc}
@@ -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;
}
}
@@ -105,6 +105,7 @@ public final class MessageReplacement {
DESTINATION(replace("{destination}")),
ERROR(replace("{error}")),
GAMERULE(replace("{gamerule}")),
LOCATION(replace("{location}")),
NAME(replace("{name}")),
PLAYER(replace("{player}")),
REASON(replace("{reason}")),
@@ -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.
*/
@@ -108,8 +106,8 @@ public abstract class MultiverseModule extends JavaPlugin {
*
* @deprecated Use {@link #registerDynamicListeners(Class)} with the new DynamicListener API.
*/
@Deprecated(since = "5.0.0", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0.0")
@Deprecated(since = "5.0", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
protected void registerEvents(Class<? extends Listener> listenerClass) {
var pluginManager = getServer().getPluginManager();
Try.run(() -> serviceLocator.getAllServices(listenerClass).forEach(
@@ -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");
@@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core.permissions;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
@@ -12,9 +13,15 @@ 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 {
/**
* Permission to bypass the join location.
*/
static final String JOINLOCATION_BYPASS = "mv.bypass.joinlocation";
/**
* Permission to access a world.
*/
@@ -52,24 +59,36 @@ public final class CorePermissions {
this.pluginManager = pluginManager;
}
@PostConstruct
void registerBasePermissions() {
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) {
@@ -78,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) {
@@ -99,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()));
}
}
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.permissions;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
@@ -50,6 +51,19 @@ public final class CorePermissionsChecker {
this.worldManager = worldManager;
}
/**
* Checks if the sender has permission to bypass the join location restriction.
*
* @param sender The command sender.
* @return True if the sender has bypass permission, false otherwise.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public boolean hasJoinLocationBypassPermission(@NotNull CommandSender sender) {
return hasPermission(sender, CorePermissions.JOINLOCATION_BYPASS);
}
/**
* Checks if the sender has permission to access the specified world.
*
@@ -349,8 +363,8 @@ public final class CorePermissionsChecker {
return scope;
}
public static Scope getApplicableScope(CommandSender sender, Entity entity) {
if (sender instanceof Entity senderEntity && senderEntity.equals(entity)) {
public static Scope getApplicableScope(CommandSender teleporter, Entity entity) {
if (teleporter instanceof Entity senderEntity && senderEntity.equals(entity)) {
return Scope.SELF;
}
return Scope.OTHER;
@@ -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.
*
@@ -11,6 +11,7 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.MultiverseCore;
@@ -20,6 +21,7 @@ import org.mvplugins.multiverse.core.utils.result.AsyncAttempt;
import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import java.util.ArrayList;
import java.util.List;
/**
@@ -35,6 +37,7 @@ public final class AsyncSafetyTeleporterAction {
private final @NotNull Either<Location, DestinationInstance<?, ?>> locationOrDestination;
private boolean checkSafety;
private PassengerMode passengerMode = PassengerModes.DEFAULT;
private @Nullable CommandSender teleporter = null;
AsyncSafetyTeleporterAction(
@@ -58,18 +61,32 @@ public final class AsyncSafetyTeleporterAction {
* Sets whether to check for safe location before teleport.
*
* @param checkSafety Whether to check for safe location
* @return A new {@link AsyncSafetyTeleporterAction} to be chained
* @return The same {@link AsyncSafetyTeleporterAction} to be chained
*/
public AsyncSafetyTeleporterAction checkSafety(boolean checkSafety) {
this.checkSafety = checkSafety;
return this;
}
/**
* Sets the passenger mode
*
* @param passengerMode The passenger mode
* @return The same {@link AsyncSafetyTeleporterAction} to be chained
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public AsyncSafetyTeleporterAction passengerMode(@NotNull PassengerMode passengerMode) {
this.passengerMode = passengerMode;
return this;
}
/**
* Sets the teleporter.
*
* @param issuer The issuer
* @return A new {@link AsyncSafetyTeleporterAction} to be chained
* @return The same {@link AsyncSafetyTeleporterAction} to be chained
*/
public AsyncSafetyTeleporterAction by(@NotNull BukkitCommandIssuer issuer) {
return by(issuer.getIssuer());
@@ -79,7 +96,7 @@ public final class AsyncSafetyTeleporterAction {
* Sets the teleporter.
*
* @param teleporter The teleporter
* @return A new {@link AsyncSafetyTeleporterAction} to be chained
* @return The same {@link AsyncSafetyTeleporterAction} to be chained
*/
public AsyncSafetyTeleporterAction by(@NotNull CommandSender teleporter) {
this.teleporter = teleporter;
@@ -94,7 +111,36 @@ public final class AsyncSafetyTeleporterAction {
* @return A list of async futures that represent the teleportation result of each entity
*/
public <T extends Entity> AsyncAttemptsAggregate<Void, TeleportFailureReason> teleport(@NotNull List<T> teleportees) {
return AsyncAttemptsAggregate.allOf(teleportees.stream().map(this::teleport).toList());
return AsyncAttemptsAggregate.allOfAggregate(teleportees.stream().map(this::teleportSingle).toList());
}
/**
* Teleports one parent entity. Multiple entities may be teleported depending on {@link #passengerMode(PassengerMode)}.
*
* @param teleportee The entity to teleport
* @return A list of async future that represents the teleportation result
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public AsyncAttemptsAggregate<Void, TeleportFailureReason> teleportSingle(@NotNull Entity teleportee) {
var localTeleporter = this.teleporter == null ? teleportee : this.teleporter;
return getLocation(teleportee).mapAttempt(this::doSafetyCheck)
.onSuccess(() -> {
if (teleportee instanceof Player player) {
this.teleportQueue.addToQueue(localTeleporter, player);
}
})
.transform(
location -> doAsyncTeleport(teleportee, location),
failure -> AsyncAttemptsAggregate.allOf(AsyncAttempt.failure(failure))
)
.thenRun(() -> {
if (teleportee instanceof Player player) {
this.teleportQueue.popFromQueue(player.getName());
}
});
}
/**
@@ -102,21 +148,14 @@ public final class AsyncSafetyTeleporterAction {
*
* @param teleportee The entity to teleport
* @return An async future that represents the teleportation result
*
* @deprecated Use {@link #teleportSingle(Entity)} instead, as teleport of single entity may result in multiple
* teleports due to vehicle and passengers based on {@link #passengerMode(PassengerMode)}.
*/
@Deprecated(forRemoval = true, since = "5.1")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public AsyncAttempt<Void, TeleportFailureReason> teleport(@NotNull Entity teleportee) {
var localTeleporter = this.teleporter == null ? teleportee : this.teleporter;
return AsyncAttempt.fromAttempt(getLocation(teleportee).mapAttempt(this::doSafetyCheck))
.onSuccess(() -> {
if (teleportee instanceof Player player) {
this.teleportQueue.addToQueue(localTeleporter, player);
}
})
.mapAsyncAttempt(location -> doAsyncTeleport(teleportee, location))
.thenRun(() -> {
if (teleportee instanceof Player player) {
this.teleportQueue.popFromQueue(player.getName());
}
});
return teleportSingle(teleportee).getAttempts().get(0);
}
private Attempt<Location, TeleportFailureReason> getLocation(@NotNull Entity teleportee) {
@@ -160,9 +199,64 @@ public final class AsyncSafetyTeleporterAction {
return Attempt.success(safeLocation);
}
private AsyncAttempt<Void, TeleportFailureReason> doAsyncTeleport(
private AsyncAttemptsAggregate<Void, TeleportFailureReason> doAsyncTeleport(
@NotNull Entity teleportee,
@NotNull Location location) {
@NotNull Location location
) {
if (passengerMode.isDismountVehicle() && teleportee.isInsideVehicle()) {
Entity vehicle = teleportee.getVehicle();
if (vehicle != null) {
return doVehicleTeleport(teleportee, location, vehicle);
}
}
List<Entity> passengers = teleportee.getPassengers();
if (passengerMode.isDismountPassengers() && !passengers.isEmpty()) {
passengers.forEach(teleportee::removePassenger);
if (passengerMode.isPassengersFollow()) {
return doPassengersTeleport(teleportee, location, passengers);
}
}
return AsyncAttemptsAggregate.allOf(doSingleTeleport(teleportee, location));
}
private AsyncAttemptsAggregate<Void, TeleportFailureReason> doVehicleTeleport(
@NotNull Entity teleportee,
@NotNull Location location,
@NotNull Entity vehicle
) {
if (passengerMode.isVehicleFollow()) {
return doPassengersTeleport(vehicle, location, vehicle.getPassengers());
}
teleportee.leaveVehicle();
return doAsyncTeleport(teleportee, location);
}
private AsyncAttemptsAggregate<Void, TeleportFailureReason> doPassengersTeleport(
@NotNull Entity teleportee,
@NotNull Location location,
@NotNull List<Entity> passengers
) {
List<Entity> toTeleport = new ArrayList<>(passengers.size() + 1);
toTeleport.add(teleportee);
toTeleport.addAll(passengers);
return AsyncAttemptsAggregate.allOfAggregate(
toTeleport.stream()
.map(passenger -> doAsyncTeleport(passenger, location))
.toList()
)
.onSuccess(() -> Bukkit.getScheduler().runTask(multiverseCore, () -> {
passengers.forEach(teleportee::addPassenger);
Logging.finer("Mounted %d passengers to %s", passengers.size(), teleportee.getName());
}));
}
private AsyncAttempt<Void, TeleportFailureReason> doSingleTeleport(
@NotNull Entity teleportee,
@NotNull Location location
) {
return AsyncAttempt.of(PaperLib.teleportAsync(teleportee, location), exception -> {
Logging.warning("Failed to teleport %s to %s: %s",
teleportee.getName(), location, exception.getMessage());
@@ -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;
}
@@ -0,0 +1,60 @@
package org.mvplugins.multiverse.core.teleportation;
import org.jetbrains.annotations.ApiStatus;
/**
* Defines how passengers and vehicles on an entity should be handled when the entity is teleported.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public interface PassengerMode {
/**
* Defines whether the passengers should be removed from the parent entity when the parent entity is teleported.
* <br/>
* Teleports between worlds may fail if passengers are not removed.
*
* @return Whether the passengers should be removed
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
boolean isDismountPassengers();
/**
* Defines whether the passengers should follow the parent entity when the parent entity is teleported.
* <br/>
* This only applies of passengers are removed from the parent entity, i.e. {@link #isDismountPassengers()} is true.
*
* @return Whether the passengers should follow
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
boolean isPassengersFollow();
/**
* Defines whether the entity should dismount from the vehicle when the entity is teleported.
* <br/>
* Teleport between worlds may fail if entity is not dismounted.
*
* @return Whether the entity should dismount
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
boolean isDismountVehicle();
/**
* Defines whether the vehicle and other passengers on it should follow the target entity on the vehicle when
* the target entity is teleported.
* <br/>
* This only applies if the entity is dismounted from the vehicle, i.e. {@link #isDismountVehicle()} is true.
*
* @return Whether the vehicle should follow
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
boolean isVehicleFollow();
}
@@ -0,0 +1,114 @@
package org.mvplugins.multiverse.core.teleportation;
import org.jetbrains.annotations.ApiStatus;
/**
* Enum shorthands for {@link PassengerMode} to define common passenger modes configurations.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public enum PassengerModes implements PassengerMode {
/**
* All passengers and vehicles are handled by the server. This usually means that entities with passengers
* or in vehicles will not be able to teleport to a different world until they manually dismount.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
DEFAULT(false, false, false, false),
/**
* All passengers will be removed from the parent entity before the teleport. Passengers will be left behind.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
DISMOUNT_PASSENGERS(true, false, false, false),
/**
* Parent entity will be dismounted from the vehicle before the teleport. Vehicle will be left behind.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
DISMOUNT_VEHICLE(true, false, true, false),
/**
* All passengers and vehicles will be removed from the parent entity before the teleport. Passengers and vehicles
* will be left behind.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
DISMOUNT_ALL(true, false, true, false),
/**
* All passengers will teleport together with the parent entity.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
RETAIN_PASSENGERS(true, true, false, false),
/**
* All vehicles will teleport together with the parent entity.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
RETAIN_VEHICLE(false, false, true, true),
/**
* All passengers and vehicles will teleport together with the parent entity.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
RETAIN_ALL(true, true, true, true),
;
private final boolean dismountPassengers;
private final boolean passengersFollow;
private final boolean dismountVehicle;
private final boolean vehicleFollow;
PassengerModes(boolean dismountPassengers, boolean mountPassengers, boolean dismountVehicle, boolean mountVehicle) {
this.dismountPassengers = dismountPassengers;
this.passengersFollow = mountPassengers;
this.dismountVehicle = dismountVehicle;
this.vehicleFollow = mountVehicle;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isDismountPassengers() {
return dismountPassengers;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isPassengersFollow() {
return passengersFollow;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isDismountVehicle() {
return dismountVehicle;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isVehicleFollow() {
return vehicleFollow;
}
}
@@ -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;
Loaded 100 of 185 files, more files were not shown because too many files have changed in this diff. Show more