diff --git a/src/main/java/org/mvplugins/multiverse/core/MultiverseCore.java b/src/main/java/org/mvplugins/multiverse/core/MultiverseCore.java index ec1e1d12b..c729a0c5e 100644 --- a/src/main/java/org/mvplugins/multiverse/core/MultiverseCore.java +++ b/src/main/java/org/mvplugins/multiverse/core/MultiverseCore.java @@ -126,6 +126,7 @@ public void onDisable() { MultiverseCoreApi.shutdown(); shutdownDependencyInjection(); PluginServiceLocatorFactory.get().shutdown(); + Logging.info("- Disabled"); Logging.shutdown(); } diff --git a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandCompletions.java b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandCompletions.java index 71acc4e50..167ef6c5f 100644 --- a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandCompletions.java +++ b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandCompletions.java @@ -53,7 +53,7 @@ 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 { @@ -163,7 +163,7 @@ private Collection completeWithPreconditions( } } if (context.hasConfig("multiple")) { - return addonToCommaSeperated(context.getInput(), handler.getCompletions(context)); + return addOnToCommaSeparated(context.getInput(), handler.getCompletions(context)); } return handler.getCompletions(context); } @@ -334,7 +334,7 @@ private Collection suggestPlayersArray(BukkitCommandCompletionContext co matchedPlayers.add(name); } } - return addonToCommaSeperated(context.getInput(), matchedPlayers); + return addOnToCommaSeparated(context.getInput(), matchedPlayers); } private Collection suggestSpawnCategoryPropsName(BukkitCommandCompletionContext context) { diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/CreateCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/CreateCommand.java index acc880566..7be73b0a6 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/CreateCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/CreateCommand.java @@ -19,7 +19,6 @@ 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.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(" [--seed --generator --world-type --adjust-spawn " - + "--no-structures --biome ]") + + "--no-structures --biome --properties ]") @Description("{@@mv-core.create.description}") void onCreateCommand( MVCommandIssuer issuer, @@ -68,7 +68,7 @@ void onCreateCommand( @Optional @Syntax("[--seed --generator --world-type --adjust-spawn " - + "--no-structures --biome ]") + + "--no-structures --biome --properties ]") @Description("{@@mv-core.create.flags.description}") String[] flagArray) { ParsedCommandFlags parsedFlags = flags.parse(flagArray); @@ -78,14 +78,15 @@ void onCreateCommand( 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 @@ private Flags( .addAlias("-b") .completion(input -> biomeProviderFactory.suggestBiomeString(input)) .build()); + + private final CommandValueFlag properties = flag(CommandValueFlag.builder("--properties", String.class) + .addAlias("-p") + .build()); } @Service diff --git a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java index d8a1bec15..9aaf5fc4c 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java @@ -186,6 +186,16 @@ public boolean getEnforceFlight() { return configHandle.get(configNodes.enforceFlight); } + @ApiStatus.AvailableSince("5.5") + public Try 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 setApplyEntitySpawnRate(boolean applyEntitySpawnRate) { return configHandle.set(configNodes.applyEntitySpawnRate, applyEntitySpawnRate); @@ -641,6 +651,16 @@ public EventPriority getEventPriorityPlayerTeleport() { return configHandle.get(configNodes.eventPriorityPlayerTeleport); } + @ApiStatus.AvailableSince("5.5") + public Try setEventPriorityPlayerWorldChange(EventPriority eventPriorityPlayerWorldChange) { + return configHandle.set(configNodes.eventPriorityPlayerWorldChange, eventPriorityPlayerWorldChange); + } + + @ApiStatus.AvailableSince("5.5") + public EventPriority getEventPriorityPlayerWorldChange() { + return configHandle.get(configNodes.eventPriorityPlayerWorldChange); + } + public Try setBukkitYmlPath(String bukkitYmlPath) { return configHandle.set(configNodes.bukkitYmlPath, bukkitYmlPath); } diff --git a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java index 0dc3f6dda..ce857c86e 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java @@ -135,6 +135,16 @@ private N node(N node) { .name("enforce-flight") .build()); + final ConfigNode 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 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.") @@ -545,6 +555,18 @@ private N node(N node) { "full effect after a server restart.")) .build()); + final ConfigNode eventPriorityPlayerWorldChange = node(ConfigNode.builder("event-priority.player-changed-world", EventPriority.class) + .defaultValue(EventPriority.NORMAL) + .name("event-priority-player-changed-world") + .comment("") + .comment("This config option defines the priority for the PlayerChangedWorldEvent.") + .onLoadAndChange((oldValue, newValue) -> + eventPriorityMapper.get().setPriority("mvcore-player-changed-world", newValue)) + .onChange((sender, oldValue, newValue) -> + sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-changed-world' config option will only take " + + "full effect after a server restart.")) + .build()); + private final ConfigHeaderNode miscHeader = node(ConfigHeaderNode.builder("misc") .comment("") .comment("") diff --git a/src/main/java/org/mvplugins/multiverse/core/config/node/ListConfigNode.java b/src/main/java/org/mvplugins/multiverse/core/config/node/ListConfigNode.java index 453c7a6e0..c68468bde 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/node/ListConfigNode.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/node/ListConfigNode.java @@ -120,9 +120,9 @@ private void setDefaults() { 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)); } } diff --git a/src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java b/src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java index 9d459e9bc..5397ba417 100644 --- a/src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java +++ b/src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java @@ -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; @@ -257,9 +255,9 @@ private void teleportToDestinationOnJoin(Player player, DestinationInstance { - 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); } } diff --git a/src/main/java/org/mvplugins/multiverse/core/teleportation/BlockSafety.java b/src/main/java/org/mvplugins/multiverse/core/teleportation/BlockSafety.java index f242ef880..0c29b75f4 100644 --- a/src/main/java/org/mvplugins/multiverse/core/teleportation/BlockSafety.java +++ b/src/main/java/org/mvplugins/multiverse/core/teleportation/BlockSafety.java @@ -132,6 +132,10 @@ public boolean canSpawnAtLocationSafely(@NotNull Location location) { */ 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); diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/StringFormatter.java b/src/main/java/org/mvplugins/multiverse/core/utils/StringFormatter.java index e4b34896a..acfe5da65 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/StringFormatter.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/StringFormatter.java @@ -2,13 +2,17 @@ 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 @@ private 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 list) { + public static @NotNull String joinAnd(@Nullable List 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 @@ private StringFormatter() { * @param lastSeparator the separator to use before the last element. For example, " and ". * @return the concatenated string */ - public static @NotNull String join(List list, String separator, String lastSeparator) { + public static @NotNull String join(@Nullable List list, @NotNull String separator, @NotNull String lastSeparator) { if (list == null || list.isEmpty()) { return ""; } @@ -71,11 +75,30 @@ private 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 addonToCommaSeperated(@Nullable String input, @NotNull Collection 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 addOnToCommaSeparated(@Nullable String input, @NotNull Collection addons) { if (Strings.isNullOrEmpty(input)) { return addons; } @@ -94,7 +117,7 @@ public static Collection addonToCommaSeperated(@Nullable String input, @ * @param args The args to parse * @return The parsed args */ - public static Collection parseQuotesInArgs(String[] args) { + public static @NotNull Collection parseQuotesInArgs(@NotNull String[] args) { List result = new ArrayList<>(args.length); StringBuilder current = new StringBuilder(); boolean inQuotes = false; @@ -139,7 +162,28 @@ public static Collection parseQuotesInArgs(String[] args) { * @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 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])); } } diff --git a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java index 08e259661..64a20179f 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java @@ -6,9 +6,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; @@ -278,7 +276,16 @@ private Attempt doCreateWorld( generatorString, options.biome(), options.useSpawnAdjust())) - .peek(loadedWorld -> pluginManager.callEvent(new MVWorldCreatedEvent(loadedWorld))); + .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(); } /** diff --git a/src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java b/src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java index 9ea1d4ebf..68db0b636 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java @@ -3,8 +3,14 @@ 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 worldPropertyStrings = new HashMap<>(); /** * Creates a new {@link CreateWorldOptions} instance with the given world name. @@ -260,4 +267,44 @@ public boolean useSpawnAdjust() { 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. + */ + @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 worldPropertyStrings() { + return Collections.unmodifiableMap(worldPropertyStrings); + } } diff --git a/src/test/java/org/mvplugins/multiverse/core/utils/StringFormatterTest.kt b/src/test/java/org/mvplugins/multiverse/core/utils/StringFormatterTest.kt index b59ac1808..bd51a43c4 100644 --- a/src/test/java/org/mvplugins/multiverse/core/utils/StringFormatterTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/utils/StringFormatterTest.kt @@ -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(), + 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") + ) } } diff --git a/src/test/resources/configs/fresh_config.yml b/src/test/resources/configs/fresh_config.yml index 556916250..7fc906999 100644 --- a/src/test/resources/configs/fresh_config.yml +++ b/src/test/resources/configs/fresh_config.yml @@ -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 @@ -54,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