Compare commits

...
276 Commits
Author SHA1 Message Date
Ben Woo f6b3ed0f8b Merge pull request #3484 from Multiverse/fix/acf-locales
Allow locales folder to add and override acf messages
2026-06-05 15:23:48 +09:00
Ben Woo afce49b8d8 Merge pull request #3485 from Multiverse/chore/plugin-yml
Update link and description in plugins.yml
2026-06-05 15:22:08 +09:00
Ben Woo b9271195e9 Update link and description in plugins.yml 2026-06-05 12:59:05 +08:00
Ben Woo c76faf0353 Allow locales folder to add and override acf messages 2026-06-05 12:55:15 +08:00
Ben Woo a27b2186c5 Merge pull request #3481 from Multiverse/fix/load-world-migration
Fix folder detection during 26.1 migration at world load
2026-05-24 11:06:42 +08:00
Ben Woo 97d4f6cea2 Fix folder detection during 26.1 migration at world load 2026-05-24 11:02:17 +08:00
Ben Woo e4f3b4612c Merge pull request #3450 from Multiverse/v5.7
V5.7
2026-05-20 14:42:34 +08:00
Ben Woo 5c822340c9 Merge pull request #3480 from Multiverse/feat/portal-form-message
Add portal form disabled messages for end and nether portals
2026-05-20 14:32:27 +08:00
Ben Woo 665bd1c9ae Merge pull request #3479 from Multiverse/fix/key-bugs
Fix crashes with regen and cloning world
2026-05-20 14:31:45 +08:00
Ben Woo 2bc3965283 Fix crashes with regen and cloning world 2026-05-20 14:27:41 +08:00
Ben Woo 54dd5bfb34 Add portal form disabled messages for end and nether portals 2026-05-20 12:55:19 +08:00
Ben Woo c2c1cc4f0a Merge pull request #3473 from Multiverse/feat/better-dumps
Add support for new dumps viewer with more debug info and better ui
2026-05-20 12:21:00 +08:00
Ben Woo b8364a00b5 Add support for new dumps viewer with more debug info and better ui 2026-05-13 16:37:05 +08:00
Ben Woo 6e0ebf752b Merge pull request #3475 from Multiverse/feat/completion-filter
Improve tab completion for namespaced keys and properties
2026-05-13 14:52:48 +08:00
Ben Woo ebb809f899 Merge pull request #3472 from Multiverse/fix/entity-purge-player
Fix entity purging should ignore players
2026-05-13 14:21:01 +08:00
Ben Woo 8b1aea5d99 Improve tab completion for namespaced keys and properties 2026-05-13 14:17:47 +08:00
Ben Woo 4f92268ba2 Fix entity purging should ignore players 2026-05-09 11:38:31 +08:00
Ben Woo 39ac80be2c Merge pull request #3471 from Multiverse/refactor/jspecify
Remove use of jspecify annotation in favour of jetbrains
2026-05-08 20:42:24 +08:00
Ben Woo 5f43d756e5 Merge pull request #3470 from Multiverse/fix/name-key-lookup
Fix world exist checking not looking up both name and key
2026-05-08 20:41:37 +08:00
Ben Woo 50efb7e262 Remove use of jspecify annotation in favour of jetbrains 2026-05-08 20:39:05 +08:00
Ben Woo 142ef02ca0 Fix world exist checking not looking up both name and key 2026-05-08 20:37:06 +08:00
Ben Woo 54900f55a9 Merge pull request #3469 from Multiverse/feat/world-meta
Add support for setting metadata key value for each world
2026-05-08 15:02:43 +08:00
Ben Woo dbc91e401c Proper yaml key format checking for map node key 2026-05-08 14:43:40 +08:00
Ben Woo e32b478cef Add support for setting metadata key value for each world 2026-05-07 17:57:57 +08:00
Ben Woo e82bc005f2 Merge pull request #3468 from Multiverse/fix/regen-bonus-chest
Retain bonus chest setting on regen world
2026-05-06 23:14:19 +08:00
Ben Woo 504f17eb1a Bump mockbukkit to version that supports bonus chest 2026-05-06 22:21:36 +08:00
Ben Woo a54a3b6152 Merge pull request #3467 from Multiverse/refactor/final-api-class
Make MultiverseCoreApi class final to prevent subclassing
2026-05-06 22:19:34 +08:00
Ben Woo 0fdc74ab56 Retain bonus chest setting on regen world 2026-05-06 22:18:05 +08:00
Ben Woo e29b851c61 Make MultiverseCoreApi class final to prevent subclassing 2026-05-06 20:31:58 +08:00
Ben Woo 534f2743c4 Merge pull request #3466 from Multiverse/fix/warn-keep-spawn
Notify users about the removal of the keep spawn in memory feature in Minecraft 1.21.9+
2026-05-06 15:50:32 +08:00
Ben Woo c486f0306e Notify users about the removal of the keep spawn in memory feature in Minecraft 1.21.9+ 2026-05-06 15:44:53 +08:00
Ben Woo 392d66ac85 Merge pull request #3465 from Multiverse/feat/generator-settings
Store generator settings used to ensure it persist during regen
2026-05-06 15:01:49 +08:00
Ben Woo 6d88c6c8c4 Add generator-settings property to test files 2026-05-06 14:59:01 +08:00
Ben Woo 2e6dc23b1b Merge pull request #3464 from Multiverse/fix/remove-world-name
Fix unable to remove world config with legacy name as key
2026-05-06 14:55:26 +08:00
Ben Woo bf05abc3ca Fix unable to remove world config with legacy name as key 2026-05-06 14:51:56 +08:00
Ben Woo d65cde74e0 Store generator settings used to ensure it persist during regen 2026-05-05 21:10:43 +08:00
Ben Woo 357532d500 Add missing world properties to InfoCommand output and remove unused spawning info methods 2026-05-05 20:32:57 +08:00
Ben Woo a59ec16737 Merge pull request #3462 from Multiverse/feat/worldborder-ticks
Add support for setting world border time with ticks
2026-05-05 20:17:41 +08:00
Ben Woo cab6358f03 Merge pull request #3463 from Multiverse/refactor/get-coordinate-scale
Improve getCoordinateScale method checking
2026-05-05 16:38:27 +08:00
Ben Woo 5ad47da967 Add tests for TickDuration parsing and conversion methods 2026-05-05 16:22:54 +08:00
Ben Woo 603ab5bc0e Improve getCoordinateScale method checking 2026-05-05 16:02:44 +08:00
Ben Woo 5e7637bc42 Add support for setting world border time with ticks 2026-05-05 15:57:45 +08:00
Ben Woo bc07b24360 Merge pull request #3461 from Multiverse/feat/remove-player-dest
Add support for custom destination with `--remove-player` flag
2026-05-05 15:56:00 +08:00
Ben Woo 5451e55f17 Add support for custom destination with --remove-player flag 2026-05-04 16:53:38 +08:00
Ben Woo fd8d0261aa Merge pull request #3459 from Multiverse/feat/bonus-chest-force-spawn
Add support for creating world with bonus chest and force spawn
2026-05-04 14:19:59 +08:00
Ben Woo a7d067222a Merge pull request #3458 from Multiverse/feat/pre-creature-spawn
Add preCreatureSpawn event handler to optimize spawn checks
2026-05-04 14:18:49 +08:00
Ben Woo 0b18c8d832 Merge pull request #3456 from Multiverse/feat/potential-worlds
Add support for potential worlds finding with new 26.1 dimensions format
2026-05-04 14:18:37 +08:00
Ben Woo 7937d889da Add preCreatureSpawn event handler to optimize spawn checks 2026-05-04 14:15:16 +08:00
Ben Woo 867462b6ed Merge pull request #3460 from Multiverse/chore/bump-mockbukkit
Bump mockbukkit to fix skipped tests
2026-05-04 14:13:52 +08:00
Ben Woo ed923491b5 Merge pull request #3457 from Multiverse/fix/command-typo
Fix command typo in spawn adjustment logging message
2026-05-04 14:13:41 +08:00
Ben Woo dfeb72e501 Bump mockbukkit to fix skipped tests 2026-05-04 14:09:52 +08:00
Ben Woo 35133ee647 Add support for creating world with bonus chest and force spawn 2026-05-04 14:09:21 +08:00
Ben Woo 3e62084b23 Fix command typo in spawn adjustment logging message 2026-05-03 20:42:03 +08:00
Ben Woo 944acd8387 Add support for potential worlds finding with new 26.1 dimensions format 2026-05-03 20:01:39 +08:00
Ben Woo 9460f0b2b1 Merge pull request #3454 from Multiverse/feat/key-lookup
Add support for looking up worlds with namespace key string
2026-05-03 19:54:10 +08:00
Ben Woo 9ed58375d4 Merge pull request #3455 from Multiverse/fix/isIteratingOverLevels
Fix isIteratingOverLevels field not found
2026-05-03 19:24:12 +08:00
Ben Woo f470df6f2c Fix isIteratingOverLevels field not found 2026-05-03 17:32:47 +08:00
Ben Woo c291df201f Add support for looking up worlds with namespace key string 2026-05-03 17:28:49 +08:00
Ben Woo 58402b010d Merge pull request #3452 from Multiverse/fix/instantiation
Prevent instantiation of utility classes by adding private constructors
2026-05-03 17:16:29 +08:00
Ben Woo 8060514e47 Merge pull request #3453 from Multiverse/refactor/coord-scale
Move getCoordinateScale to WorldCompatibility
2026-05-03 17:16:15 +08:00
Ben Woo ac86b691ba Merge pull request #3451 from Multiverse/feat/info-key
Add world key to output in `/mv info` command
2026-05-03 12:57:33 +08:00
Ben Woo ee40eb5822 Prevent instantiation of utility classes by adding private constructors 2026-05-03 12:56:44 +08:00
Ben Woo 3052489952 Move getCoordinateScale to WorldCompatibility 2026-05-03 12:55:53 +08:00
Ben Woo a6cbe44411 Add world key to output in /mv info command 2026-05-03 12:39:01 +08:00
Ben Woo 65db598fd8 Merge pull request #3449 from Multiverse/feat/namespace-key
Migrate world config to store worlds using namespace key
2026-05-03 11:40:00 +08:00
Ben Woo 272e118eed Create, import and clone worlds with namespaced key 2026-05-02 23:20:25 +08:00
Ben Woo 9643cce272 Migrate world config to store worlds using namespace key 2026-05-02 16:18:18 +08:00
Ben Woo 372b600d68 Merge pull request #3447 from Multiverse/refactor/fail-if
Refactor long chained if-else to use Attempt#failIf
2026-05-01 22:57:43 +08:00
Ben Woo c9db931ad5 Merge pull request #3446 from Multiverse/refactor/try-reflect
Refactor compatibility utilities to use Try for method and class retrieval
2026-05-01 22:56:56 +08:00
Ben Woo 8a1bbebb0b Refactor long chained if-else to use Attempt#failIf 2026-05-01 13:53:22 +08:00
Ben Woo 192ee269fa Refactor compatibility utilities to use Try for method and class retrieval 2026-05-01 12:10:42 +08:00
Ben Woo 6c86c41f98 Merge pull request #3445 from Multiverse/chore/26.1.2-platform
Update platform upload version list to include 26.1.2
2026-04-30 22:42:35 +08:00
Ben Woo 0afd4dbb62 Update platform upload version list to include 26.1.2 2026-04-30 22:35:44 +08:00
Ben Woo f527fbd693 Merge pull request #3441 from xiaohuang2004/patch-2
Update multiverse-core_zh.properties
2026-04-16 10:22:04 +08:00
xiaohuang2004 b50687db3b Fix formatting in multiverse-core_zh.properties 2026-04-13 11:50:57 +08:00
xiaohuang2004 a28a071e1e Fix punctuation in Chinese property file 2026-04-13 11:48:04 +08:00
xiaohuang2004 66dc376633 Update multiverse-core_zh.properties
The Chinese translation has been updated based on the latest English file, and some translations have been optimized simultaneously for more precise expression of meaning.
2026-04-10 15:41:53 +08:00
Ben Woo 5e8e9380e3 Merge pull request #3440 from Multiverse/fix/26.1-folders-lowercased
Fix world folder being lowercased due to namespace changes in 26.1
2026-04-08 16:14:28 +08:00
Ben Woo 4a16fa2297 Fix world folder being lowercased due to namespace changes in 26.1 2026-04-08 14:20:01 +08:00
Ben Woo b9e07a85d0 Merge pull request #3439 from Multiverse/fix/papermc-26.1-migration
Add support for detecting PaperMC 26.1 folder migration
2026-04-08 11:11:29 +08:00
Ben Woo 53c7459c1d Add support for detecting PaperMC 26.1 folder migration 2026-04-08 11:00:25 +08:00
Ben Woo 112505bc93 Merge pull request #3437 from Multiverse/fix/ansi-reset
Fix ANSI color format reset in log messages
2026-04-04 20:55:50 +08:00
Ben Woo e27fe73faa Fix ANSI color format reset in log messages 2026-04-04 14:46:06 +08:00
Ben Woo 9bc2b4dfaf Merge pull request #3422 from Multiverse/feat/clone-unloaded
Add support for cloning unloaded worlds
2026-04-04 14:32:14 +08:00
Ben Woo 3ba8beefc5 Merge pull request #3436 from Multiverse/fix/clone-paper-metadata
Fix clone not working due to duplicated Paper world metadata in 26.1
2026-04-04 13:28:16 +08:00
Ben Woo 05b6b380e0 Fix clone not working due to duplicated Paper world metadata in 26.1 2026-04-04 12:31:25 +08:00
Ben Woo b58ea2e04b Update error messages for invalid world folder following the format of 2026-04-04 12:17:03 +08:00
Ben Woo f4405874d1 Merge branch 'main' into feat/clone-unloaded 2026-04-04 12:05:01 +08:00
Ben Woo 247685c93d Merge pull request #3435 from Multiverse/feat/26.1-compatibility
Update world folder discovery and checking to support new 26.1 format
2026-04-04 11:38:16 +08:00
Ben Woo 490107b5d5 Fix tests due to world folder format changes 2026-04-04 11:35:30 +08:00
Ben Woo 122e35103a Fix some formatting and documentation 2026-04-04 11:22:08 +08:00
Ben Woo 25eeefbc77 Update platform upload version list to include 26.1 2026-04-04 11:16:12 +08:00
Ben Woo 6b5ce2aebb Update world folder discovery and checking to support new 26.1 format 2026-04-04 11:13:43 +08:00
Ben Woo e9905ca281 Merge pull request #3434 from Multiverse/fix/load-flags
Fix `--skip-folder-check` flag not showing for `/mv load` command
2026-04-03 22:01:02 +08:00
Ben Woo e002282b13 Fix --skip-folder-check flag not showing for /mv load command 2026-04-03 21:56:13 +08:00
Ben Woo 39711527fa Merge pull request #3431 from Multiverse/feat/placeholder-isloaded
Add PlaceholderAPI support for showing world load status (`%multiverse-core_isloaded_<worldname>%`)
2026-03-10 22:31:42 +08:00
Ben Woo 8d9b29ff87 Add PlaceholderAPI support for showing world load status (%multiverse-core_isloaded_<worldname>%) 2026-03-10 22:27:38 +08:00
Ben Woo 82fe350afa Merge pull request #3429 from Multiverse/feat/apply-passenger-mode
Apply passenger mode to join location and first spawn as well
2026-03-09 10:48:15 +08:00
Ben Woo a7b4b9bbda Merge pull request #3430 from Multiverse/chore/access-modifier
Change entry fee migrator class access modifier to private
2026-03-09 10:47:35 +08:00
Ben Woo 15f511b660 Change entry fee migrator class access modifier to private 2026-03-09 10:40:00 +08:00
Ben Woo 8347cf8fbb Apply passenger mode to join location and first spawn as well 2026-03-09 10:39:13 +08:00
Ben Woo 4c27fea26b Merge pull request #3427 from Multiverse/fix/regen-biome
Fix biome not applying during world regen
2026-02-26 10:43:47 +08:00
Ben Woo 4111da8fd6 Fix biome not applying during world regen 2026-02-26 10:34:13 +08:00
Ben Woo 167f6f8a37 Merge pull request #3423 from Multiverse/fix/placeholder-null
Fix null error when world name for non-player placeholders not specified
2026-01-28 10:22:42 +08:00
Ben Woo 325b5d7643 Fix null error when world name for non-player placeholders not specified 2026-01-28 10:16:03 +08:00
Ben Woo 5dfb098d5c Add support for cloning unloaded worlds 2026-01-26 12:18:47 +08:00
Ben Woo 6c50bfb08e Merge pull request #3421 from Multiverse/fix/null-to-location
Better null checking for to and from event locations
2026-01-20 22:50:49 +08:00
Ben Woo 37c0580828 Merge pull request #3420 from Multiverse/chore/bump-mockbukkit
Bump mockbukkit and associated mc version to 1.21.11
2026-01-20 22:49:48 +08:00
Ben Woo e1a42cec92 Bump mockbukkit and associated mc version to 1.21.11 2026-01-20 22:45:13 +08:00
Ben Woo a6cc8bd68e Better null checking for to and from event locations 2026-01-20 22:44:04 +08:00
Ben Woo ee4cd18522 Merge pull request #3410 from Multiverse/v5.5-2
V5.5-2
2026-01-03 23:17:13 +08:00
Ben Woo b6c309a332 Merge pull request #3417 from Multiverse/fix/since-annotation
Add @since annotation for worldPropertyStrings method in CreateWorldOptions
2026-01-03 23:01:44 +08:00
Ben Woo 87571e7037 Add @since annotation for worldPropertyStrings method in CreateWorldOptions 2026-01-03 22:59:18 +08:00
Ben Woo 0822b1460a Merge pull request #3415 from Multiverse/refactor/string-formatter
Enhance StringFormatter methods to handle null inputs and improve type annotations
2026-01-03 22:23:19 +08:00
Ben Woo 1220998c12 Merge pull request #3416 from Multiverse/chore/logging
Add a logging message when plugin shutdown
2026-01-02 14:27:06 +08:00
Ben Woo e078c5e5dd Add a logging message when plugin shutdown 2026-01-02 14:08:49 +08:00
Ben Woo c1ef8f656b Enhance StringFormatter methods to handle null inputs and improve type annotations 2026-01-02 09:49:35 +08:00
Ben Woo 86e2112d87 Merge pull request #3414 from Multiverse/feat/properties-flag
Add support for customising world properties during world creation
2026-01-02 09:39:36 +08:00
Ben Woo 8a77dc807e Add --properties syntax to create command 2026-01-02 09:37:08 +08:00
Ben Woo 77382dacf3 Add support for customising world properties during world creation 2026-01-02 09:14:00 +08:00
Ben Woo 588a8e1baf Merge pull request #3413 from Multiverse/fix/seperated-typo
Fix spelling error for addOnToCommaSeparated method name
2026-01-02 08:59:18 +08:00
Ben Woo 5bd37d17c3 Fix spelling error for addOnToCommaSeparated method name 2026-01-01 23:32:10 +08:00
Ben Woo 815637e00f Merge pull request #3412 from Multiverse/feat/world-change-priority
Add event priority configuration for PlayerChangedWorldEvent
2026-01-01 23:19:22 +08:00
Ben Woo 78194389eb Merge pull request #3411 from Multiverse/feat/enforce-delay
Add configurable delay for gamemode and flight enforcement on world change
2026-01-01 23:13:52 +08:00
Ben Woo 2db55fab3b Add event priority configuration for PlayerChangedWorldEvent 2026-01-01 23:12:42 +08:00
Ben Woo c505da76ec Fix config test 2026-01-01 22:36:26 +08:00
Ben Woo 6152620541 Merge pull request #3409 from Multiverse/fix/worldborder-safety
Add world border check for spawn safety
2026-01-01 22:30:46 +08:00
Ben Woo cf70b3b839 Add configurable delay for gamemode and flight enforcement on world change 2026-01-01 22:30:10 +08:00
Ben Woo 527085713c Add world border check for spawn safety 2026-01-01 15:03:24 +08:00
Ben Woo f446c94dbc Merge pull request #3407 from Multiverse/v5.5
V5.5
2026-01-01 13:48:07 +08:00
Ben Woo aac0111dde Merge pull request #3408 from Multiverse/fix/case-insensitive-worldname
Fix case-insensitive world name handling
2026-01-01 12:59:53 +08:00
Ben Woo f66e673ea9 Fix case-insensitive world name handling 2026-01-01 12:42:18 +08:00
Ben Woo 1434ca176a Merge pull request #3406 from Multiverse/feat/papi-config
Add config options `warn-invalid-papi-format` and `invalid-papi-format-returns-blank`
2025-12-31 22:36:48 +08:00
Ben Woo 212b68437e Add config options warn-invalid-papi-format and invalid-papi-format-returns-blank 2025-12-31 21:14:56 +08:00
Ben Woo 05d01884ad Merge pull request #3405 from Multiverse/feat/papi-unloaded-world
Allow placerholders to work on unloaded worlds
2025-12-31 20:48:42 +08:00
Ben Woo 65c399740e Allow placerholders to work on unloaded worlds 2025-12-31 20:41:08 +08: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
172 changed files with 9183 additions and 1875 deletions

No files matched your search

+2 -2
View File
@@ -11,9 +11,9 @@ jobs:
pull-requests: write
continue-on-error: true
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- uses: dbelyaev/action-checkstyle@v0.8.4
- uses: dbelyaev/action-checkstyle@v3
continue-on-error: true
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -107,9 +107,9 @@ jobs:
files: '["${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar"]'
name: ${{ steps.release-info.outputs.tag_name }}
changelog: ${{ steps.release-artifact.outputs.body }}
game_versions: 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
game_versions: 26.1.2, 26.1.1, 26.1, 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
version_type: ${{ steps.parse-release-type.outputs.release_type }}
loaders: bukkit, spigot, paper
loaders: bukkit, spigot, paper, purpur
dependencies: ${{ inputs.modrinth_dependencies }}
- name: Upload to DBO
@@ -121,7 +121,7 @@ jobs:
changelog: ${{ steps.release-artifact.outputs.body }}
changelog_type: markdown
display_name: ${{ steps.release-info.outputs.tag_name }}
game_versions: 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
game_versions: 26.1.2, 26.1.1, 26.1, 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
release_type: ${{ steps.parse-release-type.outputs.release_type }}
project_relations: ${{ inputs.dbo_project_relations }}
file_path: ${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar
@@ -136,5 +136,5 @@ jobs:
channel: ${{ steps.parse-release-type.outputs.release_type }}
files: '[{"path": "${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar", "platforms": ["PAPER"]}]'
description: ${{ steps.release-artifact.outputs.body }}
platform_dependencies: '{"PAPER": ["1.18.2-1.21.8"]}'
platform_dependencies: '{"PAPER": ["1.18.2-26.1.2"]}'
plugin_dependencies: ${{ inputs.hangar_plugin_dependencies }}
+3 -3
View File
@@ -1,7 +1,7 @@
import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'org.mvplugins.multiverse-plugin' version '1.2.0'
id 'org.mvplugins.multiverse-plugin' version '1.2.2'
}
group = 'org.mvplugins.multiverse.core'
@@ -25,9 +25,9 @@ repositories {
}
configure(apiDependencies) {
serverApiVersion = '1.19.4-R0.1-SNAPSHOT'
serverApiVersion = '1.21.11-R0.1-SNAPSHOT'
mockBukkitServerApiVersion = '1.21'
mockBukkitVersion = '4.72.2'
mockBukkitVersion = '4.110.0'
}
dependencies {
@@ -124,9 +124,9 @@ public class MultiverseCore extends MultiverseModule {
public void onDisable() {
super.onDisable();
MultiverseCoreApi.shutdown();
saveAllConfigs();
shutdownDependencyInjection();
PluginServiceLocatorFactory.get().shutdown();
Logging.info("- Disabled");
Logging.shutdown();
}
@@ -215,22 +215,23 @@ 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()));
}
/**
* Logs the enable message.
*/
private void logEnableMessage() {
Logging.config("\u001B[32mVersion %s (API v%s) Enabled - By %s\u001B[39m",
Logging.config("\u001B[32mVersion %s (API v%s) Enabled - By %s\u001B[0m",
this.getDescription().getVersion(), getVersionAsNumber(), StringFormatter.joinAnd(getDescription().getAuthors()));
if (configProvider.get().isShowingDonateMessage()) {
Logging.config("\u001B[32mLoving Multiverse-Core? Please consider supporting the project with a small donation: https://github.com/sponsors/Multiverse\u001B[39m");
Logging.config("\u001B[32mLoving Multiverse-Core? Please consider supporting the project with a " +
"small donation: https://github.com/sponsors/Multiverse\u001B[0m");
}
}
@@ -24,7 +24,7 @@ import java.util.function.Consumer;
/**
* Provides access to the MultiverseCore API.
*/
public class MultiverseCoreApi {
public final class MultiverseCoreApi {
private static MultiverseCoreApi instance;
private static final List<Consumer<MultiverseCoreApi>> whenLoadedCallbacks = new ArrayList<>();
@@ -13,27 +13,33 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.CoreConfig;
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.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@Service
final class PlaceholderExpansionHook extends PlaceholderExpansion {
private final MultiverseCore plugin;
private final CoreConfig coreConfig;
private final WorldManager worldManager;
private final MVEconomist economist;
@Inject
public PlaceholderExpansionHook(MultiverseCore plugin, WorldManager worldManager, MVEconomist economist) {
public PlaceholderExpansionHook(@NotNull MultiverseCore plugin,
@NotNull CoreConfig coreConfig,
@NotNull WorldManager worldManager,
@NotNull MVEconomist economist) {
this.plugin = plugin;
this.coreConfig = coreConfig;
this.worldManager = worldManager;
this.economist = economist;
}
@@ -44,6 +50,13 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
return super.register();
}
@Override
public void warning(String msg) {
if (coreConfig.getWarnInvalidPapiFormat()) {
super.warning(msg);
}
}
@Override
public @NotNull String getIdentifier() {
return "multiverse-core";
@@ -82,130 +95,115 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
return null;
}
final var placeholder = paramsArray.removeFirst();
final var placeholder = paramsArray.remove(0).toLowerCase(Locale.ENGLISH);
String worldName = parseWorldName(offlinePlayer, paramsArray);
if (worldName == null) return null;
return worldManager.getLoadedWorld(worldName)
.onEmpty(() -> warning("Multiverse World not found: " + worldName))
.map(world -> getWorldPlaceHolderValue(placeholder, paramsArray, world))
.getOrNull();
return parseWorldName(offlinePlayer, paramsArray)
.flatMap(worldName -> worldManager.getWorld(worldName)
.onEmpty(() -> warning("Multiverse World not found: " + worldName)))
.flatMap(world -> world.asLoadedWorld()
.flatMap(loadedWorld -> getLoadedWorldPlaceHolderValue(placeholder, paramsArray, loadedWorld))
.orElse(() -> getWorldPlaceHolderValue(placeholder, paramsArray, world)))
.getOrElse(() -> coreConfig.getInvalidPapiFormatReturnsBlank() ? "" : null);
}
private @Nullable String parseWorldName(OfflinePlayer offlinePlayer, List<String> paramsArray) {
private @NotNull Option<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();
return Option.of(player.getWorld().getName());
} else {
warning("You must specify a world name for non-player placeholders");
return null;
return Option.none();
}
}
// Try get from params
String paramWorldName = paramsArray.getLast();
if (worldManager.isLoadedWorld(paramWorldName)) {
paramsArray.removeLast();
return paramWorldName;
String paramWorldName = paramsArray.get(paramsArray.size() - 1);
if (worldManager.isWorld(paramWorldName)) {
paramsArray.remove(paramsArray.size() - 1);
return Option.of(paramWorldName);
}
// Param not a world, fallback to player
if (offlinePlayer instanceof Player player) {
return player.getWorld().getName();
return Option.of(player.getWorld().getName());
}
warning("Multiverse World not found: " + paramWorldName);
return null;
return Option.none();
}
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()) {
case "alias" -> {
return world.getAliasOrName();
}
case "animalspawn" -> {
return String.valueOf(world.getEntitySpawnConfig().getSpawnCategoryConfig(SpawnCategory.ANIMAL).isSpawn());
}
case "autoheal" -> {
return String.valueOf(world.getAutoHeal());
}
case "blacklist" -> {
return String.join(", ", world.getWorldBlacklist());
}
case "currency" -> {
return String.valueOf(world.getCurrency());
}
case "difficulty" -> {
return world.getDifficulty().toString();
}
case "entryfee" -> {
return economist.formatPrice(world.getPrice(), world.getCurrency());
}
case "environment" -> {
return world.getEnvironment().toString().toLowerCase();
}
case "flight" -> {
return String.valueOf(world.isAllowFlight());
}
case "gamemode" -> {
return world.getGameMode().toString().toLowerCase();
}
case "generator" -> {
return world.getGenerator();
}
case "hunger" -> {
return String.valueOf(world.isHunger());
}
case "monstersspawn" -> {
return String.valueOf(world.getEntitySpawnConfig().getSpawnCategoryConfig(SpawnCategory.MONSTER).isSpawn());
}
case "name" -> {
return world.getName();
}
case "playerlimit" -> {
return String.valueOf(world.getPlayerLimit());
}
case "price" -> {
return String.valueOf(world.getPrice());
}
case "pvp" -> {
return String.valueOf(world.getPvp());
}
case "seed" -> {
return String.valueOf(world.getSeed());
}
case "time" -> {
String timeFormat = !placeholderParams.isEmpty() ? placeholderParams.getFirst() : "";
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);
}
private @NotNull Option<String> getWorldPlaceHolderValue(@NotNull String placeholder,
@NotNull List<String> placeholderParams,
@NotNull MultiverseWorld world) {
return Option.of(switch (placeholder) {
case "alias" -> world.getAliasOrName();
case "animalspawn" -> String.valueOf(world.getEntitySpawnConfig()
.getSpawnCategoryConfig(SpawnCategory.ANIMAL)
.isSpawn());
case "autoheal" -> String.valueOf(world.getAutoHeal());
case "blacklist" -> String.join(", ", world.getWorldBlacklist());
case "currency" -> String.valueOf(world.getCurrency());
case "difficulty" -> world.getDifficulty().toString();
case "entryfee" -> economist.formatPrice(world.getPrice(), world.getCurrency());
case "environment" -> world.getEnvironment().toString().toLowerCase();
case "flight" -> String.valueOf(world.isAllowFlight());
case "gamemode" -> world.getGameMode().toString().toLowerCase();
case "generator" -> world.getGenerator();
case "hunger" -> String.valueOf(world.isHunger());
case "isloaded" -> String.valueOf(world.isLoaded());
case "key" -> String.valueOf(world.getKey());
case "meta" -> {
if (placeholderParams.isEmpty()) {
warning("No meta key specified.");
yield null;
}
String metaKey = String.join("_", placeholderParams);
yield world.getMeta(metaKey)
.getOrElse(() -> {
warning("Meta key '" + metaKey + "' not found for world '" + world.getName() + "'.");
return null;
});
}
case "type" -> {
return world.getBukkitWorld().map(World::getWorldType).map(Enum::name).getOrElse("null");
}
case "weather" -> {
return String.valueOf(world.isAllowWeather());
case "monstersspawn" -> String.valueOf(world.getEntitySpawnConfig()
.getSpawnCategoryConfig(SpawnCategory.MONSTER)
.isSpawn());
case "name" -> world.getName();
case "playerlimit" -> String.valueOf(world.getPlayerLimit());
case "price" -> String.valueOf(world.getPrice());
case "pvp" -> String.valueOf(world.getPvp());
case "seed" -> String.valueOf(world.getSeed());
case "weather" -> String.valueOf(world.isAllowWeather());
case "playercount", "time", "type" -> {
warning("Placeholder '" + placeholder + "' is only available for loaded worlds.");
yield null;
}
default -> {
warning("Unknown placeholder: " + placeholder);
return null;
warning("Unknown Placeholder: " + placeholder);
yield null;
}
}
});
}
private @NotNull Option<String> getLoadedWorldPlaceHolderValue(@NotNull String placeholder,
@NotNull List<String> placeholderParams,
@NotNull LoadedMultiverseWorld world) {
return Option.of(switch (placeholder) {
case "playercount" -> String.valueOf(world.getBukkitWorld()
.map(World::getPlayers)
.map(List::size)
.getOrElse(-1));
case "time" -> {
String timeFormat = !placeholderParams.isEmpty() ? placeholderParams.get(0) : "";
long time = world.getBukkitWorld().map(World::getTime).getOrElse(0L);
yield switch (timeFormat) {
case "" -> String.valueOf(time);
case "12h" -> MinecraftTimeFormatter.format12h(time);
case "24h" -> MinecraftTimeFormatter.format24h(time);
default -> MinecraftTimeFormatter.formatTime(time, timeFormat);
};
}
case "type" -> world.getBukkitWorld().map(World::getWorldType).map(Enum::name).getOrElse("null");
default -> null;
});
}
}
@@ -0,0 +1,60 @@
package org.mvplugins.multiverse.core.command;
import co.aikar.commands.CommandCompletionContext;
import co.aikar.commands.CommandCompletionFilter;
import co.aikar.commands.apachecommonslang.ApacheCommonsLangUtil;
import org.jetbrains.annotations.ApiStatus;
import java.util.Locale;
/**
* Utility filters for command completion matching.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public final class MVCommandCompletionFilters {
/**
* Matches namespaced keys (for example, {@code minecraft:stone}) using namespace-aware checks.
* <p>
* This filter accepts completions that:
* <ul>
* <li>start with the current input,</li>
* <li>have a namespace starting with the input, or</li>
* <li>have a key value containing the input.</li>
* </ul>
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static final CommandCompletionFilter NAMESPACED_KEY = (context, completion) -> {
String[] split = completion.split(":", 2);
if (split.length < 2) {
return ApacheCommonsLangUtil.startsWithIgnoreCase(completion, context.getInput());
}
String lowerCase = context.getInput().toLowerCase(Locale.ROOT);
return ApacheCommonsLangUtil.startsWithIgnoreCase(completion, context.getInput())
|| split[0].toLowerCase(Locale.ROOT).startsWith(lowerCase)
|| split[1].toLowerCase(Locale.ROOT).contains(lowerCase);
};
/**
* Gets the namespaced-key completion filter with a generic type-safe signature.
* Use this method instead of {@link MVCommandCompletionFilters#NAMESPACED_KEY}
* to avoid raw type unchecked warnings.
*
* @param <C> completion context type
* @return the namespaced key filter
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static <C extends CommandCompletionContext> CommandCompletionFilter<C> namespacedKey() {
return NAMESPACED_KEY;
}
private MVCommandCompletionFilters() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -6,10 +6,12 @@ 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;
import co.aikar.commands.BukkitCommandCompletionContext;
import co.aikar.commands.CommandCompletionFilter;
import co.aikar.commands.CommandIssuer;
import co.aikar.commands.PaperCommandCompletions;
import co.aikar.commands.RegisteredCommand;
@@ -26,6 +28,7 @@ import org.bukkit.GameRule;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.SpawnCategory;
import org.jetbrains.annotations.NotNull;
@@ -42,16 +45,17 @@ 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;
import org.mvplugins.multiverse.core.world.generators.GeneratorPlugin;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
import org.mvplugins.multiverse.core.world.helpers.PotentialWorldFinder;
import static org.mvplugins.multiverse.core.utils.StringFormatter.addonToCommaSeperated;
import static org.mvplugins.multiverse.core.utils.StringFormatter.addOnToCommaSeparated;
@Service
public class MVCommandCompletions extends PaperCommandCompletions {
@@ -63,6 +67,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
private final CorePermissionsChecker corePermissionsChecker;
private final AnchorManager anchorManager;
private final GeneratorProvider generatorProvider;
private final PotentialWorldFinder potentialWorldFinder;
@Inject
MVCommandCompletions(
@@ -72,7 +77,8 @@ public class MVCommandCompletions extends PaperCommandCompletions {
@NotNull CoreConfig config,
@NotNull CorePermissionsChecker corePermissionsChecker,
@NotNull AnchorManager anchorManager,
@NotNull GeneratorProvider generatorProvider
@NotNull GeneratorProvider generatorProvider,
@NotNull PotentialWorldFinder potentialWorldFinder
) {
super(mvCommandManager);
this.commandManager = mvCommandManager;
@@ -82,27 +88,29 @@ public class MVCommandCompletions extends PaperCommandCompletions {
this.corePermissionsChecker = corePermissionsChecker;
this.anchorManager = anchorManager;
this.generatorProvider = generatorProvider;
this.potentialWorldFinder = potentialWorldFinder;
registerAsyncCompletion("anchornames", this::suggestAnchorNames);
registerAsyncCompletion("anchornames", this::suggestAnchorNames, CommandCompletionFilter.contains());
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);
registerStaticCompletion("gamerules", this::suggestGamerules, MVCommandCompletionFilters.namespacedKey());
registerAsyncCompletion("gamerulesvalues", this::suggestGamerulesValues);
registerAsyncCompletion("generatorplugins", this::suggestGeneratorPlugins);
registerStaticCompletion("materials", suggestEnums(Material.class));
registerStaticCompletion("mvconfigs", config.getStringPropertyHandle().getAllPropertyNames());
registerAsyncCompletion("generatorplugins", this::suggestGeneratorPlugins, CommandCompletionFilter.contains());
registerStaticCompletion("materials", suggestEnums(Material.class), CommandCompletionFilter.contains());
registerStaticCompletion("mvconfigs", config.getStringPropertyHandle().getAllPropertyNames(), CommandCompletionFilter.contains());
registerAsyncCompletion("mvconfigvalues", this::suggestMVConfigValues);
registerAsyncCompletion("mvworlds", this::suggestMVWorlds);
registerAsyncCompletion("mvworldpropsname", this::suggestMVWorldPropsName);
registerAsyncCompletion("mvworlds", this::suggestMVWorlds, MVCommandCompletionFilters.namespacedKey());
registerAsyncCompletion("mvworldmetakey", this::suggestMVWorldMetaKey, CommandCompletionFilter.contains());
registerAsyncCompletion("mvworldpropsname", this::suggestMVWorldPropsName, CommandCompletionFilter.contains());
registerAsyncCompletion("mvworldpropsvalue", this::suggestMVWorldPropsValue);
registerAsyncCompletion("playersarray", this::suggestPlayersArray);
registerCompletion("playersarray", this::suggestPlayersArray, CommandCompletionFilter.contains()); // getting online players cannot be async
registerStaticCompletion("propsmodifyaction", suggestEnums(PropertyModifyAction.class));
registerStaticCompletion("spawncategories", suggestEnums(SpawnCategory.class));
registerAsyncCompletion("spawncategorypropsname", this::suggestSpawnCategoryPropsName);
registerAsyncCompletion("spawncategorypropsname", this::suggestSpawnCategoryPropsName, CommandCompletionFilter.contains());
registerAsyncCompletion("spawncategorypropsvalue", this::suggestSpawnCategoryPropsValue);
setDefaultCompletion("destinations", DestinationInstance.class);
@@ -161,7 +169,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
}
}
if (context.hasConfig("multiple")) {
return addonToCommaSeperated(context.getInput(), handler.getCompletions(context));
return addOnToCommaSeparated(context.getInput(), handler.getCompletions(context));
}
return handler.getCompletions(context);
}
@@ -176,7 +184,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
public <T extends Enum<T>> Collection<String> suggestEnums(Class<T> enumClass) {
return EnumSet.allOf(enumClass).stream()
.map(Enum::name)
.map(String::toLowerCase)
.map(name -> name.toLowerCase(Locale.ENGLISH))
.toList();
}
@@ -223,18 +231,22 @@ public class MVCommandCompletions extends PaperCommandCompletions {
// Most likely console did not specify a player
return Collections.<String>emptyList();
}
if (context.hasConfig("othersOnly") && (players.length == 1 && players[0].equals(context.getIssuer().getIssuer()))) {
CommandSender sender = context.getIssuer().getIssuer();
if (context.hasConfig("othersOnly") && (players.length == 1 && players[0].equals(sender))) {
return Collections.<String>emptyList();
}
return suggestDestinationsWithPerms(context.getIssuer().getIssuer(), players, context.getInput());
return suggestDestinationsWithPerms(sender, Arrays.asList(players), context.getInput());
})
.getOrElse(Collections.emptyList());
}
private Collection<String> suggestDestinationsWithPerms(CommandSender teleporter, Player[] players, String deststring) {
private Collection<String> suggestDestinationsWithPerms(CommandSender teleporter, List<Entity> teleportees, String deststring) {
return destinationsProvider.suggestDestinations(teleporter, deststring).stream()
.filter(packet -> !config.getSimplifiedDestinationTabCompletion()
|| packet.destination() instanceof WorldDestination
|| deststring.startsWith(packet.destination().getIdentifier() + ":"))
.filter(packet -> corePermissionsChecker
.checkDestinationPacketPermission(teleporter, Arrays.asList(players), packet))
.checkDestinationPacketPermission(teleporter, teleportees, packet))
.map(DestinationSuggestionPacket::parsableString)
.toList();
}
@@ -278,29 +290,33 @@ 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" -> {
return worldManager.getPotentialWorlds();
return potentialWorldFinder.findPotentialWorlds();
}
}
Logging.severe("Invalid MVWorld scope: " + scope);
return Collections.emptyList();
}
private Collection<String> suggestMVWorldMetaKey(BukkitCommandCompletionContext context) {
return Try.of(() -> context.getContextValue(MultiverseWorld.class))
.map(MultiverseWorld::getAllMeta)
.map(Map::keySet)
.getOrElse(Collections.emptySet());
}
private Collection<String> suggestMVWorldPropsName(BukkitCommandCompletionContext context) {
return Try.of(() -> {
MultiverseWorld world = context.getContextValue(MultiverseWorldValue.class).value();
@@ -331,7 +347,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
matchedPlayers.add(name);
}
}
return addonToCommaSeperated(context.getInput(), matchedPlayers);
return addOnToCommaSeparated(context.getInput(), matchedPlayers);
}
private Collection<String> suggestSpawnCategoryPropsName(BukkitCommandCompletionContext context) {
@@ -11,6 +11,7 @@ import co.aikar.commands.MinecraftMessageKeys;
import co.aikar.commands.PaperCommandContexts;
import co.aikar.commands.contexts.ContextResolver;
import com.google.common.base.Strings;
import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
@@ -41,6 +42,7 @@ 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.utils.tick.TickDuration;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -92,6 +94,7 @@ public class MVCommandContexts extends PaperCommandContexts {
registerIssuerAwareContext(PlayerArrayValue.class, playerArrayContextBuilder().generateContext(PlayerArrayValue::new));
registerIssuerAwareContext(PlayerLocation.class, this::parsePlayerLocation);
registerContext(SpawnCategory[].class, this::parseSpawnCategories);
registerContext(TickDuration.class, this::parseTickDuration);
}
private MVCommandIssuer parseMVCommandIssuer(BukkitCommandExecutionContext context) {
@@ -116,7 +119,8 @@ public class MVCommandContexts extends PaperCommandContexts {
}
return destinationsProvider.parseDestination(context.getSender(), destination)
.getOrThrow(failure -> MVInvalidCommandArgument.of(failure.getFailureMessage()));
.getOrThrow(failure ->
new InvalidCommandArgument(failure.getFailureMessage().formatted(context.getIssuer())));
}
private GameRule<?> parseGameRule(BukkitCommandExecutionContext context) {
@@ -271,13 +275,25 @@ public class MVCommandContexts extends PaperCommandContexts {
private IssuerAwareContextBuilder<Player[]> playerArrayContextBuilder() {
return new IssuerAwareContextBuilder<Player[]>()
.fromPlayer((context, player) -> new Player[]{player})
.fromInput((context, input) -> {
Player[] players = PlayerFinder.getMulti(input, context.getSender()).toArray(new Player[0]);
return (players.length == 0) ? null : players;
})
.fromInput((context, input) -> PlayerFinder
.tryGetMulti(input, context.getSender())
.map(list -> list.toArray(new Player[0]))
.map(arr -> (arr.length == 0) ? null : arr)
.getOrElseThrow(failure -> {
if (failure instanceof MVInvalidCommandArgument mvFailure) {
throw mvFailure;
}
throw new InvalidCommandArgument(failure.getLocalizedMessage() + " "
+ Option.of(failure.getCause()).map(Throwable::getLocalizedMessage).getOrElse(""));
}))
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player."))
.issuerAwareInputFailMessage((context, input) -> Message.of("Invalid player: " + input + ". Either specify an online player or use this command as a player."))
.inputOnlyFailMessage((context, input) -> Message.of("Player " + input + " not found."));
.inputOnlyFailMessage((context, input) -> {
if (PlayerFinder.isSelector(input)) {
return Message.of("No player(s) matched selector: " + input + ".");
}
return Message.of("Player(s) " + input + " not found.");
});
}
private PlayerLocation parsePlayerLocation(BukkitCommandExecutionContext context) {
@@ -379,4 +395,13 @@ public class MVCommandContexts extends PaperCommandContexts {
}
return categories.toArray(new SpawnCategory[0]);
}
private TickDuration parseTickDuration(BukkitCommandExecutionContext context) {
String arg = context.popFirstArg();
return TickDuration.parseString(arg)
.getOrElseThrow(failure ->
new InvalidCommandArgument("Invalid time duration format: \"" +arg + "\". Use a number " +
"followed by an optional suffix (s for seconds, d for game days) or just a number " +
"for ticks. Examples: 100, 5s, 2d"));
}
}
@@ -112,9 +112,9 @@ public class MVCommandIssuer extends BukkitCommandIssuer {
} else {
var formatter = getManager().getFormat(messageType);
if (formatter != null) {
sendMessage(formatter.format(message.formatted()));
sendMessage(formatter.format(message.formatted(getManager().getLocales(), this)));
} else {
sendMessage(message.formatted());
sendMessage(message.formatted(getManager().getLocales(), this));
}
}
}
@@ -62,7 +62,6 @@ public class MVCommandManager extends PaperCommandManager {
this.config = config;
this.pluginLocales = new PluginLocales(this);
this.locales = this.pluginLocales;
this.pluginLocales.loadLanguages();
MVCommandConditions.load(this, worldManager, worldNameChecker);
this.enableUnstableAPI("help");
@@ -22,6 +22,9 @@ public class MVRootCommand extends BukkitRootCommand {
@Override
public List<String> getTabCompletions(CommandIssuer sender, String alias, String[] args, boolean commandsOnly, boolean isAsync) {
String[] quoteFormatedArgs = StringFormatter.parseQuotesInArgs(args).toArray(String[]::new);
return super.getTabCompletions(sender, alias, quoteFormatedArgs, commandsOnly, isAsync);
return super.getTabCompletions(sender, alias, quoteFormatedArgs, commandsOnly, isAsync)
.stream()
.map(StringFormatter::quoteMultiWordString)
.toList();
}
}
@@ -123,48 +123,8 @@ public final class IssuerAwareContextBuilder<T> {
*/
@ApiStatus.AvailableSince("5.1")
public IssuerAwareContextResolver<T, BukkitCommandExecutionContext> generateContext() {
Objects.requireNonNull(fromPlayer);
Objects.requireNonNull(fromInput);
Objects.requireNonNull(issuerOnlyFailMessage);
Objects.requireNonNull(issuerAwarePlayerFailMessage);
Objects.requireNonNull(issuerAwareInputFailMessage);
Objects.requireNonNull(inputOnlyFailMessage);
return context -> {
BukkitCommandIssuer issuer = context.getIssuer();
String resolve = context.getFlagValue("resolve", "");
if (resolve.equals("issuerOnly")) {
if (issuer.isPlayer()) {
T result = fromPlayer.apply(context, issuer.getPlayer());
if (result != null) {
return result;
}
}
throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer));
}
String input = context.getFirstArg();
T result = fromInput.apply(context, input);
if (result != null) {
context.popFirstArg();
return result;
}
if (resolve.equals("issuerAware")) {
if (issuer.isPlayer()) {
Player player = issuer.getPlayer();
result = fromPlayer.apply(context, player);
if (result != null) {
return result;
}
throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer));
}
throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
};
validateRequiredVariables();
return context -> resolveValue(context, GenericIssuerAwareValue::new).value;
}
/**
@@ -178,49 +138,66 @@ public final class IssuerAwareContextBuilder<T> {
*/
@ApiStatus.AvailableSince("5.1")
public <I extends IssuerAwareValue> IssuerAwareContextResolver<I, BukkitCommandExecutionContext> generateContext(BiFunction<Boolean, T, I> createValue) {
// todo: This is a copy and paste from above
validateRequiredVariables();
return context -> resolveValue(context, createValue);
}
private void validateRequiredVariables() {
Objects.requireNonNull(fromPlayer);
Objects.requireNonNull(fromInput);
Objects.requireNonNull(issuerOnlyFailMessage);
Objects.requireNonNull(issuerAwarePlayerFailMessage);
Objects.requireNonNull(issuerAwareInputFailMessage);
Objects.requireNonNull(inputOnlyFailMessage);
}
return context -> {
BukkitCommandIssuer issuer = context.getIssuer();
String resolve = context.getFlagValue("resolve", "");
private <I extends IssuerAwareValue> I resolveValue(BukkitCommandExecutionContext context, BiFunction<Boolean, T, I> createValue) {
BukkitCommandIssuer issuer = context.getIssuer();
String resolve = context.getFlagValue("resolve", "");
if (resolve.equals("issuerOnly")) {
if (issuer.isPlayer()) {
T result = fromPlayer.apply(context, issuer.getPlayer());
if (result != null) {
return createValue.apply(true, result);
}
if (resolve.equals("issuerOnly")) {
if (issuer.isPlayer()) {
T result = fromPlayer.apply(context, issuer.getPlayer());
if (result != null) {
return createValue.apply(true, result);
}
throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer));
}
throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer));
}
String input = context.getFirstArg();
T result = fromInput.apply(context, input);
if (result != null) {
context.popFirstArg();
return createValue.apply(false, result);
}
String input = context.getFirstArg();
T result = fromInput.apply(context, input);
if (result != null) {
context.popFirstArg();
return createValue.apply(false, result);
}
if (resolve.equals("issuerAware")) {
if (issuer.isPlayer()) {
Player player = issuer.getPlayer();
result = fromPlayer.apply(context, player);
if (result != null) {
return createValue.apply(true, result);
}
throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
int maxArgForAware = context.getFlagValue("maxArgForAware", Integer.MAX_VALUE);
long argLengthWithoutFlags = context.getArgs().stream()
.takeWhile(value -> !value.startsWith("-") && !value.isEmpty()) // ignore the flags
.count();
if (resolve.equals("issuerAware") && argLengthWithoutFlags <= maxArgForAware) {
if (issuer.isPlayer()) {
Player player = issuer.getPlayer();
result = fromPlayer.apply(context, player);
if (result != null) {
return createValue.apply(true, result);
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer));
throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer));
}
throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
};
throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
}
private static class GenericIssuerAwareValue<T> extends IssuerAwareValue {
private final T value;
public GenericIssuerAwareValue(boolean byIssuer, T value) {
super(byIssuer);
this.value = value;
}
}
}
@@ -3,9 +3,12 @@ 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 java.util.function.Supplier;
import co.aikar.commands.InvalidCommandArgument;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -39,34 +42,34 @@ public class CommandValueFlag<T> extends CommandFlag {
private final Class<T> type;
private final boolean optional;
private final T defaultValue;
private final Supplier<? extends T> defaultValueSupplier;
private final Function<String, T> context;
private final Function<String, Collection<String>> completion;
/**
* Creates a new flag.
*
* @param key The key for the new flag.
* @param aliases The aliases that also refer to this flag.
* @param type The type of the value.
* @param optional Allow for flag without value.
* @param defaultValue The default value if optional is true and user does not specify a value.
* @param context Function to parse string into value type.
* @param completion Function to get completion for this flag.
* @param key The key for the new flag.
* @param aliases The aliases that also refer to this flag.
* @param type The type of the value.
* @param optional Allow for flag without value.
* @param defaultValueSupplier The default value if optional is true and user does not specify a value.
* @param context Function to parse string into value type.
* @param completion Function to get completion for this flag.
*/
protected CommandValueFlag(
@NotNull String key,
@NotNull List<String> aliases,
@NotNull Class<T> type,
boolean optional,
@Nullable T defaultValue,
@Nullable Supplier<? extends T> defaultValueSupplier,
@Nullable Function<String, T> context,
@Nullable Function<String, Collection<String>> completion
) {
super(key, aliases);
this.type = type;
this.optional = optional;
this.defaultValue = defaultValue;
this.defaultValueSupplier = defaultValueSupplier;
this.context = context;
this.completion = completion;
}
@@ -95,7 +98,7 @@ public class CommandValueFlag<T> extends CommandFlag {
* @return The default value.
*/
public @Nullable T getDefaultValue() {
return defaultValue;
return defaultValueSupplier == null ? null : defaultValueSupplier.get();
}
/**
@@ -125,7 +128,7 @@ public class CommandValueFlag<T> extends CommandFlag {
public static class Builder<T, S extends Builder<T, S>> extends CommandFlag.Builder<S> {
protected final Class<T> type;
protected boolean optional = false;
protected T defaultValue = null;
protected Supplier<? extends T> defaultValueSupplier = null;
protected Function<String, T> context = null;
protected Function<String, Collection<String>> completion = null;
@@ -157,7 +160,21 @@ public class CommandValueFlag<T> extends CommandFlag {
* @return The builder.
*/
public @NotNull S defaultValue(@NotNull T defaultValue) {
this.defaultValue = defaultValue;
return defaultValue(() -> defaultValue);
}
/**
* Set the default value supplier. Used if optional is true and user does not specify a value.
* Supplier is only called when command is executed with the flag is present in input.
*
* @param defaultValueSupplier The default value supplier
* @return The builder
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull S defaultValue(@NotNull Supplier<? extends T> defaultValueSupplier) {
this.defaultValueSupplier = defaultValueSupplier;
return (S) this;
}
@@ -193,7 +210,7 @@ public class CommandValueFlag<T> extends CommandFlag {
if (context == null && !String.class.equals(type)) {
throw new IllegalStateException("Context is required for non-string value flags");
}
return new CommandValueFlag<>(key, aliases, type, optional, defaultValue, context, completion);
return new CommandValueFlag<>(key, aliases, type, optional, defaultValueSupplier, context, completion);
}
}
@@ -206,7 +223,7 @@ public class CommandValueFlag<T> extends CommandFlag {
public static class EnumBuilder<T extends Enum<T>, S extends EnumBuilder<T, S>> extends CommandFlag.Builder<S> {
protected final Class<T> type;
protected boolean optional = false;
protected T defaultValue = null;
protected Supplier<? extends T> defaultValueSupplier = null;
protected Function<String, T> context = null;
protected Function<String, Collection<String>> completion = null;
@@ -220,7 +237,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 +246,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;
@@ -252,7 +269,21 @@ public class CommandValueFlag<T> extends CommandFlag {
* @return The builder.
*/
public @NotNull S defaultValue(@NotNull T defaultValue) {
this.defaultValue = defaultValue;
return defaultValue(() -> defaultValue);
}
/**
* Set the default value to supply. Used if optional is true and user does not specify a value.
* Supplier is only called when command is executed with the flag is present in input.
*
* @param defaultValueSupplier The default value supplier.
* @return The builder.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull S defaultValue(@NotNull Supplier<? extends T> defaultValueSupplier) {
this.defaultValueSupplier = defaultValueSupplier;
return (S) this;
}
@@ -263,7 +294,7 @@ public class CommandValueFlag<T> extends CommandFlag {
*/
@Override
public @NotNull CommandValueFlag<T> build() {
return new CommandValueFlag<>(key, aliases, type, optional, defaultValue, context, completion);
return new CommandValueFlag<>(key, aliases, type, optional, defaultValueSupplier, context, completion);
}
}
}
@@ -0,0 +1,65 @@
package org.mvplugins.multiverse.core.command.flags;
import co.aikar.commands.InvalidCommandArgument;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.CommandValueFlag;
import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
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.exceptions.command.MVInvalidCommandArgument;
import org.mvplugins.multiverse.core.world.WorldManager;
@ApiStatus.AvailableSince("5.7")
@Service
public class RemovePlayerDestinationFlags extends FlagBuilder {
public static final String NAME = "removeplayer";
private WorldManager worldManager;
private DestinationsProvider destinationsProvider;
private WorldDestination worldDestination;
protected RemovePlayerDestinationFlags(
@NotNull String name,
@NotNull CommandFlagsManager flagsManager,
@NotNull WorldManager worldManager,
@NotNull DestinationsProvider destinationsProvider,
@NotNull WorldDestination worldDestination
) {
super(name, flagsManager);
this.worldManager = worldManager;
this.destinationsProvider = destinationsProvider;
this.worldDestination = worldDestination;
}
@Inject
private RemovePlayerDestinationFlags(
@NotNull CommandFlagsManager flagsManager,
@NotNull WorldManager worldManager,
@NotNull DestinationsProvider destinationsProvider,
@NotNull WorldDestination worldDestination
) {
super(NAME, flagsManager);
this.destinationsProvider = destinationsProvider;
this.worldManager = worldManager;
this.worldDestination = worldDestination;
}
public final CommandValueFlag<DestinationInstance> removePlayers = flag(CommandValueFlag.builder("--remove-players", DestinationInstance.class)
.addAlias("-r")
.defaultValue(() -> worldManager.getDefaultWorld()
.map(defaultWorld -> worldDestination.fromWorld(defaultWorld))
.getOrElseThrow(() -> new InvalidCommandArgument("No default world found, so the --remove-players flag requires a destination argument."))) //TODO: locale
.completion(input -> destinationsProvider.suggestDestinationStrings(Bukkit.getConsoleSender(), input))
.context(input -> destinationsProvider.parseDestination(input)
.getOrThrow(failure ->
MVInvalidCommandArgument.of(failure.getFailureMessage())))
.optional()
.build());
}
@@ -1,12 +1,19 @@
package org.mvplugins.multiverse.core.command.flags;
import jakarta.inject.Inject;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
/**
* @deprecated The --remove-players flag is being removed in favor of a more flexible system that allows for specifying
* a destination for players to be teleported to when a world is unloaded. See {@link RemovePlayerDestinationFlags}.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Service
public class RemovePlayerFlags extends FlagBuilder {
@@ -20,7 +20,7 @@ import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.options.CloneWorldOptions;
@@ -48,7 +48,7 @@ class CloneCommand extends CoreCommand {
@Syntax("<world>")
@Description("{@@mv-core.clone.world.description}")
LoadedMultiverseWorld world,
MultiverseWorld world,
@Syntax("<new-world-name>")
@Description("{@@mv-core.clone.newWorld.description}")
@@ -114,7 +114,7 @@ class CloneCommand extends CoreCommand {
@Override
@CommandAlias("mvcl|mvclone")
void onCloneCommand(MVCommandIssuer issuer, LoadedMultiverseWorld world, String newWorldName, String[] flags) {
void onCloneCommand(MVCommandIssuer issuer, MultiverseWorld world, String newWorldName, String[] flags) {
super.onCloneCommand(issuer, world, newWorldName, flags);
}
}
@@ -67,12 +67,10 @@ class ConfigCommand extends CoreCommand {
private void updateConfigValue(MVCommandIssuer issuer, String name, String value) {
var stringPropertyHandle = config.getStringPropertyHandle();
stringPropertyHandle.setPropertyString(issuer.getIssuer(), name, value)
.onSuccess(ignore -> {
config.save();
issuer.sendMessage(MVCorei18n.CONFIG_SET_SUCCESS,
Replace.NAME.with(name),
Replace.VALUE.with(stringPropertyHandle.getProperty(name).getOrNull()));
})
.andThenTry(config::save)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.CONFIG_SET_SUCCESS,
Replace.NAME.with(name),
Replace.VALUE.with(stringPropertyHandle.getProperty(name).getOrNull())))
.onFailure(e -> issuer.sendMessage(MVCorei18n.CONFIG_SET_ERROR,
Replace.NAME.with(name),
Replace.VALUE.with(value),
@@ -11,6 +11,7 @@ import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.World;
import org.bukkit.WorldType;
@@ -19,14 +20,16 @@ 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.CommandValueFlag;
import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.exceptions.command.MVInvalidCommandArgument;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.utils.result.Attempt.Failure;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -52,8 +55,9 @@ class CreateCommand extends CoreCommand {
@Subcommand("create")
@CommandPermission("multiverse.core.create")
@CommandCompletion("@empty @environments @flags:groupName=" + Flags.NAME)
@Syntax("<name> <environment> [--seed <seed> --generator <generator[:id]> --world-type <worldtype> --adjust-spawn "
+ "--no-structures --biome <biome>]")
@Syntax("<name> <environment> [--seed <seed> --generator <generator[:id]> --world-type <worldtype> " +
"--adjust-spawn --no-structures --generate-bonus-chest --force-spawn-position <x,y,z:pitch:yaw> " +
"--biome <biome> --generator-settings <json-settings> --properties <prop1=value1,prop2=value2,...>]")
@Description("{@@mv-core.create.description}")
void onCreateCommand(
MVCommandIssuer issuer,
@@ -67,8 +71,9 @@ class CreateCommand extends CoreCommand {
World.Environment environment,
@Optional
@Syntax("[--seed <seed> --generator <generator[:id]> --world-type <worldtype> --adjust-spawn "
+ "--no-structures --biome <biome>]")
@Syntax("[--seed <seed> --generator <generator[:id]> --world-type <worldtype> --adjust-spawn " +
"--no-structures --generate-bonus-chest --force-spawn-position <x,y,z:pitch:yaw> --biome <biome> " +
"--generator-settings <json-settings> --properties <prop1=value1,prop2=value2,...>]")
@Description("{@@mv-core.create.flags.description}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -78,14 +83,17 @@ class CreateCommand extends CoreCommand {
issuer.sendInfo(MVCorei18n.CREATE_LOADING);
worldManager.createWorld(CreateWorldOptions.worldName(worldName)
.biome(parsedFlags.flagValue(flags.biome, ""))
.environment(environment)
.seed(parsedFlags.flagValue(flags.seed))
.worldType(parsedFlags.flagValue(flags.worldType, WorldType.NORMAL))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn))
.generator(parsedFlags.flagValue(flags.generator, ""))
.generatorSettings(parsedFlags.flagValue(flags.generatorSettings, ""))
.generateStructures(!parsedFlags.hasFlag(flags.noStructures)))
.biome(parsedFlags.flagValue(flags.biome, ""))
.bonusChest(parsedFlags.hasFlag(flags.bonusChest))
.environment(environment)
.forcedSpawnPosition(parsedFlags.flagValue(flags.forceSpawnPosition))
.generator(parsedFlags.flagValue(flags.generator, ""))
.generatorSettings(parsedFlags.flagValue(flags.generatorSettings, ""))
.generateStructures(!parsedFlags.hasFlag(flags.noStructures))
.seed(parsedFlags.flagValue(flags.seed))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn))
.worldPropertyStrings(StringFormatter.parseCSVMap(parsedFlags.flagValue(flags.properties)))
.worldType(parsedFlags.flagValue(flags.worldType, WorldType.NORMAL)))
.onSuccess(newWorld -> messageSuccess(issuer, newWorld))
.onFailure(failure -> messageFailure(issuer, failure));
}
@@ -180,6 +188,19 @@ class CreateCommand extends CoreCommand {
.addAlias("-b")
.completion(input -> biomeProviderFactory.suggestBiomeString(input))
.build());
private final CommandValueFlag<String> properties = flag(CommandValueFlag.builder("--properties", String.class)
.addAlias("-p")
.build());
private final CommandFlag bonusChest = flag(CommandFlag.builder("--generate-bonus-chest")
.addAlias("-c")
.build());
private final CommandValueFlag<EntityPosition> forceSpawnPosition = flag(CommandValueFlag.builder("--force-spawn-position", EntityPosition.class)
.addAlias("-f")
.context(input -> Try.of(() -> EntityPosition.fromString(input)).getOrElseThrow(MVInvalidCommandArgument::causeBy))
.build());
}
@Service
@@ -15,11 +15,11 @@ 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.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerDestinationFlags;
import org.mvplugins.multiverse.core.command.queue.CommandQueueManager;
import org.mvplugins.multiverse.core.command.queue.CommandQueuePayload;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
@@ -31,6 +31,8 @@ import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions;
import java.util.Objects;
@Service
class DeleteCommand extends CoreCommand {
@@ -38,7 +40,7 @@ class DeleteCommand extends CoreCommand {
private final WorldManager worldManager;
private final PlayerWorldTeleporter playerWorldTeleporter;
private final WorldTickDeferrer worldTickDeferrer;
private final RemovePlayerFlags flags;
private final RemovePlayerDestinationFlags flags;
@Inject
DeleteCommand(
@@ -46,7 +48,7 @@ class DeleteCommand extends CoreCommand {
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter,
@NotNull WorldTickDeferrer worldTickDeferrer,
@NotNull RemovePlayerFlags flags
@NotNull RemovePlayerDestinationFlags flags
) {
this.commandQueueManager = commandQueueManager;
this.worldManager = worldManager;
@@ -57,8 +59,8 @@ class DeleteCommand extends CoreCommand {
@Subcommand("delete")
@CommandPermission("multiverse.core.delete")
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + RemovePlayerFlags.NAME)
@Syntax("<world>")
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + RemovePlayerDestinationFlags.NAME)
@Syntax("<world> [--remove-players [destination]]")
@Description("{@@mv-core.delete.description}")
void onDeleteCommand(
MVCommandIssuer issuer,
@@ -69,7 +71,7 @@ class DeleteCommand extends CoreCommand {
MultiverseWorld world,
@Optional
@Syntax("[--remove-players]")
@Syntax("[--remove-players [destination]]")
@Description("")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -84,10 +86,11 @@ class DeleteCommand extends CoreCommand {
private void runDeleteCommand(MVCommandIssuer issuer, MultiverseWorld world, ParsedCommandFlags parsedFlags) {
issuer.sendInfo(MVCorei18n.DELETE_DELETING, Replace.WORLD.with(world.getName()));
var future = parsedFlags.hasFlag(flags.removePlayers)
DestinationInstance<?, ?> removeToDestination = parsedFlags.flagValue(flags.removePlayers);
var future = Objects.nonNull(removeToDestination)
&& world.isLoaded()
&& world instanceof LoadedMultiverseWorld loadedWorld
? playerWorldTeleporter.removeFromWorld(loadedWorld)
? playerWorldTeleporter.transferAllFromWorldToDestination(loadedWorld, removeToDestination)
: AsyncAttemptsAggregate.emptySuccess();
future.onSuccess(() -> worldTickDeferrer.deferWorldTick(() -> doWorldDeleting(issuer, world)))
@@ -113,7 +116,7 @@ class DeleteCommand extends CoreCommand {
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter,
@NotNull WorldTickDeferrer worldTickDeferrer,
@NotNull RemovePlayerFlags flags) {
@NotNull RemovePlayerDestinationFlags flags) {
super(commandQueueManager, worldManager, playerWorldTeleporter, worldTickDeferrer, flags);
}
@@ -12,13 +12,10 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
import org.mvplugins.multiverse.core.commands.DumpsLogPoster.LogsType;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.commands.DumpsLogPoster.UploadType;
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.CommandValueFlag;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
@Service
final class DumpsCommand extends CoreCommand {
@@ -35,22 +32,17 @@ final class DumpsCommand extends CoreCommand {
@Subcommand("dumps")
@CommandPermission("multiverse.core.dumps")
@CommandCompletion("@flags:groupName=" + Flags.NAME)
@Syntax("[--logs <mclogs | append>] [--upload <pastesdev | pastegg>] [--paranoid]")
@Syntax("[--upload <pastesdev|mclogs>]")
@Description("{@@mv-core.dumps.description}")
void onDumpsCommand(
MVCommandIssuer issuer,
@Optional
@Syntax("[--logs <mclogs | append>] [--upload <pastesdev | pastegg>] [--paranoid]")
@Syntax("[--upload <pastesdev|mclogs>]")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
// Grab all our flags
boolean paranoid = parsedFlags.hasFlag(flags.paranoid);
LogsType logsType = parsedFlags.flagValue(flags.logs, LogsType.MCLOGS);
UploadType servicesType = parsedFlags.flagValue(flags.upload, UploadType.PASTESDEV);
dumpsService.postLogs(issuer, logsType, servicesType, paranoid);
dumpsService.postLogs(issuer, servicesType);
}
@Service
@@ -63,19 +55,9 @@ final class DumpsCommand extends CoreCommand {
super(NAME, flagsManager);
}
private final CommandValueFlag<LogsType> logs = flag(CommandValueFlag
.enumBuilder("--logs", LogsType.class)
.addAlias("-l")
.build());
private final CommandValueFlag<UploadType> upload = flag(CommandValueFlag
.enumBuilder("--upload", UploadType.class)
.addAlias("-u")
.build());
// Does not upload logs or plugin list (except if --logs mclogs is there)
private final CommandFlag paranoid = flag(CommandFlag.builder("--paranoid")
.addAlias("-p")
.build());
}
}
@@ -1,11 +1,19 @@
package org.mvplugins.multiverse.core.commands;
import java.lang.management.ManagementFactory;
import java.time.Instant;
import java.util.Date;
import java.util.Map;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import net.minidev.json.JSONArray;
import net.minidev.json.JSONObject;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.event.MVDumpsDebugInfoEvent;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
@@ -18,8 +26,10 @@ final class DumpsLogPoster extends BukkitRunnable {
enum UploadType {
// BEGIN CHECKSTYLE-SUPPRESSION: JavadocVariable
PASTEGG(PasteServiceType.PASTEGG, "paste.gg"),
PASTESDEV(PasteServiceType.PASTESDEV, "pastes.dev");
// PASTEBIN(PasteServiceType.PASTEBIN, "pastebin.com"),
PASTESDEV(PasteServiceType.PASTESDEV, "pastes.dev"),
MCLOGS(PasteServiceType.MCLOGS, "mclo.gs"),
;
// END CHECKSTYLE-SUPPRESSION: JavadocVariable
private final PasteServiceType pasteServiceType;
@@ -31,34 +41,21 @@ final class DumpsLogPoster extends BukkitRunnable {
}
}
enum LogsType {
/**
* Appends the log file to the debug info.
*/
APPEND,
/**
* Uploads the log file to a paste service.
*/
MCLOGS
}
@NotNull
private final MultiverseCore plugin;
private final MVCommandIssuer issuer;
private final LogsType logsType;
private final UploadType uploadType;
private final boolean paranoid;
private final String logs;
private final MVDumpsDebugInfoEvent versionEvent;
DumpsLogPoster(@NotNull MVCommandIssuer issuer,
@NotNull DumpsLogPoster.LogsType logsType,
DumpsLogPoster(@NotNull MultiverseCore core,
@NotNull MVCommandIssuer issuer,
@NotNull DumpsLogPoster.UploadType uploadType,
boolean paranoid,
@NotNull String logs,
@NotNull MVDumpsDebugInfoEvent versionEvent) {
this.plugin = core;
this.issuer = issuer;
this.logsType = logsType;
this.uploadType = uploadType;
this.paranoid = paranoid;
this.logs = logs;
this.versionEvent = versionEvent;
}
@@ -69,24 +66,12 @@ final class DumpsLogPoster extends BukkitRunnable {
handleVersionEvent();
}
@SuppressWarnings("checkstyle:MissingSwitchDefault")
private void handleLogs() {
Logging.finer("Logs type is: " + logsType);
if (!paranoid) {
switch (logsType) {
case MCLOGS -> sendDumpsUrl("Logs", postRawDataToMcLogs(logs));
case APPEND -> versionEvent.putDetailedDebugInfo("latest.log", logs);
default -> Logging.finer("Not uploading logs.");
}
} else {
Logging.finer("Paranoid mode is on, not uploading logs.");
}
versionEvent.putDetailedDebugInfo("latest.log", logs);
}
private void handleVersionEvent() {
Logging.finer("Upload service is: " + uploadType);
final Map<String, String> files = versionEvent.getDetailedDebugInfo();
sendDumpsUrl(uploadType.serviceName, postFilesToService(files));
}
@@ -95,37 +80,77 @@ final class DumpsLogPoster extends BukkitRunnable {
issuer.sendInfo(MVCorei18n.DUMPS_URL_LIST, "{service}", service, "{link}", url);
}
private String postRawDataToMcLogs(@NotNull String rawPasteData) {
PasteService pasteService = PasteServiceFactory.getService(PasteServiceType.MCLOGS, true);
return pasteToService(() -> pasteService.postData(rawPasteData));
}
private String postFilesToService(@NotNull Map<String, String> pasteFiles) {
PasteService pasteService = PasteServiceFactory.getService(uploadType.pasteServiceType, true);
return pasteToService(() -> {
if (pasteService.supportsMultiFile()) {
return pasteService.postData(pasteFiles);
} else {
return pasteService.postData(this.encodeAsString(pasteFiles));
}
});
PasteService pasteService = PasteServiceFactory.getService(uploadType.pasteServiceType, false);
return pasteToService(() -> pasteService.postData(encodePasteAsJson(pasteFiles)));
}
private String encodeAsString(Map<String, String> files) {
StringBuilder uploadData = new StringBuilder();
for (Map.Entry<String, String> entry : files.entrySet()) {
String file = entry.getKey();
String data = entry.getValue();
uploadData.append("# ---------- ")
.append(file)
.append(" ----------\n\n")
.append(data)
.append("\n\n");
private String encodePasteAsJson(Map<String, String> files) {
JSONObject rootObject = new JSONObject();
JSONArray plugins = new JSONArray();
JSONArray filesObject = new JSONArray();
for (Plugin plugin : plugin.getServer().getPluginManager().getPlugins()) {
plugins.add(new JSONObject(Map.of(
"name", plugin.getName(),
"version", plugin.getDescription().getVersion(),
"description", Option.of(plugin.getDescription().getDescription()).getOrElse(""),
"authors", plugin.getDescription().getAuthors(),
"website", Option.of(plugin.getDescription().getWebsite()).getOrElse(""),
"enabled", plugin.isEnabled()
)));
}
return uploadData.toString();
for (Map.Entry<String, String> entry : files.entrySet()) {
String fileType = "text";
if (entry.getKey().endsWith(".yml") || entry.getKey().endsWith(".yaml")) {
fileType = "yaml";
} else if (entry.getKey().endsWith(".json")) {
fileType = "json";
} else if (entry.getKey().endsWith(".md")) {
fileType = "markdown";
} else if (entry.getKey().endsWith(".log")) {
fileType = "accesslog";
}
filesObject.add(new JSONObject(Map.of(
"name", entry.getKey(),
"data", entry.getValue(),
"type", fileType
)));
}
rootObject.put("server", new JSONObject(Map.of(
"name", plugin.getServer().getName(),
"version", plugin.getServer().getVersion(),
"bukkitVersion", plugin.getServer().getBukkitVersion(),
"onlineMode", plugin.getServer().getOnlineMode(),
"javaVersion", Runtime.version().toString(),
"operatingSystem", System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch"),
"uptime", calculateUptimeString(),
"memory", "%d free / %d total / %d max".formatted(
toMb(Runtime.getRuntime().freeMemory()),
toMb(Runtime.getRuntime().totalMemory()),
toMb(Runtime.getRuntime().maxMemory()))
)));
rootObject.put("plugins", plugins);
rootObject.put("files", filesObject);
rootObject.put("createdAt", Instant.now().toString());
return rootObject.toJSONString();
}
private long toMb(long value) {
return value / 1024 / 1024;
}
private String calculateUptimeString() {
long uptimeMs = System.currentTimeMillis() - ManagementFactory.getRuntimeMXBean().getStartTime();
long totalSeconds = uptimeMs / 1000;
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;
return String.format("%d hrs, %d mins, %d sec", hours, minutes, seconds);
}
private String pasteToService(LogPaster paster) {
@@ -8,13 +8,12 @@ import java.nio.file.Path;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.module.MultiverseModulesRegistry;
import org.mvplugins.multiverse.core.commands.DumpsLogPoster.LogsType;
import org.mvplugins.multiverse.core.commands.DumpsLogPoster.UploadType;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.event.MVDumpsDebugInfoEvent;
@@ -38,16 +37,12 @@ final class DumpsService {
this.fileUtils = fileUtils;
}
void postLogs(MVCommandIssuer issuer, LogsType logsType, UploadType servicesType, boolean paranoid) {
void postLogs(MVCommandIssuer issuer, UploadType servicesType) {
issuer.sendInfo(MVCorei18n.DUMPS_STARTING);
// Initialise and add info to the debug event
MVDumpsDebugInfoEvent versionEvent = createAndCallDebugInfoEvent();
// Add plugin list if user isn't paranoid
if (!paranoid) {
versionEvent.putDetailedDebugInfo("plugins.md", "# Plugins\n\n" + getPluginList());
}
new DumpsLogPoster(issuer, logsType, servicesType, paranoid, getLogs(), versionEvent)
new DumpsLogPoster(plugin, issuer, servicesType, getLogs(), versionEvent)
.runTaskAsynchronously(plugin);
}
@@ -72,18 +67,18 @@ final class DumpsService {
private @NotNull String readLogsFromFile(Path logsPath) {
String logs = "Could not read log";
// Try reading as ANSI encoded
// Try reading as UTF-8 first
try {
logs = Files.readString(logsPath, StandardCharsets.UTF_8);
} catch (IOException e) {
Logging.severe("Could not read logs/latest.log. See below for stack trace");
Logging.severe("Could not read logs/latest.log as UTF-8. Trying ISO-8859-1. See below for stack trace");
e.printStackTrace();
try {
logs = Files.readString(logsPath, StandardCharsets.UTF_8);
logs = Files.readString(logsPath, StandardCharsets.ISO_8859_1);
} catch (IOException ex) {
// It is some other strange encoding
Logging.severe("Could not read ./logs/latest.log. See below for stack trace");
Logging.severe("Could not read ./logs/latest.log as ISO-8859-1. See below for stack trace");
ex.printStackTrace();
}
}
@@ -91,15 +86,6 @@ final class DumpsService {
return logs;
}
private String getDebugInfoString() {
return "# Multiverse-Core Version info" + "\n\n"
+ " - Multiverse-Core Version: " + this.plugin.getDescription().getVersion() + '\n'
+ " - Bukkit Version: " + this.plugin.getServer().getVersion() + '\n'
+ " - Loaded Worlds: " + worldManager.getLoadedWorlds() + '\n'
+ " - Multiverse Plugins Loaded: " + StringFormatter.joinAnd(MultiverseModulesRegistry.get().getRegisteredPlugins()) + '\n'
+ " - Multiverse Plugins Count: " + MultiverseModulesRegistry.get().getPluginCount() + '\n';
}
private MVDumpsDebugInfoEvent createAndCallDebugInfoEvent() {
MVDumpsDebugInfoEvent event = new MVDumpsDebugInfoEvent();
addDebugInfoToEvent(event);
@@ -111,31 +97,63 @@ final class DumpsService {
// Add the legacy file, but as markdown, so it's readable
event.putDetailedDebugInfo("version.md", this.getDebugInfoString());
// add config.yml
File configFile = new File(plugin.getDataFolder(), "config.yml");
event.putDetailedDebugInfo("Multiverse-Core/config.yml", configFile);
// add worlds.yml
File worldsFile = new File(plugin.getDataFolder(), "worlds.yml");
event.putDetailedDebugInfo("Multiverse-Core/worlds.yml", worldsFile);
// Add bukkit.yml if we found it
if (fileUtils.getBukkitConfig() != null) {
event.putDetailedDebugInfo(fileUtils.getBukkitConfig().getPath(), fileUtils.getBukkitConfig());
} else {
Logging.warning("/mv dumps could not find bukkit.yml. Not including file");
}
// Add server.properties if we found it
if (fileUtils.getServerProperties() != null) {
event.putDetailedDebugInfo(fileUtils.getServerProperties().getPath(), fileUtils.getServerProperties());
event.putDetailedDebugInfo(fileUtils.getServerProperties().getName(), fileUtils.getServerProperties());
} else {
Logging.warning("/mv dumps could not find server.properties. Not including file");
}
// Add bukkit.yml if we found it
if (fileUtils.getBukkitConfig() != null) {
event.putDetailedDebugInfo(fileUtils.getBukkitConfig().getName(), fileUtils.getBukkitConfig());
} else {
Logging.warning("/mv dumps could not find bukkit.yml. Not including file");
}
// Add spigot.yml
File spigotYml = fileUtils.getServerFolder().toPath().resolve("spigot.yml").toFile();
if (spigotYml.isFile()) {
event.putDetailedDebugInfo(spigotYml.getName(), spigotYml);
} else {
Logging.warning("/mv dumps could not find spigot.yml. Not including file");
}
// Add paper-global.yml
File paperGlobalYml = fileUtils.getServerFolder().toPath().resolve("config/paper-global.yml").toFile();
if (paperGlobalYml.isFile()) {
event.putDetailedDebugInfo(paperGlobalYml.getName(), paperGlobalYml);
} else {
Logging.warning("/mv dumps could not find paper-global.yml. Not including file");
}
// add paper-world-defaults.yml
File paperWorldDefaultsYml = fileUtils.getServerFolder().toPath().resolve("config/paper-world-defaults.yml").toFile();
if (paperWorldDefaultsYml.isFile()) {
event.putDetailedDebugInfo(paperWorldDefaultsYml.getName(), paperWorldDefaultsYml);
} else {
Logging.warning("/mv dumps could not find paper-global.yml. Not including file");
}
// add multiverse-core config.yml
File configFile = new File(plugin.getDataFolder(), "config.yml");
event.putDetailedDebugInfo("multiverse-core/config.yml", configFile);
// add multiverse-core worlds.yml
File worldsFile = new File(plugin.getDataFolder(), "worlds.yml");
event.putDetailedDebugInfo("multiverse-core/worlds.yml", worldsFile);
// add multiverse-core anchors.yml
File anchorsFile = new File(plugin.getDataFolder(), "anchors.yml");
event.putDetailedDebugInfo("multiverse-core/anchors.yml", anchorsFile);
}
private String getPluginList() {
return " - " + StringUtils.join(plugin.getServer().getPluginManager().getPlugins(), "\n - ");
private String getDebugInfoString() {
return "# Multiverse-Core Version info" + "\n\n"
+ " - Multiverse-Core Version: " + this.plugin.getDescription().getVersion() + '\n'
+ " - Bukkit Version: " + this.plugin.getServer().getVersion() + '\n'
+ " - Loaded Worlds: " + worldManager.getLoadedWorlds() + '\n'
+ " - Multiverse Plugins Loaded: " + StringFormatter.joinAnd(MultiverseModulesRegistry.get().getRegisteredPlugins()) + '\n'
+ " - Multiverse Plugins Count: " + MultiverseModulesRegistry.get().getPluginCount() + '\n';
}
}
@@ -52,7 +52,7 @@ final class EntitySpawnConfigCommand extends CoreCommand {
void onInfoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
MultiverseWorld world,
@@ -91,7 +91,7 @@ final class EntitySpawnConfigCommand extends CoreCommand {
void onModifyCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=4")
@Syntax("[world]")
MultiverseWorld world,
@@ -113,11 +113,10 @@ final class EntitySpawnConfigCommand extends CoreCommand {
.getSpawnCategoryConfig(spawnCategory)
.getStringPropertyHandle()
.modifyPropertyString(property, value, action)
.onSuccess(ignore -> issuer.sendMessage("Successfully set " + property + " to " + value
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendInfo("Successfully set " + property + " to " + value
+ " for " + spawnCategory.name() + " in " + world.getName()))
.onFailure(e -> issuer.sendMessage("Unable to set " + property + " to " + value
.onFailure(e -> issuer.sendError("Unable to set " + property + " to " + value
+ " for " + spawnCategory.name() + " in " + world.getName() + ": " + e.getMessage()));
worldManager.saveWorldsConfig();
}
}
@@ -67,7 +67,7 @@ class GameruleCommand extends CoreCommand {
@Description("{@@mv-core.gamerule.set.value.description}")
GameRuleValue gameRuleValue,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=2")
@Syntax("[World or *]")
@Description("{@@mv-core.gamerule.set.world.description}")
LoadedMultiverseWorld[] worlds) {
@@ -114,7 +114,7 @@ class GameruleCommand extends CoreCommand {
@Description("{@@mv-core.gamerule.reset.gamerule.description}")
GameRule gamerule,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[World or *]")
@Description("{@@mv-core.gamerule.reset.world.description}")
LoadedMultiverseWorld[] worlds) {
@@ -152,7 +152,7 @@ class GameruleCommand extends CoreCommand {
void onGameruleListCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("<world>")
@Description("{@@mv-core.gamerule.list.description.world}")
LoadedMultiverseWorld world,
@@ -48,7 +48,7 @@ class ImportCommand extends CoreCommand {
void onImportCommand(
MVCommandIssuer issuer,
@Conditions("worldname:scope=new")
// @Conditions("worldname:scope=new")
@Syntax("<name>")
@Description("{@@mv-core.import.name.description}")
String worldName,
@@ -67,7 +67,8 @@ class ImportCommand extends CoreCommand {
worldManager.importWorld(ImportWorldOptions.worldName(worldName)
.biome(parsedFlags.flagValue(flags.biome, ""))
.environment(environment)
.generator(parsedFlags.flagValue(flags.generator, String.class))
.generator(parsedFlags.flagValue(flags.generator))
.generatorSettings(parsedFlags.flagValue(flags.generatorSettings, ""))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn))
.doFolderCheck(!parsedFlags.hasFlag(flags.skipFolderCheck)))
.onSuccess(newWorld -> {
@@ -110,6 +111,11 @@ class ImportCommand extends CoreCommand {
private final CommandFlag skipFolderCheck = flag(CommandFlag.builder("--skip-folder-check")
.addAlias("-f")
.build());
private final CommandValueFlag<String> generatorSettings = flag(CommandValueFlag
.builder("--generator-settings", String.class)
.addAlias("-gs")
.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,
@@ -87,6 +87,7 @@ class InfoCommand extends CoreCommand {
private Map<String, String> getInfo(LoadedMultiverseWorld world) {
Map<String, String> outMap = new LinkedHashMap<>();
outMap.put("World Key", world.getKey().toString());
outMap.put("World Name", world.getName());
outMap.put("World Alias", world.getAlias());
outMap.put("World UID", world.getUID().toString());
@@ -103,6 +104,7 @@ class InfoCommand extends CoreCommand {
outMap.put("World Type", world.getWorldType().map(WorldType::getName).getOrNull());
outMap.put("Biome", world.getBiome());
outMap.put("Generator", world.getGenerator());
outMap.put("Generator Settings", world.getGeneratorSettings());
outMap.put("Generate Structures", world.canGenerateStructures().map(String::valueOf).getOrNull());
outMap.put("Auto Load", String.valueOf(world.isAutoLoad()));
@@ -112,13 +114,12 @@ class InfoCommand extends CoreCommand {
outMap.put("World Scale", String.valueOf(world.getScale()));
outMap.put("Weather Enabled", String.valueOf(world.isAllowWeather()));
outMap.put("Allow Flight", String.valueOf(world.isAllowFlight()));
outMap.put("Grant Advancements", String.valueOf(world.isAllowAdvancementGrant()));
outMap.put("Auto Heal", String.valueOf(world.getAutoHeal()));
outMap.put("Hunger Depletes", String.valueOf(world.isHunger()));
outMap.put("Keep Spawn In Memory", String.valueOf(world.isKeepSpawnInMemory()));
outMap.put("PVP Enabled", String.valueOf(world.getPvp()));
outMap.put("Portal Form", String.valueOf(world.getPortalForm()));
outMap.put("Player Limit", String.valueOf(world.getPlayerLimit()));
// getAnimalSpawningInfo(outMap, world);
// getMonsterSpawningInfo(outMap, world);
outMap.put("World Blacklist", String.join(", ", world.getWorldBlacklist()));
return outMap;
@@ -135,30 +136,6 @@ class InfoCommand extends CoreCommand {
}
}
// private void getAnimalSpawningInfo(Map<String, String> outMap, MultiverseWorld world) {
// if (world.isSpawningAnimals()) {
// outMap.put("Spawning Animals", "ALL");
// } else {
// if (!world.getSpawningAnimalsExceptions().isEmpty()) {
// outMap.put("Spawning Animals", world.getSpawningAnimalsExceptions().toString());
// } else {
// outMap.put("Spawning Animals", "NONE");
// }
// }
// }
//
// private void getMonsterSpawningInfo(Map<String, String> outMap, MultiverseWorld world) {
// if (world.isSpawningMonsters()) {
// outMap.put("Spawning Monsters", "ALL");
// } else {
// if (!world.getSpawningMonstersExceptions().isEmpty()) {
// outMap.put("Spawning Monsters", world.getSpawningMonstersExceptions().toString());
// } else {
// outMap.put("Spawning Monsters", "NONE");
// }
// }
// }
@Service
private static final class LegacyAlias extends InfoCommand implements LegacyAliasCommand {
@Inject
@@ -16,8 +16,8 @@ import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
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;
@@ -38,7 +38,7 @@ class LoadCommand extends CoreCommand {
@Subcommand("load")
@CommandPermission("multiverse.core.load")
@CommandCompletion("@mvworlds:scope=unloaded")
@CommandCompletion("@mvworlds:scope=unloaded @flags:groupName=" + Flags.NAME)
@Syntax("<world>")
@Description("{@@mv-core.load.description}")
void onLoadCommand(
@@ -49,7 +49,7 @@ class LoadCommand extends CoreCommand {
MultiverseWorld world,
@Optional
@Syntax("[--remove-players]")
@Syntax("[--skip-folder-check]")
@Description("")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -67,9 +67,9 @@ class LoadCommand extends CoreCommand {
}
@Service
private static final class Flags extends RemovePlayerFlags {
private static final class Flags extends FlagBuilder {
private static final String NAME = "mvunload";
private static final String NAME = "mvload";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
@@ -0,0 +1,121 @@
package org.mvplugins.multiverse.core.commands;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Flags;
import co.aikar.commands.annotation.Optional;
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.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.PageFilterFlags;
import org.mvplugins.multiverse.core.display.ContentDisplay;
import org.mvplugins.multiverse.core.display.filters.DefaultContentFilter;
import org.mvplugins.multiverse.core.display.handlers.PagedSendHandler;
import org.mvplugins.multiverse.core.display.parsers.MapContentProvider;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.util.Locale;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
@Subcommand("meta")
final class MetaCommand extends CoreCommand {
private final WorldManager worldManager;
private final PageFilterFlags flags;
@Inject
MetaCommand(@NotNull WorldManager worldManager, @NotNull PageFilterFlags flags) {
this.worldManager = worldManager;
this.flags = flags;
}
@Subcommand("info")
@CommandPermission("multiverse.core.meta.info")
@CommandCompletion("@mvworlds:scope=both|@flags:resolveUntil=arg1,groupName=" + PageFilterFlags.NAME + " " +
"@flags:groupName=" + PageFilterFlags.NAME)
@Syntax("[world] [--page <page>] [--filter <filter>]")
@Description("{@@mv-core.meta.info.description}")
void infoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
@Description("{@@mv-core.meta.info.world.description}")
MultiverseWorld world,
@Optional
@Syntax("[--page <page>] [--filter <filter>]")
String[] flagArray
) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
ContentDisplay.create()
.addContent(MapContentProvider.forContent(world.getAllMeta())
.withKeyColor(ChatColor.AQUA)
.withValueColor(ChatColor.WHITE))
.withSendHandler(PagedSendHandler.create()
.withHeader(Message.of(MVCorei18n.META_INFO_HEADER, replace("{world}").with(world.getName())))
.noContentMessage(Message.of(MVCorei18n.META_INFO_NOCONTENT, replace("{world}").with(world.getName())))
.withTargetPage(parsedFlags.flagValue(flags.page, 1))
.withFilter(parsedFlags.flagValue(flags.filter, DefaultContentFilter.get())))
.send(issuer);
}
@Subcommand("modify")
@CommandPermission("multiverse.core.meta.modify")
@CommandCompletion("@mvworlds:scope=both set|remove @mvworldmetakey @empty")
@Syntax("[world] <set|remove> <key> [value]")
void modifyCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware,maxArgForAware=4")
@Syntax("[world]")
MultiverseWorld world,
@Syntax("<set|remove>")
String action,
@Syntax("<key>")
String key,
@Optional
@Single
@Syntax("[value]")
String value
) {
switch (action.toLowerCase(Locale.ROOT)) {
case "set" -> world.setMeta(key, value)
.onSuccess(ignore -> issuer.sendInfo(MVCorei18n.META_MODIFY_SET_SUCCESS,
replace("{key}").with(key),
replace("{value}").with(value),
Replace.WORLD.with(world.getName())))
.onFailure(throwable -> issuer.sendError(MVCorei18n.META_MODIFY_SET_FAILURE,
replace("{key}").with(key),
replace("{value}").with(value),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(throwable)));
case "remove" -> world.removeMeta(key)
.onSuccess(ignore -> issuer.sendInfo(MVCorei18n.META_MODIFY_REMOVE_SUCCESS,
replace("{key}").with(key),
Replace.WORLD.with(world.getName())))
.onFailure(throwable -> issuer.sendError(MVCorei18n.META_MODIFY_REMOVE_FAILURE,
replace("{key}").with(key),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(throwable)));
default -> issuer.sendError(MVCorei18n.META_MODIFY_INVALIDACTION, replace("{action}").with(action));
}
worldManager.saveWorldsConfig();
}
}
@@ -49,12 +49,12 @@ class ModifyCommand extends CoreCommand {
"@propsmodifyaction:notByIssuerForArg=arg1|@mvworldpropsname:byIssuerForArg=arg1 " +
"@mvworldpropsname:notByIssuerForArg=arg1|@mvworldpropsvalue:byIssuerForArg=arg1 " +
"@mvworldpropsvalue:notByIssuerForArg=arg1")
@Syntax("[world] <set|add|remove|reset> <property> <value>")
@Syntax("[world] <set|add|remove|reset> <property> [value]")
@Description("{@@mv-core.modify.description}")
void onModifyCommand(// SUPPRESS CHECKSTYLE: ParameterNumber
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=3")
@Syntax("[world]")
@Description("{@@mv-core.modify.world.description}")
@NotNull MultiverseWorldValue worldValue,
@@ -94,28 +94,28 @@ class ModifyCommand extends CoreCommand {
}
StringPropertyHandle worldPropertyHandle = world.getStringPropertyHandle();
worldPropertyHandle.modifyPropertyString(issuer.getIssuer(), propertyName, propertyValue, action).onSuccess(ignore -> {
issuer.sendMessage(MVCorei18n.MODIFY_SUCCESS,
worldPropertyHandle.modifyPropertyString(issuer.getIssuer(), propertyName, propertyValue, action)
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.MODIFY_SUCCESS,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(worldPropertyHandle.getProperty(propertyName).getOrNull()),
Replace.WORLD.with(world.getName()));
worldManager.saveWorldsConfig();
}).onFailure(exception -> {
if (propertyValue == null) {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE_NOVALUE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
} else {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(propertyValue),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
}
Replace.WORLD.with(world.getName())))
.onFailure(exception -> {
if (propertyValue == null) {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE_NOVALUE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
} else {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(propertyValue),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
}
});
}
@@ -33,7 +33,7 @@ final class PurgeAllEntitiesCommand extends CoreCommand {
void onPurgeAllEntitiesCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[world]")
LoadedMultiverseWorld world,
@@ -28,7 +28,7 @@ final class PurgeEntitiesCommand extends CoreCommand {
void onPurgeEntityCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core.commands;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import co.aikar.commands.ACFUtil;
import co.aikar.commands.annotation.CommandAlias;
@@ -23,9 +24,12 @@ import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.CommandValueFlag;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerDestinationFlags;
import org.mvplugins.multiverse.core.command.queue.CommandQueueManager;
import org.mvplugins.multiverse.core.command.queue.CommandQueuePayload;
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.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
@@ -65,7 +69,8 @@ class RegenCommand extends CoreCommand {
@Subcommand("regen")
@CommandPermission("multiverse.core.regen")
@CommandCompletion("@mvworlds:scope=loaded @flags:groupName=" + Flags.NAME)
@Syntax("<world> [--seed [seed] --reset-world-config --reset-gamerules --reset-world-border --remove-players]")
@Syntax("<world> [--seed [seed]] [--reset-world-config] [--reset-gamerules] [--reset-world-border] " +
"[--remove-players [destination]]")
@Description("{@@mv-core.regen.description}")
void onRegenCommand(
MVCommandIssuer issuer,
@@ -75,7 +80,8 @@ class RegenCommand extends CoreCommand {
LoadedMultiverseWorld world,
@Optional
@Syntax("[--seed [seed] --reset-world-config --reset-gamerules --reset-world-border --remove-players]")
@Syntax("[--seed [seed]] [--reset-world-config] [--reset-gamerules] [--reset-world-border] " +
"[--remove-players [destination]]")
@Description("{@@mv-core.regen.other.description}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -91,8 +97,9 @@ class RegenCommand extends CoreCommand {
issuer.sendInfo(MVCorei18n.REGEN_REGENERATING, Replace.WORLD.with(world.getName()));
List<Player> worldPlayers = world.getPlayers().getOrElse(Collections.emptyList());
var future = parsedFlags.hasFlag(flags.removePlayers)
? playerWorldTeleporter.removeFromWorld(world)
DestinationInstance<?, ?> removeToDestination = parsedFlags.flagValue(flags.removePlayers);
var future = Objects.nonNull(removeToDestination)
? playerWorldTeleporter.transferAllFromWorldToDestination(world, removeToDestination)
: AsyncAttemptsAggregate.emptySuccess();
// todo: using future will hide stacktrace
@@ -128,13 +135,18 @@ class RegenCommand extends CoreCommand {
}
@Service
private static final class Flags extends RemovePlayerFlags {
private static final class Flags extends RemovePlayerDestinationFlags {
private static final String NAME = "mvregen";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
private Flags(
@NotNull CommandFlagsManager flagsManager,
@NotNull WorldManager worldManager,
@NotNull DestinationsProvider destinationsProvider,
@NotNull WorldDestination worldDestination
) {
super(NAME, flagsManager, worldManager, destinationsProvider, worldDestination);
}
private final CommandValueFlag<String> seed = flag(CommandValueFlag.builder("--seed", String.class)
@@ -14,11 +14,13 @@ 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.command.flags.RemovePlayerDestinationFlags;
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.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate;
@@ -27,6 +29,8 @@ import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
import org.mvplugins.multiverse.core.world.options.RemoveWorldOptions;
import java.util.Objects;
@Service
class RemoveCommand extends CoreCommand {
@@ -48,7 +52,7 @@ class RemoveCommand extends CoreCommand {
@Subcommand("remove")
@CommandPermission("multiverse.core.remove")
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + Flags.NAME)
@Syntax("<world>")
@Syntax("<world> [--remove-players [destination]] [--no-unload-bukkit-world] [--no-save]")
@Description("{@@mv-core.remove.description}")
void onRemoveCommand(
MVCommandIssuer issuer,
@@ -58,13 +62,16 @@ class RemoveCommand extends CoreCommand {
MultiverseWorld world,
@Optional
@Syntax("[--remove-players]")
@Syntax("[--remove-players [destination]] [--no-unload-bukkit-world] [--no-save]")
@Description("")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
var future = parsedFlags.hasFlag(flags.removePlayers)
? worldManager.getLoadedWorld(world).map(playerWorldTeleporter::removeFromWorld).getOrElse(AsyncAttemptsAggregate::emptySuccess)
DestinationInstance<?, ?> removeToDestination = parsedFlags.flagValue(flags.removePlayers);
var future = Objects.nonNull(removeToDestination)
? world.asLoadedWorld()
.map(loadedWorld -> playerWorldTeleporter.transferAllFromWorldToDestination(loadedWorld, removeToDestination))
.getOrElse(AsyncAttemptsAggregate::emptySuccess)
: AsyncAttemptsAggregate.emptySuccess();
future.onSuccess(() -> doWorldRemoving(issuer, world, parsedFlags))
@@ -85,13 +92,18 @@ class RemoveCommand extends CoreCommand {
}
@Service
private static final class Flags extends RemovePlayerFlags {
private static final class Flags extends RemovePlayerDestinationFlags {
private static final String NAME = "mvremove";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
private Flags(
@NotNull CommandFlagsManager flagsManager,
@NotNull WorldManager worldManager,
@NotNull DestinationsProvider destinationsProvider,
@NotNull WorldDestination worldDestination
) {
super(NAME, flagsManager, worldManager, destinationsProvider, worldDestination);
}
private final CommandFlag noUnloadBukkitWorld = flag(CommandFlag.builder("--no-unload-bukkit-world")
@@ -63,7 +63,7 @@ class SetSpawnCommand extends CoreCommand {
worldManager.getLoadedWorld(location.getWorld())
.peek(mvWorld -> mvWorld.setSpawnLocation(location)
.flatMap(ignore -> worldManager.saveWorldsConfig())
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.SETSPAWN_SUCCESS,
Replace.WORLD.with(mvWorld.getName()),
Replace.LOCATION.with(prettyLocation(location))))
@@ -119,7 +119,7 @@ final class SpawnCommand extends CoreCommand {
? Message.of(MVCorei18n.GENERIC_YOU)
: Message.of(entity.getName())),
Replace.WORLD.with(mvWorld.getName()),
Replace.REASON.with(failure.getFirst().getFailureMessage())));
Replace.REASON.with(failure.get(0).getFailureMessage())));
}
private void handleMultiTeleport(MVCommandIssuer issuer, LoadedMultiverseWorld mvWorld,
@@ -19,7 +19,10 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.context.issueraware.PlayerArrayValue;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.PageFilterFlags;
import org.mvplugins.multiverse.core.command.flags.UnsafeFlags;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
@@ -35,14 +38,14 @@ final class TeleportCommand extends CoreCommand {
private final CoreConfig config;
private final CorePermissionsChecker permissionsChecker;
private final AsyncSafetyTeleporter safetyTeleporter;
private final UnsafeFlags flags;
private final Flags flags;
@Inject
TeleportCommand(
@NotNull CoreConfig config,
@NotNull CorePermissionsChecker permissionsChecker,
@NotNull AsyncSafetyTeleporter safetyTeleporter,
@NotNull UnsafeFlags flags
@NotNull Flags flags
) {
this.config = config;
this.permissionsChecker = permissionsChecker;
@@ -54,14 +57,14 @@ final class TeleportCommand extends CoreCommand {
@Subcommand("teleport|tp")
@CommandPermission("@mvteleport")
@CommandCompletion("@playersarray:checkPermissions=@mvteleportother|@destinations:byIssuerForArg=arg1 "
+ "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + UnsafeFlags.NAME + " "
+ "@flags:notByIssuerForArg=arg1,groupName=" + UnsafeFlags.NAME)
+ "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + Flags.NAME + " "
+ "@flags:notByIssuerForArg=arg1,groupName=" + Flags.NAME)
@Syntax("[player] <destination> [--unsafe]")
@Description("{@@mv-core.teleport.description}")
void onTeleportCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@co.aikar.commands.annotation.Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[player]")
@Description("{@@mv-core.teleport.player.description}")
PlayerArrayValue playersValue,
@@ -103,16 +106,21 @@ final class TeleportCommand extends CoreCommand {
.checkSafety(!parsedFlags.hasFlag(flags.unsafe) && destination.checkTeleportSafety())
.passengerMode(config.getPassengerMode())
.teleportSingle(player)
.onSuccess(() -> issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(getYouOrName(issuer, player)),
Replace.DESTINATION.with(destination.toString())))
.onSuccess(() -> {
if (parsedFlags.hasFlag(flags.silent)) {
return;
}
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(getYouOrName(issuer, player)),
Replace.DESTINATION.with(destination.getDisplayMessage()));
})
.onFailureCount(reasonsCountMap -> {
for (var entry : reasonsCountMap.entrySet()) {
Logging.finer("Failed to teleport %s players to %s: %s",
entry.getValue(), destination, entry.getKey());
issuer.sendError(MVCorei18n.TELEPORT_FAILED,
Replace.PLAYER.with(player.getName()),
Replace.DESTINATION.with(destination.toString()),
Replace.DESTINATION.with(destination.getDisplayMessage()),
Replace.REASON.with(Message.of(entry.getKey())));
}
});
@@ -136,18 +144,38 @@ final class TeleportCommand extends CoreCommand {
.checkSafety(!parsedFlags.hasFlag(flags.unsafe) && destination.checkTeleportSafety())
.passengerMode(config.getPassengerMode())
.teleport(List.of(players))
.onSuccessCount(successCount -> issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(successCount + " players"),
Replace.DESTINATION.with(destination.toString())))
.onSuccessCount(successCount -> {
if (parsedFlags.hasFlag(flags.silent)) {
return;
}
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(successCount + " players"),
Replace.DESTINATION.with(destination.getDisplayMessage()));
})
.onFailureCount(reasonsCountMap -> {
for (var entry : reasonsCountMap.entrySet()) {
Logging.finer("Failed to teleport %s players to %s: %s",
entry.getValue(), destination, entry.getKey());
issuer.sendError(MVCorei18n.TELEPORT_FAILED,
Replace.PLAYER.with(entry.getValue() + " players"),
Replace.DESTINATION.with(destination.toString()),
Replace.DESTINATION.with(destination.getDisplayMessage()),
Replace.REASON.with(Message.of(entry.getKey())));
}
});
}
@Service
private static final class Flags extends UnsafeFlags {
private static final String NAME = "mvteleport";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}
private final CommandFlag silent = flag(CommandFlag.builder("--silent")
.addAlias("-s")
.build());
}
}
@@ -14,11 +14,13 @@ 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.command.flags.RemovePlayerDestinationFlags;
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.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate;
@@ -27,6 +29,8 @@ import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions;
import java.util.Objects;
@Service
class UnloadCommand extends CoreCommand {
@@ -48,7 +52,7 @@ class UnloadCommand extends CoreCommand {
@Subcommand("unload")
@CommandPermission("multiverse.core.unload")
@CommandCompletion("@mvworlds @flags:groupName=" + Flags.NAME)
@Syntax("<world>")
@Syntax("<world> [--remove-players [destination]] [--no-save]")
@Description("{@@mv-core.unload.description}")
void onUnloadCommand(
MVCommandIssuer issuer,
@@ -58,15 +62,16 @@ class UnloadCommand extends CoreCommand {
LoadedMultiverseWorld world,
@Optional
@Syntax("[--remove-players] [--no-save]")
@Syntax("[--remove-players [destination]] [--no-save]")
@Description("{@@mv-core.gamerules.description.page}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
issuer.sendInfo(MVCorei18n.UNLOAD_UNLOADING, Replace.WORLD.with(world.getAliasOrName()));
var future = parsedFlags.hasFlag(flags.removePlayers)
? playerWorldTeleporter.removeFromWorld(world)
DestinationInstance<?, ?> removeToDestination = parsedFlags.flagValue(flags.removePlayers);
var future = Objects.nonNull(removeToDestination)
? playerWorldTeleporter.transferAllFromWorldToDestination(world, removeToDestination)
: AsyncAttemptsAggregate.emptySuccess();
future.onSuccess(() -> doWorldUnloading(issuer, world, parsedFlags))
@@ -88,13 +93,18 @@ class UnloadCommand extends CoreCommand {
}
@Service
private static final class Flags extends RemovePlayerFlags {
private static final class Flags extends RemovePlayerDestinationFlags {
private static final String NAME = "mvunload";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
private Flags(
@NotNull CommandFlagsManager flagsManager,
@NotNull WorldManager worldManager,
@NotNull DestinationsProvider destinationsProvider,
@NotNull WorldDestination worldDestination
) {
super(NAME, flagsManager, worldManager, destinationsProvider, worldDestination);
}
private final CommandFlag noUnloadBukkitWorld = flag(CommandFlag.builder("--no-unload-bukkit-world")
@@ -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,
@@ -12,8 +12,11 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.compatibility.WorldBorderCompatibility;
import org.mvplugins.multiverse.core.utils.tick.TickDuration;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import java.time.temporal.ChronoUnit;
import java.util.function.Consumer;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@@ -33,15 +36,14 @@ final class WorldBorderCommand extends CoreCommand {
@Optional
@Default("0")
@Syntax("[time]")
int time,
TickDuration duration,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
worldBorderAction(issuer, world, worldBorder -> {
onWorldBorderSet(issuer, worldBorder.getSize() + size, time, world);
});
worldBorderAction(issuer, world, worldBorder ->
onWorldBorderSet(issuer, worldBorder.getSize() + size, duration, world));
}
@Subcommand("center")
@@ -54,7 +56,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("[z]")
double z,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -79,7 +81,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<damage>")
double damage,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -103,7 +105,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<distance>")
double distance,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -124,7 +126,7 @@ final class WorldBorderCommand extends CoreCommand {
void onWorldBorderGet(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -145,9 +147,9 @@ final class WorldBorderCommand extends CoreCommand {
@Optional
@Default("0")
@Syntax("[time]")
int time,
TickDuration duration,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -158,15 +160,18 @@ final class WorldBorderCommand extends CoreCommand {
Replace.WORLD.with(world.getAliasOrName()));
return;
}
worldBorder.setSize(size, time);
if (time <= 0) {
if (!WorldBorderCompatibility.supportsChangeSizeInTicks() && !duration.isExactTo(ChronoUnit.SECONDS)) {
setRoundOffError(issuer, duration);
}
WorldBorderCompatibility.changeSizeDuration(worldBorder, size, duration);
if (duration.toTicks() <= 0) {
issuer.sendMessage(MVCorei18n.WORLDBORDER_SET_IMMEDIATE,
replace("{size}").with(worldBorder.getSize()),
Replace.WORLD.with(world.getAliasOrName()));
} else {
issuer.sendMessage(previousSize > size ? MVCorei18n.WORLDBORDER_SET_GROWING : MVCorei18n.WORLDBORDER_SET_SHRINKING,
replace("{size}").with(size),
replace("{time}").with(time),
replace("{time}").with(String.format("%.2f", duration.toSeconds())),
Replace.WORLD.with(world.getAliasOrName()));
}
});
@@ -179,7 +184,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<distance>")
int distance,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -201,25 +206,34 @@ final class WorldBorderCommand extends CoreCommand {
MVCommandIssuer issuer,
@Syntax("<time>")
int time,
TickDuration duration,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
worldBorderAction(issuer, world, worldBorder -> {
if (worldBorder.getWarningTime() == time) {
if (WorldBorderCompatibility.getWarningTimeTicks(worldBorder) == duration.toTicks()) {
issuer.sendMessage(MVCorei18n.WORLDBORDER_WARNINGTIME_NOTHINGCHANGED,
Replace.WORLD.with(world.getAliasOrName()));
return;
}
worldBorder.setWarningTime(time);
if (!WorldBorderCompatibility.supportsSetWarningTimeInTicks() && !duration.isExactTo(ChronoUnit.SECONDS)) {
setRoundOffError(issuer, duration);
}
WorldBorderCompatibility.setWarningTimeDuration(worldBorder, duration);
issuer.sendMessage(MVCorei18n.WORLDBORDER_WARNINGTIME_SUCCESS,
replace("{time}").with(worldBorder.getWarningTime()),
replace("{time}").with(String.format("%.2f", WorldBorderCompatibility.getWarningTime(worldBorder))),
Replace.WORLD.with(world.getAliasOrName()));
});
}
private void setRoundOffError(MVCommandIssuer issuer, TickDuration duration) {
issuer.sendError(MVCorei18n.WORLDBORDER_ROUNDOFF_WARNING,
replace("{fromTime}").with(String.format("%.2f", duration.toSeconds())),
replace("{toTime}").with(Math.round(duration.toSeconds())));
}
private void worldBorderAction(MVCommandIssuer issuer, LoadedMultiverseWorld world, Consumer<WorldBorder> worldBorderAction) {
Try.run(() -> world.getWorldBorder().peek(worldBorderAction))
.onFailure(error -> issuer.sendError(error.getLocalizedMessage()));
@@ -8,16 +8,13 @@ import java.util.Locale;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.event.EventPriority;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.queue.ConfirmMode;
import org.mvplugins.multiverse.core.config.handle.CommentedConfigurationHandle;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
@@ -28,7 +25,6 @@ import org.mvplugins.multiverse.core.config.migration.action.InvertBoolMigratorA
import org.mvplugins.multiverse.core.config.migration.action.MoveMigratorAction;
import org.mvplugins.multiverse.core.config.migration.VersionMigrator;
import org.mvplugins.multiverse.core.config.migration.action.SetMigratorAction;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.teleportation.PassengerMode;
import org.mvplugins.multiverse.core.teleportation.PassengerModes;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat;
@@ -190,6 +186,36 @@ public final class CoreConfig {
return configHandle.get(configNodes.enforceFlight);
}
@ApiStatus.AvailableSince("5.5")
public Try<Void> setGamemodeAndFlightEnforceDelay(int delayTicks) {
return configHandle.set(configNodes.gamemodeAndFlightEnforceDelay, delayTicks);
}
@ApiStatus.AvailableSince("5.5")
public int getGamemodeAndFlightEnforceDelay() {
return configHandle.get(configNodes.gamemodeAndFlightEnforceDelay);
}
@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}
*/
@@ -477,6 +503,26 @@ public final class CoreConfig {
return configHandle.get(configNodes.registerPapiHook);
}
@ApiStatus.AvailableSince("5.5")
public Try<Void> setWarnInvalidPapiFormat(boolean warnInvalidPapiFormat) {
return configHandle.set(configNodes.warnInvalidPapiFormat, warnInvalidPapiFormat);
}
@ApiStatus.AvailableSince("5.5")
public boolean getWarnInvalidPapiFormat() {
return configHandle.get(configNodes.warnInvalidPapiFormat);
}
@ApiStatus.AvailableSince("5.5")
public Try<Void> setInvalidPapiFormatReturnsBlank(boolean invalidPapiFormatReturnsBlank) {
return configHandle.set(configNodes.invalidPapiFormatReturnsBlank, invalidPapiFormatReturnsBlank);
}
@ApiStatus.AvailableSince("5.5")
public boolean getInvalidPapiFormatReturnsBlank() {
return configHandle.get(configNodes.invalidPapiFormatReturnsBlank);
}
/**
* {@inheritDoc}
*/
@@ -519,6 +565,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}
*/
@@ -595,6 +651,16 @@ public final class CoreConfig {
return configHandle.get(configNodes.eventPriorityPlayerTeleport);
}
@ApiStatus.AvailableSince("5.5")
public Try<Void> setEventPriorityPlayerWorldChange(EventPriority eventPriorityPlayerWorldChange) {
return configHandle.set(configNodes.eventPriorityPlayerWorldChange, eventPriorityPlayerWorldChange);
}
@ApiStatus.AvailableSince("5.5")
public EventPriority getEventPriorityPlayerWorldChange() {
return configHandle.get(configNodes.eventPriorityPlayerWorldChange);
}
public Try<Void> setBukkitYmlPath(String bukkitYmlPath) {
return configHandle.set(configNodes.bukkitYmlPath, bukkitYmlPath);
}
@@ -603,6 +669,26 @@ public final class CoreConfig {
return configHandle.get(configNodes.bukkitYmlPath);
}
@ApiStatus.AvailableSince("5.3")
public Try<Void> setServerPropertiesPath(String serverPropertiesPath) {
return configHandle.set(configNodes.serverPropertiesPath, serverPropertiesPath);
}
@ApiStatus.AvailableSince("5.3")
public String getServerPropertiesPath() {
return configHandle.get(configNodes.serverPropertiesPath);
}
@ApiStatus.AvailableSince("5.3")
public Try<Void> setAutoDetectGeneratorPlugins(boolean autoDetectGeneratorPlugins) {
return configHandle.set(configNodes.autoDetectGeneratorPlugins, autoDetectGeneratorPlugins);
}
@ApiStatus.AvailableSince("5.3")
public boolean getAutoDetectGeneratorPlugins() {
return configHandle.get(configNodes.autoDetectGeneratorPlugins);
}
/**
* {@inheritDoc}
*/
@@ -4,6 +4,7 @@ import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.event.EventPriority;
import org.bukkit.plugin.PluginManager;
@@ -76,13 +77,10 @@ final class CoreConfigNodes {
.comment("# █░▀░█ █▄█ █▄▄ ░█░ █ ▀▄▀ ██▄ █▀▄ ▄█ ██▄   █▄▄ █▄█ █▀▄ ██▄ #")
.comment("# #")
.comment("# #")
.comment("# WIKI: https://github.com/Multiverse/Multiverse-Core/wiki #")
.comment("# WIKI: https://mvplugins.org/core/ #")
.comment("# DISCORD: https://discord.gg/NZtfKky #")
.comment("# BUG REPORTS: https://github.com/Multiverse/Multiverse-Core/issues #")
.comment("# #")
.comment("# #")
.comment("# Each option in this file is documented and explained here: #")
.comment("# ==> https://github.com/Multiverse/Multiverse-Core/wiki/config.yml #")
.comment("# DONATE: https://github.com/sponsors/Multiverse #")
.comment("# #")
.comment("# #")
.comment("# New options are added to this file automatically. If you manually made changes #")
@@ -137,6 +135,42 @@ final class CoreConfigNodes {
.name("enforce-flight")
.build());
final ConfigNode<Integer> gamemodeAndFlightEnforceDelay = node(ConfigNode.builder("world.gamemode-and-flight-enforce-delay", Integer.class)
.comment("")
.comment("Sets the delay in ticks before Multiverse enforces gamemode and flight ability on world change.")
.comment("Increase this value if you are experiencing issues with other plugins overriding gamemode or flight ability.")
.comment("Or set to 0 to enforce immediately during world change event.")
.defaultValue(1)
.name("gamemode-and-flight-enforce-delay")
.suggester((sender -> List.of("0", "1", "2", "5", "10")))
.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.")
@@ -186,8 +220,12 @@ final class CoreConfigNodes {
final ConfigNode<PassengerModes> passengerMode = node(ConfigNode.builder("teleport.passenger-mode", PassengerModes.class)
.comment("")
.comment("Configures how passengers and vehicles are handled when an entity is teleported.")
.comment(" default: Server will handle passengers and vehicles, this usually means entities will not be teleported to a different world if they have passengers.")
.comment("Configures how passengers and vehicles are handled when an entity is teleported. This config is")
.comment("only applied to /mvtp, join-destination and first-spawn-override. It will not touch any other")
.comment("teleport by other plugins or commands.")
.comment("========= Available options: =========")
.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.")
@@ -365,12 +403,29 @@ final class CoreConfigNodes {
.name("register-papi-hook")
.build());
final ConfigNode<Boolean> warnInvalidPapiFormat = node(ConfigNode.builder("messaging.warn-invalid-papi-format", Boolean.class)
.comment("")
.comment("This config option defines whether or not Multiverse should warn about invalid PlaceholderAPI formats.")
.comment("Only applies if PlaceholderAPI is installed, and only to multiverse placeholders.")
.defaultValue(true)
.name("warn-invalid-papi-format")
.build());
final ConfigNode<Boolean> invalidPapiFormatReturnsBlank = node(ConfigNode.builder("messaging.invalid-papi-format-returns-blank", Boolean.class)
.comment("")
.comment("By default, invalid formats will return the original placeholder string.")
.comment("This config option defines whether or not Multiverse should show invalid PlaceholderAPI formats as blank.")
.comment("Only applies if PlaceholderAPI is installed, and only to multiverse placeholders.")
.defaultValue(false)
.name("invalid-papi-format-returns-blank")
.build());
final ConfigNode<Locale> defaultLocale = node(ConfigNode.builder("messaging.default-locale", Locale.class)
.comment("")
.comment("This config option defines the default language Multiverse should use.")
.defaultValue(Locale.ENGLISH)
.name("default-locale")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
commandManager.get().getLocales().setDefaultLocale(newValue))
.build());
@@ -380,9 +435,8 @@ final class CoreConfigNodes {
.comment("If the player's language does not have a translation, it will use the default language set above instead.")
.defaultValue(true)
.name("per-player-locale")
.onSetValue((oldValue, newValue) -> {
commandManager.get().usePerIssuerLocale(newValue);
})
.onLoadAndChange((oldValue, newValue) ->
commandManager.get().usePerIssuerLocale(newValue))
.build());
private final ConfigHeaderNode commandHeader = node(ConfigHeaderNode.builder("command")
@@ -398,6 +452,15 @@ final class CoreConfigNodes {
.name("resolve-alias-name")
.build());
final ConfigNode<Boolean> simplifiedDestinationTabCompletion = node(ConfigNode.builder("command.simplified-destination-tab-completion", Boolean.class)
.comment("")
.comment("If this is set to true, Multiverse will only suggest simple destination formats in tab completion.")
.comment("This means only world names will be suggested without other destination type such as `e:worldname:x,y,z` or `p:playername`.")
.comment("Note: This DOES NOT prevent players from using the destinations, as that is controlled by permissions.")
.defaultValue(false)
.name("simplified-destination-tab-completion")
.build());
final ConfigNode<ConfirmMode> confirmMode = node(ConfigNode.builder("command.confirm-mode", ConfirmMode.class)
.comment("")
.comment("This config option defines whether `/mv confirm` is needed before running a DANGEROUS action.")
@@ -452,8 +515,11 @@ final class CoreConfigNodes {
.comment("")
.comment("This config option defines the priority for the PlayerPortalEvent.")
.name("event-priority-player-portal")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-portal", newValue))
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-portal' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerRespawn = node(ConfigNode.builder("event-priority.player-respawn", EventPriority.class)
@@ -461,16 +527,23 @@ final class CoreConfigNodes {
.name("event-priority-player-respawn")
.comment("")
.comment("This config option defines the priority for the PlayerRespawnEvent.")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-respawn", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-respawn' config option will only take " +
"full effect after a server restart."))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerSpawnLocation = node(ConfigNode.builder("event-priority.player-spawn-location", EventPriority.class)
.defaultValue(EventPriority.NORMAL)
.comment("")
.comment("This config option defines the priority for the PlayerSpawnLocationEvent.")
.name("event-priority-player-spawn-location").onSetValue((oldValue, newValue) ->
.name("event-priority-player-spawn-location")
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-spawn-location", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-spawn-location' config option will only take " +
"full effect after a server restart."))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerTeleport = node(ConfigNode.builder("event-priority.player-teleport", EventPriority.class)
@@ -478,8 +551,23 @@ 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());
final ConfigNode<EventPriority> eventPriorityPlayerWorldChange = node(ConfigNode.builder("event-priority.player-changed-world", EventPriority.class)
.defaultValue(EventPriority.NORMAL)
.name("event-priority-player-changed-world")
.comment("")
.comment("This config option defines the priority for the PlayerChangedWorldEvent.")
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-changed-world", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-changed-world' config option will only take " +
"full effect after a server restart."))
.build());
private final ConfigHeaderNode miscHeader = node(ConfigHeaderNode.builder("misc")
@@ -492,6 +580,29 @@ final class CoreConfigNodes {
.comment("Note: this config option needs a server restart to take effect.")
.defaultValue("bukkit.yml")
.name("bukkit-yml-path")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'bukkit-yml-path' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<String> serverPropertiesPath = node(ConfigNode.builder("misc.server-properties-path", String.class)
.comment("")
.comment("Change this if you use a custom path for the server.properties file with `--config` startup flag.")
.comment("Note: this config option needs a server restart to take effect.")
.defaultValue("server.properties")
.name("server-properties-path")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'server-properties-path' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<Boolean> autoDetectGeneratorPlugins = node(ConfigNode.builder("misc.auto-detect-generator-plugins", Boolean.class)
.comment("")
.comment("When enabled, Multiverse will attempt to automatically detect world generator plugins installed on your server.")
.comment("This option only affects tab-completion within `/mv create` command and output of `/mv generators` command.")
.comment("Disabling this will not have any affect on the usages of world generator plugins itself.")
.defaultValue(true)
.name("auto-detect-generator-plugins")
.build());
final ConfigNode<Integer> globalDebug = node(ConfigNode.builder("misc.global-debug", Integer.class)
@@ -509,7 +620,7 @@ final class CoreConfigNodes {
.validator(value -> (value < 0 || value > 3)
? Try.failure(new MultiverseException("Debug level must be between 0 and 3."))
: Try.success(null))
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (newValue != Logging.getDebugLevel()) {
Logging.setDebugLevel(newValue);
pluginManager.callEvent(new MVDebugModeEvent(newValue));
@@ -523,7 +634,7 @@ final class CoreConfigNodes {
.comment("This will only work if the above 'global-debug' is set to 1 or more.")
.defaultValue(false)
.name("debug-permissions")
.onSetValue((oldValue, newValue) -> PermissionUtils.setDebugPermissions(newValue))
.onLoadAndChange((oldValue, newValue) -> PermissionUtils.setDebugPermissions(newValue))
.build());
final ConfigNode<Boolean> silentStart = node(ConfigNode.builder("misc.silent-start", Boolean.class)
@@ -531,12 +642,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());
@@ -6,16 +6,18 @@ import java.util.Map;
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.MapValueNode;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.config.node.ValueNode;
@@ -84,22 +86,19 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
});
nodeValueMap.forEach((valueNode, value) -> {
valueNode.onSetValue(value, value);
valueNode.onLoad(value);
valueNode.onLoadAndChange(Bukkit.getConsoleSender(), null, value);
});
}
protected <T> T deserializeNodeFromConfig(ValueNode<T> node) {
if (node.getSerializer() == null) {
return Option.of(config.getObject(node.getPath(), node.getType())).getOrElse(node::getDefaultValue);
}
return Try.of(() -> {
var value = config.get(node.getPath());
if (value == null) {
return node.getDefaultValue();
}
return node.getSerializer().deserialize(value, node.getType());
}).flatMap(value -> node.validate(value).map(ignore -> value))
.onFailure(e -> Logging.warning("Failed to deserialize node %s: %s", node.getPath(), e.getMessage()))
return value == null ? node.getDefaultValue() : node.deserialize(value);
})
.flatMap(value -> node.validate(value).map(ignore -> value))
.onFailure(e -> Logging.warning("Failed to deserialize node %s: %s",
node.getPath(), e.getMessage()))
.getOrElse(node::getDefaultValue);
}
@@ -120,12 +119,7 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
if (value == null) {
value = node.getDefaultValue();
}
if (node.getSerializer() != null) {
var serialized = node.getSerializer().serialize(value, node.getType());
config.set(node.getPath(), serialized);
} else {
config.set(node.getPath(), value);
}
config.set(node.getPath(), node.serialize(value));
}
/**
@@ -156,10 +150,36 @@ 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);
}
public <K, V> Try<Void> set(@NotNull MapValueNode<K, V> node, K key, V value) {
return node.validateEntry(key, value).map(ignore -> {
Map<K,V> map = get(node);
map.put(key, value);
// node.onSetEntryValue(null, key, null, value);
return null;
});
}
/**
* 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;
});
}
@@ -200,6 +220,18 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
});
}
public <K, V> Try<Void> remove(@NotNull MapValueNode<K, V> node, K key) {
return node.validateKey(key).map(ignore -> {
Map<K,V> map = get(node);
V value = map.remove(key);
if (value == null) {
throw new IllegalArgumentException("Cannot remove entry as it is already not in the map!");
}
// node.onSetEntryValue(key, value, null);
return null;
});
}
/**
* Sets the default value of a node.
*
@@ -112,10 +112,5 @@ public abstract class FileConfigurationHandle<C extends FileConfiguration> exten
* @return The configuration handle.
*/
public abstract @NotNull FileConfigurationHandle<C> build();
@SuppressWarnings("unchecked")
protected B self() {
return (B) this;
}
}
}
@@ -226,7 +226,7 @@ public class StringPropertyHandle {
public Try<Void> setPropertyString(@NotNull CommandSender sender, @Nullable String name, @Nullable String value) {
return findNode(name, ValueNode.class)
.flatMap(node -> node.parseFromString(sender, value)
.flatMap(parsedValue -> handle.set(node, parsedValue)));
.flatMap(parsedValue -> handle.set(sender, node, parsedValue)));
}
/**
@@ -14,6 +14,9 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.node.functions.NodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeValueCallback;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeStringParser;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeSuggester;
import org.mvplugins.multiverse.core.config.node.serializer.DefaultSerializerProvider;
@@ -53,7 +56,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected @Nullable NodeStringParser<T> stringParser;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
protected @Nullable NodeValueCallback<T> onLoad;
protected @Nullable NodeChangeCallback<T> onLoadAndChange;
protected @Nullable NodeChangeCallback<T> onChange;
protected ConfigNode(
@NotNull String path,
@@ -66,7 +71,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@Nullable NodeStringParser<T> stringParser,
@Nullable NodeSerializer<T> serializer,
@Nullable Function<T, Try<Void>> validator,
@Nullable BiConsumer<T, T> onSetValue) {
@Nullable NodeValueCallback<T> onLoad,
@Nullable NodeChangeCallback<T> onLoadAndChange,
@Nullable NodeChangeCallback<T> onChange) {
super(path, comments);
this.name = name;
this.type = type;
@@ -82,7 +89,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
? serializer
: DefaultSerializerProvider.getDefaultSerializer(type);
this.validator = validator;
this.onSetValue = onSetValue;
this.onLoad = onLoad;
this.onLoadAndChange = onLoadAndChange;
this.onChange = onChange;
}
/**
@@ -171,6 +180,24 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return serializer;
}
/**
* {@inheritDoc}
*/
@Override
public T deserialize(@Nullable Object object) {
return serializer == null
? (type.isInstance(object)) ? type.cast(object) : getDefaultValue()
: serializer.deserialize(object, type);
}
/**
* {@inheritDoc}
*/
@Override
public Object serialize(T value) {
return serializer == null ? value : serializer.serialize(value, type);
}
/**
* {@inheritDoc}
*/
@@ -186,9 +213,29 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
* {@inheritDoc}
*/
@Override
public void onSetValue(@Nullable T oldValue, @Nullable T newValue) {
if (onSetValue != null) {
onSetValue.accept(oldValue, newValue);
public void onLoad(@Nullable T value) {
if (onLoad != null) {
onLoad.run(value);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLoadAndChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue) {
if (onLoadAndChange != null) {
onLoadAndChange.run(sender, oldValue, newValue);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue) {
if (onChange != null) {
onChange.run(sender, oldValue, newValue);
}
}
@@ -208,7 +255,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected @Nullable NodeStringParser<T> stringParser;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
protected @Nullable NodeValueCallback<T> onLoad;
protected @Nullable NodeChangeCallback<T> onLoadAndChange;
protected @Nullable NodeChangeCallback<T> onChange;
/**
* Creates a new builder.
@@ -368,15 +417,83 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return self();
}
/**
* Sets the action to be performed when the value is loaded.
*
* @param onLoad The action to be performed.
* @return This builder.
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onLoad(@NotNull NodeValueCallback<T> onLoad) {
this.onLoad = this.onLoad == null ? onLoad : this.onLoad.then(onLoad);
return self();
}
/**
* Sets the action to be performed when the value is loaded and changed.
*
* @param onLoadAndChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onLoadAndChange(@NotNull NodeChangeCallback<T> onLoadAndChange) {
this.onLoadAndChange = this.onLoadAndChange == null ? onLoadAndChange : this.onLoadAndChange.then(onLoadAndChange);
return self();
}
/**
* Sets the action to be performed when the value is loaded and changed.
*
* @param onLoadAndChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onLoadAndChange(@NotNull SenderNodeChangeCallback<T> onLoadAndChange) {
return onLoadAndChange((NodeChangeCallback<T>) onLoadAndChange);
}
/**
* Sets the action to be performed when the value is changed.
*
* @param onChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onChange(@NotNull NodeChangeCallback<T> onChange) {
this.onChange = this.onChange == null ? onChange : this.onChange.then(onChange);
return self();
}
/**
* Sets the action to be performed when the value is changed.
*
* @param onChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onChange(@NotNull SenderNodeChangeCallback<T> onChange) {
return onChange((NodeChangeCallback<T>) onChange);
}
/**
* Sets the action to be performed when the value is set.
*
* @param onSetValue The action to be performed.
* @return This builder.
*
* @deprecated Use {@link #onLoadAndChange(NodeChangeCallback)} instead.
*/
@Deprecated(since = "5.4", forRemoval = true)
public @NotNull B onSetValue(@NotNull BiConsumer<T, T> onSetValue) {
this.onSetValue = this.onSetValue == null ? onSetValue : this.onSetValue.andThen(onSetValue);
return self();
return onLoadAndChange(onSetValue::accept);
}
/**
@@ -385,7 +502,8 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@Override
public @NotNull ConfigNode<T> build() {
return new ConfigNode<>(path, comments.toArray(new String[0]),
name, type, aliases, defaultValue, suggester, stringParser, serializer, validator, onSetValue);
name, type, aliases, defaultValue, suggester, stringParser, serializer, validator,
onLoad, onLoadAndChange, onChange);
}
}
}
@@ -19,6 +19,8 @@ import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.node.functions.DefaultStringParserProvider;
import org.mvplugins.multiverse.core.config.node.functions.DefaultSuggesterProvider;
import org.mvplugins.multiverse.core.config.node.functions.NodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeValueCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeStringParser;
import org.mvplugins.multiverse.core.config.node.functions.NodeSuggester;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeStringParser;
@@ -67,7 +69,9 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
@Nullable NodeStringParser<List<I>> stringParser,
@Nullable NodeSerializer<List<I>> serializer,
@Nullable Function<List<I>, Try<Void>> validator,
@Nullable BiConsumer<List<I>, List<I>> onSetValue,
@Nullable NodeValueCallback<List<I>> onLoad,
@Nullable NodeChangeCallback<List<I>> onLoadAndChange,
@Nullable NodeChangeCallback<List<I>> onChange,
@NotNull Class<I> itemType,
@Nullable NodeSuggester itemSuggester,
@Nullable NodeStringParser<I> itemStringParser,
@@ -75,7 +79,7 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
@Nullable Function<I, Try<Void>> itemValidator,
@Nullable BiConsumer<I, I> onSetItemValue) {
super(path, comments, name, type, aliases, defaultValueSupplier, suggester, stringParser, serializer,
validator, onSetValue);
validator, onLoad, onLoadAndChange, onChange);
this.itemType = itemType;
this.itemSuggester = itemSuggester != null
? itemSuggester
@@ -105,8 +109,8 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
if (this.itemSerializer != null && this.serializer == null) {
setDefaultSerialiser();
}
if (this.onSetItemValue != null && this.onSetValue == null) {
setDefaultOnSetValue();
if (this.onSetItemValue != null && this.onLoadAndChange == null) {
setDefaultOnLoadAndChange();
}
if (this.defaultValue == null) {
this.defaultValue = ArrayList::new;
@@ -116,9 +120,9 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
private void setDefaultSuggester() {
if (itemSuggester instanceof SenderNodeSuggester senderItemSuggester) {
this.suggester = (SenderNodeSuggester)(sender, input) ->
StringFormatter.addonToCommaSeperated(input, senderItemSuggester.suggest(sender, input));
StringFormatter.addOnToCommaSeparated(input, senderItemSuggester.suggest(sender, input));
} else {
this.suggester = input -> StringFormatter.addonToCommaSeperated(input, itemSuggester.suggest(input));
this.suggester = input -> StringFormatter.addOnToCommaSeparated(input, itemSuggester.suggest(input));
}
}
@@ -168,8 +172,8 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
};
}
private void setDefaultOnSetValue() {
this.onSetValue = (oldValue, newValue) -> {
private void setDefaultOnLoadAndChange() {
this.onLoadAndChange = (oldValue, newValue) -> {
if (oldValue != null) {
oldValue.stream()
.filter(value -> !newValue.contains(value))
@@ -279,7 +283,7 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
//noinspection unchecked
super(path, (Class<List<I>>) (Object) List.class);
this.itemType = itemType;
this.defaultValue = () -> (List<I>) new ArrayList<>();
this.defaultValue = ArrayList::new;
}
/**
@@ -381,7 +385,9 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
stringParser,
serializer,
validator,
onSetValue,
onLoad,
onLoadAndChange,
onChange,
itemType,
itemSuggester,
itemStringParser,
@@ -0,0 +1,603 @@
package org.mvplugins.multiverse.core.config.node;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
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.NodeStringParser;
import org.mvplugins.multiverse.core.config.node.functions.NodeSuggester;
import org.mvplugins.multiverse.core.config.node.functions.NodeValueCallback;
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.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.utils.REPatterns;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* A config node that contains key-value mappings.
*
* @param <K> The key type.
* @param <V> The value type.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public class MapConfigNode<K, V> extends ConfigNode<Map<K, V>> implements MapValueNode<K, V> {
/**
* Creates a new builder for a {@link MapConfigNode}.
*
* @param path The path of the node.
* @param keyType The map key type.
* @param valueType The map value type.
* @param <K> The key type.
* @param <V> The value type.
* @return The new builder.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static @NotNull <K, V> Builder<K, V> mapBuilder(
@NotNull String path,
@NotNull Class<K> keyType,
@NotNull Class<V> valueType) {
return new Builder<>(path, keyType, valueType);
}
protected final @NotNull Class<K> keyType;
protected final @NotNull Class<V> valueType;
protected final @Nullable NodeSuggester keySuggester;
protected final @Nullable NodeSuggester valueSuggester;
protected final @Nullable NodeStringParser<K> keyStringParser;
protected final @Nullable NodeStringParser<V> valueStringParser;
protected final @Nullable NodeSerializer<K> keySerializer;
protected final @Nullable NodeSerializer<V> valueSerializer;
protected final @Nullable Function<K, Try<Void>> keyValidator;
protected final @Nullable Function<V, Try<Void>> valueValidator;
protected MapConfigNode(
@NotNull String path,
@NotNull String[] comments,
@Nullable String name,
@NotNull Class<Map<K, V>> type,
@NotNull String[] aliases,
@Nullable Supplier<Map<K, V>> defaultValueSupplier,
@Nullable NodeSuggester suggester,
@Nullable NodeStringParser<Map<K, V>> stringParser,
@Nullable NodeSerializer<Map<K, V>> serializer,
@Nullable Function<Map<K, V>, Try<Void>> validator,
@Nullable NodeValueCallback<Map<K, V>> onLoad,
@Nullable NodeChangeCallback<Map<K, V>> onLoadAndChange,
@Nullable NodeChangeCallback<Map<K, V>> onChange,
@NotNull Class<K> keyType,
@NotNull Class<V> valueType,
@Nullable NodeSuggester keySuggester,
@Nullable NodeSuggester valueSuggester,
@Nullable NodeStringParser<K> keyStringParser,
@Nullable NodeStringParser<V> valueStringParser,
@Nullable NodeSerializer<K> keySerializer,
@Nullable NodeSerializer<V> valueSerializer,
@Nullable Function<K, Try<Void>> keyValidator,
@Nullable Function<V, Try<Void>> valueValidator) {
super(path, comments, name, type, aliases, defaultValueSupplier, suggester, stringParser, serializer,
validator, onLoad, onLoadAndChange, onChange);
this.keyType = keyType;
this.valueType = valueType;
this.keySuggester = keySuggester != null
? keySuggester
: DefaultSuggesterProvider.getDefaultSuggester(keyType);
this.valueSuggester = valueSuggester != null
? valueSuggester
: DefaultSuggesterProvider.getDefaultSuggester(valueType);
this.keyStringParser = keyStringParser != null
? keyStringParser
: DefaultStringParserProvider.getDefaultStringParser(keyType);
this.valueStringParser = valueStringParser != null
? valueStringParser
: DefaultStringParserProvider.getDefaultStringParser(valueType);
this.keySerializer = keySerializer != null
? keySerializer
: DefaultSerializerProvider.getDefaultSerializer(keyType);
this.valueSerializer = valueSerializer != null
? valueSerializer
: DefaultSerializerProvider.getDefaultSerializer(valueType);
this.keyValidator = keyValidator != null
? keyValidator
: defaultYamlKeyValidator();
this.valueValidator = valueValidator;
setDefaults();
}
private Function<K, Try<Void>> defaultYamlKeyValidator() {
return key -> Try.of(() -> {
if (!REPatterns.YAML_KEY.matcher(String.valueOf(serializeKey(key))).matches()) {
throw new MultiverseException("Invalid yaml key: '" + key + "'. Keys can only " +
"contain alphanumeric characters, underscores and hyphens.");
}
return null;
});
}
private void setDefaults() {
if ((this.keySuggester != null || this.valueSuggester != null) && this.suggester == null) {
setDefaultSuggester();
}
if (this.keyStringParser != null && this.valueStringParser != null && this.stringParser == null) {
setDefaultStringParser();
}
if ((this.keyValidator != null || this.valueValidator != null) && this.validator == null) {
setDefaultValidator();
}
if ((this.keySerializer != null || this.valueSerializer != null) && this.serializer == null) {
setDefaultSerializer();
}
if (this.defaultValue == null) {
this.defaultValue = LinkedHashMap::new;
}
}
private void setDefaultSuggester() {
if (this.keySuggester instanceof SenderNodeSuggester || this.valueSuggester instanceof SenderNodeSuggester) {
this.suggester = (SenderNodeSuggester) this::suggestEntries;
} else {
this.suggester = input -> suggestEntries(null, input);
}
}
private @NotNull Collection<String> suggestEntries(@Nullable CommandSender sender, @Nullable String input) {
String content = input == null ? "" : input;
int lastComma = content.lastIndexOf(',');
String prefix = lastComma == -1 ? "" : content.substring(0, lastComma + 1);
String currentEntry = lastComma == -1 ? content : content.substring(lastComma + 1);
String[] keyValue = REPatterns.EQUALS.split(currentEntry, 2);
if (keyValue.length == 2 && valueSuggester != null) {
String keyPart = keyValue[0];
String valuePart = keyValue[1];
return suggestFrom(valueSuggester, sender, valuePart).stream()
.map(suggestedValue -> prefix + keyPart + "=" + suggestedValue)
.toList();
}
if (keySuggester == null) {
return Collections.emptyList();
}
return suggestFrom(keySuggester, sender, currentEntry).stream()
.map(suggestedKey -> prefix + suggestedKey + "=")
.toList();
}
private @NotNull Collection<String> suggestFrom(
@NotNull NodeSuggester suggester,
@Nullable CommandSender sender,
@Nullable String input) {
if (sender != null && suggester instanceof SenderNodeSuggester senderSuggester) {
return senderSuggester.suggest(sender, input);
}
return suggester.suggest(input);
}
private void setDefaultStringParser() {
this.stringParser = (input, type) -> Try.of(() -> {
Map<K, V> parsed = new LinkedHashMap<>();
if (input == null || input.isBlank()) {
return parsed;
}
for (String entry : REPatterns.SEMICOLON.split(input)) {
String[] keyValue = REPatterns.EQUALS.split(entry, 2);
if (keyValue.length != 2) {
throw new IllegalArgumentException("Invalid map entry '" + entry + "'. Expected format key=value");
}
NodeStringParser<K> keyParser = Objects.requireNonNull(keyStringParser, "keyStringParser");
NodeStringParser<V> valueParser = Objects.requireNonNull(valueStringParser, "valueStringParser");
K parsedKey = keyParser.parse(keyValue[0].trim(), keyType).get();
V parsedValue = valueParser.parse(keyValue[1].trim(), valueType).get();
parsed.put(parsedKey, parsedValue);
}
return parsed;
});
}
private void setDefaultValidator() {
this.validator = value -> Try.of(() -> {
if (value == null) {
return null;
}
for (Map.Entry<K, V> entry : value.entrySet()) {
if (keyValidator != null) {
keyValidator.apply(entry.getKey()).get();
}
if (valueValidator != null) {
valueValidator.apply(entry.getValue()).get();
}
}
return null;
});
}
private void setDefaultSerializer() {
this.serializer = new NodeSerializer<>() {
@Override
public Map<K, V> deserialize(Object object, Class<Map<K, V>> type) {
if (object == null) {
return new LinkedHashMap<>();
}
if (object instanceof Map<?, ?> rawMap) {
Map<K, V> result = new LinkedHashMap<>();
rawMap.forEach((key, value) -> {
Map.Entry<K, V> entry = deserializeEntry(key, value);
result.put(entry.getKey(), entry.getValue());
});
return result;
}
if (object instanceof ConfigurationSection section) {
Map<K, V> result = new LinkedHashMap<>();
for (String key : section.getKeys(false)) {
Object rawValue = section.get(key);
Map.Entry<K, V> entry = deserializeEntry(key, rawValue);
result.put(entry.getKey(), entry.getValue());
}
return result;
}
throw new IllegalArgumentException("Invalid map object '" + object + "'. Expected map or section.");
}
@Override
public Object serialize(Map<K, V> object, Class<Map<K, V>> type) {
Map<Object, Object> serialized = new LinkedHashMap<>();
if (object == null) {
return serialized;
}
object.forEach((key, value) -> {
Map.Entry<Object, Object> entry = serializeEntry(key, value);
serialized.put(entry.getKey(), entry.getValue());
});
return serialized;
}
};
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Try<Void> validateKey(@Nullable K key) {
return keyValidator == null ? Try.success(null) : keyValidator.apply(key);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Try<Void> validateValue(@Nullable V value) {
return valueValidator == null ? Try.success(null) : valueValidator.apply(value);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Try<Void> validateEntry(@Nullable K key, @Nullable V value) {
return validateKey(key).flatMap(ignored -> validateValue(value));
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Map.Entry<K, V> deserializeEntry(@Nullable Object key, @Nullable Object value) {
K deserializedKey = keySerializer == null
? keyType.isInstance(key) ? keyType.cast(key) : null
: keySerializer.deserialize(key, keyType);
V deserializedValue = valueSerializer == null
? keyType.isInstance(value) ? valueType.cast(value) : null
: valueSerializer.deserialize(value, valueType);
return new SimpleEntry<>(deserializedKey, deserializedValue);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Map.Entry<Object, Object> serializeEntry(@Nullable K key, @Nullable V value) {
return new SimpleEntry<>(serializeKey(key), serializeValue(value));
}
private @Nullable Object serializeKey(@Nullable K key) {
return keySerializer == null ? key : keySerializer.serialize(key, keyType);
}
private @Nullable Object serializeValue(@Nullable V value) {
return valueSerializer == null ? value : valueSerializer.serialize(value, valueType);
}
private record SimpleEntry<K, V>(K key, V value) implements Map.Entry<K, V> {
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(V value) {
throw new UnsupportedOperationException();
}
}
/**
* Builder for {@link MapConfigNode} instances.
*
* <p>This builder extends {@link ConfigNode.Builder} with additional configuration specific to map entries:
* entry-level suggesters, string parsers, serializers and validators for both keys and values. When any
* entry-level component is omitted the builder provides a sensible default (for example default parsers,
* suggesters, serializers or an empty map default value).
*
* @param <K> The map key type.
* @param <V> The map value type.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static class Builder<K, V> extends ConfigNode.Builder<Map<K, V>, Builder<K, V>> {
protected final @NotNull Class<K> keyType;
protected final @NotNull Class<V> valueType;
protected @Nullable NodeSuggester keySuggester;
protected @Nullable NodeSuggester valueSuggester;
protected @Nullable NodeStringParser<K> keyStringParser;
protected @Nullable NodeStringParser<V> valueStringParser;
protected @Nullable NodeSerializer<K> keySerializer;
protected @Nullable NodeSerializer<V> valueSerializer;
protected @Nullable Function<K, Try<Void>> keyValidator;
protected @Nullable Function<V, Try<Void>> valueValidator;
/**
* Creates a new builder for a map config node.
*
* @param path The configuration path for the node.
* @param keyType The runtime type of the map keys.
* @param valueType The runtime type of the map values.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
protected Builder(@NotNull String path, @NotNull Class<K> keyType, @NotNull Class<V> valueType) {
//noinspection unchecked
super(path, (Class<Map<K, V>>) (Object) Map.class);
this.keyType = keyType;
this.valueType = valueType;
this.defaultValue = LinkedHashMap::new;
}
/**
* Sets a suggester for map keys. The suggester is used when computing tab-completion suggestions for the
* key part of an entry.
*
* @param keySuggester The suggester to use for keys.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> keySuggester(@NotNull NodeSuggester keySuggester) {
this.keySuggester = keySuggester;
return self();
}
/**
* Sets a sender-aware suggester for map keys. Use this when suggestions depend on the command sender
* (permissions, location, etc.).
*
* @param keySuggester The sender-aware suggester.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> keySuggester(@NotNull SenderNodeSuggester keySuggester) {
this.keySuggester = keySuggester;
return self();
}
/**
* Sets a suggester for map values. The suggester is used when computing tab-completion suggestions for the
* value part of an entry.
*
* @param valueSuggester The suggester to use for values.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> valueSuggester(@NotNull NodeSuggester valueSuggester) {
this.valueSuggester = valueSuggester;
return self();
}
/**
* Sets a sender-aware suggester for map values. Use this when value suggestions depend on the
* command sender.
*
* @param valueSuggester The sender-aware suggester.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> valueSuggester(@NotNull SenderNodeSuggester valueSuggester) {
this.valueSuggester = valueSuggester;
return self();
}
/**
* Sets the string parser for keys. The parser is used when parsing user-provided key strings into key
* instances (e.g. when parsing a map from a single-line string representation).
*
* @param keyStringParser The parser to use for keys.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> keyStringParser(@NotNull NodeStringParser<K> keyStringParser) {
this.keyStringParser = keyStringParser;
return self();
}
/**
* Sets a sender-aware string parser for keys. Use this when parsing depends on the sender context.
*
* @param keyStringParser The sender-aware key parser.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> keyStringParser(@NotNull SenderNodeStringParser<K> keyStringParser) {
this.keyStringParser = keyStringParser;
return self();
}
/**
* Sets the string parser for values. The parser converts the textual part after '=' in an entry into the
* value type.
*
* @param valueStringParser The parser to use for values.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> valueStringParser(@NotNull NodeStringParser<V> valueStringParser) {
this.valueStringParser = valueStringParser;
return self();
}
/**
* Sets a sender-aware string parser for values.
*
* @param valueStringParser The sender-aware value parser.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> valueStringParser(@NotNull SenderNodeStringParser<V> valueStringParser) {
this.valueStringParser = valueStringParser;
return self();
}
/**
* Sets the serializer for keys. Serializers convert keys to/from persisted forms stored in the configuration
* backend.
*
* @param keySerializer The key serializer.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> keySerializer(@NotNull NodeSerializer<K> keySerializer) {
this.keySerializer = keySerializer;
return self();
}
/**
* Sets the serializer for values.
*
* @param valueSerializer The value serializer.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> valueSerializer(@NotNull NodeSerializer<V> valueSerializer) {
this.valueSerializer = valueSerializer;
return self();
}
/**
* Sets a validator for keys. The validator should return a successful {@link Try} for valid keys or a failed
* {@link Try} describing the validation error for invalid keys.
*
* @param keyValidator The key validator.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> keyValidator(@NotNull Function<K, Try<Void>> keyValidator) {
this.keyValidator = keyValidator;
return self();
}
/**
* Sets a validator for values.
*
* @param valueValidator The value validator.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> valueValidator(@NotNull Function<V, Try<Void>> valueValidator) {
this.valueValidator = valueValidator;
return self();
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull MapConfigNode<K, V> build() {
return new MapConfigNode<>(
path,
comments.toArray(new String[0]),
name,
type,
aliases,
defaultValue,
suggester,
stringParser,
serializer,
validator,
onLoad,
onLoadAndChange,
onChange,
keyType,
valueType,
keySuggester,
valueSuggester,
keyStringParser,
valueStringParser,
keySerializer,
valueSerializer,
keyValidator,
valueValidator);
}
}
}
@@ -0,0 +1,93 @@
package org.mvplugins.multiverse.core.config.node;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
/**
* A {@link ValueNode} specialization that stores a {@link Map} of key-value pairs.
*
* @param <K> The key type.
* @param <V> The value type.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public interface MapValueNode<K, V> extends ValueNode<Map<K, V>> {
/**
* Validates a single map key.
* <br />
* Implementations should return a successful {@link Try} when the key is acceptable, or a failed
* {@link Try} when the key is invalid.
*
* @param key The key to validate, or {@code null} if the key is absent.
* @return A successful {@link Try} if the key is valid, or a failed {@link Try} if it is not.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull Try<Void> validateKey(@Nullable K key);
/**
* Validates a single map value.
* <br />
* Implementations should return a successful {@link Try} when the value is acceptable, or a failed
* {@link Try} when the value is invalid.
*
* @param value The value to validate, or {@code null} if the value is absent.
* @return A successful {@link Try} if the value is valid, or a failed {@link Try} if it is not.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull Try<Void> validateValue(@Nullable V value);
/**
* Validates a key-value entry.
* <br />
* This is typically implemented by composing {@link #validateKey(Object)} and {@link #validateValue(Object)}.
* If either part fails, the entry is considered invalid.
*
* @param key The entry key, or {@code null} if the key is absent.
* @param value The entry value, or {@code null} if the value is absent.
* @return A successful {@link Try} if both parts are valid, or a failed {@link Try} if either part is invalid.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull Try<Void> validateEntry(@Nullable K key, @Nullable V value);
/**
* Deserializes a raw key-value pair into a map entry.
* <br />
* This method converts a single persisted entry, not the full map. If either component cannot be converted,
* implementations may return {@code null} for that side of the entry.
*
* @param key The raw key object, or {@code null} if no key is present.
* @param value The raw value object, or {@code null} if no value is present.
* @return A map entry containing the deserialized key and value.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull Map.Entry<K, V> deserializeEntry(@Nullable Object key, @Nullable Object value);
/**
* Serializes a map entry into raw configuration objects.
* <br />
* This method converts a single entry to a persisted representation, not the full map. The returned entry
* contains the serialized key and serialized value.
*
* @param key The entry key, or {@code null} if the key is absent.
* @param value The entry value, or {@code null} if the value is absent.
* @return A map entry containing the serialized key and value.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull Map.Entry<Object, Object> serializeEntry(@Nullable K key, @Nullable V value);
}
@@ -5,11 +5,13 @@ import java.util.Collection;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.apache.logging.log4j.util.Strings;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.handle.BaseConfigurationHandle;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
public interface ValueNode<T> extends Node {
@@ -32,7 +34,7 @@ public interface ValueNode<T> extends Node {
* 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.
* @return The aliases of this node, or an empty array if the node has no aliases.
*
* @since 5.1
*/
@@ -42,27 +44,36 @@ public interface ValueNode<T> extends Node {
}
/**
* Gets the default value with type {@link T} of the node.
* Gets the default value for this node.
*
* @return The default value of the node.
* <p>The returned value is used when no explicit value is present or when deserialization falls back to the
* node's default. Implementations may return {@code null} to indicate that no default value is available.
*
* @return The default value of the node, or {@code null} if none is configured.
*/
@Nullable T getDefaultValue();
/**
* Suggests possible string values for this node. Generated based on the current user input.
* Suggests possible string values for this node.
*
* @param input The current partial user input
* <p>The returned values are based on the current partial user input and should be suitable for command-line
* tab completion or other interactive hints.
*
* @param input The current partial user input, or {@code null} if no input has been typed yet.
* @return A collection of possible string values.
*/
@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.
* Suggests possible string values for this node using sender context.
*
* @param sender The sender context.
* @param input The input string.
* @return A collection of possible string values
* <p>Implementations may use the sender's name, permissions, or player location to tailor the returned
* suggestions. If no sender-specific logic is available, implementations should fall back to
* {@link #suggest(String)}.
*
* @param sender The sender context.
* @param input The current partial user input, or {@code null} if no input has been typed yet.
* @return A collection of possible string values.
*
* @since 5.1
*/
@@ -70,20 +81,25 @@ public interface ValueNode<T> extends Node {
@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.
* Parses the given string into a value of type {@link T}.
*
* @param input The string to parse.
* @return The parsed value, or given exception if parsing failed.
* <p>This is typically used for values entered by a user or read from configuration text.
*
* @param input The string to parse, or {@code null} if the source value is absent.
* @return The parsed value, or a failed {@link Try} containing the parsing error.
*/
@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.
* <p>Sender-aware implementations may use permissions or other sender-specific information when converting the
* input. If no sender-aware parser is available, implementations should fall back to
* {@link #parseFromString(String)}.
*
* @param sender The sender context.
* @param input The string to parse, or {@code null} if the source value is absent.
* @return The parsed value, or a failed {@link Try} containing the parsing error.
*
* @since 5.1
*/
@@ -95,24 +111,108 @@ public interface ValueNode<T> extends Node {
/**
* Gets the serializer for this node.
*
* @return The serializer for this node.
* <p>This method exists for backward compatibility while the preferred API is
* {@link #serialize(Object)} and {@link #deserialize(Object)}.
*
* @return The serializer for this node, or {@code null} if none is configured.
*
* @deprecated Use {@link #serialize(Object)} and {@link #deserialize(Object)} instead.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable NodeSerializer<T> getSerializer();
/**
* Deserializes a raw configuration object into a value of type {@link T}.
*
* <p>The input is typically a value read from YAML or another configuration source. Implementations should
* return a sensible fallback, such as {@link #getDefaultValue()}, when the object cannot be converted.
*
* @param object The raw object to deserialize.
* @return The deserialized value, or {@code null} if the object cannot be converted and no fallback exists.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Nullable T deserialize(@Nullable Object object);
/**
* Serializes a value of type {@link T} into a configuration-friendly object.
*
* <p>The returned object should be suitable for persistence in a configuration file.
*
* @param value The value to serialize.
* @return The serialized representation, or {@code null} if the value cannot be serialized.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Nullable Object serialize(@Nullable T value);
/**
* Validates the value of this node.
*
* @param value The value to validate.
* @return An empty {@link Try} if the value is valid, or a {@link Try} containing an exception if the value is
* invalid.
* @return A successful {@link Try} if the value is valid, or a failed {@link Try} containing the validation
* exception if the value is invalid.
*/
Try<Void> validate(@Nullable T value);
/**
* Called when the value of this node is loaded by {@link BaseConfigurationHandle#load()}.
*
* <p>This callback is invoked after the value has been deserialized and validated during a configuration load.
*
* @param value The loaded value, or {@code null} if the configured value could not be resolved.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void onLoad(@Nullable T value);
/**
* Called when the value of this node is loaded or changed.
*
* <p>This callback is shared by both initial load and subsequent updates. When invoked during a load, the
* {@code oldValue} parameter will be {@code null} and the sender will be the console sender.
*
* @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 previous value, or {@code null} when called during 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)}.
*
* <p>This callback is only invoked for explicit updates after the value already exists.
*
* @param sender The sender who changed the value, or the console sender if no target sender was 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.
*
* <p>This is the legacy, senderless form of {@link #onLoadAndChange(CommandSender, Object, Object)}.
*
* @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)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
default void onSetValue(@Nullable T oldValue, @Nullable T newValue) {
onLoadAndChange(Bukkit.getConsoleSender(), oldValue, newValue);
}
}
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.config.node.functions;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import co.aikar.commands.ACFUtil;
@@ -41,13 +42,13 @@ public final class DefaultStringParserProvider {
}
private static final NodeStringParser<Enum> ENUM_STRING_PARSER = (input, type) -> Try.of(
() -> Enum.valueOf(type, input.toUpperCase()));
() -> Enum.valueOf(type, input.toUpperCase(Locale.ENGLISH)));
private static final NodeStringParser<String> STRING_STRING_PARSER = (input, type) -> Try.of(
() -> input);
private static final NodeStringParser<Boolean> BOOLEAN_STRING_PARSER = (input, type) -> Try.of(
() -> switch (String.valueOf(input).toLowerCase()) {
() -> switch (String.valueOf(input).toLowerCase(Locale.ENGLISH)) {
case "t", "true", "on", "y", "yes", "1", "allow" -> true;
case "f", "false", "off", "n", "no", "0", "deny" -> false;
default -> throw new MultiverseException("Unable to convert '" + input + "' to boolean. Please use 'true' or 'false'");
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.config.node.functions;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -43,7 +44,7 @@ public final class DefaultSuggesterProvider {
private static NodeSuggester enumSuggester(Class<?> clazz) {
return input -> Arrays.stream(clazz.getEnumConstants())
.map(Object::toString)
.map(String::toLowerCase)
.map(name -> name.toLowerCase(Locale.ENGLISH))
.collect(Collectors.toList());
}
@@ -0,0 +1,65 @@
package org.mvplugins.multiverse.core.config.node.functions;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
/**
* A callback interface for handling changes to configuration node values.
*
* @param <T> The type of the node value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@FunctionalInterface
public interface NodeChangeCallback<T> {
/**
* Called when the value of a node changes.
*
* @param oldValue The old value of the node.
* @param newValue The new value of the node.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void run(T oldValue, T newValue);
/**
* Called when the value of a node changes, with the sender of the change.
*
* @param sender The sender of the change.
* @param oldValue The old value of the node.
* @param newValue The new value of the node.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
default void run(CommandSender sender, T oldValue, T newValue) {
run(oldValue, newValue);
}
/**
* Chains another {@link NodeChangeCallback} to be executed after this one.
*
* @param after The callback to execute after this one.
* @return A new {@link NodeChangeCallback} that executes both callbacks in order.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
default NodeChangeCallback<T> then(NodeChangeCallback<T> after) {
return new SenderNodeChangeCallback<T>() {
@Override
public void run(CommandSender sender, T oldValue, T newValue) {
NodeChangeCallback.this.run(sender, oldValue, newValue);
after.run(sender, oldValue, newValue);
}
@Override
public void run(T oldValue, T newValue) {
NodeChangeCallback.this.run(oldValue, newValue);
after.run(oldValue, newValue);
}
};
}
}
@@ -0,0 +1,38 @@
package org.mvplugins.multiverse.core.config.node.functions;
import org.jetbrains.annotations.ApiStatus;
/**
* Handler called when an action is performed on a node such as loading.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@FunctionalInterface
public interface NodeValueCallback<T> {
/**
* Called when an action is performed on a node.
*
* @param value The value of the node.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void run(T value);
/**
* Chains another {@link NodeValueCallback} to be executed after this one.
*
* @param after The callback to execute after this one.
* @return A new {@link NodeValueCallback} that executes both callbacks in order.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
default NodeValueCallback<T> then(NodeValueCallback<T> after) {
return (T value) -> {
this.run(value);
after.run(value);
};
}
}
@@ -0,0 +1,31 @@
package org.mvplugins.multiverse.core.config.node.functions;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
/**
* A callback interface for handling changes to configuration node values with contextual information from the sender.
*
* @param <T> The type of the node value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@FunctionalInterface
public interface SenderNodeChangeCallback<T> extends NodeChangeCallback<T> {
/**
* {@inheritDoc}
*/
@ApiStatus.AvailableSince("5.4")
void run(CommandSender sender, T oldValue, T newValue);
/**
* {@inheritDoc}
*/
@ApiStatus.AvailableSince("5.4")
@Override
default void run(T oldValue, T newValue) {
run(Bukkit.getConsoleSender(), oldValue, newValue);
}
}
@@ -11,8 +11,11 @@ import org.jetbrains.annotations.Nullable;
* A function that parses a string into a node value object of type {@link T} with contextual information from the sender.
*
* @param <T> The type of the object to parse.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
@FunctionalInterface
public interface SenderNodeStringParser<T> extends NodeStringParser<T> {
/**
* Parses a string into a node value object of type {@link T} with contextual information from the sender.
@@ -48,12 +48,12 @@ public final class DefaultSerializerProvider {
if (type.isInstance(object)) {
return (Enum<?>) object;
}
return Enum.valueOf(type, String.valueOf(object).toUpperCase());
return Enum.valueOf(type, String.valueOf(object).toUpperCase(Locale.ENGLISH));
}
@Override
public Object serialize(Enum object, Class<Enum> type) {
return object.name().toLowerCase();
return object.name().toLowerCase(Locale.ENGLISH);
}
};
@@ -80,7 +80,7 @@ public final class DefaultSerializerProvider {
}
String input = String.valueOf(object);
//todo: this is a copy from string parser
return switch (input.toLowerCase()) {
return switch (input.toLowerCase(Locale.ENGLISH)) {
case "t", "true", "on", "y", "yes", "1", "allow" -> true;
case "f", "false", "off", "n", "no", "0", "deny" -> false;
default -> throw new RuntimeException("Unable to convert '" + input + "' to boolean.");
@@ -4,8 +4,9 @@ import io.vavr.control.Option;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
/**
@@ -79,6 +80,20 @@ public abstract class DestinationInstance<I extends DestinationInstance<I, T>,
*/
public abstract @NotNull Option<String> getFinerPermissionSuffix();
/**
* Gets a user-friendly display text for this destination instance. This is used when displaying the destination
* to the player. By default, this returns the same as {@link #toString()}. Override this method to provide a more
* user-friendly display text with colors, formatting and localization support.
*
* @return The display message.
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@NotNull
public Message getDisplayMessage() {
return Message.of(this.toString());
}
/**
* Serialises the destination instance to a savable string.
*
@@ -7,6 +7,7 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
/**
* Destination instance implementation for the {@link AnchorDestination}.
@@ -63,6 +64,15 @@ public final class AnchorDestinationInstance extends DestinationInstance<AnchorD
return Option.of(anchorName);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
//TODO Localize
return Message.of("anchor '" + anchorName + "'");
}
/**
* {@inheritDoc}
*/
@@ -6,7 +6,7 @@ import java.util.stream.Collectors;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.bukkit.Bukkit;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@@ -20,8 +20,7 @@ import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker;
/**
* {@link Destination} implementation for beds.
@@ -30,7 +29,11 @@ import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.re
public final class BedDestination implements Destination<BedDestination, BedDestinationInstance, BedDestination.InstanceFailureReason> {
static final String OWN_BED_STRING = "playerbed";
BedDestination() {
private final ConcurrentPlayerWorldTracker worldTracker;
@Inject
BedDestination(@NotNull ConcurrentPlayerWorldTracker worldTracker) {
this.worldTracker = worldTracker;
}
/**
@@ -62,8 +65,8 @@ public final class BedDestination implements Destination<BedDestination, BedDest
@Override
public @NotNull Collection<DestinationSuggestionPacket> suggestDestinations(
@NotNull CommandSender sender, @Nullable String destinationParams) {
List<DestinationSuggestionPacket> collect = Bukkit.getOnlinePlayers().stream()
.map(player -> new DestinationSuggestionPacket(this, player.getName(), player.getName()))
List<DestinationSuggestionPacket> collect = worldTracker.getOnlinePlayers().stream()
.map(player -> new DestinationSuggestionPacket(this, player, player))
.collect(Collectors.toList());
if (sender instanceof Player) {
collect.add(new DestinationSuggestionPacket(this, OWN_BED_STRING, OWN_BED_STRING));
@@ -9,12 +9,13 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
/**
* Destination instance implementation for the {@link BedDestination}.
*/
public final class BedDestinationInstance extends DestinationInstance<BedDestinationInstance, BedDestination> {
private final Player player;
private final @Nullable Player player;
/**
* Constructor.
@@ -64,6 +65,15 @@ public final class BedDestinationInstance extends DestinationInstance<BedDestina
return Option.of(player != null ? player.getName() : BedDestination.OWN_BED_STRING);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
//TODO Localize
return Message.of(player == null ? "your bed/respawn point" : player.getName() + "'s bed/respawn point");
}
/**
* {@inheritDoc}
*/
@@ -19,14 +19,15 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.entrycheck.WorldEntryCheckerProvider;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.*;
@@ -62,7 +63,12 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
* @return A new {@link ExactDestinationInstance}
*/
public @NotNull ExactDestinationInstance fromLocation(@NotNull Location location) {
return new ExactDestinationInstance(this, new UnloadedWorldLocation(location));
return new ExactDestinationInstance(
this,
worldManager,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
);
}
/**
@@ -73,12 +79,17 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
@NotNull CommandSender sender,
@NotNull String destinationParams
) {
String[] items = REPatterns.COLON.split(destinationParams);
String[] items = REPatterns.COLON.split(destinationParams, 2);
if (items.length < 2) {
if (items[0].equals("@here")) {
return getLocationFromSender(sender)
.map(location -> Attempt.<ExactDestinationInstance, InstanceFailureReason>success(
new ExactDestinationInstance(this, new UnloadedWorldLocation(location))
new ExactDestinationInstance(
this,
worldManager,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
)
))
.getOrElse(() -> Attempt.failure(InstanceFailureReason.INVALID_COORDINATES_FORMAT)); // todo: specific failure reason for this case
}
@@ -86,41 +97,21 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
}
String worldName = items[0];
String coordinates = items[1];
String[] coordinatesParams = REPatterns.COMMA.split(coordinates);
if (coordinatesParams.length != 3) {
return Attempt.failure(InstanceFailureReason.INVALID_COORDINATES_FORMAT);
}
String positionStr = items[1];
World world = getLoadedMultiverseWorld(worldName).flatMap(LoadedMultiverseWorld::getBukkitWorld).getOrNull();
if (world == null) {
return Attempt.failure(InstanceFailureReason.WORLD_NOT_FOUND, Replace.WORLD.with(worldName));
}
UnloadedWorldLocation location;
EntityPosition position;
try {
location = new UnloadedWorldLocation(
world,
Double.parseDouble(coordinatesParams[0]),
Double.parseDouble(coordinatesParams[1]),
Double.parseDouble(coordinatesParams[2])
);
} catch (NumberFormatException e) {
position = EntityPosition.fromString(positionStr);
} catch (PositionParseException e) {
return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e));
}
if (items.length == 4) {
String pitch = items[2];
String yaw = items[3];
try {
location.setPitch(Float.parseFloat(pitch));
location.setYaw(Float.parseFloat(yaw));
} catch (NumberFormatException e) {
return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e));
}
}
return Attempt.success(new ExactDestinationInstance(this, location));
return Attempt.success(new ExactDestinationInstance(this, worldManager, worldName, position));
}
//TODO: Extract to a world finder class
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.destination.core;
import io.vavr.control.Option;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
@@ -8,22 +9,34 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
/**
* Destination instance implementation for the {@link ExactDestination}.
*/
public final class ExactDestinationInstance extends DestinationInstance<ExactDestinationInstance, ExactDestination> {
private final UnloadedWorldLocation location;
private final WorldManager worldManager;
private final String worldName;
private final EntityPosition position;
/**
* Constructor.
*
* @param location The location to teleport to.
* @param destination The parent destination.
* @param worldName The name of the world.
* @param position The position in the world.
*/
ExactDestinationInstance(@NotNull ExactDestination destination, @NotNull UnloadedWorldLocation location) {
ExactDestinationInstance(@NotNull ExactDestination destination,
@NotNull WorldManager worldManager,
@NotNull String worldName,
@NotNull EntityPosition position) {
super(destination);
this.location = location;
this.worldManager = worldManager;
this.worldName = worldName;
this.position = position;
}
/**
@@ -31,10 +44,13 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
*/
@Override
public @NotNull Option<Location> getLocation(@NotNull Entity teleportee) {
if (location.getWorld() == null) {
World world = Bukkit.getWorld(worldName);
if (world == null) {
return Option.none();
}
return Option.of(location.toBukkitLocation());
Location destinationLocation = position.toBukkitLocation(teleportee.getLocation());
destinationLocation.setWorld(world);
return Option.of(destinationLocation);
}
/**
@@ -58,7 +74,18 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
*/
@Override
public @NotNull Option<String> getFinerPermissionSuffix() {
return Option.of(location.getWorld()).map(World::getName);
return Option.of(worldName);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
String displayWorldName = worldManager.getWorld(worldName)
.map(MultiverseWorld::getAliasOrName)
.getOrElse(worldName);
return Message.of(displayWorldName + " at " + position.toString());
}
/**
@@ -66,7 +93,6 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
*/
@Override
public @NotNull String serialise() {
return location.getWorldName() + ":" + location.getX() + "," + location.getY()
+ "," + location.getZ() + ":" + location.getPitch() + ":" + location.getYaw();
return worldName + ":" + position.toString();
}
}
@@ -4,7 +4,7 @@ import java.util.Collection;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.bukkit.Bukkit;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@@ -14,21 +14,26 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker;
/**
* {@link Destination} implementation for players.s
*/
@Service
public final class PlayerDestination implements Destination<PlayerDestination, PlayerDestinationInstance, PlayerDestination.InstanceFailureReason> {
private final ConcurrentPlayerWorldTracker playerWorldTracker;
/**
* Creates a new instance of the PlayerDestination.
*/
PlayerDestination() {
@Inject
PlayerDestination(@NotNull ConcurrentPlayerWorldTracker playerWorldTracker) {
this.playerWorldTracker = playerWorldTracker;
}
/**
@@ -60,8 +65,8 @@ public final class PlayerDestination implements Destination<PlayerDestination, P
@Override
public @NotNull Collection<DestinationSuggestionPacket> suggestDestinations(
@NotNull CommandSender sender, @Nullable String destinationParams) {
return Bukkit.getOnlinePlayers().stream()
.map(p -> new DestinationSuggestionPacket(this, p.getName(), p.getName()))
return playerWorldTracker.getOnlinePlayers().stream()
.map(player -> new DestinationSuggestionPacket(this, player, player))
.toList();
}
@@ -9,6 +9,7 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
import java.util.UUID;
@@ -66,6 +67,14 @@ public final class PlayerDestinationInstance extends DestinationInstance<PlayerD
return Option.of(playerName);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
return Message.of(playerName);
}
/**
* {@inheritDoc}
*/
@@ -6,6 +6,7 @@ import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import jakarta.inject.Inject;
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;
@@ -14,13 +15,11 @@ 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.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.teleportation.LocationManipulation;
import org.mvplugins.multiverse.core.utils.REPatterns;
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.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.entrycheck.WorldEntryCheckerProvider;
@@ -56,6 +55,19 @@ public final class WorldDestination implements Destination<WorldDestination, Wor
return "w";
}
/**
* Directly create a world destination instance for a Multiverse World without any string parsing validation needed.
*
* @param multiverseWorld The destination's target world.
* @return The destination instance with the target world.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull WorldDestinationInstance fromWorld(MultiverseWorld multiverseWorld) {
return new WorldDestinationInstance(this, multiverseWorld, null, -1.0f);
}
/**
* {@inheritDoc}
*/
@@ -8,12 +8,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
/**
* Destination instance implementation for the {@link WorldDestination}.
*/
@@ -87,6 +84,14 @@ public final class WorldDestinationInstance extends DestinationInstance<WorldDes
return Option.of(world.getName());
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
return Message.of(world.getAliasOrName());
}
/**
* {@inheritDoc}
*/
@@ -35,7 +35,7 @@ public final class DynamicListenerRegistration {
private static final boolean hasEventExecutorCreate;
static {
hasEventExecutorCreate = ReflectHelper.getMethod(EventExecutor.class, "create", Method.class, Class.class) != null;
hasEventExecutorCreate = ReflectHelper.hasMethod(EventExecutor.class, "create", Method.class, Class.class);
}
private final EventPriorityMapper eventPriorityMapper;
@@ -98,18 +98,22 @@ public final class DynamicListenerRegistration {
}
method.setAccessible(true);
Object methodOutput = ReflectHelper.invokeMethod(listener, method);
if (!(methodOutput instanceof EventRunnable eventRunnable)) {
Logging.warning("Event method %s in %s did not return a SingleEventListener",
method.getName(), listener.getClass().getName());
return;
}
EventExecutor executor = new EventRunnableExecutor<>(eventClass, eventRunnable);
EventPriority priority = getDynamicEventPriority(method);
boolean ignoreCancelled = isIgnoreIfCancelled(method);
Logging.finest("Registering dynamic event for %s with priority %s", eventClass.getName(), priority);
Bukkit.getPluginManager().registerEvent(eventClass, listener, priority, executor, plugin, ignoreCancelled);
ReflectHelper.tryInvokeMethod(listener, method)
.onFailure(e -> Logging.warning("Failed to invoke event method %s in %s: $s",
method.getName(), listener.getClass().getName(), e.getMessage()))
.filter(EventRunnable.class::isInstance)
.onFailure(e -> Logging.warning("Event method %s in %s did not return an EventRunnable instance!",
method.getName(), listener.getClass().getName()))
.map(EventRunnable.class::cast)
.map(eventRunnable -> new EventRunnableExecutor<>(eventClass, eventRunnable))
.peek(executor -> {
EventPriority priority = getDynamicEventPriority(method);
boolean ignoreCancelled = isIgnoreIfCancelled(method);
Bukkit.getPluginManager().registerEvent(eventClass, listener, priority, executor, plugin, ignoreCancelled);
Logging.finest("Registered dynamic event: %s, priority: %s, ignore if cancelled: %s",
eventClass.getName(), priority, ignoreCancelled);
});
}
private Class<? extends Event> getEventClass(Method method) {
@@ -129,13 +133,12 @@ public final class DynamicListenerRegistration {
}
private Class<? extends Event> getEventClassFromString(String className) {
Class<?> annotatedClass = ReflectHelper.getClass(className);
if (annotatedClass == null || !Event.class.isAssignableFrom(annotatedClass)) {
// Usually means the server software used did not implement the event
Logging.fine("Event class does not exist: %s", className);
return null;
}
return annotatedClass.asSubclass(Event.class);
return ReflectHelper.tryGetClass(className)
.onFailure(throwable -> Logging.fine("Failed to find event class: %s", className))
.filter(Event.class::isAssignableFrom)
.onFailure(throwable -> Logging.warning("Class is not an Event: %s", className))
.map(clazz -> (Class<? extends Event>) clazz.asSubclass(Event.class))
.getOrNull();
}
private EventPriority getDynamicEventPriority(Method method) {
@@ -7,6 +7,7 @@ import java.io.FileReader;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import com.dumptruckman.minecraft.util.Logging;
@@ -24,7 +25,7 @@ public final class MVDumpsDebugInfoEvent extends Event {
public MVDumpsDebugInfoEvent() {
debugInfoBuilder = new StringBuilder();
detailedDebugInfo = new HashMap<>();
detailedDebugInfo = new LinkedHashMap<>();
}
private static final HandlerList HANDLERS = new HandlerList();
@@ -1,8 +1,11 @@
package org.mvplugins.multiverse.core.event.world;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
/**
* Called when a world has been successfully cloned.
@@ -10,14 +13,24 @@ import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
public final class MVWorldClonedEvent extends MultiverseWorldEvent<LoadedMultiverseWorld> {
private static final HandlerList HANDLERS = new HandlerList();
private final LoadedMultiverseWorld fromWorld;
private final MultiverseWorld fromWorld;
public MVWorldClonedEvent(LoadedMultiverseWorld world, LoadedMultiverseWorld fromWorld) {
public MVWorldClonedEvent(LoadedMultiverseWorld world, MultiverseWorld fromWorld) {
super(world);
this.fromWorld = fromWorld;
}
public LoadedMultiverseWorld getFromWorld() {
/**
* @deprecated Cloning can be done from unloaded worlds as well. Use {@link #getSourceWorld()} instead.
*/
@Deprecated(forRemoval = true, since = "5.6")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public @Nullable LoadedMultiverseWorld getFromWorld() {
return fromWorld.asLoadedWorld().getOrNull();
}
@ApiStatus.AvailableSince("5.6")
public @NotNull MultiverseWorld getSourceWorld() {
return fromWorld;
}
@@ -37,5 +50,4 @@ public final class MVWorldClonedEvent extends MultiverseWorldEvent<LoadedMultive
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -1,6 +1,8 @@
package org.mvplugins.multiverse.core.exceptions.command;
import co.aikar.commands.InvalidCommandArgument;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.message.LocalizableMessage;
import org.mvplugins.multiverse.core.locale.message.LocalizedMessage;
import org.mvplugins.multiverse.core.locale.message.Message;
@@ -9,6 +11,18 @@ import org.mvplugins.multiverse.core.locale.message.Message;
*/
public class MVInvalidCommandArgument extends InvalidCommandArgument {
@ApiStatus.AvailableSince("5.7")
public static MVInvalidCommandArgument causeBy(Throwable throwable) {
return causeBy(throwable, true);
}
@ApiStatus.AvailableSince("5.7")
public static MVInvalidCommandArgument causeBy(Throwable throwable, boolean showSyntax) {
return (throwable instanceof LocalizableMessage localizableMessage)
? of(localizableMessage.getLocalizableMessage(), showSyntax)
: new MVInvalidCommandArgument(throwable.getLocalizedMessage(), showSyntax);
}
public static MVInvalidCommandArgument of(Message message) {
return of(message, true);
}
@@ -19,6 +33,10 @@ public class MVInvalidCommandArgument extends InvalidCommandArgument {
: new MVInvalidCommandArgument(message, showSyntax);
}
private MVInvalidCommandArgument(String message, boolean showSyntax) {
super(message, showSyntax);
}
private MVInvalidCommandArgument(Message message, boolean showSyntax) {
super(message.formatted(), showSyntax);
}
@@ -0,0 +1,23 @@
package org.mvplugins.multiverse.core.exceptions.utils.position;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.locale.message.Message;
public class PositionParseException extends MultiverseException {
public PositionParseException(String message) {
super(message);
}
public PositionParseException(@Nullable Message message) {
super(message);
}
public PositionParseException(@Nullable String message, @Nullable Throwable cause) {
super(message, cause);
}
public PositionParseException(@Nullable Message message, @Nullable Throwable cause) {
super(message, cause);
}
}
@@ -16,6 +16,7 @@ import org.mvplugins.multiverse.core.dynamiclistener.annotations.IgnoreIfCancell
import org.mvplugins.multiverse.core.dynamiclistener.annotations.SkipIfEventExist;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker;
/**
* Multiverse's Listener for players.
@@ -24,16 +25,17 @@ import org.mvplugins.multiverse.core.world.WorldManager;
final class MVChatListener implements CoreListener {
private final CoreConfig config;
private final WorldManager worldManager;
private final MVPlayerListener playerListener;
private final ConcurrentPlayerWorldTracker playerWorldTracker;
@Inject
MVChatListener(
CoreConfig config,
WorldManager worldManager,
MVPlayerListener playerListener) {
ConcurrentPlayerWorldTracker playerWorldTracker
) {
this.config = config;
this.worldManager = worldManager;
this.playerListener = playerListener;
this.playerWorldTracker = playerWorldTracker;
}
@EventClass("io.papermc.paper.event.player.AsyncChatEvent")
@@ -94,12 +96,9 @@ final class MVChatListener implements CoreListener {
}
private String getWorldName(Player player) {
String world = playerListener.getPlayerWorld().get(player.getName());
if (world == null) {
world = player.getWorld().getName();
playerListener.getPlayerWorld().put(player.getName(), world);
}
return this.worldManager.getLoadedWorld(world)
String worldName = playerWorldTracker.getPlayerWorld(player.getName())
.getOrElse(() -> player.getWorld().getName());
return this.worldManager.getLoadedWorld(worldName)
.map(mvworld -> mvworld.isHidden() ? "" : mvworld.getAliasOrName())
.getOrElse("");
}
@@ -7,11 +7,11 @@
package org.mvplugins.multiverse.core.listeners;
import com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent;
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;
@@ -22,6 +22,8 @@ import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.dynamiclistener.EventRunnable;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventClass;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -43,7 +45,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 +67,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;
}
@@ -78,13 +80,44 @@ final class MVEntityListener implements CoreListener {
});
}
/**
* Fired before other spawn checks is done, helps in performance by cancelling early and preventing unnecessary
* checks for spawn reasons that are not allowed in the world.
*
* @return Event wrapper
*/
@EventClass("com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent")
EventRunnable<?> preCreatureSpawn() {
return new EventRunnable<PreCreatureSpawnEvent>() {
@Override
public void onEvent(PreCreatureSpawnEvent event) {
// Always allow custom command and plugins to spawn creatures
if (event.getReason() == SpawnReason.CUSTOM
|| event.getReason() == SpawnReason.COMMAND
|| event.getReason() == SpawnReason.BREEDING
|| event.getReason() == SpawnReason.SPAWNER_EGG) {
return;
}
worldManager.getLoadedWorld(event.getSpawnLocation().getWorld())
.peek(world -> {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getType())) {
Logging.finest("Cancelling Pre Creature Spawn Event for: " + event.getType());
event.setCancelled(true);
event.setShouldAbortSpawn(true);
}
});
}
};
}
/**
* Handle Spawn Category settings.
*
* @param event The event.
*/
@EventMethod
public void creatureSpawn(CreatureSpawnEvent event) {
void creatureSpawn(CreatureSpawnEvent event) {
if (event.isCancelled()) {
return;
}
@@ -110,7 +143,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;
@@ -7,9 +7,7 @@
package org.mvplugins.multiverse.core.listeners;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
@@ -23,6 +21,7 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventPriority;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerPortalEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
@@ -32,16 +31,17 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.dynamiclistener.EventRunnable;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventClass;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventPriorityKey;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.IgnoreIfCancelled;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.event.MVRespawnEvent;
import org.mvplugins.multiverse.core.locale.PluginLocales;
import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker;
import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter;
import org.mvplugins.multiverse.core.teleportation.BlockSafety;
import org.mvplugins.multiverse.core.teleportation.TeleportQueue;
import org.mvplugins.multiverse.core.utils.result.ResultChain;
@@ -52,7 +52,6 @@ import org.mvplugins.multiverse.core.world.entrycheck.EntryFeeResult;
import org.mvplugins.multiverse.core.world.entrycheck.WorldEntryCheckerProvider;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder;
import org.mvplugins.multiverse.core.world.helpers.EnforcementHandler;
import org.spigotmc.event.player.PlayerSpawnLocationEvent;
/**
* Multiverse's Listener for players.
@@ -72,8 +71,7 @@ final class MVPlayerListener implements CoreListener {
private final EnforcementHandler enforcementHandler;
private final DimensionFinder dimensionFinder;
private final CorePermissionsChecker corePermissionsChecker;
private final Map<String, String> playerWorld = new ConcurrentHashMap<>();
private final AsyncSafetyTeleporter asyncSafetyTeleporter;
@Inject
MVPlayerListener(
@@ -89,7 +87,8 @@ final class MVPlayerListener implements CoreListener {
DestinationsProvider destinationsProvider,
EnforcementHandler enforcementHandler,
DimensionFinder dimensionFinder,
CorePermissionsChecker corePermissionsChecker) {
CorePermissionsChecker corePermissionsChecker,
AsyncSafetyTeleporter asyncSafetyTeleporter) {
this.plugin = plugin;
this.config = config;
this.worldManagerProvider = worldManagerProvider;
@@ -103,6 +102,7 @@ final class MVPlayerListener implements CoreListener {
this.enforcementHandler = enforcementHandler;
this.dimensionFinder = dimensionFinder;
this.corePermissionsChecker = corePermissionsChecker;
this.asyncSafetyTeleporter = asyncSafetyTeleporter;
}
private WorldManager getWorldManager() {
@@ -113,19 +113,10 @@ final class MVPlayerListener implements CoreListener {
return commandManagerProvider.get();
}
public PluginLocales getLocales() {
private PluginLocales getLocales() {
return getCommandManager().getLocales();
}
/**
* Gets the map of player and the world name they are in.
*
* @return the playerWorld-map
*/
public Map<String, String> getPlayerWorld() {
return playerWorld;
}
/**
* This method is called when a player respawns.
*
@@ -134,7 +125,7 @@ final class MVPlayerListener implements CoreListener {
@EventMethod
@EventPriorityKey("mvcore-player-respawn")
@DefaultEventPriority(EventPriority.LOW)
public void playerRespawn(PlayerRespawnEvent event) {
void playerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer();
LoadedMultiverseWorld mvWorld = getWorldManager().getLoadedWorld(player.getWorld()).getOrNull();
if (mvWorld == null) {
@@ -201,68 +192,63 @@ final class MVPlayerListener implements CoreListener {
return Option.of(mvWorld.getSpawnLocation());
}
@EventClass("org.spigotmc.event.player.PlayerSpawnLocationEvent")
@EventMethod
// TODO: Consider if this key is needed anymore, need to remove from config.yml as well
@EventPriorityKey("mvcore-player-spawn-location")
EventRunnable playerSpawnLocation() {
return new EventRunnable<PlayerSpawnLocationEvent>() {
@Override
public void onEvent(PlayerSpawnLocationEvent event) {
Player player = event.getPlayer();
MultiverseWorld world = getWorldManager().getLoadedWorld(player.getWorld()).getOrNull();
if (world == null) {
Logging.finer("Player joined in a world that is not managed by Multiverse.");
return;
}
if (!player.hasPlayedBefore()) {
handleFirstSpawn(event);
} else {
handleJoinLocation(event);
}
handleGameModeAndFlight(player, event.getSpawnLocation().getWorld());
}
void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
if (player.hasPlayedBefore()) {
handleJoinLocation(player);
} else {
handleFirstSpawn(player);
}
handleGameModeAndFlight(player, player.getWorld());
}
private void handleFirstSpawn(PlayerSpawnLocationEvent event) {
if (!config.getFirstSpawnOverride()) {
Logging.finer("FirstSpawnOverride is disabled");
// User has disabled the feature in config
return;
}
Logging.fine("Moving NEW player to(firstspawnoverride): %s", config.getFirstSpawnLocation());
destinationsProvider.parseDestination(config.getFirstSpawnLocation())
.map(destination -> destination.getLocation(event.getPlayer())
.peek(event::setSpawnLocation)
.onEmpty(() -> Logging.warning("The destination in FirstSpawnLocation in config is invalid")))
.onFailure(failure -> {
Logging.warning("Invalid destination in FirstSpawnLocation in config: %s");
Logging.warning(failure.getFailureMessage().formatted(getLocales()));
});
}
private void handleFirstSpawn(Player player) {
if (!config.getFirstSpawnOverride()) {
Logging.finer("FirstSpawnOverride is disabled");
// User has disabled the feature in config
return;
}
Logging.fine("Moving NEW player to(firstspawnoverride): %s", config.getFirstSpawnLocation());
destinationsProvider.parseDestination(config.getFirstSpawnLocation())
.peek(destination -> teleportToDestinationOnJoin(player, destination))
.onFailure(failure -> {
Logging.warning("Invalid destination in FirstSpawnLocation in config: %s");
Logging.warning(failure.getFailureMessage().formatted(getLocales()));
});
}
private void handleJoinLocation(PlayerSpawnLocationEvent event) {
if (!config.getEnableJoinDestination()) {
Logging.finer("JoinDestination is disabled");
// User has disabled the feature in config
return;
}
if (config.getJoinDestination().isBlank()) {
Logging.warning("Joindestination is enabled but no destination has been specified in config!");
return;
}
if (corePermissionsChecker.hasJoinLocationBypassPermission(event.getPlayer())) {
Logging.finer("Player %s has bypass permission for JoinDestination", event.getPlayer().getName());
return;
}
Logging.finer("JoinDestination is " + config.getJoinDestination());
destinationsProvider.parseDestination(config.getJoinDestination())
.map(destination -> destination.getLocation(event.getPlayer())
.peek(event::setSpawnLocation)
.onEmpty(() -> Logging.warning("The destination in JoinDestination in config is invalid")))
.onFailure(failure -> {
Logging.warning("Invalid destination in JoinDestination in config: %s");
Logging.warning(failure.getFailureMessage().formatted(getLocales()));
});
}
};
private void handleJoinLocation(Player player) {
if (!config.getEnableJoinDestination()) {
Logging.finer("JoinDestination is disabled");
// User has disabled the feature in config
return;
}
if (config.getJoinDestination().isBlank()) {
Logging.warning("Joindestination is enabled but no destination has been specified in config!");
return;
}
if (corePermissionsChecker.hasJoinLocationBypassPermission(player)) {
Logging.finer("Player %s has bypass permission for JoinDestination", player.getName());
return;
}
Logging.finer("JoinDestination is " + config.getJoinDestination());
destinationsProvider.parseDestination(config.getJoinDestination())
.peek(destination -> teleportToDestinationOnJoin(player, destination))
.onFailure(failure -> {
Logging.warning("Invalid destination in JoinDestination in config: %s");
Logging.warning(failure.getFailureMessage().formatted(getLocales()));
});
}
private void teleportToDestinationOnJoin(Player player, DestinationInstance<?, ?> destination) {
asyncSafetyTeleporter.to(destination).passengerMode(config.getPassengerMode()).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)));
}
/**
@@ -270,11 +256,10 @@ final class MVPlayerListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
@DefaultEventPriority(EventPriority.MONITOR)
public void playerChangedWorld(PlayerChangedWorldEvent event) {
// Permissions now determine whether or not to handle a gamemode.
@EventPriorityKey("mvcore-player-changed-world")
@DefaultEventPriority(EventPriority.NORMAL)
void playerChangedWorld(PlayerChangedWorldEvent event) {
this.handleGameModeAndFlight(event.getPlayer(), event.getPlayer().getWorld());
playerWorld.put(event.getPlayer().getName(), event.getPlayer().getWorld().getName());
}
/**
@@ -284,13 +269,29 @@ final class MVPlayerListener implements CoreListener {
@EventMethod
@EventPriorityKey("mvcore-player-teleport")
@DefaultEventPriority(EventPriority.HIGHEST)
public void playerTeleport(PlayerTeleportEvent event) {
Logging.finer("Got teleport event for player '"
+ event.getPlayer().getName() + "' with cause '" + event.getCause() + "'");
void playerTeleport(PlayerTeleportEvent event) {
Player teleportee = event.getPlayer();
if (event.isCancelled()) {
Logging.finer("Teleport event for player '" + teleportee.getName() + "' was already cancelled.");
return;
}
Player teleportee = event.getPlayer();
Logging.finer("Got teleport event for player '"
+ teleportee.getName() + "' with cause '" + event.getCause() + "'");
Location fromLocation = event.getFrom();
if (fromLocation == null || fromLocation.getWorld() == null) { // should never be null, but just in case
Logging.finer("Teleport event for player '" + teleportee.getName() + "' has null from-location or world.");
return;
}
Location toLocation = event.getTo();
if (toLocation == null || toLocation.getWorld() == null) { // may be null on spigot
Logging.finer("Teleport event for player '" + teleportee.getName() + "' has null to-location or world.");
return;
}
Option<String> teleporterName = teleportQueue.popFromQueue(teleportee.getName());
CommandSender teleporter = teleporterName.map(name -> {
if (name.equalsIgnoreCase("CONSOLE")) {
@@ -311,17 +312,17 @@ final class MVPlayerListener implements CoreListener {
}
Logging.finer("Teleporter %s is teleporting %s from %s to %s", teleporter.getName(), teleportee.getName(),
event.getFrom(), event.getTo());
fromLocation, toLocation);
MultiverseWorld fromWorld = getWorldManager().getLoadedWorld(event.getFrom().getWorld()).getOrNull();
LoadedMultiverseWorld toWorld = getWorldManager().getLoadedWorld(event.getTo().getWorld()).getOrNull();
MultiverseWorld fromWorld = getWorldManager().getLoadedWorld(fromLocation.getWorld()).getOrNull();
LoadedMultiverseWorld toWorld = getWorldManager().getLoadedWorld(toLocation.getWorld()).getOrNull();
if (toWorld == null) {
Logging.fine("Player '" + teleportee.getName() + "' is teleporting to world '"
+ event.getTo().getWorld().getName() + "' which is not managed by Multiverse-Core. No further "
+ toLocation.getWorld().getName() + "' which is not managed by Multiverse-Core. No further "
+ "actions will be taken by Multiverse-Core.");
return;
}
if (event.getFrom().getWorld().equals(event.getTo().getWorld())) {
if (fromLocation.getWorld().equals(toLocation.getWorld())) {
// The player is Teleporting to the same world.
Logging.finer("Player '" + teleportee.getName() + "' is teleporting to the same world.");
this.stateSuccess(teleportee.getName(), toWorld.getName());
@@ -355,20 +356,19 @@ final class MVPlayerListener implements CoreListener {
*/
@EventMethod
@DefaultEventPriority(EventPriority.LOWEST)
public void playerPortalCheck(PlayerPortalEvent event) {
if (event.isCancelled()) {
return;
}
if (event.getFrom().getWorld() == null) {
@IgnoreIfCancelled
void playerPortalCheck(PlayerPortalEvent event) {
Location fromLocation = event.getFrom();
if (fromLocation == null || fromLocation.getWorld() == null) {
Logging.warning("PlayerPortalEvent's from world is null!");
return;
}
if (event.getFrom().getWorld().getBlockAt(event.getFrom()).getType() == Material.NETHER_PORTAL) {
if (fromLocation.getWorld().getBlockAt(fromLocation).getType() == Material.NETHER_PORTAL) {
return;
}
// Player was actually outside of the portal, adjust the from location
Location newLocation = blockSafety.findPortalBlockNextTo(event.getFrom());
Location newLocation = blockSafety.findPortalBlockNextTo(fromLocation);
if (newLocation != null) {
event.setFrom(newLocation);
}
@@ -381,28 +381,33 @@ final class MVPlayerListener implements CoreListener {
@EventMethod
@EventPriorityKey("mvcore-player-portal")
@DefaultEventPriority(EventPriority.HIGH)
public void playerPortal(PlayerPortalEvent event) {
if (event.isCancelled()) {
@IgnoreIfCancelled
void playerPortal(PlayerPortalEvent event) {
Location fromLocation = event.getFrom();
if (fromLocation == null || fromLocation.getWorld() == null) { // should never be null, but just in case
Logging.finer("PlayerPortalEvent's from world is null!");
return;
}
if (event.getTo() == null || event.getTo().getWorld() == null) {
Location toLocation = event.getTo();
if (toLocation == null || toLocation.getWorld() == null) { // may be null on spigot
Logging.finer("PlayerPortalEvent's to world is null!");
return;
}
if (config.isUsingCustomPortalSearch()) {
event.setSearchRadius(config.getCustomPortalSearchRadius());
}
if (Objects.equals(event.getFrom().getWorld(), event.getTo().getWorld())) {
if (Objects.equals(fromLocation.getWorld(), toLocation.getWorld())) {
// The player is Portaling to the same world.
Logging.finer("Player '" + event.getPlayer().getName() + "' is portaling to the same world.");
return;
}
MultiverseWorld fromWorld = getWorldManager().getLoadedWorld(event.getFrom().getWorld()).getOrNull();
LoadedMultiverseWorld toWorld = getWorldManager().getLoadedWorld(event.getTo().getWorld()).getOrNull();
MultiverseWorld fromWorld = getWorldManager().getLoadedWorld(fromLocation.getWorld()).getOrNull();
LoadedMultiverseWorld toWorld = getWorldManager().getLoadedWorld(toLocation.getWorld()).getOrNull();
if (toWorld == null) {
Logging.fine("Player '" + event.getPlayer().getName() + "' is portaling to world '"
+ event.getTo().getWorld().getName() + "' which is not managed by Multiverse-Core. No further "
+ toLocation.getWorld().getName() + "' which is not managed by Multiverse-Core. No further "
+ "actions will be taken by Multiverse-Core.");
return;
}
@@ -416,21 +421,31 @@ final class MVPlayerListener implements CoreListener {
}
/**
* Handles the gamemode for the specified {@link Player}.
* Handles the gamemode for the specified {@link Player}. Delays the enforcement if configured to do so
* to ensure multiverse has final say over other plugins changing gamemodes.
*
* @param player The {@link Player}.
* @param world The {@link World} the player is supposed to be in.
*/
private void handleGameModeAndFlight(final Player player, World world) {
// We perform this task one tick later to MAKE SURE that the player actually reaches the
// destination world, otherwise we'd be changing the player mode if they havent moved anywhere.
this.server.getScheduler().runTaskLater(this.plugin, () -> {
if (!player.isOnline() || !player.getWorld().equals(world)) {
return;
}
Logging.finer("Handling gamemode and flight for player %s in world '%s'", player.getName(), world.getName());
enforcementHandler.handleFlightEnforcement(player);
enforcementHandler.handleGameModeEnforcement(player);
}, 1L);
if (config.getGamemodeAndFlightEnforceDelay() <= 0) {
doGameModeAndFlightEnforcement(player, world);
return;
}
server.getScheduler().runTaskLater(
this.plugin,
() -> doGameModeAndFlightEnforcement(player, world),
config.getGamemodeAndFlightEnforceDelay()
);
}
private void doGameModeAndFlightEnforcement(Player player, World world) {
if (!player.isOnline() || !player.getWorld().equals(world)) {
Logging.finer("Player %s is no longer online or not in the expected world '%s'", player.getName(), world.getName());
return;
}
Logging.finer("Handling gamemode and flight for player %s in world '%s'", player.getName(), world.getName());
enforcementHandler.handleFlightEnforcement(player);
enforcementHandler.handleGameModeEnforcement(player);
}
}
@@ -12,16 +12,17 @@ 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;
import org.bukkit.event.world.PortalCreateEvent;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.IgnoreIfCancelled;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.world.WorldManager;
import static org.bukkit.PortalType.CUSTOM;
@@ -34,11 +35,15 @@ final class MVPortalListener implements CoreListener {
private final CoreConfig config;
private final WorldManager worldManager;
private final MVCommandManager commandManager;
@Inject
MVPortalListener(@NotNull CoreConfig config, @NotNull WorldManager worldManager) {
MVPortalListener(@NotNull CoreConfig config,
@NotNull WorldManager worldManager,
@NotNull MVCommandManager commandManager) {
this.config = config;
this.worldManager = worldManager;
this.commandManager = commandManager;
}
/**
@@ -48,7 +53,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());
@@ -60,6 +65,7 @@ final class MVPortalListener implements CoreListener {
if (!world.getPortalForm().isPortalAllowed(targetType)) {
Logging.fine("Cancelling creation of %s portal because portalForm disallows.", targetType);
event.setCancelled(true);
commandManager.getCommandIssuer(event.getEntity()).sendError(MVCorei18n.PORTALFORM_DISABLED_NETHER);
}
}).onEmpty(() ->
Logging.fine("World '%s' is not managed by Multiverse! Ignoring at PortalCreateEvent.",
@@ -93,7 +99,7 @@ final class MVPortalListener implements CoreListener {
*/
@EventMethod
@IgnoreIfCancelled
public void playerInteract(PlayerInteractEvent event) {
void playerInteract(PlayerInteractEvent event) {
if (isCreateEndPortalInteraction(event)) {
return;
}
@@ -102,6 +108,7 @@ final class MVPortalListener implements CoreListener {
if (!world.getPortalForm().isPortalAllowed(PortalType.ENDER)) {
Logging.fine("Cancelling creation of ENDER portal because portalForm disallows.");
event.setCancelled(true);
commandManager.getCommandIssuer(event.getPlayer()).sendError(MVCorei18n.PORTALFORM_DISABLED_END);
}
}).onEmpty(() ->
Logging.fine("World '%s' is not managed by Multiverse! Ignoring at PlayerInteractEvent.",
@@ -122,7 +129,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;
}
@@ -91,6 +91,7 @@ public enum MVCorei18n implements MessageKeyProvider {
// /mv dumps
DUMPS_DESCRIPTION,
DUMPS_STARTING,
DUMPS_URL_LIST,
// /mv gamerule set
@@ -146,6 +147,19 @@ public enum MVCorei18n implements MessageKeyProvider {
LOAD_LOADING,
LOAD_SUCCESS,
// /mv meta info
META_INFO_DESCRIPTION,
META_INFO_WORLD,
META_INFO_HEADER,
META_INFO_NOCONTENT,
// /mv meta modify
META_MODIFY_SET_SUCCESS,
META_MODIFY_SET_FAILURE,
META_MODIFY_REMOVE_SUCCESS,
META_MODIFY_REMOVE_FAILURE,
META_MODIFY_INVALIDACTION,
// /mv modify
MODIFY_DESCRIPTION,
MODIFY_WORLD_DESCRIPTION,
@@ -250,6 +264,8 @@ public enum MVCorei18n implements MessageKeyProvider {
WORLDBORDER_WARNINGTIME_NOTHINGCHANGED,
WORLDBORDER_WARNINGTIME_SUCCESS,
WORLDBORDER_ROUNDOFF_WARNING,
// commands error
COMMANDS_ERROR_PLAYERSONLY,
COMMANDS_ERROR_MULTIVERSEWORLDONLY,
@@ -278,6 +294,10 @@ public enum MVCorei18n implements MessageKeyProvider {
DESTINATION_SHARED_FAILUREREASON_WORLDNOTFOUND,
DESTINATION_PARSE_FAILUREREASON_INVALIDDESTINATIONID,
// portal form disabled
PORTALFORM_DISABLED_END,
PORTALFORM_DISABLED_NETHER,
// teleport failure reason
TELEPORTFAILUREREASON_NULL_DESTINATION,
TELEPORTFAILUREREASON_NULL_LOCATION,
@@ -292,6 +312,7 @@ public enum MVCorei18n implements MessageKeyProvider {
CLONEWORLD_WORLDEXISTFOLDER,
CLONEWORLD_WORLDEXISTUNLOADED,
CLONEWORLD_WORLDEXISTLOADED,
CLONEWORLD_FROMWORLDFOLDERINVALID,
CLONEWORLD_COPYFAILED,
CREATEWORLD_INVALIDWORLDNAME,
@@ -309,12 +330,14 @@ public enum MVCorei18n implements MessageKeyProvider {
IMPORTWORLD_WORLDEXISTLOADED,
IMPORTWORLD_WORLDFOLDERINVALID,
IMPORTWORLD_BUKKITENVIRONMENTMISMATCH,
IMPORTWORLD_BUKKITNAMESPACEDKEYMISMATCH,
LOADWORLD_WORLDALREADYLOADING,
LOADWORLD_WORLDNONEXISTENT,
LOADWORLD_WORLDEXISTFOLDER,
LOADWORLD_WORLDEXISTLOADED,
LOADWORLD_BUKKITENVIRONMENTMISMATCH,
LOADWORLD_BUKKITNAMESPACEDKEYMISMATCH,
REMOVEWORLD_WORLDNONEXISTENT,
@@ -346,6 +369,17 @@ public enum MVCorei18n implements MessageKeyProvider {
EXCEPTION_MULTIVERSEWORLD_UNLOADPLAYERSINWORLD,
EXCEPTION_MULTIVERSEWORLD_UNLOADERROR,
// multiverse position parse exception
EXCEPTION_POSITIONPARSE_INVALIDDIRECTION,
EXCEPTION_POSITIONPARSE_INVALIDCOORDINATES,
EXCEPTION_POSITIONPARSE_INVALIDNUMBER,
// world key parse failures
WORLDKEYPARSE_EMPTY,
WORLDKEYPARSE_INVALIDWORLDNAME,
WORLDKEYPARSE_INVALIDNAMESPACEDKEY,
WORLDKEYPARSE_NAMESPACEDKEYUNSUPPORTED,
// generic
GENERIC_SUCCESS,
GENERIC_FAILURE,
@@ -27,39 +27,12 @@ public final class LocalizedMessage extends Message implements MessageKeyProvide
return messageKeyProvider.getMessageKey();
}
@Override
public @NotNull String[] getReplacements(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
return toReplacementsArray(locales, commandIssuer, replacements);
}
@Override
public @NotNull String formatted(@NotNull CommandIssuer commandIssuer) {
return formatted(commandIssuer.getManager().getLocales(), commandIssuer);
}
@Override
public @NotNull String formatted(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
Objects.requireNonNull(locales, "locales must not be null");
String[] parsedReplacements = getReplacements(locales, commandIssuer);
if (parsedReplacements.length == 0) {
return locales.getMessage(commandIssuer, getMessageKey());
}
return ACFUtil.replaceStrings(locales.getMessage(commandIssuer, getMessageKey()), parsedReplacements);
}
private static String[] toReplacementsArray(
@NotNull Locales locales,
@Nullable CommandIssuer commandIssuer,
@NotNull MessageReplacement... replacements) {
String[] replacementsArray = new String[replacements.length * 2];
int i = 0;
for (MessageReplacement replacement : replacements) {
replacementsArray[i++] = replacement.getKey();
replacementsArray[i++] = replacement.getReplacement().fold(
str -> str,
message -> message.formatted(locales, commandIssuer));
}
return replacementsArray;
}
}
@@ -103,7 +103,7 @@ public sealed class Message permits LocalizedMessage {
* @return The replacements
*/
public @NotNull String[] getReplacements(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
return getReplacements();
return toReplacementsArray(locales, commandIssuer, replacements);
}
/**
@@ -152,7 +152,7 @@ public sealed class Message permits LocalizedMessage {
* @return The formatted, localized message
*/
public @NotNull String formatted(@NotNull CommandIssuer commandIssuer) {
return formatted();
return formatted(commandIssuer.getManager().getLocales(), commandIssuer);
}
/**
@@ -166,7 +166,11 @@ public sealed class Message permits LocalizedMessage {
* @return The formatted, localized message
*/
public @NotNull String formatted(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
return formatted();
String[] parsedReplacements = getReplacements(locales, commandIssuer);
if (parsedReplacements.length == 0) {
return raw();
}
return ACFUtil.replaceStrings(message, parsedReplacements);
}
private static String[] toReplacementsArray(@NotNull MessageReplacement... replacements) {
@@ -178,4 +182,19 @@ public sealed class Message permits LocalizedMessage {
}
return replacementsArray;
}
private static String[] toReplacementsArray(
@NotNull Locales locales,
@Nullable CommandIssuer commandIssuer,
@NotNull MessageReplacement... replacements) {
String[] replacementsArray = new String[replacements.length * 2];
int i = 0;
for (MessageReplacement replacement : replacements) {
replacementsArray[i++] = replacement.getKey();
replacementsArray[i++] = replacement.getReplacement().fold(
str -> str,
message -> message.formatted(locales, commandIssuer));
}
return replacementsArray;
}
}
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.locale.message;
import io.vavr.control.Either;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -107,6 +108,7 @@ public final class MessageReplacement {
GAMERULE(replace("{gamerule}")),
LOCATION(replace("{location}")),
NAME(replace("{name}")),
NAMESPACE(replace("{namespace}")),
PLAYER(replace("{player}")),
REASON(replace("{reason}")),
VALUE(replace("{value}")),
@@ -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.
*/
@@ -149,9 +147,10 @@ public abstract class MultiverseModule extends JavaPlugin {
Try.of(() -> commandManagerProvider.get())
.mapTry(MVCommandManager::getLocales)
.andThen(pluginLocales -> {
pluginLocales.addFileResClassLoader(this);
pluginLocales.addBundleClassLoader(this.getClassLoader());
pluginLocales.addMessageBundles(this.getDescription().getName().toLowerCase());
pluginLocales.addFileResClassLoader(this);
pluginLocales.addMessageBundles(this.getName().toLowerCase(Locale.ENGLISH));
pluginLocales.loadLanguages();
})
.onFailure(e -> {
Logging.severe("Failed to register locales");
@@ -13,6 +13,7 @@ import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import static org.mvplugins.multiverse.core.permissions.PermissionUtils.concatPermission;
import static org.mvplugins.multiverse.core.permissions.PermissionUtils.registerPermissionWithWildcards;
@Service
public final class CorePermissions {
@@ -60,28 +61,34 @@ public final class CorePermissions {
@PostConstruct
void registerBasePermissions() {
pluginManager.addPermission(new Permission(JOINLOCATION_BYPASS, PermissionDefault.FALSE));
Logging.fine("Successfully registered base permissions");
Try.run(() -> {
registerPermissionWithWildcards(new Permission(JOINLOCATION_BYPASS, PermissionDefault.FALSE));
}).onSuccess(ignore -> {
Logging.fine("Successfully registered base permissions");
}).onFailure(e -> {
Logging.fine("Failed to register base permissions: %s", e.getMessage());
});
}
public Try<Void> addWorldPermissions(@NotNull MultiverseWorld world) {
return Try.run(() -> {
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(WORLD_ACCESS, world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(WORLD_EXEMPT, world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(GAMEMODE_BYPASS, world.getName()), PermissionDefault.FALSE));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(PLAYERLIMIT_BYPASS, world.getName()), PermissionDefault.FALSE));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(SPAWN, world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(SPAWN, "self", world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(SPAWN, "other", world.getName()), PermissionDefault.OP));
Logging.fine("Successfully registered permissions for world %s", world.getName());
});
}).onFailure(e -> Logging.fine("Failed to register permissions for world %s: %s",
world.getName(), e.getMessage()));
}
public Try<Void> removeWorldPermissions(@NotNull MultiverseWorld world) {
@@ -90,20 +97,23 @@ public final class CorePermissions {
pluginManager.removePermission(concatPermission(WORLD_EXEMPT, world.getName()));
pluginManager.removePermission(concatPermission(GAMEMODE_BYPASS, world.getName()));
pluginManager.removePermission(concatPermission(PLAYERLIMIT_BYPASS, world.getName()));
pluginManager.removePermission(concatPermission(SPAWN, world.getName()));
pluginManager.removePermission(concatPermission(SPAWN, "self", world.getName()));
pluginManager.removePermission(concatPermission(SPAWN, "other", world.getName()));
Logging.fine("Successfully removed permissions for world %s", world.getName());
});
}).onFailure(e -> Logging.fine("Failed to remove permissions for world %s: %s",
world.getName(), e.getMessage()));
}
public Try<Void> addDestinationPermissions(@NotNull Destination destination) {
return Try.run(() -> {
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(TELEPORT, "self", destination.getIdentifier()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(TELEPORT, "other", destination.getIdentifier()), PermissionDefault.OP));
Logging.fine("Successfully registered permissions for destination %s", destination.getIdentifier());
});
}).onFailure(e -> Logging.fine("Failed to register permissions for destination %s: %s",
destination.getIdentifier(), e.getMessage()));
}
public Try<Void> removeDestinationPermissions(@NotNull Destination destination) {
@@ -111,6 +121,7 @@ public final class CorePermissions {
pluginManager.removePermission(concatPermission(TELEPORT, "self", destination.getIdentifier()));
pluginManager.removePermission(concatPermission(TELEPORT, "other", destination.getIdentifier()));
Logging.fine("Successfully removed permissions for destination %s", destination.getIdentifier());
});
}).onFailure(e -> Logging.fine("Failed to remove permissions for destination %s: %s",
destination.getIdentifier(), e.getMessage()));
}
}
@@ -1,8 +1,14 @@
package org.mvplugins.multiverse.core.permissions;
import com.dumptruckman.minecraft.util.Logging;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.jetbrains.annotations.ApiStatus;
import java.util.Arrays;
public final class PermissionUtils {
@@ -20,6 +26,37 @@ public final class PermissionUtils {
PermissionUtils.debugPermissions = debugPermissions;
}
/**
* Registers a permission along with all its wildcard parents.
* <br />
* For example, registering "mv.bypass.joinlocation" will also register "mv.*" and "mv.bypass.*" as parents.
*
* @param permission The permission to register.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static void registerPermissionWithWildcards(Permission permission) {
Bukkit.getServer().getPluginManager().addPermission(permission);
String[] split = permission.getName().split("\\.");
StringBuilder prefix = new StringBuilder();
// Skip the last element since it's the actual permission
Arrays.stream(Arrays.copyOfRange(split, 0, split.length - 1)).forEach(s -> {
prefix.append(s).append(".");
Permission perm = getOrAddPermission(prefix + "*");
permission.addParent(perm, true);
});
}
private static Permission getOrAddPermission(String permission) {
Permission perm = Bukkit.getServer().getPluginManager().getPermission(permission);
if (perm == null) {
perm = new Permission(permission, PermissionDefault.FALSE);
Bukkit.getServer().getPluginManager().addPermission(perm);
}
return perm;
}
/**
* Joins permissions with a dot.
*
@@ -132,6 +132,10 @@ public final class BlockSafety {
*/
public boolean canSpawnAtBlockSafely(@NotNull Block block) {
Logging.finest("Checking spawn safety for location: %s, %s, %s", block.getX(), block.getY(), block.getZ());
if (!block.getWorld().getWorldBorder().isInside(block.getLocation())) {
Logging.finest("Location is outside world border.");
return false;
}
if (isUnsafeSpawnBody(block)) {
// Player body will be stuck in solid
Logging.finest("Unsafe location for player's body: " + block);
@@ -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,92 @@
package org.mvplugins.multiverse.core.utils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* A map with case-insensitive String keys. All keys are stored in lower-case form.
*
* @param <T> the type of mapped values
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public class CaseInsensitiveStringMap<T> implements Map<String, T> {
private final Map<String, T> map;
public CaseInsensitiveStringMap() {
map = new HashMap<>();
}
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return map.containsKey(normalizeKey(key));
}
@Override
public boolean containsValue(Object value) {
return map.containsValue(value);
}
@Override
public T get(Object key) {
return map.get(normalizeKey(key));
}
@Override
public @Nullable T put(String key, T value) {
return map.put(normalizeKey(key), value);
}
@Override
public T remove(Object key) {
return map.remove(normalizeKey(key));
}
@Override
public void putAll(@NotNull Map<? extends String, ? extends T> m) {
m.forEach((key, value) -> map.put(normalizeKey(key), value));
}
@Override
public void clear() {
map.clear();
}
@Override
public @NotNull Set<String> keySet() {
return map.keySet();
}
@Override
public @NotNull Collection<T> values() {
return map.values();
}
@Override
public @NotNull Set<Entry<String, T>> entrySet() {
return map.entrySet();
}
private String normalizeKey(Object key) {
return String.valueOf(key).toLowerCase(Locale.ROOT);
}
}
@@ -18,6 +18,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.dumptruckman.minecraft.util.Logging;
@@ -77,7 +78,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;
@@ -210,28 +211,31 @@ public final class FileUtils {
private final Path sourceDir;
private final Path targetDir;
private final List<String> excludeFiles;
private final List<Path> excludeFiles;
private CopyDirFileVisitor(@NotNull Path sourceDir, @NotNull Path targetDir, @NotNull List<String> excludeFiles) {
this.sourceDir = sourceDir;
this.targetDir = targetDir;
this.excludeFiles = excludeFiles;
this.excludeFiles = excludeFiles.stream()
.map(sourceDir::resolve)
.toList();
Logging.finest(this.excludeFiles.stream().map(Path::toString).collect(Collectors.joining(", ", "Exclude files: [", "]")));
}
@Override
public @NotNull FileVisitResult preVisitDirectory(Path dir, @NotNull BasicFileAttributes attrs) throws IOException {
public @NotNull FileVisitResult preVisitDirectory(@NotNull Path dir, @NotNull BasicFileAttributes attrs) throws IOException {
Path newDir = targetDir.resolve(sourceDir.relativize(dir));
if (!Files.isDirectory(newDir)) {
Files.createDirectory(newDir);
Files.createDirectories(newDir);
}
return FileVisitResult.CONTINUE;
}
@Override
public @NotNull FileVisitResult visitFile(Path file, @NotNull BasicFileAttributes attrs) throws IOException {
public @NotNull FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) throws IOException {
// Pass files that are set to ignore
if (excludeFiles.contains(file.getFileName().toString())) {
Logging.finest("Ignoring file: " + file.getFileName());
if (excludeFiles.contains(file)) {
Logging.finest("Ignoring file: " + file);
return FileVisitResult.CONTINUE;
}
// Copy the files
@@ -1,8 +1,8 @@
package org.mvplugins.multiverse.core.utils;
import io.vavr.control.Try;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.utils.tick.TickDuration;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
@@ -15,10 +15,6 @@ import java.time.format.DateTimeFormatter;
@ApiStatus.AvailableSince("5.1")
public final class MinecraftTimeFormatter {
private static final double TIME_MULTIPLIER = 3.6;
private static final long DAY_SECONDS = 24 * 60 * 60;
private static final long START_OFFSET = 6 * 60 * 60;
/**
* Formats Minecraft time to 12-hour format.
*
@@ -48,19 +44,14 @@ public final class MinecraftTimeFormatter {
* See <a href="https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html">DateTimeFormatter documentation</a>
* for available patterns.
*
* @param time The Minecraft time to format.
* @param format The format string for the time.
* @param ticks The Minecraft time to format.
* @param format The format string for the time.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public static String formatTime(long time, String format) {
// Convert Minecraft time to real-world time
long realTime = (long) ((time * TIME_MULTIPLIER) + START_OFFSET) % DAY_SECONDS; // Minecraft ticks to seconds
// Convert seconds to LocalTime
LocalTime localTime = LocalTime.ofSecondOfDay(realTime);
public static String formatTime(long ticks, String format) {
LocalTime localTime = TickDuration.ofTicks(ticks).toLocalTime();
return Try.of(() -> DateTimeFormatter.ofPattern(format))
.map(localTime::format)
.getOrElse("invalid time format: " + format);
@@ -1,16 +1,18 @@
package org.mvplugins.multiverse.core.utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.dumptruckman.minecraft.util.Logging;
import com.google.common.base.Strings;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -76,22 +78,33 @@ public final class PlayerFinder {
public static @NotNull List<Player> getMulti(@Nullable String playerIdentifiers,
@NotNull CommandSender sender
) {
List<Player> playerResults = new ArrayList<>();
return tryGetMulti(playerIdentifiers, sender)
.getOrElse(Collections.emptyList());
}
/**
* Get multiple {@link Player} based on many identifiers of name UUID or selector.
*
* @param playerIdentifiers An identifier of multiple names, UUIDs or selectors, separated by comma.
* @param sender Target sender for selector.
* @return A list of all the {@link Player} found.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static @NotNull Try<@NotNull List<Player>> tryGetMulti(@Nullable String playerIdentifiers,
@NotNull CommandSender sender
) {
if (playerIdentifiers == null || Strings.isNullOrEmpty(playerIdentifiers)) {
return playerResults;
return Try.success(Collections.emptyList());
}
// TODO: Currently just assume entire string is a selector. Add support for comma seperated mixture of names, uuids and selectors
if (isSelector(playerIdentifiers)) {
Logging.finer("Using selector: %s", playerIdentifiers);
List<Player> targetPlayers = getMultiBySelector(playerIdentifiers, sender);
if (targetPlayers != null) {
playerResults.addAll(targetPlayers);
}
return playerResults;
return tryGetMultiBySelector(playerIdentifiers, sender);
}
List<Player> playerResults = new ArrayList<>();
String[] playerIdentifierArray = REPatterns.COMMA.split(playerIdentifiers);
for (String playerIdentifier : playerIdentifierArray) {
Player targetPlayer = getByName(playerIdentifier);
@@ -104,15 +117,25 @@ public final class PlayerFinder {
playerResults.add(targetPlayer);
continue;
}
List<Player> targetPlayers = getMultiBySelector(playerIdentifier, sender);
if (targetPlayers != null) {
playerResults.addAll(targetPlayers);
Try<@NotNull List<Player>> selectorParseResult = tryGetMultiBySelector(playerIdentifier, sender);
if (selectorParseResult.isFailure()) {
return Try.failure(selectorParseResult.getCause());
}
playerResults.addAll(selectorParseResult.getOrElse(Collections.emptyList()));
}
return playerResults;
return Try.success(playerResults);
}
private static boolean isSelector(@NotNull String playerIdentifier) {
/**
* Check if the player identifier is a selector.
*
* @param playerIdentifier An identifier of name, UUID or selector.
* @return True if the identifier is a selector, else false.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static boolean isSelector(@NotNull String playerIdentifier) {
return VANILLA_SELECTORS.stream().anyMatch(playerIdentifier::startsWith);
}
@@ -167,7 +190,7 @@ public final class PlayerFinder {
@NotNull CommandSender sender
) {
List<Player> matchedPlayers = getMultiBySelector(playerSelector, sender);
if (matchedPlayers == null || matchedPlayers.isEmpty()) {
if (matchedPlayers.isEmpty()) {
Logging.fine("No player found with selector '%s' for %s.", playerSelector, sender.getName());
return null;
}
@@ -186,22 +209,36 @@ public final class PlayerFinder {
* @param sender Target sender for selector.
* @return A list of all the {@link Player} found.
*/
public static @Nullable List<Player> getMultiBySelector(@NotNull String playerSelector,
public static @NotNull List<Player> getMultiBySelector(@NotNull String playerSelector,
@NotNull CommandSender sender
) {
return tryGetMultiBySelector(playerSelector, sender)
.onFailure(throwable -> Logging.warning(
"Error selecting entities with selector '%s' for %s: %s",
playerSelector, sender.getName(), throwable.getMessage()
))
.getOrElse(Collections::emptyList);
}
/**
* Get multiple {@link Player} based on <a href="https://minecraft.gamepedia.com/Commands#Target_selectors">vanilla selectors</a>.
*
* @param playerSelector A target selector, usually starts with an '@'.
* @param sender Target sender for selector.
* @return A list of all the {@link Player} found.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static @NotNull Try<@NotNull List<Player>> tryGetMultiBySelector(@NotNull String playerSelector,
@NotNull CommandSender sender
) {
if (playerSelector.charAt(0) != '@') {
return null;
}
try {
return Bukkit.selectEntities(sender, playerSelector).stream()
.filter(e -> e instanceof Player)
.map(e -> ((Player) e))
.collect(Collectors.toList());
} catch (IllegalArgumentException e) {
Logging.warning("An error occurred while parsing selector '%s' for %s. Is it is the correct format?",
playerSelector, sender.getName());
e.printStackTrace();
return null;
return Try.success(Collections.emptyList());
}
return Try.of(() -> Bukkit.selectEntities(sender, playerSelector).stream()
.filter(e -> e instanceof Player)
.map(e -> ((Player) e))
.collect(Collectors.toList()));
}
}
@@ -1,5 +1,7 @@
package org.mvplugins.multiverse.core.utils;
import org.jetbrains.annotations.ApiStatus;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
@@ -16,7 +18,17 @@ public class REPatterns {
public static final Pattern COMMA = get(",");
public static final Pattern DOT = get("\\.");
public static final Pattern EQUALS = get("=");
@ApiStatus.AvailableSince("5.4")
public static final Pattern HYPHEN = get("-");
@ApiStatus.AvailableSince("5.4")
public static final Pattern NAMESPACE = get("^[a-z0-9_.-]+$");
@ApiStatus.AvailableSince("5.4")
public static final Pattern NAMESPACE_KEY = get("^[a-z0-9/_.-]+$");
public static final Pattern SEMICOLON = get(";");
@ApiStatus.AvailableSince("5.4")
public static final Pattern SPACE = get(" ");
public static final Pattern UNDERSCORE = get("_");
public static final Pattern UUID = get("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
@ApiStatus.AvailableSince("5.4")
public static final Pattern YAML_KEY = get("^[a-zA-Z0-9\\[\\]_-]+$");
}
@@ -3,6 +3,8 @@ package org.mvplugins.multiverse.core.utils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -15,8 +17,178 @@ public final class ReflectHelper {
* Try to get the {@link Class} based on its classpath.
*
* @param classPath The target classpath.
* @return A {@link Class} if found, else null.
* @return A {@link Try} containing the {@link Class} if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static Try<Class<?>> tryGetClass(@NotNull String classPath) {
return Try.of(() -> Class.forName(classPath));
}
/**
* Check if the {@link Class} for a give classpath is present/valid.
*
* @param classPath Target classpath.
* @return True if class path is a valid class, else false.
*/
public static boolean hasClass(String classPath) {
return tryGetClass(classPath).isSuccess();
}
/**
* Try to get a {@link Method} from a given class.
*
* @param clazz The class to search the method on.
* @param methodName Name of the method to get.
* @param parameterTypes Parameters present for that method.
* @param <C> The class type.
* @return A {@link Try} containing the {@link Method} if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static <C> Try<Method> tryGetMethod(@NotNull Class<C> clazz, @NotNull String methodName, Class<?>... parameterTypes) {
return Try.of(() -> clazz.getMethod(methodName, parameterTypes))
.orElse(Try.of(() -> {
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return method;
}));
}
/**
* Check if a {@link Method} exists on a given class.
*
* @param clazz The class to search the method on.
* @param methodName Name of the method to check.
* @param parameterTypes Parameters present for that method.
* @return True if method exists, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean hasMethod(@NotNull Class<?> clazz, @NotNull String methodName, Class<?>... parameterTypes) {
return tryGetMethod(clazz, methodName, parameterTypes).isSuccess();
}
/**
* Try to invoke a {@link Method} on a class instance.
*
* @param classInstance Instance of the class responsible for the method.
* @param method The method to invoke.
* @param parameters Parameters needed when invoking the method.
* @param <C> The class type.
* @param <R> The return type of the method.
* @return A {@link Try} containing the return value of the method if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
@SuppressWarnings("unchecked")
public static <C, R> Try<R> tryInvokeMethod(@NotNull C classInstance, @NotNull Method method, Object...parameters) {
return Try.of(() -> (R) method.invoke(classInstance, parameters));
}
/**
* Try to invoke a static {@link Method}.
*
* @param method The static method to invoke.
* @param parameters Parameters needed when invoking the method.
* @param <R> The return type of the method.
* @return A {@link Try} containing the return value of the method if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
@SuppressWarnings("unchecked")
public static <R> Try<R> tryInvokeStaticMethod(@NotNull Method method, Object...parameters) {
return Try.of(() -> (R) method.invoke(null, parameters));
}
/**
* Try to get a {@link Field} from a given class.
*
* @param clazz The class to search the field on.
* @param fieldName Name of the field to get.
* @param <C> The class type.
* @return A {@link Try} containing the {@link Field} if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static <C> Try<Field> tryGetField(@NotNull Class<C> clazz, @NotNull String fieldName) {
return Try.of(() -> clazz.getField(fieldName))
.orElse(Try.of(() -> {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
}));
}
/**
* Check if a {@link Field} exists on a given class.
*
* @param clazz The class to search the field on.
* @param fieldName Name of the field to check.
* @return True if field exists, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean hasField(@NotNull Class<?> clazz, @NotNull String fieldName) {
return tryGetField(clazz, fieldName).isSuccess();
}
/**
* Try to get the value of a {@link Field} from an instance of the class responsible.
*
* @param classInstance Instance of the class to get the field value from.
* @param field The field to get the value from.
* @param fieldType Type of the field.
* @param <C> The class type.
* @param <V> The field value type.
* @return A {@link Try} containing the field value if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static <C, V> Try<V> tryGetFieldValue(@NotNull C classInstance, @NotNull Field field, @NotNull Class<V> fieldType) {
return Try.of(() -> fieldType.cast(field.get(classInstance)));
}
/**
* Try to get the value of a static {@link Field}.
*
* @param field The static field to get the value from.
* @param fieldType Type of the field.
* @param <V> The field value type.
* @return A {@link Try} containing the field value if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static <V> Try<V> tryGetStaticFieldValue(@NotNull Field field, @NotNull Class<V> fieldType) {
return Try.of(() -> fieldType.cast(field.get(null)));
}
/**
* Try to get the {@link Class} based on its classpath.
*
* @param classPath The target classpath.
* @return A {@link Class} if found, else null.
*
* @deprecated Use {@link #tryGetClass(String)} instead, which returns a {@link Try} that can be used to handle
* the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static Class<?> getClass(String classPath) {
try {
@@ -26,16 +198,6 @@ public final class ReflectHelper {
}
}
/**
* Check if the {@link Class} for a give classpath is present/valid.
*
* @param classPath Target classpath.
* @return True if class path is a valid class, else false.
*/
public static boolean hasClass(String classPath) {
return getClass(classPath) != null;
}
/**
* Try to get a {@link Method} from a given class.
*
@@ -44,7 +206,11 @@ public final class ReflectHelper {
* @param parameterTypes Parameters present for that method.
* @param <C> The class type.
* @return A {@link Method} if found, else null.
*
* @deprecated Use {@link #tryGetMethod(Class, String, Class[])} instead, which returns a {@link Try} that can be
* used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static <C> Method getMethod(Class<C> clazz, String methodName, Class<?>... parameterTypes) {
try {
@@ -64,7 +230,11 @@ public final class ReflectHelper {
* @param parameterTypes Parameters present for that method.
* @param <C> The class type.
* @return A {@link Method} if found, else null.
*
* @deprecated Use {@link #tryGetMethod(Class, String, Class[])} instead, which returns a {@link Try} that can be
* used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static <C> Method getMethod(C classInstance, String methodName, Class<?>... parameterTypes) {
return getMethod(classInstance.getClass(), methodName, parameterTypes);
@@ -79,8 +249,13 @@ public final class ReflectHelper {
* @param <C> The class type.
* @param <R> The return type.
* @return Return value of the method call if any, else null.
*
* @deprecated Use {@link #tryInvokeMethod(Object, Method, Object...)} instead, which returns a {@link Try} that can
* be used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
@SuppressWarnings("unchecked")
public static <C, R> R invokeMethod(C classInstance, Method method, Object...parameters) {
try {
return (R) method.invoke(classInstance, parameters);
@@ -96,7 +271,11 @@ public final class ReflectHelper {
* @param fieldName Name of the field to get.
* @param <C> The class type.
* @return A {@link Field} if found, else null.
*
* @deprecated Use {@link #tryGetField(Class, String)} instead, which returns a {@link Try} that can be used to
* handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static <C> Field getField(Class<C> clazz, String fieldName) {
try {
@@ -115,7 +294,11 @@ public final class ReflectHelper {
* @param fieldName Name of the field to get.
* @param <C> The class type.
* @return A {@link Field} if found, else null.
*
* @deprecated Use {@link #tryGetField(Class, String)} instead, which returns a {@link Try} that can be used to
* handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static <C> Field getField(C classInstance, String fieldName) {
return getField(classInstance.getClass(), fieldName);
@@ -130,7 +313,11 @@ public final class ReflectHelper {
* @param <C> The class type.
* @param <V> The field value type.
* @return The field value if any, else null.
*
* @deprecated Use {@link #tryGetFieldValue(Object, Field, Class)} instead, which returns a {@link Try} that can be
* used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static <C, V> V getFieldValue(C classInstance, @Nullable Field field, @NotNull Class<V> fieldType) {
try {
@@ -153,7 +340,11 @@ public final class ReflectHelper {
* @param <C> The class type.
* @param <V> The field value type.
* @return The field value if any, else null.
*
* @deprecated Use {@link #tryGetField(Class, String)} then map to {@link #tryGetFieldValue(Object, Field, Class)} instead,
* which returns a {@link Try} that can be used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static <C, V> V getFieldValue(C classInstance, @Nullable String fieldName, @NotNull Class<V> fieldType) {
return getFieldValue(classInstance, getField(classInstance, fieldName), fieldType);
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.utils;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
@@ -14,6 +15,21 @@ import java.util.Map;
@Service
public final class ServerProperties {
private static ServerProperties instance;
/**
* A very bad static way of obtaining the level name from server.properties of older mc versions that
* don't have any way of getting main level name.
* <br />
* For internal use ONLY!
*
* @return The level name if it can be obtained, else none.
*/
@ApiStatus.Internal
public static Option<String> getStaticLevelName() {
return Option.of(instance).flatMap(ServerProperties::getLevelName);
}
private final Map<String, String> properties;
private final FileUtils fileUtils;
@@ -22,6 +38,8 @@ public final class ServerProperties {
this.fileUtils = fileUtils;
properties = new HashMap<>();
parseServerPropertiesFile();
instance = this;
}
private void parseServerPropertiesFile() {
@@ -2,13 +2,17 @@ package org.mvplugins.multiverse.core.utils;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -30,15 +34,15 @@ public final class StringFormatter {
* @param list the list of strings to join. If the list is empty, an empty string is returned.
* @return the concatenated string
*/
public static @NotNull String joinAnd(List<String> list) {
public static @NotNull String joinAnd(@Nullable List<String> list) {
return join(list, ", ", " and ");
}
public static @NotNull String join(Collection list, String separator) {
public static @NotNull String join(@Nullable Collection<?> list, @NotNull String separator) {
if (list == null || list.isEmpty()) {
return "";
}
return list.stream().map(String::valueOf).collect(Collectors.joining(separator)).toString();
return list.stream().map(String::valueOf).collect(Collectors.joining(separator));
}
/**
@@ -50,7 +54,7 @@ public final class StringFormatter {
* @param lastSeparator the separator to use before the last element. For example, " and ".
* @return the concatenated string
*/
public static @NotNull String join(List<String> list, String separator, String lastSeparator) {
public static @NotNull String join(@Nullable List<String> list, @NotNull String separator, @NotNull String lastSeparator) {
if (list == null || list.isEmpty()) {
return "";
}
@@ -71,11 +75,30 @@ public final class StringFormatter {
/**
* Appends a list of suggestions to the end of the input string, separated by commas.
*
* @param input The current input
* @param addons The autocomplete suggestions
* @return A collection of suggestions with the next suggestion appended
*
* @deprecated Method name has a spelling error. Use {@link #addOnToCommaSeparated(String, Collection)} instead.
*/
@Deprecated(forRemoval = true, since = "5.5")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public static Collection<String> addonToCommaSeperated(@Nullable String input, @NotNull Collection<String> addons) {
return addOnToCommaSeparated(input, addons);
}
/**
* Appends a list of suggestions to the end of the input string, separated by commas.
*
* @param input The current input
* @param addons The autocomplete suggestions
* @return A collection of suggestions with the next suggestion appended
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public static Collection<String> addOnToCommaSeparated(@Nullable String input, @NotNull Collection<String> addons) {
if (Strings.isNullOrEmpty(input)) {
return addons;
}
@@ -94,7 +117,7 @@ public final class StringFormatter {
* @param args The args to parse
* @return The parsed args
*/
public static Collection<String> parseQuotesInArgs(String[] args) {
public static @NotNull Collection<String> parseQuotesInArgs(@NotNull String[] args) {
List<String> result = new ArrayList<>(args.length);
StringBuilder current = new StringBuilder();
boolean inQuotes = false;
@@ -139,7 +162,28 @@ public final class StringFormatter {
* @param input The string to add quotes to
* @return The quoted string
*/
public static String quoteMultiWordString(String input) {
return input.contains(" ") ? "\"" + input + "\"" : input;
@Contract("null -> null")
public static @Nullable String quoteMultiWordString(@Nullable String input) {
return input != null && input.contains(" ") ? "\"" + input + "\"" : input;
}
/**
* Parses a CSV string of key=value pairs into a map.
* E.g. "key1=value1,key2=value2" -> {key1=value1, key2=value2}
*
* @param input The CSV string to parse
* @return The parsed map
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public static @Unmodifiable Map<String, String> parseCSVMap(@Nullable String input) {
if (Strings.isNullOrEmpty(input)) {
return Map.of();
}
return REPatterns.COMMA.splitAsStream(input)
.map(s -> REPatterns.EQUALS.split(s, 2))
.filter(parts -> parts.length == 2)
.collect(Collectors.toUnmodifiableMap(parts -> parts[0], parts -> parts[1]));
}
}
@@ -1,7 +1,7 @@
package org.mvplugins.multiverse.core.utils;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.Server;
import org.bukkit.scheduler.BukkitRunnable;
@@ -10,8 +10,6 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* Defers action that cannot be done during world tick.
@@ -21,26 +19,22 @@ public final class WorldTickDeferrer {
private final MultiverseCore plugin;
private Object console = null;
private Field isIteratingOverLevelsMethod = null;
private final Option<Object> console;
private final Option<Field> isIteratingOverLevelsMethod;
@Inject
WorldTickDeferrer(@NotNull MultiverseCore plugin, @NotNull Server server) {
this.plugin = plugin;
Method getServerMethod = ReflectHelper.getMethod(server.getClass(), "getServer");
if (getServerMethod == null) {
Logging.fine("Unable to find getServer method.");
return;
}
this.console = ReflectHelper.invokeMethod(server, getServerMethod);
if (console == null) {
Logging.fine("Unable to find console.");
return;
}
this.isIteratingOverLevelsMethod = Try.of(() -> console.getClass().getField("isIteratingOverLevels")).getOrNull();
if (isIteratingOverLevelsMethod == null) {
Logging.fine("Unable to find isIteratingOverLevels field.");
}
this.console = ReflectHelper.tryGetMethod(server.getClass(), "getServer")
.onFailure(throwable -> Logging.fine("Unable to find getServer method."))
.flatMap(getServerMethod -> ReflectHelper.tryInvokeMethod(server, getServerMethod))
.onFailure(throwable -> Logging.fine("Unable to find console."))
.toOption();
this.isIteratingOverLevelsMethod = console.toTry()
.map(Object::getClass)
.flatMap(consoleClazz -> ReflectHelper.tryGetField(consoleClazz, "isIteratingOverLevels"))
.onFailure(throwable -> Logging.fine("Unable to find isIteratingOverLevels field."))
.toOption();
}
/**
@@ -63,16 +57,14 @@ public final class WorldTickDeferrer {
}
/**
* Check if the the server is currently doing a world tick.
* Check if the server is currently doing a world tick.
*
* @return True if the server is currently doing a world tick
*/
private boolean isIteratingOverLevels() {
if (console == null || isIteratingOverLevelsMethod == null) {
return false;
}
return Objects.requireNonNullElse(
ReflectHelper.getFieldValue(console, isIteratingOverLevelsMethod, Boolean.class),
false);
return isIteratingOverLevelsMethod
.flatMap(field -> console
.flatMap(c -> ReflectHelper.tryGetFieldValue(c, field, Boolean.class).toOption()))
.getOrElse(false);
}
}
@@ -0,0 +1,123 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.Server;
import org.bukkit.World;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import java.lang.reflect.Method;
import java.nio.file.Path;
/**
* Compatibility class used to handle API changes in {@link Bukkit} class.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
public final class BukkitCompatibility {
private static final Try<Method> GET_LEVEL_DIRECTORY_METHOD;
private static final Try<Method> GET_WORLD_NAMESPACED_KEY_METHOD;
static {
GET_LEVEL_DIRECTORY_METHOD = ReflectHelper.tryGetMethod(Server.class, "getLevelDirectory");
GET_WORLD_NAMESPACED_KEY_METHOD = ReflectHelper.tryGetMethod(Bukkit.class, "getWorld", NamespacedKey.class);
}
/**
* Check whether the server is using the new dimension storage system introduced in PaperMC 26.1.
* <br />
* This is based of whether getLevelDirectory method exists in the server class,which is the main API change for
* the new dimension storage system.
*
* @return True if the server is using the new dimension storage system, else false.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
public static boolean isUsingNewDimensionStorage() {
return GET_LEVEL_DIRECTORY_METHOD
.flatMap(method -> ReflectHelper.tryInvokeMethod(Bukkit.getServer(), method))
.isSuccess();
}
/**
* Gets the folder where all the worlds will be store. Before 26.1, all worlds are stored in the root directory
* of the server, which can be obtained by {@link Server#getWorldContainer()}.
* <br />
* After 26.1, PaperMC changed all worlds are stored in the "[level]/dimensions" folder under the world
* level directory, which needs to be manually parsed.
*
* @return The location where all the worlds folders should be, depending on server's mc version.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
@NotNull
public static Path getWorldFoldersDirectory() {
Server server = Bukkit.getServer();
return GET_LEVEL_DIRECTORY_METHOD.flatMap(method -> ReflectHelper.tryInvokeMethod(server, method))
.filter(Path.class::isInstance)
.map(Path.class::cast)
.map(path -> path.resolve("dimensions"))
.getOrElse(() -> server.getWorldContainer().toPath());
}
/**
* Check if the world with the given name or namespaced key (e.g. minecraft:overworld) exists,
* and return it if it does.
* <br />
* Note that some default world names have different namespaced key matched with them.
* E.g.: world -> minecraft:overworld, world_nether -> minecraft:the_nether, world_the_end -> minecraft:the_end.
*
* @param nameOrKey Either a name or namespaced key string representation.
* @return The world if it exists
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
@NotNull
public static Option<World> getWorldByNameOrKey(@NotNull String nameOrKey) {
return Option.of(Bukkit.getWorld(nameOrKey))
.orElse(() -> GET_WORLD_NAMESPACED_KEY_METHOD
.flatMap(method -> ReflectHelper.tryInvokeStaticMethod(method, NamespacedKey.fromString(nameOrKey)))
.filter(World.class::isInstance)
.map(World.class::cast)
.toOption());
}
/**
* Check if the world represented by the given {@link WorldKeyOrName} exists and return it if present.
*
* <p>This first attempts to resolve the world by name using {@link WorldKeyOrName#usableName()} (which
* delegates to the traditional Bukkit world name lookup). If that fails it will attempt to resolve a
* {@link NamespacedKey} using {@link WorldKeyOrName#usableKey()} (which mirrors the newer Bukkit API
* that accepts namespaced keys, e.g. "minecraft:overworld").
*
* @param keyOrName The key-or-name wrapper providing either a world name or a {@link NamespacedKey}.
* @return The world if it exists.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static Option<World> getWorldByNameOrKey(@NotNull WorldKeyOrName keyOrName) {
return Option.of(Bukkit.getWorld(keyOrName.usableName()))
.orElse(() -> GET_WORLD_NAMESPACED_KEY_METHOD
.flatMap(method -> ReflectHelper.tryInvokeStaticMethod(method, keyOrName.usableKey()))
.filter(World.class::isInstance)
.map(World.class::cast)
.toOption());
}
private BukkitCompatibility() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -0,0 +1,45 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.UnsafeValues;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import java.lang.reflect.Method;
/**
* Compatibility class used to handle API changes in {@link UnsafeValues} class.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.7")
public final class UnsafeValuesCompatibility {
private static final Try<Method> GET_MAIN_LEVEL_NAME_METHOD;
static {
GET_MAIN_LEVEL_NAME_METHOD = ReflectHelper.tryGetMethod(UnsafeValues.class, "getMainLevelName");
}
/**
* Try to call the getMainLevelName method in UnsafeValues, which is used to get level-name configured in
* server.properties file.
*
* @return The level-name configured in server.properties file, or empty if the method is not available.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static Option<String> getMainLevelName() {
return GET_MAIN_LEVEL_NAME_METHOD
.flatMap(method -> ReflectHelper.tryInvokeMethod(Bukkit.getUnsafe(), method))
.map(String.class::cast)
.toOption();
}
private UnsafeValuesCompatibility() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -0,0 +1,170 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import org.bukkit.WorldBorder;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import org.mvplugins.multiverse.core.utils.tick.TickDuration;
import java.util.concurrent.TimeUnit;
/**
* Compatibility class used to handle API changes in {@link WorldBorder} class.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@SuppressWarnings("removal")
public final class WorldBorderCompatibility {
private static final boolean HAS_GET_WARNING_TIME_TICKS_METHOD;
private static final boolean HAS_SET_WARNING_TIME_TICKS_METHOD;
private static final boolean HAS_SET_WARNING_TIME_METHOD;
private static final boolean HAS_GET_WARNING_TIME_METHOD;
private static final boolean HAS_CHANGE_SIZE_METHOD;
private static final boolean HAS_SET_SIZE_METHOD;
static {
HAS_GET_WARNING_TIME_TICKS_METHOD = ReflectHelper.hasMethod(WorldBorder.class, "getWarningTimeTicks");
HAS_SET_WARNING_TIME_TICKS_METHOD = ReflectHelper.hasMethod(WorldBorder.class, "setWarningTimeTicks", int.class);
HAS_GET_WARNING_TIME_METHOD = ReflectHelper.hasMethod(WorldBorder.class, "getWarningTime");
HAS_SET_WARNING_TIME_METHOD = ReflectHelper.hasMethod(WorldBorder.class, "setWarningTime", int.class);
HAS_CHANGE_SIZE_METHOD = ReflectHelper.hasMethod(WorldBorder.class, "changeSize", double.class, long.class);
HAS_SET_SIZE_METHOD = ReflectHelper.hasMethod(WorldBorder.class, "setSize", double.class, long.class);
}
/**
* Gets the world border warning time in ticks, using the most precise API available.
* <br />
* If the server exposes {@code getWarningTimeTicks()}, that value is returned directly. Otherwise, the older
* {@code getWarningTime()} API is used and converted from seconds to ticks.
*
* @param worldBorder The world border to query.
* @return The warning time in ticks.
*
* @since 5.7
* @throws IllegalStateException If neither warning-time getter is available.
*/
@ApiStatus.AvailableSince("5.7")
public static int getWarningTimeTicks(WorldBorder worldBorder) {
if (HAS_GET_WARNING_TIME_TICKS_METHOD) {
return worldBorder.getWarningTimeTicks();
} else if (HAS_GET_WARNING_TIME_METHOD) {
return worldBorder.getWarningTime() * 20;
}
throw new IllegalStateException("Neither getWarningTimeTicks nor getWarningTime method is available in WorldBorder class.");
}
/**
* Gets the world border warning time in seconds, using the most appropriate API available.
* <br />
* If the server exposes {@code getWarningTimeTicks()}, that value is converted from ticks to seconds. Otherwise,
* the older {@code getWarningTime()} API is used directly.
*
* @param worldBorder The world border to query.
* @return The warning time in seconds.
*
* @since 5.7
* @throws IllegalStateException If neither warning-time getter is available.
*/
@ApiStatus.AvailableSince("5.7")
public static double getWarningTime(WorldBorder worldBorder) {
if (HAS_GET_WARNING_TIME_TICKS_METHOD) {
return (double) worldBorder.getWarningTimeTicks() / 20.0;
} else if (HAS_GET_WARNING_TIME_METHOD) {
return worldBorder.getWarningTime();
}
throw new IllegalStateException("Neither getWarningTimeTicks nor getWarningTime method is available in WorldBorder class.");
}
/**
* Checks whether the current server supports setting the world border warning time in ticks.
*
* @return True if {@code setWarningTimeTicks(int)} is available, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean supportsSetWarningTimeInTicks() {
return HAS_SET_WARNING_TIME_TICKS_METHOD;
}
/**
* Sets the world border warning time in ticks.
* <br />
* This is a convenience wrapper around {@link #setWarningTimeDuration(WorldBorder, TickDuration)} that converts
* the given tick value into a {@link TickDuration} first.
*
* @param worldBorder The world border to update.
* @param ticks The warning time in ticks.
*
* @since 5.7
*/
public static void setWarningTimeTicks(WorldBorder worldBorder, int ticks) {
setWarningTimeDuration(worldBorder, TickDuration.ofTicks(ticks));
}
/**
* Sets the world border warning time, preferring tick-based APIs when available.
* <br />
* If the server supports {@code setWarningTimeTicks(int)}, the provided duration is applied in ticks. Otherwise,
* the older {@code setWarningTime(int)} API is used and the duration is converted to seconds.
*
* @param worldBorder The world border to update.
* @param duration The warning duration to apply.
*
* @since 5.7
* @throws IllegalStateException If neither warning-time setter is available.
*/
@ApiStatus.AvailableSince("5.7")
public static void setWarningTimeDuration(WorldBorder worldBorder, TickDuration duration) {
if (HAS_SET_WARNING_TIME_TICKS_METHOD) {
worldBorder.setWarningTimeTicks((int) duration.toTicks());
} else if (HAS_SET_WARNING_TIME_METHOD) {
worldBorder.setWarningTime((int) Math.round(duration.to(TimeUnit.SECONDS)));
} else {
throw new IllegalStateException("Neither setWarningTimeTicks nor setWarningTime method is available in WorldBorder class.");
}
}
/**
* Checks whether the current server supports changing world border size over a duration in ticks.
*
* @return True if {@code changeSize(double, long)} is available, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean supportsChangeSizeInTicks() {
return HAS_CHANGE_SIZE_METHOD;
}
/**
* Changes the world border size, preferring the tick-based API when available.
* <br />
* If the server supports {@code changeSize(double, long)}, the size change is applied in ticks. Otherwise, the
* older {@code setSize(double, long)} API is used and the duration is converted to seconds.
*
* @param worldBorder The world border to update.
* @param newSize The new border size.
* @param duration The transition duration to apply.
*
* @since 5.7
* @throws IllegalStateException If neither size-changing API is available.
*/
@ApiStatus.AvailableSince("5.7")
public static void changeSizeDuration(WorldBorder worldBorder, double newSize, TickDuration duration) {
if (HAS_CHANGE_SIZE_METHOD) {
worldBorder.changeSize(newSize, duration.toTicks());
} else if (HAS_SET_SIZE_METHOD) {
worldBorder.setSize(newSize, Math.round(duration.to(TimeUnit.SECONDS)));
} else {
throw new IllegalStateException("Neither changeSize nor setSize method is available in WorldBorder class.");
}
}
private WorldBorderCompatibility() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -0,0 +1,92 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import io.papermc.lib.PaperLib;
import io.vavr.control.Try;
import org.bukkit.World;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import java.lang.reflect.Method;
/**
* Compatibility class used to handle API changes in {@link World} class.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public final class WorldCompatibility {
private static final Try<Method> SAVE_WITH_FLUSH_METHOD;
private static final boolean HAS_GET_COORDINATE_SCALE_METHOD;
private static final boolean HAS_HAS_BONUS_CHEST_METHOD;
static {
SAVE_WITH_FLUSH_METHOD = ReflectHelper.tryGetMethod(World.class, "save", boolean.class);
HAS_GET_COORDINATE_SCALE_METHOD = ReflectHelper.hasMethod(World.class, "getCoordinateScale");
HAS_HAS_BONUS_CHEST_METHOD = ReflectHelper.hasMethod(World.class, "hasBonusChest");
}
/**
* Saves the world, with an option to wait for chunk writers to finish if the method is available. Saving with
* flush is only supported on PaperMC 1.21+.
* <br />
* If the method is not available, it will fall back to the normal save method, which may not wait for chunk writers
* to finish, but is the best we can do for older versions of Minecraft.
*
* @param world The world to save
* @param flush Waits for chunk writers to finish
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static void saveWithFlush(World world, boolean flush) {
SAVE_WITH_FLUSH_METHOD
.flatMap(method -> ReflectHelper.tryInvokeMethod(world, method, flush))
.orElseRun(ignore -> world.save());
}
/**
* Gets the coordinate scale for the world, which is used to convert between overworld and nether coordinates.
* On PaperMC, this can be obtained directly from the API. On older versions, it is manually determined based on the
* world environment (overworld = 1, nether = 8, end = 1).
*
* @param world The world to get the coordinate scale for
* @return The scale
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static double getCoordinateScale(World world) {
if (HAS_GET_COORDINATE_SCALE_METHOD) {
return world.getCoordinateScale();
}
return switch (world.getEnvironment()) {
case NORMAL -> 1;
case NETHER -> 8;
default -> 1;
};
}
/**
* Checks if the world has a bonus chest.
* <br />
* On PaperMC 1.21.5+, this can be obtained directly from the API. On older versions, it always returns false as
* bonus chest is not supported.
*
* @param world The world to check
* @return True if the world has a bonus chest, false otherwise
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean hasBonusChest(World world) {
if (HAS_HAS_BONUS_CHEST_METHOD) {
return world.hasBonusChest();
}
return false;
}
private WorldCompatibility() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -0,0 +1,188 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.bukkit.NamespacedKey;
import org.bukkit.WorldCreator;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import java.lang.reflect.Method;
import java.util.Locale;
/**
* Compatibility class used to handle {@link WorldCreator} API changes across different server versions.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public final class WorldCreatorCompatibility {
private static final Try<Class<?>> POSITION_CLASS;
private static final Try<Method> FORCED_SPAWN_POSITION_METHOD;
private static final Try<Method> OF_KEY_METHOD;
private static final Try<Method> OF_NAME_AND_KEY_METHOD;
private static final Try<Method> BONUS_CHEST_METHOD;
static {
POSITION_CLASS = ReflectHelper.tryGetClass("io.papermc.paper.math.Position");
FORCED_SPAWN_POSITION_METHOD = POSITION_CLASS.flatMap(positionClass ->
ReflectHelper.tryGetMethod(WorldCreator.class, "forcedSpawnPosition", positionClass, float.class, float.class));
BONUS_CHEST_METHOD = ReflectHelper.tryGetMethod(WorldCreator.class, "bonusChest", boolean.class);
OF_KEY_METHOD = ReflectHelper.tryGetMethod(WorldCreator.class, "ofKey", NamespacedKey.class);
OF_NAME_AND_KEY_METHOD = ReflectHelper.tryGetMethod(WorldCreator.class, "ofNameAndKey", String.class, NamespacedKey.class);
}
/**
* Check whether the current server version supports creating worlds with a {@link NamespacedKey}.
* <br />
* This is based on whether the {@code ofKey(NamespacedKey)} method exists in the {@link WorldCreator} class,
* which was introduced in later server versions.
*
* @return True if the server supports world creation with NamespacedKey, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean canCreateWorldWithKey() {
return OF_KEY_METHOD.isSuccess();
}
/**
* Creates a {@link WorldCreator} from a {@link WorldKeyOrName}, using the most appropriate method
* based on the server version and the key/name state.
* <br />
* If the server supports NamespacedKey-based creation and the provided keyOrName is a key,
* this method will use {@code ofKey()}. Otherwise, it falls back to {@code name()}.
*
* @param keyOrName Either a world key or name to create from.
* @return The {@link WorldCreator} initialized for the given key or name.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static WorldCreator ofKeyOrName(@NotNull WorldKeyOrName keyOrName) {
return ofNameAndKey(keyOrName.usableKey(), keyOrName.usableName());
}
/**
* Creates a {@link WorldCreator} from a {@link NamespacedKey} and world name, using the most
* appropriate method based on the server version and dimension storage configuration.
* <br />
* This method attempts to use the most feature-rich creation method available:
* if {@code ofNameAndKey()} is available and the parameters are compatible, use it;
* otherwise, if {@code ofKey()} is available, use it; as a fallback, use {@code name()}.
*
* @param worldKey The namespaced key for the world.
* @param worldName The display name for the world.
* @return The {@link WorldCreator} initialized with the given key and name.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static WorldCreator ofNameAndKey(@NotNull NamespacedKey worldKey, String worldName) {
if (OF_NAME_AND_KEY_METHOD.isSuccess() && canPassIntoNameAndKey(worldKey)) {
return WorldCreator.ofNameAndKey(worldName, worldKey);
}
if (OF_KEY_METHOD.isSuccess()) {
return WorldCreator.ofKey(worldKey);
}
return WorldCreator.name(worldName);
}
/**
* Check if the given world key and name can be passed into the {@code ofNameAndKey()} method.
* <br />
* The parameters are compatible only if the server is not using new dimension storage,
* or if the namespace is the Minecraft namespace and the world name (lowercased) matches the key.
*
* @param worldKey The namespaced key for the world.
* @return True if the parameters can be safely passed to {@code ofNameAndKey()}, else false.
*/
private static boolean canPassIntoNameAndKey(@NotNull NamespacedKey worldKey) {
return !BukkitCompatibility.isUsingNewDimensionStorage()
|| worldKey.getNamespace().equals(NamespacedKey.MINECRAFT);
}
/**
* Checks if the server supports configuring a forced spawn position via the WorldCreator API.
* <br />
* The force spawn position API is generally only available on PaperMC 26.1+
*
* @return Whether force spawn position API is supported on the current server version.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean supportsForcedSpawnPosition() {
return FORCED_SPAWN_POSITION_METHOD.isSuccess();
}
/**
* Tries to set the forced spawn position if the server implements the API. This call will do nothing if server
* software does not implement the required APIs.
*
* @param worldCreator Target creator instance to set on.
* @param position The position to set as the forced spawn point for the world.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static void setForcedSpawnPosition(WorldCreator worldCreator, EntityPosition position) {
if (!supportsForcedSpawnPosition()) {
Logging.fine("Server does not support forced spawn position configuration via WorldCreator API.");
return;
}
ReflectHelper.tryInvokeMethod(
worldCreator,
FORCED_SPAWN_POSITION_METHOD.get(),
io.papermc.paper.math.Position.fine(
position.getVector().getX().getRawValue(),
position.getVector().getY().getRawValue(),
position.getVector().getZ().getRawValue()
),
(float) position.getDirection().getYaw().getRawValue(),
(float) position.getDirection().getPitch().getRawValue()
).onFailure(ex ->
Logging.warning("Failed to set forced spawn position on WorldCreator: %s", ex.getMessage()));
}
/**
* Checks if the server supports configuring bonus chest generation via the WorldCreator API.
* <br />
* The bonus chest API is generally only available on PaperMC 1.21.5+
*
* @return Whether bonus chest API is supported on the current server version.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean supportsBonusChest() {
return BONUS_CHEST_METHOD.isSuccess();
}
/**
* Tries to set bonus chest if the server implements the API. This call will do nothing if server software does not
* implement the required APIs.
*
* @param worldCreator Target creator instance to set on.
* @param generateBonusChest Whether to generate a bonus chest at the world spawn point.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static void setBonusChest(WorldCreator worldCreator, boolean generateBonusChest) {
if (!supportsBonusChest()) {
Logging.fine("Server does not support bonus chest generation via WorldCreator API.");
return;
}
worldCreator.bonusChest(generateBonusChest);
}
private WorldCreatorCompatibility() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff. Show more