Compare commits

...
135 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 5dfb098d5c Add support for cloning unloaded worlds 2026-01-26 12:18:47 +08:00
132 changed files with 7243 additions and 1140 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 }}
+6 -6
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 {
@@ -27,7 +27,7 @@ repositories {
configure(apiDependencies) {
serverApiVersion = '1.21.11-R0.1-SNAPSHOT'
mockBukkitServerApiVersion = '1.21'
mockBukkitVersion = '4.100.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
@@ -226,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);
}
}
@@ -150,6 +150,20 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
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 "monstersspawn" -> String.valueOf(world.getEntitySpawnConfig()
.getSpawnCategoryConfig(SpawnCategory.MONSTER)
.isSpawn());
@@ -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,6 +53,7 @@ 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.addOnToCommaSeparated;
@@ -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);
@@ -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();
@@ -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;
@@ -24,9 +25,11 @@ 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> --properties <prop1=value1,prop2=value2,...>]")
@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> --properties <prop1=value1,prop2=value2,...>]")
@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);
@@ -79,14 +84,16 @@ class CreateCommand extends CoreCommand {
worldManager.createWorld(CreateWorldOptions.worldName(worldName)
.biome(parsedFlags.flagValue(flags.biome, ""))
.bonusChest(parsedFlags.hasFlag(flags.bonusChest))
.environment(environment)
.seed(parsedFlags.flagValue(flags.seed))
.worldType(parsedFlags.flagValue(flags.worldType, WorldType.NORMAL))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn))
.forcedSpawnPosition(parsedFlags.flagValue(flags.forceSpawnPosition))
.generator(parsedFlags.flagValue(flags.generator, ""))
.generatorSettings(parsedFlags.flagValue(flags.generatorSettings, ""))
.generateStructures(!parsedFlags.hasFlag(flags.noStructures))
.worldPropertyStrings(StringFormatter.parseCSVMap(parsedFlags.flagValue(flags.properties))))
.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));
}
@@ -185,6 +192,15 @@ class CreateCommand extends CoreCommand {
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)));
}
}
@@ -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;
@@ -220,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.")
@@ -368,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());
@@ -484,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());
@@ -615,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}
*/
@@ -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());
}
});
}
}
@@ -244,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",
@@ -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");
@@ -1,8 +1,8 @@
package org.mvplugins.multiverse.core.utils;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashMap;
@@ -62,7 +62,7 @@ public class CaseInsensitiveStringMap<T> implements Map<String, T> {
}
@Override
public void putAll(@NonNull Map<? extends String, ? extends T> m) {
public void putAll(@NotNull Map<? extends String, ? extends T> m) {
m.forEach((key, value) -> map.put(normalizeKey(key), value));
}
@@ -72,17 +72,17 @@ public class CaseInsensitiveStringMap<T> implements Map<String, T> {
}
@Override
public @NonNull Set<String> keySet() {
public @NotNull Set<String> keySet() {
return map.keySet();
}
@Override
public @NonNull Collection<T> values() {
public @NotNull Collection<T> values() {
return map.values();
}
@Override
public @NonNull Set<Entry<String, T>> entrySet() {
public @NotNull Set<Entry<String, T>> entrySet() {
return map.entrySet();
}
@@ -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() {
@@ -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,6 +1,9 @@
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;
@@ -10,14 +13,17 @@ 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;
@@ -25,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.
*/
@@ -38,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);
@@ -47,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);
}
/**
@@ -144,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();
}
/**
@@ -393,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.
@@ -454,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
@@ -689,7 +786,7 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
*
* @return The world config.
*/
WorldConfig getWorldConfig() {
@NotNull WorldConfig getWorldConfig() {
return worldConfig;
}
@@ -698,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;
}
@@ -708,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,6 +380,38 @@ 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);
}
@@ -396,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,11 +1,9 @@
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.List;
import java.util.Map;
import java.util.Objects;
@@ -15,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;
@@ -46,20 +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.CaseInsensitiveStringMap;
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;
@@ -86,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 CaseInsensitiveStringMap<MultiverseWorld> worldsMap;
private final CaseInsensitiveStringMap<LoadedMultiverseWorld> loadedWorldsMap;
private final WorldStore worldStore;
private final List<String> unloadTracker;
private final List<String> loadTracker;
private final WorldsConfigManager worldsConfigManager;
@@ -106,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,
@@ -120,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;
@@ -133,9 +141,8 @@ public final class WorldManager {
this.serverProperties = serverProperties;
this.config = config;
this.entityPurger = entityPurger;
this.potentialWorldFinder = potentialWorldFinder;
this.worldsMap = new CaseInsensitiveStringMap<>();
this.loadedWorldsMap = new CaseInsensitiveStringMap<>();
this.unloadTracker = new ArrayList<>();
this.loadTracker = new ArrayList<>();
}
@@ -167,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)));
}
/**
@@ -191,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);
@@ -240,42 +252,50 @@ 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()))
bukkitWorld,
generatorString,
options.biome(),
options.generatorSettings(),
options.useSpawnAdjust()))
.peek(loadedWorld -> postCreateWorld(loadedWorld, options));
}
@@ -295,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)
@@ -330,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()),
@@ -342,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(), mvWorld);
private MultiverseWorld newMultiverseWorld(WorldConfig worldConfig) {
MultiverseWorld mvWorld = new MultiverseWorld(worldConfig, config);
worldStore.putUnloadedWorld(mvWorld);
corePermissions.addWorldPermissions(mvWorld);
return mvWorld;
}
@@ -366,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,
@@ -390,23 +440,12 @@ public final class WorldManager {
locationManipulation,
entityPurger
);
loadedWorldsMap.put(loadedWorld.getName(), 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.
*
@@ -473,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())
@@ -500,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,
@@ -509,7 +572,7 @@ public final class WorldManager {
locationManipulation,
entityPurger
);
loadedWorldsMap.put(loadedWorld.getName(), loadedWorld);
worldStore.putLoadedWorld(loadedWorld);
saveWorldsConfig();
pluginManager.callEvent(new MVWorldLoadedEvent(loadedWorld));
return Attempt.success(loadedWorld);
@@ -543,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);
@@ -638,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());
}
@@ -699,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(() -> {
@@ -714,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);
}
});
}
});
}
/**
@@ -809,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))
@@ -837,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(
@@ -944,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();
}
/**
@@ -976,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);
}
/**
@@ -989,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)));
}
/**
@@ -1001,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();
}
/**
@@ -1018,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);
return worldName != null && worldStore.getWorld(worldName).isDefined();
}
/**
@@ -1028,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)));
return worldStore.getUnloadedWorld(worldName);
}
/**
@@ -1040,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)));
}
/**
@@ -1059,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();
}
/**
@@ -1102,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)));
return Option.of(worldName).flatMap(worldStore::getLoadedWorld);
}
/**
@@ -1114,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();
}
/**
@@ -1164,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);
return worldName != null && worldStore.getLoadedWorld(worldName).isDefined();
}
/**
@@ -1192,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.
*
@@ -0,0 +1,99 @@
package org.mvplugins.multiverse.core.world.helpers;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.io.File;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
/**
* Tool that traversals the server folders to find all potential worlds that can be imported, based on folder contents
* and name.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Service
public final class PotentialWorldFinder {
private static final int MAX_RECURSION_DEPTH = 3;
private static final int MAX_FOLDER_TRAVERSAL_LIMIT = 100;
private final WorldManager worldManager;
private final WorldNameChecker worldNameChecker;
@Inject
private PotentialWorldFinder(@NotNull WorldManager worldManager, @NotNull WorldNameChecker worldNameChecker) {
this.worldManager = worldManager;
this.worldNameChecker = worldNameChecker;
}
/**
* Gets a list of all potential worlds that can be loaded from the server folders.
* Checks based on folder contents and name.
*
* @return A list of all potential worlds.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Unmodifiable
@NotNull
public List<String> findPotentialWorlds() {
return BukkitCompatibility.isUsingNewDimensionStorage()
? Stream.concat(findFromDimensionsFolder(), findFromRootFolder()).toList()
: findFromRootFolder().toList();
}
private Stream<String> findFromRootFolder() {
Path worldContainer = Bukkit.getWorldContainer().toPath();
return Arrays.stream(listFolders(worldContainer))
.limit(MAX_FOLDER_TRAVERSAL_LIMIT)
.filter(worldNameChecker::isValidWorldFolder)
.map(File::getName)
.filter(worldNameChecker::isValidWorldName)
.filter(worldName -> !worldManager.isWorld(worldName));
}
private Stream<String> findFromDimensionsFolder() {
Path worldContainer = BukkitCompatibility.getWorldFoldersDirectory();
return Arrays.stream(listFolders(worldContainer))
.limit(MAX_FOLDER_TRAVERSAL_LIMIT)
.flatMap(namespaceFile -> recursiveFindFromNamespaceFolder("", namespaceFile.toPath(), 0)
.map(worldKey -> namespaceFile.getName() + ":" + worldKey))
.filter(namespacedKey -> NamespacedKey.fromString(namespacedKey) != null)
.filter(namespacedKey -> !worldManager.isWorld(namespacedKey));
}
private Stream<String> recursiveFindFromNamespaceFolder(String prefix, Path subWorldContainer, int depth) {
if (depth >= MAX_RECURSION_DEPTH) {
return Stream.empty();
}
return Arrays.stream(listFolders(subWorldContainer))
.limit(MAX_FOLDER_TRAVERSAL_LIMIT)
.flatMap(folder -> worldNameChecker.isValidWorldFolder(folder)
? Stream.of(concatPath(prefix, folder.getName()))
: recursiveFindFromNamespaceFolder(
concatPath(prefix, folder.getName()), folder.toPath(), depth + 1));
}
private String concatPath(String prefix, String folderName) {
return prefix.isEmpty() ? folderName : prefix + "/" + folderName;
}
private @NotNull File[] listFolders(Path folder) {
return Option.of(folder.toFile().listFiles(File::isDirectory))
.getOrElse(() -> new File[0]);
}
}
@@ -0,0 +1,108 @@
package org.mvplugins.multiverse.core.world.helpers;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import java.io.File;
/**
* <p>Utility class for resolving the file system location of a world folder.</p>
*
* <p>This class handles the differences in world folder locations between server versions,
* particularly the new dimension storage system introduced in PaperMC 26.1 which stores worlds
* in a dimensions folder under the world level directory.</p>
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public final class WorldFolderResolver {
/**
* Resolves the world folder file path for the given world key or name.
* <br />
* This method automatically selects the appropriate resolution method based on whether the server
* is using the new dimension storage system introduced in PaperMC 26.1.
*
* @param keyOrName The world key or name to resolve.
* @return The resolved world folder file.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static File resolve(@NotNull WorldKeyOrName keyOrName) {
return BukkitCompatibility.isUsingNewDimensionStorage()
? resolveAsDimensionKey(keyOrName.usableKey())
: resolveAsLegacyWorldName(keyOrName.usableName());
}
/**
* Resolves the world folder file path for the given Multiverse world.
* <br />
* This method automatically selects the appropriate resolution method based on whether the server
* is using the new dimension storage system introduced in PaperMC 26.1.
*
* @param multiverseWorld The Multiverse world to resolve.
* @return The resolved world folder file.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static File resolve(@NotNull MultiverseWorld multiverseWorld) {
File dimensionKey = resolveAsDimensionKey(multiverseWorld.getKey());
File legacy = resolveAsLegacyWorldName(multiverseWorld.getName());
if (BukkitCompatibility.isUsingNewDimensionStorage()) {
return !dimensionKey.isDirectory() && legacy.isDirectory() ? legacy : dimensionKey;
}
return legacy;
}
/**
* Gets the world folder using the new dimension storage path format of {@code [namespace]/[key]}.
* <br />
* This method is only valid for PaperMC 26.1 and above. For earlier server versions,
* use {@link #resolveAsLegacyWorldName(String)} instead.
*
* @param namespacedKey The namespaced key of the world.
* @return The resolved world folder file using the new dimension storage format.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static File resolveAsDimensionKey(@NotNull NamespacedKey namespacedKey) {
return BukkitCompatibility.getWorldFoldersDirectory()
.resolve(namespacedKey.getNamespace())
.resolve(namespacedKey.getKey())
.toFile();
}
/**
* Gets the world folder using the legacy world name format from the server root directory.
* <br />
* This method is the default for server versions before PaperMC 26.1.
*
* @param worldName The name of the world.
* @return The resolved world folder file using the legacy world name format.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static File resolveAsLegacyWorldName(@NotNull String worldName) {
return Bukkit.getWorldContainer()
.toPath()
.resolve(worldName)
.toFile();
}
private WorldFolderResolver() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -1,15 +1,18 @@
package org.mvplugins.multiverse.core.world.helpers;
import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import io.vavr.control.Option;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
/**
* <p>Utility class in helping to check the status of a world name and it's associated world folder.</p>
@@ -29,6 +32,18 @@ public final class WorldNameChecker {
"plugins",
"versions");
private static final List<WorldFolderSchema> WORLD_FOLDER_SCHEMA = List.of(
// OLD
WorldFolderSchema.file("level.dat"),
WorldFolderSchema.folder("DIM1"),
WorldFolderSchema.folder("DIM-1"),
// NEW
WorldFolderSchema.file("paper-world.yml"),
WorldFolderSchema.folder("data"),
WorldFolderSchema.folder("entities"),
WorldFolderSchema.folder("poi"),
WorldFolderSchema.folder("region"));
/**
* Checks if a world name is valid.
*
@@ -53,7 +68,7 @@ public final class WorldNameChecker {
if (name.isEmpty()) {
return NameStatus.EMPTY;
}
if (BLACKLIST_NAMES.contains(name)) {
if (!BukkitCompatibility.isUsingNewDimensionStorage() && BLACKLIST_NAMES.contains(name)) {
return NameStatus.BLACKLISTED;
}
if (!REPatterns.NAMESPACE_KEY.matcher(name).matches()) {
@@ -69,19 +84,54 @@ public final class WorldNameChecker {
*
* @param worldName The world name to check on.
* @return True if the folder exists, else false.
*
* @deprecated Use {@link #hasWorldFolder(WorldKeyOrName)} instead, which is more robust and supports namespaced keys.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public boolean hasWorldFolder(@Nullable String worldName) {
return checkFolder(worldName) != FolderStatus.DOES_NOT_EXIST;
}
/**
* Check if a world name has a world folder directory. It may not contain valid world data.
*
* @param worldKey The world key to check on.
* @return True if the folder exists, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public boolean hasWorldFolder(@Nullable WorldKeyOrName worldKey) {
return checkFolder(worldKey) != FolderStatus.DOES_NOT_EXIST;
}
/**
* Checks if a world name has a valid world folder with basic world data.
*
* @param worldName The world name to check on.
* @return True if check result is valid, else false.
*
* @deprecated Use {@link #isValidWorldFolder(WorldKeyOrName)} instead, which is more robust and supports
* namespaced keys.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public boolean isValidWorldFolder(@Nullable String worldName) {
return checkFolder(worldName) == FolderStatus.VALID;
return checkFolder(worldName).loadable;
}
/**
* Checks if a world name has a valid world folder with basic world data.
*
* @param nameOrKey The world key to check on.
* @return True if check result is valid, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public boolean isValidWorldFolder(@Nullable WorldKeyOrName nameOrKey) {
return checkFolder(nameOrKey).loadable;
}
/**
@@ -91,7 +141,7 @@ public final class WorldNameChecker {
* @return True if check result is valid, else false.
*/
public boolean isValidWorldFolder(@Nullable File worldFolder) {
return checkFolder(worldFolder) == FolderStatus.VALID;
return checkFolder(worldFolder).loadable;
}
/**
@@ -99,14 +149,45 @@ public final class WorldNameChecker {
*
* @param worldName The world name to check on.
* @return The resulting folder status.
*
* @deprecated Use {@link #checkFolder(WorldKeyOrName)} instead, which is more robust and supports namespaced keys.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@NotNull
public FolderStatus checkFolder(@Nullable String worldName) {
if (worldName == null) {
return FolderStatus.DOES_NOT_EXIST;
}
File worldFolder = new File(Bukkit.getWorldContainer(), worldName);
return checkFolder(worldFolder);
return WorldKeyOrName.parse(worldName)
.map(this::checkFolder)
.getOrElse(FolderStatus.NOT_A_WORLD);
}
/**
* Checks the current folder status for a world key or name.
*
* @param keyOrName The world key or name to check on.
* @return The resulting folder status.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public FolderStatus checkFolder(@Nullable WorldKeyOrName keyOrName) {
if (keyOrName == null) {
return FolderStatus.DOES_NOT_EXIST;
}
if (BukkitCompatibility.isUsingNewDimensionStorage()) {
File oldWorldFolder = WorldFolderResolver.resolveAsLegacyWorldName(keyOrName.usableName());
if (checkFolder(oldWorldFolder) == FolderStatus.VALID) {
return FolderStatus.REQUIRES_MIGRATION;
}
}
return checkFolder(WorldFolderResolver.resolve(keyOrName));
}
/**
@@ -120,7 +201,7 @@ public final class WorldNameChecker {
if (worldFolder == null || !worldFolder.exists() || !worldFolder.isDirectory()) {
return FolderStatus.DOES_NOT_EXIST;
}
if (!folderHasDat(worldFolder)) {
if (!folderWorldSchemaCheck(worldFolder)) {
return FolderStatus.NOT_A_WORLD;
}
return FolderStatus.VALID;
@@ -133,9 +214,54 @@ public final class WorldNameChecker {
* @param worldFolder The File that may be a world.
* @return True if it looks like a world, else false.
*/
private boolean folderHasDat(@NotNull File worldFolder) {
File[] files = worldFolder.listFiles((file, name) -> name.toLowerCase(Locale.ENGLISH).endsWith(".dat"));
return files != null && files.length > 0;
private boolean folderWorldSchemaCheck(@NotNull File worldFolder) {
return WORLD_FOLDER_SCHEMA.stream()
.filter(schema -> schema.check(worldFolder))
.count() >= 2;
}
/**
* Helper class to check if a file or folder exist.
*/
private interface WorldFolderSchema {
static WorldFolderSchema file(String path) {
return new WorldFile(path);
}
static WorldFolderSchema folder(String path) {
return new WorldFolder(path);
}
boolean check(File worldFolder);
final class WorldFile implements WorldFolderSchema {
private final String path;
private WorldFile(String path) {
this.path = path;
}
@Override
public boolean check(File worldFolder) {
File thisFolder = worldFolder.toPath().resolve(path).toFile();
return thisFolder.exists() && thisFolder.isFile();
}
}
final class WorldFolder implements WorldFolderSchema {
private final String path;
private WorldFolder(String path) {
this.path = path;
}
@Override
public boolean check(File worldFolder) {
File thisFolder = worldFolder.toPath().resolve(path).toFile();
return thisFolder.exists() && thisFolder.isDirectory();
}
}
}
/**
@@ -170,16 +296,39 @@ public final class WorldNameChecker {
/**
* Folder is valid.
*/
VALID,
VALID(true),
/**
* This folder will cause PaperMC to migrate to new dimension world folder in 26.1+
*/
REQUIRES_MIGRATION(true),
/**
* Folder exist, but contents in it doesnt look like a world.
*/
NOT_A_WORLD,
NOT_A_WORLD(false),
/**
* Folder does not exist.
*/
DOES_NOT_EXIST
DOES_NOT_EXIST(false),
;
private final boolean loadable;
FolderStatus(boolean loadable) {
this.loadable = loadable;
}
/**
* Whether this folder status is loadable, meaning it has the basic world data and can be loaded as a world.
* Note that this does not guarantee the server will definitely load the world with no errors.
*
* @return True if folder probably is loadable by the server, else false.
*/
@ApiStatus.AvailableSince("5.6")
public boolean isLoadable() {
return loadable;
}
}
}
Loaded 100 of 132 files, more files were not shown because too many files have changed in this diff. Show more