Compare commits

...
167 Commits
Author SHA1 Message Date
Ben Woo 892ce24845 Merge pull request #3507 from Multiverse/chore/glare-down
Switch to use jitpack for config dep as glare is down
2026-07-23 21:41:50 +08:00
Ben Woo 091eec96b9 Switch to use jitpack for config dep as glare is down 2026-07-23 21:37:28 +08:00
Ben Woo 37ecceb179 Merge pull request #3506 from Multiverse/fix/load-callback
Improve error handling for when loaded API callback
2026-07-21 13:49:15 +08:00
Ben Woo 7697065519 Improve error handling for when loaded API callback 2026-07-21 12:41:26 +08:00
Ben Woo ad7b9638f1 Merge pull request #3505 from Multiverse/feat/nested-potential-worlds
Add support for searching potential world in nested sub-folders
2026-07-20 20:02:49 +08:00
Ben Woo 37ba5ecc04 Add support for searching potential world in nested sub-folders 2026-07-20 19:59:07 +08:00
Ben Woo 2445464ab8 Merge pull request #3504 from Multiverse/fix/exact-dest-alias
Fix exact destination not working with alias names
2026-07-20 14:37:11 +08:00
Ben Woo 7fad9aa2e4 Fix exact destination not working with alias names 2026-07-20 14:19:20 +08:00
Ben Woo a8c0b221cd Merge pull request #3503 from Multiverse/fix/alias-mapping
Map alias even when world isn't loaded and case insensitive lookup
2026-07-20 13:18:39 +08:00
Ben Woo 826fe4c6d0 Map alias even when world isn't loaded and case insensitive lookup 2026-07-20 13:15:00 +08:00
Ben Woo 97cbb4576d Merge pull request #3499 from Multiverse/refactor/entity-purge
Clean up entity purger and ensure player is not removed
2026-07-16 12:48:58 +08:00
Ben Woo d50cd6cce5 Merge pull request #3498 from Multiverse/fix/purge-entities-chunk
Remove entities on chunk load based on spawn config
2026-07-16 12:44:30 +08:00
Ben Woo b500956555 Clean up entity purger and ensure player is not removed 2026-07-16 12:43:22 +08:00
Ben Woo de80667bb4 Remove entities on chunk load based on spawn config 2026-07-16 12:22:37 +08:00
Ben Woo 9dddbdcd97 Merge pull request #3493 from Multiverse/feat/polish-trans
Add polish translation
2026-06-22 11:08:07 +08:00
Ben Woo 0c829f5a4e Add polish translation
Thanks to @santandersonek on discord!
2026-06-22 11:04:08 +08:00
Ben Woo 6f91d290db Merge pull request #3491 from Multiverse/feat/localise
Refactor all remaining issuer raw string messages to use localized strings
2026-06-22 10:55:25 +08:00
Ben Woo cf65076f69 Merge pull request #3492 from amhunter1/feat/turkish-locale
feat: add Turkish (tr) locale
2026-06-22 10:34:13 +08:00
Ben Woo 25daacadba Merge pull request #3490 from Multiverse/fix/name-or-alias
Always try to get world by name/namespace first before alias
2026-06-22 10:19:40 +08:00
amhunter1andClaude Sonnet 4.6 8d9daa4bdf feat: update Turkish locale with keys from PR #3491
Added translations for new keys introduced in the localisation refactor:
- config string parser errors, core config validations
- anchor commands (delete/list/set)
- entity spawn config commands
- generators info keys
- spawn/teleport permission messages
- commands error context keys (loadedmultiverseworld, player, resolve, removeplayers)
- destination display keys (anchor, bed)
- purge entities commands
- queue command prompt/expired keys
- content display pagefilter key
- generic error.details, playercount, teleportplayers.failed keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 20:07:57 +03:00
amhunter1andClaude Sonnet 4.6 d2c35a029d feat: add Turkish (tr) locale
Adds Turkish language support via multiverse-core_tr.properties.
All 267 translation keys from the English source file are included.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 18:55:25 +03:00
Ben Woo 61f4d2308c Refactor all remaining issuer raw string messages to use localized strings 2026-06-21 14:41:23 +08:00
Ben Woo 767b7af5e1 Always try to get world by name/namespace first before alias 2026-06-21 12:09:28 +08:00
Ben Woo 3e46f198f5 Merge pull request #3489 from Multiverse/fix/entity-warning
Improve spawn category map logging
2026-06-20 21:11:18 +08:00
Ben Woo 9f06740873 Improve spawn category map logging 2026-06-20 21:08:51 +08:00
Ben Woo 5648211722 Merge pull request #3488 from Multiverse/fix/spawn-category
Fix spawn category mapping changes in 26.2
2026-06-20 15:36:41 +08:00
Ben Woo ed96764474 Merge pull request #3487 from Multiverse/fix/tele-perms-msg
Make teleport no permission message red
2026-06-20 15:36:17 +08:00
Ben Woo 15453e6443 Add logging if spawn category map is empty after building 2026-06-20 15:32:51 +08:00
Ben Woo 0df35f0e26 Make teleport no permission message red 2026-06-20 15:29:07 +08:00
Ben Woo 0424ac81bd Add 26.2 as supported mc version for platforms upload 2026-06-20 15:28:41 +08:00
Ben Woo 4ee6901df1 Fix spawn category mapping changes in 26.2 2026-06-20 15:27:42 +08:00
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
137 changed files with 7759 additions and 1305 deletions

No files matched your search

@@ -107,7 +107,7 @@ 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.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
game_versions: 26.2, 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, purpur
dependencies: ${{ inputs.modrinth_dependencies }}
@@ -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.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
game_versions: 26.2, 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.11"]}'
platform_dependencies: '{"PAPER": ["1.18.2-26.2"]}'
plugin_dependencies: ${{ inputs.hangar_plugin_dependencies }}
+7 -7
View File
@@ -9,13 +9,13 @@ description = 'Multiverse-Core'
repositories {
maven {
name = 'aikar repo'
url = uri('https://repo.aikar.co/content/groups/aikar/')
name = 'jitpack repo'
url = uri('https://jitpack.io')
}
maven {
name = 'glaremasters repo'
url = 'https://repo.glaremasters.me/repository/towny/'
name = 'aikar repo'
url = uri('https://repo.aikar.co/content/groups/aikar/')
}
maven {
@@ -25,9 +25,9 @@ repositories {
}
configure(apiDependencies) {
serverApiVersion = '1.21.8-R0.1-SNAPSHOT'
serverApiVersion = '1.21.11-R0.1-SNAPSHOT'
mockBukkitServerApiVersion = '1.21'
mockBukkitVersion = '4.84.0'
mockBukkitVersion = '4.110.0'
}
dependencies {
@@ -43,7 +43,7 @@ dependencies {
shadowed 'co.aikar:acf-paper:0.5.1-SNAPSHOT'
// Config
shadowed('io.github.townyadvanced.commentedconfiguration:CommentedConfiguration:1.0.1') {
shadowed('com.github.TownyAdvanced:CommentedConfiguration:1.0.3') {
exclude group: 'org.spigotmc', module: 'spigot-api'
}
@@ -30,6 +30,7 @@ import org.mvplugins.multiverse.core.inject.PluginServiceLocatorFactory;
import org.mvplugins.multiverse.core.module.MultiverseModule;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.entity.SpawnCategoryMapper;
import org.mvplugins.multiverse.core.world.location.NullSpawnLocation;
import org.mvplugins.multiverse.core.world.location.SpawnLocation;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
@@ -97,6 +98,9 @@ public class MultiverseCore extends MultiverseModule {
}
Logging.setShowingConfig(shouldShowConfig());
// Build it here so our logger can work, and failure messages will be logged
SpawnCategoryMapper.buildSpawnCategoryMap();
// Initialize the worlds
worldManagerProvider.get().initAllWorlds().andThenTry(() -> {
loadEconomist(); // Setup economy here so vault is loaded
@@ -126,6 +130,7 @@ public class MultiverseCore extends MultiverseModule {
MultiverseCoreApi.shutdown();
shutdownDependencyInjection();
PluginServiceLocatorFactory.get().shutdown();
Logging.info("- Disabled");
Logging.shutdown();
}
@@ -225,11 +230,12 @@ public class MultiverseCore extends MultiverseModule {
* 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");
}
}
@@ -1,9 +1,17 @@
package org.mvplugins.multiverse.core;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.plugin.ServicePriority;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
@@ -16,18 +24,13 @@ import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.biomeprovider.BiomeProviderFactory;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Provides access to the MultiverseCore API.
*/
public class MultiverseCoreApi {
public final class MultiverseCoreApi {
private static MultiverseCoreApi instance;
private static final List<Consumer<MultiverseCoreApi>> whenLoadedCallbacks = new ArrayList<>();
private static final List<Consumer<MultiverseCoreApi>> WHEN_LOADED_CALLBACKS = new ArrayList<>();
static void init(@NotNull MultiverseCore multiverseCore) {
if (instance != null) {
@@ -36,11 +39,24 @@ public class MultiverseCoreApi {
instance = new MultiverseCoreApi(multiverseCore.getServiceLocator());
Bukkit.getServicesManager().register(MultiverseCoreApi.class, instance, multiverseCore, ServicePriority.Normal);
whenLoadedCallbacks.forEach(c -> c.accept(instance));
whenLoadedCallbacks.clear();
List<Consumer<MultiverseCoreApi>> callbacks = List.copyOf(WHEN_LOADED_CALLBACKS);
WHEN_LOADED_CALLBACKS.clear();
MultiverseCoreApi loadedApi = instance;
callbacks.forEach(callback -> runLoadCallback(callback, loadedApi));
}
private static void runLoadCallback(
@NotNull Consumer<MultiverseCoreApi> callback,
@NotNull MultiverseCoreApi loadedApi) {
Try.run(() -> callback.accept(loadedApi))
.onFailure(exception -> Logging.warning(
"A Multiverse-Core API load callback failed: %s", exception.getMessage()));
}
static void shutdown() {
if (instance == null) {
return;
}
Bukkit.getServicesManager().unregister(instance);
instance = null;
}
@@ -62,7 +78,7 @@ public class MultiverseCoreApi {
if (instance != null) {
consumer.accept(instance);
} else {
whenLoadedCallbacks.add(consumer);
WHEN_LOADED_CALLBACKS.add(consumer);
}
}
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core;
import com.google.common.collect.Lists;
import io.vavr.control.Option;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
@@ -12,11 +13,13 @@ 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.List;
@@ -26,12 +29,17 @@ import java.util.Locale;
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;
}
@@ -42,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";
@@ -80,136 +95,115 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
return null;
}
final var placeholder = paramsArray.remove(0);
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.get(paramsArray.size() - 1);
if (worldManager.isLoadedWorld(paramWorldName)) {
if (worldManager.isWorld(paramWorldName)) {
paramsArray.remove(paramsArray.size() - 1);
return paramWorldName;
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(Locale.ENGLISH)) {
case "alias" -> {
return world.getAliasOrName();
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 "animalspawn" -> {
return String.valueOf(world.getEntitySpawnConfig().getSpawnCategoryConfig(SpawnCategory.ANIMAL).isSpawn());
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;
}
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 "playercount" -> {
return String.valueOf(world.getBukkitWorld()
.map(World::getPlayers)
.map(List::size)
.getOrElse(-1));
}
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());
default -> {
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);
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);
}
}
yield switch (timeFormat) {
case "" -> String.valueOf(time);
case "12h" -> MinecraftTimeFormatter.format12h(time);
case "24h" -> MinecraftTimeFormatter.format24h(time);
default -> MinecraftTimeFormatter.formatTime(time, timeFormat);
};
}
case "type" -> {
return world.getBukkitWorld().map(World::getWorldType).map(Enum::name).getOrElse("null");
}
case "weather" -> {
return String.valueOf(world.isAllowWeather());
}
default -> {
warning("Unknown placeholder: " + placeholder);
return null;
}
}
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");
}
}
@@ -11,6 +11,7 @@ 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;
@@ -52,8 +53,9 @@ 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 {
@@ -65,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(
@@ -74,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;
@@ -84,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", 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);
registerCompletion("playersarray", this::suggestPlayersArray); // getting online players cannot be async
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);
@@ -163,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);
}
@@ -297,13 +303,20 @@ public class MVCommandCompletions extends PaperCommandCompletions {
.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();
@@ -334,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) {
@@ -39,9 +39,12 @@ import org.mvplugins.multiverse.core.display.filters.ContentFilter;
import org.mvplugins.multiverse.core.display.filters.DefaultContentFilter;
import org.mvplugins.multiverse.core.display.filters.RegexContentFilter;
import org.mvplugins.multiverse.core.exceptions.command.MVInvalidCommandArgument;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
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;
@@ -93,6 +96,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) {
@@ -166,15 +170,67 @@ public class MVCommandContexts extends PaperCommandContexts {
return generatorPlugin;
}
private Message loadedMultiverseWorldPlayerOnlyMessage() {
return Message.of(MVCorei18n.COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_PLAYERSONLY);
}
private Message loadedMultiverseWorldIssuerMessage() {
return Message.of(MVCorei18n.COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_ISSUER);
}
private Message loadedMultiverseWorldInputConsoleMessage(String worldName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_INPUTCONSOLE,
Replace.WORLD.with(worldName));
}
private Message loadedMultiverseWorldInputMessage(String worldName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_INPUT, Replace.WORLD.with(worldName));
}
private Message multiverseWorldPlayerOnlyMessage() {
return Message.of(MVCorei18n.COMMANDS_ERROR_MULTIVERSEWORLD_PLAYERSONLY);
}
private Message multiverseWorldIssuerMessage() {
return Message.of(MVCorei18n.COMMANDS_ERROR_MULTIVERSEWORLD_ISSUER);
}
private Message multiverseWorldInputConsoleMessage(String worldName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_MULTIVERSEWORLD_INPUTCONSOLE, Replace.WORLD.with(worldName));
}
private Message multiverseWorldInputMessage(String worldName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_MULTIVERSEWORLD_INPUT, Replace.WORLD.with(worldName));
}
private Message playerOnlyMessage() {
return Message.of(MVCorei18n.COMMANDS_ERROR_PLAYERSONLY);
}
private Message playerInputIssuerMessage(String playerName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_PLAYER_ISSUERINPUT, Replace.PLAYER.with(playerName));
}
private Message playerInputMessage(String playerName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_PLAYER_INPUT, Replace.PLAYER.with(playerName));
}
private Message playerSelectorMessage(String selector) {
return Message.of(MVCorei18n.COMMANDS_ERROR_PLAYER_SELECTOR, Replace.PLAYER.with(selector));
}
private Message playersInputMessage(String playerName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_PLAYERS_INPUT, Replace.PLAYER.with(playerName));
}
private IssuerAwareContextBuilder<LoadedMultiverseWorld> loadedMultiverseWorldContextBuilder() {
return new IssuerAwareContextBuilder<LoadedMultiverseWorld>()
.fromPlayer((context, player) -> worldManager.getLoadedWorld(player.getWorld()).getOrNull())
.fromInput((context, input) -> getLoadedMultiverseWorld(input))
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a loaded Multiverse World."))
.issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a loaded multiverse world. Either specify a multiverse world name or use this command in a loaded multiverse world."))
.issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console."))
.inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a loaded multiverse world."));
.issuerOnlyFailMessage((context) -> loadedMultiverseWorldPlayerOnlyMessage())
.issuerAwarePlayerFailMessage((context, player) -> loadedMultiverseWorldIssuerMessage())
.issuerAwareInputFailMessage((context, input) -> loadedMultiverseWorldInputConsoleMessage(input))
.inputOnlyFailMessage((context, input) -> loadedMultiverseWorldInputMessage(input));
}
private IssuerAwareContextBuilder<LoadedMultiverseWorld[]> loadedMultiverseWorldArrayContextBuilder() {
@@ -192,16 +248,17 @@ public class MVCommandContexts extends PaperCommandContexts {
}
LoadedMultiverseWorld world = getLoadedMultiverseWorld(worldName);
if (world == null) {
throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world.");
throw new InvalidCommandArgument(
loadedMultiverseWorldInputMessage(worldName).formatted(context.getIssuer()));
}
worlds.add(world);
}
return worlds.isEmpty() ? null : worlds.toArray(new LoadedMultiverseWorld[0]);
})
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a loaded Multiverse World."))
.issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a loaded multiverse world. Either specify a multiverse world name or use this command in a loaded multiverse world."))
.issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console."))
.inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a loaded multiverse world."));
.issuerOnlyFailMessage((context) -> loadedMultiverseWorldPlayerOnlyMessage())
.issuerAwarePlayerFailMessage((context, player) -> loadedMultiverseWorldIssuerMessage())
.issuerAwareInputFailMessage((context, input) -> loadedMultiverseWorldInputConsoleMessage(input))
.inputOnlyFailMessage((context, input) -> loadedMultiverseWorldInputMessage(input));
}
@Nullable
@@ -215,10 +272,10 @@ public class MVCommandContexts extends PaperCommandContexts {
return new IssuerAwareContextBuilder<MultiverseWorld>()
.fromPlayer((context, player) -> worldManager.getWorld(player.getWorld()).getOrNull())
.fromInput((context, input) -> getMultiverseWorld(input))
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a Multiverse World."))
.issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world."))
.issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a multiverse world. Remember to specify the world name when using this command in console."))
.inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a multiverse world."));
.issuerOnlyFailMessage((context) -> multiverseWorldPlayerOnlyMessage())
.issuerAwarePlayerFailMessage((context, player) -> multiverseWorldIssuerMessage())
.issuerAwareInputFailMessage((context, input) -> multiverseWorldInputConsoleMessage(input))
.inputOnlyFailMessage((context, input) -> multiverseWorldInputMessage(input));
}
private IssuerAwareContextBuilder<MultiverseWorld[]> multiverseWorldArrayContextBuilder() {
@@ -236,16 +293,17 @@ public class MVCommandContexts extends PaperCommandContexts {
}
MultiverseWorld world = getMultiverseWorld(worldName);
if (world == null) {
throw new InvalidCommandArgument("World " + worldName + " is not a multiverse world.");
throw new InvalidCommandArgument(
multiverseWorldInputMessage(worldName).formatted(context.getIssuer()));
}
worlds.add(world);
}
return worlds.isEmpty() ? null : worlds.toArray(new MultiverseWorld[0]);
})
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a Multiverse World."))
.issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world."))
.issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a multiverse world. Remember to specify the world name when using this command in console."))
.inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a multiverse world."));
.issuerOnlyFailMessage((context) -> multiverseWorldPlayerOnlyMessage())
.issuerAwarePlayerFailMessage((context, player) -> multiverseWorldIssuerMessage())
.issuerAwareInputFailMessage((context, input) -> multiverseWorldInputConsoleMessage(input))
.inputOnlyFailMessage((context, input) -> multiverseWorldInputMessage(input));
}
@Nullable
@@ -265,9 +323,9 @@ public class MVCommandContexts extends PaperCommandContexts {
return new IssuerAwareContextBuilder<Player>()
.fromPlayer((context, player) -> player)
.fromInput((context, input) -> PlayerFinder.get(input, context.getSender()))
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player."))
.issuerAwareInputFailMessage((context, input) -> Message.of("Invalid player: " + input + ". Either specify an online player or use this command as a player."))
.inputOnlyFailMessage((context, input) -> Message.of("Player " + input + " not found."));
.issuerOnlyFailMessage((context) -> playerOnlyMessage())
.issuerAwareInputFailMessage((context, input) -> playerInputIssuerMessage(input))
.inputOnlyFailMessage((context, input) -> playerInputMessage(input));
}
private IssuerAwareContextBuilder<Player[]> playerArrayContextBuilder() {
@@ -284,13 +342,13 @@ public class MVCommandContexts extends PaperCommandContexts {
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."))
.issuerOnlyFailMessage((context) -> playerOnlyMessage())
.issuerAwareInputFailMessage((context, input) -> playerInputIssuerMessage(input))
.inputOnlyFailMessage((context, input) -> {
if (PlayerFinder.isSelector(input)) {
return Message.of("No player(s) matched selector: " + input + ".");
return playerSelectorMessage(input);
}
return Message.of("Player(s) " + input + " not found.");
return playersInputMessage(input);
});
}
@@ -393,4 +451,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"));
}
}
@@ -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");
@@ -6,12 +6,16 @@ import co.aikar.commands.InvalidCommandArgument;
import co.aikar.commands.contexts.IssuerAwareContextResolver;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Reusable logic of issuer only and issuer aware context resolvers
*
@@ -22,10 +26,17 @@ public final class IssuerAwareContextBuilder<T> {
private BiFunction<BukkitCommandExecutionContext, Player, T> fromPlayer;
private BiFunction<BukkitCommandExecutionContext, String, T> fromInput;
private Function<BukkitCommandExecutionContext, Message> issuerOnlyFailMessage = context -> Message.of("This command can only be used by a player.");
private BiFunction<BukkitCommandExecutionContext, Player, Message> issuerAwarePlayerFailMessage = (context, player) -> Message.of("Unable to resolve context for player '" + player.getName() + "'.");
private BiFunction<BukkitCommandExecutionContext, String, Message> issuerAwareInputFailMessage = (context, input) -> Message.of("Unable to resolve context for input '" + input + "'.");
private BiFunction<BukkitCommandExecutionContext, String, Message> inputOnlyFailMessage = (context, input) -> Message.of("Unable to resolve context for input '" + input + "'.");
private Function<BukkitCommandExecutionContext, Message> issuerOnlyFailMessage =
context -> Message.of(MVCorei18n.COMMANDS_ERROR_PLAYERSONLY);
private BiFunction<BukkitCommandExecutionContext, Player, Message> issuerAwarePlayerFailMessage =
(context, player) -> Message.of(MVCorei18n.COMMANDS_ERROR_RESOLVE_PLAYER,
Replace.PLAYER.with(player.getName()));
private BiFunction<BukkitCommandExecutionContext, String, Message> issuerAwareInputFailMessage =
(context, input) -> Message.of(MVCorei18n.COMMANDS_ERROR_RESOLVE_INPUT,
replace("{input}").with(input));
private BiFunction<BukkitCommandExecutionContext, String, Message> inputOnlyFailMessage =
(context, input) -> Message.of(MVCorei18n.COMMANDS_ERROR_RESOLVE_INPUT,
replace("{input}").with(input));
public IssuerAwareContextBuilder() {
}
@@ -5,8 +5,10 @@ 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;
@@ -40,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;
}
@@ -96,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();
}
/**
@@ -126,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;
@@ -158,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;
}
@@ -194,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);
}
}
@@ -207,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;
@@ -253,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;
}
@@ -264,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,67 @@
package org.mvplugins.multiverse.core.command.flags;
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.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
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(() -> MVInvalidCommandArgument.of(
Message.of(MVCorei18n.COMMANDS_ERROR_REMOVEPLAYERS_NODEFAULT))))
.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 {
@@ -15,7 +15,6 @@ import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.block.data.type.CommandBlock;
import org.bukkit.command.BlockCommandSender;
import org.bukkit.command.CommandSender;
@@ -28,6 +27,7 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.*;
@@ -79,8 +79,9 @@ public class CommandQueueManager {
if (config.getUseConfirmOtp()) {
confirmCommand += " " + payload.otp();
}
payload.issuer().sendMessage(String.format("Run %s%s %sto continue. This will expire in %s seconds.",
ChatColor.GREEN, confirmCommand, ChatColor.WHITE, config.getConfirmTimeout()));
payload.issuer().sendMessage(MVCorei18n.QUEUECOMMAND_PROMPT,
replace("{command}").with(confirmCommand),
replace("{timeout}").with(config.getConfirmTimeout()));
}
/**
@@ -127,7 +128,7 @@ public class CommandQueueManager {
// Payload already removed
return;
}
payload.issuer().sendMessage("Your queued command has expired.");
payload.issuer().sendMessage(MVCorei18n.QUEUECOMMAND_EXPIRED);
};
}
@@ -5,6 +5,7 @@ import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
/**
@@ -23,12 +24,10 @@ public class CommandQueuePayload {
return new CommandQueuePayload(issuer);
}
private static final String DEFAULT_PROMPT_MESSAGE = "The command you are trying to run is deemed dangerous."; // todo: localize
private final MVCommandIssuer issuer;
private String otp;
private Runnable action = () -> {};
private Message prompt = Message.of(DEFAULT_PROMPT_MESSAGE);
private Message prompt = Message.of(MVCorei18n.QUEUECOMMAND_DEFAULTPROMPT);
private BukkitTask expireTask;
protected CommandQueuePayload(@NotNull MVCommandIssuer issuer) {
@@ -12,6 +12,9 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
import org.mvplugins.multiverse.core.anchor.MultiverseAnchor;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
final class AnchorDeleteCommand extends CoreCommand {
@@ -35,9 +38,9 @@ final class AnchorDeleteCommand extends CoreCommand {
@Description("")
MultiverseAnchor anchor) {
if (anchorManager.deleteAnchor(anchor).isSuccess()) {
issuer.sendMessage("&aAnchor &f" + anchor.getName() + "&a deleted.");
issuer.sendMessage(MVCorei18n.ANCHOR_DELETE_SUCCESS, replace("{anchor}").with(anchor.getName()));
} else {
issuer.sendMessage("&cFailed to delete anchor.");
issuer.sendError(MVCorei18n.ANCHOR_DELETE_FAILURE);
}
}
}
@@ -15,15 +15,18 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
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.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.ListContentProvider;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.teleportation.LocationManipulation;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
final class AnchorListCommand extends CoreCommand {
@@ -58,18 +61,19 @@ final class AnchorListCommand extends CoreCommand {
ContentDisplay.create()
.addContent(ListContentProvider.forContent(getAnchors(issuer.getPlayer())))
.withSendHandler(PagedSendHandler.create()
.withHeader("&3==== [ Multiverse Anchors ] ====")
.withHeader(Message.of(MVCorei18n.ANCHOR_LIST_HEADER))
.doPagination(true)
.withTargetPage(parsedFlags.flagValue(flags.page, 1))
.withFilter(parsedFlags.flagValue(flags.filter, DefaultContentFilter.get())))
.send(issuer);
}
private List<String> getAnchors(Player player) {
private List<? extends Message> getAnchors(Player player) {
return anchorManager.getAnchors(player).stream()
.map(anchor ->
"&a%s&7 - &f%s".formatted(
anchor.getName(), locationManipulation.locationToString(anchor.getLocation())))
Message.of(MVCorei18n.ANCHOR_LIST_ENTRY,
replace("{anchor}").with(anchor.getName()),
replace("{location}").with(locationManipulation.locationToString(anchor.getLocation()))))
.toList();
}
}
@@ -15,8 +15,11 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.teleportation.LocationManipulation;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
final class AnchorSetCommand extends CoreCommand {
@@ -80,10 +83,12 @@ final class AnchorSetCommand extends CoreCommand {
}
private void sendAnchorSetSuccessMessage(MVCommandIssuer issuer, String anchorName, String locationString) {
issuer.sendMessage("&aAnchor &f" + anchorName + "&a set to &f" + locationString);
issuer.sendMessage(MVCorei18n.ANCHOR_SET_SUCCESS,
replace("{anchor}").with(anchorName),
replace("{location}").with(locationString));
}
private void sendAnchorSetFailedMessage(MVCommandIssuer issuer, String anchorName) {
issuer.sendMessage("&cFailed to set anchor &f" + anchorName + ".");
issuer.sendMessage(MVCorei18n.ANCHOR_SET_FAILURE, replace("{anchor}").with(anchorName));
}
}
@@ -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);
}
}
@@ -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,14 +86,15 @@ 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)))
.onFailure(() -> issuer.sendError("Failed to teleport one or more players out of the world!"));
.onFailure(() -> issuer.sendError(MVCorei18n.GENERIC_TELEPORTPLAYERS_FAILED));
}
private void doWorldDeleting(MVCommandIssuer issuer, MultiverseWorld 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) {
@@ -5,17 +5,15 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
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;
@@ -39,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);
}
@@ -73,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();
}
}
@@ -92,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);
@@ -112,34 +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(Arrays.stream(plugin.getServer().getPluginManager().getPlugins())
.map(Object::toString)
.sorted()
.toList(), "\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';
}
}
@@ -21,7 +21,9 @@ 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.ListContentProvider;
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.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -31,6 +33,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
@Subcommand("entity-spawn-config")
final class EntitySpawnConfigCommand extends CoreCommand {
@@ -64,21 +68,26 @@ final class EntitySpawnConfigCommand extends CoreCommand {
ContentDisplay.create()
.addContent(ListContentProvider.forContent(getEntitySpawnConfigList(world)))
.withSendHandler(PagedSendHandler.create()
.withHeader(Message.of("==== [ Entity Spawn Config '" + world.getName() + "' ] ===="))
.withHeader(Message.of(MVCorei18n.ENTITYSPAWNCONFIG_INFO_HEADER,
Replace.WORLD.with(world.getName())))
.withLinesPerPage(8)
.withTargetPage(parsedFlags.flagValue(flags.page, 1))
.withFilter(parsedFlags.flagValue(flags.filter, DefaultContentFilter.get())))
.send(issuer);
}
private List<String> getEntitySpawnConfigList(MultiverseWorld world) {
List<String> list = new ArrayList<>();
private List<Message> getEntitySpawnConfigList(MultiverseWorld world) {
List<Message> list = new ArrayList<>();
Arrays.stream(SpawnCategory.values()).forEach(spawnCategory -> {
list.add(spawnCategory.name() + ": ");
list.add(Message.of(MVCorei18n.ENTITYSPAWNCONFIG_INFO_CATEGORY,
replace("{category}").with(spawnCategory.name())));
SpawnCategoryConfig spawnCategoryConfig = world.getEntitySpawnConfig().getSpawnCategoryConfig(spawnCategory);
list.add(" spawn: " + spawnCategoryConfig.isSpawn());
list.add(" tick-rate: " + spawnCategoryConfig.getTickRate());
list.add(" exceptions: " + StringFormatter.join(spawnCategoryConfig.getExceptions(), ", "));
list.add(Message.of(MVCorei18n.ENTITYSPAWNCONFIG_INFO_SPAWN,
replace("{spawn}").with(spawnCategoryConfig.isSpawn())));
list.add(Message.of(MVCorei18n.ENTITYSPAWNCONFIG_INFO_TICKRATE,
replace("{tickRate}").with(spawnCategoryConfig.getTickRate())));
list.add(Message.of(MVCorei18n.ENTITYSPAWNCONFIG_INFO_EXCEPTIONS,
replace("{exceptions}").with(StringFormatter.join(spawnCategoryConfig.getExceptions(), ", "))));
});
return list;
}
@@ -114,9 +123,16 @@ final class EntitySpawnConfigCommand extends CoreCommand {
.getStringPropertyHandle()
.modifyPropertyString(property, value, action)
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendInfo("Successfully set " + property + " to " + value
+ " for " + spawnCategory.name() + " in " + world.getName()))
.onFailure(e -> issuer.sendError("Unable to set " + property + " to " + value
+ " for " + spawnCategory.name() + " in " + world.getName() + ": " + e.getMessage()));
.onSuccess(ignore -> issuer.sendInfo(MVCorei18n.ENTITYSPAWNCONFIG_MODIFY_SUCCESS,
replace("{property}").with(property),
Replace.VALUE.with(value),
replace("{category}").with(spawnCategory.name()),
Replace.WORLD.with(world.getName())))
.onFailure(e -> issuer.sendError(MVCorei18n.ENTITYSPAWNCONFIG_MODIFY_FAILURE,
replace("{property}").with(property),
Replace.VALUE.with(value),
replace("{category}").with(spawnCategory.name()),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(e)));
}
}
@@ -10,7 +10,6 @@ import co.aikar.commands.annotation.Optional;
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;
@@ -23,10 +22,13 @@ import org.mvplugins.multiverse.core.display.filters.DefaultContentFilter;
import org.mvplugins.multiverse.core.display.handlers.PagedSendHandler;
import org.mvplugins.multiverse.core.display.parsers.ListContentProvider;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.generators.GeneratorPlugin;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* List all gamerules in your current or specified world.
*/
@@ -68,7 +70,7 @@ class GeneratorsCommand extends CoreCommand {
ContentDisplay.create()
.addContent(ListContentProvider.forContent(generators))
.withSendHandler(PagedSendHandler.create()
.withHeader("%s====[ Multiverse Generator List ]====", ChatColor.AQUA)
.withHeader(Message.of(MVCorei18n.GENERATORS_HEADER))
.doPagination(true)
.withTargetPage(parsedFlags.flagValue(flags.page, 1))
.withFilter(parsedFlags.flagValue(flags.filter, DefaultContentFilter.get())))
@@ -85,10 +87,13 @@ class GeneratorsCommand extends CoreCommand {
@Syntax("<generator>")
GeneratorPlugin generatorPlugin
) {
issuer.sendMessage(ChatColor.RESET + "Generator Plugin: " + generatorPlugin.getPluginName());
issuer.sendMessage(ChatColor.RESET + "Example usages: ");
issuer.sendMessage(ChatColor.RESET + StringFormatter.join(generatorPlugin.getExampleUsages(), "\n"));
issuer.sendMessage(ChatColor.RESET + "Link to more info: " + generatorPlugin.getInfoLink());
issuer.sendMessage(MVCorei18n.GENERATORS_INFO_PLUGIN,
replace("{plugin}").with(generatorPlugin.getPluginName()));
issuer.sendMessage(MVCorei18n.GENERATORS_INFO_EXAMPLEUSAGES);
issuer.sendMessage(MVCorei18n.GENERATORS_INFO_USAGES,
replace("{usages}").with(StringFormatter.join(generatorPlugin.getExampleUsages(), "\n")));
issuer.sendMessage(MVCorei18n.GENERATORS_INFO_INFOLINK,
replace("{link}").with(generatorPlugin.getInfoLink()));
}
@Service
@@ -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
@@ -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();
}
}
@@ -107,14 +107,14 @@ class ModifyCommand extends CoreCommand {
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
Replace.ERROR.with(exception));
} 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.ERROR.with(exception));
}
});
}
@@ -10,12 +10,16 @@ import jakarta.inject.Inject;
import org.bukkit.entity.SpawnCategory;
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.StringFormatter;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.entity.EntityPurger;
import java.util.List;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
final class PurgeAllEntitiesCommand extends CoreCommand {
@@ -43,12 +47,16 @@ final class PurgeAllEntitiesCommand extends CoreCommand {
) {
if (spawnCategories == null || spawnCategories.length == 0) {
int purgeCount = entityPurger.purgeAllEntities(world);
issuer.sendMessage("Successfully purged " + purgeCount + " entities in world " + world.getName() + ".");
issuer.sendMessage(MVCorei18n.PURGEALLENTITIES_SUCCESS,
Replace.COUNT.with(purgeCount),
Replace.WORLD.with(world.getName()));
return;
}
int purgeCount = entityPurger.purgeEntities(world, spawnCategories);
issuer.sendMessage("Successfully purged " + purgeCount + " entities in world " + world.getName() +
" for spawn categories " + StringFormatter.join(List.of(spawnCategories), ", ") + ".");
issuer.sendMessage(MVCorei18n.PURGEALLENTITIES_SUCCESS_CATEGORIES,
Replace.COUNT.with(purgeCount),
Replace.WORLD.with(world.getName()),
replace("{categories}").with(StringFormatter.join(List.of(spawnCategories), ", ")));
}
}
@@ -8,6 +8,8 @@ import co.aikar.commands.annotation.Syntax;
import jakarta.inject.Inject;
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.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.entity.EntityPurger;
@@ -33,6 +35,8 @@ final class PurgeEntitiesCommand extends CoreCommand {
LoadedMultiverseWorld world
) {
int purgeCount = entityPurger.purgeEntities(world);
issuer.sendMessage("Successfully purged " + purgeCount + " entities in world " + world.getName() + ".");
issuer.sendMessage(MVCorei18n.PURGEENTITIES_SUCCESS,
Replace.COUNT.with(purgeCount),
Replace.WORLD.with(world.getName()));
}
}
@@ -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,14 +97,15 @@ 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
future.onSuccess(() -> worldTickDeferrer
.deferWorldTick(() -> doWorldRegening(issuer, world, parsedFlags, worldPlayers)))
.onFailure(() -> issuer.sendError("Failed to teleport one or more players out of the world!"));
.onFailure(() -> issuer.sendError(MVCorei18n.GENERIC_TELEPORTPLAYERS_FAILED));
}
private void doWorldRegening(
@@ -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,17 +62,20 @@ 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))
.onFailure(() -> issuer.sendError("Failed to teleport one or more players out of the world!"));
.onFailure(() -> issuer.sendError(MVCorei18n.GENERIC_TELEPORTPLAYERS_FAILED));
}
private void doWorldRemoving(MVCommandIssuer issuer, MultiverseWorld world, ParsedCommandFlags 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")
@@ -88,11 +88,11 @@ final class SpawnCommand extends CoreCommand {
List<Entity> entities, boolean checkSafety) {
LoadedMultiverseWorld mvWorld = worldManager.getLoadedWorld(world).getOrNull();
if (mvWorld == null) {
issuer.sendMessage("The world '" + world.getName() + "' is not a multiverse world!");
issuer.sendMessage(MVCorei18n.SPAWN_NOTMVWORLD, Replace.WORLD.with(world.getName()));
return;
}
if (!permissionsChecker.checkSpawnPermission(issuer.getIssuer(), entities, mvWorld)) {
issuer.sendMessage("You do not have permission to use this command in this world!");
issuer.sendMessage(MVCorei18n.SPAWN_NOPERMISSION);
return;
}
@@ -129,16 +129,18 @@ final class SpawnCommand extends CoreCommand {
.checkSafety(checkSafety)
.teleport(entities)
.onSuccessCount(successCount -> issuer.sendMessage(MVCorei18n.SPAWN_SUCCESS,
Replace.PLAYER.with(successCount + " players"), //todo: replace this with localised "{count} players"
Replace.PLAYER.with(Message.of(MVCorei18n.GENERIC_PLAYERCOUNT,
Replace.COUNT.with(successCount))),
Replace.WORLD.with(mvWorld.getName())))
.onFailureCount(reasonsCountMap -> {
for (var entry : reasonsCountMap.entrySet()) {
Logging.finer("Failed to teleport %s players to %s: %s",
entry.getValue(), mvWorld.getName(), entry.getKey());
issuer.sendError(MVCorei18n.SPAWN_FAILED,
Replace.PLAYER.with(entry.getValue() + " players"),
Replace.PLAYER.with(Message.of(MVCorei18n.GENERIC_PLAYERCOUNT,
Replace.COUNT.with(entry.getValue()))),
Replace.WORLD.with(mvWorld.getName()),
Replace.REASON.with(entry.getKey().getMessageKey()));
Replace.REASON.with(Message.of(entry.getKey())));
}
});
}
@@ -94,10 +94,9 @@ final class TeleportCommand extends CoreCommand {
DestinationInstance<?, ?> destination,
ParsedCommandFlags parsedFlags) {
if (!permissionsChecker.checkTeleportPermission(issuer.getIssuer(), player, destination)) {
// TODO localize
issuer.sendMessage(player == issuer.getPlayer()
? "You do not have permission to teleport yourself!"
: "You do not have permission to teleport other players!");
issuer.sendError(player == issuer.getPlayer()
? MVCorei18n.TELEPORT_NOPERMISSION_SELF
: MVCorei18n.TELEPORT_NOPERMISSION_OTHER);
return;
}
@@ -134,8 +133,7 @@ final class TeleportCommand extends CoreCommand {
DestinationInstance<?, ?> destination,
ParsedCommandFlags parsedFlags) {
if (!permissionsChecker.checkTeleportPermission(issuer.getIssuer(), Arrays.asList(players), destination)) {
// TODO localize
issuer.sendMessage("You do not have permission to teleport all these players!");
issuer.sendError(MVCorei18n.TELEPORT_NOPERMISSION_ALL);
return;
}
@@ -149,7 +147,8 @@ final class TeleportCommand extends CoreCommand {
return;
}
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(successCount + " players"),
Replace.PLAYER.with(Message.of(MVCorei18n.GENERIC_PLAYERCOUNT,
Replace.COUNT.with(successCount))),
Replace.DESTINATION.with(destination.getDisplayMessage()));
})
.onFailureCount(reasonsCountMap -> {
@@ -157,7 +156,8 @@ final class TeleportCommand extends CoreCommand {
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.PLAYER.with(Message.of(MVCorei18n.GENERIC_PLAYERCOUNT,
Replace.COUNT.with(entry.getValue()))),
Replace.DESTINATION.with(destination.getDisplayMessage()),
Replace.REASON.with(Message.of(entry.getKey())));
}
@@ -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,19 +62,20 @@ 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))
.onFailure(() -> issuer.sendError("Failed to teleport one or more players out of the world!"));
.onFailure(() -> issuer.sendError(MVCorei18n.GENERIC_TELEPORTPLAYERS_FAILED));
}
private void doWorldUnloading(MVCommandIssuer issuer, LoadedMultiverseWorld world, ParsedCommandFlags 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")
@@ -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,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")
@@ -145,7 +147,7 @@ final class WorldBorderCommand extends CoreCommand {
@Optional
@Default("0")
@Syntax("[time]")
int time,
TickDuration duration,
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[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()));
}
});
@@ -201,27 +206,36 @@ final class WorldBorderCommand extends CoreCommand {
MVCommandIssuer issuer,
@Syntax("<time>")
int time,
TickDuration duration,
@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()));
.onFailure(error -> issuer.sendError(MVCorei18n.GENERIC_ERROR_DETAILS, Replace.ERROR.with(error)));
}
}
@@ -186,6 +186,16 @@ 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);
@@ -493,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}
*/
@@ -621,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);
}
@@ -25,6 +25,8 @@ import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.dynamiclistener.EventPriorityMapper;
import org.mvplugins.multiverse.core.event.MVDebugModeEvent;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.permissions.PermissionUtils;
import org.mvplugins.multiverse.core.teleportation.PassengerModes;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat;
@@ -135,6 +137,16 @@ 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.")
@@ -210,7 +222,10 @@ 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("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.")
@@ -358,7 +373,8 @@ final class CoreConfigNodes {
.defaultValue(128)
.name("custom-portal-search-radius")
.validator(value -> value < 0
? Try.failure(new MultiverseException("The value must be greater than or equal to 0.", null))
? Try.failure(new MultiverseException(
Message.of(MVCorei18n.CORE_CONFIG_CUSTOMPORTALSEARCHRADIUS_NONNEGATIVE), null))
: Try.success(null))
.build());
@@ -390,6 +406,23 @@ 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.")
@@ -457,7 +490,8 @@ final class CoreConfigNodes {
.defaultValue(30)
.name("confirm-timeout")
.validator(value -> (value <= 0)
? Try.failure(new MultiverseException("Confirm timeout must be a positive number!"))
? Try.failure(new MultiverseException(
Message.of(MVCorei18n.CORE_CONFIG_CONFIRMTIMEOUT_POSITIVE)))
: Try.success(null))
.build());
@@ -528,6 +562,18 @@ final class CoreConfigNodes {
"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")
.comment("")
.comment("")
@@ -576,7 +622,8 @@ final class CoreConfigNodes {
.name("global-debug")
.suggester(input -> List.of("0", "1", "2", "3"))
.validator(value -> (value < 0 || value > 3)
? Try.failure(new MultiverseException("Debug level must be between 0 and 3."))
? Try.failure(new MultiverseException(
Message.of(MVCorei18n.CORE_CONFIG_DEBUGLEVEL_RANGE)))
: Try.success(null))
.onLoadAndChange((oldValue, newValue) -> {
if (newValue != Logging.getDebugLevel()) {
@@ -3,11 +3,9 @@ package org.mvplugins.multiverse.core.config.handle;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Logger;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
@@ -19,6 +17,7 @@ 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.MapValueNode;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.config.node.ValueNode;
@@ -93,17 +92,13 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
}
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);
}
@@ -124,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));
}
/**
@@ -163,6 +153,15 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
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.
*
@@ -221,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;
}
}
}
@@ -180,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}
*/
@@ -120,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));
}
}
@@ -283,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;
}
/**
@@ -0,0 +1,607 @@
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.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
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;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* 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(Message.of(MVCorei18n.CONFIG_NODE_INVALIDYAMLKEY,
replace("{key}").with(key)));
}
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);
}
@@ -34,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
*/
@@ -44,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
*/
@@ -72,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
*/
@@ -97,23 +111,59 @@ 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()}.
*
* @param value The loaded value.
* <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
*/
@@ -123,9 +173,12 @@ public interface ValueNode<T> extends Node {
/**
* Called when the value of this node is loaded or changed.
*
* @param sender The sender who triggered the change. If triggered by loading or no target sender is specified,
* it will be the console sender.
* @param oldValue The old value, will always be null on load.
* <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
@@ -136,9 +189,11 @@ public interface ValueNode<T> extends Node {
/**
* Called when the value of this node is changed by {@link BaseConfigurationHandle#set(ValueNode, Object)}.
*
* @param sender The sender who changed the value, or console sender if no target sender specified.
* @param oldValue The old value.
* @param newValue The new value.
* <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
*/
@@ -148,12 +203,15 @@ public interface ValueNode<T> extends Node {
/**
* 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.
*/
@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);
}
@@ -8,6 +8,10 @@ import co.aikar.commands.ACFUtil;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Provides default string parsers for common types.
@@ -51,24 +55,33 @@ public final class DefaultStringParserProvider {
() -> 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'");
default -> throw new MultiverseException(Message.of(MVCorei18n.CONFIG_STRING_PARSER_INVALIDBOOLEAN,
replace("{input}").with(input)));
});
private static final NodeStringParser<Integer> INTEGER_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseInt(input))
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException("Unable to convert '" + input + "' to number. (integer)")));
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException(Message.of(
MVCorei18n.CONFIG_STRING_PARSER_INVALIDINTEGER,
replace("{input}").with(input)))));
private static final NodeStringParser<Double> DOUBLE_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseDouble(input))
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException("Unable to convert '" + input + "' to number. (double)")));
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException(Message.of(
MVCorei18n.CONFIG_STRING_PARSER_INVALIDDOUBLE,
replace("{input}").with(input)))));
private static final NodeStringParser<Float> FLOAT_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseFloat(input))
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException("Unable to convert '" + input + "' to number. (float)")));
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException(Message.of(
MVCorei18n.CONFIG_STRING_PARSER_INVALIDFLOAT,
replace("{input}").with(input)))));
private static final NodeStringParser<Long> LONG_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseLong(input))
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException("Unable to convert '" + input + "' to number. (long)")));
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException(Message.of(
MVCorei18n.CONFIG_STRING_PARSER_INVALIDLONG,
replace("{input}").with(input)))));
static {
addDefaultStringParser(String.class, STRING_STRING_PARSER);
@@ -7,8 +7,11 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Destination instance implementation for the {@link AnchorDestination}.
*/
@@ -69,8 +72,7 @@ public final class AnchorDestinationInstance extends DestinationInstance<AnchorD
*/
@Override
public @NotNull Message getDisplayMessage() {
//TODO Localize
return Message.of("anchor '" + anchorName + "'");
return Message.of(MVCorei18n.DESTINATION_ANCHOR_DISPLAY, replace("{anchor}").with(anchorName));
}
/**
@@ -9,7 +9,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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;
/**
* Destination instance implementation for the {@link BedDestination}.
@@ -70,8 +72,9 @@ public final class BedDestinationInstance extends DestinationInstance<BedDestina
*/
@Override
public @NotNull Message getDisplayMessage() {
//TODO Localize
return Message.of(player == null ? "your bed/respawn point" : player.getName() + "'s bed/respawn point");
return player == null
? Message.of(MVCorei18n.DESTINATION_BED_DISPLAY_OWN)
: Message.of(MVCorei18n.DESTINATION_BED_DISPLAY_OTHER, Replace.PLAYER.with(player.getName()));
}
/**
@@ -96,12 +96,14 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
return Attempt.failure(InstanceFailureReason.INVALID_FORMAT);
}
String worldName = items[0];
String worldNameOrAlias = items[0];
String positionStr = items[1];
World world = getLoadedMultiverseWorld(worldName).flatMap(LoadedMultiverseWorld::getBukkitWorld).getOrNull();
World world = getLoadedMultiverseWorld(worldNameOrAlias)
.flatMap(LoadedMultiverseWorld::getBukkitWorld)
.getOrNull();
if (world == null) {
return Attempt.failure(InstanceFailureReason.WORLD_NOT_FOUND, Replace.WORLD.with(worldName));
return Attempt.failure(InstanceFailureReason.WORLD_NOT_FOUND, Replace.WORLD.with(worldNameOrAlias));
}
EntityPosition position;
@@ -111,7 +113,7 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e));
}
return Attempt.success(new ExactDestinationInstance(this, worldManager, worldName, position));
return Attempt.success(new ExactDestinationInstance(this, worldManager, world.getName(), position));
}
//TODO: Extract to a world finder class
@@ -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}
*/
@@ -2,8 +2,6 @@ package org.mvplugins.multiverse.core.display.handlers;
import java.util.List;
import co.aikar.commands.BukkitCommandIssuer;
import org.bukkit.ChatColor;
import org.bukkit.command.ConsoleCommandSender;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
@@ -69,12 +67,12 @@ public class PagedSendHandler extends BaseSendHandler<PagedSendHandler> {
private void sendPaged(@NotNull MVCommandIssuer issuer, @NotNull List<String> content) {
int totalPages = (content.size() + linesPerPage - 1) / linesPerPage; // Basically just divide round up
if (targetPage < 1 || targetPage > totalPages) {
issuer.sendMessage(String.format("%sInvalid page number. Please enter a page number between 1 and %s", ChatColor.RED, totalPages));
issuer.sendMessage(MVCorei18n.CONTENTDISPLAY_INVALIDPAGE, replace("{total}").with(totalPages));
return;
}
if (filter.needToFilter()) {
issuer.sendMessage("{page} {filter}",
issuer.sendMessage(MVCorei18n.CONTENTDISPLAY_PAGEFILTER,
replace("{page}").with(Message.of(MVCorei18n.CONTENTDISPLAY_PAGE,
replace("{current}").with(targetPage),
replace("{total}").with(totalPages))),
@@ -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);
}
@@ -7,8 +7,10 @@
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.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
@@ -18,12 +20,17 @@ import org.bukkit.event.entity.EntityRegainHealthEvent;
import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason;
import org.bukkit.event.entity.EntitySpawnEvent;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.bukkit.event.world.ChunkLoadEvent;
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;
import java.util.Arrays;
/**
* Multiverse's Entity {@link Listener}.
*/
@@ -77,6 +84,37 @@ 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.
*
@@ -98,7 +136,7 @@ final class MVEntityListener implements CoreListener {
worldManager.getLoadedWorld(event.getEntity().getWorld())
.peek(world -> {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntity())) {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntityType())) {
Logging.finest("Cancelling Creature Spawn Event for: " + event.getEntity());
event.setCancelled(true);
}
@@ -117,10 +155,26 @@ final class MVEntityListener implements CoreListener {
worldManager.getLoadedWorld(event.getEntity().getWorld())
.peek(world -> {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntity())) {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntityType())) {
Logging.finest("Cancelling Entity Spawn Event for: " + event.getEntity());
event.setCancelled(true);
}
});
}
@EventMethod
void chunkLoad(ChunkLoadEvent event) {
worldManager.getLoadedWorld(event.getWorld())
.peek(world -> {
long count = Arrays.stream(event.getChunk().getEntities())
.filter(entity -> !(entity instanceof Player))
.filter(entity -> !world.getEntitySpawnConfig().shouldAllowSpawn(entity))
.peek(Entity::remove)
.count();
if (count > 0) {
Logging.finest("Removed %d entities from chunk %d, %d in world %s due to spawn category settings.",
count, event.getChunk().getX(), event.getChunk().getZ(), event.getWorld().getName());
}
});
}
}
@@ -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;
@@ -38,6 +36,7 @@ import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority;
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;
@@ -245,7 +244,7 @@ final class MVPlayerListener implements CoreListener {
}
private void teleportToDestinationOnJoin(Player player, DestinationInstance<?, ?> destination) {
asyncSafetyTeleporter.to(destination).teleportSingle(player)
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",
@@ -257,9 +256,9 @@ final class MVPlayerListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
@DefaultEventPriority(EventPriority.MONITOR)
@EventPriorityKey("mvcore-player-changed-world")
@DefaultEventPriority(EventPriority.NORMAL)
void playerChangedWorld(PlayerChangedWorldEvent event) {
// Permissions now determine whether or not to handle a gamemode.
this.handleGameModeAndFlight(event.getPlayer(), event.getPlayer().getWorld());
}
@@ -271,12 +270,28 @@ final class MVPlayerListener implements CoreListener {
@EventPriorityKey("mvcore-player-teleport")
@DefaultEventPriority(EventPriority.HIGHEST)
void playerTeleport(PlayerTeleportEvent event) {
Logging.finer("Got teleport event for player '"
+ event.getPlayer().getName() + "' with cause '" + event.getCause() + "'");
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")) {
@@ -297,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());
@@ -341,20 +356,19 @@ final class MVPlayerListener implements CoreListener {
*/
@EventMethod
@DefaultEventPriority(EventPriority.LOWEST)
@IgnoreIfCancelled
void playerPortalCheck(PlayerPortalEvent event) {
if (event.isCancelled()) {
return;
}
if (event.getFrom().getWorld() == null) {
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);
}
@@ -367,28 +381,33 @@ final class MVPlayerListener implements CoreListener {
@EventMethod
@EventPriorityKey("mvcore-player-portal")
@DefaultEventPriority(EventPriority.HIGH)
@IgnoreIfCancelled
void playerPortal(PlayerPortalEvent event) {
if (event.isCancelled()) {
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;
}
@@ -402,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);
}
}
@@ -18,9 +18,11 @@ 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;
@@ -33,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;
}
/**
@@ -59,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.",
@@ -101,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.",
@@ -20,6 +20,25 @@ public enum MVCorei18n implements MessageKeyProvider {
// configuration
CONFIG_SAVE_FAILED,
CONFIG_NODE_NOTFOUND,
CONFIG_NODE_INVALIDYAMLKEY,
CONFIG_STRING_PARSER_INVALIDBOOLEAN,
CONFIG_STRING_PARSER_INVALIDINTEGER,
CONFIG_STRING_PARSER_INVALIDDOUBLE,
CONFIG_STRING_PARSER_INVALIDFLOAT,
CONFIG_STRING_PARSER_INVALIDLONG,
// core config
CORE_CONFIG_CUSTOMPORTALSEARCHRADIUS_NONNEGATIVE,
CORE_CONFIG_CONFIRMTIMEOUT_POSITIVE,
CORE_CONFIG_DEBUGLEVEL_RANGE,
// /mv anchor
ANCHOR_DELETE_SUCCESS,
ANCHOR_DELETE_FAILURE,
ANCHOR_LIST_HEADER,
ANCHOR_LIST_ENTRY,
ANCHOR_SET_SUCCESS,
ANCHOR_SET_FAILURE,
// /mv check
CHECK_DESCRIPTION,
@@ -91,8 +110,18 @@ public enum MVCorei18n implements MessageKeyProvider {
// /mv dumps
DUMPS_DESCRIPTION,
DUMPS_STARTING,
DUMPS_URL_LIST,
// /mv entity-spawn-config
ENTITYSPAWNCONFIG_INFO_HEADER,
ENTITYSPAWNCONFIG_INFO_CATEGORY,
ENTITYSPAWNCONFIG_INFO_SPAWN,
ENTITYSPAWNCONFIG_INFO_TICKRATE,
ENTITYSPAWNCONFIG_INFO_EXCEPTIONS,
ENTITYSPAWNCONFIG_MODIFY_SUCCESS,
ENTITYSPAWNCONFIG_MODIFY_FAILURE,
// /mv gamerule set
GAMERULE_SET_DESCRIPTION,
GAMERULE_SET_GAMERULE_DESCRIPTION,
@@ -119,7 +148,12 @@ public enum MVCorei18n implements MessageKeyProvider {
// /mv generators
GENERATORS_DESCRIPTION,
GENERATORS_DESCRIPTION_FLAGS,
GENERATORS_HEADER,
GENERATORS_EMPTY,
GENERATORS_INFO_PLUGIN,
GENERATORS_INFO_EXAMPLEUSAGES,
GENERATORS_INFO_USAGES,
GENERATORS_INFO_INFOLINK,
// /mv import
IMPORT_DESCRIPTION,
@@ -146,6 +180,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,
@@ -158,6 +205,13 @@ public enum MVCorei18n implements MessageKeyProvider {
MODIFY_FAILURE,
MODIFY_FAILURE_NOVALUE,
// /mv purge-all-entities
PURGEALLENTITIES_SUCCESS,
PURGEALLENTITIES_SUCCESS_CATEGORIES,
// /mv purge-entities
PURGEENTITIES_SUCCESS,
// /mv regen
REGEN_DESCRIPTION,
REGEN_WORLD_DESCRIPTION,
@@ -192,6 +246,8 @@ public enum MVCorei18n implements MessageKeyProvider {
// /mv spawn
SPAWN_DESCRIPTION,
SPAWN_PLAYER_DESCRIPTION,
SPAWN_NOTMVWORLD,
SPAWN_NOPERMISSION,
SPAWN_SUCCESS,
SPAWN_FAILED,
@@ -200,6 +256,9 @@ public enum MVCorei18n implements MessageKeyProvider {
TELEPORT_PLAYER_DESCRIPTION,
TELEPORT_DESTINATION_DESCRIPTION,
TELEPORT_TOOMANYPLAYERS,
TELEPORT_NOPERMISSION_SELF,
TELEPORT_NOPERMISSION_OTHER,
TELEPORT_NOPERMISSION_ALL,
TELEPORT_SUCCESS,
TELEPORT_FAILED,
@@ -250,9 +309,26 @@ public enum MVCorei18n implements MessageKeyProvider {
WORLDBORDER_WARNINGTIME_NOTHINGCHANGED,
WORLDBORDER_WARNINGTIME_SUCCESS,
WORLDBORDER_ROUNDOFF_WARNING,
// commands error
COMMANDS_ERROR_PLAYERSONLY,
COMMANDS_ERROR_MULTIVERSEWORLDONLY,
COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_PLAYERSONLY,
COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_ISSUER,
COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_INPUTCONSOLE,
COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_INPUT,
COMMANDS_ERROR_MULTIVERSEWORLD_PLAYERSONLY,
COMMANDS_ERROR_MULTIVERSEWORLD_ISSUER,
COMMANDS_ERROR_MULTIVERSEWORLD_INPUTCONSOLE,
COMMANDS_ERROR_MULTIVERSEWORLD_INPUT,
COMMANDS_ERROR_PLAYER_ISSUERINPUT,
COMMANDS_ERROR_PLAYER_INPUT,
COMMANDS_ERROR_PLAYER_SELECTOR,
COMMANDS_ERROR_PLAYERS_INPUT,
COMMANDS_ERROR_RESOLVE_PLAYER,
COMMANDS_ERROR_RESOLVE_INPUT,
COMMANDS_ERROR_REMOVEPLAYERS_NODEFAULT,
// entry check
ENTRYCHECK_BLACKLISTED,
@@ -269,7 +345,10 @@ public enum MVCorei18n implements MessageKeyProvider {
// multiverse parse destination failure reason
DESTINATION_ANCHOR_FAILUREREASON_ANCHORNOTFOUND,
DESTINATION_ANCHOR_DISPLAY,
DESTINATION_BED_FAILUREREASON_PLAYERNOTFOUND,
DESTINATION_BED_DISPLAY_OWN,
DESTINATION_BED_DISPLAY_OTHER,
DESTINATION_CANNON_FAILUREREASON_INVALIDFORMAT,
DESTINATION_EXACT_FAILUREREASON_INVALIDFORMAT,
DESTINATION_PLAYER_FAILUREREASON_PLAYERNOTFOUND,
@@ -278,6 +357,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 +375,7 @@ public enum MVCorei18n implements MessageKeyProvider {
CLONEWORLD_WORLDEXISTFOLDER,
CLONEWORLD_WORLDEXISTUNLOADED,
CLONEWORLD_WORLDEXISTLOADED,
CLONEWORLD_FROMWORLDFOLDERINVALID,
CLONEWORLD_COPYFAILED,
CREATEWORLD_INVALIDWORLDNAME,
@@ -309,12 +393,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,
@@ -328,6 +414,9 @@ public enum MVCorei18n implements MessageKeyProvider {
WORLDCREATOR_BUKKITCREATIONFAILED,
// queue command result
QUEUECOMMAND_DEFAULTPROMPT,
QUEUECOMMAND_PROMPT,
QUEUECOMMAND_EXPIRED,
QUEUECOMMAND_NOCOMMANDINQUEUE,
QUEUECOMMAND_INVALIDOTP,
QUEUECOMMAND_COMMANDEXECUTIONERROR,
@@ -336,6 +425,7 @@ public enum MVCorei18n implements MessageKeyProvider {
CONTENTDISPLAY_NOCONTENT,
CONTENTDISPLAY_FILTER,
CONTENTDISPLAY_PAGE,
CONTENTDISPLAY_PAGEFILTER,
CONTENTDISPLAY_INVALIDPAGE,
CONTENTDISPLAY_NULL,
CONTENTDISPLAY_EMPTY,
@@ -351,12 +441,21 @@ public enum MVCorei18n implements MessageKeyProvider {
EXCEPTION_POSITIONPARSE_INVALIDCOORDINATES,
EXCEPTION_POSITIONPARSE_INVALIDNUMBER,
// world key parse failures
WORLDKEYPARSE_EMPTY,
WORLDKEYPARSE_INVALIDWORLDNAME,
WORLDKEYPARSE_INVALIDNAMESPACEDKEY,
WORLDKEYPARSE_NAMESPACEDKEYUNSUPPORTED,
// generic
GENERIC_SUCCESS,
GENERIC_FAILURE,
GENERIC_ERROR,
GENERIC_ERROR_DETAILS,
GENERIC_NULL,
GENERIC_YOU,
GENERIC_PLAYERCOUNT,
GENERIC_TELEPORTPLAYERS_FAILED,
;
// END CHECKSTYLE-SUPPRESSION: Javadoc
@@ -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}")),
@@ -147,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.addFileResClassLoader(this);
pluginLocales.addMessageBundles(this.getName().toLowerCase(Locale.ENGLISH));
pluginLocales.loadLanguages();
})
.onFailure(e -> {
Logging.severe("Failed to register locales");
@@ -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);
@@ -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;
@@ -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);
@@ -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");
}
}
@@ -0,0 +1,5 @@
/**
* Contains classes to handle compatibility due to differing API methods across different server versions and the
* API gaps between PaperMC and Spigot.
*/
package org.mvplugins.multiverse.core.utils.compatibility;
@@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core.utils.result;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import io.vavr.control.Either;
@@ -19,6 +20,38 @@ import org.mvplugins.multiverse.core.locale.message.MessageReplacement;
*/
public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Success, Attempt.Failure {
/**
* Creates a new success attempt.
*
* @param value The value.
* @param <T> The type of the value.
* @param <F> The type of failure reason.
* @return The new success attempt.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
static <T, F extends FailureReason> Attempt.Success<T, F> successRef(T value) {
return new Success<>(value);
}
/**
* Creates a new failure attempt.
*
* @param failureReason The reason for failure.
* @param messageReplacements The replacements for the failure message.
* @param <T> The type of the value.
* @param <F> The type of failure reason.
* @return The new failure attempt.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
static <T, F extends FailureReason> Attempt.Failure<T, F> failureRef(
F failureReason, MessageReplacement... messageReplacements) {
return new Failure<>(failureReason, Message.of(failureReason, "Failed!", messageReplacements));
}
/**
* Creates a new success attempt.
*
@@ -28,7 +61,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @return The new success attempt.
*/
static <T, F extends FailureReason> Attempt<T, F> success(T value) {
return new Success<>(value);
return successRef(value);
}
/**
@@ -42,7 +75,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
*/
static <T, F extends FailureReason> Attempt<T, F> failure(
F failureReason, MessageReplacement... messageReplacements) {
return new Failure<>(failureReason, Message.of(failureReason, "Failed!", messageReplacements));
return failureRef(failureReason, messageReplacements);
}
/**
@@ -203,6 +236,33 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
*/
<U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper);
/**
* Fails this attempt with the given failure attempt if the value matches the given predicate.This will do nothing
* if this is already a failure attempt.
*
* @param predicate The predicate to test the value.
* @param failureAttempt The failure attempt to return if the predicate matches.
* @return The new attempt
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
Attempt<T, F> failIf(Predicate<? super T> predicate, Supplier<Attempt.Failure<T, F>> failureAttempt);
/**
* Fails this attempt with the given failure attempt if the value matches the given predicate.This will do nothing
* if this is already a failure attempt.
*
* @param predicate The predicate to test the value.
* @param failureAttempt The failure attempt to return if the predicate matches, which can be generated based
* on the value.
* @return The new attempt
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
Attempt<T, F> failIf(Predicate<? super T> predicate, Function<? super T, Attempt.Failure<T, F>> failureAttempt);
/**
* Maps to another attempt with a different fail reason.
*
@@ -370,6 +430,16 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
return mapper.get();
}
@Override
public Attempt<T, F> failIf(Predicate<? super T> predicate, Supplier<Attempt.Failure<T, F>> failureAttempt) {
return predicate.test(value) ? failureAttempt.get() : this;
}
@Override
public Attempt<T, F> failIf(Predicate<? super T> predicate, Function<? super T, Failure<T, F>> failureAttempt) {
return predicate.test(value) ? failureAttempt.apply(value) : this;
}
@Override
public <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
return changeFailureType();
@@ -532,6 +602,16 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
return changeValueType();
}
@Override
public Attempt<T, F> failIf(Predicate<? super T> predicate, Supplier<Attempt.Failure<T, F>> failureAttempt) {
return this;
}
@Override
public Attempt<T, F> failIf(Predicate<? super T> predicate, Function<? super T, Failure<T, F>> failureAttempt) {
return this;
}
@Override
public <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
return new Failure<>(failureReason, getFailureMessage(), this);
@@ -0,0 +1,274 @@
package org.mvplugins.multiverse.core.utils.tick;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.time.LocalTime;
import java.time.temporal.TemporalUnit;
import java.util.concurrent.TimeUnit;
/**
* Represents a duration in Minecraft game ticks.
* <p>
* This class wraps a {@link Duration} and provides convenient conversion methods
* between game ticks (50ms per tick), game days (24,000 ticks), and other time units.
* Game time is mapped to real-world seconds via: realSeconds = (ticks * 3.6) + 6*3600 (mod 24h),
* which means 00:00 maps to 06:00 in-game time.
* </p>
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public final class TickDuration {
/**
* Parses a time string into a {@link TickDuration}.
* <br />
* Formats supported:
* <ul>
* <li>{@code "123"} - raw tick count</li>
* <li>{@code "10s"} - seconds</li>
* <li>{@code "5d"} - game days</li>
* </ul>
*
* @param timeString the time string to parse
* @return a {@link Try} containing the parsed {@link TickDuration}, or a failure if parsing fails
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static Try<TickDuration> parseString(@NotNull String timeString) {
return Try.of(() -> {
if (timeString.endsWith("s")) {
String subString = timeString.substring(0, timeString.length() - 1);
return of(Long.parseLong(subString), TimeUnit.SECONDS);
} else if (timeString.endsWith("d")) {
String subString = timeString.substring(0, timeString.length() - 1);
return ofGameDays(Long.parseLong(subString));
}
return ofTicks(Long.parseLong(timeString));
});
}
/**
* Creates a {@link TickDuration} from a {@link Duration}.
*
* @param duration the duration
* @return a new {@link TickDuration}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Contract(value = "_ -> new", pure = true)
public static @NotNull TickDuration ofDuration(@NotNull Duration duration) {
return new TickDuration(duration);
}
/**
* Creates a {@link TickDuration} from seconds (as a {@link Duration}).
*
* @param duration the duration representing seconds
* @return a new {@link TickDuration}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Contract(value = "_ -> new", pure = true)
public static @NotNull TickDuration ofSeconds(@NotNull Duration duration) {
return new TickDuration(duration);
}
/**
* Creates a {@link TickDuration} from a tick count.
*
* @param ticks the number of game ticks
* @return a new {@link TickDuration}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Contract(value = "_ -> new", pure = true)
public static @NotNull TickDuration ofTicks(long ticks) {
return of(ticks, TickUnit.TICK);
}
/**
* Creates a {@link TickDuration} from a game day count.
*
* @param gameDays the number of game days (24,000 ticks each)
* @return a new {@link TickDuration}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Contract(value = "_ -> new", pure = true)
public static @NotNull TickDuration ofGameDays(long gameDays) {
return of(gameDays, TickUnit.GAME_DAY);
}
/**
* Creates a {@link TickDuration} from a time value and {@link TimeUnit}.
*
* @param time the time value
* @param unit the time unit
* @return a new {@link TickDuration}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Contract(value = "_, _ -> new", pure = true)
public static @NotNull TickDuration of(long time, @NotNull TimeUnit unit) {
return of(time, unit.toChronoUnit());
}
/**
* Creates a {@link TickDuration} from a time value and {@link TemporalUnit}.
*
* @param time the time value
* @param unit the temporal unit
* @return a new {@link TickDuration}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Contract(value = "_, _ -> new", pure = true)
public static @NotNull TickDuration of(long time, @NotNull TemporalUnit unit) {
return new TickDuration(Duration.of(time, unit));
}
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;
private final Duration duration;
/**
* Creates a {@link TickDuration} from a {@link Duration}.
*
* @param duration the underlying duration
*/
private TickDuration(@NotNull Duration duration) {
this.duration = duration;
}
/**
* Converts this duration to Minecraft local time.
* <p>
* Maps the duration to real-world seconds using the formula:
* realSeconds = (ticks * 3.6) + 6*3600 (mod 24h)
* </p>
*
* @return the {@link LocalTime} in Minecraft time (where 00:00 = 06:00 in-game)
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull LocalTime toLocalTime() {
// Minecraft maps ticks -> seconds by: realSeconds = (ticks * 3.6) + 6*3600 (mod 24h)
long realSeconds = (long) ((toTicks() * TIME_MULTIPLIER) + START_OFFSET) % DAY_SECONDS;
return LocalTime.ofSecondOfDay(realSeconds);
}
/**
* Converts this duration to game ticks.
*
* @return the number of game ticks (1 tick = 50ms)
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public long toTicks() {
return duration.toMillis() / TickUnit.TICK.inMillis();
}
/**
* Converts this duration to game days.
*
* @return the number of game days (1 day = 24,000 ticks)
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public double toGameDays() {
return (double) duration.toMillis() / (double) TickUnit.GAME_DAY.inMillis();
}
/**
* Converts this duration to seconds.
*
* @return the duration in seconds
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public double toSeconds() {
return to(TimeUnit.SECONDS);
}
/**
* Converts this duration to a specified {@link TimeUnit}.
*
* @param unit the target time unit
* @return the duration in the specified unit
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public double to(@NotNull TimeUnit unit) {
return to(unit.toChronoUnit());
}
/**
* Converts this duration to a specified {@link TemporalUnit}.
*
* @param unit the target temporal unit
* @return the duration in the specified unit
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public double to(@NotNull TemporalUnit unit) {
return (double) duration.toMillis() / (double) unit.getDuration().toMillis();
}
/**
* Checks whether this duration is an exact multiple of the given {@link TimeUnit}.
*
* @param unit the time unit to test against
* @return {@code true} if this duration divides evenly by the unit, otherwise {@code false}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public boolean isExactTo(@NotNull TimeUnit unit) {
return isExactTo(unit.toChronoUnit());
}
/**
* Checks whether this duration is an exact multiple of the given {@link TemporalUnit}.
*
* @param unit the temporal unit to test against
* @return {@code true} if this duration divides evenly by the unit, otherwise {@code false}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public boolean isExactTo(@NotNull TemporalUnit unit) {
return duration.toMillis() % unit.getDuration().toMillis() == 0;
}
/**
* Returns the underlying {@link Duration}.
*
* @return the duration
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public Duration getDuration() {
return duration;
}
}
@@ -0,0 +1,112 @@
package org.mvplugins.multiverse.core.utils.tick;
import org.jetbrains.annotations.ApiStatus;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalUnit;
/**
* A {@link TemporalUnit} implementation for Minecraft game ticks.
* <br />
* Provides two standard units:
* <ul>
* <li>{@link #TICK} - A single game tick (50 milliseconds)</li>
* <li>{@link #GAME_DAY} - A full game day (24,000 ticks)</li>
* </ul>
* This unit is not date-based and is precisely defined without estimation.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public final class TickUnit implements TemporalUnit {
private static final long SINGLE_TICK_DURATION_MS = 50L;
private static final long TICKS_PER_GAME_DAY = 24_000L;
/**
* A single game tick (50 milliseconds).
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static final TickUnit TICK = new TickUnit(SINGLE_TICK_DURATION_MS);
/**
* A full game day (24,000 ticks = 20 minutes in real time).
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static final TickUnit GAME_DAY = new TickUnit(SINGLE_TICK_DURATION_MS * TICKS_PER_GAME_DAY);
private final long milliseconds;
private final Duration duration;
private TickUnit(long milliseconds) {
this.milliseconds = milliseconds;
this.duration = Duration.ofMillis(milliseconds);
}
/**
* Gets the duration of this unit in milliseconds.
*
* @return the duration in milliseconds
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public long inMillis() {
return milliseconds;
}
/**
* {@inheritDoc}
*/
@Override
public Duration getDuration() {
return duration;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isDurationEstimated() {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isDateBased() {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isTimeBased() {
return true;
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked") // following ChronoUnit#addTo
@Override
public <R extends Temporal> R addTo(R temporal, long amount) {
return (R) temporal.plus(duration.multipliedBy(amount));
}
/**
* {@inheritDoc}
*/
@Override
public long between(final Temporal start, final Temporal end) {
return start.until(end, ChronoUnit.MILLIS) / milliseconds;
}
}
@@ -16,6 +16,9 @@ import javax.net.ssl.HttpsURLConnection;
* HTTP API-client.
*/
abstract sealed class HttpAPIClient permits PasteService, URLShortener {
static final String DUMPS_VIEWER_URL = "https://mvdumps.c0ding.party/";
/**
* The URL for this API-request, and if necessary, the access token.
* If an access token is not necessary, it should be set to null.
@@ -34,7 +34,9 @@ final class McloGsPasteService extends PasteService {
public String postData(String data) throws PasteFailedException {
try {
String stringJSON = this.exec(encodeData(data), ContentType.URLENCODED); // Execute request
return String.valueOf(((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(stringJSON)).get("url")); // Interpret result
return DUMPS_VIEWER_URL +
((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(stringJSON)).get("id") +
"?service=mclogs";
} catch (IOException | ParseException e) {
throw new PasteFailedException(e);
}
@@ -44,7 +46,9 @@ final class McloGsPasteService extends PasteService {
public String postData(Map<String, String> data) throws PasteFailedException {
try {
String stringJSON = this.exec(encodeData(data), ContentType.JSON); // Execute request
return String.valueOf(((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(stringJSON)).get("url")); // Interpret result
return DUMPS_VIEWER_URL +
((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(stringJSON)).get("id") +
"?service=mclogs";
} catch (IOException | ParseException e) {
throw new PasteFailedException(e);
}
@@ -26,7 +26,7 @@ final class PastebinPasteService extends PasteService {
"&" + URLEncoder.encode("api_option", StandardCharsets.UTF_8) + "=" + URLEncoder.encode("paste", StandardCharsets.UTF_8) +
"&" + URLEncoder.encode("api_paste_code", StandardCharsets.UTF_8) + "=" + URLEncoder.encode(data, StandardCharsets.UTF_8) +
"&" + URLEncoder.encode("api_paste_private", StandardCharsets.UTF_8) + "=" + URLEncoder.encode(this.isPrivate ? "1" : "0", StandardCharsets.UTF_8) +
"&" + URLEncoder.encode("api_paste_format", StandardCharsets.UTF_8) + "=" + URLEncoder.encode("yaml", StandardCharsets.UTF_8) +
"&" + URLEncoder.encode("api_paste_format", StandardCharsets.UTF_8) + "=" + URLEncoder.encode("json", StandardCharsets.UTF_8) +
"&" + URLEncoder.encode("api_paste_name", StandardCharsets.UTF_8) + "=" + URLEncoder.encode("Multiverse-Core Debug Info", StandardCharsets.UTF_8);
}
@@ -39,8 +39,9 @@ final class PastesDevPasteService extends PasteService {
@Override
public String postData(String data) throws PasteFailedException {
try {
String stringJSON = this.exec(encodeData(data), ContentType.PLAINTEXT_YAML);
return "https://pastes.dev/" + ((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(stringJSON)).get("key");
String stringJSON = this.exec(encodeData(data), ContentType.JSON);
return DUMPS_VIEWER_URL +
((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(stringJSON)).get("key");
} catch (IOException | ParseException e) {
throw new PasteFailedException(e);
}
@@ -39,7 +39,7 @@ public final class LoadedMultiverseWorld extends MultiverseWorld {
@NotNull LocationManipulation locationManipulation,
@NotNull EntityPurger entityPurger
) {
super(world.getName(), worldConfig, config);
super(worldConfig, config);
this.worldUid = world.getUID();
this.blockSafety = blockSafety;
this.locationManipulation = locationManipulation;
@@ -77,7 +77,7 @@ public final class LoadedMultiverseWorld extends MultiverseWorld {
Logging.fine("Spawn location from world.dat file was unsafe!!");
Logging.fine("NOT adjusting spawn for '" + this.getAliasOrName() + "' because you told me not to.");
Logging.fine("To turn on spawn adjustment for this world simply type:");
Logging.fine("/mvm set adjustspawn true " + this.getAliasOrName());
Logging.fine("/mv modify %s set adjust-spawn true", getName());
return location;
}
@@ -173,7 +173,7 @@ public final class LoadedMultiverseWorld extends MultiverseWorld {
* {@inheritDoc}
*/
@Override
void setWorldConfig(WorldConfig worldConfig) {
void setWorldConfig(@NotNull WorldConfig worldConfig) {
super.setWorldConfig(worldConfig);
setupWorldConfig(getBukkitWorld().get());
}
@@ -184,7 +184,8 @@ public final class LoadedMultiverseWorld extends MultiverseWorld {
@Override
public String toString() {
return "LoadedMultiverseWorld{"
+ "name='" + worldName + "', "
+ "key='" + getKey() + "', "
+ "name='" + getName() + "', "
+ "env='" + getEnvironment() + "', "
+ "type='" + getWorldType().getOrNull() + "', "
+ "gen='" + getGenerator() + "'"
@@ -1,21 +1,29 @@
package org.mvplugins.multiverse.core.world;
import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.base.Strings;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.Difficulty;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
import org.mvplugins.multiverse.core.world.helpers.WorldFolderResolver;
import org.mvplugins.multiverse.core.world.location.SpawnLocation;
import org.mvplugins.multiverse.core.world.entity.EntitySpawnConfig;
@@ -23,11 +31,6 @@ import org.mvplugins.multiverse.core.world.entity.EntitySpawnConfig;
* Represents a world handled by Multiverse which has all the custom properties provided by Multiverse.
*/
public sealed class MultiverseWorld permits LoadedMultiverseWorld {
/**
* This world's name.
*/
protected final String worldName;
/**
* This world's configuration.
*/
@@ -36,8 +39,7 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
protected final CoreConfig config;
private String colourlessAlias = "";
MultiverseWorld(String worldName, WorldConfig worldConfig, CoreConfig config) {
this.worldName = worldName;
MultiverseWorld(WorldConfig worldConfig, CoreConfig config) {
this.worldConfig = worldConfig;
this.config = config;
this.worldConfig.setMVWorld(this);
@@ -45,15 +47,46 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
}
/**
* Gets the name of this world. The name cannot be changed.
* The key that represents this world. This should now be used as the unique key for the world instead
* of the world name. Also note that the key cannot be modified.
*
* @return The world key
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public NamespacedKey getKey() {
return worldConfig.getWorldKeyOrName().usableKey();
}
/**
* Gets the name of this world. Prefer to use {@link #getKey()} as unique id instead of this name.
* Also note that the name cannot be modified.
* <br/>
* Note for plugin developers: Usually {@link #getAliasOrName()}
* is what you want to use instead of this method.
* Note for plugin developers: Usually {@link #getAliasOrName()}is what you want to use instead of this method.
*
* @return The name of the world as a String.
*/
@NotNull
public String getName() {
return worldName;
return worldConfig.getLegacyWorldName();
}
/**
* Gets the folder where all the world contents are stored in. Generally, the location of the folder is at the server
* roots directory for pre-26.1 servers, and at "[level]/dimensions" folder under the world level directory for
* 26.1+ PaperMC servers.
* <br />
* Note this folder location is based on Multiverse's understanding of Paper and Spigot's folder structure. If the
* server software does something weird, this folder will not reflect the actual world folder location.
* <br />
* If the world is loaded, you should use {@link World#getWorldFolder()} instead as it is more accurate.
* This method is more of a fallback for when the world is not loaded.
*
* @return The world folder.
*/
@ApiStatus.AvailableSince("5.7")
public File getOfflineWorldFolder() {
return WorldFolderResolver.resolve(this);
}
/**
@@ -66,7 +99,8 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
}
/**
* Gets whether this world is loaded.
* Gets whether this world is loaded. Note that this may return false even if this is a stored instance of
* {@link LoadedMultiverseWorld} as worlds can be unloaded anytime.
*
* @return True if the world is loaded, else false.
*/
@@ -74,6 +108,23 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
return worldConfig.isLoadedWorld();
}
/**
* Attempts to get loaded world instance. Only returns a value if the world is loaded at the time of calling, even
* if this is a stored instance of {@link LoadedMultiverseWorld}.
* <br/>
* You may use {@link #isLoaded()} to check if the world is loaded first.
*
* @return Loaded world instance wrapped in an Option, or None if the world is not loaded.
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public Option<LoadedMultiverseWorld> asLoadedWorld() {
return Option.of(worldConfig.getMVWorld())
.filter(world -> world instanceof LoadedMultiverseWorld)
.map(world -> (LoadedMultiverseWorld) world);
}
/**
* Gets the properties handler of this world.
*
@@ -124,7 +175,7 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
* @return The alias of the world as a String.
*/
public String getAliasOrName() {
return Strings.isNullOrEmpty(worldConfig.getAlias()) ? worldName : worldConfig.getAlias();
return Strings.isNullOrEmpty(worldConfig.getAlias()) ? getName() : worldConfig.getAlias();
}
/**
@@ -373,6 +424,19 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
return worldConfig.getGenerator();
}
/**
* Gets the generator settings used to create this world. You cannot change this after world creation.
* Changing this manually in config on existing world will have no effect on the world generation.
*
* @return The generator settings string.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public String getGeneratorSettings() {
return worldConfig.getGeneratorSettings();
}
/**
* Gets whether or not this world will display in chat, mvw and mvl regardless if a user has the
* access permissions to go to this world.
@@ -434,6 +498,59 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
return worldConfig.setKeepSpawnInMemory(keepSpawnInMemory);
}
/**
* Get all meta key-value pairs for this world. This is an unmodifiable view of the internal map, so changes to
* the world meta will be reflected in this map, but attempts to modify this map will throw an exception.
*
* @return An unmodifiable view of all meta key-value pairs for this world.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @UnmodifiableView Map<String, String> getAllMeta() {
return Collections.unmodifiableMap(worldConfig.getMeta());
}
/**
* Gets the meta value for the given key if it exists.
*
* @param key The meta key to look up, may be {@code null} in which case an empty result is returned.
* @return An {@link Option} containing the meta value if present, otherwise an empty {@link Option}.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Option<String> getMeta(@Nullable String key) {
return Option.of(worldConfig.getMeta().get(key));
}
/**
* Sets a meta key-value pair for this world. Existing value for the key will be replaced.
*
* @param key The meta key to set, must not be {@code null}.
* @param value The meta value to set.
* @return Result of setting the property.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Try<Void> setMeta(@NotNull String key, @Nullable String value) {
return worldConfig.setMeta(key, value);
}
/**
* Removes the meta entry associated with the given key.
*
* @param key The meta key to remove.
* @return Result of removing the property.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Try<Void> removeMeta(@NotNull String key) {
return worldConfig.removeMeta(key);
}
/**
* Gets the player limit for this world after which players without an override
* permission node will not be allowed in. A value of -1 or less signifies no limit
@@ -669,7 +786,7 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
*
* @return The world config.
*/
WorldConfig getWorldConfig() {
@NotNull WorldConfig getWorldConfig() {
return worldConfig;
}
@@ -678,7 +795,7 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
*
* @param worldConfig The world config.
*/
void setWorldConfig(WorldConfig worldConfig) {
void setWorldConfig(@NotNull WorldConfig worldConfig) {
this.worldConfig = worldConfig;
}
@@ -688,7 +805,8 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
@Override
public String toString() {
return "MultiverseWorld{"
+ "name='" + worldName + "', "
+ "key='" + getKey() + "', "
+ "name='" + getName() + "', "
+ "env='" + getEnvironment() + "', "
+ "gen='" + getGenerator() + "'"
+ '}';
@@ -1,5 +1,7 @@
package org.mvplugins.multiverse.core.world;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import java.util.List;
/**
@@ -8,5 +10,5 @@ import java.util.List;
* @param newWorlds List of the new WorldConfigs added
* @param removedWorlds List of the worlds removed from the config
*/
record NewAndRemovedWorlds(List<WorldConfig> newWorlds, List<String> removedWorlds) {
record NewAndRemovedWorlds(List<WorldConfig> newWorlds, List<WorldKeyOrName> removedWorlds) {
}
@@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core.world;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import com.dumptruckman.minecraft.util.Logging;
@@ -29,6 +30,7 @@ import org.mvplugins.multiverse.core.config.migration.action.MoveMigratorAction;
import org.mvplugins.multiverse.core.config.migration.action.NullStringMigratorAction;
import org.mvplugins.multiverse.core.config.migration.VersionMigrator;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import org.mvplugins.multiverse.core.world.location.SpawnLocation;
import org.mvplugins.multiverse.core.world.entity.EntitySpawnConfig;
@@ -37,17 +39,17 @@ import org.mvplugins.multiverse.core.world.entity.EntitySpawnConfig;
*/
final class WorldConfig {
private final String worldName;
private final WorldKeyOrName keyOrName;
private final WorldConfigNodes configNodes;
private final MemoryConfigurationHandle configHandle;
private final StringPropertyHandle stringPropertyHandle;
WorldConfig(
@NotNull String worldName,
@NotNull WorldKeyOrName keyOrName,
@NotNull ConfigurationSection configSection,
@NotNull MultiverseCore multiverseCore) {
this.worldName = worldName;
this.configNodes = new WorldConfigNodes(multiverseCore);
this.keyOrName = keyOrName;
this.configNodes = new WorldConfigNodes(multiverseCore, keyOrName);
this.configHandle = MemoryConfigurationHandle.builder(configSection, configNodes.getNodes())
.logger(Logging.getLogger())
.migrator(migrator())
@@ -118,6 +120,11 @@ final class WorldConfig {
.addAction(MoveMigratorAction.of("spawning.animals", "spawning.animal"))
.addAction(MoveMigratorAction.of("spawning.monsters", "spawning.monster"))
.build())
.addVersionMigrator(VersionMigrator.builder(1.3)
.addAction(config -> config.set("read-only.legacy-world-name", keyOrName.usableName()))
.addAction(MoveMigratorAction.of("environment", "read-only.environment"))
.addAction(MoveMigratorAction.of("seed", "read-only.seed"))
.build())
.build();
}
@@ -132,7 +139,7 @@ final class WorldConfig {
Try<Void> save() {
return configHandle.save().onFailure(ex ->
Logging.warning("Failed to save world config for world '%s'. %s",
worldName, ex.getLocalizedMessage()));
keyOrName, ex.getLocalizedMessage()));
}
ConfigurationSection getConfigurationSection() {
@@ -143,8 +150,8 @@ final class WorldConfig {
return stringPropertyHandle;
}
String getWorldName() {
return worldName;
WorldKeyOrName getWorldKeyOrName() {
return keyOrName;
}
boolean getAdjustSpawn() {
@@ -258,14 +265,6 @@ final class WorldConfig {
return configHandle.set(configNodes.entryFeeCurrency, entryFeeCurrency);
}
World.Environment getEnvironment() {
return configHandle.get(configNodes.environment);
}
Try<Void> setEnvironment(World.Environment environment) {
return configHandle.set(configNodes.environment, environment);
}
GameMode getGameMode() {
return configHandle.get(configNodes.gamemode);
}
@@ -305,6 +304,18 @@ final class WorldConfig {
return configHandle.set(configNodes.keepSpawnInMemory, keepSpawnInMemory);
}
Map<String, String> getMeta() {
return configHandle.get(configNodes.meta);
}
Try<Void> setMeta(String key, String value) {
return configHandle.set(configNodes.meta, key, value);
}
Try<Void> removeMeta(String key) {
return configHandle.remove(configNodes.meta, key);
}
int getPlayerLimit() {
return configHandle.get(configNodes.playerLimit);
}
@@ -345,14 +356,6 @@ final class WorldConfig {
return configHandle.set(configNodes.scale, scale);
}
long getSeed() {
return configHandle.get(configNodes.seed);
}
Try<Void> setSeed(long seed) {
return configHandle.set(configNodes.seed, seed);
}
SpawnLocation getSpawnLocation() {
return configHandle.get(configNodes.spawnLocation);
}
@@ -377,10 +380,46 @@ final class WorldConfig {
return configHandle.set(configNodes.worldBlacklist, worldBlacklist);
}
World.Environment getEnvironment() {
return configHandle.get(configNodes.environment);
}
Try<Void> setEnvironment(World.Environment environment) {
return configHandle.set(configNodes.environment, environment);
}
String getGeneratorSettings() {
return configHandle.get(configNodes.generatorSettings);
}
Try<Void> setGeneratorSettings(String generatorSettings) {
return configHandle.set(configNodes.generatorSettings, generatorSettings);
}
String getLegacyWorldName() {
return configHandle.get(configNodes.legacyWorldName);
}
Try<Void> setLegacyWorldName(String legacyWorldName) {
return configHandle.set(configNodes.legacyWorldName, legacyWorldName);
}
long getSeed() {
return configHandle.get(configNodes.seed);
}
Try<Void> setSeed(long seed) {
return configHandle.set(configNodes.seed, seed);
}
void setMVWorld(@NotNull MultiverseWorld world) {
configNodes.setWorld(world);
}
MultiverseWorld getMVWorld() {
return configNodes.getWorld();
}
boolean isLoadedWorld() {
return configNodes.getWorld() instanceof LoadedMultiverseWorld;
}
@@ -392,7 +431,7 @@ final class WorldConfig {
/**
* Migrates the entry fee settings. Assumes entry fee is disabled if currency is not set.
*/
static final class EntryFeeMigrator implements MigratorAction {
private static final class EntryFeeMigrator implements MigratorAction {
@Override
public void migrate(ConfigurationSection config) {
String currency = config.getString("entry-fee.currency", "");
@@ -7,6 +7,7 @@ import java.util.Objects;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Difficulty;
import org.bukkit.GameMode;
import org.bukkit.Material;
@@ -17,6 +18,7 @@ import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.config.node.MapConfigNode;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
import org.mvplugins.multiverse.core.event.world.MVWorldPropertyChangedEvent;
import org.mvplugins.multiverse.core.config.node.ConfigNode;
@@ -24,7 +26,9 @@ import org.mvplugins.multiverse.core.config.node.ListConfigNode;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.utils.MaterialConverter;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
import org.mvplugins.multiverse.core.world.helpers.EnforcementHandler;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import org.mvplugins.multiverse.core.world.location.NullSpawnLocation;
import org.mvplugins.multiverse.core.world.location.SpawnLocation;
import org.mvplugins.multiverse.core.world.entity.EntitySpawnConfig;
@@ -39,12 +43,14 @@ final class WorldConfigNodes {
private WorldManager worldManager;
private EnforcementHandler enforcementHandler;
private CoreConfig config;
private WorldKeyOrName keyOrName;
private MultiverseWorld world = null;
WorldConfigNodes(@NotNull MultiverseCore multiverseCore) {
WorldConfigNodes(@NotNull MultiverseCore multiverseCore, @NotNull WorldKeyOrName keyOrName) {
this.worldManager = multiverseCore.getServiceLocator().getService(WorldManager.class);
this.enforcementHandler = multiverseCore.getServiceLocator().getService(EnforcementHandler.class);
this.config = multiverseCore.getServiceLocator().getService(CoreConfig.class);
this.keyOrName = keyOrName;
}
MultiverseWorld getWorld() {
@@ -88,6 +94,11 @@ final class WorldConfigNodes {
final ConfigNode<String> alias = node(ConfigNode.builder("alias", String.class)
.defaultValue("")
.onLoadAndChange((oldValue, newValue) -> {
worldManager.getWorldStore().changeAlias(
ChatTextFormatter.removeColor(oldValue),
ChatTextFormatter.removeColor(newValue),
keyOrName.usableKey()
);
if (world == null) return;
world.updateColourlessAlias();
}));
@@ -172,11 +183,6 @@ final class WorldConfigNodes {
}
}));
final ConfigNode<World.Environment> environment = node(ConfigNode
.builder("environment", World.Environment.class)
.defaultValue(World.Environment.NORMAL)
.hidden());
final ConfigNode<GameMode> gamemode = node(ConfigNode.builder("gamemode", GameMode.class)
.defaultValue(GameMode.SURVIVAL)
.onLoadAndChange((oldValue, newValue) -> {
@@ -198,11 +204,21 @@ final class WorldConfigNodes {
final ConfigNode<Boolean> keepSpawnInMemory = node(ConfigNode
.builder("keep-spawn-in-memory", Boolean.class)
.defaultValue(true)
.onLoadAndChange((oldValue, newValue) -> {
.onLoadAndChange((sender, oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.setKeepSpawnInMemory(newValue));
loadedWorld.getBukkitWorld().peek(bukkitWorld -> {
bukkitWorld.setKeepSpawnInMemory(newValue);
if (bukkitWorld.getKeepSpawnInMemory() != newValue) {
sender.sendMessage(ChatColor.RED + "Keep spawn in memory feature has been removed by " +
"Minecraft in 1.21.9+ and will no longer have any effect when set to true.");
}
});
}));
final MapConfigNode<String, String> meta = (MapConfigNode<String, String>) node(MapConfigNode
.mapBuilder("meta", String.class, String.class)
.hidden());
final ConfigNode<Integer> playerLimit = node(ConfigNode.builder("player-limit", Integer.class)
.defaultValue(-1));
@@ -234,10 +250,6 @@ final class WorldConfigNodes {
};
}));
final ConfigNode<Long> seed = node(ConfigNode.builder("seed", Long.class)
.defaultValue(Long.MIN_VALUE)
.hidden());
final ConfigNode<SpawnLocation> spawnLocation = node(ConfigNode.builder("spawn-location", SpawnLocation.class)
.defaultValue(NullSpawnLocation.get())
.hidden()
@@ -274,6 +286,22 @@ final class WorldConfigNodes {
final ConfigNode<List<String>> worldBlacklist = node(ListConfigNode.listBuilder("world-blacklist", String.class));
final ConfigNode<World.Environment> environment = node(ConfigNode
.builder("read-only.environment", World.Environment.class)
.defaultValue(World.Environment.NORMAL)
.hidden());
final ConfigNode<String> generatorSettings = node(ConfigNode.builder("read-only.generator-settings", String.class)
.defaultValue("")
.hidden());
final ConfigNode<String> legacyWorldName = node(ConfigNode.builder("read-only.legacy-world-name", String.class)
.hidden());
final ConfigNode<Long> seed = node(ConfigNode.builder("read-only.seed", Long.class)
.defaultValue(Long.MIN_VALUE)
.hidden());
final ConfigNode<Double> version = node(ConfigNode.builder("version", Double.class)
.defaultValue(CONFIG_VERSION)
.hidden());
@@ -1,14 +1,10 @@
package org.mvplugins.multiverse.core.world;
import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
@@ -17,19 +13,22 @@ import java.util.stream.Collectors;
import com.dumptruckman.minecraft.util.Logging;
import com.google.common.base.Strings;
import io.papermc.lib.PaperLib;
import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.Bukkit;
import org.bukkit.GameRule;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldBorder;
import org.bukkit.WorldCreator;
import org.bukkit.WorldType;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.CoreConfig;
@@ -48,19 +47,23 @@ import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.permissions.CorePermissions;
import org.mvplugins.multiverse.core.teleportation.BlockSafety;
import org.mvplugins.multiverse.core.teleportation.LocationManipulation;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import org.mvplugins.multiverse.core.utils.ServerProperties;
import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility;
import org.mvplugins.multiverse.core.utils.compatibility.WorldCompatibility;
import org.mvplugins.multiverse.core.utils.compatibility.WorldCreatorCompatibility;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.utils.FileUtils;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
import org.mvplugins.multiverse.core.world.biomeprovider.BiomeProviderFactory;
import org.mvplugins.multiverse.core.world.entity.EntityPurger;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
import org.mvplugins.multiverse.core.world.helpers.DataStore.GameRulesStore;
import org.mvplugins.multiverse.core.world.helpers.DataTransfer;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder;
import org.mvplugins.multiverse.core.world.helpers.PotentialWorldFinder;
import org.mvplugins.multiverse.core.world.helpers.WorldFolderResolver;
import org.mvplugins.multiverse.core.world.helpers.WorldNameChecker;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import org.mvplugins.multiverse.core.world.options.CloneWorldOptions;
import org.mvplugins.multiverse.core.world.options.CreateWorldOptions;
import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions;
@@ -87,12 +90,12 @@ import static org.mvplugins.multiverse.core.world.helpers.DataStore.WorldConfigS
@Service // SUPPRESS CHECKSTYLE: ClassFanOutComplexity This is the world manager, it's going to be complex.
public final class WorldManager {
private static final List<String> CLONE_IGNORE_FILES = Arrays.asList("uid.dat", "session.lock");
private static final DimensionFormat DEFAULT_NETHER_FORMAT = new DimensionFormat("%overworld%_nether");
private static final DimensionFormat DEFAULT_END_FORMAT = new DimensionFormat("%overworld%_the_end");
private static final List<String> CLONE_IGNORE_FILES = List.of(
"uid.dat", "session.lock", // All pre 26.1 format
"data/paper/metadata.dat" // New papermc format for 26.1+
);
private final Map<String, MultiverseWorld> worldsMap;
private final Map<String, LoadedMultiverseWorld> loadedWorldsMap;
private final WorldStore worldStore;
private final List<String> unloadTracker;
private final List<String> loadTracker;
private final WorldsConfigManager worldsConfigManager;
@@ -107,9 +110,11 @@ public final class WorldManager {
private final ServerProperties serverProperties;
private final CoreConfig config;
private final EntityPurger entityPurger;
private final Provider<PotentialWorldFinder> potentialWorldFinder;
@Inject
WorldManager(
@NotNull WorldStore worldStore,
@NotNull WorldsConfigManager worldsConfigManager,
@NotNull WorldNameChecker worldNameChecker,
@NotNull BiomeProviderFactory biomeProviderFactory,
@@ -121,7 +126,9 @@ public final class WorldManager {
@NotNull CorePermissions corePermissions,
@NotNull ServerProperties serverProperties,
@NotNull CoreConfig config,
@NotNull EntityPurger entityPurger) {
@NotNull EntityPurger entityPurger,
@NotNull Provider<PotentialWorldFinder> potentialWorldFinder) {
this.worldStore = worldStore;
this.worldsConfigManager = worldsConfigManager;
this.worldNameChecker = worldNameChecker;
this.biomeProviderFactory = biomeProviderFactory;
@@ -134,9 +141,8 @@ public final class WorldManager {
this.serverProperties = serverProperties;
this.config = config;
this.entityPurger = entityPurger;
this.potentialWorldFinder = potentialWorldFinder;
this.worldsMap = new HashMap<>();
this.loadedWorldsMap = new HashMap<>();
this.unloadTracker = new ArrayList<>();
this.loadTracker = new ArrayList<>();
}
@@ -168,19 +174,23 @@ public final class WorldManager {
}
private void loadNewWorldConfigs(Collection<WorldConfig> newWorldConfigs) {
newWorldConfigs.forEach(worldConfig -> Option.of(worldsMap.get(worldConfig.getWorldName()))
.peek(unloadedWorld -> unloadedWorld.setWorldConfig(worldConfig))
.onEmpty(() -> newMultiverseWorld(worldConfig.getWorldName(), worldConfig)));
newWorldConfigs.forEach(worldConfig -> {
worldStore.getUnloadedWorldRef(worldConfig.getWorldKeyOrName().usableName())
.peek(unloadedWorld -> unloadedWorld.setWorldConfig(worldConfig))
.onEmpty(() -> newMultiverseWorld(worldConfig));
worldStore.getLoadedWorld(worldConfig.getWorldKeyOrName().usableName())
.peek(loadedWorld -> loadedWorld.setWorldConfig(worldConfig));
});
}
private void removeWorldsNotInConfigs(Collection<String> removedWorlds) {
removedWorlds.forEach(worldName -> getWorld(worldName)
private void removeWorldsNotInConfigs(Collection<WorldKeyOrName> removedWorlds) {
removedWorlds.forEach(keyOrName -> getWorld(keyOrName.usableName())
.map(world -> removeWorld(RemoveWorldOptions.world(world)))
.getOrElse(() -> worldActionResult(RemoveFailureReason.WORLD_NON_EXISTENT, worldName))
.getOrElse(() -> worldActionResult(RemoveFailureReason.WORLD_NON_EXISTENT, keyOrName))
.onFailure(failure ->
Logging.severe("Failed to unload world %s: %s", worldName, failure))
Logging.severe("Failed to unload world %s: %s", keyOrName, failure))
.onSuccess(success ->
Logging.fine("Unloaded world %s as it was removed from config", worldName)));
Logging.fine("Unloaded world %s as it was removed from config", keyOrName)));
}
/**
@@ -192,9 +202,10 @@ public final class WorldManager {
.collect(Collectors.toMap(World::getName, Function.identity()));
serverProperties.getLevelName().peek(overworldName -> {
//TODO: check by namespaced key instead
World overworld = bukkitWorlds.remove(overworldName);
World nether = bukkitWorlds.remove(DEFAULT_NETHER_FORMAT.replaceOverworld(overworldName));
World end = bukkitWorlds.remove(DEFAULT_END_FORMAT.replaceOverworld(overworldName));
World nether = bukkitWorlds.remove(DimensionFinder.DEFAULT_NETHER_FORMAT.replaceOverworld(overworldName));
World end = bukkitWorlds.remove(DimensionFinder.DEFAULT_END_FORMAT.replaceOverworld(overworldName));
if (config.getAutoImportDefaultWorlds()) {
importExistingBukkitWorld(overworld);
@@ -241,43 +252,60 @@ public final class WorldManager {
* @return The result of the creation.
*/
public Attempt<LoadedMultiverseWorld, CreateFailureReason> createWorld(CreateWorldOptions options) {
return validateCreateWorldOptions(options).mapAttempt(this::doCreateWorld);
return parseCreateWorldOptionsKeyOrName(options).mapAttempt(this::doCreateWorld);
}
private Attempt<CreateWorldOptions, CreateFailureReason> validateCreateWorldOptions(
private Attempt<KeyOrNameWithOptions<CreateWorldOptions>, CreateFailureReason> parseCreateWorldOptionsKeyOrName(
CreateWorldOptions options) {
if (!worldNameChecker.isValidWorldName(options.worldName())) {
return worldActionResult(CreateFailureReason.INVALID_WORLDNAME, options.worldName());
} else if (getLoadedWorld(options.worldName()).isDefined()) {
return worldActionResult(CreateFailureReason.WORLD_EXIST_LOADED, options.worldName());
} else if (getWorld(options.worldName()).isDefined()) {
return worldActionResult(CreateFailureReason.WORLD_EXIST_UNLOADED, options.worldName());
} else if (options.doFolderCheck() && worldNameChecker.hasWorldFolder(options.worldName())) {
return worldActionResult(CreateFailureReason.WORLD_EXIST_FOLDER, options.worldName());
}
return worldActionResult(options);
return options.keyOrName()
.fold(WorldKeyOrName::parse, Attempt::success)
.transform(CreateFailureReason.INVALID_WORLDNAME)
.failIf(keyOrName -> !worldNameChecker.isValidWorldName(keyOrName.usableName()),
keyOrName -> worldActionResult(CreateFailureReason.INVALID_WORLDNAME, keyOrName))
.failIf(keyOrName -> keyOrName.isKey() && !WorldCreatorCompatibility.canCreateWorldWithKey(),
keyOrName -> worldActionResult(CreateFailureReason.NAMESPACEDKEY_UNSUPPORTED, keyOrName))
.failIf(worldStore::isLoadedWorld,
keyOrName -> worldActionResult(CreateFailureReason.WORLD_EXIST_LOADED, keyOrName))
.failIf(worldStore::isUnloadedWorld,
keyOrName -> worldActionResult(CreateFailureReason.WORLD_EXIST_UNLOADED, keyOrName))
.failIf(keyOrName -> options.doFolderCheck() && worldNameChecker.hasWorldFolder(keyOrName),
keyOrName -> worldActionResult(CreateFailureReason.WORLD_EXIST_FOLDER, keyOrName))
.map(keyOrName -> new KeyOrNameWithOptions<>(keyOrName, options));
}
private Attempt<LoadedMultiverseWorld, CreateFailureReason> doCreateWorld(
CreateWorldOptions options) {
String generatorString = generatorProvider.parseGeneratorString(options.worldName(), options.generator());
WorldCreator worldCreator = WorldCreator.name(options.worldName())
KeyOrNameWithOptions<CreateWorldOptions> keyOrNameWithOptions) {
WorldKeyOrName keyOrName = keyOrNameWithOptions.keyOrName();
CreateWorldOptions options = keyOrNameWithOptions.options();
String generatorString = generatorProvider.parseGeneratorString(keyOrName.usableName(), options.generator());
WorldCreator worldCreator = WorldCreatorCompatibility.ofKeyOrName(keyOrName)
.environment(options.environment())
.generateStructures(options.generateStructures())
.generatorSettings(options.generatorSettings())
.seed(options.seed())
.type(options.worldType());
return addBiomeProviderToCreator(worldCreator, options.worldName(), options.biome())
WorldCreatorCompatibility.setBonusChest(worldCreator, options.bonusChest());
options.forcedSpawnPosition().peek(position -> WorldCreatorCompatibility.setForcedSpawnPosition(worldCreator, position));
return addBiomeProviderToCreator(worldCreator, keyOrName.usableName(), options.biome())
.mapAttempt(creator -> addGeneratorToCreator(creator, generatorString))
.mapAttempt(this::createBukkitWorld)
.transform(CreateFailureReason.WORLD_CREATOR_FAILED)
.map(bukkitWorld -> newLoadedMultiverseWorld(
bukkitWorld,
generatorString,
options.biome(),
options.useSpawnAdjust()))
.peek(loadedWorld -> pluginManager.callEvent(new MVWorldCreatedEvent(loadedWorld)));
bukkitWorld,
generatorString,
options.biome(),
options.generatorSettings(),
options.useSpawnAdjust()))
.peek(loadedWorld -> postCreateWorld(loadedWorld, options));
}
private void postCreateWorld(LoadedMultiverseWorld loadedWorld, CreateWorldOptions options) {
options.worldPropertyStrings().forEach((key, value) -> loadedWorld.getStringPropertyHandle()
.setPropertyString(key, value)
.onFailure(failure -> Logging.warning("Failed to set property '%s' to '%s' for world %s: %s",
key, value, loadedWorld.getName(), failure.getMessage())));
pluginManager.callEvent(new MVWorldCreatedEvent(loadedWorld));
saveWorldsConfig();
}
/**
@@ -287,34 +315,51 @@ public final class WorldManager {
* @return The result of the import.
*/
public Attempt<LoadedMultiverseWorld, ImportFailureReason> importWorld(ImportWorldOptions options) {
String worldName = options.worldName();
if (isLoadedWorld(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_EXIST_LOADED, worldName);
} else if (isWorld(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_EXIST_UNLOADED, worldName);
}
return Option.of(Bukkit.getWorld(worldName))
.map(bukkitWorld -> doImportBukkitWorld(options, bukkitWorld))
.getOrElse(() -> validateImportWorldOptions(options).mapAttempt(this::doImportWorld));
return options.keyOrName()
.fold(WorldKeyOrName::parse, Attempt::success)
.transform(ImportFailureReason.INVALID_WORLDNAME)
.failIf(keyOrName -> !worldNameChecker.isValidWorldName(keyOrName.usableName()),
keyOrName -> worldActionResult(ImportFailureReason.INVALID_WORLDNAME, keyOrName))
.failIf(keyOrName -> keyOrName.isKey() && !WorldCreatorCompatibility.canCreateWorldWithKey(),
keyOrName -> worldActionResult(ImportFailureReason.NAMESPACEDKEY_UNSUPPORTED, keyOrName))
.failIf(worldStore::isLoadedWorld,
keyOrName -> worldActionResult(ImportFailureReason.WORLD_EXIST_LOADED, keyOrName))
.failIf(worldStore::isUnloadedWorld,
keyOrName -> worldActionResult(ImportFailureReason.WORLD_EXIST_UNLOADED, keyOrName))
.map(keyOrName -> new KeyOrNameWithOptions<>(keyOrName, options))
.mapAttempt(pair -> BukkitCompatibility.getWorldByNameOrKey(pair.keyOrName())
.map(bukkitWorld -> doImportBukkitWorld(pair, bukkitWorld))
.getOrElse(() -> validateImportWorldOptions(pair).mapAttempt(this::doImportWorld)));
}
private Attempt<ImportWorldOptions, ImportFailureReason> validateImportWorldOptions(ImportWorldOptions options) {
String worldName = options.worldName();
if (!worldNameChecker.isValidWorldName(worldName)) {
return worldActionResult(ImportFailureReason.INVALID_WORLDNAME, worldName);
} else if (options.doFolderCheck() && !worldNameChecker.isValidWorldFolder(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_FOLDER_INVALID, worldName);
private Attempt<KeyOrNameWithOptions<ImportWorldOptions>, ImportFailureReason> validateImportWorldOptions(
KeyOrNameWithOptions<ImportWorldOptions> keyOrNameWithOptions) {
WorldKeyOrName keyOrName = keyOrNameWithOptions.keyOrName();
if (keyOrNameWithOptions.options().doFolderCheck()) {
//todo This is a duplicate of folder check in load world
WorldNameChecker.FolderStatus folderStatus = worldNameChecker.checkFolder(keyOrName);
if (!folderStatus.isLoadable()) {
return worldActionResult(ImportFailureReason.WORLD_FOLDER_INVALID, keyOrName);
}
if (folderStatus == WorldNameChecker.FolderStatus.REQUIRES_MIGRATION) {
Logging.info("World '%s' will be automatically migrated by PaperMC to the new dimension " +
"location. If you face any issue with migration, please contact PaperMC support!",
keyOrName);
}
}
return worldActionResult(options);
return worldActionResult(keyOrNameWithOptions);
}
private Attempt<LoadedMultiverseWorld, ImportFailureReason> doImportWorld(
ImportWorldOptions options) {
String generatorString = generatorProvider.parseGeneratorString(options.worldName(), options.generator());
WorldCreator worldCreator = WorldCreator.name(options.worldName())
.environment(options.environment());
KeyOrNameWithOptions<ImportWorldOptions> keyOrNameWithOptions) {
WorldKeyOrName keyOrName = keyOrNameWithOptions.keyOrName();
ImportWorldOptions options = keyOrNameWithOptions.options();
String generatorString = generatorProvider.parseGeneratorString(keyOrName.usableName(), options.generator());
WorldCreator worldCreator = WorldCreatorCompatibility.ofKeyOrName(keyOrName)
.environment(options.environment())
.generatorSettings(options.generatorSettings());
return addBiomeProviderToCreator(worldCreator, options.worldName(), options.biome())
return addBiomeProviderToCreator(worldCreator, keyOrName.usableName(), options.biome())
.mapAttempt(creator -> addGeneratorToCreator(creator, generatorString))
.mapAttempt(this::createBukkitWorld)
.transform(ImportFailureReason.WORLD_CREATOR_FAILED)
@@ -322,11 +367,15 @@ public final class WorldManager {
bukkitWorld,
generatorString,
options.biome(),
options.generatorSettings(),
options.useSpawnAdjust()))
.peek(loadedWorld -> pluginManager.callEvent(new MVWorldImportedEvent(loadedWorld)));
}
private Attempt<LoadedMultiverseWorld, ImportFailureReason> doImportBukkitWorld(ImportWorldOptions options, World bukkitWorld) {
private Attempt<LoadedMultiverseWorld, ImportFailureReason> doImportBukkitWorld(
KeyOrNameWithOptions<ImportWorldOptions> keyOrNameWithOptions, World bukkitWorld) {
WorldKeyOrName keyOrName = keyOrNameWithOptions.keyOrName();
ImportWorldOptions options = keyOrNameWithOptions.options();
if (options.environment() != bukkitWorld.getEnvironment()) {
return Attempt.failure(ImportFailureReason.BUKKIT_ENVIRONMENT_MISMATCH,
Replace.WORLD.with(bukkitWorld.getName()),
@@ -334,18 +383,21 @@ public final class WorldManager {
replace("{mvEnvironment}").with(options.environment().name()));
}
//todo: check for key mismatch?
LoadedMultiverseWorld loadedWorld = newLoadedMultiverseWorld(
bukkitWorld,
generatorProvider.parseGeneratorString(options.worldName(), options.generator()),
generatorProvider.parseGeneratorString(keyOrName.usableName(), options.generator()),
options.biome(),
options.generatorSettings(),
options.useSpawnAdjust());
pluginManager.callEvent(new MVWorldImportedEvent(loadedWorld));
return Attempt.success(loadedWorld);
}
private MultiverseWorld newMultiverseWorld(String worldName, WorldConfig worldConfig) {
MultiverseWorld mvWorld = new MultiverseWorld(worldName, worldConfig, config);
worldsMap.put(mvWorld.getName().toLowerCase(Locale.ENGLISH), mvWorld);
private MultiverseWorld newMultiverseWorld(WorldConfig worldConfig) {
MultiverseWorld mvWorld = new MultiverseWorld(worldConfig, config);
worldStore.putUnloadedWorld(mvWorld);
corePermissions.addWorldPermissions(mvWorld);
return mvWorld;
}
@@ -358,22 +410,28 @@ public final class WorldManager {
* @param adjustSpawn Whether to adjust spawn.
*/
private LoadedMultiverseWorld newLoadedMultiverseWorld(
@NotNull World world, @Nullable String generator, @Nullable String biome, boolean adjustSpawn) {
WorldConfig worldConfig = worldsConfigManager.addWorldConfig(world.getName());
@NotNull World world,
@Nullable String generator,
@Nullable String biome,
@Nullable String generatorSettings,
boolean adjustSpawn) {
WorldConfig worldConfig = worldsConfigManager.addWorldConfig(world.getKey());
// Properties from multiverse input
worldConfig.setAdjustSpawn(adjustSpawn);
worldConfig.setGenerator(generator == null ? "" : generator);
worldConfig.setBiome(biome == null ? "" : biome);
worldConfig.setGeneratorSettings(generatorSettings == null ? "" : generatorSettings);
// Properties from the bukkit world
worldConfig.setLegacyWorldName(world.getName());
worldConfig.setDifficulty(world.getDifficulty());
worldConfig.setKeepSpawnInMemory(world.getKeepSpawnInMemory());
worldConfig.setScale(getCoordinateScale(world));
worldConfig.setScale(WorldCompatibility.getCoordinateScale(world));
worldConfig.save();
newMultiverseWorld(world.getName(), worldConfig);
newMultiverseWorld(worldConfig);
LoadedMultiverseWorld loadedWorld = new LoadedMultiverseWorld(
world,
worldConfig,
@@ -382,23 +440,12 @@ public final class WorldManager {
locationManipulation,
entityPurger
);
loadedWorldsMap.put(loadedWorld.getName().toLowerCase(Locale.ENGLISH), loadedWorld);
worldStore.putLoadedWorld(loadedWorld);
saveWorldsConfig();
pluginManager.callEvent(new MVWorldLoadedEvent(loadedWorld));
return loadedWorld;
}
private double getCoordinateScale(World world) {
if (PaperLib.isPaper()) {
return world.getCoordinateScale();
}
return switch (world.getEnvironment()) {
case NORMAL -> 1;
case NETHER -> 8;
default -> 1;
};
}
/**
* Loads an existing world in config.
*
@@ -465,11 +512,19 @@ public final class WorldManager {
return doLoadBukkitWorld(bukkitWorld, mvWorld);
}
if (options.doFolderCheck() && !worldNameChecker.isValidWorldFolder(mvWorld.getName())) {
return worldActionResult(LoadFailureReason.WORLD_FOLDER_INVALID, mvWorld.getName());
if (options.doFolderCheck()) {
WorldNameChecker.FolderStatus folderStatus = worldNameChecker.checkFolder(mvWorld.getOfflineWorldFolder());
if (!folderStatus.isLoadable()) {
return worldActionResult(LoadFailureReason.WORLD_FOLDER_INVALID, mvWorld.getName());
}
if (folderStatus == WorldNameChecker.FolderStatus.REQUIRES_MIGRATION) {
Logging.info("World '%s' will be automatically migrated by PaperMC to the new dimension " +
"location. If you face any issue with migration, please contact PaperMC support!",
mvWorld.getName());
}
}
WorldCreator worldCreator = WorldCreator.name(mvWorld.getName())
WorldCreator worldCreator = WorldCreatorCompatibility.ofNameAndKey(mvWorld.getKey(), mvWorld.getName())
.environment(mvWorld.getEnvironment())
.seed(mvWorld.getSeed());
return addBiomeProviderToCreator(worldCreator, mvWorld.getName(), mvWorld.getBiome())
@@ -492,7 +547,23 @@ public final class WorldManager {
}
private Attempt<LoadedMultiverseWorld, LoadFailureReason> newLoadedMultiverseWorld(MultiverseWorld mvWorld, World bukkitWorld) {
WorldConfig worldConfig = worldsConfigManager.getWorldConfig(mvWorld.getName()).get(); //TODO: null check here, but logically it should never be null.
WorldConfig worldConfig = mvWorld.getWorldConfig();
if (worldConfig.getWorldKeyOrName().isName() && mvWorld.getName().equalsIgnoreCase(bukkitWorld.getName())) {
// do migration of namespaced key
Logging.info("Migrating world config for '%s' to use namespaced key '%s'...",
mvWorld.getName(), bukkitWorld.getKey());
worldConfig = worldsConfigManager.migrateWorldConfigKey(worldConfig, bukkitWorld.getKey());
mvWorld.setWorldConfig(worldConfig);
}
if (!bukkitWorld.getKey().equals(mvWorld.getKey())) {
return Attempt.failure(LoadFailureReason.BUKKIT_NAMESPACED_KEY_MISMATCH,
Replace.WORLD.with(mvWorld.getName()),
replace("{bukkitNamespace}").with(bukkitWorld.getKey()),
replace("{mvNamespace}").with(mvWorld.getKey()));
}
LoadedMultiverseWorld loadedWorld = new LoadedMultiverseWorld(
bukkitWorld,
worldConfig,
@@ -501,7 +572,7 @@ public final class WorldManager {
locationManipulation,
entityPurger
);
loadedWorldsMap.put(loadedWorld.getName().toLowerCase(Locale.ENGLISH), loadedWorld);
worldStore.putLoadedWorld(loadedWorld);
saveWorldsConfig();
pluginManager.callEvent(new MVWorldLoadedEvent(loadedWorld));
return Attempt.success(loadedWorld);
@@ -535,12 +606,10 @@ public final class WorldManager {
success -> removeLoadedMultiverseWorld(world));
}
private Attempt<MultiverseWorld, UnloadFailureReason> removeLoadedMultiverseWorld(@NotNull LoadedMultiverseWorld world) {
MultiverseWorld mvWorld = Objects.requireNonNull(loadedWorldsMap.remove(world.getName()),
"For some reason, the loaded world isn't in the map... BUGGG");
Logging.fine("Removed MultiverseWorld from map: " + world.getName());
var unloadedWorld = Objects.requireNonNull(worldsMap.get(world.getName()),
"For some reason, the unloaded world isn't in the map... BUGGG");
private Attempt<MultiverseWorld, UnloadFailureReason> removeLoadedMultiverseWorld(@NotNull LoadedMultiverseWorld mvWorld) {
MultiverseWorld unloadedWorld = worldStore.getUnloadedWorldRef(mvWorld.getKey().toString()).getOrElseThrow(
() -> new IllegalStateException("Unloaded ref of world not found: " + mvWorld));
worldStore.removeLoadedWorld(mvWorld);
mvWorld.getWorldConfig().setMVWorld(unloadedWorld);
pluginManager.callEvent(new MVWorldUnloadedEvent(mvWorld));
return worldActionResult(unloadedWorld);
@@ -630,11 +699,15 @@ public final class WorldManager {
*/
private Attempt<String, RemoveFailureReason> removeWorldFromConfig(@NotNull MultiverseWorld world) {
// Remove world from config
worldsMap.remove(world.getName());
world.getWorldConfig().deferenceMVWorld();
worldsConfigManager.deleteWorldConfig(world.getName());
worldStore.removeWorld(world);
WorldConfig worldConfig = world.getWorldConfig();
worldConfig.deferenceMVWorld();
worldsConfigManager.deleteWorldConfig(worldConfig);
saveWorldsConfig();
corePermissions.removeWorldPermissions(world);
pluginManager.callEvent(new MVWorldRemovedEvent(world));
return worldActionResult(world.getName());
}
@@ -691,6 +764,7 @@ public final class WorldManager {
private Attempt<File, DeleteFailureReason> validateWorldToDelete(
@NotNull LoadedMultiverseWorld world) {
return world.getBukkitWorld().map(World::getWorldFolder)
.peek(folder -> Logging.finer("World folder for world %s is at: %s", world.getName(), folder.getPath()))
.filter(worldNameChecker::isValidWorldFolder)
.map(this::<File, DeleteFailureReason>worldActionResult)
.getOrElse(() -> {
@@ -706,91 +780,91 @@ public final class WorldManager {
* @return The result of the clone.
*/
public Attempt<LoadedMultiverseWorld, CloneFailureReason> cloneWorld(@NotNull CloneWorldOptions options) {
return cloneWorldValidateWorld(options)
return parseCloneWorldOptionsNewWorld(options)
.mapAttempt(this::cloneWorldCopyFolder)
.mapAttempt(validatedOptions -> {
.mapAttempt(keyOrNameWithOptions -> {
ImportWorldOptions importWorldOptions = ImportWorldOptions
.worldName(validatedOptions.newWorldName())
.biome(validatedOptions.world().getBiome())
.environment(validatedOptions.world().getEnvironment())
.generator(validatedOptions.world().getGenerator());
.worldKeyOrName(keyOrNameWithOptions.keyOrName())
.biome(options.fromWorld().getBiome())
.environment(options.fromWorld().getEnvironment())
.generator(options.fromWorld().getGenerator());
return importWorld(importWorldOptions).transform(CloneFailureReason.IMPORT_FAILED);
})
.onSuccess(newWorld -> {
cloneWorldTransferData(options, newWorld);
if (options.keepWorldConfig()) {
newWorld.setSpawnLocation(options.world().getSpawnLocation());
// Special case for spawn location as it's considered a hidden config
newWorld.setSpawnLocation(options.fromWorld().getSpawnLocation());
}
saveWorldsConfig();
pluginManager.callEvent(new MVWorldClonedEvent(newWorld, options.world()));
pluginManager.callEvent(new MVWorldClonedEvent(newWorld, options.fromWorld()));
});
}
private Attempt<CloneWorldOptions, CloneFailureReason> cloneWorldValidateWorld(
private Attempt<KeyOrNameWithOptions<CloneWorldOptions>, CloneFailureReason> parseCloneWorldOptionsNewWorld(
@NotNull CloneWorldOptions options) {
String newWorldName = options.newWorldName();
if (!worldNameChecker.isValidWorldName(newWorldName)) {
Logging.severe("Invalid world name: " + newWorldName);
return worldActionResult(CloneFailureReason.INVALID_WORLDNAME, newWorldName);
}
if (isLoadedWorld(newWorldName)) {
Logging.severe("World already loaded when attempting to clone: " + newWorldName);
return worldActionResult(CloneFailureReason.WORLD_EXIST_LOADED, newWorldName);
}
if (isWorld(newWorldName)) {
Logging.severe("World already exist unloaded: " + newWorldName);
return worldActionResult(CloneFailureReason.WORLD_EXIST_UNLOADED, newWorldName);
}
if (worldNameChecker.hasWorldFolder(newWorldName)) {
return worldActionResult(CloneFailureReason.WORLD_EXIST_FOLDER, newWorldName);
}
return worldActionResult(options);
return options.newWorldKeyOrName()
.fold(WorldKeyOrName::parse, Attempt::success)
.transform(CloneFailureReason.INVALID_WORLDNAME)
.failIf(keyOrName -> !worldNameChecker.isValidWorldName(keyOrName.usableName()),
keyOrName -> worldActionResult(CloneFailureReason.INVALID_WORLDNAME, keyOrName))
.failIf(keyOrName -> keyOrName.isKey() && !WorldCreatorCompatibility.canCreateWorldWithKey(),
keyOrName -> worldActionResult(CloneFailureReason.NAMESPACEDKEY_UNSUPPORTED, keyOrName))
.failIf(worldStore::isLoadedWorld,
keyOrName -> worldActionResult(CloneFailureReason.WORLD_EXIST_LOADED, keyOrName))
.failIf(worldStore::isWorld,
keyOrName -> worldActionResult(CloneFailureReason.WORLD_EXIST_UNLOADED, keyOrName))
.failIf(worldNameChecker::hasWorldFolder,
keyOrName -> worldActionResult(CloneFailureReason.WORLD_EXIST_FOLDER, keyOrName))
.failIf(keyOrName -> !worldNameChecker.isValidWorldFolder(options.fromWorld().getOfflineWorldFolder()),
keyOrName -> worldActionResult(CloneFailureReason.FROM_WORLD_FOLDER_INVALID,
options.fromWorld().getName()))
.map(keyOrName -> new KeyOrNameWithOptions<>(keyOrName, options));
}
private Attempt<CloneWorldOptions, CloneFailureReason> cloneWorldCopyFolder(@NotNull CloneWorldOptions options) {
private Attempt<KeyOrNameWithOptions<CloneWorldOptions>, CloneFailureReason> cloneWorldCopyFolder(
@NotNull KeyOrNameWithOptions<CloneWorldOptions> keyOrNameWithOptions) {
WorldKeyOrName newWorldkeyOrName = keyOrNameWithOptions.keyOrName();
CloneWorldOptions options = keyOrNameWithOptions.options();
if (options.saveBukkitWorld()) {
Logging.finer("Saving bukkit world before cloning: " + options.world().getName());
options.world().getBukkitWorld().peek(this::saveWorldWithFlush);
options.fromWorld().asLoadedWorld().peek(loadedWorld -> {
Logging.finer("Saving world before cloning: " + loadedWorld.getName());
loadedWorld.getBukkitWorld().peek(bukkitWorld -> WorldCompatibility.saveWithFlush(bukkitWorld, true));
});
}
File worldFolder = options.world().getBukkitWorld().map(World::getWorldFolder).get();
File newWorldFolder = new File(Bukkit.getWorldContainer(), options.newWorldName());
File worldFolder = WorldFolderResolver.resolve(options.fromWorld());
File newWorldFolder = WorldFolderResolver.resolve(newWorldkeyOrName);
return fileUtils.copyFolder(worldFolder, newWorldFolder, CLONE_IGNORE_FILES).fold(
exception -> worldActionResult(CloneFailureReason.COPY_FAILED,
options.world().getName(), exception),
success -> worldActionResult(options));
}
// This method is only available since 1.21
private final Method saveWithFlush = ReflectHelper.getMethod(World.class, "save", boolean.class);
private void saveWorldWithFlush(World world) {
if (saveWithFlush != null) {
Logging.fine("Using world save method with flush...");
ReflectHelper.invokeMethod(world, saveWithFlush, true);
} else {
world.save();
}
options.fromWorld().getName(), exception),
success -> worldActionResult(keyOrNameWithOptions));
}
private void cloneWorldTransferData(@NotNull CloneWorldOptions options, @NotNull LoadedMultiverseWorld newWorld) {
DataTransfer<LoadedMultiverseWorld> dataTransfer = transferData(options, options.world());
dataTransfer.pasteAllTo(newWorld);
}
private DataTransfer<LoadedMultiverseWorld> transferData(
@NotNull KeepWorldSettingsOptions options, @NotNull LoadedMultiverseWorld world) {
DataTransfer<LoadedMultiverseWorld> dataTransfer = new DataTransfer<>();
if (options.keepWorldConfig()) {
dataTransfer.addDataStore(new WorldConfigStore(), world);
}
if (options.keepGameRule()) {
dataTransfer.addDataStore(new GameRulesStore(), world);
}
if (options.keepWorldBorder()) {
dataTransfer.addDataStore(new WorldBorderStore(), world);
new DataTransfer<MultiverseWorld>()
.addDataStore(new WorldConfigStore<>(), options.fromWorld())
.pasteAllTo(newWorld);
}
return dataTransfer;
newWorld.getBukkitWorld().peek(bukkitWorld -> {
if (!options.keepWorldBorder()) {
WorldBorder worldBorder = bukkitWorld.getWorldBorder();
worldBorder.reset();
}
if (!options.keepGameRule()) {
Arrays.stream(bukkitWorld.getGameRules())
.map(gameRuleName -> GameRule.getByName(gameRuleName))
.filter(Objects::nonNull)
.forEach(gameRule -> {
Object gameRuleDefault = bukkitWorld.getGameRuleDefault(gameRule);
if (gameRuleDefault != null) {
bukkitWorld.setGameRule(gameRule, gameRuleDefault);
}
});
}
});
}
/**
@@ -801,14 +875,16 @@ public final class WorldManager {
*/
public Attempt<LoadedMultiverseWorld, RegenFailureReason> regenWorld(@NotNull RegenWorldOptions options) {
LoadedMultiverseWorld world = options.world();
DataTransfer<LoadedMultiverseWorld> dataTransfer = transferData(options, world);
DataTransfer<LoadedMultiverseWorld> dataTransfer = regenWorldTransferData(options, world);
boolean shouldKeepSpawnLocation = options.keepWorldConfig() && options.seed() == world.getSeed();
Location spawnLocation = world.getSpawnLocation();
CreateWorldOptions createWorldOptions = CreateWorldOptions.worldName(world.getName())
CreateWorldOptions createWorldOptions = CreateWorldOptions.worldKeyOrName(world.worldConfig.getWorldKeyOrName())
.biome(world.getBiome())
.bonusChest(world.getBukkitWorld().map(WorldCompatibility::hasBonusChest).getOrElse(false))
.environment(world.getEnvironment())
.generateStructures(world.canGenerateStructures().getOrElse(true))
.generator(world.getGenerator())
.generatorSettings(world.getGeneratorSettings())
.seed(options.seed())
.useSpawnAdjust(!shouldKeepSpawnLocation && world.getAdjustSpawn())
.worldType(world.getWorldType().getOrElse(WorldType.NORMAL))
@@ -829,18 +905,35 @@ public final class WorldManager {
});
}
private DataTransfer<LoadedMultiverseWorld> regenWorldTransferData(
@NotNull KeepWorldSettingsOptions options, @NotNull LoadedMultiverseWorld world) {
DataTransfer<LoadedMultiverseWorld> dataTransfer = new DataTransfer<>();
if (options.keepWorldConfig()) {
dataTransfer.addDataStore(new WorldConfigStore<>(), world);
}
if (options.keepGameRule()) {
dataTransfer.addDataStore(new GameRulesStore(), world);
}
if (options.keepWorldBorder()) {
dataTransfer.addDataStore(new WorldBorderStore(), world);
}
return dataTransfer;
}
private <T, F extends FailureReason> Attempt<T, F> worldActionResult(@NotNull T value) {
return Attempt.success(value);
}
private <T, F extends FailureReason> Attempt<T, F> worldActionResult(
@NotNull F failureReason, @NotNull String worldName) {
return Attempt.failure(failureReason, Replace.WORLD.with(worldName));
private <T, F extends FailureReason> Attempt.Failure<T, F> worldActionResult(
@NotNull F failureReason, @NotNull Object worldName) {
return Attempt.failureRef(failureReason, Replace.WORLD.with(worldName));
}
private <T, F extends FailureReason> Attempt<T, F> worldActionResult(
private <T, F extends FailureReason> Attempt.Failure<T, F> worldActionResult(
@NotNull F failureReason, @NotNull String worldName, @NotNull Throwable error) {
return Attempt.failure(failureReason, Replace.WORLD.with(worldName), Replace.ERROR.with(error));
return Attempt.failureRef(failureReason, Replace.WORLD.with(worldName), Replace.ERROR.with(error));
}
private Attempt<WorldCreator, WorldCreatorFailureReason> addBiomeProviderToCreator(
@@ -936,17 +1029,13 @@ public final class WorldManager {
* Checks based on folder contents and name.
*
* @return A list of all potential worlds.
*
* @deprecated Use {@link PotentialWorldFinder#findPotentialWorlds()} instead.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public List<String> getPotentialWorlds() {
File[] files = Bukkit.getWorldContainer().listFiles();
if (files == null) {
return Collections.emptyList();
}
return Arrays.stream(files)
.filter(file -> !isWorld(file.getName()))
.filter(worldNameChecker::isValidWorldFolder)
.map(File::getName)
.toList();
return potentialWorldFinder.get().findPotentialWorlds();
}
/**
@@ -968,9 +1057,7 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getWorld(@Nullable String worldName) {
return getLoadedWorld(worldName)
.map(world -> (MultiverseWorld) world)
.orElse(() -> getUnloadedWorld(worldName));
return worldStore.getWorld(worldName);
}
/**
@@ -981,9 +1068,8 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getWorldByNameOrAlias(@Nullable String worldNameOrAlias) {
return getLoadedWorldByNameOrAlias(worldNameOrAlias)
.map(world -> (MultiverseWorld) world)
.orElse(() -> getUnloadedWorldByNameOrAlias(worldNameOrAlias));
return getWorld(worldNameOrAlias)
.orElse(() -> getWorld(worldStore.translateAlias(worldNameOrAlias)));
}
/**
@@ -993,14 +1079,15 @@ public final class WorldManager {
* <p>If you want only unloaded worlds, use {@link #getUnloadedWorlds()}. If you want only loaded worlds, use
* {@link #getLoadedWorlds()}.</p>
*
* <p>Note that this is an unmodifiable copy of the current worlds. It will not update as worlds are added/removed.
* Call it everytime you need the most updated list of worlds.</p>
*
* @return A list of all worlds that may or may not be loaded.
*/
@Unmodifiable
@NotNull
public Collection<MultiverseWorld> getWorlds() {
return worldsMap.values().stream()
.map(world -> getLoadedWorld(world)
.map(loadedWorld -> (MultiverseWorld) loadedWorld)
.getOrElse(world))
.toList();
return worldStore.getWorlds();
}
/**
@@ -1010,7 +1097,7 @@ public final class WorldManager {
* @return True if the world is a world is known to multiverse, but may or may not be loaded.
*/
public boolean isWorld(@Nullable String worldName) {
return worldName != null && worldsMap.containsKey(worldName.toLowerCase(Locale.ENGLISH));
return worldName != null && worldStore.getWorld(worldName).isDefined();
}
/**
@@ -1020,9 +1107,7 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getUnloadedWorld(@Nullable String worldName) {
return isLoadedWorld(worldName)
? Option.none()
: Option.of(worldName).flatMap(name -> Option.of(worldsMap.get(name.toLowerCase(Locale.ENGLISH))));
return worldStore.getUnloadedWorld(worldName);
}
/**
@@ -1032,18 +1117,8 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getUnloadedWorldByNameOrAlias(@Nullable String worldNameOrAlias) {
return getUnloadedWorld(worldNameOrAlias).orElse(() -> getUnloadedWorldByAlias(worldNameOrAlias));
}
private Option<MultiverseWorld> getUnloadedWorldByAlias(@Nullable String alias) {
if (alias == null || alias.isEmpty()) {
return Option.none();
}
String colourlessAlias = ChatTextFormatter.removeColor(alias);
return Option.ofOptional(worldsMap.values().stream()
.filter(world -> !world.isLoaded())
.filter(world -> world.getColourlessAlias().equalsIgnoreCase(colourlessAlias))
.findFirst());
return getUnloadedWorld(worldNameOrAlias)
.orElse(() -> getUnloadedWorld(worldStore.translateAlias(worldNameOrAlias)));
}
/**
@@ -1051,10 +1126,10 @@ public final class WorldManager {
*
* @return A list of all worlds that are not loaded.
*/
@Unmodifiable
@NotNull
public Collection<MultiverseWorld> getUnloadedWorlds() {
return worldsMap.values().stream()
.filter(world -> !world.isLoaded())
.toList();
return worldStore.getUnloadedWorlds();
}
/**
@@ -1094,8 +1169,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable String worldName) {
return Option.of(worldName)
.flatMap(name -> Option.of(loadedWorldsMap.get(name.toLowerCase(Locale.ENGLISH))));
return Option.of(worldName).flatMap(worldStore::getLoadedWorld);
}
/**
@@ -1106,27 +1180,21 @@ public final class WorldManager {
*/
public Option<LoadedMultiverseWorld> getLoadedWorldByNameOrAlias(@Nullable String worldNameOrAlias) {
return getLoadedWorld(worldNameOrAlias)
.orElse(() -> getLoadedWorldByAlias(worldNameOrAlias));
}
private Option<LoadedMultiverseWorld> getLoadedWorldByAlias(@Nullable String alias) {
if (alias == null || alias.isEmpty()) {
return Option.none();
}
return Option.ofOptional(loadedWorldsMap.values().stream()
.filter(world -> world.getColourlessAlias()
.equalsIgnoreCase(ChatTextFormatter.removeColor(alias)))
.findFirst());
.orElse(() -> getLoadedWorld(worldStore.translateAlias(worldNameOrAlias)));
}
/**
* Get a read-only list of all multiverse worlds that are loaded.
*
* <p>Note that this is an unmodifiable copy of the current worlds. It will not update as worlds are added/removed.
* Call it everytime you need the most updated list of worlds.</p>
*
* @return A list of all multiverse worlds that are loaded.
*/
@Unmodifiable
@NotNull
public Collection<LoadedMultiverseWorld> getLoadedWorlds() {
return loadedWorldsMap.values().stream()
.toList();
return worldStore.getLoadedWorlds();
}
/**
@@ -1156,7 +1224,7 @@ public final class WorldManager {
* @return True if the world is a multiverse world that is loaded.
*/
public boolean isLoadedWorld(@Nullable String worldName) {
return worldName != null && loadedWorldsMap.containsKey(worldName.toLowerCase(Locale.ENGLISH));
return worldName != null && worldStore.getLoadedWorld(worldName).isDefined();
}
/**
@@ -1184,4 +1252,18 @@ public final class WorldManager {
failure.printStackTrace();
});
}
WorldStore getWorldStore() {
return worldStore;
}
/**
* A simple pair wrapper for convenience to pass both the world key or name and the options together between methods
* when parsing world options.
*
* @param keyOrName The world key or name.
* @param options The world options.
* @param <T> The world options type.
*/
private record KeyOrNameWithOptions<T>(WorldKeyOrName keyOrName, T options) { }
}
@@ -0,0 +1,271 @@
package org.mvplugins.multiverse.core.world;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.NamespacedKey;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.utils.CaseInsensitiveStringMap;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@Service
final class WorldStore {
/**
* Only loaded worlds, i.e. will be empty if all worlds are unloaded.
*/
private final List<LoadedMultiverseWorld> loadedList;
/**
* Only unloaded worlds, i.e. will be empty if all worlds are loaded.
*/
private final List<MultiverseWorld> unloadedList;
/**
* Contains single reference to all worlds, either loaded or unloaded ref depending on the world's current status.
*/
private final List<MultiverseWorld> worldList;
/**
* Maps all key and key to loaded ref.
*/
private final Map<String, LoadedMultiverseWorld> loadedMap;
/**
* Maps all key and key to unloaded ref.
*/
private final Map<String, MultiverseWorld> unloadedMap;
/**
* Maps colorless alias to world key. As alias may not be unique, this is a multimap.
*/
private final Multimap<String, String> aliasMap;
@Inject
private WorldStore() {
this.loadedList = new ArrayList<>();
this.unloadedList = new ArrayList<>();
this.worldList = new ArrayList<>();
this.loadedMap = new CaseInsensitiveStringMap<>();
this.unloadedMap = new CaseInsensitiveStringMap<>();
this.aliasMap = HashMultimap.create();
}
void putUnloadedWorld(MultiverseWorld world) {
if (world instanceof LoadedMultiverseWorld) {
throw new IllegalArgumentException("Loaded world cannot be put in unloaded map");
}
unloadedMap.put(world.getKey().toString(), world);
unloadedMap.put(world.getName(), world);
unloadedList.add(world);
worldList.add(world);
}
void putLoadedWorld(LoadedMultiverseWorld world) {
loadedMap.put(world.getKey().toString(), world);
loadedMap.put(world.getName(), world);
unloadedList.remove(unloadedMap.get(world.getKey().toString()));
worldList.remove(unloadedMap.get(world.getKey().toString()));
loadedList.add(world);
worldList.add(world);
}
void removeWorld(MultiverseWorld world) {
LoadedMultiverseWorld loadedRef = loadedMap.get(world.getKey().toString());
MultiverseWorld unloadedRef = unloadedMap.get(world.getKey().toString());
// remove from list
unloadedList.remove(unloadedRef);
loadedList.remove(loadedRef);
worldList.remove(unloadedRef);
worldList.remove(loadedRef);
// remove from maps
loadedMap.remove(world.getKey().toString());
unloadedMap.remove(world.getName());
unloadedMap.remove(world.getKey().toString());
unloadedMap.remove(world.getName());
// remove alias
aliasMap.remove(world.getColourlessAlias(), world.getKey().toString());
}
void removeLoadedWorld(LoadedMultiverseWorld world) {
// remove from list
loadedList.remove(world);
worldList.remove(world);
// remove from maps
loadedMap.remove(world.getKey().toString());
loadedMap.remove(world.getName());
// Add back unloaded
unloadedList.add(unloadedMap.get(world.getKey().toString()));
}
void changeAlias(@Nullable String oldAlias, @Nullable String newAlias, @NotNull NamespacedKey worldKey) {
if (Objects.equals(oldAlias, newAlias)) {
// nothing changed, ignore
return;
}
if (!Strings.isNullOrEmpty(oldAlias)) {
aliasMap.remove(oldAlias.toLowerCase(Locale.ROOT), worldKey.toString());
}
if (!Strings.isNullOrEmpty(newAlias)) {
aliasMap.put(newAlias.toLowerCase(Locale.ROOT), worldKey.toString());
}
}
@Unmodifiable
@NotNull
List<MultiverseWorld> getWorlds() {
return List.copyOf(worldList);
}
@Unmodifiable
@NotNull
List<LoadedMultiverseWorld> getLoadedWorlds() {
return List.copyOf(loadedList);
}
@Unmodifiable
@NotNull
List<MultiverseWorld> getUnloadedWorlds() {
return List.copyOf(unloadedList);
}
@NotNull
Option<MultiverseWorld> getWorld(@Nullable WorldKeyOrName keyOrName) {
return keyOrName == null
? Option.none()
: getWorld(keyOrName.usableKey()).orElse(() -> getWorld(keyOrName.usableName()));
}
@NotNull
Option<MultiverseWorld> getWorld(@Nullable NamespacedKey worldKey) {
return Option.of(worldKey).map(NamespacedKey::toString).flatMap(this::getWorld);
}
@NotNull
Option<MultiverseWorld> getWorld(@Nullable String worldKeyString) {
return Option.of((MultiverseWorld) loadedMap.get(worldKeyString))
.orElse(() -> Option.of(unloadedMap.get(worldKeyString)));
}
boolean isWorld(@Nullable WorldKeyOrName keyOrName) {
return keyOrName != null && (isWorld(keyOrName.usableKey()) || isWorld(keyOrName.usableName()));
}
boolean isWorld(@Nullable NamespacedKey worldKey) {
return getWorld(worldKey).isDefined();
}
boolean isWorld(@Nullable String worldKeyString) {
return getWorld(worldKeyString).isDefined();
}
@NotNull
Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable WorldKeyOrName keyOrName) {
return keyOrName == null
? Option.none()
: getLoadedWorld(keyOrName.usableKey()).orElse(() -> getLoadedWorld(keyOrName.usableName()));
}
@NotNull
Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable NamespacedKey worldKey) {
return Option.of(worldKey).map(NamespacedKey::toString).flatMap(this::getLoadedWorld);
}
@NotNull
Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable String worldKeyString) {
return Option.of(loadedMap.get(worldKeyString))
.filter(MultiverseWorld::isLoaded);
}
boolean isLoadedWorld(@Nullable WorldKeyOrName keyOrName) {
return keyOrName != null && (isLoadedWorld(keyOrName.usableKey()) || isLoadedWorld(keyOrName.usableName()));
}
boolean isLoadedWorld(@Nullable NamespacedKey worldKey) {
return getLoadedWorld(worldKey).isDefined();
}
boolean isLoadedWorld(@Nullable String worldKeyString) {
return getLoadedWorld(worldKeyString).isDefined();
}
@NotNull
Option<MultiverseWorld> getUnloadedWorld(@Nullable WorldKeyOrName keyOrName) {
return keyOrName == null
? Option.none()
: getUnloadedWorld(keyOrName.usableKey()).orElse(() -> getUnloadedWorld(keyOrName.usableName()));
}
@NotNull
Option<MultiverseWorld> getUnloadedWorld(@Nullable NamespacedKey worldKey) {
return Option.of(worldKey).map(NamespacedKey::toString).flatMap(this::getUnloadedWorld);
}
@NotNull
Option<MultiverseWorld> getUnloadedWorld(@Nullable String worldKeyString) {
return Option.of(unloadedMap.get(worldKeyString))
.filter(world -> !world.isLoaded());
}
boolean isUnloadedWorld(@Nullable WorldKeyOrName keyOrName) {
return keyOrName != null && (isUnloadedWorld(keyOrName.usableKey()) || isUnloadedWorld(keyOrName.usableName()));
}
boolean isUnloadedWorld(@Nullable NamespacedKey worldKey) {
return getUnloadedWorld(worldKey).isDefined();
}
boolean isUnloadedWorld(@Nullable String worldKeyString) {
return getUnloadedWorld(worldKeyString).isDefined();
}
@NotNull
Option<MultiverseWorld> getUnloadedWorldRef(@Nullable WorldKeyOrName keyOrName) {
return keyOrName == null
? Option.none()
: getUnloadedWorldRef(keyOrName.usableKey()).orElse(() -> getUnloadedWorldRef(keyOrName.usableName()));
}
@NotNull
Option<MultiverseWorld> getUnloadedWorldRef(@Nullable NamespacedKey worldKey) {
return Option.of(worldKey).map(NamespacedKey::toString).flatMap(this::getUnloadedWorldRef);
}
@NotNull
Option<MultiverseWorld> getUnloadedWorldRef(@Nullable String worldKeyString) {
return Option.of(unloadedMap.get(worldKeyString));
}
@Nullable
String translateAlias(@Nullable String worldNameOrAlias) {
if (Strings.isNullOrEmpty(worldNameOrAlias)) {
return null;
}
//TODO: Not sure if we should fail if there is multiple alias of the same name, but for now just return the first one
return aliasMap.get(worldNameOrAlias.toLowerCase(Locale.ROOT)).stream()
.findFirst()
.orElse(worldNameOrAlias);
}
}
@@ -10,6 +10,7 @@ import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
@@ -17,6 +18,9 @@ import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import org.mvplugins.multiverse.core.world.key.WorldKeyParseFailReason;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
@@ -28,7 +32,7 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
final class WorldsConfigManager {
private static final String CONFIG_FILENAME = "worlds.yml";
private final SortedMap<String, WorldConfig> worldConfigMap;
private final SortedMap<WorldKeyOrName, WorldConfig> worldConfigMap;
private final File worldConfigFile;
private YamlConfiguration worldsConfig;
@@ -47,7 +51,7 @@ final class WorldsConfigManager {
*
* @return A {@link NewAndRemovedWorlds} record.
*/
public Try<NewAndRemovedWorlds> load() {
Try<NewAndRemovedWorlds> load() {
return Try.of(() -> {
loadWorldYmlFile();
return parseNewAndRemovedWorlds();
@@ -113,7 +117,7 @@ final class WorldsConfigManager {
config.set("worlds", null);
for (Map.Entry<String, ConfigurationSection> entry : worldConfigMap.entrySet()) {
config.set(encodeWorldName(entry.getKey()), entry.getValue());
config.set(encodeConfigKey(entry.getKey()), entry.getValue());
}
config.save(worldConfigFile);
})
@@ -162,33 +166,39 @@ final class WorldsConfigManager {
* @return A tuple containing a list of the new WorldConfigs added and a list of the worlds removed from the config.
*/
private NewAndRemovedWorlds parseNewAndRemovedWorlds() {
List<String> allWorldsInConfig = worldsConfig.getKeys(false)
List<WorldKeyOrName> allWorldsInConfig = worldsConfig.getKeys(false)
.stream()
.map(this::decodeWorldName)
.map(keyStr -> decodeConfigKey(keyStr)
.onFailure(reason ->
Logging.warning("Failed to parse world key '%s' in config: %s", keyStr, reason))
.getOrNull())
.filter(Objects::nonNull)
.toList();
List<WorldConfig> newWorldsAdded = new ArrayList<>();
for (String worldName : allWorldsInConfig) {
getWorldConfig(worldName)
.peek(config -> config.load(getWorldConfigSection(worldName)))
for (WorldKeyOrName worldKeyStr : allWorldsInConfig) {
getWorldConfig(worldKeyStr)
.peek(config -> config.load(getWorldConfigSection(worldKeyStr))
.onFailure(e -> Logging.warning("Failed to load world config for world '%s': %s",
worldKeyStr, e.getMessage())))
.orElse(() -> {
WorldConfig newWorldConfig = new WorldConfig(
worldName,
getWorldConfigSection(worldName),
worldKeyStr,
getWorldConfigSection(worldKeyStr),
multiverseCore);
worldConfigMap.put(worldName, newWorldConfig);
worldConfigMap.put(worldKeyStr, newWorldConfig);
newWorldsAdded.add(newWorldConfig);
return Option.of(newWorldConfig);
})
.peek(WorldConfig::save);
}
List<String> worldsRemoved = worldConfigMap.keySet().stream()
List<WorldKeyOrName> worldsRemoved = worldConfigMap.keySet().stream()
.filter(worldName -> !allWorldsInConfig.contains(worldName))
.toList();
for (String s : worldsRemoved) {
for (WorldKeyOrName s : worldsRemoved) {
worldConfigMap.remove(s);
}
@@ -200,7 +210,7 @@ final class WorldsConfigManager {
*
* @return Whether the worlds.yml file has been loaded.
*/
public boolean isLoaded() {
boolean isLoaded() {
return worldsConfig != null;
}
@@ -209,17 +219,17 @@ final class WorldsConfigManager {
*
* @return Whether the save was successful or the error that occurred.
*/
public Try<Void> save() {
Try<Void> save() {
return Try.run(() -> {
if (!isLoaded()) {
throw new IllegalStateException("WorldsConfigManager is not loaded!");
}
worldsConfig = new YamlConfiguration();
worldConfigMap.forEach((worldName, worldConfig) -> {
worldConfigMap.forEach((keyOrName, worldConfig) -> {
worldConfig.save().onFailure(e -> {
throw new RuntimeException("Failed to save world config: " + worldName, e);
throw new RuntimeException("Failed to save world " + keyOrName + " in config: " + e.getMessage(), e);
});
worldsConfig.set(encodeWorldName(worldName), worldConfig.getConfigurationSection());
worldsConfig.set(encodeConfigKey(keyOrName), worldConfig.getConfigurationSection());
});
worldsConfig.save(worldConfigFile);
}).onFailure(e -> {
@@ -228,63 +238,81 @@ final class WorldsConfigManager {
}
/**
* Gets the {@link WorldConfig} instance of all worlds in the worlds.yml file.
* Gets the {@link WorldConfig} instance of a world in the worlds.yml file.
*
* @param worldName The name of the world to check.
* @param keyOrName The target key to get
* @return The {@link WorldConfig} instance of the world, or empty option if it doesn't exist.
*/
public @NotNull Option<WorldConfig> getWorldConfig(@NotNull String worldName) {
return Option.of(worldConfigMap.get(worldName));
@NotNull Option<WorldConfig> getWorldConfig(@NotNull WorldKeyOrName keyOrName) {
return Option.of(worldConfigMap.get(keyOrName));
}
@NotNull WorldConfig migrateWorldConfigKey(@NotNull WorldConfig worldConfig, @NotNull NamespacedKey toKey) {
worldConfig.save();
WorldKeyOrName newKeyOrName = WorldKeyOrName.parseKey(toKey);
WorldConfig migratedWorldConfig = new WorldConfig(
newKeyOrName,
worldConfig.getConfigurationSection(),
multiverseCore
);
deleteWorldConfig(worldConfig);
worldConfigMap.put(newKeyOrName, migratedWorldConfig);
return migratedWorldConfig;
}
/**
* Add a new world to the worlds.yml file. If a world with the given name already exists, an exception is thrown.
*
* @param worldName The name of the world to add.
* @param namespacedKey The target key to add
* @return The newly created {@link WorldConfig} instance.
*/
public @NotNull WorldConfig addWorldConfig(@NotNull String worldName) {
if (worldConfigMap.containsKey(worldName)) {
throw new IllegalArgumentException("WorldConfig for world " + worldName + " already exists.");
@NotNull WorldConfig addWorldConfig(@NotNull NamespacedKey namespacedKey) {
WorldKeyOrName keyOrName = WorldKeyOrName.parseKey(namespacedKey);
if (worldConfigMap.containsKey(keyOrName)) {
throw new IllegalStateException("WorldConfig for world " + namespacedKey + " already exists.");
}
WorldConfig worldConfig = new WorldConfig(worldName, getWorldConfigSection(worldName), multiverseCore);
worldConfigMap.put(worldName, worldConfig);
WorldConfig worldConfig = new WorldConfig(keyOrName, getWorldConfigSection(keyOrName), multiverseCore);
worldConfigMap.put(keyOrName, worldConfig);
return worldConfig;
}
/**
* Deletes the world config for the given world.
*
* @param worldName The name of the world to delete.
* @param worldConfig The target world config to delete
*/
public void deleteWorldConfig(@NotNull String worldName) {
worldConfigMap.remove(worldName);
worldsConfig.set(encodeWorldName(worldName), null);
void deleteWorldConfig(@NotNull WorldConfig worldConfig) {
worldConfigMap.remove(worldConfig.getWorldKeyOrName());
worldsConfig.set(encodeConfigKey(worldConfig.getWorldKeyOrName()), null);
}
/**
* Gets the {@link ConfigurationSection} for the given world in the worlds.yml file. If the section doesn't exist,
* it is created.
*
* @param worldName The name of the world.
* @param keyOrName the config key of the world to get the configuration section for.
* @return The {@link ConfigurationSection} for the given world.
*/
private ConfigurationSection getWorldConfigSection(String worldName) {
worldName = encodeWorldName(worldName);
return worldsConfig.isConfigurationSection(worldName)
? worldsConfig.getConfigurationSection(worldName)
: worldsConfig.createSection(worldName);
@NotNull
private ConfigurationSection getWorldConfigSection(@NotNull WorldKeyOrName keyOrName) {
String encodeWorldKey = encodeConfigKey(keyOrName);
ConfigurationSection section = worldsConfig.getConfigurationSection(encodeWorldKey);
return section == null ? worldsConfig.createSection(encodeWorldKey) : section;
}
private String encodeConfigKey(@NotNull WorldKeyOrName worldKeyOrName) {
return encodeConfigKey(worldKeyOrName.serialise());
}
/**
* Dot is a special character in YAML that causes sub-path issues.
* Remove dot . with [dot] as it is a special character in YAML that causes sub-path issues.
*/
private String encodeWorldName(String worldName) {
private String encodeConfigKey(@NotNull String worldName) {
return worldName.replace(".", "[dot]");
}
private String decodeWorldName(String worldName) {
return worldName.replace("[dot]", ".");
private Attempt<WorldKeyOrName, WorldKeyParseFailReason> decodeConfigKey(@NotNull String worldKeyStr) {
return WorldKeyOrName.parse(worldKeyStr.replace("[dot]", "."));
}
private static final class ConfigMigratedException extends RuntimeException {
@@ -1,64 +1,41 @@
package org.mvplugins.multiverse.core.world.entity;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.SpawnCategory;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
@Service
public final class EntityPurger {
public int purgeEntities(LoadedMultiverseWorld world) {
AtomicInteger purgeCount = new AtomicInteger(0);
world.getBukkitWorld().peek(bukkitWorld -> {
for (Entity entity : bukkitWorld.getEntities()) {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(entity)) {
entity.remove();
purgeCount.incrementAndGet();
}
}
});
return purgeCount.get();
return purgeEntitiesWithCondition(world, entity -> !world.getEntitySpawnConfig().shouldAllowSpawn(entity));
}
public int purgeEntities(LoadedMultiverseWorld world, SpawnCategory spawnCategory) {
AtomicInteger purgeCount = new AtomicInteger(0);
world.getBukkitWorld().peek(bukkitWorld -> {
for (Entity entity : bukkitWorld.getEntities()) {
if (entity.getSpawnCategory() == spawnCategory) {
entity.remove();
purgeCount.incrementAndGet();
}
}
});
return purgeCount.get();
return purgeEntitiesWithCondition(world, entity -> entity.getSpawnCategory().equals(spawnCategory));
}
public int purgeEntities(LoadedMultiverseWorld world, SpawnCategory... spawnCategories) {
Set<SpawnCategory> spawnCategoriesSet = Set.of(spawnCategories);
AtomicInteger purgeCount = new AtomicInteger(0);
world.getBukkitWorld().peek(bukkitWorld -> {
for (Entity entity : bukkitWorld.getEntities()) {
if (spawnCategoriesSet.contains(entity.getSpawnCategory())) {
entity.remove();
purgeCount.incrementAndGet();
}
}
});
return purgeCount.get();
return purgeEntitiesWithCondition(world, entity -> spawnCategoriesSet.contains(entity.getSpawnCategory()));
}
public int purgeAllEntities(LoadedMultiverseWorld world) {
AtomicInteger purgeCount = new AtomicInteger(0);
world.getBukkitWorld().peek(bukkitWorld -> {
for (Entity entity : bukkitWorld.getEntities()) {
entity.remove();
purgeCount.incrementAndGet();
}
});
return purgeCount.get();
return purgeEntitiesWithCondition(world, entity -> true);
}
private int purgeEntitiesWithCondition(LoadedMultiverseWorld world, Predicate<Entity> condition) {
return Math.toIntExact(world.getBukkitWorld()
.map(bukkitWorld -> bukkitWorld.getEntities().stream()
.filter(entity -> !(entity instanceof Player))
.filter(condition)
.peek(Entity::remove)
.count())
.getOrElse(0L));
}
}
@@ -4,8 +4,10 @@ import com.dumptruckman.minecraft.util.Logging;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.MemoryConfiguration;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.SpawnCategory;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
@@ -19,12 +21,13 @@ public final class EntitySpawnConfig {
private final CoreConfig config;
private final Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig;
EntitySpawnConfig(CoreConfig config, Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig) {
EntitySpawnConfig(@NotNull CoreConfig config,
@NotNull Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig) {
this.config = config;
this.spawnCategoriesConfig = spawnCategoriesConfig;
}
public SpawnCategoryConfig getSpawnCategoryConfig(SpawnCategory spawnCategory) {
public @NotNull SpawnCategoryConfig getSpawnCategoryConfig(@NotNull SpawnCategory spawnCategory) {
return spawnCategoriesConfig.computeIfAbsent(spawnCategory,
computeSpawnCategory -> new SpawnCategoryConfig(
config,
@@ -33,8 +36,17 @@ public final class EntitySpawnConfig {
));
}
public boolean shouldAllowSpawn(Entity entity) {
return getSpawnCategoryConfig(entity.getSpawnCategory()).shouldAllowSpawn(entity);
@ApiStatus.AvailableSince("5.7")
public boolean shouldAllowSpawn(@NotNull EntityType entityType) {
// Ensure it defaults to true if SpawnCategoryMapper fails to find a spawn category for the entity type,
// to avoid accidentally breaking mob spawns.
return SpawnCategoryMapper.getSpawnCategory(entityType)
.map(spawnCategory -> getSpawnCategoryConfig(spawnCategory).shouldAllowSpawn(entityType))
.getOrElse(true);
}
public boolean shouldAllowSpawn(@NotNull Entity entity) {
return shouldAllowSpawn(entity.getType());
}
public void applyConfigToWorld() {
@@ -51,16 +63,19 @@ public final class EntitySpawnConfig {
}
@ApiStatus.Internal
public ConfigurationSection toSection() {
public @NotNull ConfigurationSection toSection() {
MemoryConfiguration section = new MemoryConfiguration();
spawnCategoriesConfig.forEach((spawnCategory, spawnCategoryConfig) -> {
section.set(spawnCategory.toString().toLowerCase(Locale.ENGLISH), spawnCategoryConfig.saveSection());
section.set(String.valueOf(spawnCategory).toLowerCase(Locale.ENGLISH), spawnCategoryConfig.saveSection());
});
return section;
}
@ApiStatus.Internal
public static EntitySpawnConfig fromSection(CoreConfig config, ConfigurationSection section) {
public static @NotNull EntitySpawnConfig fromSection(
@NotNull CoreConfig config,
@NotNull ConfigurationSection section
) {
Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig = new LinkedHashMap<>();
Map<String, Object> existingCategories = section.getValues(false);
for (SpawnCategory category : SpawnCategory.values()) {
@@ -8,6 +8,7 @@ import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.SpawnCategory;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.config.handle.MemoryConfigurationHandle;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
@@ -32,7 +33,11 @@ public final class SpawnCategoryConfig {
private MultiverseWorld world;
SpawnCategoryConfig(CoreConfig config, SpawnCategory spawnCategory, ConfigurationSection section) {
SpawnCategoryConfig(
@NotNull CoreConfig config,
@NotNull SpawnCategory spawnCategory,
@NotNull ConfigurationSection section
) {
this.config = config;
this.spawnCategory = spawnCategory;
this.nodes = new Nodes();
@@ -67,7 +72,7 @@ public final class SpawnCategoryConfig {
});
}
private void applyTickPerSpawns(World bukkitWorld) {
private void applyTickPerSpawns(@NotNull World bukkitWorld) {
if (!config.getApplyEntitySpawnLimit()) {
Logging.finer("World %s %s skipping setTicksPerSpawns due to core config", world.getName(), spawnCategory);
return;
@@ -85,7 +90,7 @@ public final class SpawnCategoryConfig {
bukkitWorld.setTicksPerSpawns(spawnCategory, getTickRate());
}
private void applySpawnLimit(World bukkitWorld) {
private void applySpawnLimit(@NotNull World bukkitWorld) {
if (!config.getApplyEntitySpawnLimit()) {
Logging.finer("Skipping World %s %s setSpawnLimit due to core config", world.getName(), spawnCategory);
return;
@@ -98,11 +103,11 @@ public final class SpawnCategoryConfig {
bukkitWorld.setSpawnLimit(spawnCategory, getSpawnLimit());
}
public StringPropertyHandle getStringPropertyHandle() {
public @NotNull StringPropertyHandle getStringPropertyHandle() {
return stringPropertyHandle;
}
public SpawnCategory getSpawnCategory() {
public @NotNull SpawnCategory getSpawnCategory() {
return spawnCategory;
}
@@ -110,7 +115,7 @@ public final class SpawnCategoryConfig {
return handle.get(nodes.spawn);
}
public Try<Void> setSpawn(boolean spawn) {
public @NotNull Try<Void> setSpawn(boolean spawn) {
return handle.set(nodes.spawn, spawn);
}
@@ -118,7 +123,7 @@ public final class SpawnCategoryConfig {
return handle.get(nodes.tickRate);
}
public Try<Void> setTickRate(int tickRate) {
public @NotNull Try<Void> setTickRate(int tickRate) {
return handle.set(nodes.tickRate, tickRate);
}
@@ -126,23 +131,23 @@ public final class SpawnCategoryConfig {
return handle.get(nodes.spawnLimit);
}
public Try<Void> setSpawnLimit(int spawnLimit) {
public @NotNull Try<Void> setSpawnLimit(int spawnLimit) {
return handle.set(nodes.spawnLimit, spawnLimit);
}
public List<EntityType> getExceptions() {
public @NotNull List<EntityType> getExceptions() {
return handle.get(nodes.exceptions);
}
public Try<Void> setExceptions(List<EntityType> exceptions) {
public @NotNull Try<Void> setExceptions(@NotNull List<EntityType> exceptions) {
return handle.set(nodes.exceptions, exceptions);
}
public boolean shouldAllowSpawn(Entity entity) {
public boolean shouldAllowSpawn(@NotNull Entity entity) {
return shouldAllowSpawn(entity.getType());
}
public boolean shouldAllowSpawn(EntityType entityType) {
public boolean shouldAllowSpawn(@NotNull EntityType entityType) {
return isSpawn() != getExceptions().contains(entityType);
}
@@ -1,13 +1,15 @@
package org.mvplugins.multiverse.core.world.entity;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.SpawnCategory;
import org.jvnet.hk2.annotations.Service;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import java.lang.reflect.Field;
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
@@ -19,28 +21,36 @@ import java.util.Map;
/**
* Uses reflection into nms to map entity types to spawn categories.
*/
final class SpawnCategoryMapper {
@ApiStatus.Internal
public final class SpawnCategoryMapper {
private static final @NotNull Map<EntityType, SpawnCategory> entityTypeToSpawnCategoryMap;
private static final @NotNull Map<SpawnCategory, List<EntityType>> spawnCategoryMap;
static {
buildSpawnCategoryMap();
entityTypeToSpawnCategoryMap = new HashMap<>();
spawnCategoryMap = new HashMap<>();
}
private static Map<SpawnCategory, List<EntityType>> spawnCategoryMap;
private static void buildSpawnCategoryMap() {
spawnCategoryMap = new HashMap<>();
Class<?> entityTypeClass = ReflectHelper.getClass("net.minecraft.world.entity.EntityType");
public static void buildSpawnCategoryMap() {
Class<?> entityTypeClass = ReflectHelper.tryGetClass("net.minecraft.world.entity.EntityType").getOrNull();
if (entityTypeClass == null) {
Logging.warning("Failed to find EntityType class. SpawnCategoryMapper will not work.");
return;
}
Method getCategoryMethod = ReflectHelper.getMethod(entityTypeClass, "getCategory");
// In 26.2, the list of static fields has been moved from EntityType to EntityTypes class.
Class<?> entityTypesListClass = ReflectHelper.tryGetClass("net.minecraft.world.entity.EntityTypes").getOrNull();
if (entityTypesListClass == null) {
// Assume it's the older mc versions that has list of static fields in EntityType class instead.
Logging.finer("Failed to find EntityTypes class. falling back to EntityType.");
entityTypesListClass = entityTypeClass;
}
Method getCategoryMethod = ReflectHelper.tryGetMethod(entityTypeClass, "getCategory").getOrNull();
if (getCategoryMethod == null) {
Logging.warning("Failed to find getCategory method. SpawnCategoryMapper will not work.");
return;
}
Class<?> craftSpawnCategoryClass = ReflectHelper.getClass("org.bukkit.craftbukkit.util.CraftSpawnCategory");
Class<?> craftSpawnCategoryClass = ReflectHelper.tryGetClass("org.bukkit.craftbukkit.util.CraftSpawnCategory").getOrNull();
if (craftSpawnCategoryClass == null) {
Logging.warning("Failed to find CraftSpawnCategory class. SpawnCategoryMapper will not work.");
return;
@@ -53,25 +63,44 @@ final class SpawnCategoryMapper {
Logging.warning("Failed to find toBukkit method. SpawnCategoryMapper will not work.");
return;
}
Field[] entityTypeFields = entityTypeClass.getFields();
for (Field entityTypeField : entityTypeFields) {
String entityName = entityTypeField.getName();
EntityType entityType = Try.of(() -> EntityType.valueOf(entityName)).getOrNull();
if (entityType == null) {
continue;
}
Object nmsEntityType = ReflectHelper.getFieldValue(null, entityTypeField, entityTypeClass);
Object nsmMobCategory = ReflectHelper.invokeMethod(nmsEntityType, getCategoryMethod);
if (nsmMobCategory == null) {
continue;
}
Object bukkitSpawnCategory = ReflectHelper.invokeMethod(null, toBukkitMethod, nsmMobCategory);
if (!(bukkitSpawnCategory instanceof SpawnCategory spawnCategory)) {
continue;
}
spawnCategoryMap.computeIfAbsent(spawnCategory, ignore -> new ArrayList<>())
.add(entityType);
Logging.finer("Mapping entities to its spawn category...");
Arrays.stream(entityTypesListClass.getFields()).forEach(entityTypeField -> Try.of(() -> EntityType.valueOf(entityTypeField.getName()))
.peek(entityType -> ReflectHelper.tryGetStaticFieldValue(entityTypeField, entityTypeClass)
.flatMap(nmsEntityType -> ReflectHelper.tryInvokeMethod(nmsEntityType, getCategoryMethod))
.flatMap(nsmMobCategory -> ReflectHelper.tryInvokeStaticMethod(toBukkitMethod, nsmMobCategory))
.filter(bukkitSpawnCategory -> bukkitSpawnCategory instanceof SpawnCategory)
.map(bukkitSpawnCategory -> (SpawnCategory) bukkitSpawnCategory)
.peek(bukkitSpawnCategory -> entityTypeToSpawnCategoryMap.put(entityType, bukkitSpawnCategory))
.peek(bukkitSpawnCategory -> spawnCategoryMap
.computeIfAbsent(bukkitSpawnCategory, ignore -> new ArrayList<>())
.add(entityType))
.onFailure(throwable -> Logging.warning("Failed to map entity type "
+ entityTypeField.getName() + " to its spawn category. " + throwable.getLocalizedMessage())))
.onFailure(throwable -> Logging.fine("Failed to map entity type "
+ entityTypeField.getName() + " to its spawn category. " + throwable.getLocalizedMessage())));
if (entityTypeToSpawnCategoryMap.isEmpty()) {
Logging.warning("entityTypeToSpawnCategoryMap map is empty after building... " +
"SpawnCategoryMapper will not work.");
}
if (spawnCategoryMap.isEmpty()) {
Logging.warning("spawnCategoryMap map is empty after building... " +
"SpawnCategoryMapper will not work.");
}
Logging.finer("Mapped " + entityTypeToSpawnCategoryMap.size() + " entities to its spawn category.");
}
/**
* Gets the spawn category that the given entity type is in.
*
* @param entityType The entity type
* @return The spawn category that the given entity type is in, or null if it cannot be determined
*/
static @NotNull Option<SpawnCategory> getSpawnCategory(@Nullable EntityType entityType) {
return Option.of(entityTypeToSpawnCategoryMap.get(entityType));
}
/**
@@ -80,10 +109,11 @@ final class SpawnCategoryMapper {
* @param spawnCategory The spawn category
* @return The entity types associated with the spawn category
*/
static List<EntityType> getEntityTypes(SpawnCategory spawnCategory) {
if (spawnCategoryMap == null) {
return Collections.emptyList();
}
return spawnCategoryMap.get(spawnCategory);
static @NotNull List<EntityType> getEntityTypes(@Nullable SpawnCategory spawnCategory) {
return spawnCategoryMap.getOrDefault(spawnCategory, Collections.emptyList());
}
private SpawnCategoryMapper() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -9,7 +9,9 @@ import org.bukkit.GameRule;
import org.bukkit.World;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.utils.compatibility.WorldBorderCompatibility;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
/**
* A data store for storing and restoring data from an object.
@@ -90,14 +92,14 @@ public interface DataStore<T> {
/**
* A {@link DataStore} for storing and restoring world properties for a multiverse world.
*/
class WorldConfigStore implements DataStore<LoadedMultiverseWorld> {
class WorldConfigStore<T extends MultiverseWorld> implements DataStore<T> {
private Map<String, Object> configMap;
/**
* {@inheritDoc}
*/
@Override
public WorldConfigStore copyFrom(LoadedMultiverseWorld world) {
public WorldConfigStore<T> copyFrom(T world) {
this.configMap = new HashMap<>();
StringPropertyHandle worldPropertyHandler = world.getStringPropertyHandle();
worldPropertyHandler.getAllPropertyNames().forEach(name -> worldPropertyHandler.getProperty(name)
@@ -111,7 +113,7 @@ public interface DataStore<T> {
* {@inheritDoc}
*/
@Override
public WorldConfigStore pasteTo(LoadedMultiverseWorld world) {
public WorldConfigStore<T> pasteTo(T world) {
if (configMap == null) {
return this;
}
@@ -139,14 +141,16 @@ public interface DataStore<T> {
*/
@Override
public WorldBorderStore copyFrom(LoadedMultiverseWorld world) {
world.getBukkitWorld().peek(bukkitWorld -> {
borderCenterX = bukkitWorld.getWorldBorder().getCenter().getX();
borderCenterZ = bukkitWorld.getWorldBorder().getCenter().getZ();
borderDamageAmount = bukkitWorld.getWorldBorder().getDamageAmount();
borderDamageBuffer = bukkitWorld.getWorldBorder().getDamageBuffer();
borderSize = bukkitWorld.getWorldBorder().getSize();
borderTimeRemaining = bukkitWorld.getWorldBorder().getWarningTime();
});
world.getBukkitWorld()
.map(World::getWorldBorder)
.peek(worldBorder -> {
borderCenterX = worldBorder.getCenter().getX();
borderCenterZ = worldBorder.getCenter().getZ();
borderDamageAmount = worldBorder.getDamageAmount();
borderDamageBuffer = worldBorder.getDamageBuffer();
borderSize = worldBorder.getSize();
borderTimeRemaining = WorldBorderCompatibility.getWarningTimeTicks(worldBorder);
});
return this;
}
@@ -155,13 +159,15 @@ public interface DataStore<T> {
*/
@Override
public WorldBorderStore pasteTo(LoadedMultiverseWorld world) {
world.getBukkitWorld().peek(bukkitWorld -> {
bukkitWorld.getWorldBorder().setCenter(borderCenterX, borderCenterZ);
bukkitWorld.getWorldBorder().setDamageAmount(borderDamageAmount);
bukkitWorld.getWorldBorder().setDamageBuffer(borderDamageBuffer);
bukkitWorld.getWorldBorder().setSize(borderSize);
bukkitWorld.getWorldBorder().setWarningTime(borderTimeRemaining);
});
world.getBukkitWorld()
.map(World::getWorldBorder)
.peek(worldBorder -> {
worldBorder.setCenter(borderCenterX, borderCenterZ);
worldBorder.setDamageAmount(borderDamageAmount);
worldBorder.setDamageBuffer(borderDamageBuffer);
worldBorder.setSize(borderSize);
WorldBorderCompatibility.setWarningTimeTicks(worldBorder, borderTimeRemaining);
});
return this;
}
}
@@ -4,6 +4,7 @@ import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.World.Environment;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.CoreConfig;
@@ -19,6 +20,24 @@ import java.util.regex.Pattern;
@Service
public final class DimensionFinder {
/**
* Default world for nether worlds, The default is "%overworld_nether%", which means that the nether world of
* "world" will be named "world_nether".
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static final DimensionFormat DEFAULT_NETHER_FORMAT = new DimensionFormat("%overworld%_nether");
/**
* Default world name format for end worlds. The default is "%overworld%_the_end", which means that the end world
* of "world" will be named "world_the_end".
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static final DimensionFormat DEFAULT_END_FORMAT = new DimensionFormat("%overworld%_the_end");
private final CoreConfig config;
private final WorldManager worldManager;
@@ -7,9 +7,11 @@ import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter;
import org.mvplugins.multiverse.core.teleportation.TeleportFailureReason;
import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate;
@@ -84,6 +86,24 @@ public final class PlayerWorldTeleporter {
.getOrElse(AsyncAttemptsAggregate::emptySuccess);
}
/**
* Transfers all players from the given world to the given destination.
*
* @param world The world to transfer players from.
* @param destinationInstance The destination instance to transfer players to.
* @return A list of async futures that represent the teleportation result of each player.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public AsyncAttemptsAggregate<Void, TeleportFailureReason> transferAllFromWorldToDestination(
@NotNull LoadedMultiverseWorld world,
@NotNull DestinationInstance<?, ?> destinationInstance) {
return world.getPlayers()
.map(players -> safetyTeleporter.to(destinationInstance).teleport(players))
.getOrElse(AsyncAttemptsAggregate::emptySuccess);
}
/**
* Teleports all players to the given world's spawn location.
*
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff. Show more