Compare commits

...
117 Commits
Author SHA1 Message Date
Ben Woo 112505bc93 Merge pull request #3437 from Multiverse/fix/ansi-reset
Fix ANSI color format reset in log messages
2026-04-04 20:55:50 +08:00
Ben Woo e27fe73faa Fix ANSI color format reset in log messages 2026-04-04 14:46:06 +08:00
Ben Woo 9bc2b4dfaf Merge pull request #3422 from Multiverse/feat/clone-unloaded
Add support for cloning unloaded worlds
2026-04-04 14:32:14 +08:00
Ben Woo 3ba8beefc5 Merge pull request #3436 from Multiverse/fix/clone-paper-metadata
Fix clone not working due to duplicated Paper world metadata in 26.1
2026-04-04 13:28:16 +08:00
Ben Woo 05b6b380e0 Fix clone not working due to duplicated Paper world metadata in 26.1 2026-04-04 12:31:25 +08:00
Ben Woo b58ea2e04b Update error messages for invalid world folder following the format of 2026-04-04 12:17:03 +08:00
Ben Woo f4405874d1 Merge branch 'main' into feat/clone-unloaded 2026-04-04 12:05:01 +08:00
Ben Woo 247685c93d Merge pull request #3435 from Multiverse/feat/26.1-compatibility
Update world folder discovery and checking to support new 26.1 format
2026-04-04 11:38:16 +08:00
Ben Woo 490107b5d5 Fix tests due to world folder format changes 2026-04-04 11:35:30 +08:00
Ben Woo 122e35103a Fix some formatting and documentation 2026-04-04 11:22:08 +08:00
Ben Woo 25eeefbc77 Update platform upload version list to include 26.1 2026-04-04 11:16:12 +08:00
Ben Woo 6b5ce2aebb Update world folder discovery and checking to support new 26.1 format 2026-04-04 11:13:43 +08:00
Ben Woo e9905ca281 Merge pull request #3434 from Multiverse/fix/load-flags
Fix `--skip-folder-check` flag not showing for `/mv load` command
2026-04-03 22:01:02 +08:00
Ben Woo e002282b13 Fix --skip-folder-check flag not showing for /mv load command 2026-04-03 21:56:13 +08:00
Ben Woo 39711527fa Merge pull request #3431 from Multiverse/feat/placeholder-isloaded
Add PlaceholderAPI support for showing world load status (`%multiverse-core_isloaded_<worldname>%`)
2026-03-10 22:31:42 +08:00
Ben Woo 8d9b29ff87 Add PlaceholderAPI support for showing world load status (%multiverse-core_isloaded_<worldname>%) 2026-03-10 22:27:38 +08:00
Ben Woo 82fe350afa Merge pull request #3429 from Multiverse/feat/apply-passenger-mode
Apply passenger mode to join location and first spawn as well
2026-03-09 10:48:15 +08:00
Ben Woo a7b4b9bbda Merge pull request #3430 from Multiverse/chore/access-modifier
Change entry fee migrator class access modifier to private
2026-03-09 10:47:35 +08:00
Ben Woo 15f511b660 Change entry fee migrator class access modifier to private 2026-03-09 10:40:00 +08:00
Ben Woo 8347cf8fbb Apply passenger mode to join location and first spawn as well 2026-03-09 10:39:13 +08:00
Ben Woo 4c27fea26b Merge pull request #3427 from Multiverse/fix/regen-biome
Fix biome not applying during world regen
2026-02-26 10:43:47 +08:00
Ben Woo 4111da8fd6 Fix biome not applying during world regen 2026-02-26 10:34:13 +08:00
Ben Woo 167f6f8a37 Merge pull request #3423 from Multiverse/fix/placeholder-null
Fix null error when world name for non-player placeholders not specified
2026-01-28 10:22:42 +08:00
Ben Woo 325b5d7643 Fix null error when world name for non-player placeholders not specified 2026-01-28 10:16:03 +08:00
Ben Woo 5dfb098d5c Add support for cloning unloaded worlds 2026-01-26 12:18:47 +08:00
Ben Woo 6c50bfb08e Merge pull request #3421 from Multiverse/fix/null-to-location
Better null checking for to and from event locations
2026-01-20 22:50:49 +08:00
Ben Woo 37c0580828 Merge pull request #3420 from Multiverse/chore/bump-mockbukkit
Bump mockbukkit and associated mc version to 1.21.11
2026-01-20 22:49:48 +08:00
Ben Woo e1a42cec92 Bump mockbukkit and associated mc version to 1.21.11 2026-01-20 22:45:13 +08:00
Ben Woo a6cc8bd68e Better null checking for to and from event locations 2026-01-20 22:44:04 +08:00
Ben Woo ee4cd18522 Merge pull request #3410 from Multiverse/v5.5-2
V5.5-2
2026-01-03 23:17:13 +08:00
Ben Woo b6c309a332 Merge pull request #3417 from Multiverse/fix/since-annotation
Add @since annotation for worldPropertyStrings method in CreateWorldOptions
2026-01-03 23:01:44 +08:00
Ben Woo 87571e7037 Add @since annotation for worldPropertyStrings method in CreateWorldOptions 2026-01-03 22:59:18 +08:00
Ben Woo 0822b1460a Merge pull request #3415 from Multiverse/refactor/string-formatter
Enhance StringFormatter methods to handle null inputs and improve type annotations
2026-01-03 22:23:19 +08:00
Ben Woo 1220998c12 Merge pull request #3416 from Multiverse/chore/logging
Add a logging message when plugin shutdown
2026-01-02 14:27:06 +08:00
Ben Woo e078c5e5dd Add a logging message when plugin shutdown 2026-01-02 14:08:49 +08:00
Ben Woo c1ef8f656b Enhance StringFormatter methods to handle null inputs and improve type annotations 2026-01-02 09:49:35 +08:00
Ben Woo 86e2112d87 Merge pull request #3414 from Multiverse/feat/properties-flag
Add support for customising world properties during world creation
2026-01-02 09:39:36 +08:00
Ben Woo 8a77dc807e Add --properties syntax to create command 2026-01-02 09:37:08 +08:00
Ben Woo 77382dacf3 Add support for customising world properties during world creation 2026-01-02 09:14:00 +08:00
Ben Woo 588a8e1baf Merge pull request #3413 from Multiverse/fix/seperated-typo
Fix spelling error for addOnToCommaSeparated method name
2026-01-02 08:59:18 +08:00
Ben Woo 5bd37d17c3 Fix spelling error for addOnToCommaSeparated method name 2026-01-01 23:32:10 +08:00
Ben Woo 815637e00f Merge pull request #3412 from Multiverse/feat/world-change-priority
Add event priority configuration for PlayerChangedWorldEvent
2026-01-01 23:19:22 +08:00
Ben Woo 78194389eb Merge pull request #3411 from Multiverse/feat/enforce-delay
Add configurable delay for gamemode and flight enforcement on world change
2026-01-01 23:13:52 +08:00
Ben Woo 2db55fab3b Add event priority configuration for PlayerChangedWorldEvent 2026-01-01 23:12:42 +08:00
Ben Woo c505da76ec Fix config test 2026-01-01 22:36:26 +08:00
Ben Woo 6152620541 Merge pull request #3409 from Multiverse/fix/worldborder-safety
Add world border check for spawn safety
2026-01-01 22:30:46 +08:00
Ben Woo cf70b3b839 Add configurable delay for gamemode and flight enforcement on world change 2026-01-01 22:30:10 +08:00
Ben Woo 527085713c Add world border check for spawn safety 2026-01-01 15:03:24 +08:00
Ben Woo f446c94dbc Merge pull request #3407 from Multiverse/v5.5
V5.5
2026-01-01 13:48:07 +08:00
Ben Woo aac0111dde Merge pull request #3408 from Multiverse/fix/case-insensitive-worldname
Fix case-insensitive world name handling
2026-01-01 12:59:53 +08:00
Ben Woo f66e673ea9 Fix case-insensitive world name handling 2026-01-01 12:42:18 +08:00
Ben Woo 1434ca176a Merge pull request #3406 from Multiverse/feat/papi-config
Add config options `warn-invalid-papi-format` and `invalid-papi-format-returns-blank`
2025-12-31 22:36:48 +08:00
Ben Woo 212b68437e Add config options warn-invalid-papi-format and invalid-papi-format-returns-blank 2025-12-31 21:14:56 +08:00
Ben Woo 05d01884ad Merge pull request #3405 from Multiverse/feat/papi-unloaded-world
Allow placerholders to work on unloaded worlds
2025-12-31 20:48:42 +08:00
Ben Woo 65c399740e Allow placerholders to work on unloaded worlds 2025-12-31 20:41:08 +08:00
Ben Woo 72ee0d5bed Merge pull request #3402 from Multiverse/fix/on-change-node
Always fire onChange for node even if old and new values may be equal
2025-12-12 12:01:31 +08:00
Ben Woo 1afecd40ea Merge pull request #3401 from Multiverse/chore/1.21.11
Update platforms supported version for 1.21.11
2025-12-12 11:57:24 +08:00
Ben Woo 00944ed3b2 Always fire onChange for node even if old and new values may be equal 2025-12-12 11:56:50 +08:00
Ben Woo 5f659a80d8 Update platforms supported version for 1.21.11 2025-12-11 14:24:39 +08:00
Ben Woo 5f9b0af8d1 Merge pull request #3400 from Multiverse/feat/notify-restart
Add restart message for config options that only applies after a server restart
2025-12-10 10:44:34 +08:00
Ben Woo 5c44c05f6f Add restart message for config options that only applies after a server restart 2025-12-09 20:48:04 +08:00
Ben Woo 1699a47011 Merge pull request #3396 from Multiverse/feat/unset-entity-rate
Default spawn rate and limit to "unset" instead of server wide's default
2025-12-09 17:16:27 +08:00
Ben Woo 96deee719b Merge pull request #3397 from Multiverse/fix/config-reset
Remove config saving on plugin disable
2025-12-09 17:15:33 +08:00
Ben Woo 8a60761098 Merge pull request #3398 from Multiverse/fix/arg-handling
Add maxArgForAware flag to gamerule commands for improved argument handling
2025-12-09 17:14:35 +08:00
Ben Woo a38b4e03b4 Merge pull request #3399 from Multiverse/fix/localise-replacements
Properly localise message replacement even if message itself is not localised
2025-12-09 17:14:18 +08:00
Ben Woo db9b725e10 Properly localise message replacement even if message itself is not localised 2025-12-09 16:17:43 +08:00
Ben Woo d43b23e7ea Add maxArgForAware flag to gamerule commands for improved argument handling 2025-12-09 15:59:57 +08:00
Ben Woo 86d77ae796 Remove config saving on plugin disable 2025-12-09 15:50:38 +08:00
Ben Woo 86bf785c84 Improve config saving error checking 2025-12-09 15:50:19 +08:00
Ben Woo 626537290d Default spawn rate and limit to "unset" instead of server wide's default 2025-12-09 14:41:01 +08:00
Ben Woo 9fa47727f4 Merge pull request #3395 from Multiverse/feat/node-load-change
Refactor configuration node on set callbacks to split up load and change events
2025-12-09 12:34:12 +08:00
Ben Woo ae3bc6d201 Rename NodeLoadCallback to NodeValueCallback to extend it potential use case 2025-12-09 11:28:02 +08:00
Ben Woo b1d126397e Refactor configuration node on set callbacks to split up load and change events 2025-12-08 21:18:10 +08:00
Ben Woo 8ab9993e97 Merge pull request #3394 from Multiverse/feat/case-insensitive
Make world name matching case-insensitive to follow papermc behvaiour
2025-12-08 21:03:28 +08:00
Ben Woo 13b949c692 Make world name matching case-insensitive to follow papermc behvaiour 2025-12-08 17:39:10 +08:00
Ben Woo 2b41c1367e Merge pull request #3393 from Multiverse/feat/more-patterns
Add more useful common regex patterns
2025-12-08 17:21:25 +08:00
Ben Woo fde3f388bd Add more useful common regex patterns 2025-12-07 20:56:00 +08:00
Ben Woo eb3968b7d6 Merge pull request #3391 from Multiverse/feat/sort-plugin-list
Sort plugin list alphabetically for dumps
2025-12-04 10:20:19 +08:00
Ben Woo fed1fdcbea Sort plugin list alphabetically for dumps 2025-12-03 10:34:05 +08:00
Ben Woo bfb6fea5f5 Merge pull request #3390 from Multiverse/feat/placeholder-playercount
Add player count placeholder
2025-12-02 23:13:13 +08:00
Ben Woo 5b02452a3d Add player count placeholder 2025-12-02 23:03:30 +08:00
Ben Woo 4a2bfc74fa Merge pull request #3361 from Multiverse/v5.4
V5.4
2025-12-01 17:06:01 +08:00
Ben Woo f0539441cf Merge pull request #3388 from Multiverse/chore/purpur-loader
Add purpur as a supported loader tag on modrinth
2025-12-01 17:04:51 +08:00
Ben Woo efe490d8c4 Add purpur as a supported loader tag on modrinth 2025-12-01 16:55:41 +08:00
Ben Woo 3074cfab17 Fix typo in maxArgForAware for who command 2025-12-01 16:51:16 +08:00
Ben Woo 7a5dd44feb Merge pull request #3383 from Multiverse/refactor/using-option
Refactor world manager methods with better use of streams and option
2025-12-01 16:41:21 +08:00
Ben Woo 88a9ee5b6d Merge pull request #3387 from Multiverse/fix/selectors-error-spam
Fix selectors spamming stacktrace and improve invalid format message
2025-12-01 16:41:02 +08:00
Ben Woo 46f67240e8 Merge pull request #3384 from Multiverse/fix/issuer-aware
Fix ambiguous error message with issue aware args is not found by using maxArgForAware
2025-12-01 16:40:45 +08:00
Ben Woo 89e765b2e8 Fix selectors spamming stacktrace and improve invalid format message 2025-11-29 11:15:04 +08:00
Ben Woo b8bdec22d3 Fix ambiguous error message with issue aware args is not found by using maxArgForAware 2025-11-19 15:32:26 +08:00
Ben Woo 792913cff7 Refactor world manager methods with better use of streams and option 2025-11-19 15:00:39 +08:00
Ben Woo 6dcb40fdcb Merge pull request #3380 from Multiverse/fix/issuer-aware-argument
Better error message for teleport command when no players match selectors
2025-11-19 11:25:46 +08:00
Ben Woo 2bb601098a Merge pull request #3382 from Multiverse/refactor/attempt
Refactor Attempt interface to remove default method implementations and enforce contract
2025-11-18 16:40:22 +08:00
Ben Woo 4bcebd51f5 Merge pull request #3381 from Multiverse/chore/whitespace
Remove unnecessary blank line in CloneWorldOptions.java
2025-11-18 16:40:01 +08:00
Ben Woo e207eb5d0d Merge branch 'main' into v5.4 2025-11-18 16:37:44 +08:00
Ben Woo f181bb7ade Better error message for teleport command when no players match selectors. 2025-11-18 16:34:38 +08:00
Ben Woo 7207d9303f Refactor IssuerAwareContextBuilder and add maxArgForAware option 2025-11-18 16:34:00 +08:00
Ben Woo 82799cdbe8 Open up PlayerFinder::isSelector method 2025-11-18 16:31:12 +08:00
Ben Woo 3629babdbc Remove unnecessary blank line in CloneWorldOptions.java 2025-11-18 15:34:49 +08:00
Ben Woo eed3353e2b Refactor Attempt interface to remove default method implementations and enforce contract 2025-11-18 15:34:28 +08:00
Ben Woo 147564caab Merge pull request #3372 from Multiverse/fix/perm-register
Register permission with its wildcards explicitly
2025-11-13 10:52:04 +08:00
Ben Woo f7d86884b4 Merge pull request #3369 from Multiverse/feat/simplified-destination
Add simplified destination tab completion configuration option
2025-10-30 11:30:12 +08:00
Ben Woo 0e0e4e0434 Register permission with its wildcards explicitly 2025-10-30 11:29:12 +08:00
Ben Woo b52d65757a Fix config test 2025-10-26 13:27:55 +08:00
Ben Woo ff2135c775 Add simplified destination tab completion configuration option 2025-10-26 12:09:06 +08:00
Ben Woo c4e4e311e2 Merge pull request #3368 from Multiverse/perf/suggest-destination
Minor perf improvement to suggestDestinationsWithPerms
2025-10-26 11:59:39 +08:00
Ben Woo 3e4ded52e3 Merge branch 'main' into v5.4 2025-10-26 11:45:23 +08:00
Ben Woo 4ef8390ea3 Minor perf improvement to suggestDestinationsWithPerms 2025-10-26 11:44:57 +08:00
Ben Woo 8bffffdc65 Merge pull request #3364 from Multiverse/feat/silent-teleport
Add a silent flag to turn off teleport output message
2025-10-16 11:21:03 +08:00
Ben Woo 79a71fa58f Add a silent flag to turn off teleport output message 2025-10-15 22:43:22 +08:00
Ben Woo fdd9257e2c Merge pull request #3359 from Multiverse/fix/async-online-players
Implement ConcurrentPlayerWorldTracker to allow for async online player names and worlds lookup
2025-10-13 17:41:37 +08:00
Ben Woo 55d1347d4f Merge pull request #3360 from Multiverse/feat/destination-message
Implement display message for destination instances to improve teleport command output
2025-10-13 17:38:45 +08:00
Ben Woo 29cc97486b Add @since 5.4 annotations to ConcurrentPlayerWorldTracker methods 2025-10-13 17:37:41 +08:00
Ben Woo 8d06b92edd Implement display message for destination instances to improve teleport command output 2025-10-12 23:27:12 +08:00
Ben Woo ce3b6ab700 Make getOnlinePlayers unmodifiable and getPlayerWorld return option 2025-10-12 23:00:01 +08:00
Ben Woo 58ac0f505c Make tab completion for players a non-async call 2025-10-12 22:58:50 +08:00
Ben Woo c068225950 Implement ConcurrentPlayerWorldTracker to allow for async online player names and worlds lookup 2025-10-01 21:45:27 +08:00
85 changed files with 2848 additions and 824 deletions

No files matched your search

@@ -107,9 +107,9 @@ jobs:
files: '["${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar"]'
name: ${{ steps.release-info.outputs.tag_name }}
changelog: ${{ steps.release-artifact.outputs.body }}
game_versions: 1.21.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.1.1, 26.1, 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
version_type: ${{ steps.parse-release-type.outputs.release_type }}
loaders: bukkit, spigot, paper
loaders: bukkit, spigot, paper, purpur
dependencies: ${{ inputs.modrinth_dependencies }}
- name: Upload to DBO
@@ -121,7 +121,7 @@ jobs:
changelog: ${{ steps.release-artifact.outputs.body }}
changelog_type: markdown
display_name: ${{ steps.release-info.outputs.tag_name }}
game_versions: 1.21.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.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.10"]}'
platform_dependencies: '{"PAPER": ["1.18.2-26.1.1"]}'
plugin_dependencies: ${{ inputs.hangar_plugin_dependencies }}
+2 -2
View File
@@ -25,9 +25,9 @@ repositories {
}
configure(apiDependencies) {
serverApiVersion = '1.21.8-R0.1-SNAPSHOT'
serverApiVersion = '1.21.11-R0.1-SNAPSHOT'
mockBukkitServerApiVersion = '1.21'
mockBukkitVersion = '4.84.0'
mockBukkitVersion = '4.100.0'
}
dependencies {
@@ -124,9 +124,9 @@ public class MultiverseCore extends MultiverseModule {
public void onDisable() {
super.onDisable();
MultiverseCoreApi.shutdown();
saveAllConfigs();
shutdownDependencyInjection();
PluginServiceLocatorFactory.get().shutdown();
Logging.info("- Disabled");
Logging.shutdown();
}
@@ -215,22 +215,23 @@ public class MultiverseCore extends MultiverseModule {
*/
private Try<Void> saveAllConfigs() {
return configProvider.get().save()
.flatMap(ignore -> worldManagerProvider.get().saveWorldsConfig())
.flatMap(ignore ->anchorManagerProvider.get().saveAllAnchors())
.onFailure(e -> {
Logging.severe("Failed to save configs, things may not work as expected.");
});
.andThenTry(() -> worldManagerProvider.get().saveWorldsConfig())
.andThenTry(() -> anchorManagerProvider.get().saveAllAnchors())
.onFailure(e ->
Logging.severe("Failed to save all configs, things may not work as expected. %s",
e.getLocalizedMessage()));
}
/**
* Logs the enable message.
*/
private void logEnableMessage() {
Logging.config("\u001B[32mVersion %s (API v%s) Enabled - By %s\u001B[39m",
Logging.config("\u001B[32mVersion %s (API v%s) Enabled - By %s\u001B[0m",
this.getDescription().getVersion(), getVersionAsNumber(), StringFormatter.joinAnd(getDescription().getAuthors()));
if (configProvider.get().isShowingDonateMessage()) {
Logging.config("\u001B[32mLoving Multiverse-Core? Please consider supporting the project with a small donation: https://github.com/sponsors/Multiverse\u001B[39m");
Logging.config("\u001B[32mLoving Multiverse-Core? Please consider supporting the project with a " +
"small donation: https://github.com/sponsors/Multiverse\u001B[0m");
}
}
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core;
import com.google.common.collect.Lists;
import io.vavr.control.Option;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
@@ -12,11 +13,13 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.utils.MinecraftTimeFormatter;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.util.List;
@@ -26,12 +29,17 @@ import java.util.Locale;
final class PlaceholderExpansionHook extends PlaceholderExpansion {
private final MultiverseCore plugin;
private final CoreConfig coreConfig;
private final WorldManager worldManager;
private final MVEconomist economist;
@Inject
public PlaceholderExpansionHook(MultiverseCore plugin, WorldManager worldManager, MVEconomist economist) {
public PlaceholderExpansionHook(@NotNull MultiverseCore plugin,
@NotNull CoreConfig coreConfig,
@NotNull WorldManager worldManager,
@NotNull MVEconomist economist) {
this.plugin = plugin;
this.coreConfig = coreConfig;
this.worldManager = worldManager;
this.economist = economist;
}
@@ -42,6 +50,13 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
return super.register();
}
@Override
public void warning(String msg) {
if (coreConfig.getWarnInvalidPapiFormat()) {
super.warning(msg);
}
}
@Override
public @NotNull String getIdentifier() {
return "multiverse-core";
@@ -80,130 +95,102 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
return null;
}
final var placeholder = paramsArray.remove(0);
final var placeholder = paramsArray.remove(0).toLowerCase(Locale.ENGLISH);
String worldName = parseWorldName(offlinePlayer, paramsArray);
if (worldName == null) return null;
return worldManager.getLoadedWorld(worldName)
.onEmpty(() -> warning("Multiverse World not found: " + worldName))
.map(world -> getWorldPlaceHolderValue(placeholder, paramsArray, world))
.getOrNull();
return parseWorldName(offlinePlayer, paramsArray)
.flatMap(worldName -> worldManager.getWorld(worldName)
.onEmpty(() -> warning("Multiverse World not found: " + worldName)))
.flatMap(world -> world.asLoadedWorld()
.flatMap(loadedWorld -> getLoadedWorldPlaceHolderValue(placeholder, paramsArray, loadedWorld))
.orElse(() -> getWorldPlaceHolderValue(placeholder, paramsArray, world)))
.getOrElse(() -> coreConfig.getInvalidPapiFormatReturnsBlank() ? "" : null);
}
private @Nullable String parseWorldName(OfflinePlayer offlinePlayer, List<String> paramsArray) {
private @NotNull Option<String> parseWorldName(OfflinePlayer offlinePlayer, List<String> paramsArray) {
// No world defined, get from player
if (paramsArray.isEmpty()) {
if (offlinePlayer instanceof Player player) {
return player.getWorld().getName();
return Option.of(player.getWorld().getName());
} else {
warning("You must specify a world name for non-player placeholders");
return null;
return Option.none();
}
}
// Try get from params
String paramWorldName = paramsArray.get(paramsArray.size() - 1);
if (worldManager.isLoadedWorld(paramWorldName)) {
if (worldManager.isWorld(paramWorldName)) {
paramsArray.remove(paramsArray.size() - 1);
return paramWorldName;
return Option.of(paramWorldName);
}
// Param not a world, fallback to player
if (offlinePlayer instanceof Player player) {
return player.getWorld().getName();
return Option.of(player.getWorld().getName());
}
warning("Multiverse World not found: " + paramWorldName);
return null;
return Option.none();
}
private @Nullable String getWorldPlaceHolderValue(@NotNull String placeholder,
@NotNull List<String> placeholderParams,
@NotNull LoadedMultiverseWorld world) {
// Switch to find what specific placeholder we want
switch (placeholder.toLowerCase(Locale.ENGLISH)) {
case "alias" -> {
return world.getAliasOrName();
private @NotNull Option<String> getWorldPlaceHolderValue(@NotNull String placeholder,
@NotNull List<String> placeholderParams,
@NotNull MultiverseWorld world) {
return Option.of(switch (placeholder) {
case "alias" -> world.getAliasOrName();
case "animalspawn" -> String.valueOf(world.getEntitySpawnConfig()
.getSpawnCategoryConfig(SpawnCategory.ANIMAL)
.isSpawn());
case "autoheal" -> String.valueOf(world.getAutoHeal());
case "blacklist" -> String.join(", ", world.getWorldBlacklist());
case "currency" -> String.valueOf(world.getCurrency());
case "difficulty" -> world.getDifficulty().toString();
case "entryfee" -> economist.formatPrice(world.getPrice(), world.getCurrency());
case "environment" -> world.getEnvironment().toString().toLowerCase();
case "flight" -> String.valueOf(world.isAllowFlight());
case "gamemode" -> world.getGameMode().toString().toLowerCase();
case "generator" -> world.getGenerator();
case "hunger" -> String.valueOf(world.isHunger());
case "isloaded" -> String.valueOf(world.isLoaded());
case "monstersspawn" -> String.valueOf(world.getEntitySpawnConfig()
.getSpawnCategoryConfig(SpawnCategory.MONSTER)
.isSpawn());
case "name" -> world.getName();
case "playerlimit" -> String.valueOf(world.getPlayerLimit());
case "price" -> String.valueOf(world.getPrice());
case "pvp" -> String.valueOf(world.getPvp());
case "seed" -> String.valueOf(world.getSeed());
case "weather" -> String.valueOf(world.isAllowWeather());
case "playercount", "time", "type" -> {
warning("Placeholder '" + placeholder + "' is only available for loaded worlds.");
yield null;
}
case "animalspawn" -> {
return String.valueOf(world.getEntitySpawnConfig().getSpawnCategoryConfig(SpawnCategory.ANIMAL).isSpawn());
}
case "autoheal" -> {
return String.valueOf(world.getAutoHeal());
}
case "blacklist" -> {
return String.join(", ", world.getWorldBlacklist());
}
case "currency" -> {
return String.valueOf(world.getCurrency());
}
case "difficulty" -> {
return world.getDifficulty().toString();
}
case "entryfee" -> {
return economist.formatPrice(world.getPrice(), world.getCurrency());
}
case "environment" -> {
return world.getEnvironment().toString().toLowerCase();
}
case "flight" -> {
return String.valueOf(world.isAllowFlight());
}
case "gamemode" -> {
return world.getGameMode().toString().toLowerCase();
}
case "generator" -> {
return world.getGenerator();
}
case "hunger" -> {
return String.valueOf(world.isHunger());
}
case "monstersspawn" -> {
return String.valueOf(world.getEntitySpawnConfig().getSpawnCategoryConfig(SpawnCategory.MONSTER).isSpawn());
}
case "name" -> {
return world.getName();
}
case "playerlimit" -> {
return String.valueOf(world.getPlayerLimit());
}
case "price" -> {
return String.valueOf(world.getPrice());
}
case "pvp" -> {
return String.valueOf(world.getPvp());
}
case "seed" -> {
return String.valueOf(world.getSeed());
default -> {
warning("Unknown Placeholder: " + placeholder);
yield null;
}
});
}
private @NotNull Option<String> getLoadedWorldPlaceHolderValue(@NotNull String placeholder,
@NotNull List<String> placeholderParams,
@NotNull LoadedMultiverseWorld world) {
return Option.of(switch (placeholder) {
case "playercount" -> String.valueOf(world.getBukkitWorld()
.map(World::getPlayers)
.map(List::size)
.getOrElse(-1));
case "time" -> {
String timeFormat = !placeholderParams.isEmpty() ? placeholderParams.get(0) : "";
long time = world.getBukkitWorld().map(World::getTime).getOrElse(0L);
switch (timeFormat) {
case "" -> {
return String.valueOf(time);
}
case "12h" -> {
return MinecraftTimeFormatter.format12h(time);
}
case "24h" -> {
return MinecraftTimeFormatter.format24h(time);
}
default -> {
return MinecraftTimeFormatter.formatTime(time, timeFormat);
}
}
yield switch (timeFormat) {
case "" -> String.valueOf(time);
case "12h" -> MinecraftTimeFormatter.format12h(time);
case "24h" -> MinecraftTimeFormatter.format24h(time);
default -> MinecraftTimeFormatter.formatTime(time, timeFormat);
};
}
case "type" -> {
return world.getBukkitWorld().map(World::getWorldType).map(Enum::name).getOrElse("null");
}
case "weather" -> {
return String.valueOf(world.isAllowWeather());
}
default -> {
warning("Unknown placeholder: " + placeholder);
return null;
}
}
case "type" -> world.getBukkitWorld().map(World::getWorldType).map(Enum::name).getOrElse("null");
default -> null;
});
}
}
@@ -27,6 +27,7 @@ import org.bukkit.GameRule;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.SpawnCategory;
import org.jetbrains.annotations.NotNull;
@@ -43,16 +44,16 @@ import org.mvplugins.multiverse.core.config.handle.PropertyModifyAction;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.core.WorldDestination;
import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.generators.GeneratorPlugin;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
import static org.mvplugins.multiverse.core.utils.StringFormatter.addonToCommaSeperated;
import static org.mvplugins.multiverse.core.utils.StringFormatter.addOnToCommaSeparated;
@Service
public class MVCommandCompletions extends PaperCommandCompletions {
@@ -100,7 +101,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
registerAsyncCompletion("mvworlds", this::suggestMVWorlds);
registerAsyncCompletion("mvworldpropsname", this::suggestMVWorldPropsName);
registerAsyncCompletion("mvworldpropsvalue", this::suggestMVWorldPropsValue);
registerAsyncCompletion("playersarray", this::suggestPlayersArray);
registerCompletion("playersarray", this::suggestPlayersArray); // getting online players cannot be async
registerStaticCompletion("propsmodifyaction", suggestEnums(PropertyModifyAction.class));
registerStaticCompletion("spawncategories", suggestEnums(SpawnCategory.class));
registerAsyncCompletion("spawncategorypropsname", this::suggestSpawnCategoryPropsName);
@@ -162,7 +163,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
}
}
if (context.hasConfig("multiple")) {
return addonToCommaSeperated(context.getInput(), handler.getCompletions(context));
return addOnToCommaSeparated(context.getInput(), handler.getCompletions(context));
}
return handler.getCompletions(context);
}
@@ -224,18 +225,22 @@ public class MVCommandCompletions extends PaperCommandCompletions {
// Most likely console did not specify a player
return Collections.<String>emptyList();
}
if (context.hasConfig("othersOnly") && (players.length == 1 && players[0].equals(context.getIssuer().getIssuer()))) {
CommandSender sender = context.getIssuer().getIssuer();
if (context.hasConfig("othersOnly") && (players.length == 1 && players[0].equals(sender))) {
return Collections.<String>emptyList();
}
return suggestDestinationsWithPerms(context.getIssuer().getIssuer(), players, context.getInput());
return suggestDestinationsWithPerms(sender, Arrays.asList(players), context.getInput());
})
.getOrElse(Collections.emptyList());
}
private Collection<String> suggestDestinationsWithPerms(CommandSender teleporter, Player[] players, String deststring) {
private Collection<String> suggestDestinationsWithPerms(CommandSender teleporter, List<Entity> teleportees, String deststring) {
return destinationsProvider.suggestDestinations(teleporter, deststring).stream()
.filter(packet -> !config.getSimplifiedDestinationTabCompletion()
|| packet.destination() instanceof WorldDestination
|| deststring.startsWith(packet.destination().getIdentifier() + ":"))
.filter(packet -> corePermissionsChecker
.checkDestinationPacketPermission(teleporter, Arrays.asList(players), packet))
.checkDestinationPacketPermission(teleporter, teleportees, packet))
.map(DestinationSuggestionPacket::parsableString)
.toList();
}
@@ -329,7 +334,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
matchedPlayers.add(name);
}
}
return addonToCommaSeperated(context.getInput(), matchedPlayers);
return addOnToCommaSeparated(context.getInput(), matchedPlayers);
}
private Collection<String> suggestSpawnCategoryPropsName(BukkitCommandCompletionContext context) {
@@ -11,6 +11,7 @@ import co.aikar.commands.MinecraftMessageKeys;
import co.aikar.commands.PaperCommandContexts;
import co.aikar.commands.contexts.ContextResolver;
import com.google.common.base.Strings;
import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
@@ -272,13 +273,25 @@ public class MVCommandContexts extends PaperCommandContexts {
private IssuerAwareContextBuilder<Player[]> playerArrayContextBuilder() {
return new IssuerAwareContextBuilder<Player[]>()
.fromPlayer((context, player) -> new Player[]{player})
.fromInput((context, input) -> {
Player[] players = PlayerFinder.getMulti(input, context.getSender()).toArray(new Player[0]);
return (players.length == 0) ? null : players;
})
.fromInput((context, input) -> PlayerFinder
.tryGetMulti(input, context.getSender())
.map(list -> list.toArray(new Player[0]))
.map(arr -> (arr.length == 0) ? null : arr)
.getOrElseThrow(failure -> {
if (failure instanceof MVInvalidCommandArgument mvFailure) {
throw mvFailure;
}
throw new InvalidCommandArgument(failure.getLocalizedMessage() + " "
+ Option.of(failure.getCause()).map(Throwable::getLocalizedMessage).getOrElse(""));
}))
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player."))
.issuerAwareInputFailMessage((context, input) -> Message.of("Invalid player: " + input + ". Either specify an online player or use this command as a player."))
.inputOnlyFailMessage((context, input) -> Message.of("Player " + input + " not found."));
.inputOnlyFailMessage((context, input) -> {
if (PlayerFinder.isSelector(input)) {
return Message.of("No player(s) matched selector: " + input + ".");
}
return Message.of("Player(s) " + input + " not found.");
});
}
private PlayerLocation parsePlayerLocation(BukkitCommandExecutionContext context) {
@@ -112,9 +112,9 @@ public class MVCommandIssuer extends BukkitCommandIssuer {
} else {
var formatter = getManager().getFormat(messageType);
if (formatter != null) {
sendMessage(formatter.format(message.formatted()));
sendMessage(formatter.format(message.formatted(getManager().getLocales(), this)));
} else {
sendMessage(message.formatted());
sendMessage(message.formatted(getManager().getLocales(), this));
}
}
}
@@ -123,48 +123,8 @@ public final class IssuerAwareContextBuilder<T> {
*/
@ApiStatus.AvailableSince("5.1")
public IssuerAwareContextResolver<T, BukkitCommandExecutionContext> generateContext() {
Objects.requireNonNull(fromPlayer);
Objects.requireNonNull(fromInput);
Objects.requireNonNull(issuerOnlyFailMessage);
Objects.requireNonNull(issuerAwarePlayerFailMessage);
Objects.requireNonNull(issuerAwareInputFailMessage);
Objects.requireNonNull(inputOnlyFailMessage);
return context -> {
BukkitCommandIssuer issuer = context.getIssuer();
String resolve = context.getFlagValue("resolve", "");
if (resolve.equals("issuerOnly")) {
if (issuer.isPlayer()) {
T result = fromPlayer.apply(context, issuer.getPlayer());
if (result != null) {
return result;
}
}
throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer));
}
String input = context.getFirstArg();
T result = fromInput.apply(context, input);
if (result != null) {
context.popFirstArg();
return result;
}
if (resolve.equals("issuerAware")) {
if (issuer.isPlayer()) {
Player player = issuer.getPlayer();
result = fromPlayer.apply(context, player);
if (result != null) {
return result;
}
throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer));
}
throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
};
validateRequiredVariables();
return context -> resolveValue(context, GenericIssuerAwareValue::new).value;
}
/**
@@ -178,49 +138,66 @@ public final class IssuerAwareContextBuilder<T> {
*/
@ApiStatus.AvailableSince("5.1")
public <I extends IssuerAwareValue> IssuerAwareContextResolver<I, BukkitCommandExecutionContext> generateContext(BiFunction<Boolean, T, I> createValue) {
// todo: This is a copy and paste from above
validateRequiredVariables();
return context -> resolveValue(context, createValue);
}
private void validateRequiredVariables() {
Objects.requireNonNull(fromPlayer);
Objects.requireNonNull(fromInput);
Objects.requireNonNull(issuerOnlyFailMessage);
Objects.requireNonNull(issuerAwarePlayerFailMessage);
Objects.requireNonNull(issuerAwareInputFailMessage);
Objects.requireNonNull(inputOnlyFailMessage);
}
return context -> {
BukkitCommandIssuer issuer = context.getIssuer();
String resolve = context.getFlagValue("resolve", "");
private <I extends IssuerAwareValue> I resolveValue(BukkitCommandExecutionContext context, BiFunction<Boolean, T, I> createValue) {
BukkitCommandIssuer issuer = context.getIssuer();
String resolve = context.getFlagValue("resolve", "");
if (resolve.equals("issuerOnly")) {
if (issuer.isPlayer()) {
T result = fromPlayer.apply(context, issuer.getPlayer());
if (result != null) {
return createValue.apply(true, result);
}
if (resolve.equals("issuerOnly")) {
if (issuer.isPlayer()) {
T result = fromPlayer.apply(context, issuer.getPlayer());
if (result != null) {
return createValue.apply(true, result);
}
throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer));
}
throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer));
}
String input = context.getFirstArg();
T result = fromInput.apply(context, input);
if (result != null) {
context.popFirstArg();
return createValue.apply(false, result);
}
String input = context.getFirstArg();
T result = fromInput.apply(context, input);
if (result != null) {
context.popFirstArg();
return createValue.apply(false, result);
}
if (resolve.equals("issuerAware")) {
if (issuer.isPlayer()) {
Player player = issuer.getPlayer();
result = fromPlayer.apply(context, player);
if (result != null) {
return createValue.apply(true, result);
}
throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
int maxArgForAware = context.getFlagValue("maxArgForAware", Integer.MAX_VALUE);
long argLengthWithoutFlags = context.getArgs().stream()
.takeWhile(value -> !value.startsWith("-") && !value.isEmpty()) // ignore the flags
.count();
if (resolve.equals("issuerAware") && argLengthWithoutFlags <= maxArgForAware) {
if (issuer.isPlayer()) {
Player player = issuer.getPlayer();
result = fromPlayer.apply(context, player);
if (result != null) {
return createValue.apply(true, result);
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer));
throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer));
}
throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
};
throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
}
private static class GenericIssuerAwareValue<T> extends IssuerAwareValue {
private final T value;
public GenericIssuerAwareValue(boolean byIssuer, T value) {
super(byIssuer);
this.value = value;
}
}
}
@@ -20,7 +20,7 @@ import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.options.CloneWorldOptions;
@@ -48,7 +48,7 @@ class CloneCommand extends CoreCommand {
@Syntax("<world>")
@Description("{@@mv-core.clone.world.description}")
LoadedMultiverseWorld world,
MultiverseWorld world,
@Syntax("<new-world-name>")
@Description("{@@mv-core.clone.newWorld.description}")
@@ -114,7 +114,7 @@ class CloneCommand extends CoreCommand {
@Override
@CommandAlias("mvcl|mvclone")
void onCloneCommand(MVCommandIssuer issuer, LoadedMultiverseWorld world, String newWorldName, String[] flags) {
void onCloneCommand(MVCommandIssuer issuer, MultiverseWorld world, String newWorldName, String[] flags) {
super.onCloneCommand(issuer, world, newWorldName, flags);
}
}
@@ -67,12 +67,10 @@ class ConfigCommand extends CoreCommand {
private void updateConfigValue(MVCommandIssuer issuer, String name, String value) {
var stringPropertyHandle = config.getStringPropertyHandle();
stringPropertyHandle.setPropertyString(issuer.getIssuer(), name, value)
.onSuccess(ignore -> {
config.save();
issuer.sendMessage(MVCorei18n.CONFIG_SET_SUCCESS,
Replace.NAME.with(name),
Replace.VALUE.with(stringPropertyHandle.getProperty(name).getOrNull()));
})
.andThenTry(config::save)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.CONFIG_SET_SUCCESS,
Replace.NAME.with(name),
Replace.VALUE.with(stringPropertyHandle.getProperty(name).getOrNull())))
.onFailure(e -> issuer.sendMessage(MVCorei18n.CONFIG_SET_ERROR,
Replace.NAME.with(name),
Replace.VALUE.with(value),
@@ -19,7 +19,6 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.CommandValueFlag;
@@ -27,6 +26,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.utils.StringFormatter;
import org.mvplugins.multiverse.core.utils.result.Attempt.Failure;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -53,7 +53,7 @@ class CreateCommand extends CoreCommand {
@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>]")
+ "--no-structures --biome <biome> --properties <prop1=value1,prop2=value2,...>]")
@Description("{@@mv-core.create.description}")
void onCreateCommand(
MVCommandIssuer issuer,
@@ -68,7 +68,7 @@ class CreateCommand extends CoreCommand {
@Optional
@Syntax("[--seed <seed> --generator <generator[:id]> --world-type <worldtype> --adjust-spawn "
+ "--no-structures --biome <biome>]")
+ "--no-structures --biome <biome> --properties <prop1=value1,prop2=value2,...>]")
@Description("{@@mv-core.create.flags.description}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -78,14 +78,15 @@ class CreateCommand extends CoreCommand {
issuer.sendInfo(MVCorei18n.CREATE_LOADING);
worldManager.createWorld(CreateWorldOptions.worldName(worldName)
.biome(parsedFlags.flagValue(flags.biome, ""))
.environment(environment)
.seed(parsedFlags.flagValue(flags.seed))
.worldType(parsedFlags.flagValue(flags.worldType, WorldType.NORMAL))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn))
.generator(parsedFlags.flagValue(flags.generator, ""))
.generatorSettings(parsedFlags.flagValue(flags.generatorSettings, ""))
.generateStructures(!parsedFlags.hasFlag(flags.noStructures)))
.biome(parsedFlags.flagValue(flags.biome, ""))
.environment(environment)
.seed(parsedFlags.flagValue(flags.seed))
.worldType(parsedFlags.flagValue(flags.worldType, WorldType.NORMAL))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn))
.generator(parsedFlags.flagValue(flags.generator, ""))
.generatorSettings(parsedFlags.flagValue(flags.generatorSettings, ""))
.generateStructures(!parsedFlags.hasFlag(flags.noStructures))
.worldPropertyStrings(StringFormatter.parseCSVMap(parsedFlags.flagValue(flags.properties))))
.onSuccess(newWorld -> messageSuccess(issuer, newWorld))
.onFailure(failure -> messageFailure(issuer, failure));
}
@@ -180,6 +181,10 @@ class CreateCommand extends CoreCommand {
.addAlias("-b")
.completion(input -> biomeProviderFactory.suggestBiomeString(input))
.build());
private final CommandValueFlag<String> properties = flag(CommandValueFlag.builder("--properties", String.class)
.addAlias("-p")
.build());
}
@Service
@@ -5,6 +5,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
@@ -136,6 +137,9 @@ final class DumpsService {
}
private String getPluginList() {
return " - " + StringUtils.join(plugin.getServer().getPluginManager().getPlugins(), "\n - ");
return " - " + StringUtils.join(Arrays.stream(plugin.getServer().getPluginManager().getPlugins())
.map(Object::toString)
.sorted()
.toList(), "\n - ");
}
}
@@ -52,7 +52,7 @@ final class EntitySpawnConfigCommand extends CoreCommand {
void onInfoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
MultiverseWorld world,
@@ -91,7 +91,7 @@ final class EntitySpawnConfigCommand extends CoreCommand {
void onModifyCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=4")
@Syntax("[world]")
MultiverseWorld world,
@@ -113,11 +113,10 @@ final class EntitySpawnConfigCommand extends CoreCommand {
.getSpawnCategoryConfig(spawnCategory)
.getStringPropertyHandle()
.modifyPropertyString(property, value, action)
.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()));
worldManager.saveWorldsConfig();
}
}
@@ -67,7 +67,7 @@ class GameruleCommand extends CoreCommand {
@Description("{@@mv-core.gamerule.set.value.description}")
GameRuleValue gameRuleValue,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=2")
@Syntax("[World or *]")
@Description("{@@mv-core.gamerule.set.world.description}")
LoadedMultiverseWorld[] worlds) {
@@ -114,7 +114,7 @@ class GameruleCommand extends CoreCommand {
@Description("{@@mv-core.gamerule.reset.gamerule.description}")
GameRule gamerule,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[World or *]")
@Description("{@@mv-core.gamerule.reset.world.description}")
LoadedMultiverseWorld[] worlds) {
@@ -152,7 +152,7 @@ class GameruleCommand extends CoreCommand {
void onGameruleListCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("<world>")
@Description("{@@mv-core.gamerule.list.description.world}")
LoadedMultiverseWorld world,
@@ -61,8 +61,8 @@ class InfoCommand extends CoreCommand {
public void onInfoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Syntax("<world>")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
@Description("{@@mv-core.info.description.world}")
LoadedMultiverseWorld world,
@@ -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) {
@@ -49,12 +49,12 @@ class ModifyCommand extends CoreCommand {
"@propsmodifyaction:notByIssuerForArg=arg1|@mvworldpropsname:byIssuerForArg=arg1 " +
"@mvworldpropsname:notByIssuerForArg=arg1|@mvworldpropsvalue:byIssuerForArg=arg1 " +
"@mvworldpropsvalue:notByIssuerForArg=arg1")
@Syntax("[world] <set|add|remove|reset> <property> <value>")
@Syntax("[world] <set|add|remove|reset> <property> [value]")
@Description("{@@mv-core.modify.description}")
void onModifyCommand(// SUPPRESS CHECKSTYLE: ParameterNumber
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=3")
@Syntax("[world]")
@Description("{@@mv-core.modify.world.description}")
@NotNull MultiverseWorldValue worldValue,
@@ -94,28 +94,28 @@ class ModifyCommand extends CoreCommand {
}
StringPropertyHandle worldPropertyHandle = world.getStringPropertyHandle();
worldPropertyHandle.modifyPropertyString(issuer.getIssuer(), propertyName, propertyValue, action).onSuccess(ignore -> {
issuer.sendMessage(MVCorei18n.MODIFY_SUCCESS,
worldPropertyHandle.modifyPropertyString(issuer.getIssuer(), propertyName, propertyValue, action)
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.MODIFY_SUCCESS,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(worldPropertyHandle.getProperty(propertyName).getOrNull()),
Replace.WORLD.with(world.getName()));
worldManager.saveWorldsConfig();
}).onFailure(exception -> {
if (propertyValue == null) {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE_NOVALUE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
} else {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(propertyValue),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
}
Replace.WORLD.with(world.getName())))
.onFailure(exception -> {
if (propertyValue == null) {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE_NOVALUE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
} else {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(propertyValue),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
}
});
}
@@ -33,7 +33,7 @@ final class PurgeAllEntitiesCommand extends CoreCommand {
void onPurgeAllEntitiesCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[world]")
LoadedMultiverseWorld world,
@@ -28,7 +28,7 @@ final class PurgeEntitiesCommand extends CoreCommand {
void onPurgeEntityCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -63,7 +63,7 @@ class SetSpawnCommand extends CoreCommand {
worldManager.getLoadedWorld(location.getWorld())
.peek(mvWorld -> mvWorld.setSpawnLocation(location)
.flatMap(ignore -> worldManager.saveWorldsConfig())
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.SETSPAWN_SUCCESS,
Replace.WORLD.with(mvWorld.getName()),
Replace.LOCATION.with(prettyLocation(location))))
@@ -19,7 +19,10 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.context.issueraware.PlayerArrayValue;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.PageFilterFlags;
import org.mvplugins.multiverse.core.command.flags.UnsafeFlags;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
@@ -35,14 +38,14 @@ final class TeleportCommand extends CoreCommand {
private final CoreConfig config;
private final CorePermissionsChecker permissionsChecker;
private final AsyncSafetyTeleporter safetyTeleporter;
private final UnsafeFlags flags;
private final Flags flags;
@Inject
TeleportCommand(
@NotNull CoreConfig config,
@NotNull CorePermissionsChecker permissionsChecker,
@NotNull AsyncSafetyTeleporter safetyTeleporter,
@NotNull UnsafeFlags flags
@NotNull Flags flags
) {
this.config = config;
this.permissionsChecker = permissionsChecker;
@@ -54,14 +57,14 @@ final class TeleportCommand extends CoreCommand {
@Subcommand("teleport|tp")
@CommandPermission("@mvteleport")
@CommandCompletion("@playersarray:checkPermissions=@mvteleportother|@destinations:byIssuerForArg=arg1 "
+ "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + UnsafeFlags.NAME + " "
+ "@flags:notByIssuerForArg=arg1,groupName=" + UnsafeFlags.NAME)
+ "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + Flags.NAME + " "
+ "@flags:notByIssuerForArg=arg1,groupName=" + Flags.NAME)
@Syntax("[player] <destination> [--unsafe]")
@Description("{@@mv-core.teleport.description}")
void onTeleportCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@co.aikar.commands.annotation.Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[player]")
@Description("{@@mv-core.teleport.player.description}")
PlayerArrayValue playersValue,
@@ -103,16 +106,21 @@ final class TeleportCommand extends CoreCommand {
.checkSafety(!parsedFlags.hasFlag(flags.unsafe) && destination.checkTeleportSafety())
.passengerMode(config.getPassengerMode())
.teleportSingle(player)
.onSuccess(() -> issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(getYouOrName(issuer, player)),
Replace.DESTINATION.with(destination.toString())))
.onSuccess(() -> {
if (parsedFlags.hasFlag(flags.silent)) {
return;
}
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(getYouOrName(issuer, player)),
Replace.DESTINATION.with(destination.getDisplayMessage()));
})
.onFailureCount(reasonsCountMap -> {
for (var entry : reasonsCountMap.entrySet()) {
Logging.finer("Failed to teleport %s players to %s: %s",
entry.getValue(), destination, entry.getKey());
issuer.sendError(MVCorei18n.TELEPORT_FAILED,
Replace.PLAYER.with(player.getName()),
Replace.DESTINATION.with(destination.toString()),
Replace.DESTINATION.with(destination.getDisplayMessage()),
Replace.REASON.with(Message.of(entry.getKey())));
}
});
@@ -136,18 +144,38 @@ final class TeleportCommand extends CoreCommand {
.checkSafety(!parsedFlags.hasFlag(flags.unsafe) && destination.checkTeleportSafety())
.passengerMode(config.getPassengerMode())
.teleport(List.of(players))
.onSuccessCount(successCount -> issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(successCount + " players"),
Replace.DESTINATION.with(destination.toString())))
.onSuccessCount(successCount -> {
if (parsedFlags.hasFlag(flags.silent)) {
return;
}
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(successCount + " players"),
Replace.DESTINATION.with(destination.getDisplayMessage()));
})
.onFailureCount(reasonsCountMap -> {
for (var entry : reasonsCountMap.entrySet()) {
Logging.finer("Failed to teleport %s players to %s: %s",
entry.getValue(), destination, entry.getKey());
issuer.sendError(MVCorei18n.TELEPORT_FAILED,
Replace.PLAYER.with(entry.getValue() + " players"),
Replace.DESTINATION.with(destination.toString()),
Replace.DESTINATION.with(destination.getDisplayMessage()),
Replace.REASON.with(Message.of(entry.getKey())));
}
});
}
@Service
private static final class Flags extends UnsafeFlags {
private static final String NAME = "mvteleport";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}
private final CommandFlag silent = flag(CommandFlag.builder("--silent")
.addAlias("-s")
.build());
}
}
@@ -74,12 +74,12 @@ class WhoCommand extends CoreCommand {
@Subcommand("who")
@CommandPermission("multiverse.core.list.who")
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + PageFilterFlags.NAME)
@Syntax("<world> [--page <page>] [--filter <filter>]")
@Syntax("[world] [--page <page>] [--filter <filter>]")
@Description("{@@mv-core.who.description}")
void onWhoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Syntax("<world>")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
@Description("{@@mv-core.who.world.description}")
LoadedMultiverseWorld inputtedWorld,
@@ -35,7 +35,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("[time]")
int time,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -54,7 +54,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("[z]")
double z,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -79,7 +79,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<damage>")
double damage,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -103,7 +103,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<distance>")
double distance,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -124,7 +124,7 @@ final class WorldBorderCommand extends CoreCommand {
void onWorldBorderGet(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -147,7 +147,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("[time]")
int time,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -179,7 +179,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<distance>")
int distance,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -203,7 +203,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<time>")
int time,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -8,16 +8,13 @@ import java.util.Locale;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.event.EventPriority;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.queue.ConfirmMode;
import org.mvplugins.multiverse.core.config.handle.CommentedConfigurationHandle;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
@@ -28,7 +25,6 @@ import org.mvplugins.multiverse.core.config.migration.action.InvertBoolMigratorA
import org.mvplugins.multiverse.core.config.migration.action.MoveMigratorAction;
import org.mvplugins.multiverse.core.config.migration.VersionMigrator;
import org.mvplugins.multiverse.core.config.migration.action.SetMigratorAction;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.teleportation.PassengerMode;
import org.mvplugins.multiverse.core.teleportation.PassengerModes;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat;
@@ -190,6 +186,16 @@ public final class CoreConfig {
return configHandle.get(configNodes.enforceFlight);
}
@ApiStatus.AvailableSince("5.5")
public Try<Void> setGamemodeAndFlightEnforceDelay(int delayTicks) {
return configHandle.set(configNodes.gamemodeAndFlightEnforceDelay, delayTicks);
}
@ApiStatus.AvailableSince("5.5")
public int getGamemodeAndFlightEnforceDelay() {
return configHandle.get(configNodes.gamemodeAndFlightEnforceDelay);
}
@ApiStatus.AvailableSince("5.3")
public Try<Void> setApplyEntitySpawnRate(boolean applyEntitySpawnRate) {
return configHandle.set(configNodes.applyEntitySpawnRate, applyEntitySpawnRate);
@@ -497,6 +503,26 @@ public final class CoreConfig {
return configHandle.get(configNodes.registerPapiHook);
}
@ApiStatus.AvailableSince("5.5")
public Try<Void> setWarnInvalidPapiFormat(boolean warnInvalidPapiFormat) {
return configHandle.set(configNodes.warnInvalidPapiFormat, warnInvalidPapiFormat);
}
@ApiStatus.AvailableSince("5.5")
public boolean getWarnInvalidPapiFormat() {
return configHandle.get(configNodes.warnInvalidPapiFormat);
}
@ApiStatus.AvailableSince("5.5")
public Try<Void> setInvalidPapiFormatReturnsBlank(boolean invalidPapiFormatReturnsBlank) {
return configHandle.set(configNodes.invalidPapiFormatReturnsBlank, invalidPapiFormatReturnsBlank);
}
@ApiStatus.AvailableSince("5.5")
public boolean getInvalidPapiFormatReturnsBlank() {
return configHandle.get(configNodes.invalidPapiFormatReturnsBlank);
}
/**
* {@inheritDoc}
*/
@@ -539,6 +565,16 @@ public final class CoreConfig {
return configHandle.get(configNodes.resolveAliasName);
}
@ApiStatus.AvailableSince("5.4")
public Try<Void> setSimplifiedDestinationTabCompletion(boolean simplifiedDestinationTabCompletion) {
return configHandle.set(configNodes.simplifiedDestinationTabCompletion, simplifiedDestinationTabCompletion);
}
@ApiStatus.AvailableSince("5.4")
public boolean getSimplifiedDestinationTabCompletion() {
return configHandle.get(configNodes.simplifiedDestinationTabCompletion);
}
/**
* {@inheritDoc}
*/
@@ -615,6 +651,16 @@ public final class CoreConfig {
return configHandle.get(configNodes.eventPriorityPlayerTeleport);
}
@ApiStatus.AvailableSince("5.5")
public Try<Void> setEventPriorityPlayerWorldChange(EventPriority eventPriorityPlayerWorldChange) {
return configHandle.set(configNodes.eventPriorityPlayerWorldChange, eventPriorityPlayerWorldChange);
}
@ApiStatus.AvailableSince("5.5")
public EventPriority getEventPriorityPlayerWorldChange() {
return configHandle.get(configNodes.eventPriorityPlayerWorldChange);
}
public Try<Void> setBukkitYmlPath(String bukkitYmlPath) {
return configHandle.set(configNodes.bukkitYmlPath, bukkitYmlPath);
}
@@ -4,6 +4,7 @@ import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.event.EventPriority;
import org.bukkit.plugin.PluginManager;
@@ -134,13 +135,27 @@ final class CoreConfigNodes {
.name("enforce-flight")
.build());
final ConfigNode<Integer> gamemodeAndFlightEnforceDelay = node(ConfigNode.builder("world.gamemode-and-flight-enforce-delay", Integer.class)
.comment("")
.comment("Sets the delay in ticks before Multiverse enforces gamemode and flight ability on world change.")
.comment("Increase this value if you are experiencing issues with other plugins overriding gamemode or flight ability.")
.comment("Or set to 0 to enforce immediately during world change event.")
.defaultValue(1)
.name("gamemode-and-flight-enforce-delay")
.suggester((sender -> List.of("0", "1", "2", "5", "10")))
.build());
final ConfigNode<Boolean> applyEntitySpawnRate = node(ConfigNode.builder("world.apply-entity-spawn-rate", Boolean.class)
.comment("")
.comment("Sets whether Multiverse will apply the world's entity `tick-rate` config in worlds.yml.")
.comment("If disabled, the `tick-rate` config in worlds.yml will be ignored.")
.comment("Disable this if you want paper-world.yml or another plugin to handle entity spawn rate per world.")
.comment("!!!NOTE: This will only apply after a server restart!")
.defaultValue(true)
.name("apply-entity-spawn-rate")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'apply-entity-spawn-rate' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<Boolean> applyEntitySpawnLimit = node(ConfigNode.builder("world.apply-entity-spawn-limit", Boolean.class)
@@ -148,8 +163,12 @@ final class CoreConfigNodes {
.comment("Sets whether Multiverse will apply the world's entity `spawn-limit` config when a world is loaded.")
.comment("If disabled, the `spawn-limit` config in worlds.yml will be ignored.")
.comment("Disable this if you want paper-world.yml or another plugin to handle entity limits per world.")
.comment("!!!NOTE: This will only apply after a server restart!")
.defaultValue(true)
.name("apply-entity-spawn-limit")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'apply-entity-spawn-limit' config option will only take" +
" full effect after a server restart.")))
.build());
final ConfigNode<Boolean> autoPurgeEntities = node(ConfigNode.builder("world.auto-purge-entities", Boolean.class)
@@ -201,7 +220,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.")
@@ -381,12 +403,29 @@ final class CoreConfigNodes {
.name("register-papi-hook")
.build());
final ConfigNode<Boolean> warnInvalidPapiFormat = node(ConfigNode.builder("messaging.warn-invalid-papi-format", Boolean.class)
.comment("")
.comment("This config option defines whether or not Multiverse should warn about invalid PlaceholderAPI formats.")
.comment("Only applies if PlaceholderAPI is installed, and only to multiverse placeholders.")
.defaultValue(true)
.name("warn-invalid-papi-format")
.build());
final ConfigNode<Boolean> invalidPapiFormatReturnsBlank = node(ConfigNode.builder("messaging.invalid-papi-format-returns-blank", Boolean.class)
.comment("")
.comment("By default, invalid formats will return the original placeholder string.")
.comment("This config option defines whether or not Multiverse should show invalid PlaceholderAPI formats as blank.")
.comment("Only applies if PlaceholderAPI is installed, and only to multiverse placeholders.")
.defaultValue(false)
.name("invalid-papi-format-returns-blank")
.build());
final ConfigNode<Locale> defaultLocale = node(ConfigNode.builder("messaging.default-locale", Locale.class)
.comment("")
.comment("This config option defines the default language Multiverse should use.")
.defaultValue(Locale.ENGLISH)
.name("default-locale")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
commandManager.get().getLocales().setDefaultLocale(newValue))
.build());
@@ -396,9 +435,8 @@ final class CoreConfigNodes {
.comment("If the player's language does not have a translation, it will use the default language set above instead.")
.defaultValue(true)
.name("per-player-locale")
.onSetValue((oldValue, newValue) -> {
commandManager.get().usePerIssuerLocale(newValue);
})
.onLoadAndChange((oldValue, newValue) ->
commandManager.get().usePerIssuerLocale(newValue))
.build());
private final ConfigHeaderNode commandHeader = node(ConfigHeaderNode.builder("command")
@@ -414,6 +452,15 @@ final class CoreConfigNodes {
.name("resolve-alias-name")
.build());
final ConfigNode<Boolean> simplifiedDestinationTabCompletion = node(ConfigNode.builder("command.simplified-destination-tab-completion", Boolean.class)
.comment("")
.comment("If this is set to true, Multiverse will only suggest simple destination formats in tab completion.")
.comment("This means only world names will be suggested without other destination type such as `e:worldname:x,y,z` or `p:playername`.")
.comment("Note: This DOES NOT prevent players from using the destinations, as that is controlled by permissions.")
.defaultValue(false)
.name("simplified-destination-tab-completion")
.build());
final ConfigNode<ConfirmMode> confirmMode = node(ConfigNode.builder("command.confirm-mode", ConfirmMode.class)
.comment("")
.comment("This config option defines whether `/mv confirm` is needed before running a DANGEROUS action.")
@@ -468,8 +515,11 @@ final class CoreConfigNodes {
.comment("")
.comment("This config option defines the priority for the PlayerPortalEvent.")
.name("event-priority-player-portal")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-portal", newValue))
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-portal' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerRespawn = node(ConfigNode.builder("event-priority.player-respawn", EventPriority.class)
@@ -477,16 +527,23 @@ final class CoreConfigNodes {
.name("event-priority-player-respawn")
.comment("")
.comment("This config option defines the priority for the PlayerRespawnEvent.")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-respawn", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-respawn' config option will only take " +
"full effect after a server restart."))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerSpawnLocation = node(ConfigNode.builder("event-priority.player-spawn-location", EventPriority.class)
.defaultValue(EventPriority.NORMAL)
.comment("")
.comment("This config option defines the priority for the PlayerSpawnLocationEvent.")
.name("event-priority-player-spawn-location").onSetValue((oldValue, newValue) ->
.name("event-priority-player-spawn-location")
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-spawn-location", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-spawn-location' config option will only take " +
"full effect after a server restart."))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerTeleport = node(ConfigNode.builder("event-priority.player-teleport", EventPriority.class)
@@ -494,8 +551,23 @@ final class CoreConfigNodes {
.name("event-priority-player-teleport")
.comment("")
.comment("This config option defines the priority for the PlayerTeleportEvent.")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-teleport", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-teleport' config option will only take " +
"full effect after a server restart."))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerWorldChange = node(ConfigNode.builder("event-priority.player-changed-world", EventPriority.class)
.defaultValue(EventPriority.NORMAL)
.name("event-priority-player-changed-world")
.comment("")
.comment("This config option defines the priority for the PlayerChangedWorldEvent.")
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-changed-world", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-changed-world' config option will only take " +
"full effect after a server restart."))
.build());
private final ConfigHeaderNode miscHeader = node(ConfigHeaderNode.builder("misc")
@@ -508,6 +580,9 @@ final class CoreConfigNodes {
.comment("Note: this config option needs a server restart to take effect.")
.defaultValue("bukkit.yml")
.name("bukkit-yml-path")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'bukkit-yml-path' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<String> serverPropertiesPath = node(ConfigNode.builder("misc.server-properties-path", String.class)
@@ -516,6 +591,9 @@ final class CoreConfigNodes {
.comment("Note: this config option needs a server restart to take effect.")
.defaultValue("server.properties")
.name("server-properties-path")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'server-properties-path' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<Boolean> autoDetectGeneratorPlugins = node(ConfigNode.builder("misc.auto-detect-generator-plugins", Boolean.class)
@@ -542,7 +620,7 @@ final class CoreConfigNodes {
.validator(value -> (value < 0 || value > 3)
? Try.failure(new MultiverseException("Debug level must be between 0 and 3."))
: Try.success(null))
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (newValue != Logging.getDebugLevel()) {
Logging.setDebugLevel(newValue);
pluginManager.callEvent(new MVDebugModeEvent(newValue));
@@ -556,7 +634,7 @@ final class CoreConfigNodes {
.comment("This will only work if the above 'global-debug' is set to 1 or more.")
.defaultValue(false)
.name("debug-permissions")
.onSetValue((oldValue, newValue) -> PermissionUtils.setDebugPermissions(newValue))
.onLoadAndChange((oldValue, newValue) -> PermissionUtils.setDebugPermissions(newValue))
.build());
final ConfigNode<Boolean> silentStart = node(ConfigNode.builder("misc.silent-start", Boolean.class)
@@ -564,7 +642,7 @@ final class CoreConfigNodes {
.comment("If true, the startup console messages will no longer show.")
.defaultValue(false)
.name("silent-start")
.onSetValue((oldValue, newValue) -> Logging.setShowingConfig(!newValue))
.onLoadAndChange((oldValue, newValue) -> Logging.setShowingConfig(!newValue))
.build());
final ConfigNode<Boolean> showDonationMessage = node(ConfigNode.builder("misc.show-donation-message", Boolean.class)
@@ -3,19 +3,22 @@ package org.mvplugins.multiverse.core.config.handle;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Logger;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.migration.ConfigMigrator;
import org.mvplugins.multiverse.core.config.node.ListValueNode;
import org.mvplugins.multiverse.core.config.node.Node;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.config.node.ValueNode;
@@ -84,7 +87,8 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
});
nodeValueMap.forEach((valueNode, value) -> {
valueNode.onSetValue(value, value);
valueNode.onLoad(value);
valueNode.onLoadAndChange(Bukkit.getConsoleSender(), null, value);
});
}
@@ -156,10 +160,27 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
* @return Empty try if the value was set, try containing an error otherwise.
*/
public <T> Try<Void> set(@NotNull ValueNode<T> node, T value) {
return set(Bukkit.getConsoleSender(), node, value);
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param sender The sender who triggered the change.
* @param node The node to set the value of.
* @param value The value to set.
* @param <T> The type of the node value.
* @return Empty try if the value was set, try containing an error otherwise.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public <T> Try<Void> set(@NotNull CommandSender sender, @NotNull ValueNode<T> node, T value) {
return node.validate(value).map(ignore -> {
T oldValue = get(node);
nodeValueMap.put(node, value);
node.onSetValue(oldValue, get(node));
node.onLoadAndChange(sender, oldValue, value);
node.onChange(sender, oldValue, value);
return null;
});
}
@@ -226,7 +226,7 @@ public class StringPropertyHandle {
public Try<Void> setPropertyString(@NotNull CommandSender sender, @Nullable String name, @Nullable String value) {
return findNode(name, ValueNode.class)
.flatMap(node -> node.parseFromString(sender, value)
.flatMap(parsedValue -> handle.set(node, parsedValue)));
.flatMap(parsedValue -> handle.set(sender, node, parsedValue)));
}
/**
@@ -14,6 +14,9 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.node.functions.NodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeValueCallback;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeStringParser;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeSuggester;
import org.mvplugins.multiverse.core.config.node.serializer.DefaultSerializerProvider;
@@ -53,7 +56,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected @Nullable NodeStringParser<T> stringParser;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
protected @Nullable NodeValueCallback<T> onLoad;
protected @Nullable NodeChangeCallback<T> onLoadAndChange;
protected @Nullable NodeChangeCallback<T> onChange;
protected ConfigNode(
@NotNull String path,
@@ -66,7 +71,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@Nullable NodeStringParser<T> stringParser,
@Nullable NodeSerializer<T> serializer,
@Nullable Function<T, Try<Void>> validator,
@Nullable BiConsumer<T, T> onSetValue) {
@Nullable NodeValueCallback<T> onLoad,
@Nullable NodeChangeCallback<T> onLoadAndChange,
@Nullable NodeChangeCallback<T> onChange) {
super(path, comments);
this.name = name;
this.type = type;
@@ -82,7 +89,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
? serializer
: DefaultSerializerProvider.getDefaultSerializer(type);
this.validator = validator;
this.onSetValue = onSetValue;
this.onLoad = onLoad;
this.onLoadAndChange = onLoadAndChange;
this.onChange = onChange;
}
/**
@@ -186,9 +195,29 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
* {@inheritDoc}
*/
@Override
public void onSetValue(@Nullable T oldValue, @Nullable T newValue) {
if (onSetValue != null) {
onSetValue.accept(oldValue, newValue);
public void onLoad(@Nullable T value) {
if (onLoad != null) {
onLoad.run(value);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLoadAndChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue) {
if (onLoadAndChange != null) {
onLoadAndChange.run(sender, oldValue, newValue);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue) {
if (onChange != null) {
onChange.run(sender, oldValue, newValue);
}
}
@@ -208,7 +237,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected @Nullable NodeStringParser<T> stringParser;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
protected @Nullable NodeValueCallback<T> onLoad;
protected @Nullable NodeChangeCallback<T> onLoadAndChange;
protected @Nullable NodeChangeCallback<T> onChange;
/**
* Creates a new builder.
@@ -368,15 +399,83 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return self();
}
/**
* Sets the action to be performed when the value is loaded.
*
* @param onLoad The action to be performed.
* @return This builder.
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onLoad(@NotNull NodeValueCallback<T> onLoad) {
this.onLoad = this.onLoad == null ? onLoad : this.onLoad.then(onLoad);
return self();
}
/**
* Sets the action to be performed when the value is loaded and changed.
*
* @param onLoadAndChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onLoadAndChange(@NotNull NodeChangeCallback<T> onLoadAndChange) {
this.onLoadAndChange = this.onLoadAndChange == null ? onLoadAndChange : this.onLoadAndChange.then(onLoadAndChange);
return self();
}
/**
* Sets the action to be performed when the value is loaded and changed.
*
* @param onLoadAndChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onLoadAndChange(@NotNull SenderNodeChangeCallback<T> onLoadAndChange) {
return onLoadAndChange((NodeChangeCallback<T>) onLoadAndChange);
}
/**
* Sets the action to be performed when the value is changed.
*
* @param onChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onChange(@NotNull NodeChangeCallback<T> onChange) {
this.onChange = this.onChange == null ? onChange : this.onChange.then(onChange);
return self();
}
/**
* Sets the action to be performed when the value is changed.
*
* @param onChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onChange(@NotNull SenderNodeChangeCallback<T> onChange) {
return onChange((NodeChangeCallback<T>) onChange);
}
/**
* Sets the action to be performed when the value is set.
*
* @param onSetValue The action to be performed.
* @return This builder.
*
* @deprecated Use {@link #onLoadAndChange(NodeChangeCallback)} instead.
*/
@Deprecated(since = "5.4", forRemoval = true)
public @NotNull B onSetValue(@NotNull BiConsumer<T, T> onSetValue) {
this.onSetValue = this.onSetValue == null ? onSetValue : this.onSetValue.andThen(onSetValue);
return self();
return onLoadAndChange(onSetValue::accept);
}
/**
@@ -385,7 +484,8 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@Override
public @NotNull ConfigNode<T> build() {
return new ConfigNode<>(path, comments.toArray(new String[0]),
name, type, aliases, defaultValue, suggester, stringParser, serializer, validator, onSetValue);
name, type, aliases, defaultValue, suggester, stringParser, serializer, validator,
onLoad, onLoadAndChange, onChange);
}
}
}
@@ -19,6 +19,8 @@ import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.node.functions.DefaultStringParserProvider;
import org.mvplugins.multiverse.core.config.node.functions.DefaultSuggesterProvider;
import org.mvplugins.multiverse.core.config.node.functions.NodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeValueCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeStringParser;
import org.mvplugins.multiverse.core.config.node.functions.NodeSuggester;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeStringParser;
@@ -67,7 +69,9 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
@Nullable NodeStringParser<List<I>> stringParser,
@Nullable NodeSerializer<List<I>> serializer,
@Nullable Function<List<I>, Try<Void>> validator,
@Nullable BiConsumer<List<I>, List<I>> onSetValue,
@Nullable NodeValueCallback<List<I>> onLoad,
@Nullable NodeChangeCallback<List<I>> onLoadAndChange,
@Nullable NodeChangeCallback<List<I>> onChange,
@NotNull Class<I> itemType,
@Nullable NodeSuggester itemSuggester,
@Nullable NodeStringParser<I> itemStringParser,
@@ -75,7 +79,7 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
@Nullable Function<I, Try<Void>> itemValidator,
@Nullable BiConsumer<I, I> onSetItemValue) {
super(path, comments, name, type, aliases, defaultValueSupplier, suggester, stringParser, serializer,
validator, onSetValue);
validator, onLoad, onLoadAndChange, onChange);
this.itemType = itemType;
this.itemSuggester = itemSuggester != null
? itemSuggester
@@ -105,8 +109,8 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
if (this.itemSerializer != null && this.serializer == null) {
setDefaultSerialiser();
}
if (this.onSetItemValue != null && this.onSetValue == null) {
setDefaultOnSetValue();
if (this.onSetItemValue != null && this.onLoadAndChange == null) {
setDefaultOnLoadAndChange();
}
if (this.defaultValue == null) {
this.defaultValue = ArrayList::new;
@@ -116,9 +120,9 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
private void setDefaultSuggester() {
if (itemSuggester instanceof SenderNodeSuggester senderItemSuggester) {
this.suggester = (SenderNodeSuggester)(sender, input) ->
StringFormatter.addonToCommaSeperated(input, senderItemSuggester.suggest(sender, input));
StringFormatter.addOnToCommaSeparated(input, senderItemSuggester.suggest(sender, input));
} else {
this.suggester = input -> StringFormatter.addonToCommaSeperated(input, itemSuggester.suggest(input));
this.suggester = input -> StringFormatter.addOnToCommaSeparated(input, itemSuggester.suggest(input));
}
}
@@ -168,8 +172,8 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
};
}
private void setDefaultOnSetValue() {
this.onSetValue = (oldValue, newValue) -> {
private void setDefaultOnLoadAndChange() {
this.onLoadAndChange = (oldValue, newValue) -> {
if (oldValue != null) {
oldValue.stream()
.filter(value -> !newValue.contains(value))
@@ -381,7 +385,9 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
stringParser,
serializer,
validator,
onSetValue,
onLoad,
onLoadAndChange,
onChange,
itemType,
itemSuggester,
itemStringParser,
@@ -5,11 +5,13 @@ import java.util.Collection;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.apache.logging.log4j.util.Strings;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.handle.BaseConfigurationHandle;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
public interface ValueNode<T> extends Node {
@@ -108,11 +110,51 @@ public interface ValueNode<T> extends Node {
*/
Try<Void> validate(@Nullable T value);
/**
* Called when the value of this node is loaded by {@link BaseConfigurationHandle#load()}.
*
* @param value The loaded value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void onLoad(@Nullable T value);
/**
* Called when the value of this node is loaded or changed.
*
* @param sender The sender who triggered the change. If triggered by loading or no target sender is specified,
* it will be the console sender.
* @param oldValue The old value, will always be null on load.
* @param newValue The new value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void onLoadAndChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue);
/**
* Called when the value of this node is changed by {@link BaseConfigurationHandle#set(ValueNode, Object)}.
*
* @param sender The sender who changed the value, or console sender if no target sender specified.
* @param oldValue The old value.
* @param newValue The new value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void onChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue);
/**
* Called when the value of this node is set.
*
* @param oldValue The old value.
* @param newValue The new value.
*
* @deprecated Use {@link #onLoadAndChange(CommandSender, Object, Object)} instead.
*/
void onSetValue(@Nullable T oldValue, @Nullable T newValue);
@Deprecated(since = "5.4", forRemoval = true)
default void onSetValue(@Nullable T oldValue, @Nullable T newValue) {
onLoadAndChange(Bukkit.getConsoleSender(), oldValue, newValue);
}
}
@@ -0,0 +1,65 @@
package org.mvplugins.multiverse.core.config.node.functions;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
/**
* A callback interface for handling changes to configuration node values.
*
* @param <T> The type of the node value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@FunctionalInterface
public interface NodeChangeCallback<T> {
/**
* Called when the value of a node changes.
*
* @param oldValue The old value of the node.
* @param newValue The new value of the node.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void run(T oldValue, T newValue);
/**
* Called when the value of a node changes, with the sender of the change.
*
* @param sender The sender of the change.
* @param oldValue The old value of the node.
* @param newValue The new value of the node.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
default void run(CommandSender sender, T oldValue, T newValue) {
run(oldValue, newValue);
}
/**
* Chains another {@link NodeChangeCallback} to be executed after this one.
*
* @param after The callback to execute after this one.
* @return A new {@link NodeChangeCallback} that executes both callbacks in order.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
default NodeChangeCallback<T> then(NodeChangeCallback<T> after) {
return new SenderNodeChangeCallback<T>() {
@Override
public void run(CommandSender sender, T oldValue, T newValue) {
NodeChangeCallback.this.run(sender, oldValue, newValue);
after.run(sender, oldValue, newValue);
}
@Override
public void run(T oldValue, T newValue) {
NodeChangeCallback.this.run(oldValue, newValue);
after.run(oldValue, newValue);
}
};
}
}
@@ -0,0 +1,38 @@
package org.mvplugins.multiverse.core.config.node.functions;
import org.jetbrains.annotations.ApiStatus;
/**
* Handler called when an action is performed on a node such as loading.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@FunctionalInterface
public interface NodeValueCallback<T> {
/**
* Called when an action is performed on a node.
*
* @param value The value of the node.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void run(T value);
/**
* Chains another {@link NodeValueCallback} to be executed after this one.
*
* @param after The callback to execute after this one.
* @return A new {@link NodeValueCallback} that executes both callbacks in order.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
default NodeValueCallback<T> then(NodeValueCallback<T> after) {
return (T value) -> {
this.run(value);
after.run(value);
};
}
}
@@ -0,0 +1,31 @@
package org.mvplugins.multiverse.core.config.node.functions;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
/**
* A callback interface for handling changes to configuration node values with contextual information from the sender.
*
* @param <T> The type of the node value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@FunctionalInterface
public interface SenderNodeChangeCallback<T> extends NodeChangeCallback<T> {
/**
* {@inheritDoc}
*/
@ApiStatus.AvailableSince("5.4")
void run(CommandSender sender, T oldValue, T newValue);
/**
* {@inheritDoc}
*/
@ApiStatus.AvailableSince("5.4")
@Override
default void run(T oldValue, T newValue) {
run(Bukkit.getConsoleSender(), oldValue, newValue);
}
}
@@ -11,8 +11,11 @@ import org.jetbrains.annotations.Nullable;
* A function that parses a string into a node value object of type {@link T} with contextual information from the sender.
*
* @param <T> The type of the object to parse.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
@FunctionalInterface
public interface SenderNodeStringParser<T> extends NodeStringParser<T> {
/**
* Parses a string into a node value object of type {@link T} with contextual information from the sender.
@@ -4,8 +4,9 @@ import io.vavr.control.Option;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
/**
@@ -79,6 +80,20 @@ public abstract class DestinationInstance<I extends DestinationInstance<I, T>,
*/
public abstract @NotNull Option<String> getFinerPermissionSuffix();
/**
* Gets a user-friendly display text for this destination instance. This is used when displaying the destination
* to the player. By default, this returns the same as {@link #toString()}. Override this method to provide a more
* user-friendly display text with colors, formatting and localization support.
*
* @return The display message.
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@NotNull
public Message getDisplayMessage() {
return Message.of(this.toString());
}
/**
* Serialises the destination instance to a savable string.
*
@@ -7,6 +7,7 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
/**
* Destination instance implementation for the {@link AnchorDestination}.
@@ -63,6 +64,15 @@ public final class AnchorDestinationInstance extends DestinationInstance<AnchorD
return Option.of(anchorName);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
//TODO Localize
return Message.of("anchor '" + anchorName + "'");
}
/**
* {@inheritDoc}
*/
@@ -6,7 +6,7 @@ import java.util.stream.Collectors;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.bukkit.Bukkit;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@@ -20,8 +20,7 @@ import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker;
/**
* {@link Destination} implementation for beds.
@@ -30,7 +29,11 @@ import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.re
public final class BedDestination implements Destination<BedDestination, BedDestinationInstance, BedDestination.InstanceFailureReason> {
static final String OWN_BED_STRING = "playerbed";
BedDestination() {
private final ConcurrentPlayerWorldTracker worldTracker;
@Inject
BedDestination(@NotNull ConcurrentPlayerWorldTracker worldTracker) {
this.worldTracker = worldTracker;
}
/**
@@ -62,8 +65,8 @@ public final class BedDestination implements Destination<BedDestination, BedDest
@Override
public @NotNull Collection<DestinationSuggestionPacket> suggestDestinations(
@NotNull CommandSender sender, @Nullable String destinationParams) {
List<DestinationSuggestionPacket> collect = Bukkit.getOnlinePlayers().stream()
.map(player -> new DestinationSuggestionPacket(this, player.getName(), player.getName()))
List<DestinationSuggestionPacket> collect = worldTracker.getOnlinePlayers().stream()
.map(player -> new DestinationSuggestionPacket(this, player, player))
.collect(Collectors.toList());
if (sender instanceof Player) {
collect.add(new DestinationSuggestionPacket(this, OWN_BED_STRING, OWN_BED_STRING));
@@ -9,12 +9,13 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
/**
* Destination instance implementation for the {@link BedDestination}.
*/
public final class BedDestinationInstance extends DestinationInstance<BedDestinationInstance, BedDestination> {
private final Player player;
private final @Nullable Player player;
/**
* Constructor.
@@ -64,6 +65,15 @@ public final class BedDestinationInstance extends DestinationInstance<BedDestina
return Option.of(player != null ? player.getName() : BedDestination.OWN_BED_STRING);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
//TODO Localize
return Message.of(player == null ? "your bed/respawn point" : player.getName() + "'s bed/respawn point");
}
/**
* {@inheritDoc}
*/
@@ -23,14 +23,11 @@ import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseExce
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.utils.position.PositionNumber;
import org.mvplugins.multiverse.core.utils.position.VectorPosition;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.entrycheck.WorldEntryCheckerProvider;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.*;
@@ -68,6 +65,7 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
public @NotNull ExactDestinationInstance fromLocation(@NotNull Location location) {
return new ExactDestinationInstance(
this,
worldManager,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
);
@@ -88,6 +86,7 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
.map(location -> Attempt.<ExactDestinationInstance, InstanceFailureReason>success(
new ExactDestinationInstance(
this,
worldManager,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
)
@@ -112,7 +111,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, worldName, position));
return Attempt.success(new ExactDestinationInstance(this, worldManager, worldName, position));
}
//TODO: Extract to a world finder class
@@ -9,14 +9,16 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.utils.position.VectorPosition;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
/**
* Destination instance implementation for the {@link ExactDestination}.
*/
public final class ExactDestinationInstance extends DestinationInstance<ExactDestinationInstance, ExactDestination> {
private final WorldManager worldManager;
private final String worldName;
private final EntityPosition position;
@@ -28,9 +30,11 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
* @param position The position in the world.
*/
ExactDestinationInstance(@NotNull ExactDestination destination,
@NotNull WorldManager worldManager,
@NotNull String worldName,
@NotNull EntityPosition position) {
super(destination);
this.worldManager = worldManager;
this.worldName = worldName;
this.position = position;
}
@@ -73,6 +77,17 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
return Option.of(worldName);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
String displayWorldName = worldManager.getWorld(worldName)
.map(MultiverseWorld::getAliasOrName)
.getOrElse(worldName);
return Message.of(displayWorldName + " at " + position.toString());
}
/**
* {@inheritDoc}
*/
@@ -4,7 +4,7 @@ import java.util.Collection;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.bukkit.Bukkit;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@@ -14,21 +14,26 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker;
/**
* {@link Destination} implementation for players.s
*/
@Service
public final class PlayerDestination implements Destination<PlayerDestination, PlayerDestinationInstance, PlayerDestination.InstanceFailureReason> {
private final ConcurrentPlayerWorldTracker playerWorldTracker;
/**
* Creates a new instance of the PlayerDestination.
*/
PlayerDestination() {
@Inject
PlayerDestination(@NotNull ConcurrentPlayerWorldTracker playerWorldTracker) {
this.playerWorldTracker = playerWorldTracker;
}
/**
@@ -60,8 +65,8 @@ public final class PlayerDestination implements Destination<PlayerDestination, P
@Override
public @NotNull Collection<DestinationSuggestionPacket> suggestDestinations(
@NotNull CommandSender sender, @Nullable String destinationParams) {
return Bukkit.getOnlinePlayers().stream()
.map(p -> new DestinationSuggestionPacket(this, p.getName(), p.getName()))
return playerWorldTracker.getOnlinePlayers().stream()
.map(player -> new DestinationSuggestionPacket(this, player, player))
.toList();
}
@@ -9,6 +9,7 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
import java.util.UUID;
@@ -66,6 +67,14 @@ public final class PlayerDestinationInstance extends DestinationInstance<PlayerD
return Option.of(playerName);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
return Message.of(playerName);
}
/**
* {@inheritDoc}
*/
@@ -8,12 +8,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
/**
* Destination instance implementation for the {@link WorldDestination}.
*/
@@ -87,6 +84,14 @@ public final class WorldDestinationInstance extends DestinationInstance<WorldDes
return Option.of(world.getName());
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
return Message.of(world.getAliasOrName());
}
/**
* {@inheritDoc}
*/
@@ -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;
}
}
@@ -16,6 +16,7 @@ import org.mvplugins.multiverse.core.dynamiclistener.annotations.IgnoreIfCancell
import org.mvplugins.multiverse.core.dynamiclistener.annotations.SkipIfEventExist;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker;
/**
* Multiverse's Listener for players.
@@ -24,16 +25,17 @@ import org.mvplugins.multiverse.core.world.WorldManager;
final class MVChatListener implements CoreListener {
private final CoreConfig config;
private final WorldManager worldManager;
private final MVPlayerListener playerListener;
private final ConcurrentPlayerWorldTracker playerWorldTracker;
@Inject
MVChatListener(
CoreConfig config,
WorldManager worldManager,
MVPlayerListener playerListener) {
ConcurrentPlayerWorldTracker playerWorldTracker
) {
this.config = config;
this.worldManager = worldManager;
this.playerListener = playerListener;
this.playerWorldTracker = playerWorldTracker;
}
@EventClass("io.papermc.paper.event.player.AsyncChatEvent")
@@ -94,12 +96,9 @@ final class MVChatListener implements CoreListener {
}
private String getWorldName(Player player) {
String world = playerListener.getPlayerWorld().get(player.getName());
if (world == null) {
world = player.getWorld().getName();
playerListener.getPlayerWorld().put(player.getName(), world);
}
return this.worldManager.getLoadedWorld(world)
String worldName = playerWorldTracker.getPlayerWorld(player.getName())
.getOrElse(() -> player.getWorld().getName());
return this.worldManager.getLoadedWorld(worldName)
.map(mvworld -> mvworld.isHidden() ? "" : mvworld.getAliasOrName())
.getOrElse("");
}
@@ -7,9 +7,7 @@
package org.mvplugins.multiverse.core.listeners;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
@@ -38,6 +36,7 @@ import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventPriorityKey;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.IgnoreIfCancelled;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.event.MVRespawnEvent;
import org.mvplugins.multiverse.core.locale.PluginLocales;
@@ -74,8 +73,6 @@ final class MVPlayerListener implements CoreListener {
private final CorePermissionsChecker corePermissionsChecker;
private final AsyncSafetyTeleporter asyncSafetyTeleporter;
private final Map<String, String> playerWorld = new ConcurrentHashMap<>();
@Inject
MVPlayerListener(
MultiverseCore plugin,
@@ -120,15 +117,6 @@ final class MVPlayerListener implements CoreListener {
return getCommandManager().getLocales();
}
/**
* Gets the map of player and the world name they are in.
*
* @return the playerWorld-map
*/
Map<String, String> getPlayerWorld() {
return playerWorld;
}
/**
* This method is called when a player respawns.
*
@@ -256,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",
@@ -268,11 +256,10 @@ final class MVPlayerListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
@DefaultEventPriority(EventPriority.MONITOR)
@EventPriorityKey("mvcore-player-changed-world")
@DefaultEventPriority(EventPriority.NORMAL)
void playerChangedWorld(PlayerChangedWorldEvent event) {
// Permissions now determine whether or not to handle a gamemode.
this.handleGameModeAndFlight(event.getPlayer(), event.getPlayer().getWorld());
playerWorld.put(event.getPlayer().getName(), event.getPlayer().getWorld().getName());
}
/**
@@ -283,12 +270,28 @@ final class MVPlayerListener implements CoreListener {
@EventPriorityKey("mvcore-player-teleport")
@DefaultEventPriority(EventPriority.HIGHEST)
void playerTeleport(PlayerTeleportEvent event) {
Logging.finer("Got teleport event for player '"
+ event.getPlayer().getName() + "' with cause '" + event.getCause() + "'");
Player teleportee = event.getPlayer();
if (event.isCancelled()) {
Logging.finer("Teleport event for player '" + teleportee.getName() + "' was already cancelled.");
return;
}
Player teleportee = event.getPlayer();
Logging.finer("Got teleport event for player '"
+ teleportee.getName() + "' with cause '" + event.getCause() + "'");
Location fromLocation = event.getFrom();
if (fromLocation == null || fromLocation.getWorld() == null) { // should never be null, but just in case
Logging.finer("Teleport event for player '" + teleportee.getName() + "' has null from-location or world.");
return;
}
Location toLocation = event.getTo();
if (toLocation == null || toLocation.getWorld() == null) { // may be null on spigot
Logging.finer("Teleport event for player '" + teleportee.getName() + "' has null to-location or world.");
return;
}
Option<String> teleporterName = teleportQueue.popFromQueue(teleportee.getName());
CommandSender teleporter = teleporterName.map(name -> {
if (name.equalsIgnoreCase("CONSOLE")) {
@@ -309,17 +312,17 @@ final class MVPlayerListener implements CoreListener {
}
Logging.finer("Teleporter %s is teleporting %s from %s to %s", teleporter.getName(), teleportee.getName(),
event.getFrom(), event.getTo());
fromLocation, toLocation);
MultiverseWorld fromWorld = getWorldManager().getLoadedWorld(event.getFrom().getWorld()).getOrNull();
LoadedMultiverseWorld toWorld = getWorldManager().getLoadedWorld(event.getTo().getWorld()).getOrNull();
MultiverseWorld fromWorld = getWorldManager().getLoadedWorld(fromLocation.getWorld()).getOrNull();
LoadedMultiverseWorld toWorld = getWorldManager().getLoadedWorld(toLocation.getWorld()).getOrNull();
if (toWorld == null) {
Logging.fine("Player '" + teleportee.getName() + "' is teleporting to world '"
+ event.getTo().getWorld().getName() + "' which is not managed by Multiverse-Core. No further "
+ toLocation.getWorld().getName() + "' which is not managed by Multiverse-Core. No further "
+ "actions will be taken by Multiverse-Core.");
return;
}
if (event.getFrom().getWorld().equals(event.getTo().getWorld())) {
if (fromLocation.getWorld().equals(toLocation.getWorld())) {
// The player is Teleporting to the same world.
Logging.finer("Player '" + teleportee.getName() + "' is teleporting to the same world.");
this.stateSuccess(teleportee.getName(), toWorld.getName());
@@ -353,20 +356,19 @@ final class MVPlayerListener implements CoreListener {
*/
@EventMethod
@DefaultEventPriority(EventPriority.LOWEST)
@IgnoreIfCancelled
void playerPortalCheck(PlayerPortalEvent event) {
if (event.isCancelled()) {
return;
}
if (event.getFrom().getWorld() == null) {
Location fromLocation = event.getFrom();
if (fromLocation == null || fromLocation.getWorld() == null) {
Logging.warning("PlayerPortalEvent's from world is null!");
return;
}
if (event.getFrom().getWorld().getBlockAt(event.getFrom()).getType() == Material.NETHER_PORTAL) {
if (fromLocation.getWorld().getBlockAt(fromLocation).getType() == Material.NETHER_PORTAL) {
return;
}
// Player was actually outside of the portal, adjust the from location
Location newLocation = blockSafety.findPortalBlockNextTo(event.getFrom());
Location newLocation = blockSafety.findPortalBlockNextTo(fromLocation);
if (newLocation != null) {
event.setFrom(newLocation);
}
@@ -379,28 +381,33 @@ final class MVPlayerListener implements CoreListener {
@EventMethod
@EventPriorityKey("mvcore-player-portal")
@DefaultEventPriority(EventPriority.HIGH)
@IgnoreIfCancelled
void playerPortal(PlayerPortalEvent event) {
if (event.isCancelled()) {
Location fromLocation = event.getFrom();
if (fromLocation == null || fromLocation.getWorld() == null) { // should never be null, but just in case
Logging.finer("PlayerPortalEvent's from world is null!");
return;
}
if (event.getTo() == null || event.getTo().getWorld() == null) {
Location toLocation = event.getTo();
if (toLocation == null || toLocation.getWorld() == null) { // may be null on spigot
Logging.finer("PlayerPortalEvent's to world is null!");
return;
}
if (config.isUsingCustomPortalSearch()) {
event.setSearchRadius(config.getCustomPortalSearchRadius());
}
if (Objects.equals(event.getFrom().getWorld(), event.getTo().getWorld())) {
if (Objects.equals(fromLocation.getWorld(), toLocation.getWorld())) {
// The player is Portaling to the same world.
Logging.finer("Player '" + event.getPlayer().getName() + "' is portaling to the same world.");
return;
}
MultiverseWorld fromWorld = getWorldManager().getLoadedWorld(event.getFrom().getWorld()).getOrNull();
LoadedMultiverseWorld toWorld = getWorldManager().getLoadedWorld(event.getTo().getWorld()).getOrNull();
MultiverseWorld fromWorld = getWorldManager().getLoadedWorld(fromLocation.getWorld()).getOrNull();
LoadedMultiverseWorld toWorld = getWorldManager().getLoadedWorld(toLocation.getWorld()).getOrNull();
if (toWorld == null) {
Logging.fine("Player '" + event.getPlayer().getName() + "' is portaling to world '"
+ event.getTo().getWorld().getName() + "' which is not managed by Multiverse-Core. No further "
+ toLocation.getWorld().getName() + "' which is not managed by Multiverse-Core. No further "
+ "actions will be taken by Multiverse-Core.");
return;
}
@@ -414,21 +421,31 @@ final class MVPlayerListener implements CoreListener {
}
/**
* Handles the gamemode for the specified {@link Player}.
* Handles the gamemode for the specified {@link Player}. Delays the enforcement if configured to do so
* to ensure multiverse has final say over other plugins changing gamemodes.
*
* @param player The {@link Player}.
* @param world The {@link World} the player is supposed to be in.
*/
private void handleGameModeAndFlight(final Player player, World world) {
// We perform this task one tick later to MAKE SURE that the player actually reaches the
// destination world, otherwise we'd be changing the player mode if they havent moved anywhere.
this.server.getScheduler().runTaskLater(this.plugin, () -> {
if (!player.isOnline() || !player.getWorld().equals(world)) {
return;
}
Logging.finer("Handling gamemode and flight for player %s in world '%s'", player.getName(), world.getName());
enforcementHandler.handleFlightEnforcement(player);
enforcementHandler.handleGameModeEnforcement(player);
}, 1L);
if (config.getGamemodeAndFlightEnforceDelay() <= 0) {
doGameModeAndFlightEnforcement(player, world);
return;
}
server.getScheduler().runTaskLater(
this.plugin,
() -> doGameModeAndFlightEnforcement(player, world),
config.getGamemodeAndFlightEnforceDelay()
);
}
private void doGameModeAndFlightEnforcement(Player player, World world) {
if (!player.isOnline() || !player.getWorld().equals(world)) {
Logging.finer("Player %s is no longer online or not in the expected world '%s'", player.getName(), world.getName());
return;
}
Logging.finer("Handling gamemode and flight for player %s in world '%s'", player.getName(), world.getName());
enforcementHandler.handleFlightEnforcement(player);
enforcementHandler.handleGameModeEnforcement(player);
}
}
@@ -292,6 +292,7 @@ public enum MVCorei18n implements MessageKeyProvider {
CLONEWORLD_WORLDEXISTFOLDER,
CLONEWORLD_WORLDEXISTUNLOADED,
CLONEWORLD_WORLDEXISTLOADED,
CLONEWORLD_FROMWORLDFOLDERINVALID,
CLONEWORLD_COPYFAILED,
CREATEWORLD_INVALIDWORLDNAME,
@@ -27,39 +27,12 @@ public final class LocalizedMessage extends Message implements MessageKeyProvide
return messageKeyProvider.getMessageKey();
}
@Override
public @NotNull String[] getReplacements(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
return toReplacementsArray(locales, commandIssuer, replacements);
}
@Override
public @NotNull String formatted(@NotNull CommandIssuer commandIssuer) {
return formatted(commandIssuer.getManager().getLocales(), commandIssuer);
}
@Override
public @NotNull String formatted(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
Objects.requireNonNull(locales, "locales must not be null");
String[] parsedReplacements = getReplacements(locales, commandIssuer);
if (parsedReplacements.length == 0) {
return locales.getMessage(commandIssuer, getMessageKey());
}
return ACFUtil.replaceStrings(locales.getMessage(commandIssuer, getMessageKey()), parsedReplacements);
}
private static String[] toReplacementsArray(
@NotNull Locales locales,
@Nullable CommandIssuer commandIssuer,
@NotNull MessageReplacement... replacements) {
String[] replacementsArray = new String[replacements.length * 2];
int i = 0;
for (MessageReplacement replacement : replacements) {
replacementsArray[i++] = replacement.getKey();
replacementsArray[i++] = replacement.getReplacement().fold(
str -> str,
message -> message.formatted(locales, commandIssuer));
}
return replacementsArray;
}
}
@@ -103,7 +103,7 @@ public sealed class Message permits LocalizedMessage {
* @return The replacements
*/
public @NotNull String[] getReplacements(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
return getReplacements();
return toReplacementsArray(locales, commandIssuer, replacements);
}
/**
@@ -152,7 +152,7 @@ public sealed class Message permits LocalizedMessage {
* @return The formatted, localized message
*/
public @NotNull String formatted(@NotNull CommandIssuer commandIssuer) {
return formatted();
return formatted(commandIssuer.getManager().getLocales(), commandIssuer);
}
/**
@@ -166,7 +166,11 @@ public sealed class Message permits LocalizedMessage {
* @return The formatted, localized message
*/
public @NotNull String formatted(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
return formatted();
String[] parsedReplacements = getReplacements(locales, commandIssuer);
if (parsedReplacements.length == 0) {
return raw();
}
return ACFUtil.replaceStrings(message, parsedReplacements);
}
private static String[] toReplacementsArray(@NotNull MessageReplacement... replacements) {
@@ -178,4 +182,19 @@ public sealed class Message permits LocalizedMessage {
}
return replacementsArray;
}
private static String[] toReplacementsArray(
@NotNull Locales locales,
@Nullable CommandIssuer commandIssuer,
@NotNull MessageReplacement... replacements) {
String[] replacementsArray = new String[replacements.length * 2];
int i = 0;
for (MessageReplacement replacement : replacements) {
replacementsArray[i++] = replacement.getKey();
replacementsArray[i++] = replacement.getReplacement().fold(
str -> str,
message -> message.formatted(locales, commandIssuer));
}
return replacementsArray;
}
}
@@ -13,6 +13,7 @@ import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import static org.mvplugins.multiverse.core.permissions.PermissionUtils.concatPermission;
import static org.mvplugins.multiverse.core.permissions.PermissionUtils.registerPermissionWithWildcards;
@Service
public final class CorePermissions {
@@ -60,28 +61,34 @@ public final class CorePermissions {
@PostConstruct
void registerBasePermissions() {
pluginManager.addPermission(new Permission(JOINLOCATION_BYPASS, PermissionDefault.FALSE));
Logging.fine("Successfully registered base permissions");
Try.run(() -> {
registerPermissionWithWildcards(new Permission(JOINLOCATION_BYPASS, PermissionDefault.FALSE));
}).onSuccess(ignore -> {
Logging.fine("Successfully registered base permissions");
}).onFailure(e -> {
Logging.fine("Failed to register base permissions: %s", e.getMessage());
});
}
public Try<Void> addWorldPermissions(@NotNull MultiverseWorld world) {
return Try.run(() -> {
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(WORLD_ACCESS, world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(WORLD_EXEMPT, world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(GAMEMODE_BYPASS, world.getName()), PermissionDefault.FALSE));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(PLAYERLIMIT_BYPASS, world.getName()), PermissionDefault.FALSE));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(SPAWN, world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(SPAWN, "self", world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(SPAWN, "other", world.getName()), PermissionDefault.OP));
Logging.fine("Successfully registered permissions for world %s", world.getName());
});
}).onFailure(e -> Logging.fine("Failed to register permissions for world %s: %s",
world.getName(), e.getMessage()));
}
public Try<Void> removeWorldPermissions(@NotNull MultiverseWorld world) {
@@ -90,20 +97,23 @@ public final class CorePermissions {
pluginManager.removePermission(concatPermission(WORLD_EXEMPT, world.getName()));
pluginManager.removePermission(concatPermission(GAMEMODE_BYPASS, world.getName()));
pluginManager.removePermission(concatPermission(PLAYERLIMIT_BYPASS, world.getName()));
pluginManager.removePermission(concatPermission(SPAWN, world.getName()));
pluginManager.removePermission(concatPermission(SPAWN, "self", world.getName()));
pluginManager.removePermission(concatPermission(SPAWN, "other", world.getName()));
Logging.fine("Successfully removed permissions for world %s", world.getName());
});
}).onFailure(e -> Logging.fine("Failed to remove permissions for world %s: %s",
world.getName(), e.getMessage()));
}
public Try<Void> addDestinationPermissions(@NotNull Destination destination) {
return Try.run(() -> {
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(TELEPORT, "self", destination.getIdentifier()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(TELEPORT, "other", destination.getIdentifier()), PermissionDefault.OP));
Logging.fine("Successfully registered permissions for destination %s", destination.getIdentifier());
});
}).onFailure(e -> Logging.fine("Failed to register permissions for destination %s: %s",
destination.getIdentifier(), e.getMessage()));
}
public Try<Void> removeDestinationPermissions(@NotNull Destination destination) {
@@ -111,6 +121,7 @@ public final class CorePermissions {
pluginManager.removePermission(concatPermission(TELEPORT, "self", destination.getIdentifier()));
pluginManager.removePermission(concatPermission(TELEPORT, "other", destination.getIdentifier()));
Logging.fine("Successfully removed permissions for destination %s", destination.getIdentifier());
});
}).onFailure(e -> Logging.fine("Failed to remove permissions for destination %s: %s",
destination.getIdentifier(), e.getMessage()));
}
}
@@ -1,8 +1,14 @@
package org.mvplugins.multiverse.core.permissions;
import com.dumptruckman.minecraft.util.Logging;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.jetbrains.annotations.ApiStatus;
import java.util.Arrays;
public final class PermissionUtils {
@@ -20,6 +26,37 @@ public final class PermissionUtils {
PermissionUtils.debugPermissions = debugPermissions;
}
/**
* Registers a permission along with all its wildcard parents.
* <br />
* For example, registering "mv.bypass.joinlocation" will also register "mv.*" and "mv.bypass.*" as parents.
*
* @param permission The permission to register.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static void registerPermissionWithWildcards(Permission permission) {
Bukkit.getServer().getPluginManager().addPermission(permission);
String[] split = permission.getName().split("\\.");
StringBuilder prefix = new StringBuilder();
// Skip the last element since it's the actual permission
Arrays.stream(Arrays.copyOfRange(split, 0, split.length - 1)).forEach(s -> {
prefix.append(s).append(".");
Permission perm = getOrAddPermission(prefix + "*");
permission.addParent(perm, true);
});
}
private static Permission getOrAddPermission(String permission) {
Permission perm = Bukkit.getServer().getPluginManager().getPermission(permission);
if (perm == null) {
perm = new Permission(permission, PermissionDefault.FALSE);
Bukkit.getServer().getPluginManager().addPermission(perm);
}
return perm;
}
/**
* Joins permissions with a dot.
*
@@ -132,6 +132,10 @@ public final class BlockSafety {
*/
public boolean canSpawnAtBlockSafely(@NotNull Block block) {
Logging.finest("Checking spawn safety for location: %s, %s, %s", block.getX(), block.getY(), block.getZ());
if (!block.getWorld().getWorldBorder().isInside(block.getLocation())) {
Logging.finest("Location is outside world border.");
return false;
}
if (isUnsafeSpawnBody(block)) {
// Player body will be stuck in solid
Logging.finest("Unsafe location for player's body: " + block);
@@ -0,0 +1,92 @@
package org.mvplugins.multiverse.core.utils;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* A map with case-insensitive String keys. All keys are stored in lower-case form.
*
* @param <T> the type of mapped values
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public class CaseInsensitiveStringMap<T> implements Map<String, T> {
private final Map<String, T> map;
public CaseInsensitiveStringMap() {
map = new HashMap<>();
}
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return map.containsKey(normalizeKey(key));
}
@Override
public boolean containsValue(Object value) {
return map.containsValue(value);
}
@Override
public T get(Object key) {
return map.get(normalizeKey(key));
}
@Override
public @Nullable T put(String key, T value) {
return map.put(normalizeKey(key), value);
}
@Override
public T remove(Object key) {
return map.remove(normalizeKey(key));
}
@Override
public void putAll(@NonNull Map<? extends String, ? extends T> m) {
m.forEach((key, value) -> map.put(normalizeKey(key), value));
}
@Override
public void clear() {
map.clear();
}
@Override
public @NonNull Set<String> keySet() {
return map.keySet();
}
@Override
public @NonNull Collection<T> values() {
return map.values();
}
@Override
public @NonNull Set<Entry<String, T>> entrySet() {
return map.entrySet();
}
private String normalizeKey(Object key) {
return String.valueOf(key).toLowerCase(Locale.ROOT);
}
}
@@ -18,6 +18,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.dumptruckman.minecraft.util.Logging;
@@ -25,6 +26,7 @@ import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NonNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.CoreConfig;
@@ -210,16 +212,19 @@ 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(@NonNull Path dir, @NotNull BasicFileAttributes attrs) throws IOException {
Path newDir = targetDir.resolve(sourceDir.relativize(dir));
if (!Files.isDirectory(newDir)) {
Files.createDirectory(newDir);
@@ -228,10 +233,10 @@ public final class FileUtils {
}
@Override
public @NotNull FileVisitResult visitFile(Path file, @NotNull BasicFileAttributes attrs) throws IOException {
public @NotNull FileVisitResult visitFile(@NonNull 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,16 +1,18 @@
package org.mvplugins.multiverse.core.utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.dumptruckman.minecraft.util.Logging;
import com.google.common.base.Strings;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -76,22 +78,33 @@ public final class PlayerFinder {
public static @NotNull List<Player> getMulti(@Nullable String playerIdentifiers,
@NotNull CommandSender sender
) {
List<Player> playerResults = new ArrayList<>();
return tryGetMulti(playerIdentifiers, sender)
.getOrElse(Collections.emptyList());
}
/**
* Get multiple {@link Player} based on many identifiers of name UUID or selector.
*
* @param playerIdentifiers An identifier of multiple names, UUIDs or selectors, separated by comma.
* @param sender Target sender for selector.
* @return A list of all the {@link Player} found.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static @NotNull Try<@NotNull List<Player>> tryGetMulti(@Nullable String playerIdentifiers,
@NotNull CommandSender sender
) {
if (playerIdentifiers == null || Strings.isNullOrEmpty(playerIdentifiers)) {
return playerResults;
return Try.success(Collections.emptyList());
}
// TODO: Currently just assume entire string is a selector. Add support for comma seperated mixture of names, uuids and selectors
if (isSelector(playerIdentifiers)) {
Logging.finer("Using selector: %s", playerIdentifiers);
List<Player> targetPlayers = getMultiBySelector(playerIdentifiers, sender);
if (targetPlayers != null) {
playerResults.addAll(targetPlayers);
}
return playerResults;
return tryGetMultiBySelector(playerIdentifiers, sender);
}
List<Player> playerResults = new ArrayList<>();
String[] playerIdentifierArray = REPatterns.COMMA.split(playerIdentifiers);
for (String playerIdentifier : playerIdentifierArray) {
Player targetPlayer = getByName(playerIdentifier);
@@ -104,15 +117,25 @@ public final class PlayerFinder {
playerResults.add(targetPlayer);
continue;
}
List<Player> targetPlayers = getMultiBySelector(playerIdentifier, sender);
if (targetPlayers != null) {
playerResults.addAll(targetPlayers);
Try<@NotNull List<Player>> selectorParseResult = tryGetMultiBySelector(playerIdentifier, sender);
if (selectorParseResult.isFailure()) {
return Try.failure(selectorParseResult.getCause());
}
playerResults.addAll(selectorParseResult.getOrElse(Collections.emptyList()));
}
return playerResults;
return Try.success(playerResults);
}
private static boolean isSelector(@NotNull String playerIdentifier) {
/**
* Check if the player identifier is a selector.
*
* @param playerIdentifier An identifier of name, UUID or selector.
* @return True if the identifier is a selector, else false.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static boolean isSelector(@NotNull String playerIdentifier) {
return VANILLA_SELECTORS.stream().anyMatch(playerIdentifier::startsWith);
}
@@ -167,7 +190,7 @@ public final class PlayerFinder {
@NotNull CommandSender sender
) {
List<Player> matchedPlayers = getMultiBySelector(playerSelector, sender);
if (matchedPlayers == null || matchedPlayers.isEmpty()) {
if (matchedPlayers.isEmpty()) {
Logging.fine("No player found with selector '%s' for %s.", playerSelector, sender.getName());
return null;
}
@@ -186,22 +209,36 @@ public final class PlayerFinder {
* @param sender Target sender for selector.
* @return A list of all the {@link Player} found.
*/
public static @Nullable List<Player> getMultiBySelector(@NotNull String playerSelector,
public static @NotNull List<Player> getMultiBySelector(@NotNull String playerSelector,
@NotNull CommandSender sender
) {
return tryGetMultiBySelector(playerSelector, sender)
.onFailure(throwable -> Logging.warning(
"Error selecting entities with selector '%s' for %s: %s",
playerSelector, sender.getName(), throwable.getMessage()
))
.getOrElse(Collections::emptyList);
}
/**
* Get multiple {@link Player} based on <a href="https://minecraft.gamepedia.com/Commands#Target_selectors">vanilla selectors</a>.
*
* @param playerSelector A target selector, usually starts with an '@'.
* @param sender Target sender for selector.
* @return A list of all the {@link Player} found.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static @NotNull Try<@NotNull List<Player>> tryGetMultiBySelector(@NotNull String playerSelector,
@NotNull CommandSender sender
) {
if (playerSelector.charAt(0) != '@') {
return null;
}
try {
return Bukkit.selectEntities(sender, playerSelector).stream()
.filter(e -> e instanceof Player)
.map(e -> ((Player) e))
.collect(Collectors.toList());
} catch (IllegalArgumentException e) {
Logging.warning("An error occurred while parsing selector '%s' for %s. Is it is the correct format?",
playerSelector, sender.getName());
e.printStackTrace();
return null;
return Try.success(Collections.emptyList());
}
return Try.of(() -> Bukkit.selectEntities(sender, playerSelector).stream()
.filter(e -> e instanceof Player)
.map(e -> ((Player) e))
.collect(Collectors.toList()));
}
}
@@ -1,5 +1,7 @@
package org.mvplugins.multiverse.core.utils;
import org.jetbrains.annotations.ApiStatus;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
@@ -16,7 +18,17 @@ public class REPatterns {
public static final Pattern COMMA = get(",");
public static final Pattern DOT = get("\\.");
public static final Pattern EQUALS = get("=");
@ApiStatus.AvailableSince("5.4")
public static final Pattern HYPHEN = get("-");
@ApiStatus.AvailableSince("5.4")
public static final Pattern NAMESPACE = get("^[a-z0-9_.-]+$");
@ApiStatus.AvailableSince("5.4")
public static final Pattern NAMESPACE_KEY = get("^[a-z0-9/_.-]+$");
public static final Pattern SEMICOLON = get(";");
@ApiStatus.AvailableSince("5.4")
public static final Pattern SPACE = get(" ");
public static final Pattern UNDERSCORE = get("_");
public static final Pattern UUID = get("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
@ApiStatus.AvailableSince("5.4")
public static final Pattern YAML_KEY = get("^[a-zA-Z0-9\\[\\]_-]+$");
}
@@ -2,13 +2,17 @@ package org.mvplugins.multiverse.core.utils;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -30,15 +34,15 @@ public final class StringFormatter {
* @param list the list of strings to join. If the list is empty, an empty string is returned.
* @return the concatenated string
*/
public static @NotNull String joinAnd(List<String> list) {
public static @NotNull String joinAnd(@Nullable List<String> list) {
return join(list, ", ", " and ");
}
public static @NotNull String join(Collection list, String separator) {
public static @NotNull String join(@Nullable Collection<?> list, @NotNull String separator) {
if (list == null || list.isEmpty()) {
return "";
}
return list.stream().map(String::valueOf).collect(Collectors.joining(separator)).toString();
return list.stream().map(String::valueOf).collect(Collectors.joining(separator));
}
/**
@@ -50,7 +54,7 @@ public final class StringFormatter {
* @param lastSeparator the separator to use before the last element. For example, " and ".
* @return the concatenated string
*/
public static @NotNull String join(List<String> list, String separator, String lastSeparator) {
public static @NotNull String join(@Nullable List<String> list, @NotNull String separator, @NotNull String lastSeparator) {
if (list == null || list.isEmpty()) {
return "";
}
@@ -71,11 +75,30 @@ public final class StringFormatter {
/**
* Appends a list of suggestions to the end of the input string, separated by commas.
*
* @param input The current input
* @param addons The autocomplete suggestions
* @return A collection of suggestions with the next suggestion appended
*
* @deprecated Method name has a spelling error. Use {@link #addOnToCommaSeparated(String, Collection)} instead.
*/
@Deprecated(forRemoval = true, since = "5.5")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public static Collection<String> addonToCommaSeperated(@Nullable String input, @NotNull Collection<String> addons) {
return addOnToCommaSeparated(input, addons);
}
/**
* Appends a list of suggestions to the end of the input string, separated by commas.
*
* @param input The current input
* @param addons The autocomplete suggestions
* @return A collection of suggestions with the next suggestion appended
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public static Collection<String> addOnToCommaSeparated(@Nullable String input, @NotNull Collection<String> addons) {
if (Strings.isNullOrEmpty(input)) {
return addons;
}
@@ -94,7 +117,7 @@ public final class StringFormatter {
* @param args The args to parse
* @return The parsed args
*/
public static Collection<String> parseQuotesInArgs(String[] args) {
public static @NotNull Collection<String> parseQuotesInArgs(@NotNull String[] args) {
List<String> result = new ArrayList<>(args.length);
StringBuilder current = new StringBuilder();
boolean inQuotes = false;
@@ -139,7 +162,28 @@ public final class StringFormatter {
* @param input The string to add quotes to
* @return The quoted string
*/
public static String quoteMultiWordString(String input) {
return input.contains(" ") ? "\"" + input + "\"" : input;
@Contract("null -> null")
public static @Nullable String quoteMultiWordString(@Nullable String input) {
return input != null && input.contains(" ") ? "\"" + input + "\"" : input;
}
/**
* Parses a CSV string of key=value pairs into a map.
* E.g. "key1=value1,key2=value2" -> {key1=value1, key2=value2}
*
* @param input The CSV string to parse
* @return The parsed map
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public static @Unmodifiable Map<String, String> parseCSVMap(@Nullable String input) {
if (Strings.isNullOrEmpty(input)) {
return Map.of();
}
return REPatterns.COMMA.splitAsStream(input)
.map(s -> REPatterns.EQUALS.split(s, 2))
.filter(parts -> parts.length == 2)
.collect(Collectors.toUnmodifiableMap(parts -> parts[0], parts -> parts[1]));
}
}
@@ -0,0 +1,68 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import io.vavr.control.Option;
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 java.lang.reflect.Method;
import java.nio.file.Path;
/**
* Compatibility class used to handle API changes in {@link Bukkit} class.
*/
@ApiStatus.AvailableSince("5.6")
public final class BukkitCompatibility {
private static final Option<Method> GET_LEVEL_DIRECTORY_METHOD;
private static final Option<Method> GET_WORLD_NAMESPACED_KEY_METHOD;
static {
GET_LEVEL_DIRECTORY_METHOD = Option.of(ReflectHelper.getMethod(Server.class, "getLevelDirectory"));
GET_WORLD_NAMESPACED_KEY_METHOD = Option.of(ReflectHelper.getMethod(Bukkit.class, "getWorld", NamespacedKey.class));
}
/**
* 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/minecraft" 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.
*/
@ApiStatus.AvailableSince("5.6")
@NotNull
public static Path getWorldFoldersDirectory() {
Server server = Bukkit.getServer();
return GET_LEVEL_DIRECTORY_METHOD.map(method -> ReflectHelper.invokeMethod(server, method))
.filter(Path.class::isInstance)
.map(Path.class::cast)
.map(path -> path.resolve("dimensions/minecraft"))
.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
*/
@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
.map(method -> ReflectHelper.invokeMethod(null, method, NamespacedKey.fromString(nameOrKey)))
.filter(World.class::isInstance)
.map(World.class::cast));
}
}
@@ -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;
@@ -107,18 +107,14 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
*
* @return Whether this attempt is a success.
*/
default boolean isSuccess() {
return this instanceof Success;
}
boolean isSuccess();
/**
* Returns whether this attempt is a failure.
*
* @return Whether this attempt is a failure.
*/
default boolean isFailure() {
return this instanceof Failure;
}
boolean isFailure();
/**
* Converts this {@link Attempt} instance to an equivalent {@link Try} representation. Defaults to a
@@ -143,19 +139,25 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
@ApiStatus.AvailableSince("5.1")
Try<T> toTry(Function<Failure<T, F>, Throwable> throwableFunction);
/**
* Runs the given runnable after this attempt regardless of success or failure.
*
* @param runnable The runnable.
* @return This attempt.
*/
default Attempt<T, F> thenRun(Runnable runnable) {
runnable.run();
return this;
}
default Attempt<T, F> thenAccept(Consumer<Either<T, F>> consumer) {
if (this instanceof Success) {
consumer.accept(Either.left(get()));
} else {
consumer.accept(Either.right(getFailureReason()));
}
return this;
}
/**
* Accepts either the value or the failure reason depending on the result type.
* This will run regardless of success or failure.
*
* @param consumer The consumer with either the value or the failure reason.
* @return This attempt.
*/
Attempt<T, F> thenAccept(Consumer<Either<T, F>> consumer);
/**
* Peeks at the value if this is a success attempt.
@@ -163,12 +165,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param consumer The consumer with the value.
* @return This attempt.
*/
default Attempt<T, F> peek(Consumer<T> consumer) {
if (this instanceof Success) {
consumer.accept(get());
}
return this;
}
Attempt<T, F> peek(Consumer<T> consumer);
/**
* Maps the value to another value if this is a success attempt.
@@ -177,13 +174,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <U> The type of the new value.
* @return The new attempt.
*/
default <U> Attempt<U, F> map(Function<? super T, ? extends U> mapper) {
if (this instanceof Success) {
return new Success<>(mapper.apply(get()));
} else {
return new Failure<>((Failure<T, F>) this);
}
}
<U> Attempt<U, F> map(Function<? super T, ? extends U> mapper);
/**
* Maps the value to another attempt if this is a success attempt.
@@ -192,13 +183,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <U> The type of the new value.
* @return The new attempt.
*/
default <U> Attempt<U, F> map(Supplier<? extends U> mapper) {
if (this instanceof Success) {
return new Success<>(mapper.get());
} else {
return new Failure<>((Failure<T, F>) this);
}
}
<U> Attempt<U, F> map(Supplier<? extends U> mapper);
/**
* Maps the value to another attempt with the same fail reason if this is a success attempt.
@@ -207,13 +192,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <U> The type of the new value.
* @return The new attempt.
*/
default <U> Attempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper) {
if (this instanceof Success) {
return mapper.apply(get());
} else {
return new Failure<>((Failure<T, F>) this);
}
}
<U> Attempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper);
/**
* Maps the value to another attempt with the same fail reason if this is a success attempt.
@@ -222,13 +201,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <U> The type of the new value.
* @return The new attempt.
*/
default <U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper) {
if (this instanceof Success) {
return mapper.get();
} else {
return new Failure<>((Failure<T, F>) this);
}
}
<U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper);
/**
* Maps to another attempt with a different fail reason.
@@ -237,13 +210,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <UF> The type of the new fail reason.
* @return The new attempt.
*/
default <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
if (this instanceof Success) {
return new Success<>(get());
} else {
return new Failure<>(failureReason, getFailureMessage(), (Failure<T, F>) this);
}
}
<UF extends FailureReason> Attempt<T, UF> transform(UF failureReason);
/**
* Maps attempt result to another value.
@@ -256,13 +223,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
default <U> U transform(Function<T, U> successMapper, Function<F, U> failureMapper) {
if (this instanceof Success) {
return successMapper.apply(get());
} else {
return failureMapper.apply(getFailureReason());
}
}
<U> U transform(Function<T, U> successMapper, Function<F, U> failureMapper);
/**
* Calls either the failure or success function depending on the result type.
@@ -272,13 +233,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <N> The type of the new value.
* @return The result of the function.
*/
default <N> N fold(Function<Failure<T, F>, N> failureMapper, Function<T, N> successMapper) {
if (this instanceof Success) {
return successMapper.apply(get());
} else {
return failureMapper.apply((Failure<T, F>) this);
}
}
<N> N fold(Function<Failure<T, F>, N> failureMapper, Function<T, N> successMapper);
/**
* Calls the given runnable if this is a success attempt.
@@ -286,12 +241,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param runnable The runnable.
* @return This attempt.
*/
default Attempt<T, F> onSuccess(Runnable runnable) {
if (this instanceof Success) {
runnable.run();
}
return this;
}
Attempt<T, F> onSuccess(Runnable runnable);
/**
* Calls the given consumer if this is a success attempt.
@@ -299,12 +249,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param consumer The consumer with the value.
* @return This attempt.
*/
default Attempt<T, F> onSuccess(Consumer<T> consumer) {
if (this instanceof Success) {
consumer.accept(get());
}
return this;
}
Attempt<T, F> onSuccess(Consumer<T> consumer);
/**
* Calls the given consumer if this is a failure attempt.
@@ -312,12 +257,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param runnable The runnable.
* @return This attempt.
*/
default Attempt<T, F> onFailure(Runnable runnable) {
if (this instanceof Failure) {
runnable.run();
}
return this;
}
Attempt<T, F> onFailure(Runnable runnable);
/**
* Calls the given consumer if this is a failure attempt.
@@ -325,12 +265,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param consumer The consumer with the failure instance.
* @return This attempt.
*/
default Attempt<T, F> onFailure(Consumer<Failure<T, F>> consumer) {
if (this instanceof Failure) {
consumer.accept((Failure<T, F>) this);
}
return this;
}
Attempt<T, F> onFailure(Consumer<Failure<T, F>> consumer);
/**
* Calls the given runnable if this is a failure attempt.
@@ -338,12 +273,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param consumer The consumer with the failure reason.
* @return This attempt.
*/
default Attempt<T, F> onFailureReason(Consumer<F> consumer) {
if (this instanceof Failure) {
consumer.accept(getFailureReason());
}
return this;
}
Attempt<T, F> onFailureReason(Consumer<F> consumer);
/**
* Represents a successful attempt with a value.
@@ -378,6 +308,26 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
return value;
}
@Override
public F getFailureReason() {
throw new UnsupportedOperationException("No failure reason as attempt is a success");
}
@Override
public Message getFailureMessage() {
throw new UnsupportedOperationException("No failure message as attempt is a success");
}
@Override
public boolean isSuccess() {
return true;
}
@Override
public boolean isFailure() {
return false;
}
@Override
public Try<T> toTry() {
return Try.success(value);
@@ -389,13 +339,83 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
}
@Override
public F getFailureReason() {
throw new UnsupportedOperationException("No failure reason as attempt is a success");
public Attempt<T, F> thenAccept(Consumer<Either<T, F>> consumer) {
consumer.accept(Either.left(value));
return this;
}
@Override
public Message getFailureMessage() {
throw new UnsupportedOperationException("No failure message as attempt is a success");
public Attempt<T, F> peek(Consumer<T> consumer) {
consumer.accept(value);
return this;
}
@Override
public <U> Attempt<U, F> map(Function<? super T, ? extends U> mapper) {
return new Success<>(mapper.apply(value));
}
@Override
public <U> Attempt<U, F> map(Supplier<? extends U> mapper) {
return new Success<>(mapper.get());
}
@Override
public <U> Attempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper) {
return mapper.apply(value);
}
@Override
public <U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper) {
return mapper.get();
}
@Override
public <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
return changeFailureType();
}
@Override
public <U> U transform(Function<T, U> successMapper, Function<F, U> failureMapper) {
return successMapper.apply(value);
}
@Override
public <N> N fold(Function<Failure<T, F>, N> failureMapper, Function<T, N> successMapper) {
return successMapper.apply(value);
}
@Override
public Attempt<T, F> onSuccess(Runnable runnable) {
runnable.run();
return this;
}
@Override
public Attempt<T, F> onSuccess(Consumer<T> consumer) {
consumer.accept(value);
return this;
}
@Override
public Attempt<T, F> onFailure(Runnable runnable) {
return this;
}
@Override
public Attempt<T, F> onFailure(Consumer<Failure<T, F>> consumer) {
return this;
}
@Override
public Attempt<T, F> onFailureReason(Consumer<F> consumer) {
return this;
}
private <UF extends FailureReason> Attempt<T, UF> changeFailureType() {
@SuppressWarnings("unchecked")
Attempt<T, UF> mappedSuccess = (Attempt<T, UF>) this;
return mappedSuccess;
}
@Override
@@ -451,6 +471,26 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
throw exceptionSupplier.apply(this);
}
@Override
public F getFailureReason() {
return failureReason;
}
@Override
public Message getFailureMessage() {
return message;
}
@Override
public boolean isSuccess() {
return false;
}
@Override
public boolean isFailure() {
return true;
}
@Override
public Try<T> toTry() {
return Try.failure(new MultiverseException(message));
@@ -462,13 +502,83 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
}
@Override
public F getFailureReason() {
return failureReason;
public Attempt<T, F> thenAccept(Consumer<Either<T, F>> consumer) {
consumer.accept(Either.right(failureReason));
return this;
}
@Override
public Message getFailureMessage() {
return message;
public Attempt<T, F> peek(Consumer<T> consumer) {
return this;
}
@Override
public <U> Attempt<U, F> map(Function<? super T, ? extends U> mapper) {
return changeValueType();
}
@Override
public <U> Attempt<U, F> map(Supplier<? extends U> mapper) {
return changeValueType();
}
@Override
public <U> Attempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper) {
return changeValueType();
}
@Override
public <U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper) {
return changeValueType();
}
@Override
public <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
return new Failure<>(failureReason, getFailureMessage(), this);
}
@Override
public <U> U transform(Function<T, U> successMapper, Function<F, U> failureMapper) {
return failureMapper.apply(failureReason);
}
@Override
public <N> N fold(Function<Failure<T, F>, N> failureMapper, Function<T, N> successMapper) {
return failureMapper.apply(this);
}
@Override
public Attempt<T, F> onSuccess(Runnable runnable) {
return this;
}
@Override
public Attempt<T, F> onSuccess(Consumer<T> consumer) {
return this;
}
@Override
public Attempt<T, F> onFailure(Runnable runnable) {
runnable.run();
return this;
}
@Override
public Attempt<T, F> onFailure(Consumer<Failure<T, F>> consumer) {
consumer.accept(this);
return this;
}
@Override
public Attempt<T, F> onFailureReason(Consumer<F> consumer) {
consumer.accept(failureReason);
return this;
}
private <U> Attempt<U, F> changeValueType() {
@SuppressWarnings("unchecked")
Attempt<U, F> mappedFailure = (Attempt<U, F>) this;
return mappedFailure;
}
@Override
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.world;
import java.util.List;
import com.google.common.base.Strings;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.Difficulty;
@@ -10,6 +11,7 @@ import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -66,7 +68,8 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
}
/**
* Gets whether this world is loaded.
* Gets whether this world is loaded. Note that this may return false even if this is a stored instance of
* {@link LoadedMultiverseWorld} as worlds can be unloaded anytime.
*
* @return True if the world is loaded, else false.
*/
@@ -74,6 +77,23 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
return worldConfig.isLoadedWorld();
}
/**
* Attempts to get loaded world instance. Only returns a value if the world is loaded at the time of calling, even
* if this is a stored instance of {@link LoadedMultiverseWorld}.
* <br/>
* You may use {@link #isLoaded()} to check if the world is loaded first.
*
* @return Loaded world instance wrapped in an Option, or None if the world is not loaded.
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public Option<LoadedMultiverseWorld> asLoadedWorld() {
return Option.of(worldConfig.getMVWorld())
.filter(world -> world instanceof LoadedMultiverseWorld)
.map(world -> (LoadedMultiverseWorld) world);
}
/**
* Gets the properties handler of this world.
*
@@ -130,7 +130,9 @@ final class WorldConfig {
}
Try<Void> save() {
return configHandle.save();
return configHandle.save().onFailure(ex ->
Logging.warning("Failed to save world config for world '%s'. %s",
worldName, ex.getLocalizedMessage()));
}
ConfigurationSection getConfigurationSection() {
@@ -379,6 +381,10 @@ final class WorldConfig {
configNodes.setWorld(world);
}
MultiverseWorld getMVWorld() {
return configNodes.getWorld();
}
boolean isLoadedWorld() {
return configNodes.getWorld() instanceof LoadedMultiverseWorld;
}
@@ -390,7 +396,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", "");
@@ -60,11 +60,15 @@ final class WorldConfigNodes {
}
private <T> ConfigNode<T> node(ConfigNode.Builder<T, ?> nodeBuilder) {
nodeBuilder.onSetValue((oldValue, newValue) -> {
nodeBuilder.onChange((oldValue, newValue) -> {
if (Objects.equals(oldValue, newValue)) return;
if (world == null) return;
MVWorldPropertyChangedEvent<?> mvWorldPropertyChangeEvent = new MVWorldPropertyChangedEvent<>(
world, Option.of(nodeBuilder.name()).getOrElse(nodeBuilder.path()), oldValue, newValue);
world,
Option.of(nodeBuilder.name()).getOrElse(nodeBuilder.path()),
oldValue,
newValue
);
Bukkit.getPluginManager().callEvent(mvWorldPropertyChangeEvent);
Logging.finer("MVWorldPropertyChangeEvent fired for world '%s' with name '%s' and value '%s'",
world.getName(), nodeBuilder.path(), newValue);
@@ -83,7 +87,7 @@ final class WorldConfigNodes {
final ConfigNode<String> alias = node(ConfigNode.builder("alias", String.class)
.defaultValue("")
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (world == null) return;
world.updateColourlessAlias();
}));
@@ -93,14 +97,14 @@ final class WorldConfigNodes {
final ConfigNode<Boolean> allowFlight = node(ConfigNode.builder("allow-flight", Boolean.class)
.defaultValue(false)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
enforcementHandler.handleAllFlightEnforcement(loadedWorld);
}));
final ConfigNode<Boolean> allowWeather = node(ConfigNode.builder("allow-weather", Boolean.class)
.defaultValue(true)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(world -> {
if (!world.isClearWeather() && !newValue) {
@@ -128,7 +132,7 @@ final class WorldConfigNodes {
final ConfigNode<Difficulty> difficulty = node(ConfigNode.builder("difficulty", Difficulty.class)
.defaultValue(Difficulty.NORMAL)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.setDifficulty(newValue));
}));
@@ -175,7 +179,7 @@ final class WorldConfigNodes {
final ConfigNode<GameMode> gamemode = node(ConfigNode.builder("gamemode", GameMode.class)
.defaultValue(GameMode.SURVIVAL)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
enforcementHandler.handleAllGameModeEnforcement(loadedWorld);
}));
@@ -194,7 +198,7 @@ final class WorldConfigNodes {
final ConfigNode<Boolean> keepSpawnInMemory = node(ConfigNode
.builder("keep-spawn-in-memory", Boolean.class)
.defaultValue(true)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.setKeepSpawnInMemory(newValue));
}));
@@ -208,7 +212,7 @@ final class WorldConfigNodes {
final ConfigNode<Boolean> pvp = node(ConfigNode.builder("pvp", Boolean.class)
.defaultValue(true)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.setPVP(newValue));
}));
@@ -237,7 +241,7 @@ final class WorldConfigNodes {
final ConfigNode<SpawnLocation> spawnLocation = node(ConfigNode.builder("spawn-location", SpawnLocation.class)
.defaultValue(NullSpawnLocation.get())
.hidden()
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
if (newValue == null || newValue instanceof NullSpawnLocation) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> {
@@ -263,7 +267,7 @@ final class WorldConfigNodes {
return object.toSection();
}
})
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
newValue.setWorldRef(world);
newValue.applyConfigToWorld();
}));
@@ -6,7 +6,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -21,8 +20,10 @@ import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
import org.bukkit.GameRule;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldBorder;
import org.bukkit.WorldCreator;
import org.bukkit.WorldType;
import org.bukkit.plugin.PluginManager;
@@ -47,8 +48,10 @@ 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.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.utils.FileUtils;
@@ -86,12 +89,16 @@ 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 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 static final DimensionFormat DEFAULT_NETHER_FORMAT = new DimensionFormat("%overworld%_nether");
private static final DimensionFormat DEFAULT_END_FORMAT = new DimensionFormat("%overworld%_the_end");
private final Map<String, MultiverseWorld> worldsMap;
private final Map<String, LoadedMultiverseWorld> loadedWorldsMap;
private final CaseInsensitiveStringMap<MultiverseWorld> worldsMap;
private final CaseInsensitiveStringMap<LoadedMultiverseWorld> loadedWorldsMap;
private final List<String> unloadTracker;
private final List<String> loadTracker;
private final WorldsConfigManager worldsConfigManager;
@@ -134,8 +141,8 @@ public final class WorldManager {
this.config = config;
this.entityPurger = entityPurger;
this.worldsMap = new HashMap<>();
this.loadedWorldsMap = new HashMap<>();
this.worldsMap = new CaseInsensitiveStringMap<>();
this.loadedWorldsMap = new CaseInsensitiveStringMap<>();
this.unloadTracker = new ArrayList<>();
this.loadTracker = new ArrayList<>();
}
@@ -174,12 +181,12 @@ public final class WorldManager {
private void removeWorldsNotInConfigs(Collection<String> removedWorlds) {
removedWorlds.forEach(worldName -> getWorld(worldName)
.fold(
() -> Attempt.failure(FailureReason.GENERIC, Message.of("world already removed")),
world -> removeWorld(RemoveWorldOptions.world(world))
)
.onFailure(failure -> Logging.severe("Failed to unload world %s: %s", worldName, failure))
.onSuccess(success -> Logging.fine("Unloaded world %s as it was removed from config", worldName)));
.map(world -> removeWorld(RemoveWorldOptions.world(world)))
.getOrElse(() -> worldActionResult(RemoveFailureReason.WORLD_NON_EXISTENT, worldName))
.onFailure(failure ->
Logging.severe("Failed to unload world %s: %s", worldName, failure))
.onSuccess(success ->
Logging.fine("Unloaded world %s as it was removed from config", worldName)));
}
/**
@@ -208,30 +215,29 @@ public final class WorldManager {
}
private void importExistingBukkitWorld(World bukkitWorld) {
if (bukkitWorld == null || isWorld(bukkitWorld.getName())) {
return;
}
importWorld(ImportWorldOptions.worldName(bukkitWorld.getName())
.environment(bukkitWorld.getEnvironment())
.generator(generatorProvider.getDefaultGeneratorForWorld(bukkitWorld.getName())))
.onFailure(failure ->
Logging.severe("Failed to import world %s: %s", bukkitWorld.getName(), failure))
.onSuccess(success ->
Logging.fine("Imported existing world %s", bukkitWorld.getName()));
Option.of(bukkitWorld)
.filter(world -> !isWorld(world.getName()))
.map(world -> importWorld(
ImportWorldOptions.worldName(world.getName())
.environment(world.getEnvironment())
.generator(generatorProvider.getDefaultGeneratorForWorld(world.getName())))
.onFailure(failure ->
Logging.severe("Failed to import world %s: %s", world.getName(), failure))
.onSuccess(newMVWorld ->
Logging.fine("Imported existing world %s", newMVWorld.getName())));
}
/**
* Loads all worlds that are set to autoload.
*/
private void autoLoadWorlds() {
getWorlds().forEach(world -> {
if (isLoadedWorld(world) || !world.isAutoLoad()) {
return;
}
loadWorld(LoadWorldOptions.world(world))
.onFailure(failure ->
Logging.severe("Failed to load world %s: %s", world.getName(), failure));
});
getWorlds().stream()
.filter(world -> !isLoadedWorld(world) && world.isAutoLoad())
.forEach(world -> loadWorld(LoadWorldOptions.world(world))
.onFailure(failure ->
Logging.severe("Failed to autoload world %s: %s", world.getName(), failure))
.onSuccess(newMVWorld ->
Logging.fine("Autoloaded world %s", newMVWorld.getName())));
}
/**
@@ -272,15 +278,21 @@ public final class WorldManager {
.mapAttempt(creator -> addGeneratorToCreator(creator, generatorString))
.mapAttempt(this::createBukkitWorld)
.transform(CreateFailureReason.WORLD_CREATOR_FAILED)
.map(bukkitWorld -> {
LoadedMultiverseWorld loadedWorld = newLoadedMultiverseWorld(
.map(bukkitWorld -> newLoadedMultiverseWorld(
bukkitWorld,
generatorString,
options.biome(),
options.useSpawnAdjust());
pluginManager.callEvent(new MVWorldCreatedEvent(loadedWorld));
return loadedWorld;
});
options.useSpawnAdjust()))
.peek(loadedWorld -> postCreateWorld(loadedWorld, options));
}
private void postCreateWorld(LoadedMultiverseWorld loadedWorld, CreateWorldOptions options) {
options.worldPropertyStrings().forEach((key, value) -> loadedWorld.getStringPropertyHandle()
.setPropertyString(key, value)
.onFailure(failure -> Logging.warning("Failed to set property '%s' to '%s' for world %s: %s",
key, value, loadedWorld.getName(), failure.getMessage())));
pluginManager.callEvent(new MVWorldCreatedEvent(loadedWorld));
saveWorldsConfig();
}
/**
@@ -296,14 +308,9 @@ public final class WorldManager {
} else if (isWorld(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_EXIST_UNLOADED, worldName);
}
World bukkitWorld = Bukkit.getWorld(worldName);
if (bukkitWorld != null) {
// World is already loaded, maybe by another plugin
return doImportBukkitWorld(options, bukkitWorld);
}
return validateImportWorldOptions(options).mapAttempt(this::doImportWorld);
return Option.of(Bukkit.getWorld(worldName))
.map(bukkitWorld -> doImportBukkitWorld(options, bukkitWorld))
.getOrElse(() -> validateImportWorldOptions(options).mapAttempt(this::doImportWorld));
}
private Attempt<ImportWorldOptions, ImportFailureReason> validateImportWorldOptions(ImportWorldOptions options) {
@@ -326,15 +333,12 @@ public final class WorldManager {
.mapAttempt(creator -> addGeneratorToCreator(creator, generatorString))
.mapAttempt(this::createBukkitWorld)
.transform(ImportFailureReason.WORLD_CREATOR_FAILED)
.map(bukkitWorld -> {
LoadedMultiverseWorld loadedWorld = newLoadedMultiverseWorld(
bukkitWorld,
generatorString,
options.biome(),
options.useSpawnAdjust());
pluginManager.callEvent(new MVWorldImportedEvent(loadedWorld));
return loadedWorld;
});
.map(bukkitWorld -> newLoadedMultiverseWorld(
bukkitWorld,
generatorString,
options.biome(),
options.useSpawnAdjust()))
.peek(loadedWorld -> pluginManager.callEvent(new MVWorldImportedEvent(loadedWorld)));
}
private Attempt<LoadedMultiverseWorld, ImportFailureReason> doImportBukkitWorld(ImportWorldOptions options, World bukkitWorld) {
@@ -701,12 +705,14 @@ public final class WorldManager {
private Attempt<File, DeleteFailureReason> validateWorldToDelete(
@NotNull LoadedMultiverseWorld world) {
File worldFolder = world.getBukkitWorld().map(World::getWorldFolder).getOrNull();
if (worldFolder == null || !worldNameChecker.isValidWorldFolder(worldFolder)) {
Logging.severe("Failed to get world folder for world: " + world.getName());
return worldActionResult(DeleteFailureReason.WORLD_FOLDER_NOT_FOUND, world.getName());
}
return worldActionResult(worldFolder);
return world.getBukkitWorld().map(World::getWorldFolder)
.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(() -> {
Logging.severe("World folder does not exist for world: " + world.getName());
return worldActionResult(DeleteFailureReason.WORLD_FOLDER_NOT_FOUND, world.getName());
});
}
/**
@@ -721,18 +727,19 @@ public final class WorldManager {
.mapAttempt(validatedOptions -> {
ImportWorldOptions importWorldOptions = ImportWorldOptions
.worldName(validatedOptions.newWorldName())
.biome(validatedOptions.world().getBiome())
.environment(validatedOptions.world().getEnvironment())
.generator(validatedOptions.world().getGenerator());
.biome(validatedOptions.fromWorld().getBiome())
.environment(validatedOptions.fromWorld().getEnvironment())
.generator(validatedOptions.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()));
});
}
@@ -754,19 +761,24 @@ public final class WorldManager {
if (worldNameChecker.hasWorldFolder(newWorldName)) {
return worldActionResult(CloneFailureReason.WORLD_EXIST_FOLDER, newWorldName);
}
if (!worldNameChecker.isValidWorldFolder(options.fromWorld().getName())) {
return worldActionResult(CloneFailureReason.FROM_WORLD_FOLDER_INVALID, options.fromWorld().getName());
}
return worldActionResult(options);
}
private Attempt<CloneWorldOptions, CloneFailureReason> cloneWorldCopyFolder(@NotNull CloneWorldOptions 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(this::saveWorldWithFlush);
});
}
File worldFolder = options.world().getBukkitWorld().map(World::getWorldFolder).get();
File newWorldFolder = new File(Bukkit.getWorldContainer(), options.newWorldName());
File worldFolder = BukkitCompatibility.getWorldFoldersDirectory().resolve(options.fromWorld().getName()).toFile();
File newWorldFolder = BukkitCompatibility.getWorldFoldersDirectory().resolve(options.newWorldName()).toFile();
return fileUtils.copyFolder(worldFolder, newWorldFolder, CLONE_IGNORE_FILES).fold(
exception -> worldActionResult(CloneFailureReason.COPY_FAILED,
options.world().getName(), exception),
options.fromWorld().getName(), exception),
success -> worldActionResult(options));
}
@@ -782,25 +794,30 @@ public final class WorldManager {
}
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);
}
});
}
});
}
/**
@@ -811,11 +828,12 @@ 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())
.biome(world.getBiome())
.environment(world.getEnvironment())
.generateStructures(world.canGenerateStructures().getOrElse(true))
.generator(world.getGenerator())
@@ -839,6 +857,23 @@ 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);
}
@@ -948,12 +983,12 @@ public final class WorldManager {
* @return A list of all potential worlds.
*/
public List<String> getPotentialWorlds() {
File[] files = Bukkit.getWorldContainer().listFiles();
File[] files = BukkitCompatibility.getWorldFoldersDirectory().toFile().listFiles();
if (files == null) {
return Collections.emptyList();
}
return Arrays.stream(files)
.filter(file -> !isWorld(file.getName()))
.filter(file -> BukkitCompatibility.getWorldByNameOrKey(file.getName()).isEmpty())
.filter(worldNameChecker::isValidWorldFolder)
.map(File::getName)
.toList();
@@ -978,7 +1013,9 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getWorld(@Nullable String worldName) {
return getLoadedWorld(worldName).fold(() -> getUnloadedWorld(worldName), Option::of);
return getLoadedWorld(worldName)
.map(world -> (MultiverseWorld) world)
.orElse(() -> getUnloadedWorld(worldName));
}
/**
@@ -990,7 +1027,8 @@ public final class WorldManager {
*/
public Option<MultiverseWorld> getWorldByNameOrAlias(@Nullable String worldNameOrAlias) {
return getLoadedWorldByNameOrAlias(worldNameOrAlias)
.fold(() -> getUnloadedWorldByNameOrAlias(worldNameOrAlias), Option::of);
.map(world -> (MultiverseWorld) world)
.orElse(() -> getUnloadedWorldByNameOrAlias(worldNameOrAlias));
}
/**
@@ -1005,7 +1043,8 @@ public final class WorldManager {
public Collection<MultiverseWorld> getWorlds() {
return worldsMap.values().stream()
.map(world -> getLoadedWorld(world)
.fold(() -> world, loadedWorld -> loadedWorld))
.map(loadedWorld -> (MultiverseWorld) loadedWorld)
.getOrElse(world))
.toList();
}
@@ -1016,7 +1055,7 @@ public final class WorldManager {
* @return True if the world is a world is known to multiverse, but may or may not be loaded.
*/
public boolean isWorld(@Nullable String worldName) {
return worldsMap.containsKey(worldName);
return worldName != null && worldsMap.containsKey(worldName);
}
/**
@@ -1026,7 +1065,9 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getUnloadedWorld(@Nullable String worldName) {
return isLoadedWorld(worldName) ? Option.none() : Option.of(worldsMap.get(worldName));
return isLoadedWorld(worldName)
? Option.none()
: Option.of(worldName).flatMap(name -> Option.of(worldsMap.get(name)));
}
/**
@@ -1078,7 +1119,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable World world) {
return world == null ? Option.none() : Option.of(loadedWorldsMap.get(world.getName()));
return Option.of(world).flatMap(notNullWorld -> getLoadedWorld(notNullWorld.getName()));
}
/**
@@ -1088,7 +1129,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable MultiverseWorld world) {
return world == null ? Option.none() : Option.of(loadedWorldsMap.get(world.getName()));
return Option.of(world).flatMap(notNullWorld -> getLoadedWorld(notNullWorld.getName()));
}
/**
@@ -1098,7 +1139,8 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable String worldName) {
return Option.of(loadedWorldsMap.get(worldName));
return Option.of(worldName)
.flatMap(name -> Option.of(loadedWorldsMap.get(name)));
}
/**
@@ -1159,7 +1201,7 @@ public final class WorldManager {
* @return True if the world is a multiverse world that is loaded.
*/
public boolean isLoadedWorld(@Nullable String worldName) {
return loadedWorldsMap.containsKey(worldName);
return worldName != null && loadedWorldsMap.containsKey(worldName);
}
/**
@@ -38,8 +38,9 @@ public final class EntitySpawnConfig {
}
public void applyConfigToWorld() {
spawnCategoriesConfig.values()
.forEach(SpawnCategoryConfig::applyConfigToWorld);
for (SpawnCategory category : SpawnCategory.values()) {
getSpawnCategoryConfig(category).applyConfigToWorld();
}
}
@Override
@@ -61,14 +62,18 @@ public final class EntitySpawnConfig {
@ApiStatus.Internal
public static EntitySpawnConfig fromSection(CoreConfig config, ConfigurationSection section) {
Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig = new LinkedHashMap<>();
section.getValues(false).forEach((key, value) -> {
if (!(value instanceof ConfigurationSection sectionPart)) {
Logging.warning("Invalid spawn category config for " + key + ": " + value);
return;
Map<String, Object> existingCategories = section.getValues(false);
for (SpawnCategory category : SpawnCategory.values()) {
Object value = existingCategories.get(category.toString().toLowerCase(Locale.ENGLISH));
if (!(value instanceof ConfigurationSection)) {
if (value != null) {
Logging.warning("Invalid spawn category config for " + category + ": " + value);
}
value = section.createSection(category.toString());
}
SpawnCategory spawnCategory = SpawnCategory.valueOf(key.toUpperCase(Locale.ENGLISH));
spawnCategoriesConfig.put(spawnCategory, new SpawnCategoryConfig(config, spawnCategory, sectionPart));
});
ConfigurationSection sectionPart = (ConfigurationSection) value;
spawnCategoriesConfig.put(category, new SpawnCategoryConfig(config, category, sectionPart));
}
return new EntitySpawnConfig(config, spawnCategoriesConfig);
}
@@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core.world.entity;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.bukkit.ChatColor;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Entity;
@@ -14,6 +15,7 @@ import org.mvplugins.multiverse.core.config.node.ConfigNode;
import org.mvplugins.multiverse.core.config.node.ListConfigNode;
import org.mvplugins.multiverse.core.config.node.Node;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
@@ -70,18 +72,17 @@ public final class SpawnCategoryConfig {
Logging.finer("World %s %s skipping setTicksPerSpawns due to core config", world.getName(), spawnCategory);
return;
}
if (!isSpawn()) {
if (getExceptions().isEmpty()) {
Logging.finer("World %s %s setTicksPerSpawns: 0", world.getName(), spawnCategory);
bukkitWorld.setTicksPerSpawns(spawnCategory, 0);
} else {
Logging.finer("World %s %s setTicksPerSpawns: -1", world.getName(), spawnCategory);
bukkitWorld.setTicksPerSpawns(spawnCategory, -1);
}
} else {
Logging.finer("World %s %s setTicksPerSpawns: %d", world.getName(), spawnCategory, getTickRate());
bukkitWorld.setTicksPerSpawns(spawnCategory, getTickRate());
if (!isSpawn() && getExceptions().isEmpty()) {
Logging.finer("World %s %s setTicksPerSpawns: 0", world.getName(), spawnCategory);
bukkitWorld.setTicksPerSpawns(spawnCategory, 0);
return;
}
if (getTickRate() == -2) {
Logging.finer("World %s %s skipping setTicksPerSpawns as tick-rate is UNSET", world.getName(), spawnCategory);
return;
}
Logging.finer("World %s %s setTicksPerSpawns: %d", world.getName(), spawnCategory, getTickRate());
bukkitWorld.setTicksPerSpawns(spawnCategory, getTickRate());
}
private void applySpawnLimit(World bukkitWorld) {
@@ -89,6 +90,10 @@ public final class SpawnCategoryConfig {
Logging.finer("Skipping World %s %s setSpawnLimit due to core config", world.getName(), spawnCategory);
return;
}
if (getSpawnLimit() == -2) {
Logging.finer("World %s %s skipping setSpawnLimit as spawn-limit is UNSET", world.getName(), spawnCategory);
return;
}
Logging.finer("World %s %s setSpawnLimit: %d", world.getName(), spawnCategory, getSpawnLimit());
bukkitWorld.setSpawnLimit(spawnCategory, getSpawnLimit());
}
@@ -161,19 +166,39 @@ public final class SpawnCategoryConfig {
final ConfigNode<Boolean> spawn = node(ConfigNode.builder("spawn", Boolean.class)
.defaultValue(true)
.onSetValue((oldValue, newValue) -> applyConfigToWorld())
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.build());
final ConfigNode<Integer> tickRate = node(ConfigNode.builder("tick-rate", Integer.class)
.defaultValue(-1)
.suggester(input -> List.of("-1", "10", "100", "400", "1000"))
.onSetValue((oldValue, newValue) -> applyConfigToWorld())
.defaultValue(-2)
.suggester(input -> List.of("@unset", "@bukkit", "10", "100", "400", "1000"))
.serializer(SpawnValueSerializer.INSTANCE)
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.onChange((sender, oldValue, newValue) -> {
if (!config.getApplyEntitySpawnRate()) {
sender.sendMessage(ChatColor.RED + "Warning: Changing tick rates has no effect because " +
"'apply-entity-spawn-rate' is disabled in the core config.");
} else if (newValue == -2) {
sender.sendMessage(ChatColor.YELLOW + "Note: Setting tick-rate to '@unset' may not reset to " +
"the server default until the world is reloaded or server is restarted.");
}
})
.build());
final ConfigNode<Integer> spawnLimit = node(ConfigNode.builder("spawn-limit", Integer.class)
.defaultValue(-1)
.suggester(input -> List.of("-1", "10", "100", "400", "1000"))
.onSetValue((oldValue, newValue) -> applyConfigToWorld())
.defaultValue(-2)
.suggester(input -> List.of("@unset", "@bukkit", "10", "100", "400", "1000"))
.serializer(SpawnValueSerializer.INSTANCE)
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.onChange((sender, oldValue, newValue) -> {
if (!config.getApplyEntitySpawnLimit()) {
sender.sendMessage(ChatColor.RED + "Warning: Changing spawn limits has no effect because " +
"'apply-entity-spawn-limit' is disabled in the core config.");
} else if (newValue == -2) {
sender.sendMessage(ChatColor.YELLOW + "Note: Setting spawn-limit to '@unset' may not reset to " +
"the server default until the world is reloaded or server is restarted.");
}
})
.build());
final ListConfigNode<EntityType> exceptions = node(ListConfigNode.listBuilder("exceptions", EntityType.class)
@@ -181,7 +206,32 @@ public final class SpawnCategoryConfig {
.itemSuggester(input -> SpawnCategoryMapper.getEntityTypes(spawnCategory).stream()
.map(EntityType::name)
.toList())
.onSetValue((oldValue, newValue) -> applyConfigToWorld())
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.build());
}
private static class SpawnValueSerializer implements NodeSerializer<Integer> {
private static final SpawnValueSerializer INSTANCE = new SpawnValueSerializer();
@Override
public Integer deserialize(Object object, Class<Integer> type) {
String str = String.valueOf(object);
if (str.equalsIgnoreCase("@unset")) {
return -2;
}
if (str.equalsIgnoreCase("@bukkit")) {
return -1;
}
return Integer.parseInt(str);
}
@Override
public Object serialize(Integer object, Class<Integer> type) {
return switch (object) {
case -2 -> "@unset";
case -1 -> "@bukkit";
default -> object;
};
}
}
}
@@ -0,0 +1,90 @@
package org.mvplugins.multiverse.core.world.helpers;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnmodifiableView;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Tracks which players are in which worlds, using a thread-safe map.
* This allows async access to online players list and the world they are in.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@Service
public final class ConcurrentPlayerWorldTracker implements Listener {
private final Map<String, String> playerWorldMap;
@Inject
ConcurrentPlayerWorldTracker(@NotNull MultiverseCore plugin) {
this.playerWorldMap = new ConcurrentHashMap<>();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
/**
* Get an unmodifiable collection of all online player names on the server.
*
* @return Unmodifiable collection of online player names.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@NotNull
@UnmodifiableView
public Collection<String> getOnlinePlayers() {
return Collections.unmodifiableCollection(playerWorldMap.keySet());
}
/**
* Get the world name a player is currently in.
*
* @param playerName Name of the player.
* @return World name the player is in, or null if the player is not online.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@NotNull
public Option<String> getPlayerWorld(String playerName) {
return Option.of(playerWorldMap.get(playerName));
}
@EventHandler(priority = EventPriority.LOWEST)
private void onPlayerJoin(PlayerJoinEvent event) {
setPlayerWorld(event.getPlayer());
}
@EventHandler(priority = EventPriority.LOWEST)
private void onPlayerChangedWorld(@NotNull PlayerChangedWorldEvent event) {
setPlayerWorld(event.getPlayer());
}
private void setPlayerWorld(Player player) {
String playerName = player.getName();
String worldName = player.getWorld().getName();
playerWorldMap.put(playerName, worldName);
}
@EventHandler
private void onPlayerQuit(@NotNull PlayerQuitEvent event) {
String playerName = event.getPlayer().getName();
playerWorldMap.remove(playerName);
}
}
@@ -10,6 +10,7 @@ import org.bukkit.World;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
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 +91,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 +112,7 @@ public interface DataStore<T> {
* {@inheritDoc}
*/
@Override
public WorldConfigStore pasteTo(LoadedMultiverseWorld world) {
public WorldConfigStore<T> pasteTo(T world) {
if (configMap == null) {
return this;
}
@@ -1,15 +1,17 @@
package org.mvplugins.multiverse.core.world.helpers;
import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility;
/**
* <p>Utility class in helping to check the status of a world name and it's associated world folder.</p>
@@ -20,15 +22,27 @@ import org.jvnet.hk2.annotations.Service;
@Service
public final class WorldNameChecker {
private static final Pattern WORLD_NAME_PATTERN = Pattern.compile("[a-zA-Z0-9/._-]+");
private static final Set<String> BLACKLIST_NAMES = Set.of(
"cache",
"config",
"crash-reports",
"libraries",
"logs",
"plugins",
"versions");
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.
*
@@ -47,18 +61,21 @@ public final class WorldNameChecker {
*/
@NotNull
public NameStatus checkName(@Nullable String worldName) {
return Option.of(worldName).map(name -> {
if (name.isEmpty()) {
return NameStatus.EMPTY;
}
if (BLACKLIST_NAMES.contains(name)) {
return NameStatus.BLACKLISTED;
}
if (!WORLD_NAME_PATTERN.matcher(name).matches()) {
return NameStatus.INVALID_CHARS;
}
return NameStatus.VALID;
}).getOrElse(NameStatus.EMPTY);
return Option.of(worldName)
.map(name -> name.toLowerCase(Locale.ENGLISH))
.map(name -> {
if (name.isEmpty()) {
return NameStatus.EMPTY;
}
if (BLACKLIST_NAMES.contains(name)) {
return NameStatus.BLACKLISTED;
}
if (!REPatterns.NAMESPACE_KEY.matcher(name).matches()) {
return NameStatus.INVALID_CHARS;
}
return NameStatus.VALID;
})
.getOrElse(NameStatus.EMPTY);
}
/**
@@ -102,7 +119,8 @@ public final class WorldNameChecker {
if (worldName == null) {
return FolderStatus.DOES_NOT_EXIST;
}
File worldFolder = new File(Bukkit.getWorldContainer(), worldName);
File worldFolder = BukkitCompatibility.getWorldFoldersDirectory().resolve(worldName).toFile();
Logging.finer("Checking valid folder for world '%s' at: '%s'", worldName, worldFolder.getPath());
return checkFolder(worldFolder);
}
@@ -117,7 +135,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;
@@ -130,9 +148,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();
}
}
}
/**
@@ -1,7 +1,9 @@
package org.mvplugins.multiverse.core.world.options;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
/**
* Options for customizing the cloning of a world.
@@ -9,17 +11,35 @@ import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
public final class CloneWorldOptions implements KeepWorldSettingsOptions {
/**
* Creates a new {@link CloneWorldOptions} instance with the given world.
* Creates a new {@link CloneWorldOptions} instance with the given loaded world.
*
* @param world The world to clone.
* @param fromWorld The loaded world to clone.
* @param newWorldName The name of the new world.
* @return A new {@link CloneWorldOptions} instance.
*
* @deprecated Cloning can be done from unloaded worlds as well. Use {@link #fromTo(MultiverseWorld, String)} instead.
*/
public static CloneWorldOptions fromTo(LoadedMultiverseWorld world, String newWorldName) {
return new CloneWorldOptions(world, newWorldName);
@Deprecated(forRemoval = true, since = "5.6")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public static @NotNull CloneWorldOptions fromTo(@NotNull LoadedMultiverseWorld fromWorld, @NotNull String newWorldName) {
return new CloneWorldOptions(fromWorld, newWorldName);
}
private final LoadedMultiverseWorld world;
/**
* Creates a new {@link CloneWorldOptions} instance with the given world.
*
* @param fromWorld The world to clone.
* @param newWorldName The name of the new world.
* @return A new {@link CloneWorldOptions} instance.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
public static @NotNull CloneWorldOptions fromTo(@NotNull MultiverseWorld fromWorld, @NotNull String newWorldName) {
return new CloneWorldOptions(fromWorld, newWorldName);
}
private final MultiverseWorld fromWorld;
private final String newWorldName;
private boolean keepGameRule = true;
private boolean keepWorldConfig = true;
@@ -27,18 +47,34 @@ public final class CloneWorldOptions implements KeepWorldSettingsOptions {
private boolean keepWorldBorder = true;
CloneWorldOptions(LoadedMultiverseWorld world, String newWorldName) {
this.world = world;
CloneWorldOptions(MultiverseWorld fromWorld, String newWorldName) {
this.fromWorld = fromWorld;
this.newWorldName = newWorldName;
}
/**
* Gets the world to clone.
* Gets the loaded world to clone.
*
* @return The world to clone.
*
* @deprecated Cloning can be done from unloaded worlds as well. Use {@link #fromWorld()} instead.
*/
@Deprecated(forRemoval = true, since = "5.6")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public LoadedMultiverseWorld world() {
return world;
return fromWorld.asLoadedWorld().getOrNull();
}
/**
* Gets the loaded world to clone.
*
* @return The world to clone.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
public @NotNull MultiverseWorld fromWorld() {
return fromWorld;
}
/**
@@ -46,7 +82,7 @@ public final class CloneWorldOptions implements KeepWorldSettingsOptions {
*
* @return The name of the new world.
*/
public String newWorldName() {
public @NotNull String newWorldName() {
return newWorldName;
}
@@ -94,7 +130,6 @@ public final class CloneWorldOptions implements KeepWorldSettingsOptions {
return keepWorldConfig;
}
/**
* Sets whether to keep the world border of the world during cloning.
*
@@ -3,8 +3,14 @@ package org.mvplugins.multiverse.core.world.options;
import co.aikar.commands.ACFUtil;
import org.bukkit.World;
import org.bukkit.WorldType;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Options for customizing the creation of a new world.
@@ -31,6 +37,7 @@ public final class CreateWorldOptions {
private boolean useSpawnAdjust = true;
private WorldType worldType = WorldType.NORMAL;
private boolean doFolderCheck = true;
private final Map<String, String> worldPropertyStrings = new HashMap<>();
/**
* Creates a new {@link CreateWorldOptions} instance with the given world name.
@@ -260,4 +267,46 @@ public final class CreateWorldOptions {
public boolean doFolderCheck() {
return doFolderCheck;
}
/**
* Sets a world property string key-value pair. Overwrites any existing key.
*
* @param key The key of the world property string.
* @param value The value of the world property string.
* @return This {@link CreateWorldOptions} instance.
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public @NotNull CreateWorldOptions worldPropertyString(@NotNull String key, @Nullable String value) {
this.worldPropertyStrings.put(key, value);
return this;
}
/**
* Sets multiple world property string key-value pairs. Overwrites any existing keys.
*
* @param worldProperties A map of world property string key-value pairs.
* @return This {@link CreateWorldOptions} instance.
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public @NotNull CreateWorldOptions worldPropertyStrings(@NotNull Map<@NotNull String, @Nullable String> worldProperties) {
this.worldPropertyStrings.putAll(worldProperties);
return this;
}
/**
* Gets an unmodifiable view of the world property strings. Use {@link #worldPropertyString(String, String)} and
* {@link #worldPropertyStrings(Map)} to modify the world property strings.
*
* @return An unmodifiable view of the world property strings.
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public @UnmodifiableView @NotNull Map<String, String> worldPropertyStrings() {
return Collections.unmodifiableMap(worldPropertyStrings);
}
}
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.world.reasons;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
@@ -30,6 +31,14 @@ public enum CloneFailureReason implements FailureReason {
*/
WORLD_EXIST_LOADED(MVCorei18n.CLONEWORLD_WORLDEXISTLOADED),
/**
* The source world folder is invalid.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
FROM_WORLD_FOLDER_INVALID(MVCorei18n.CLONEWORLD_FROMWORLDFOLDERINVALID),
/**
* Failed to copy the world folder contents.
*/
@@ -273,6 +273,7 @@ mv-core.cloneworld.invalidworldname=World '{world}' contains invalid characters!
mv-core.cloneworld.worldexistfolder=World '{world}' exists in server folders! You need to delete it first before cloning.
mv-core.cloneworld.worldexistunloaded=World '{world}' already exists and it's unloaded! You need to delete it first before cloning.
mv-core.cloneworld.worldexistloaded=World '{world}' already exists! You need to delete it first before cloning.
mv-core.cloneworld.fromworldfolderinvalid=Source world '{world}' folder contents does not seem to be a valid world! Make sure it contains the correct world structure of data and region folders.
mv-core.cloneworld.copyfailed=Failed to copy world to '{world}': {error}\n&fSee console for more details.
mv-core.createworld.invalidworldname=World '{world}' contains invalid characters!
@@ -288,7 +289,7 @@ mv-core.deleteworld.failedtodeletefolder=Failed to delete world folder '{world}'
mv-core.importworld.invalidworldname=World '{world}' contains invalid characters!
mv-core.importworld.worldexistunloaded=World '{world}' already exists, but it's not loaded!&f Type '&a/mv load {world}&f' if you wish to load it.
mv-core.importworld.worldexistloaded=World '{world}' already exists!
mv-core.importworld.worldfolderinvalid=World '{world}' folder contents does not seem to be a valid world!\n&cIf the server software does something different with world folders, or you are very certain the world is valid, use '&3--skip-folder-check&c' flag.
mv-core.importworld.worldfolderinvalid=World '{world}' folder contents does not seem to be a valid world! Make sure it contains the correct world structure of data and region folders.\n&cIf the server software does something different with world folders, or you are very certain the world is valid, use '&3--skip-folder-check&c' flag.
mv-core.importworld.bukkitenvironmentmismatch=Environment mismatch detected!&f The world '{world}' is already loaded with environment '{bukkitEnvironment}', but Multiverse is trying to import it with environment '{mvEnvironment}'.
mv-core.loadworld.worldalreadyloading=World '{world}' is already loading! Please wait...
@@ -39,6 +39,8 @@ public class MVServerMock extends ServerMock {
world.getWorldFolder().mkdirs();
createFile(new File(world.getWorldFolder(), "uid.dat"));
createFile(new File(world.getWorldFolder(), "level.dat"));
new File(world.getWorldFolder(), "region").mkdir();
new File(world.getWorldFolder(), "data").mkdir();
addWorld(world);
return world;
}
@@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core.utils
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
class StringFormatterTest {
@Test
@@ -42,5 +43,22 @@ class StringFormatterTest {
"test",
StringFormatter.quoteMultiWordString("test")
)
assertNull(StringFormatter.quoteMultiWordString(null))
}
@Test
fun `StringFormatter parseCSVMap`() {
assertEquals(
emptyMap<String, String>(),
StringFormatter.parseCSVMap("")
)
assertEquals(
mapOf("key" to "value"),
StringFormatter.parseCSVMap("key=value")
)
assertEquals(
mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3"),
StringFormatter.parseCSVMap("key1=value1,key2=value2,key3=value3")
)
}
}
@@ -5,8 +5,17 @@ import org.bukkit.World
import org.bukkit.WorldType
import org.hamcrest.MatcherAssert.assertThat
import org.mockbukkit.mockbukkit.matcher.plugin.PluginManagerFiredEventClassMatcher.hasFiredEventInstance
import org.mockbukkit.mockbukkit.matcher.plugin.PluginManagerFiredEventClassMatcher.hasNotFiredEventInstance
import org.mvplugins.multiverse.core.TestWithMockBukkit
import org.mvplugins.multiverse.core.event.world.*
import org.mvplugins.multiverse.core.event.world.MVWorldClonedEvent
import org.mvplugins.multiverse.core.event.world.MVWorldCreatedEvent
import org.mvplugins.multiverse.core.event.world.MVWorldDeleteEvent
import org.mvplugins.multiverse.core.event.world.MVWorldImportedEvent
import org.mvplugins.multiverse.core.event.world.MVWorldLoadedEvent
import org.mvplugins.multiverse.core.event.world.MVWorldPropertyChangedEvent
import org.mvplugins.multiverse.core.event.world.MVWorldRegeneratedEvent
import org.mvplugins.multiverse.core.event.world.MVWorldRemovedEvent
import org.mvplugins.multiverse.core.event.world.MVWorldUnloadedEvent
import org.mvplugins.multiverse.core.world.options.CloneWorldOptions
import org.mvplugins.multiverse.core.world.options.CreateWorldOptions
import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions
@@ -35,15 +44,15 @@ class WorldManagerTest : TestWithMockBukkit() {
world = worldManager.getLoadedWorld("world").get()
assertNotNull(world)
assertTrue(worldManager.createWorld(CreateWorldOptions.worldName("world2")).isSuccess)
world2 = worldManager.getLoadedWorld("world2").get()
assertTrue(worldManager.createWorld(CreateWorldOptions.worldName("World2")).isSuccess)
world2 = worldManager.getLoadedWorld("World2").get()
assertNotNull(world2)
}
@Test
fun `Create world with custom options`() {
assertTrue(worldManager.createWorld(
CreateWorldOptions.worldName("world_nether")
CreateWorldOptions.worldName("Section/my-nether_world")
.environment(World.Environment.NETHER)
.generateStructures(false)
.seed(1234L)
@@ -51,11 +60,11 @@ class WorldManagerTest : TestWithMockBukkit() {
.worldType(WorldType.FLAT)
).isSuccess)
val getWorld = worldManager.getLoadedWorld("world_nether")
val getWorld = worldManager.getLoadedWorld("Section/my-nether_world")
assertTrue(getWorld.isDefined)
val world = getWorld.get()
assertNotNull(world)
assertEquals("world_nether", world.name)
assertEquals("Section/my-nether_world", world.name)
assertEquals(World.Environment.NETHER, world.environment)
assertFalse(world.canGenerateStructures().get())
assertEquals(1234L, world.seed)
@@ -84,6 +93,18 @@ class WorldManagerTest : TestWithMockBukkit() {
)
}
@Test
fun `Create world failed - world exists and loaded (case insensitive)`() {
assertEquals(
CreateFailureReason.WORLD_EXIST_LOADED,
worldManager.createWorld(CreateWorldOptions.worldName("WoRld2")).failureReason
)
assertEquals(
CreateFailureReason.WORLD_EXIST_LOADED,
worldManager.createWorld(CreateWorldOptions.worldName("WORLD2")).failureReason
)
}
@Test
fun `Create world failed - world exists but unloaded`() {
assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world)).isSuccess)
@@ -109,6 +130,11 @@ class WorldManagerTest : TestWithMockBukkit() {
assertFalse(worldManager.getLoadedWorld("world").isDefined)
assertFalse(worldManager.getUnloadedWorld("world").isDefined)
assertTrue(worldManager.removeWorld(RemoveWorldOptions.world(world2)).isSuccess)
assertFalse(worldManager.getWorld("World2").isDefined)
assertFalse(worldManager.getLoadedWorld("World2").isDefined)
assertFalse(worldManager.getUnloadedWorld("World2").isDefined)
assertThat(server.pluginManager, hasFiredEventInstance(MVWorldUnloadedEvent::class.java))
assertThat(server.pluginManager, hasFiredEventInstance(MVWorldRemovedEvent::class.java))
}
@@ -117,9 +143,18 @@ class WorldManagerTest : TestWithMockBukkit() {
fun `Delete world`() {
assertTrue(File(Bukkit.getWorldContainer(), "world").isDirectory)
assertTrue(worldManager.deleteWorld(DeleteWorldOptions.world(world)).isSuccess)
assertFalse(worldManager.getWorld("world").isDefined)
assertFalse(worldManager.getLoadedWorld("world").isDefined)
assertFalse(worldManager.getUnloadedWorld("world").isDefined)
assertFalse(File(Bukkit.getWorldContainer(), "world").isDirectory)
assertTrue(File(Bukkit.getWorldContainer(), "World2").isDirectory)
assertTrue(worldManager.deleteWorld(DeleteWorldOptions.world(world2)).isSuccess)
assertFalse(worldManager.getWorld("World2").isDefined)
assertFalse(worldManager.getLoadedWorld("World2").isDefined)
assertFalse(worldManager.getUnloadedWorld("World2").isDefined)
assertFalse(File(Bukkit.getWorldContainer(), "World2").isDirectory)
assertThat(server.pluginManager, hasFiredEventInstance(MVWorldDeleteEvent::class.java))
assertThat(server.pluginManager, hasFiredEventInstance(MVWorldUnloadedEvent::class.java))
assertThat(server.pluginManager, hasFiredEventInstance(MVWorldRemovedEvent::class.java))
@@ -130,21 +165,21 @@ class WorldManagerTest : TestWithMockBukkit() {
assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world2).saveBukkitWorld(true)).isSuccess)
assertFalse(world2.isLoaded)
assertFalse(world2.bukkitWorld.isDefined)
assertFalse(worldManager.getLoadedWorld("world2").isDefined)
assertTrue(worldManager.getWorld("world2").isDefined)
assertTrue(worldManager.getUnloadedWorld("world2").isDefined)
assertFalse(worldManager.getLoadedWorld("World2").isDefined)
assertTrue(worldManager.getWorld("World2").isDefined)
assertTrue(worldManager.getUnloadedWorld("World2").isDefined)
assertTrue(worldManager.loadWorld(LoadWorldOptions.world(world2)).isSuccess)
assertTrue(world2.isLoaded)
assertTrue(worldManager.getLoadedWorld("world2").flatMap{ w -> w.bukkitWorld }.isDefined)
assertTrue(worldManager.getLoadedWorld("world2").isDefined)
assertFalse(worldManager.getUnloadedWorld("world2").isDefined)
assertTrue(worldManager.getLoadedWorld("World2").flatMap{ w -> w.bukkitWorld }.isDefined)
assertTrue(worldManager.getLoadedWorld("World2").isDefined)
assertFalse(worldManager.getUnloadedWorld("World2").isDefined)
}
@Test
fun `Load world failed - invalid world folder`() {
assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world2)).isSuccess)
File(Bukkit.getWorldContainer(), "world2/").deleteRecursively()
File(Bukkit.getWorldContainer(), "World2/").deleteRecursively()
assertEquals(
LoadFailureReason.WORLD_FOLDER_INVALID,
worldManager.loadWorld(LoadWorldOptions.world(world2)).failureReason
@@ -163,6 +198,8 @@ class WorldManagerTest : TestWithMockBukkit() {
fun `Load world failed - world folder exists but not imported`() {
File(Bukkit.getWorldContainer(), "worldfolder").mkdir()
File(Bukkit.getWorldContainer(), "worldfolder/level.dat").createNewFile()
File(Bukkit.getWorldContainer(), "worldfolder/data").mkdir()
File(Bukkit.getWorldContainer(), "worldfolder/region").mkdir()
assertEquals(
LoadFailureReason.WORLD_EXIST_FOLDER,
worldManager.loadWorld("worldfolder").failureReason
@@ -177,7 +214,7 @@ class WorldManagerTest : TestWithMockBukkit() {
.seed(4321L)
).isSuccess)
val getWorld = worldManager.getLoadedWorld("world2")
val getWorld = worldManager.getLoadedWorld("World2")
assertTrue(getWorld.isDefined)
val world = getWorld.get()
assertNotNull(world)
@@ -217,7 +254,7 @@ class WorldManagerTest : TestWithMockBukkit() {
fun `Clone world failed - target world exists and loaded`() {
assertEquals(
CloneFailureReason.WORLD_EXIST_LOADED,
worldManager.cloneWorld(CloneWorldOptions.fromTo(world, "world2")).failureReason
worldManager.cloneWorld(CloneWorldOptions.fromTo(world, "World2")).failureReason
)
}
@@ -226,7 +263,7 @@ class WorldManagerTest : TestWithMockBukkit() {
assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world2)).isSuccess)
assertEquals(
CloneFailureReason.WORLD_EXIST_UNLOADED,
worldManager.cloneWorld(CloneWorldOptions.fromTo(world, "world2")).failureReason
worldManager.cloneWorld(CloneWorldOptions.fromTo(world, "World2")).failureReason
)
}
@@ -243,8 +280,10 @@ class WorldManagerTest : TestWithMockBukkit() {
fun `Get potential worlds`() {
File(Bukkit.getWorldContainer(), "newworld1").mkdir()
File(Bukkit.getWorldContainer(), "newworld1/level.dat").createNewFile()
File(Bukkit.getWorldContainer(), "newworld1/data").mkdir()
File(Bukkit.getWorldContainer(), "newworld2").mkdir()
File(Bukkit.getWorldContainer(), "newworld2/level.dat").createNewFile()
File(Bukkit.getWorldContainer(), "newworld2/data").mkdir()
assertEquals(setOf("newworld1", "newworld2"), worldManager.getPotentialWorlds().toSet())
}
@@ -255,4 +294,60 @@ class WorldManagerTest : TestWithMockBukkit() {
assertEquals(world, worldManager.getWorldByNameOrAlias("testalias").orNull)
assertNull(worldManager.getUnloadedWorldByNameOrAlias("testalias").orNull)
}
@Test
fun `Get world`() {
assertEquals(world, worldManager.getLoadedWorld("world").orNull)
assertNull(worldManager.getUnloadedWorld("world").orNull)
assertEquals(world, worldManager.getWorld("world").orNull)
assertTrue(worldManager.isLoadedWorld("world"))
assertFalse(worldManager.isUnloadedWorld("world"))
assertTrue(worldManager.isWorld("world"))
val unloadedWorld = worldManager.unloadWorld(UnloadWorldOptions.world(world)).get()
assertNull(worldManager.getLoadedWorld("world").orNull)
assertEquals(unloadedWorld, worldManager.getUnloadedWorld("world").orNull)
assertEquals(unloadedWorld, worldManager.getWorld("world").orNull)
assertFalse(worldManager.isLoadedWorld("world"))
assertTrue(worldManager.isUnloadedWorld("world"))
assertTrue(worldManager.isWorld("world"))
}
@Test
fun `Get world is case insensitive`() {
assertEquals(world2, worldManager.getLoadedWorld("WORLD2").orNull)
assertNull(worldManager.getUnloadedWorld("wOrld2").orNull)
assertEquals(world2, worldManager.getWorld("wOrld2").orNull)
assertTrue(worldManager.isLoadedWorld("WoRlD2"))
assertFalse(worldManager.isUnloadedWorld("WoRlD2"))
assertTrue(worldManager.isWorld("wORLD2"))
val unloadedWorld2 = worldManager.unloadWorld(UnloadWorldOptions.world(world2)).get()
assertNull(worldManager.getLoadedWorld("wOrld2").orNull)
assertEquals(unloadedWorld2, worldManager.getUnloadedWorld("wOrld2").orNull)
assertEquals(unloadedWorld2, worldManager.getWorld("wORLD2").orNull)
assertFalse(worldManager.isLoadedWorld("WoRlD2"))
assertTrue(worldManager.isUnloadedWorld("WoRlD2"))
assertTrue(worldManager.isWorld("wORLD2"))
}
@Test
fun `Set different value - MVWorldPropertyChangedEvent event is fired`() {
server.pluginManager.clearEvents()
world.scale = 8.0
assertThat(server.pluginManager, hasFiredEventInstance(MVWorldPropertyChangedEvent::class.java))
}
@Test
fun `Set equal value - MVWorldPropertyChangedEvent event is not fired`() {
server.pluginManager.clearEvents()
world.scale = 1.0
assertThat(server.pluginManager, hasNotFiredEventInstance(MVWorldPropertyChangedEvent::class.java))
}
}
@@ -38,12 +38,22 @@ class WorldNameCheckerTest : TestWithMockBukkit() {
fun `Blacklisted world name`() {
assertEquals(WorldNameChecker.NameStatus.BLACKLISTED, worldNameChecker.checkName("logs"))
assertEquals(WorldNameChecker.NameStatus.BLACKLISTED, worldNameChecker.checkName("plugins"))
assertEquals(WorldNameChecker.NameStatus.BLACKLISTED, worldNameChecker.checkName("CACHE"))
}
@Test
fun `Valid world folder`() {
File(Bukkit.getWorldContainer(), "test").mkdir()
File(Bukkit.getWorldContainer(), "test/level.dat").createNewFile()
File(Bukkit.getWorldContainer(), "test/data").mkdir()
assertEquals(WorldNameChecker.FolderStatus.VALID, worldNameChecker.checkFolder("test"))
}
@Test
fun `Valid world folder v26-1 format`() {
File(Bukkit.getWorldContainer(), "test").mkdir()
File(Bukkit.getWorldContainer(), "test/region").mkdir()
File(Bukkit.getWorldContainer(), "test/data").mkdir()
assertEquals(WorldNameChecker.FolderStatus.VALID, worldNameChecker.checkFolder("test"))
}
@@ -4,6 +4,7 @@ world:
enforce-access: false
enforce-gamemode: true
enforce-flight: true
gamemode-and-flight-enforce-delay: 1
apply-entity-spawn-rate: true
apply-entity-spawn-limit: true
auto-purge-entities: false
@@ -36,11 +37,14 @@ messaging:
enable-chat-prefix: false
chat-prefix-format: '[%world%]%chat%'
register-papi-hook: true
warn-invalid-papi-format: true
invalid-papi-format-returns-blank: false
default-locale: en
per-player-locale: true
command:
resolve-alias-name: true
simplified-destination-tab-completion: false
confirm-mode: enable
use-confirm-otp: true
confirm-timeout: 30
@@ -51,6 +55,7 @@ event-priority:
player-respawn: low
player-spawn-location: normal
player-teleport: highest
player-changed-world: normal
misc:
bukkit-yml-path: bukkit.yml
+82 -2
View File
@@ -33,7 +33,47 @@ world:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
world_nether:
@@ -71,6 +111,46 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
+41 -1
View File
@@ -33,6 +33,46 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
+78 -3
View File
@@ -34,11 +34,46 @@ world:
pitch: 0.0
yaw: 0.0
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
spawn: falSe
tick-rate: 123
spawn-limit: -1
exceptions: [cow]
spawn-limit: '@unset'
exceptions: [ cow ]
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: # should be parsed as list of string
- a
- 1
@@ -78,6 +113,46 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
+111 -21
View File
@@ -34,16 +34,46 @@ world_the_end:
pitch: 0.0
yaw: 0.0
spawning:
animal:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
monster:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
spawn-limit: '@unset'
tick-rate: '@bukkit'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@bukkit'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
world[dot]a[dot]b:
@@ -82,16 +112,46 @@ world[dot]a[dot]b:
pitch: 0.0
yaw: 0.0
spawning:
animal:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
monster:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
spawn-limit: '@unset'
tick-rate: '@bukkit'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@bukkit'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist:
- test
version: 1.2
@@ -131,16 +191,46 @@ world[dot]a[dot]c:
pitch: 0.0
yaw: 0.0
spawning:
animal:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
monster:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
spawn-limit: '@unset'
tick-rate: '@bukkit'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@bukkit'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist:
- test
version: 1.2
+123 -3
View File
@@ -33,7 +33,47 @@ world:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
world_nether:
@@ -71,7 +111,47 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
new[dot]world:
@@ -104,6 +184,46 @@ new[dot]world:
seed: -9223372036854775808
spawn-location:
==: MVNullLocation (It's a bug if you see this in your config file)
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
@@ -33,7 +33,47 @@ world:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
world_nether:
@@ -66,6 +106,46 @@ world_nether:
seed: -9223372036854775808
spawn-location:
==: MVNullLocation (It's a bug if you see this in your config file)
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
+74 -4
View File
@@ -34,17 +34,47 @@ world:
pitch: 0.0
yaw: 0.0
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
spawn: true
tick-rate: 111
spawn-limit: -1
spawn-limit: '@unset'
exceptions:
- cow
- pig
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
spawn: false
tick-rate: -1
spawn-limit: -1
tick-rate: '@unset'
spawn-limit: '@unset'
exceptions: [ ]
world-blacklist: []
version: 1.2
@@ -83,6 +113,46 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2