Compare commits

...
53 Commits
Author SHA1 Message Date
Ben Woo a41476de89 Merge pull request #3363 from Multiverse/fix/warning-spam
Revert throw an error when trying to set spawn-rate or spawn-limit if…
2025-10-15 22:30:32 +08:00
Ben Woo fe75e497e4 Revert throw an error when trying to set spawn-rate or spawn-limit if disabled by config 2025-10-15 22:26:31 +08:00
Ben Woo 4004554733 Merge pull request #3358 from Multiverse/chore/mockbukkit-dep
Bump mockbukkit and fix test dep
2025-10-12 22:42:15 +08:00
Ben Woo 99037b2af5 Bump mockbukkit and fix test dep 2025-10-12 22:37:30 +08:00
Ben Woo ac46a5f64a Merge pull request #3355 from Multiverse/chore/config-links
Update wiki and add donation link in `config.yml`
2025-10-09 22:08:47 +08:00
Ben Woo 4d805b921b Merge pull request #3356 from Multiverse/chore/copper-update
Add supported mc versions for 1.21.9 and 1.21.10
2025-10-09 21:55:48 +08:00
Ben Woo a612854490 Add supported mc versions for 1.21.9 and 1.21.10 2025-10-09 21:42:20 +08:00
Ben Woo e85c14e319 Update wiki and add donation link in config.yml 2025-10-09 21:39:25 +08:00
Ben Woo 3983af7b4c Merge pull request #3351 from Multiverse/fix/spawn-location
Fix join location on 1.21.9 due to deprecated PlayerSpawnLocationEvent
2025-10-03 23:07:40 +08:00
Ben Woo 8853f88f3a Fix join location on 1.21.9 due to deprecated PlayerSpawnLocationEvent 2025-10-03 23:01:07 +08:00
Ben Woo 08d361e0ec Merge pull request #3350 from Multiverse/chore/mockbukkit-bump
Bump multiverse-plugin and mockbukkit version
2025-10-01 21:51:18 +08:00
Ben Woo f985f2db06 Bump multiverse-plugin and mockbukkit version 2025-10-01 21:46:38 +08:00
Ben Woo 82acca3ff9 Merge pull request #3348 from Multiverse/feat/properties-bukkit-world
Use bukkit world properties for difficulty, scale, and keep-spawn when create/import
2025-10-01 21:34:05 +08:00
Ben Woo d712c1eb09 Merge pull request #3347 from Multiverse/fix/deprecated-biome
Refactor biome parsing to use NamespacedKey and Registry
2025-10-01 21:33:28 +08:00
Ben Woo 25e9213ee9 Merge pull request #3349 from Multiverse/fix/apply-spawn-disabled
Throw an error when trying to set spawn-rate or spawn-limit if disabled by config
2025-10-01 21:33:02 +08:00
Ben Woo 9e79708fec Refactor biome parsing to use NamespacedKey and Registry 2025-09-29 22:18:42 +08:00
Ben Woo 65d1141289 Throw an error when trying to set spawn-rate or spawn-limit if disabled by config 2025-09-29 22:01:51 +08:00
Ben Woo 63b2ffdf72 Use bukkit world properties for difficulty, scale, and keep-spawn when create/import 2025-09-29 21:34:00 +08:00
Ben Woo 14d8ad7a5c Merge pull request #3346 from Multiverse/fix/locale-lowercase
Fix issues with lower/uppercasing string when server is of non-english locale
2025-09-29 21:18:29 +08:00
Ben Woo 33e0df925f Merge pull request #3345 from Multiverse/chore/non-public-listener
Cleanup listener methods to be non-public
2025-09-29 21:17:45 +08:00
Ben Woo 1883edbb28 Fix issues with lower/uppercasing string when server is of non-english locale 2025-09-29 20:47:08 +08:00
Ben Woo b7a2bd61ca Cleanup listener methods to be non-public 2025-09-29 20:22:40 +08:00
Ben Woo a0673ac109 Merge pull request #3344 from Multiverse/fix/test-generator
Pass empty string instead of null for generator id and improve error message
2025-09-28 17:50:10 +08:00
Ben Woo 7735a33ccf Change test error message to not show full stacktrace 2025-09-28 17:47:05 +08:00
Ben Woo 5f24520685 Pass empty string instead of null for generator type 2025-09-28 17:46:31 +08:00
Ben Woo 608cb8611c Merge pull request #3343 from Multiverse/fix/custom-env
Remove tab completing "custom" environment type
2025-09-28 10:19:03 +08:00
Ben Woo a965b9ce56 Remove tab completing "custom" environment type 2025-09-28 10:15:08 +08:00
Ben Woo 981d368058 Merge pull request #3338 from Multiverse/feat/relative-positioning
Add back support for relative position with ~ in destination coordinates
2025-09-22 20:10:26 +08:00
Ben Woo 7507099a4e Merge pull request #3341 from Multiverse/api/unloadedworldlocation-final
Make UnloadedWorldLocation non-final to allow extending
2025-09-22 20:08:52 +08:00
Ben Woo 29b58b1655 Make UnloadedWorldLocation non-final to allow extending 2025-09-22 19:57:10 +08:00
Ben Woo a1081c1b6b Merge pull request #3340 from Multiverse/feat/generator-test
Improve generator plugin detection and add an option to disable it
2025-09-22 19:55:36 +08:00
Ben Woo 3152b62668 Merge pull request #3339 from Multiverse/fix/worlds-old-exist
Fix issue when trying to backup `worlds.yml.old` and file already exists
2025-09-22 19:51:57 +08:00
Ben Woo 0082cc9852 Improve generator plugin detection and add an option to disable it 2025-09-22 19:51:18 +08:00
Ben Woo f60b656bed Fix issue when trying to backup worlds.yml.old and file already exists 2025-09-22 19:27:58 +08:00
Ben Woo da5f5de5cd Add localization for position parse error messages 2025-09-20 21:33:16 +08:00
Ben Woo e9aacecc2f Add back support for relative position with ~ in destination coordinates 2025-09-20 20:31:13 +08:00
Ben Woo fa3adbe605 Merge pull request #3332 from Multiverse/fix/delete-fail-message
Fix message when delete failed due to invalid world folder
2025-09-06 13:11:39 +08:00
Ben Woo e33b2f7025 Fix message when delete failed due to invalid world folder 2025-09-06 13:07:03 +08:00
Ben Woo a9cc577754 Merge pull request #3330 from ElFrod0/config-server-properties-path
Configurable server.properties path
2025-08-31 10:34:48 +08:00
Ben Woo 02344a31cf Add api status annotation for new server-properties-path config 2025-08-31 10:30:33 +08:00
ElFrod0 2f4871ef71 Add newline for consistency 2025-08-29 19:40:41 +02:00
ElFrod0 9750fad4b7 Update fresh config for tests 2025-08-29 19:35:21 +02:00
ElFrod0 df77372d96 Configurable server.properties file location 2025-08-29 19:33:49 +02:00
Ben Woo 039af3c219 Merge pull request #3327 from Multiverse/5.3
5.3
2025-08-29 10:34:05 +08:00
Ben Woo 09ec4f87ef Merge pull request #3328 from Multiverse/fix/mvapi-not-started
Pass in config object directly into spawn config class instead of using mvapi
2025-08-27 14:06:11 +08:00
Ben Woo f996f22f4b Pass in config object directly into spawn config class instead of using mvapi 2025-08-27 14:02:37 +08:00
Ben Woo b230c5ae9c Merge pull request #3326 from Multiverse/fix/spawning-config-order
Maintain spawn category config order between config saves
2025-08-27 13:50:30 +08:00
Ben Woo 2561107697 Merge pull request #3325 from Multiverse/feat/disable-apply-spawn
Add option to disable mv handling of spawn rate and limit
2025-08-27 13:48:42 +08:00
Ben Woo 70babc8656 Maintain spawn category config order between config saves 2025-08-27 13:46:59 +08:00
Ben Woo bd09d6c950 Merge pull request #3324 from Multiverse/chore/deps
Fix some dependency issues
2025-08-27 13:35:17 +08:00
Ben Woo b94de763ca Depend on latest paper api version 2025-08-27 12:40:52 +08:00
Ben Woo 22ca57543f Change mockbukkit version to fix test issues 2025-08-27 12:40:25 +08:00
Ben Woo 25c70170d9 Add option to disable mv handling of spawn rate and limit 2025-08-27 12:35:09 +08:00
40 changed files with 1115 additions and 226 deletions

No files matched your search

@@ -107,7 +107,7 @@ jobs:
files: '["${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar"]'
name: ${{ steps.release-info.outputs.tag_name }}
changelog: ${{ steps.release-artifact.outputs.body }}
game_versions: 1.21.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: 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
dependencies: ${{ inputs.modrinth_dependencies }}
@@ -121,7 +121,7 @@ jobs:
changelog: ${{ steps.release-artifact.outputs.body }}
changelog_type: markdown
display_name: ${{ steps.release-info.outputs.tag_name }}
game_versions: 1.21.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: 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
release_type: ${{ steps.parse-release-type.outputs.release_type }}
project_relations: ${{ inputs.dbo_project_relations }}
file_path: ${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar
@@ -136,5 +136,5 @@ jobs:
channel: ${{ steps.parse-release-type.outputs.release_type }}
files: '[{"path": "${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar", "platforms": ["PAPER"]}]'
description: ${{ steps.release-artifact.outputs.body }}
platform_dependencies: '{"PAPER": ["1.18.2-1.21.8"]}'
platform_dependencies: '{"PAPER": ["1.18.2-1.21.10"]}'
plugin_dependencies: ${{ inputs.hangar_plugin_dependencies }}
+3 -3
View File
@@ -1,7 +1,7 @@
import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'org.mvplugins.multiverse-plugin' version '1.2.0'
id 'org.mvplugins.multiverse-plugin' version '1.2.2'
}
group = 'org.mvplugins.multiverse.core'
@@ -25,9 +25,9 @@ repositories {
}
configure(apiDependencies) {
serverApiVersion = '1.19.4-R0.1-SNAPSHOT'
serverApiVersion = '1.21.8-R0.1-SNAPSHOT'
mockBukkitServerApiVersion = '1.21'
mockBukkitVersion = '4.72.2'
mockBukkitVersion = '4.84.0'
}
dependencies {
@@ -1,7 +1,6 @@
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;
@@ -20,9 +19,8 @@ import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@Service
final class PlaceholderExpansionHook extends PlaceholderExpansion {
@@ -123,7 +121,7 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
@NotNull List<String> placeholderParams,
@NotNull LoadedMultiverseWorld world) {
// Switch to find what specific placeholder we want
switch (placeholder.toLowerCase()) {
switch (placeholder.toLowerCase(Locale.ENGLISH)) {
case "alias" -> {
return world.getAliasOrName();
}
@@ -6,6 +6,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
@@ -87,7 +88,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
registerAsyncCompletion("commands", this::suggestCommands);
registerAsyncCompletion("destinations", this::suggestDestinations);
registerStaticCompletion("difficulties", suggestEnums(Difficulty.class));
registerStaticCompletion("environments", suggestEnums(World.Environment.class));
registerStaticCompletion("environments", List.of("normal", "nether", "the_end")); // Don't tab complete the "custom" environment
registerAsyncCompletion("flags", this::suggestFlags);
registerStaticCompletion("gamemodes", suggestEnums(GameMode.class));
registerStaticCompletion("gamerules", this::suggestGamerules);
@@ -176,7 +177,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
public <T extends Enum<T>> Collection<String> suggestEnums(Class<T> enumClass) {
return EnumSet.allOf(enumClass).stream()
.map(Enum::name)
.map(String::toLowerCase)
.map(name -> name.toLowerCase(Locale.ENGLISH))
.toList();
}
@@ -116,7 +116,8 @@ public class MVCommandContexts extends PaperCommandContexts {
}
return destinationsProvider.parseDestination(context.getSender(), destination)
.getOrThrow(failure -> MVInvalidCommandArgument.of(failure.getFailureMessage()));
.getOrThrow(failure ->
new InvalidCommandArgument(failure.getFailureMessage().formatted(context.getIssuer())));
}
private GameRule<?> parseGameRule(BukkitCommandExecutionContext context) {
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.command.flag;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import co.aikar.commands.InvalidCommandArgument;
@@ -220,7 +221,7 @@ public class CommandValueFlag<T> extends CommandFlag {
private void setEnumContext() {
this.context = (String value) -> {
try {
return Enum.valueOf(type, value.toUpperCase());
return Enum.valueOf(type, value.toUpperCase(Locale.ENGLISH));
} catch (IllegalArgumentException e) {
throw new InvalidCommandArgument("Invalid value for argument " + key + ": " + value);
}
@@ -229,7 +230,7 @@ public class CommandValueFlag<T> extends CommandFlag {
private void setEnumCompletion() {
List<String> types = Arrays.stream(type.getEnumConstants())
.map(typeClass -> typeClass.name().toLowerCase())
.map(typeClass -> typeClass.name().toLowerCase(Locale.ENGLISH))
.toList();
this.completion = (input) -> types;
@@ -113,9 +113,9 @@ final class EntitySpawnConfigCommand extends CoreCommand {
.getSpawnCategoryConfig(spawnCategory)
.getStringPropertyHandle()
.modifyPropertyString(property, value, action)
.onSuccess(ignore -> issuer.sendMessage("Successfully set " + property + " to " + value
.onSuccess(ignore -> issuer.sendInfo("Successfully set " + property + " to " + value
+ " for " + spawnCategory.name() + " in " + world.getName()))
.onFailure(e -> issuer.sendMessage("Unable to set " + property + " to " + value
.onFailure(e -> issuer.sendError("Unable to set " + property + " to " + value
+ " for " + spawnCategory.name() + " in " + world.getName() + ": " + e.getMessage()));
worldManager.saveWorldsConfig();
@@ -190,6 +190,26 @@ public final class CoreConfig {
return configHandle.get(configNodes.enforceFlight);
}
@ApiStatus.AvailableSince("5.3")
public Try<Void> setApplyEntitySpawnRate(boolean applyEntitySpawnRate) {
return configHandle.set(configNodes.applyEntitySpawnRate, applyEntitySpawnRate);
}
@ApiStatus.AvailableSince("5.3")
public boolean getApplyEntitySpawnRate() {
return configHandle.get(configNodes.applyEntitySpawnRate);
}
@ApiStatus.AvailableSince("5.3")
public Try<Void> setApplyEntitySpawnLimit(boolean applyEntitySpawnLimit) {
return configHandle.set(configNodes.applyEntitySpawnLimit, applyEntitySpawnLimit);
}
@ApiStatus.AvailableSince("5.3")
public boolean getApplyEntitySpawnLimit() {
return configHandle.get(configNodes.applyEntitySpawnLimit);
}
/**
* {@inheritDoc}
*/
@@ -603,6 +623,26 @@ public final class CoreConfig {
return configHandle.get(configNodes.bukkitYmlPath);
}
@ApiStatus.AvailableSince("5.3")
public Try<Void> setServerPropertiesPath(String serverPropertiesPath) {
return configHandle.set(configNodes.serverPropertiesPath, serverPropertiesPath);
}
@ApiStatus.AvailableSince("5.3")
public String getServerPropertiesPath() {
return configHandle.get(configNodes.serverPropertiesPath);
}
@ApiStatus.AvailableSince("5.3")
public Try<Void> setAutoDetectGeneratorPlugins(boolean autoDetectGeneratorPlugins) {
return configHandle.set(configNodes.autoDetectGeneratorPlugins, autoDetectGeneratorPlugins);
}
@ApiStatus.AvailableSince("5.3")
public boolean getAutoDetectGeneratorPlugins() {
return configHandle.get(configNodes.autoDetectGeneratorPlugins);
}
/**
* {@inheritDoc}
*/
@@ -76,13 +76,10 @@ final class CoreConfigNodes {
.comment("# █░▀░█ █▄█ █▄▄ ░█░ █ ▀▄▀ ██▄ █▀▄ ▄█ ██▄   █▄▄ █▄█ █▀▄ ██▄ #")
.comment("# #")
.comment("# #")
.comment("# WIKI: https://github.com/Multiverse/Multiverse-Core/wiki #")
.comment("# WIKI: https://mvplugins.org/core/ #")
.comment("# DISCORD: https://discord.gg/NZtfKky #")
.comment("# BUG REPORTS: https://github.com/Multiverse/Multiverse-Core/issues #")
.comment("# #")
.comment("# #")
.comment("# Each option in this file is documented and explained here: #")
.comment("# ==> https://github.com/Multiverse/Multiverse-Core/wiki/config.yml #")
.comment("# DONATE: https://github.com/sponsors/Multiverse #")
.comment("# #")
.comment("# #")
.comment("# New options are added to this file automatically. If you manually made changes #")
@@ -137,6 +134,24 @@ final class CoreConfigNodes {
.name("enforce-flight")
.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.")
.defaultValue(true)
.name("apply-entity-spawn-rate")
.build());
final ConfigNode<Boolean> applyEntitySpawnLimit = node(ConfigNode.builder("world.apply-entity-spawn-limit", Boolean.class)
.comment("")
.comment("Sets whether Multiverse will apply the world's entity `spawn-limit` config when a world is loaded.")
.comment("If disabled, the `spawn-limit` config in worlds.yml will be ignored.")
.comment("Disable this if you want paper-world.yml or another plugin to handle entity limits per world.")
.defaultValue(true)
.name("apply-entity-spawn-limit")
.build());
final ConfigNode<Boolean> autoPurgeEntities = node(ConfigNode.builder("world.auto-purge-entities", Boolean.class)
.comment("")
.comment("Sets whether Multiverse will purge entities on world load based world's entity spawn config.")
@@ -494,6 +509,23 @@ final class CoreConfigNodes {
.name("bukkit-yml-path")
.build());
final ConfigNode<String> serverPropertiesPath = node(ConfigNode.builder("misc.server-properties-path", String.class)
.comment("")
.comment("Change this if you use a custom path for the server.properties file with `--config` startup flag.")
.comment("Note: this config option needs a server restart to take effect.")
.defaultValue("server.properties")
.name("server-properties-path")
.build());
final ConfigNode<Boolean> autoDetectGeneratorPlugins = node(ConfigNode.builder("misc.auto-detect-generator-plugins", Boolean.class)
.comment("")
.comment("When enabled, Multiverse will attempt to automatically detect world generator plugins installed on your server.")
.comment("This option only affects tab-completion within `/mv create` command and output of `/mv generators` command.")
.comment("Disabling this will not have any affect on the usages of world generator plugins itself.")
.defaultValue(true)
.name("auto-detect-generator-plugins")
.build());
final ConfigNode<Integer> globalDebug = node(ConfigNode.builder("misc.global-debug", Integer.class)
.comment("")
.comment("This is our debug flag to help identify issues with Multiverse.")
@@ -537,6 +569,7 @@ final class CoreConfigNodes {
final ConfigNode<Boolean> showDonationMessage = node(ConfigNode.builder("misc.show-donation-message", Boolean.class)
.comment("")
.comment("If you don't want to donate, you can set this to false and Multiverse will stop nagging you.")
.comment("But if you want to donate, here is our Github Sponsors page: https://github.com/sponsors/Multiverse")
.defaultValue(true)
.name("show-donation-message")
.build());
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.config.node.functions;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import co.aikar.commands.ACFUtil;
@@ -41,13 +42,13 @@ public final class DefaultStringParserProvider {
}
private static final NodeStringParser<Enum> ENUM_STRING_PARSER = (input, type) -> Try.of(
() -> Enum.valueOf(type, input.toUpperCase()));
() -> Enum.valueOf(type, input.toUpperCase(Locale.ENGLISH)));
private static final NodeStringParser<String> STRING_STRING_PARSER = (input, type) -> Try.of(
() -> input);
private static final NodeStringParser<Boolean> BOOLEAN_STRING_PARSER = (input, type) -> Try.of(
() -> switch (String.valueOf(input).toLowerCase()) {
() -> switch (String.valueOf(input).toLowerCase(Locale.ENGLISH)) {
case "t", "true", "on", "y", "yes", "1", "allow" -> true;
case "f", "false", "off", "n", "no", "0", "deny" -> false;
default -> throw new MultiverseException("Unable to convert '" + input + "' to boolean. Please use 'true' or 'false'");
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.config.node.functions;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -43,7 +44,7 @@ public final class DefaultSuggesterProvider {
private static NodeSuggester enumSuggester(Class<?> clazz) {
return input -> Arrays.stream(clazz.getEnumConstants())
.map(Object::toString)
.map(String::toLowerCase)
.map(name -> name.toLowerCase(Locale.ENGLISH))
.collect(Collectors.toList());
}
@@ -48,12 +48,12 @@ public final class DefaultSerializerProvider {
if (type.isInstance(object)) {
return (Enum<?>) object;
}
return Enum.valueOf(type, String.valueOf(object).toUpperCase());
return Enum.valueOf(type, String.valueOf(object).toUpperCase(Locale.ENGLISH));
}
@Override
public Object serialize(Enum object, Class<Enum> type) {
return object.name().toLowerCase();
return object.name().toLowerCase(Locale.ENGLISH);
}
};
@@ -80,7 +80,7 @@ public final class DefaultSerializerProvider {
}
String input = String.valueOf(object);
//todo: this is a copy from string parser
return switch (input.toLowerCase()) {
return switch (input.toLowerCase(Locale.ENGLISH)) {
case "t", "true", "on", "y", "yes", "1", "allow" -> true;
case "f", "false", "off", "n", "no", "0", "deny" -> false;
default -> throw new RuntimeException("Unable to convert '" + input + "' to boolean.");
@@ -19,8 +19,12 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.utils.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;
@@ -62,7 +66,11 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
* @return A new {@link ExactDestinationInstance}
*/
public @NotNull ExactDestinationInstance fromLocation(@NotNull Location location) {
return new ExactDestinationInstance(this, new UnloadedWorldLocation(location));
return new ExactDestinationInstance(
this,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
);
}
/**
@@ -73,12 +81,16 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
@NotNull CommandSender sender,
@NotNull String destinationParams
) {
String[] items = REPatterns.COLON.split(destinationParams);
String[] items = REPatterns.COLON.split(destinationParams, 2);
if (items.length < 2) {
if (items[0].equals("@here")) {
return getLocationFromSender(sender)
.map(location -> Attempt.<ExactDestinationInstance, InstanceFailureReason>success(
new ExactDestinationInstance(this, new UnloadedWorldLocation(location))
new ExactDestinationInstance(
this,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
)
))
.getOrElse(() -> Attempt.failure(InstanceFailureReason.INVALID_COORDINATES_FORMAT)); // todo: specific failure reason for this case
}
@@ -86,41 +98,21 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
}
String worldName = items[0];
String coordinates = items[1];
String[] coordinatesParams = REPatterns.COMMA.split(coordinates);
if (coordinatesParams.length != 3) {
return Attempt.failure(InstanceFailureReason.INVALID_COORDINATES_FORMAT);
}
String positionStr = items[1];
World world = getLoadedMultiverseWorld(worldName).flatMap(LoadedMultiverseWorld::getBukkitWorld).getOrNull();
if (world == null) {
return Attempt.failure(InstanceFailureReason.WORLD_NOT_FOUND, Replace.WORLD.with(worldName));
}
UnloadedWorldLocation location;
EntityPosition position;
try {
location = new UnloadedWorldLocation(
world,
Double.parseDouble(coordinatesParams[0]),
Double.parseDouble(coordinatesParams[1]),
Double.parseDouble(coordinatesParams[2])
);
} catch (NumberFormatException e) {
position = EntityPosition.fromString(positionStr);
} catch (PositionParseException e) {
return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e));
}
if (items.length == 4) {
String pitch = items[2];
String yaw = items[3];
try {
location.setPitch(Float.parseFloat(pitch));
location.setYaw(Float.parseFloat(yaw));
} catch (NumberFormatException e) {
return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e));
}
}
return Attempt.success(new ExactDestinationInstance(this, location));
return Attempt.success(new ExactDestinationInstance(this, worldName, position));
}
//TODO: Extract to a world finder class
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.destination.core;
import io.vavr.control.Option;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
@@ -8,22 +9,30 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.utils.position.VectorPosition;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
/**
* Destination instance implementation for the {@link ExactDestination}.
*/
public final class ExactDestinationInstance extends DestinationInstance<ExactDestinationInstance, ExactDestination> {
private final UnloadedWorldLocation location;
private final String worldName;
private final EntityPosition position;
/**
* Constructor.
*
* @param location The location to teleport to.
* @param destination The parent destination.
* @param worldName The name of the world.
* @param position The position in the world.
*/
ExactDestinationInstance(@NotNull ExactDestination destination, @NotNull UnloadedWorldLocation location) {
ExactDestinationInstance(@NotNull ExactDestination destination,
@NotNull String worldName,
@NotNull EntityPosition position) {
super(destination);
this.location = location;
this.worldName = worldName;
this.position = position;
}
/**
@@ -31,10 +40,13 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
*/
@Override
public @NotNull Option<Location> getLocation(@NotNull Entity teleportee) {
if (location.getWorld() == null) {
World world = Bukkit.getWorld(worldName);
if (world == null) {
return Option.none();
}
return Option.of(location.toBukkitLocation());
Location destinationLocation = position.toBukkitLocation(teleportee.getLocation());
destinationLocation.setWorld(world);
return Option.of(destinationLocation);
}
/**
@@ -58,7 +70,7 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
*/
@Override
public @NotNull Option<String> getFinerPermissionSuffix() {
return Option.of(location.getWorld()).map(World::getName);
return Option.of(worldName);
}
/**
@@ -66,7 +78,6 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
*/
@Override
public @NotNull String serialise() {
return location.getWorldName() + ":" + location.getX() + "," + location.getY()
+ "," + location.getZ() + ":" + location.getPitch() + ":" + location.getYaw();
return worldName + ":" + position.toString();
}
}
@@ -0,0 +1,23 @@
package org.mvplugins.multiverse.core.exceptions.utils.position;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.locale.message.Message;
public class PositionParseException extends MultiverseException {
public PositionParseException(String message) {
super(message);
}
public PositionParseException(@Nullable Message message) {
super(message);
}
public PositionParseException(@Nullable String message, @Nullable Throwable cause) {
super(message, cause);
}
public PositionParseException(@Nullable Message message, @Nullable Throwable cause) {
super(message, cause);
}
}
@@ -11,7 +11,6 @@ import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
@@ -43,7 +42,7 @@ final class MVEntityListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
public void foodLevelChange(FoodLevelChangeEvent event) {
void foodLevelChange(FoodLevelChangeEvent event) {
if (event.isCancelled()) {
return;
}
@@ -65,7 +64,7 @@ final class MVEntityListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
public void entityRegainHealth(EntityRegainHealthEvent event) {
void entityRegainHealth(EntityRegainHealthEvent event) {
if (event.isCancelled() || event.getRegainReason() != RegainReason.REGEN) {
return;
}
@@ -84,7 +83,7 @@ final class MVEntityListener implements CoreListener {
* @param event The event.
*/
@EventMethod
public void creatureSpawn(CreatureSpawnEvent event) {
void creatureSpawn(CreatureSpawnEvent event) {
if (event.isCancelled()) {
return;
}
@@ -110,7 +109,7 @@ final class MVEntityListener implements CoreListener {
* Handle Spawn Category settings for non-creature entities.
*/
@EventMethod
public void entitySpawn(EntitySpawnEvent event) {
void entitySpawn(EntitySpawnEvent event) {
if (event.getEntity() instanceof LivingEntity) {
// Handled by CreatureSpawnEvent
return;
@@ -23,6 +23,7 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventPriority;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerPortalEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
@@ -32,16 +33,16 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.dynamiclistener.EventRunnable;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventClass;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventPriorityKey;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.event.MVRespawnEvent;
import org.mvplugins.multiverse.core.locale.PluginLocales;
import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker;
import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter;
import org.mvplugins.multiverse.core.teleportation.BlockSafety;
import org.mvplugins.multiverse.core.teleportation.TeleportQueue;
import org.mvplugins.multiverse.core.utils.result.ResultChain;
@@ -52,7 +53,6 @@ import org.mvplugins.multiverse.core.world.entrycheck.EntryFeeResult;
import org.mvplugins.multiverse.core.world.entrycheck.WorldEntryCheckerProvider;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder;
import org.mvplugins.multiverse.core.world.helpers.EnforcementHandler;
import org.spigotmc.event.player.PlayerSpawnLocationEvent;
/**
* Multiverse's Listener for players.
@@ -72,6 +72,7 @@ final class MVPlayerListener implements CoreListener {
private final EnforcementHandler enforcementHandler;
private final DimensionFinder dimensionFinder;
private final CorePermissionsChecker corePermissionsChecker;
private final AsyncSafetyTeleporter asyncSafetyTeleporter;
private final Map<String, String> playerWorld = new ConcurrentHashMap<>();
@@ -89,7 +90,8 @@ final class MVPlayerListener implements CoreListener {
DestinationsProvider destinationsProvider,
EnforcementHandler enforcementHandler,
DimensionFinder dimensionFinder,
CorePermissionsChecker corePermissionsChecker) {
CorePermissionsChecker corePermissionsChecker,
AsyncSafetyTeleporter asyncSafetyTeleporter) {
this.plugin = plugin;
this.config = config;
this.worldManagerProvider = worldManagerProvider;
@@ -103,6 +105,7 @@ final class MVPlayerListener implements CoreListener {
this.enforcementHandler = enforcementHandler;
this.dimensionFinder = dimensionFinder;
this.corePermissionsChecker = corePermissionsChecker;
this.asyncSafetyTeleporter = asyncSafetyTeleporter;
}
private WorldManager getWorldManager() {
@@ -113,7 +116,7 @@ final class MVPlayerListener implements CoreListener {
return commandManagerProvider.get();
}
public PluginLocales getLocales() {
private PluginLocales getLocales() {
return getCommandManager().getLocales();
}
@@ -122,7 +125,7 @@ final class MVPlayerListener implements CoreListener {
*
* @return the playerWorld-map
*/
public Map<String, String> getPlayerWorld() {
Map<String, String> getPlayerWorld() {
return playerWorld;
}
@@ -134,7 +137,7 @@ final class MVPlayerListener implements CoreListener {
@EventMethod
@EventPriorityKey("mvcore-player-respawn")
@DefaultEventPriority(EventPriority.LOW)
public void playerRespawn(PlayerRespawnEvent event) {
void playerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer();
LoadedMultiverseWorld mvWorld = getWorldManager().getLoadedWorld(player.getWorld()).getOrNull();
if (mvWorld == null) {
@@ -201,68 +204,63 @@ final class MVPlayerListener implements CoreListener {
return Option.of(mvWorld.getSpawnLocation());
}
@EventClass("org.spigotmc.event.player.PlayerSpawnLocationEvent")
@EventMethod
// TODO: Consider if this key is needed anymore, need to remove from config.yml as well
@EventPriorityKey("mvcore-player-spawn-location")
EventRunnable playerSpawnLocation() {
return new EventRunnable<PlayerSpawnLocationEvent>() {
@Override
public void onEvent(PlayerSpawnLocationEvent event) {
Player player = event.getPlayer();
MultiverseWorld world = getWorldManager().getLoadedWorld(player.getWorld()).getOrNull();
if (world == null) {
Logging.finer("Player joined in a world that is not managed by Multiverse.");
return;
}
if (!player.hasPlayedBefore()) {
handleFirstSpawn(event);
} else {
handleJoinLocation(event);
}
handleGameModeAndFlight(player, event.getSpawnLocation().getWorld());
}
void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
if (player.hasPlayedBefore()) {
handleJoinLocation(player);
} else {
handleFirstSpawn(player);
}
handleGameModeAndFlight(player, player.getWorld());
}
private void handleFirstSpawn(PlayerSpawnLocationEvent event) {
if (!config.getFirstSpawnOverride()) {
Logging.finer("FirstSpawnOverride is disabled");
// User has disabled the feature in config
return;
}
Logging.fine("Moving NEW player to(firstspawnoverride): %s", config.getFirstSpawnLocation());
destinationsProvider.parseDestination(config.getFirstSpawnLocation())
.map(destination -> destination.getLocation(event.getPlayer())
.peek(event::setSpawnLocation)
.onEmpty(() -> Logging.warning("The destination in FirstSpawnLocation in config is invalid")))
.onFailure(failure -> {
Logging.warning("Invalid destination in FirstSpawnLocation in config: %s");
Logging.warning(failure.getFailureMessage().formatted(getLocales()));
});
}
private void handleFirstSpawn(Player player) {
if (!config.getFirstSpawnOverride()) {
Logging.finer("FirstSpawnOverride is disabled");
// User has disabled the feature in config
return;
}
Logging.fine("Moving NEW player to(firstspawnoverride): %s", config.getFirstSpawnLocation());
destinationsProvider.parseDestination(config.getFirstSpawnLocation())
.peek(destination -> teleportToDestinationOnJoin(player, destination))
.onFailure(failure -> {
Logging.warning("Invalid destination in FirstSpawnLocation in config: %s");
Logging.warning(failure.getFailureMessage().formatted(getLocales()));
});
}
private void handleJoinLocation(PlayerSpawnLocationEvent event) {
if (!config.getEnableJoinDestination()) {
Logging.finer("JoinDestination is disabled");
// User has disabled the feature in config
return;
}
if (config.getJoinDestination().isBlank()) {
Logging.warning("Joindestination is enabled but no destination has been specified in config!");
return;
}
if (corePermissionsChecker.hasJoinLocationBypassPermission(event.getPlayer())) {
Logging.finer("Player %s has bypass permission for JoinDestination", event.getPlayer().getName());
return;
}
Logging.finer("JoinDestination is " + config.getJoinDestination());
destinationsProvider.parseDestination(config.getJoinDestination())
.map(destination -> destination.getLocation(event.getPlayer())
.peek(event::setSpawnLocation)
.onEmpty(() -> Logging.warning("The destination in JoinDestination in config is invalid")))
.onFailure(failure -> {
Logging.warning("Invalid destination in JoinDestination in config: %s");
Logging.warning(failure.getFailureMessage().formatted(getLocales()));
});
}
};
private void handleJoinLocation(Player player) {
if (!config.getEnableJoinDestination()) {
Logging.finer("JoinDestination is disabled");
// User has disabled the feature in config
return;
}
if (config.getJoinDestination().isBlank()) {
Logging.warning("Joindestination is enabled but no destination has been specified in config!");
return;
}
if (corePermissionsChecker.hasJoinLocationBypassPermission(player)) {
Logging.finer("Player %s has bypass permission for JoinDestination", player.getName());
return;
}
Logging.finer("JoinDestination is " + config.getJoinDestination());
destinationsProvider.parseDestination(config.getJoinDestination())
.peek(destination -> teleportToDestinationOnJoin(player, destination))
.onFailure(failure -> {
Logging.warning("Invalid destination in JoinDestination in config: %s");
Logging.warning(failure.getFailureMessage().formatted(getLocales()));
});
}
private void teleportToDestinationOnJoin(Player player, DestinationInstance<?, ?> destination) {
asyncSafetyTeleporter.to(destination).teleportSingle(player)
.onSuccess(result -> Logging.fine("Player %s has been teleported on join",
player.getName()))
.onFailure(failure -> Logging.warning("Failed to teleport player %s on join: %s",
player.getName(), failure.getFirst()));
}
/**
@@ -271,7 +269,7 @@ final class MVPlayerListener implements CoreListener {
*/
@EventMethod
@DefaultEventPriority(EventPriority.MONITOR)
public void playerChangedWorld(PlayerChangedWorldEvent event) {
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());
@@ -284,7 +282,7 @@ final class MVPlayerListener implements CoreListener {
@EventMethod
@EventPriorityKey("mvcore-player-teleport")
@DefaultEventPriority(EventPriority.HIGHEST)
public void playerTeleport(PlayerTeleportEvent event) {
void playerTeleport(PlayerTeleportEvent event) {
Logging.finer("Got teleport event for player '"
+ event.getPlayer().getName() + "' with cause '" + event.getCause() + "'");
if (event.isCancelled()) {
@@ -355,7 +353,7 @@ final class MVPlayerListener implements CoreListener {
*/
@EventMethod
@DefaultEventPriority(EventPriority.LOWEST)
public void playerPortalCheck(PlayerPortalEvent event) {
void playerPortalCheck(PlayerPortalEvent event) {
if (event.isCancelled()) {
return;
}
@@ -381,7 +379,7 @@ final class MVPlayerListener implements CoreListener {
@EventMethod
@EventPriorityKey("mvcore-player-portal")
@DefaultEventPriority(EventPriority.HIGH)
public void playerPortal(PlayerPortalEvent event) {
void playerPortal(PlayerPortalEvent event) {
if (event.isCancelled()) {
return;
}
@@ -12,7 +12,6 @@ import jakarta.inject.Inject;
import org.bukkit.Material;
import org.bukkit.PortalType;
import org.bukkit.block.BlockState;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.Action;
import org.bukkit.event.entity.EntityPortalEvent;
import org.bukkit.event.player.PlayerInteractEvent;
@@ -48,7 +47,7 @@ final class MVPortalListener implements CoreListener {
*/
@EventMethod
@IgnoreIfCancelled
public void portalCreate(PortalCreateEvent event) {
void portalCreate(PortalCreateEvent event) {
Logging.fine("Attempting to create portal at '%s' with reason: %s",
event.getWorld().getName(), event.getReason());
@@ -93,7 +92,7 @@ final class MVPortalListener implements CoreListener {
*/
@EventMethod
@IgnoreIfCancelled
public void playerInteract(PlayerInteractEvent event) {
void playerInteract(PlayerInteractEvent event) {
if (isCreateEndPortalInteraction(event)) {
return;
}
@@ -122,7 +121,7 @@ final class MVPortalListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
public void entityPortal(EntityPortalEvent event) {
void entityPortal(EntityPortalEvent event) {
if (event.isCancelled() || event.getTo() == null) {
return;
}
@@ -9,7 +9,6 @@ package org.mvplugins.multiverse.core.listeners;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.bukkit.event.EventHandler;
import org.bukkit.event.weather.ThunderChangeEvent;
import org.bukkit.event.weather.WeatherChangeEvent;
import org.jvnet.hk2.annotations.Service;
@@ -35,7 +34,7 @@ final class MVWeatherListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
public void weatherChange(WeatherChangeEvent event) {
void weatherChange(WeatherChangeEvent event) {
if (event.isCancelled() || !event.toWeatherState()) {
return;
}
@@ -54,7 +53,7 @@ final class MVWeatherListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
public void thunderChange(ThunderChangeEvent event) {
void thunderChange(ThunderChangeEvent event) {
if (event.isCancelled() || !event.toThunderState()) {
return;
}
@@ -346,6 +346,11 @@ public enum MVCorei18n implements MessageKeyProvider {
EXCEPTION_MULTIVERSEWORLD_UNLOADPLAYERSINWORLD,
EXCEPTION_MULTIVERSEWORLD_UNLOADERROR,
// multiverse position parse exception
EXCEPTION_POSITIONPARSE_INVALIDDIRECTION,
EXCEPTION_POSITIONPARSE_INVALIDCOORDINATES,
EXCEPTION_POSITIONPARSE_INVALIDNUMBER,
// generic
GENERIC_SUCCESS,
GENERIC_FAILURE,
@@ -10,18 +10,16 @@ import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.ApiStatus;
import org.jvnet.hk2.annotations.Contract;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.MultiverseCoreApi;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.MultiverseCommand;
import org.mvplugins.multiverse.core.commands.CoreCommand;
import org.mvplugins.multiverse.core.dynamiclistener.DynamicListener;
import org.mvplugins.multiverse.core.dynamiclistener.DynamicListenerRegistration;
import org.mvplugins.multiverse.core.inject.PluginServiceLocator;
import org.mvplugins.multiverse.core.inject.PluginServiceLocatorFactory;
import org.mvplugins.multiverse.core.inject.binder.PluginBinder;
import org.mvplugins.multiverse.core.listeners.CoreListener;
import org.mvplugins.multiverse.core.utils.REPatterns;
import java.util.Locale;
/**
* Common plugin class for all Multiverse plugins.
*/
@@ -151,7 +149,7 @@ public abstract class MultiverseModule extends JavaPlugin {
.andThen(pluginLocales -> {
pluginLocales.addFileResClassLoader(this);
pluginLocales.addBundleClassLoader(this.getClassLoader());
pluginLocales.addMessageBundles(this.getDescription().getName().toLowerCase());
pluginLocales.addMessageBundles(this.getName().toLowerCase(Locale.ENGLISH));
})
.onFailure(e -> {
Logging.severe("Failed to register locales");
@@ -228,8 +228,8 @@ public final class LocationManipulation {
if (orientation == null) {
return 0;
}
if (ORIENTATION_INTS.containsKey(orientation.toLowerCase())) {
return ORIENTATION_INTS.get(orientation.toLowerCase());
if (ORIENTATION_INTS.containsKey(orientation.toLowerCase(Locale.ENGLISH))) {
return ORIENTATION_INTS.get(orientation.toLowerCase(Locale.ENGLISH));
}
return 0;
}
@@ -77,7 +77,7 @@ public final class FileUtils {
*/
public @Nullable File getServerProperties() {
if (this.serverProperties == null) {
this.serverProperties = findFileFromServerDirectory("server.properties");
this.serverProperties = findFileFromServerDirectory(config.getServerPropertiesPath());
Logging.finer("server.properties: %s", this.serverProperties);
}
return this.serverProperties;
@@ -0,0 +1,150 @@
package org.mvplugins.multiverse.core.utils.position;
import org.bukkit.Location;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.utils.REPatterns;
/**
* Represents a position for an entity in 3D space, including both coordinates and facing direction.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public class EntityPosition {
/**
* Creates an EntityPosition with absolute coordinates and direction.
*
* @param x The absolute X coordinate.
* @param y The absolute Y coordinate.
* @param z The absolute Z coordinate.
* @param pitch The absolute pitch (vertical angle).
* @param yaw The absolute yaw (horizontal angle).
* @return A new EntityPosition instance with the specified absolute coordinates and direction.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static EntityPosition ofAbsolute(double x, double y, double z, double pitch, double yaw) {
return new EntityPosition(
VectorPosition.ofAbsolute(x, y, z),
FaceDirection.ofAbsolute(pitch, yaw)
);
}
/**
* Creates an EntityPosition from a Bukkit Location with absolute coordinates and direction.
*
* @param location The Bukkit Location to convert.
* @return A new EntityPosition instance representing the given location.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static EntityPosition ofLocation(Location location) {
return new EntityPosition(VectorPosition.ofLocation(location), FaceDirection.ofLocation(location));
}
/**
* Parses an EntityPosition from a string representation.
* The expected format is "&lt;x&gt;,&lt;y&gt;,&lt;z&gt;:&lt;pitch&gt;:&lt;yaw&gt;" for absolute coordinates and direction,
* or "&lt;x&gt;,&lt;y&gt;,&lt;z&gt;" for absolute coordinates with default direction (0 pitch, 0 yaw).
* <br>
* Relative coordinates and direction can be specified using the '~' prefix, e.g., "~10,~,~-10:0:90".
*
* @param positionStr The string representation of the position.
* @return A new EntityPosition instance parsed from the string.
* @throws PositionParseException If the string format is invalid.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static EntityPosition fromString(String positionStr) throws PositionParseException {
String[] parts = REPatterns.COLON.split(positionStr, 2);
return parts.length == 2
? new EntityPosition(VectorPosition.fromString(parts[0]), FaceDirection.fromString(parts[1]))
: new EntityPosition(VectorPosition.fromString(parts[0]), FaceDirection.ofAbsolute(0, 0));
}
private final VectorPosition vector;
private final FaceDirection direction;
/**
* Creates a new EntityPosition with the specified vector and direction.
*
* @param vector The vector position (coordinates).
* @param direction The facing direction (pitch and yaw).
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public EntityPosition(VectorPosition vector, FaceDirection direction) {
this.vector = vector;
this.direction = direction;
}
/**
* Gets the vector position (coordinates).
*
* @return The vector position.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public VectorPosition getVector() {
return vector;
}
/**
* Gets the facing direction (pitch and yaw).
*
* @return The facing direction.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public FaceDirection getDirection() {
return direction;
}
/**
* Augments a given Bukkit Location by applying the relative components of this EntityPosition.
* This modifies the provided Location in place.
*
* @param base The base Bukkit Location to augment and offset for relative positioning as required.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public void augmentBukkitLocation(Location base) {
vector.augmentBukkitLocation(base);
direction.augmentBukkitLocation(base);
}
/**
* Converts this EntityPosition to a new Bukkit Location based on a given base Location.
* This does not modify the base Location, but returns a new Location instance.
*
* @param base The base Bukkit Location to use as a reference for relative positioning as required.
* @return A new Bukkit Location representing this EntityPosition.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public Location toBukkitLocation(Location base) {
return new Location(
base.getWorld(),
vector.getX().getValue(base.getX()),
vector.getY().getValue(base.getY()),
vector.getZ().getValue(base.getZ()),
(float) direction.getYaw().getValue(base.getYaw()),
(float) direction.getPitch().getValue(base.getPitch())
);
}
@Override
public String toString() {
return vector + ":" + direction;
}
}
@@ -0,0 +1,138 @@
package org.mvplugins.multiverse.core.utils.position;
import org.bukkit.Location;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.REPatterns;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Represents a direction to face, defined by pitch and yaw.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public class FaceDirection {
/**
* Creates a FaceDirection with absolute pitch and yaw.
*
* @param pitch The absolute pitch (vertical angle).
* @param yaw The absolute yaw (horizontal angle).
* @return A new FaceDirection instance with the specified absolute pitch and yaw.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static FaceDirection ofAbsolute(double pitch, double yaw) {
return new FaceDirection(
PositionNumber.ofAbsolute(pitch),
PositionNumber.ofAbsolute(yaw)
);
}
/**
* Gets the pitch and yaw from a Bukkit Location to create an FaceDirection with absolute values.
*
* @param location The Bukkit Location to convert.
* @return A new FaceDirection instance representing the direction of the given location.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static FaceDirection ofLocation(org.bukkit.Location location) {
return new FaceDirection(
PositionNumber.ofAbsolute(location.getPitch()),
PositionNumber.ofAbsolute(location.getYaw())
);
}
/**
* Parses a FaceDirection from a string representation.
* The expected format is "&lt;pitch&gt;:&lt;yaw&gt;" for absolute pitch and yaw.
* <br>
* Relative pitch and yaw can be specified using the '~' prefix, e.g., "~:~5".
*
* @param directionStr The string representation of the direction.
* @return A new FaceDirection instance parsed from the string.
* @throws PositionParseException If the string format is invalid.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static FaceDirection fromString(String directionStr) throws PositionParseException {
String[] parts = REPatterns.COLON.split(directionStr, 2);
if (parts.length != 2) {
throw new PositionParseException(Message.of(MVCorei18n.EXCEPTION_POSITIONPARSE_INVALIDDIRECTION,
replace("{format}").with(directionStr)));
}
//TODO: Add support for compass directions (N, S, E, W, NE, NW, SE, SW) for yaw
PositionNumber pitch = PositionNumber.fromString(parts[0]);
PositionNumber yaw = PositionNumber.fromString(parts[1]);
return new FaceDirection(pitch, yaw);
}
private final PositionNumber pitch;
private final PositionNumber yaw;
/**
* Creates a new FaceDirection with the specified pitch and yaw.
*
* @param pitch The pitch (vertical angle).
* @param yaw The yaw (horizontal angle).
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public FaceDirection(PositionNumber pitch, PositionNumber yaw) {
this.pitch = pitch;
this.yaw = yaw;
}
/**
* Gets the pitch component of this FaceDirection.
*
* @return The pitch number representation.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getPitch() {
return pitch;
}
/**
* Gets the yaw component of this FaceDirection.
*
* @return The yaw number representation.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getYaw() {
return yaw;
}
/**
* Applies this FaceDirection to a given Bukkit Location, modifying its pitch and yaw.
* <br>
* Relative pitch and yaw will be offset based on the current values in the Location.
*
* @param base The Bukkit Location to modify.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public void augmentBukkitLocation(Location base) {
base.setPitch((float) pitch.getValue(base.getPitch()));
base.setYaw((float) yaw.getValue(base.getYaw()));
}
@Override
public String toString() {
return pitch + ":" + yaw;
}
}
@@ -0,0 +1,187 @@
package org.mvplugins.multiverse.core.utils.position;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Represents a number that can be either absolute or relative (prefixed with '~').
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public sealed interface PositionNumber permits PositionNumber.Relative, PositionNumber.Absolute {
/**
* Creates an absolute PositionNumber. Absolute numbers represent fixed coordinates that will not be adjusted
* based on any base value.
*
* @param value the absolute value
* @return a PositionNumber representing the absolute value
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
static PositionNumber ofAbsolute(double value) {
return new Absolute(value);
}
/**
* Creates a relative PositionNumber. Relative numbers are prefixed with '~' and represent an offset
* from a base value. For example, a relative PositionNumber of '~5' means 5 units more than the base value,
* while '~ -3' means 3 units less than the base value.
*
* @param value the relative offset value
* @return a PositionNumber representing the relative offset
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
static PositionNumber ofRelative(double value) {
return new Relative(value);
}
/**
* Parses a PositionNumber from a string. The string can represent either an absolute number (e.g., "10.5")
* or a relative number prefixed with '~' (e.g., "~5" or "~ -3"). If the string is "~" with no number,
* it is treated as a relative value of 0.
*
* @param string the string to parse
* @return a PositionNumber representing the parsed value.
* @throws PositionParseException If the string format is invalid.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
static PositionNumber fromString(String string) throws PositionParseException {
if (string.startsWith("~")) {
if (string.length() == 1) {
return new Relative(0);
}
return new Relative(tryParseDouble(string.substring(1)));
}
return new Absolute(tryParseDouble(string));
}
private static double tryParseDouble(String str) throws PositionParseException {
return Try.of(() -> Double.parseDouble(str))
.getOrElseThrow(throwable -> new PositionParseException(
Message.of(MVCorei18n.EXCEPTION_POSITIONPARSE_INVALIDNUMBER,
replace("{number}").with(str))));
}
/**
* Calculates the effective value based on the base value.
* For absolute PositionNumbers, this returns the stored value.
* For relative PositionNumbers, this returns base + stored value.
*
* @param base the base value to use for relative calculations
* @return the calculated effective value
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
double getValue(double base);
/**
* Checks if this PositionNumber is relative.
*
* @return true if this is a relative PositionNumber, false if absolute
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
boolean isRelative();
/**
* Checks if this PositionNumber is absolute.
*
* @return true if this is an absolute PositionNumber, false if relative
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
boolean isAbsolute();
/**
* Gets the raw stored value without any base adjustment.
*
* @return the raw value (for relative, this is the offset; for absolute, this is the fixed value)
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
double getRawValue();
final class Relative implements PositionNumber {
private final double value;
Relative(double value) {
this.value = value;
}
@Override
public double getValue(double base) {
return base + value;
}
@Override
public boolean isRelative() {
return true;
}
@Override
public boolean isAbsolute() {
return false;
}
@Override
public double getRawValue() {
return value;
}
@Override
public String toString() {
return "~" + value;
}
}
final class Absolute implements PositionNumber {
private final double value;
Absolute(double value) {
this.value = value;
}
@Override
public double getValue(double base) {
return value;
}
@Override
public boolean isRelative() {
return false;
}
@Override
public boolean isAbsolute() {
return true;
}
@Override
public double getRawValue() {
return value;
}
@Override
public String toString() {
return Double.toString(value);
}
}
}
@@ -0,0 +1,227 @@
package org.mvplugins.multiverse.core.utils.position;
import org.bukkit.Location;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.REPatterns;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Represents an x, y, z position in 3D space, with support for absolute and relative coordinates.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public class VectorPosition {
/**
* Creates a VectorPosition with absolute coordinates.
* @param x The absolute x coordinate.
* @param y The absolute y coordinate.
* @param z The absolute z coordinate.
* @return A new VectorPosition instance with the specified absolute coordinates.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static VectorPosition ofAbsolute(double x, double y, double z) {
return new VectorPosition(
PositionNumber.ofAbsolute(x),
PositionNumber.ofAbsolute(y),
PositionNumber.ofAbsolute(z)
);
}
/**
* Creates a VectorPosition from a Bukkit Vector, using absolute coordinates.
*
* @param vector The Bukkit Vector to convert.
* @return A new VectorPosition instance representing the given vector.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static VectorPosition ofVector(Vector vector) {
return new VectorPosition(
PositionNumber.ofAbsolute(vector.getX()),
PositionNumber.ofAbsolute(vector.getY()),
PositionNumber.ofAbsolute(vector.getZ())
);
}
/**
* Creates a VectorPosition from a Bukkit Location, using absolute coordinates.
*
* @param location The Bukkit Location to convert.
* @return A new VectorPosition instance representing the given location.
*
* @since 5.3
*/
public static VectorPosition ofLocation(Location location) {
return new VectorPosition(
PositionNumber.ofAbsolute(location.getX()),
PositionNumber.ofAbsolute(location.getY()),
PositionNumber.ofAbsolute(location.getZ())
);
}
/**
* Parses a VectorPosition from a string representation.
* The expected format is "&lt;x&gt;,&lt;y&gt;,&lt;z&gt;" for absolute or relative coordinates.
* <br>
* Relative coordinates can be specified using the '~' prefix, e.g., "~10,~,~-10".
*
* @param coordStr The string representation of the coordinates.
* @return A new VectorPosition instance parsed from the string.
* @throws PositionParseException If the string format is invalid.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static VectorPosition fromString(String coordStr) throws PositionParseException {
String[] parts = REPatterns.COMMA.split(coordStr);
if (parts.length != 3) {
throw new PositionParseException(Message.of(MVCorei18n.EXCEPTION_POSITIONPARSE_INVALIDCOORDINATES,
replace("{format}").with(coordStr)));
}
return new VectorPosition(
PositionNumber.fromString(parts[0]),
PositionNumber.fromString(parts[1]),
PositionNumber.fromString(parts[2])
);
}
private final PositionNumber x;
private final PositionNumber y;
private final PositionNumber z;
/**
* Creates a new VectorPosition with the specified PositionNumbers for x, y, and z.
*
* @param x The PositionNumber for the x coordinate.
* @param y The PositionNumber for the y coordinate.
* @param z The PositionNumber for the z coordinate.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public VectorPosition(PositionNumber x, PositionNumber y, PositionNumber z) {
this.x = x;
this.y = y;
this.z = z;
}
/**
* Gets the PositionNumber for the x coordinate.
*
* @return The PositionNumber representing the x coordinate.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getX() {
return x;
}
/**
* Gets the PositionNumber for the y coordinate.
*
* @return The PositionNumber representing the y coordinate.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getY() {
return y;
}
/**
* Gets the PositionNumber for the z coordinate.
*
* @return The PositionNumber representing the z coordinate.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getZ() {
return z;
}
/**
* Augments the given Bukkit Vector in place by applying this VectorPosition's coordinates.
* Relative coordinates will adjust the existing values, while absolute coordinates will set them directly.
*
* @param base The Bukkit Vector to augment.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public void augmentBukkitVector(Vector base) {
base.setX(x.getValue(base.getX()));
base.setY(y.getValue(base.getY()));
base.setZ(z.getValue(base.getZ()));
}
/**
* Augments the given Bukkit Location in place by applying this VectorPosition's coordinates.
* Relative coordinates will adjust the existing values, while absolute coordinates will set them directly.
*
* @param location The Bukkit Location to augment.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public void augmentBukkitLocation(Location location) {
location.setX(x.getValue(location.getX()));
location.setY(y.getValue(location.getY()));
location.setZ(z.getValue(location.getZ()));
}
/**
* Converts this VectorPosition to a new Bukkit Vector based on a given base Vector.
* This does not modify the base Vector, but returns a new Vector instance.
*
* @param base The base Bukkit Vector to use as a reference for relative positioning as required.
* @return A new Bukkit Vector representing this VectorPosition.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public Vector toBukkitVector(Vector base) {
return new Vector(
x.getValue(base.getX()),
y.getValue(base.getY()),
z.getValue(base.getZ())
);
}
/**
* Converts this VectorPosition to a new Bukkit Location based on a given base Location.
* This does not modify the base Location, but returns a new Location instance.
*
* @param base The base Bukkit Location to use as a reference for relative positioning as required.
* @return A new Bukkit Location representing this VectorPosition.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public Location toBukkitLocation(Location base) {
return new Location(
base.getWorld(),
x.getValue(base.getX()),
y.getValue(base.getY()),
z.getValue(base.getZ()),
base.getYaw(),
base.getPitch()
);
}
@Override
public String toString() {
return x + "," + y + "," + z;
}
}
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.world;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
import com.dumptruckman.minecraft.util.Logging;
@@ -417,7 +418,7 @@ final class WorldConfig {
if (alias.get().isEmpty()) return;
Try.of(() -> Enum.valueOf(EnglishChatColor.class, color.toUpperCase()))
Try.of(() -> Enum.valueOf(EnglishChatColor.class, color.toUpperCase(Locale.ENGLISH)))
.map(c -> c.color)
.onSuccess(c -> {
if (c != ChatColor.WHITE) {
@@ -425,7 +426,7 @@ final class WorldConfig {
}
});
Try.of(() -> Enum.valueOf(EnglishChatStyle.class, style.toUpperCase()))
Try.of(() -> Enum.valueOf(EnglishChatStyle.class, style.toUpperCase(Locale.ENGLISH)))
.map(c -> c.color)
.onSuccess(s -> {
if (s != null) {
@@ -16,6 +16,7 @@ import org.bukkit.configuration.MemoryConfiguration;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
import org.mvplugins.multiverse.core.event.world.MVWorldPropertyChangedEvent;
import org.mvplugins.multiverse.core.config.node.ConfigNode;
@@ -37,11 +38,13 @@ final class WorldConfigNodes {
private final NodeGroup nodes = new NodeGroup();
private WorldManager worldManager;
private EnforcementHandler enforcementHandler;
private CoreConfig config;
private MultiverseWorld world = null;
WorldConfigNodes(@NotNull MultiverseCore multiverseCore) {
this.worldManager = multiverseCore.getServiceLocator().getService(WorldManager.class);
this.enforcementHandler = multiverseCore.getServiceLocator().getService(EnforcementHandler.class);
this.config = multiverseCore.getServiceLocator().getService(CoreConfig.class);
}
MultiverseWorld getWorld() {
@@ -244,7 +247,7 @@ final class WorldConfigNodes {
}));
final ConfigNode<EntitySpawnConfig> enititySpawnConfig = node(ConfigNode.builder("spawning", EntitySpawnConfig.class)
.defaultValue(() -> EntitySpawnConfig.fromSection(new MemoryConfiguration()))
.defaultValue(() -> EntitySpawnConfig.fromSection(config, new MemoryConfiguration()))
.hidden()
.serializer(new NodeSerializer<>() {
@Override
@@ -252,7 +255,7 @@ final class WorldConfigNodes {
ConfigurationSection spawnSection = (object instanceof ConfigurationSection section)
? section
: new MemoryConfiguration();
return EntitySpawnConfig.fromSection(spawnSection);
return EntitySpawnConfig.fromSection(config, spawnSection);
}
@Override
@@ -370,12 +370,20 @@ public final class WorldManager {
private LoadedMultiverseWorld newLoadedMultiverseWorld(
@NotNull World world, @Nullable String generator, @Nullable String biome, boolean adjustSpawn) {
WorldConfig worldConfig = worldsConfigManager.addWorldConfig(world.getName());
// Properties from multiverse input
worldConfig.setAdjustSpawn(adjustSpawn);
worldConfig.setGenerator(generator == null ? "" : generator);
worldConfig.setBiome(biome == null ? "" : biome);
// Properties from the bukkit world
worldConfig.setDifficulty(world.getDifficulty());
worldConfig.setKeepSpawnInMemory(world.getKeepSpawnInMemory());
worldConfig.setScale(world.getCoordinateScale());
worldConfig.save();
MultiverseWorld mvWorld = newMultiverseWorld(world.getName(), worldConfig);
newMultiverseWorld(world.getName(), worldConfig);
LoadedMultiverseWorld loadedWorld = new LoadedMultiverseWorld(
world,
worldConfig,
@@ -384,22 +392,12 @@ public final class WorldManager {
locationManipulation,
entityPurger
);
setDefaultEnvironmentScale(mvWorld);
loadedWorldsMap.put(loadedWorld.getName(), loadedWorld);
saveWorldsConfig();
pluginManager.callEvent(new MVWorldLoadedEvent(loadedWorld));
return loadedWorld;
}
private void setDefaultEnvironmentScale(MultiverseWorld world) {
double scale = switch (world.getEnvironment()) {
case NETHER -> 8.0;
case THE_END -> 16.0;
default -> 1.0;
};
world.setScale(scale);
}
/**
* Loads an existing world in config.
*
@@ -649,12 +647,18 @@ public final class WorldManager {
*/
public Attempt<String, DeleteFailureReason> deleteWorld(@NotNull DeleteWorldOptions options) {
return getLoadedWorld(options.world()).fold(
() -> loadWorld(LoadWorldOptions.world(options.world()))
.transform(DeleteFailureReason.LOAD_FAILED)
.mapAttempt(world -> doDeleteWorld(world, options)),
() -> loadThenDeleteWorld(options),
world -> doDeleteWorld(world, options));
}
private Attempt<String, DeleteFailureReason> loadThenDeleteWorld(@NotNull DeleteWorldOptions options) {
return loadWorld(LoadWorldOptions.world(options.world()))
.fold(
ignore -> worldActionResult(DeleteFailureReason.LOAD_FAILED, options.world().getName()),
loadedWorld -> doDeleteWorld(loadedWorld, options)
);
}
/**
* Deletes an existing multiverse world entirely. Warning: This will delete all world files.
*
@@ -19,6 +19,7 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
/**
* Manages the worlds.yml file.
@@ -81,8 +82,9 @@ final class WorldsConfigManager {
}
// Copy old config file to `worlds.yml.old`
Path oldWorldConfig = worldConfigFile.toPath().getParent().resolve(CONFIG_FILENAME + ".old");
Files.copy(worldConfigFile.toPath(), oldWorldConfig, COPY_ATTRIBUTES);
Path oldWorldConfig = worldConfigFile.toPath().getParent()
.resolve(CONFIG_FILENAME + ".old." + System.currentTimeMillis());
Files.copy(worldConfigFile.toPath(), oldWorldConfig, COPY_ATTRIBUTES, REPLACE_EXISTING);
return configData.replace("==: MVWorld", "w@: world")
.replace("==: MVSpawnSettings", "")
@@ -1,5 +1,7 @@
package org.mvplugins.multiverse.core.world.biomeprovider;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.block.Biome;
import org.bukkit.generator.BiomeProvider;
import org.jetbrains.annotations.NotNull;
@@ -7,6 +9,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
/**
* A parser for {@link SingleBiomeProvider}
@@ -17,13 +20,16 @@ final class SingleBiomeProviderParser implements BiomeProviderParser {
@Override
public BiomeProvider parseBiomeProvider(@NotNull String worldName, @NotNull String params) {
return new SingleBiomeProvider(Biome.valueOf(params.toUpperCase()));
NamespacedKey biomeKey = NamespacedKey.fromString(params.toLowerCase(Locale.ENGLISH));
return new SingleBiomeProvider(Registry.BIOME.get(biomeKey));
}
@Override
public Collection<String> suggestParams(@NotNull String currentInput) {
if (biomes == null) {
biomes = Arrays.stream(Biome.values()).map(biome -> biome.toString().toLowerCase()).toList();
biomes = Registry.BIOME.stream()
.map(biome -> biome.getKey().getKey().toLowerCase(Locale.ENGLISH))
.toList();
}
return biomes;
}
@@ -1,29 +1,36 @@
package org.mvplugins.multiverse.core.world.entity;
import com.dumptruckman.minecraft.util.Logging;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.MemoryConfiguration;
import org.bukkit.entity.Entity;
import org.bukkit.entity.SpawnCategory;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
public final class EntitySpawnConfig {
private final CoreConfig config;
private final Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig;
EntitySpawnConfig(Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig) {
EntitySpawnConfig(CoreConfig config, Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig) {
this.config = config;
this.spawnCategoriesConfig = spawnCategoriesConfig;
}
public SpawnCategoryConfig getSpawnCategoryConfig(SpawnCategory spawnCategory) {
return spawnCategoriesConfig.computeIfAbsent(spawnCategory,
computeSpawnCategory -> new SpawnCategoryConfig(computeSpawnCategory, new MemoryConfiguration()));
computeSpawnCategory -> new SpawnCategoryConfig(
config,
computeSpawnCategory,
new MemoryConfiguration()
));
}
public boolean shouldAllowSpawn(Entity entity) {
@@ -46,23 +53,23 @@ public final class EntitySpawnConfig {
public ConfigurationSection toSection() {
MemoryConfiguration section = new MemoryConfiguration();
spawnCategoriesConfig.forEach((spawnCategory, spawnCategoryConfig) -> {
section.set(spawnCategory.toString().toLowerCase(), spawnCategoryConfig.saveSection());
section.set(spawnCategory.toString().toLowerCase(Locale.ENGLISH), spawnCategoryConfig.saveSection());
});
return section;
}
@ApiStatus.Internal
public static EntitySpawnConfig fromSection(ConfigurationSection section) {
Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig = new HashMap<>();
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;
}
SpawnCategory spawnCategory = SpawnCategory.valueOf(key.toUpperCase());
spawnCategoriesConfig.put(spawnCategory, new SpawnCategoryConfig(spawnCategory, sectionPart));
SpawnCategory spawnCategory = SpawnCategory.valueOf(key.toUpperCase(Locale.ENGLISH));
spawnCategoriesConfig.put(spawnCategory, new SpawnCategoryConfig(config, spawnCategory, sectionPart));
});
return new EntitySpawnConfig(spawnCategoriesConfig);
return new EntitySpawnConfig(config, spawnCategoriesConfig);
}
@ApiStatus.Internal
@@ -7,6 +7,7 @@ import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.SpawnCategory;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.config.handle.MemoryConfigurationHandle;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.config.node.ConfigNode;
@@ -21,6 +22,7 @@ import java.util.List;
public final class SpawnCategoryConfig {
private final CoreConfig config;
private final SpawnCategory spawnCategory;
private final MemoryConfigurationHandle handle;
private final StringPropertyHandle stringPropertyHandle;
@@ -28,7 +30,8 @@ public final class SpawnCategoryConfig {
private MultiverseWorld world;
SpawnCategoryConfig(SpawnCategory spawnCategory, ConfigurationSection section) {
SpawnCategoryConfig(CoreConfig config, SpawnCategory spawnCategory, ConfigurationSection section) {
this.config = config;
this.spawnCategory = spawnCategory;
this.nodes = new Nodes();
this.handle = MemoryConfigurationHandle.builder(section, nodes.nodes)
@@ -57,23 +60,39 @@ public final class SpawnCategoryConfig {
return;
}
loadedWorld.getBukkitWorld().peek(bukkitWorld -> {
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());
}
Logging.finer("World %s %s setSpawnLimit: %d", world.getName(), spawnCategory, getSpawnLimit());
bukkitWorld.setSpawnLimit(spawnCategory, getSpawnLimit());
applyTickPerSpawns(bukkitWorld);
applySpawnLimit(bukkitWorld);
});
}
private void applyTickPerSpawns(World bukkitWorld) {
if (!config.getApplyEntitySpawnLimit()) {
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());
}
}
private void applySpawnLimit(World bukkitWorld) {
if (!config.getApplyEntitySpawnLimit()) {
Logging.finer("Skipping World %s %s setSpawnLimit due to core config", world.getName(), spawnCategory);
return;
}
Logging.finer("World %s %s setSpawnLimit: %d", world.getName(), spawnCategory, getSpawnLimit());
bukkitWorld.setSpawnLimit(spawnCategory, getSpawnLimit());
}
public StringPropertyHandle getStringPropertyHandle() {
return stringPropertyHandle;
}
@@ -27,6 +27,7 @@ import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.utils.FileUtils;
import org.mvplugins.multiverse.core.utils.REPatterns;
@@ -38,15 +39,17 @@ import org.mvplugins.multiverse.core.utils.REPatterns;
public final class GeneratorProvider implements Listener {
private final Map<String, String> defaultGenerators;
private final Map<String, GeneratorPlugin> generatorPlugins;
private final CoreConfig coreConfig;
private final FileUtils fileUtils;
@Inject
GeneratorProvider(@NotNull MultiverseCore multiverseCore, @NotNull FileUtils fileUtils) {
GeneratorProvider(@NotNull MultiverseCore plugin, @NotNull CoreConfig coreConfig, @NotNull FileUtils fileUtils) {
this.coreConfig = coreConfig;
this.fileUtils = fileUtils;
defaultGenerators = new HashMap<>();
generatorPlugins = new HashMap<>();
Bukkit.getPluginManager().registerEvents(this, multiverseCore);
Bukkit.getPluginManager().registerEvents(this, plugin);
loadDefaultWorldGenerators();
loadPluginGenerators();
}
@@ -73,11 +76,14 @@ public final class GeneratorProvider implements Listener {
* Find generator plugins from plugins loaded and register them.
*/
private void loadPluginGenerators() {
Arrays.stream(Bukkit.getPluginManager().getPlugins()).forEach(plugin -> {
if (!coreConfig.getAutoDetectGeneratorPlugins()) {
return;
}
for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) {
if (testIsGeneratorPlugin(plugin)) {
registerGeneratorPlugin(new SimpleGeneratorPlugin(plugin.getName()));
}
});
}
}
/**
@@ -88,14 +94,23 @@ public final class GeneratorProvider implements Listener {
*/
private boolean testIsGeneratorPlugin(Plugin plugin) {
String worldName = Bukkit.getWorlds().stream().findFirst().map(World::getName).orElse("world");
return Try.of(() -> plugin.getDefaultWorldGenerator(worldName, "") != null)
.recover(IllegalArgumentException.class, true)
.recover(throwable -> {
Logging.warning("Plugin %s threw an exception when testing if it is a generator plugin!",
plugin.getName());
throwable.printStackTrace();
return false;
}).getOrElse(false);
try {
return plugin.getDefaultWorldGenerator(worldName, "") != null;
} catch (UnsupportedOperationException e) {
// Some plugins throw this if they don't support world generation
return false;
} catch (Throwable t) {
Logging.warning("Plugin %s threw an exception when testing if it is a generator plugin! ",
plugin.getName());
Logging.warning("Error by plugin %s: %s", plugin.getName(), t.getMessage());
Logging.warning("This is NOT a bug in Multiverse. Do NOT report this to Multiverse support. " +
"Enable debug mode with `/mv debug 1` to see the full stack trace.");
if (coreConfig.getGlobalDebug() >= 1) {
t.printStackTrace();
}
// Assume it's a generator plugin since it tried to do something, most likely the id is wrong.
return true;
}
}
/**
@@ -206,6 +221,9 @@ public final class GeneratorProvider implements Listener {
*/
@EventHandler
private void onPluginEnable(PluginEnableEvent event) {
if (!coreConfig.getAutoDetectGeneratorPlugins()) {
return;
}
if (!testIsGeneratorPlugin(event.getPlugin())) {
Logging.finest("Plugin %s is not a generator plugin.", event.getPlugin().getName());
return;
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.world.helpers;
import java.io.File;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
@@ -130,7 +131,7 @@ public final class WorldNameChecker {
* @return True if it looks like a world, else false.
*/
private boolean folderHasDat(@NotNull File worldFolder) {
File[] files = worldFolder.listFiles((file, name) -> name.toLowerCase().endsWith(".dat"));
File[] files = worldFolder.listFiles((file, name) -> name.toLowerCase(Locale.ENGLISH).endsWith(".dat"));
return files != null && files.length > 0;
}
@@ -22,13 +22,13 @@ import java.util.Objects;
* This is useful to store location with world that may not be loaded yet or have been unloaded at some point.
*/
@SerializableAs("UnloadedWorldLocation")
public final class UnloadedWorldLocation extends Location {
public class UnloadedWorldLocation extends Location {
public static UnloadedWorldLocation fromLocation(@NotNull Location location) {
return new UnloadedWorldLocation(location.getWorld(), location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
}
private String worldName;
private @Nullable String worldName;
public UnloadedWorldLocation(@Nullable String worldName, double x, double y, double z) {
super(null, x, y, z);
@@ -67,7 +67,7 @@ public final class UnloadedWorldLocation extends Location {
this.worldName = worldName;
}
public String getWorldName() {
public @Nullable String getWorldName() {
return worldName;
}
@@ -160,10 +160,10 @@ public final class UnloadedWorldLocation extends Location {
@Override
public int hashCode() {
int hash = 3;
hash = 19 * hash + worldName.hashCode();
hash = 19 * hash + (int) (Double.doubleToLongBits(this.getX()) ^ (Double.doubleToLongBits(this.getX()) >>> 32));
hash = 19 * hash + (int) (Double.doubleToLongBits(this.getY()) ^ (Double.doubleToLongBits(this.getY()) >>> 32));
hash = 19 * hash + (int) (Double.doubleToLongBits(this.getZ()) ^ (Double.doubleToLongBits(this.getZ()) >>> 32));
hash = 19 * hash + String.valueOf(worldName).hashCode();
hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.getX()));
hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.getY()));
hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.getZ()));
hash = 19 * hash + Float.floatToIntBits(this.getPitch());
hash = 19 * hash + Float.floatToIntBits(this.getYaw());
return hash;
@@ -171,7 +171,7 @@ public final class UnloadedWorldLocation extends Location {
@Override
public String toString() {
return "Location{" +
return "UnloadedWorldLocation{" +
"world=" + worldName +
",x=" + getX() +
",y=" + getY() +
@@ -255,7 +255,7 @@ mv-core.destination.cannon.failurereason.invalidformat=&cCannon destination form
mv-core.destination.exact.failurereason.invalidformat=&cExact destination format is: &6e:worldname:x,y,z:pitch:yaw
mv-core.destination.player.failurereason.playernotfound=&cPlayer '&6{player}&c' does not exist or is not online!
mv-core.destination.shared.failurereason.invalidcoordinatesformat=You must specify a valid coordinate in this format: &6{x},{y},{z}
mv-core.destination.shared.failurereason.invalidnumberformat=Value is not a valid number. {error}.
mv-core.destination.shared.failurereason.invalidnumberformat=&cInvalid destination input. {error}
mv-core.destination.shared.failurereason.worldnotfound=&cWorld '&6{world}&c' does not exist or is not loaded!
mv-core.destination.parse.failurereason.invaliddestinationid=Invalid destination id: {id}. Valid destination ids are: {ids}
@@ -281,8 +281,8 @@ mv-core.createworld.worldexistunloaded=World '{world}' already exists, but it's
mv-core.createworld.worldexistloaded=World '{world}' already exists!
mv-core.deleteworld.worldnonexistent=World '{world}' not found!
mv-core.deleteworld.loadfailed=Unable to load world '{world}', does the world folder exist?&f You can run '&a/mv remove {world}&f' to remove it from Multiverse, or attempt to load again with '&a/mv load {world}&f'.
mv-core.deleteworld.worldfoldernotfound=World '{world}' folder not found!
mv-core.deleteworld.loadfailed=Unable to load world '{world}', is the world folder deleted or corrupted?&f You can run '&a/mv remove {world}&f' to remove it from Multiverse, or attempt to load again with '&a/mv load {world}&f'.
mv-core.deleteworld.worldfoldernotfound=&cUnable to locate '{world}' folder! You may '&a/mv remove {world}&c' to remove it from Multiverse, and manually delete the folder if it exists.
mv-core.deleteworld.failedtodeletefolder=Failed to delete world folder '{world}': {error}\n&fSee console for more details.
mv-core.importworld.invalidworldname=World '{world}' contains invalid characters!
@@ -327,6 +327,11 @@ mv-core.exception.multiverseworld.unloaddefaultworld=&cYou can't unload the defa
mv-core.exception.multiverseworld.unloadplayersinworld=&cThere are still &6{count}&c player(s) in the world! Use '&6--remove-players&c' flag to your command to teleport all players out of the world.
mv-core.exception.multiverseworld.unloaderror=&cAn unknown error occurred while unloading world: &6{world}&c.\n&cSee console for more details.
# multiverse position parse exception
mv-core.exception.positionparse.invaliddirection=&cInvalid direction string format: {format}. Expected format: <pitch>:<yaw>
mv-core.exception.positionparse.invalidcoordinates=&cInvalid coordinates format: {format}. Expected format: <x>,<y>,<z>
mv-core.exception.positionparse.invalidnumber=&cInvalid number format: {number}. Expects a numeric value.
# generic
mv-core.generic.success=Success!
mv-core.generic.failure=Failed!
@@ -69,6 +69,22 @@ class DestinationTest : TestWithMockBukkit() {
assertEquals("e:world:1.2,2.0,3.0:10.5:9.5", destination.toString())
}
@Test
fun `Exact destination instance with relative position`() {
val destination = destinationsProvider.parseDestination("e:world:~1.5,2,~-3:10.5:~9.5").orNull
assertTrue(destination is ExactDestinationInstance)
val expectedLocation = Location(
world.bukkitWorld.orNull,
player.location.x + 1.5,
2.0,
player.location.z - 3.0,
player.location.yaw + 9.5F,
10.5F
)
assertLocationEquals(expectedLocation, destination.getLocation(player).orNull)
assertEquals("e:world:~1.5,2.0,~-3.0:10.5:~9.5", destination.toString())
}
@Test
fun `Exact destination instance from location`() {
val exactDestination = serviceLocator.getActiveService(ExactDestination::class.java).takeIf { it != null } ?: run {
@@ -123,6 +139,7 @@ class DestinationTest : TestWithMockBukkit() {
assertTrue(destinationsProvider.parseDestination("b:invalid-bed").isFailure)
assertTrue(destinationsProvider.parseDestination("ca:invalid-cannon").isFailure)
assertTrue(destinationsProvider.parseDestination("e:world:1,2,x").isFailure)
assertTrue(destinationsProvider.parseDestination("e:world:1,2,3:").isFailure)
assertTrue(destinationsProvider.parseDestination("pl:invalid-player").isFailure)
assertTrue(destinationsProvider.parseDestination("w:invalid-world").isFailure)
// todo: should we make invalid yaw for WorldDestination fail?
@@ -4,6 +4,8 @@ world:
enforce-access: false
enforce-gamemode: true
enforce-flight: true
apply-entity-spawn-rate: true
apply-entity-spawn-limit: true
auto-purge-entities: false
world-name-format:
nether: '%overworld%_nether'
@@ -52,6 +54,8 @@ event-priority:
misc:
bukkit-yml-path: bukkit.yml
server-properties-path: server.properties
auto-detect-generator-plugins: true
global-debug: 0
debug-permissions: false
silent-start: false