Compare commits

...
100 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
Ben Woo 9926041c2f Merge pull request #3321 from KioProject123/fix/spawn-location-angle
fix spawn location angle
2025-08-18 15:06:10 +08:00
KioProject 1c99898d0d fix 2025-08-18 13:49:01 +08:00
KioProject 41e0261eee fix spawn location angle 2025-08-18 13:13:25 +08:00
Ben Woo 642f3ab1b8 Merge pull request #3320 from MrXiaoM/patch-1
Fix adventure dep in spigot-based server
2025-08-17 22:57:20 +08:00
人間工作 57d1844756 fix NoClassDefFoundError in spigot-based server 2025-08-17 22:09:17 +08:00
Ben Woo 73e1650fc3 Merge pull request #3318 from Multiverse/fix/match-invalid-regex
Properly handle error from invalid regex string in matcher
2025-08-11 13:08:49 +08:00
Ben Woo 800709ca95 Properly annotate parameters for matcher classes 2025-08-11 12:43:38 +08:00
Ben Woo 935cbad21c Properly handle error from invalid wildcard string in matcher 2025-08-11 12:40:05 +08:00
Ben Woo 0e2a27b298 Merge pull request #3317 from Multiverse/fix/remove-logging
Fix unwanted warning logging for matcher
2025-08-11 12:28:59 +08:00
Ben Woo baaa175423 Properly handle error from invalid regex string in matcher 2025-08-11 12:24:36 +08:00
Ben Woo e2b7a31191 Fix unwanted warning logging for matcher 2025-08-11 12:15:16 +08:00
Ben Woo afa2825bc7 Merge pull request #3315 from Multiverse/feat/load-folder-check
Add folder checking before loading world
2025-07-31 16:32:37 +08:00
Ben Woo 21b4f6b087 Add folder checking before loading world 2025-07-31 15:25:14 +08:00
Ben Woo a771652c8f Merge pull request #3291 from Multiverse/5.2
5.2
2025-07-28 10:11:52 +08:00
Ben Woo e89b4f19ee Merge pull request #3314 from Multiverse/fix/debug-permissions
Fix debug-permission comment spacing
2025-07-27 14:23:34 +08:00
Ben Woo eb8ef32a94 Fix debug-permission comment spacing 2025-07-27 12:38:50 +08:00
Ben Woo f9d783bc5c Merge pull request #3311 from Multiverse/refactor/remove-deprecate
Deprecate other removeWorld methods in favour of using RemoveWorldOpt…
2025-07-27 11:36:20 +08:00
Ben Woo 87dd4168cd Merge pull request #3313 from Multiverse/fix/save-flush
Use newer save method with chunk flushing when possible
2025-07-27 11:35:35 +08:00
Ben Woo 2da6ece606 Merge pull request #3312 from Multiverse/chore/bump-deps
Bump dependencies
2025-07-27 11:35:08 +08:00
Ben Woo cbb3a7a518 Merge pull request #3310 from Multiverse/fix/spawn-command
Fix spawn command using deprecated teleport method
2025-07-27 11:34:32 +08:00
Ben Woo 57796d295b Bump dependencies 2025-07-26 12:26:38 +08:00
Ben Woo 09314e8bce Use newer save method with chunk flushing when possible 2025-07-26 12:08:23 +08:00
Ben Woo 907a6dd5c0 Fix spawn command using deprecated teleport method 2025-07-26 11:50:53 +08:00
Ben Woo a50bd807b6 Deprecate other removeWorld methods in favour of using RemoveWorldOptions 2025-07-26 11:50:02 +08:00
Ben Woo 02081b4654 Merge pull request #3305 from Multiverse/fix/text-format-null
Fix ChatTextFormatter handling null strings
2025-07-26 10:44:56 +08:00
Ben Woo 156ec7911d Merge pull request #3308 from Multiverse/fix/world-name-dot
Fix v4 config migration when world name contains dot
2025-07-26 10:44:10 +08:00
Ben Woo 2b95c62a84 Fix v4 config migration when world name contains dot 2025-07-24 22:56:08 +08:00
Ben Woo 455396713e Fix ChatTextFormatter handling null strings 2025-07-21 22:44:45 +08:00
Ben Woo 8bc3a593d0 Merge pull request #3303 from Multiverse/feat/remove-options
Add options to remove a world without unloading or saving the bukkit …
2025-07-20 15:05:33 +08:00
Ben Woo 5177f290ae Merge pull request #3304 from Multiverse/refactor/wm-cleanup
Refactor/wm cleanup
2025-07-20 15:05:11 +08:00
Ben Woo 5118864bb8 Use our ChatTextFormatter to remove colour from string 2025-07-20 14:56:18 +08:00
Ben Woo 654da03add Remove unnecessary casting 2025-07-20 14:54:41 +08:00
Ben Woo 61229356ad Mark initAllWorlds as internal api 2025-07-20 14:53:16 +08:00
Ben Woo fa5aa41547 Add options to remove a world without unloading or saving the bukkit world 2025-07-20 14:49:57 +08:00
Ben Woo 04f07657eb Merge pull request #3301 from Multiverse/feat/import-bukkit
Improve support for importing worlds already loaded on the server
2025-07-20 11:44:45 +08:00
Ben Woo eea2af169f Improve support for importing worlds already loaded on the server 2025-07-19 23:33:51 +08:00
Ben Woo 6d393723ff Merge pull request #3299 from Multiverse/feat/skip-folder-check
Add option to skip folder check on import
2025-07-19 23:08:28 +08:00
Ben Woo 9137ef04d2 Merge pull request #3300 from Multiverse/feat/load-env-check
Add an environment check when loading a world that already exist in b…
2025-07-19 23:06:56 +08:00
Ben Woo 878c4bea3d Add option to skip folder check on import 2025-07-19 23:05:17 +08:00
Ben Woo 021d86e21c Add an environment check when loading a world that already exist in bukkit 2025-07-19 22:59:15 +08:00
Ben Woo 3e9425c8f1 Merge pull request #3297 from Multiverse/feat/matcher
Implement StringMatcher api
2025-07-18 15:02:20 +08:00
Ben Woo cc60b9c938 Implement StringMatcher api 2025-07-18 14:46:59 +08:00
Ben Woo d3cc950a97 Merge pull request #3296 from Multiverse/feat/attempt-causeby
Add causeBy chaining for Attempt
2025-07-18 11:09:41 +08:00
Ben Woo 7c6b1d07b0 Add causeBy chaining for Attempt 2025-07-18 11:04:41 +08:00
Ben Woo 35b129701f Merge pull request #3285 from Multiverse/feat/joinlocation-bypass
Add bypass permission for join location
2025-07-15 13:46:45 +08:00
Ben Woo d92fe17c4f Merge branch 'main' into feat/joinlocation-bypass
# Conflicts:
#	src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java
2025-07-15 13:40:51 +08:00
Ben Woo d1660d7e81 Add bypass permission for join location 2025-07-07 10:41:38 +08:00
72 changed files with 2318 additions and 349 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 }}
+8 -8
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.31.1'
mockBukkitVersion = '4.84.0'
}
dependencies {
@@ -48,9 +48,9 @@ dependencies {
}
// Utils
shadowed 'io.vavr:vavr:0.10.4'
shadowed 'org.glassfish.hk2:hk2-locator:3.0.3'
shadowed('org.glassfish.hk2:hk2-inhabitant-generator:3.0.3') {
shadowed 'io.vavr:vavr:0.10.7'
shadowed 'org.glassfish.hk2:hk2-locator:3.1.1'
shadowed('org.glassfish.hk2:hk2-inhabitant-generator:3.1.1') {
exclude group: 'org.apache.maven', module: 'maven-core'
}
shadowed('com.dumptruckman.minecraft:Logging:1.1.1') {
@@ -60,8 +60,8 @@ dependencies {
shadowed('org.bstats:bstats-bukkit:3.1.0') {
exclude group: 'org.bukkit', module: 'bukkit'
}
shadowed 'net.minidev:json-smart:2.4.9'
shadowed 'org.jetbrains:annotations:22.0.0'
shadowed 'net.minidev:json-smart:2.5.2'
shadowed 'org.jetbrains:annotations:26.0.2'
shadowed 'io.papermc:paperlib:1.0.8'
// Tests
@@ -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();
@@ -68,7 +68,8 @@ class ImportCommand extends CoreCommand {
.biome(parsedFlags.flagValue(flags.biome, ""))
.environment(environment)
.generator(parsedFlags.flagValue(flags.generator, String.class))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn)))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn))
.doFolderCheck(!parsedFlags.hasFlag(flags.skipFolderCheck)))
.onSuccess(newWorld -> {
Logging.fine("World import success: " + newWorld);
issuer.sendInfo(MVCorei18n.IMPORT_SUCCESS, Replace.WORLD.with(newWorld.getName()));
@@ -105,6 +106,10 @@ class ImportCommand extends CoreCommand {
private final CommandValueFlag<String> biome = flag(CommandValueFlag.builder("--biome", String.class)
.addAlias("-b")
.build());
private final CommandFlag skipFolderCheck = flag(CommandFlag.builder("--skip-folder-check")
.addAlias("-f")
.build());
}
@Service
@@ -3,9 +3,8 @@ package org.mvplugins.multiverse.core.commands;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Conditions;
import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Single;
import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.dumptruckman.minecraft.util.Logging;
@@ -15,19 +14,26 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.options.LoadWorldOptions;
@Service
class LoadCommand extends CoreCommand {
private final WorldManager worldManager;
private final LoadCommand.Flags flags;
@Inject
LoadCommand(@NotNull WorldManager worldManager) {
LoadCommand(@NotNull WorldManager worldManager, @NotNull Flags flags) {
this.worldManager = worldManager;
this.flags = flags;
}
@Subcommand("load")
@@ -38,13 +44,19 @@ class LoadCommand extends CoreCommand {
void onLoadCommand(
MVCommandIssuer issuer,
@Single
@Conditions("worldname:scope=unloaded")
@Syntax("<world>")
@Description("{@@mv-core.load.world.description}")
String worldName) {
issuer.sendInfo(MVCorei18n.LOAD_LOADING, Replace.WORLD.with(worldName));
worldManager.loadWorld(worldName)
MultiverseWorld world,
@Optional
@Syntax("[--remove-players]")
@Description("")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
issuer.sendInfo(MVCorei18n.LOAD_LOADING, Replace.WORLD.with(world.getName()));
worldManager.loadWorld(LoadWorldOptions.world(world)
.doFolderCheck(!parsedFlags.hasFlag(flags.skipFolderCheck)))
.onSuccess(newWorld -> {
Logging.fine("World load success: " + newWorld);
issuer.sendInfo(MVCorei18n.LOAD_SUCCESS, Replace.WORLD.with(newWorld.getName()));
@@ -54,17 +66,32 @@ class LoadCommand extends CoreCommand {
});
}
@Service
private static final class Flags extends RemovePlayerFlags {
private static final String NAME = "mvunload";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}
private final CommandFlag skipFolderCheck = flag(CommandFlag.builder("--skip-folder-check")
.addAlias("-f")
.build());
}
@Service
private static final class LegacyAlias extends LoadCommand implements LegacyAliasCommand {
@Inject
LegacyAlias(@NotNull WorldManager worldManager) {
super(worldManager);
LegacyAlias(@NotNull WorldManager worldManager, @NotNull Flags flags) {
super(worldManager, flags);
}
@Override
@CommandAlias("mvload")
void onLoadCommand(MVCommandIssuer issuer, String worldName) {
super.onLoadCommand(issuer, worldName);
void onLoadCommand(MVCommandIssuer issuer, MultiverseWorld world, String[] flagArray) {
super.onLoadCommand(issuer, world, flagArray);
}
}
}
@@ -15,6 +15,8 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
@@ -23,19 +25,20 @@ import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
import org.mvplugins.multiverse.core.world.options.RemoveWorldOptions;
@Service
class RemoveCommand extends CoreCommand {
private final WorldManager worldManager;
private final PlayerWorldTeleporter playerWorldTeleporter;
private final RemovePlayerFlags flags;
private final Flags flags;
@Inject
RemoveCommand(
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter,
@NotNull RemovePlayerFlags flags
@NotNull Flags flags
) {
this.worldManager = worldManager;
this.playerWorldTeleporter = playerWorldTeleporter;
@@ -44,7 +47,7 @@ class RemoveCommand extends CoreCommand {
@Subcommand("remove")
@CommandPermission("multiverse.core.remove")
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + RemovePlayerFlags.NAME)
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + Flags.NAME)
@Syntax("<world>")
@Description("{@@mv-core.remove.description}")
void onRemoveCommand(
@@ -64,12 +67,14 @@ class RemoveCommand extends CoreCommand {
? worldManager.getLoadedWorld(world).map(playerWorldTeleporter::removeFromWorld).getOrElse(AsyncAttemptsAggregate::emptySuccess)
: AsyncAttemptsAggregate.emptySuccess();
future.onSuccess(() -> doWorldRemoving(issuer, world))
future.onSuccess(() -> doWorldRemoving(issuer, world, parsedFlags))
.onFailure(() -> issuer.sendError("Failed to teleport one or more players out of the world!"));
}
private void doWorldRemoving(MVCommandIssuer issuer, MultiverseWorld world) {
worldManager.removeWorld(world)
private void doWorldRemoving(MVCommandIssuer issuer, MultiverseWorld world, ParsedCommandFlags parsedFlags) {
worldManager.removeWorld(RemoveWorldOptions.world(world)
.saveBukkitWorld(!parsedFlags.hasFlag(flags.noSave))
.unloadBukkitWorld(!parsedFlags.hasFlag(flags.noUnloadBukkitWorld)))
.onSuccess(removedWorldName -> {
Logging.fine("World remove success: " + removedWorldName);
issuer.sendInfo(MVCorei18n.REMOVE_SUCCESS, Replace.WORLD.with(removedWorldName));
@@ -79,13 +84,32 @@ class RemoveCommand extends CoreCommand {
});
}
@Service
private static final class Flags extends RemovePlayerFlags {
private static final String NAME = "mvremove";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}
private final CommandFlag noUnloadBukkitWorld = flag(CommandFlag.builder("--no-unload-bukkit-world")
.addAlias("-b")
.build());
private final CommandFlag noSave = flag(CommandFlag.builder("--no-save")
.addAlias("-n")
.build());
}
@Service
private static final class LegacyAlias extends RemoveCommand implements LegacyAliasCommand {
@Inject
LegacyAlias(
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter,
RemovePlayerFlags flags
@NotNull Flags flags
) {
super(worldManager, playerWorldTeleporter, flags);
}
@@ -108,7 +108,7 @@ final class SpawnCommand extends CoreCommand {
safetyTeleporter.to(mvWorld.getSpawnLocation())
.by(issuer)
.checkSafety(checkSafety)
.teleport(entity)
.teleportSingle(entity)
.onSuccess(() -> issuer.sendInfo(MVCorei18n.SPAWN_SUCCESS,
Replace.PLAYER.with(entity.equals(issuer.getPlayer())
? Message.of(MVCorei18n.GENERIC_YOU)
@@ -119,7 +119,7 @@ final class SpawnCommand extends CoreCommand {
? Message.of(MVCorei18n.GENERIC_YOU)
: Message.of(entity.getName())),
Replace.WORLD.with(mvWorld.getName()),
Replace.REASON.with(failure.getFailureMessage())));
Replace.REASON.with(failure.getFirst().getFailureMessage())));
}
private void handleMultiTeleport(MVCommandIssuer issuer, LoadedMultiverseWorld mvWorld,
@@ -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.")
@@ -518,6 +550,7 @@ final class CoreConfigNodes {
.build());
final ConfigNode<Boolean> debugPermissions = node(ConfigNode.builder("misc.debug-permissions", Boolean.class)
.comment("")
.comment("Sets whether console will log every permission check done by all multiverse plugins.")
.comment("This will only work if the above 'global-debug' is set to 1 or more.")
.defaultValue(false)
@@ -536,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();
}
}
@@ -69,9 +69,12 @@ public class RegexContentFilter implements ContentFilter {
if (!hasValidRegex()) {
return false;
}
String text = ChatTextFormatter.removeColor(String.valueOf(value)).toLowerCase();
String text = ChatTextFormatter.removeColor(String.valueOf(value));
if (text == null) {
return false;
}
try {
return regexPattern.matcher(text).find();
return regexPattern.matcher(text.toLowerCase()).find();
} catch (PatternSyntaxException ignored) {
Logging.warning("Error parsing regex '%s' for input '%s'", regexString, text);
return false;
@@ -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);
}
}
@@ -88,8 +88,9 @@ final class MVChatListener implements CoreListener {
String prefixChatFormat = config.getPrefixChatFormat();
prefixChatFormat = prefixChatFormat.replace("%world%", worldName).replace("%chat%", chat);
prefixChatFormat = ChatTextFormatter.colorize(prefixChatFormat);
event.setFormat(prefixChatFormat);
if (prefixChatFormat != null) {
event.setFormat(prefixChatFormat);
}
}
private String getWorldName(Player player) {
@@ -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,15 +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;
@@ -51,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.
@@ -70,6 +71,8 @@ final class MVPlayerListener implements CoreListener {
private final DestinationsProvider destinationsProvider;
private final EnforcementHandler enforcementHandler;
private final DimensionFinder dimensionFinder;
private final CorePermissionsChecker corePermissionsChecker;
private final AsyncSafetyTeleporter asyncSafetyTeleporter;
private final Map<String, String> playerWorld = new ConcurrentHashMap<>();
@@ -86,7 +89,9 @@ final class MVPlayerListener implements CoreListener {
Provider<MVCommandManager> commandManagerProvider,
DestinationsProvider destinationsProvider,
EnforcementHandler enforcementHandler,
DimensionFinder dimensionFinder) {
DimensionFinder dimensionFinder,
CorePermissionsChecker corePermissionsChecker,
AsyncSafetyTeleporter asyncSafetyTeleporter) {
this.plugin = plugin;
this.config = config;
this.worldManagerProvider = worldManagerProvider;
@@ -99,6 +104,8 @@ final class MVPlayerListener implements CoreListener {
this.destinationsProvider = destinationsProvider;
this.enforcementHandler = enforcementHandler;
this.dimensionFinder = dimensionFinder;
this.corePermissionsChecker = corePermissionsChecker;
this.asyncSafetyTeleporter = asyncSafetyTeleporter;
}
private WorldManager getWorldManager() {
@@ -109,7 +116,7 @@ final class MVPlayerListener implements CoreListener {
return commandManagerProvider.get();
}
public PluginLocales getLocales() {
private PluginLocales getLocales() {
return getCommandManager().getLocales();
}
@@ -118,7 +125,7 @@ final class MVPlayerListener implements CoreListener {
*
* @return the playerWorld-map
*/
public Map<String, String> getPlayerWorld() {
Map<String, String> getPlayerWorld() {
return playerWorld;
}
@@ -130,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) {
@@ -197,64 +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;
}
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()));
}
/**
@@ -263,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());
@@ -276,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()) {
@@ -347,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;
}
@@ -373,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;
}
@@ -18,6 +18,7 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.options.LoadWorldOptions;
import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions;
import org.mvplugins.multiverse.core.world.reasons.LoadFailureReason;
import org.mvplugins.multiverse.core.world.reasons.UnloadFailureReason;
@@ -68,7 +69,7 @@ final class MVWorldListener implements CoreListener {
worldManager.getUnloadedWorld(event.getWorld().getName())
.peek(world -> {
Logging.fine("Loading world: " + world.getName());
worldManager.loadWorld(world).onFailure(failure -> {
worldManager.loadWorld(LoadWorldOptions.world(world)).onFailure(failure -> {
if (failure.getFailureReason() != LoadFailureReason.WORLD_ALREADY_LOADING) {
Logging.severe("Failed to load world: " + failure);
}
@@ -308,11 +308,13 @@ public enum MVCorei18n implements MessageKeyProvider {
IMPORTWORLD_WORLDEXISTUNLOADED,
IMPORTWORLD_WORLDEXISTLOADED,
IMPORTWORLD_WORLDFOLDERINVALID,
IMPORTWORLD_BUKKITENVIRONMENTMISMATCH,
LOADWORLD_WORLDALREADYLOADING,
LOADWORLD_WORLDNONEXISTENT,
LOADWORLD_WORLDEXISTFOLDER,
LOADWORLD_WORLDEXISTLOADED,
LOADWORLD_BUKKITENVIRONMENTMISMATCH,
REMOVEWORLD_WORLDNONEXISTENT,
@@ -344,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");
@@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core.permissions;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
@@ -15,6 +16,11 @@ import static org.mvplugins.multiverse.core.permissions.PermissionUtils.concatPe
@Service
public final class CorePermissions {
/**
* Permission to bypass the join location.
*/
static final String JOINLOCATION_BYPASS = "mv.bypass.joinlocation";
/**
* Permission to access a world.
*/
@@ -52,6 +58,12 @@ public final class CorePermissions {
this.pluginManager = pluginManager;
}
@PostConstruct
void registerBasePermissions() {
pluginManager.addPermission(new Permission(JOINLOCATION_BYPASS, PermissionDefault.FALSE));
Logging.fine("Successfully registered base permissions");
}
public Try<Void> addWorldPermissions(@NotNull MultiverseWorld world) {
return Try.run(() -> {
pluginManager.addPermission(new Permission(
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.permissions;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
@@ -50,6 +51,19 @@ public final class CorePermissionsChecker {
this.worldManager = worldManager;
}
/**
* Checks if the sender has permission to bypass the join location restriction.
*
* @param sender The command sender.
* @return True if the sender has bypass permission, false otherwise.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public boolean hasJoinLocationBypassPermission(@NotNull CommandSender sender) {
return hasPermission(sender, CorePermissions.JOINLOCATION_BYPASS);
}
/**
* Checks if the sender has permission to access the specified world.
*
@@ -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,75 @@
package org.mvplugins.multiverse.core.utils.matcher;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* ExactStringMatcher is a StringMatcher that matches strings against a set of exact values.
* It can be initialized with a single string, a collection of strings, or can have exact matches added later.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public class ExactStringMatcher implements StringMatcher {
private final Set<String> exactMatches;
/**
* Creates a new ExactStringMatcher with no initial matches. Use {@link #addExactMatch(String)} to add matches later.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public ExactStringMatcher() {
this.exactMatches = new HashSet<>();
}
/**
* Creates a new ExactStringMatcher with a single exact match.
*
* @param exactMatch the exact string to match against
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public ExactStringMatcher(@NotNull String exactMatch) {
this.exactMatches = new HashSet<>();
this.exactMatches.add(exactMatch);
}
/**
* Creates a new ExactStringMatcher with multiple exact matches.
*
* @param exactMatches the collection of exact strings to match against
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public ExactStringMatcher(@NotNull Collection<String> exactMatches) {
this.exactMatches = new HashSet<>(exactMatches);
}
/**
* Adds an exact match string to this matcher.
*
* @param value the exact string to add to the matcher
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public void addExactMatch(@NotNull String value) {
this.exactMatches.add(value);
}
/**
* {@inheritDoc}
*/
@Override
public boolean matches(@Nullable String value) {
return exactMatches.contains(value);
}
}
@@ -0,0 +1,107 @@
package org.mvplugins.multiverse.core.utils.matcher;
import com.dumptruckman.minecraft.util.Logging;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* MatcherGroup is a collection of StringMatchers that can match against a string.
* It works with all format supported by {@link StringMatcher#fromString(String)}.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public class MatcherGroup implements StringMatcher {
private final ExactStringMatcher exactMatcher;
private final List<StringMatcher> stringMatchers;
/**
* Creates a new empty MatcherGroup.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public MatcherGroup() {
this.exactMatcher = new ExactStringMatcher();
this.stringMatchers = new ArrayList<>();
}
/**
* Creates a new MatcherGroup with multiple strings to be parsed into matchers.
*
* @param matchStrings the collection of match strings
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public MatcherGroup(@NotNull Collection<String> matchStrings) {
this();
for (String matchString : matchStrings) {
addMatcher(matchString);
}
}
/**
* Creates a new MatcherGroup with a single exact match.
*
* @param matchString the single match string
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public void addMatcher(@Nullable String matchString) {
if (matchString == null || matchString.isEmpty()) {
return;
}
if (isExact(matchString)) {
exactMatcher.addExactMatch(matchString);
} else {
stringMatchers.add(StringMatcher.fromString(matchString));
}
}
private boolean isExact(@NotNull String matcherString) {
return !matcherString.contains("*") && !matcherString.startsWith("r=");
}
/**
* Adds an existing matcher to the group.
*
* @param matcher the StringMatcher to add
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public void addMatcher(@NotNull StringMatcher matcher) {
stringMatchers.add(matcher);
}
/**
* {@inheritDoc}
*/
@Override
public boolean matches(@Nullable String value) {
if (exactMatcher.matches(value)) {
return true;
}
for (StringMatcher matcher : stringMatchers) {
if (matcher.matches(value)) {
return true;
}
}
return false;
}
@Override
public String toString() {
return "MatcherGroup{" + "exactMatcher=" + exactMatcher +
", stringMatchers=" + stringMatchers +
'}';
}
}
@@ -0,0 +1,57 @@
package org.mvplugins.multiverse.core.utils.matcher;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.regex.Pattern;
/**
* RegexStringMatcher is a StringMatcher that matches strings against a regex pattern.
* It can be initialized with a regex string, which can optionally start with 'r=' to indicate it's a regex.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public class RegexStringMatcher implements StringMatcher {
private final @NotNull String regexString;
private final @Nullable Pattern regexPattern;
/**
* Creates a new RegexStringMatcher with a regex string. 'r=' prefix will be stripped if present.
*
* @param regexString the regex string to match against. If it starts with 'r=', that part will be ignored.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public RegexStringMatcher(@NotNull String regexString) {
this.regexString = regexString;
this.regexPattern = compileRegex(regexString);
}
private Pattern compileRegex(String regexString) {
if (regexString.startsWith("r=")) {
regexString = regexString.substring(2);
}
String finalRegexString = regexString;
return Try.of(() -> Pattern.compile(finalRegexString))
.onFailure(ex -> Logging.warning("Failed to compile regex '%s': %s",
finalRegexString, ex.getMessage()))
.getOrNull();
}
/**
* {@inheritDoc}
*/
@Override
public boolean matches(@Nullable String value) {
if (regexPattern == null || value == null) {
return false;
}
return regexPattern.matcher(value).matches();
}
}
@@ -0,0 +1,67 @@
package org.mvplugins.multiverse.core.utils.matcher;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.stream.Collectors;
/**
* StringMatcher is an interface for matching strings against various patterns.
* It currently built-in supports exact matches, wildcard matches, and regex matches.
* <br />
* Implement this interface to create your own custom string matchers.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public interface StringMatcher {
/**
* Creates a StringMatcher from a string representation. When the string starts with "r=", it is treated as a regex.
* If the string contains a '*', it is treated as a wildcard match.
*
* @param matcherString the string to be parsed into a matcher.
* @return a StringMatcher instance based on the provided string.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
static @NotNull StringMatcher fromString(@NotNull String matcherString) {
if (matcherString.startsWith("r=")) {
return new RegexStringMatcher(matcherString);
} else if (matcherString.contains("*")) {
return new WildcardStringMatcher(matcherString);
} else {
return new ExactStringMatcher(matcherString);
}
}
/**
* Checks if the given value matches the pattern defined by this StringMatcher.
*
* @param value the string to match against the pattern.
* @return true if the value matches, false otherwise.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
boolean matches(@Nullable String value);
/**
* Filters a list of strings, returning only those that match the pattern defined by this StringMatcher.
* This method is a convenience for applying the matcher to a collection of strings.
*
* @param values the list of strings to filter.
* @return A list of strings that match the pattern defined by this StringMatcher.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
default @NotNull List<String> filter(@NotNull List<String> values) {
return values.stream()
.filter(this::matches)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,50 @@
package org.mvplugins.multiverse.core.utils.matcher;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.regex.Pattern;
/**
* WildcardStringMatcher is a StringMatcher that matches strings against a wildcard pattern.
* It supports '*' as a wildcard character, which can match any sequence of characters.
* <br />
* For example, the wildcard "foo*bar" will match any string that starts with "foo" and ends with "bar",
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public class WildcardStringMatcher implements StringMatcher {
private final String wildcard;
private final Pattern pattern;
/**
* Creates a new WildcardStringMatcher with a wildcard string.
* The wildcard string can contain '*' characters, which will be replaced with a regex equivalent.
*
* @param wildcard the wildcard string to match against.
*/
@ApiStatus.AvailableSince("5.2")
public WildcardStringMatcher(@NotNull String wildcard) {
this.wildcard = wildcard;
this.pattern = Try.of(() -> Pattern.compile(("\\Q" + wildcard + "\\E").replace("*", "\\E.*\\Q")))
.onFailure(ex -> Logging.warning("Failed to compile wildcard '%s': %s",
wildcard, ex.getMessage()))
.getOrNull();
}
/**
* {@inheritDoc}
*/
@Override
public boolean matches(@Nullable String value) {
if (pattern == null || value == null) {
return false;
}
return pattern.matcher(value).matches();
}
}
@@ -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;
}
}
@@ -181,7 +181,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
if (this instanceof Success) {
return new Success<>(mapper.apply(get()));
} else {
return new Failure<>(getFailureReason(), getFailureMessage());
return new Failure<>((Failure<T, F>) this);
}
}
@@ -196,7 +196,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
if (this instanceof Success) {
return new Success<>(mapper.get());
} else {
return new Failure<>(getFailureReason(), getFailureMessage());
return new Failure<>((Failure<T, F>) this);
}
}
@@ -211,7 +211,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
if (this instanceof Success) {
return mapper.apply(get());
} else {
return new Failure<>(getFailureReason(), getFailureMessage());
return new Failure<>((Failure<T, F>) this);
}
}
@@ -226,7 +226,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
if (this instanceof Success) {
return mapper.get();
} else {
return new Failure<>(getFailureReason(), getFailureMessage());
return new Failure<>((Failure<T, F>) this);
}
}
@@ -241,7 +241,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
if (this instanceof Success) {
return new Success<>(get());
} else {
return new Failure<>(failureReason, getFailureMessage());
return new Failure<>(failureReason, getFailureMessage(), (Failure<T, F>) this);
}
}
@@ -415,10 +415,20 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
final class Failure<T, F extends FailureReason> implements Attempt<T, F> {
private final F failureReason;
private final Message message;
private final Failure<?, ?> causeBy;
Failure(F failureReason, Message message) {
this(failureReason, message, null);
}
Failure(Failure<?, F> failure) {
this(failure.failureReason, failure.message, failure.causeBy);
}
Failure(F failureReason, Message message, Failure<?, ?> causeBy) {
this.failureReason = failureReason;
this.message = message;
this.causeBy = causeBy;
}
@Override
@@ -465,6 +475,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
public String toString() {
return "Failure{"
+ "reason=" + failureReason
+ (causeBy != null ? ", causeBy=" + causeBy : "")
+ '}';
}
}
@@ -3,12 +3,21 @@ package org.mvplugins.multiverse.core.utils.result;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
/**
* Represents a failure reason for an {@link Attempt}.
*/
public interface FailureReason extends MessageKeyProvider {
/**
* A generic failure reason that can be used when no specific reason is applicable.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
FailureReason GENERIC = new FailureReason() { };
default MessageKey getMessageKey() {
return MVCorei18n.GENERIC_FAILURE.getMessageKey();
}
@@ -3,9 +3,21 @@ package org.mvplugins.multiverse.core.utils.result;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
/**
* Represents a success reason for an {@link Attempt}.
*/
public interface SuccessReason extends MessageKeyProvider {
/**
* A generic success reason that can be used when no specific reason is applicable.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
SuccessReason GENERIC = new SuccessReason() { };
default MessageKey getMessageKey() {
return MVCorei18n.GENERIC_SUCCESS.getMessageKey();
}
@@ -4,37 +4,45 @@ import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
final class AdventureTextFormatter implements TextFormatter {
@Override
public void sendFormattedMessage(CommandSender sender, String message) {
sender.sendMessage(colorize(message));
public void sendFormattedMessage(@NotNull CommandSender sender, @Nullable String message) {
String colorizedMessage = colorize(message);
if (colorizedMessage == null) {
return; // Avoid sending null messages
}
sender.sendMessage(colorizedMessage);
}
@Override
public String removeColor(String message) {
TextComponent sectionComponent = LegacyComponentSerializer.legacySection().deserialize(colorize(message));
return PlainTextComponentSerializer.plainText().serialize(sectionComponent);
}
public String removeAmpColor(String message) {
return PlainTextComponentSerializer.plainText().serialize(toAmpComponent(message));
}
public String removeSectionColor(String message) {
return PlainTextComponentSerializer.plainText().serialize(toSectionComponent(message));
public @Nullable String removeColor(@Nullable String message) {
TextComponent sectionComponent = LegacyComponentSerializer.legacySection().deserializeOrNull(colorize(message));
return PlainTextComponentSerializer.plainText().serializeOrNull(sectionComponent);
}
@Override
public String colorize(String message) {
return LegacyComponentSerializer.legacySection().serialize(toAmpComponent(message));
public @Nullable String removeAmpColor(@Nullable String message) {
return PlainTextComponentSerializer.plainText().serializeOrNull(toAmpComponent(message));
}
private TextComponent toAmpComponent(String message) {
return LegacyComponentSerializer.legacyAmpersand().deserialize(message);
@Override
public @Nullable String removeSectionColor(@Nullable String message) {
return PlainTextComponentSerializer.plainText().serializeOrNull(toSectionComponent(message));
}
private TextComponent toSectionComponent(String message) {
return LegacyComponentSerializer.legacySection().deserialize(message);
@Override
public @Nullable String colorize(@Nullable String message) {
return LegacyComponentSerializer.legacySection().serializeOrNull(toAmpComponent(message));
}
private @Nullable TextComponent toAmpComponent(@Nullable String message) {
return LegacyComponentSerializer.legacyAmpersand().deserializeOrNull(message);
}
private @Nullable TextComponent toSectionComponent(@Nullable String message) {
return LegacyComponentSerializer.legacySection().deserializeOrNull(message);
}
}
@@ -2,30 +2,36 @@ package org.mvplugins.multiverse.core.utils.text;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@SuppressWarnings("deprecation")
final class ChatColorTextFormatter implements TextFormatter {
@Override
public void sendFormattedMessage(CommandSender sender, String message) {
public void sendFormattedMessage(@NotNull CommandSender sender, String message) {
sender.sendMessage(colorize(message));
}
@Override
public String removeColor(String message) {
public @Nullable String removeColor(@Nullable String message) {
return ChatColor.stripColor(colorize(message));
}
@Override
public String removeAmpColor(String message) {
public @Nullable String removeAmpColor(@Nullable String message) {
return removeColor(message);
}
@Override
public String removeSectionColor(String message) {
public @Nullable String removeSectionColor(@Nullable String message) {
return ChatColor.stripColor(message);
}
@Override
public String colorize(String message) {
public @Nullable String colorize(@Nullable String message) {
if (message == null) {
return null;
}
return ChatColor.translateAlternateColorCodes('&', message);
}
}
@@ -2,6 +2,8 @@ package org.mvplugins.multiverse.core.utils.text;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
/**
@@ -16,7 +18,8 @@ public final class ChatTextFormatter {
private static final TextFormatter wrapper;
static {
if (ReflectHelper.hasClass("net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer")) {
if (ReflectHelper.hasClass("net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer")
&& ReflectHelper.hasClass("net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer")) {
wrapper = new AdventureTextFormatter();
} else {
wrapper = new ChatColorTextFormatter();
@@ -32,7 +35,7 @@ public final class ChatTextFormatter {
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public static void sendFormattedMessage(CommandSender sender, String message) {
public static void sendFormattedMessage(@NotNull CommandSender sender, @Nullable String message) {
wrapper.sendFormattedMessage(sender, message);
}
@@ -44,7 +47,7 @@ public final class ChatTextFormatter {
*
* @since 5.1
*/
public static String removeColor(String message) {
public static @Nullable String removeColor(@Nullable String message) {
return wrapper.removeColor(message);
}
@@ -57,7 +60,7 @@ public final class ChatTextFormatter {
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public static String removeAmpColor(String message) {
public static @Nullable String removeAmpColor(@Nullable String message) {
return wrapper.removeAmpColor(message);
}
@@ -70,7 +73,7 @@ public final class ChatTextFormatter {
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public static String removeSectionColor(String message) {
public static @Nullable String removeSectionColor(@Nullable String message) {
return wrapper.removeSectionColor(message);
}
@@ -83,7 +86,7 @@ public final class ChatTextFormatter {
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public static String colorize(String message) {
public static @Nullable String colorize(@Nullable String message) {
return wrapper.colorize(message);
}
}
@@ -1,15 +1,17 @@
package org.mvplugins.multiverse.core.utils.text;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
interface TextFormatter {
void sendFormattedMessage(CommandSender sender, String message);
void sendFormattedMessage(@NotNull CommandSender sender, @Nullable String message);
String removeColor(String message);
@Nullable String removeColor(@Nullable String message);
String removeAmpColor(String message);
@Nullable String removeAmpColor(@Nullable String message);
String removeSectionColor(String message);
@Nullable String removeSectionColor(@Nullable String message);
String colorize(String message);
@Nullable String colorize(@Nullable String message);
}
@@ -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() {
@@ -238,13 +241,13 @@ final class WorldConfigNodes {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
if (newValue == null || newValue instanceof NullSpawnLocation) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> {
bukkitWorld.setSpawnLocation(newValue.getBlockX(), newValue.getBlockY(), newValue.getBlockZ());
bukkitWorld.setSpawnLocation(newValue);
newValue.setWorld(bukkitWorld);
});
}));
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
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.world;
import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -19,12 +20,12 @@ import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.WorldType;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
@@ -45,6 +46,7 @@ import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.permissions.CorePermissions;
import org.mvplugins.multiverse.core.teleportation.BlockSafety;
import org.mvplugins.multiverse.core.teleportation.LocationManipulation;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import org.mvplugins.multiverse.core.utils.ServerProperties;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
@@ -62,7 +64,9 @@ import org.mvplugins.multiverse.core.world.options.CreateWorldOptions;
import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions;
import org.mvplugins.multiverse.core.world.options.ImportWorldOptions;
import org.mvplugins.multiverse.core.world.options.KeepWorldSettingsOptions;
import org.mvplugins.multiverse.core.world.options.LoadWorldOptions;
import org.mvplugins.multiverse.core.world.options.RegenWorldOptions;
import org.mvplugins.multiverse.core.world.options.RemoveWorldOptions;
import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions;
import org.mvplugins.multiverse.core.world.reasons.CloneFailureReason;
import org.mvplugins.multiverse.core.world.reasons.CreateFailureReason;
@@ -140,6 +144,7 @@ public final class WorldManager {
*
* @return The result of the load.
*/
@ApiStatus.Internal
public Try<Void> initAllWorlds() {
return updateWorldsFromConfig().andThenTry(() -> {
importExistingWorlds();
@@ -167,7 +172,11 @@ public final class WorldManager {
}
private void removeWorldsNotInConfigs(Collection<String> removedWorlds) {
removedWorlds.forEach(worldName -> removeWorld(worldName)
removedWorlds.forEach(worldName -> getWorld(worldName)
.fold(
() -> Attempt.failure(FailureReason.GENERIC, Message.of("world already removed")),
world -> removeWorld(RemoveWorldOptions.world(world))
)
.onFailure(failure -> Logging.severe("Failed to unload world %s: %s", worldName, failure))
.onSuccess(success -> Logging.fine("Unloaded world %s as it was removed from config", worldName)));
}
@@ -218,8 +227,9 @@ public final class WorldManager {
if (isLoadedWorld(world) || !world.isAutoLoad()) {
return;
}
loadWorld(world).onFailure(failure -> Logging.severe("Failed to load world %s: %s",
world.getName(), failure));
loadWorld(LoadWorldOptions.world(world))
.onFailure(failure ->
Logging.severe("Failed to load world %s: %s", world.getName(), failure));
});
}
@@ -278,23 +288,30 @@ public final class WorldManager {
* @param options The options for customizing the import of an existing world folder.
* @return The result of the import.
*/
public Attempt<LoadedMultiverseWorld, ImportFailureReason> importWorld(
ImportWorldOptions options) {
return validateImportWorldOptions(options).mapAttempt(this::doImportWorld);
}
private Attempt<ImportWorldOptions, ImportFailureReason> validateImportWorldOptions(
ImportWorldOptions options) {
public Attempt<LoadedMultiverseWorld, ImportFailureReason> importWorld(ImportWorldOptions options) {
String worldName = options.worldName();
if (!worldNameChecker.isValidWorldName(worldName)) {
return worldActionResult(ImportFailureReason.INVALID_WORLDNAME, worldName);
} else if (!worldNameChecker.isValidWorldFolder(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_FOLDER_INVALID, worldName);
} else if (isLoadedWorld(worldName)) {
if (isLoadedWorld(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_EXIST_LOADED, worldName);
} else if (isWorld(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_EXIST_UNLOADED, worldName);
}
World bukkitWorld = Bukkit.getWorld(worldName);
if (bukkitWorld != null) {
// World is already loaded, maybe by another plugin
return doImportBukkitWorld(options, bukkitWorld);
}
return validateImportWorldOptions(options).mapAttempt(this::doImportWorld);
}
private Attempt<ImportWorldOptions, ImportFailureReason> validateImportWorldOptions(ImportWorldOptions options) {
String worldName = options.worldName();
if (!worldNameChecker.isValidWorldName(worldName)) {
return worldActionResult(ImportFailureReason.INVALID_WORLDNAME, worldName);
} else if (options.doFolderCheck() && !worldNameChecker.isValidWorldFolder(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_FOLDER_INVALID, worldName);
}
return worldActionResult(options);
}
@@ -319,6 +336,23 @@ public final class WorldManager {
});
}
private Attempt<LoadedMultiverseWorld, ImportFailureReason> doImportBukkitWorld(ImportWorldOptions options, World bukkitWorld) {
if (options.environment() != bukkitWorld.getEnvironment()) {
return Attempt.failure(ImportFailureReason.BUKKIT_ENVIRONMENT_MISMATCH,
Replace.WORLD.with(bukkitWorld.getName()),
replace("{bukkitEnvironment}").with(bukkitWorld.getEnvironment().name()),
replace("{mvEnvironment}").with(options.environment().name()));
}
LoadedMultiverseWorld loadedWorld = newLoadedMultiverseWorld(
bukkitWorld,
generatorProvider.parseGeneratorString(options.worldName(), options.generator()),
options.biome(),
options.useSpawnAdjust());
pluginManager.callEvent(new MVWorldImportedEvent(loadedWorld));
return Attempt.success(loadedWorld);
}
private MultiverseWorld newMultiverseWorld(String worldName, WorldConfig worldConfig) {
MultiverseWorld mvWorld = new MultiverseWorld(worldName, worldConfig, config);
worldsMap.put(mvWorld.getName(), mvWorld);
@@ -336,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,
@@ -350,31 +392,25 @@ 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.
*
* @param worldName The name of the world to load.
* @return The result of the load.
*
* @deprecated Use {@link #loadWorld(LoadWorldOptions)} instead.
*/
@Deprecated(since = "5.2", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public Attempt<LoadedMultiverseWorld, LoadFailureReason> loadWorld(@NotNull String worldName) {
return getWorld(worldName)
.map(this::loadWorld)
.map(world -> loadWorld(LoadWorldOptions.world(world)))
.getOrElse(() -> worldNameChecker.isValidWorldFolder(worldName)
? worldActionResult(LoadFailureReason.WORLD_EXIST_FOLDER, worldName)
: worldActionResult(LoadFailureReason.WORLD_NON_EXISTENT, worldName));
@@ -385,12 +421,30 @@ public final class WorldManager {
*
* @param world The world to load.
* @return The result of the load.
*
* @deprecated Use {@link #loadWorld(LoadWorldOptions)} instead.
*/
@Deprecated(since = "5.2", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public Attempt<LoadedMultiverseWorld, LoadFailureReason> loadWorld(@NotNull MultiverseWorld world) {
return validateWorldToLoad(world).mapAttempt(this::doLoadWorld);
return loadWorld(LoadWorldOptions.world(world));
}
private Attempt<MultiverseWorld, LoadFailureReason> validateWorldToLoad(@NotNull MultiverseWorld mvWorld) {
/**
* Loads an existing world in config.
*
* @param options The options for customizing the loading of a world.
* @return The result of the load.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public Attempt<LoadedMultiverseWorld, LoadFailureReason> loadWorld(@NotNull LoadWorldOptions options) {
return validateWorldToLoad(options).mapAttempt(this::doLoadWorld);
}
private Attempt<LoadWorldOptions, LoadFailureReason> validateWorldToLoad(@NotNull LoadWorldOptions options) {
MultiverseWorld mvWorld = options.world();
if (loadTracker.contains(mvWorld.getName())) {
// This is to prevent recursive calls by WorldLoadEvent
Logging.fine("World already loading: " + mvWorld.getName());
@@ -399,15 +453,19 @@ public final class WorldManager {
Logging.severe("World already loaded: " + mvWorld.getName());
return worldActionResult(LoadFailureReason.WORLD_EXIST_LOADED, mvWorld.getName());
}
return worldActionResult(mvWorld);
return worldActionResult(options);
}
private Attempt<LoadedMultiverseWorld, LoadFailureReason> doLoadWorld(@NotNull MultiverseWorld mvWorld) {
private Attempt<LoadedMultiverseWorld, LoadFailureReason> doLoadWorld(@NotNull LoadWorldOptions options) {
MultiverseWorld mvWorld = options.world();
World bukkitWorld = Bukkit.getWorld(mvWorld.getName());
if (bukkitWorld != null) {
// World already loaded, maybe by another plugin
Logging.finer("World already loaded in bukkit: " + mvWorld.getName());
return newLoadedMultiverseWorld(mvWorld, bukkitWorld);
return doLoadBukkitWorld(bukkitWorld, mvWorld);
}
if (options.doFolderCheck() && !worldNameChecker.isValidWorldFolder(mvWorld.getName())) {
return worldActionResult(LoadFailureReason.WORLD_FOLDER_INVALID, mvWorld.getName());
}
WorldCreator worldCreator = WorldCreator.name(mvWorld.getName())
@@ -420,6 +478,18 @@ public final class WorldManager {
.mapAttempt(newBukkitWorld -> newLoadedMultiverseWorld(mvWorld, newBukkitWorld));
}
private @NotNull Attempt<LoadedMultiverseWorld, LoadFailureReason> doLoadBukkitWorld(World bukkitWorld, MultiverseWorld mvWorld) {
if (bukkitWorld.getEnvironment() != mvWorld.getEnvironment()) {
return Attempt.failure(LoadFailureReason.BUKKIT_ENVIRONMENT_MISMATCH,
Replace.WORLD.with(mvWorld.getName()),
replace("{bukkitEnvironment}").with(bukkitWorld.getEnvironment().name()),
replace("{mvEnvironment}").with(mvWorld.getEnvironment().name()));
}
// World already loaded, maybe by another plugin
Logging.finer("World already loaded in bukkit: " + mvWorld.getName());
return newLoadedMultiverseWorld(mvWorld, bukkitWorld);
}
private Attempt<LoadedMultiverseWorld, LoadFailureReason> newLoadedMultiverseWorld(MultiverseWorld mvWorld, World bukkitWorld) {
WorldConfig worldConfig = worldsConfigManager.getWorldConfig(mvWorld.getName()).get(); //TODO: null check here, but logically it should never be null.
LoadedMultiverseWorld loadedWorld = new LoadedMultiverseWorld(
@@ -481,11 +551,14 @@ public final class WorldManager {
*
* @param worldName The name of the world to remove.
* @return The result of the remove.
*
* @deprecated Get the {@link MultiverseWorld} yourself and use {@link #removeWorld(RemoveWorldOptions)} instead.
*/
public Attempt<String, RemoveFailureReason> removeWorld(
@NotNull String worldName) {
@Deprecated(since = "5.2", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public Attempt<String, RemoveFailureReason> removeWorld(@NotNull String worldName) {
return getWorld(worldName)
.map(this::removeWorld)
.map(world -> removeWorld(RemoveWorldOptions.world(world)))
.getOrElse(() -> worldActionResult(RemoveFailureReason.WORLD_NON_EXISTENT, worldName));
}
@@ -495,11 +568,13 @@ public final class WorldManager {
*
* @param world The multiverse world to remove.
* @return The result of the remove.
*
* @deprecated Use {@link #removeWorld(RemoveWorldOptions)} instead.
*/
@Deprecated(since = "5.2", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public Attempt<String, RemoveFailureReason> removeWorld(@NotNull MultiverseWorld world) {
return getLoadedWorld(world).fold(
() -> removeWorldFromConfig(world),
this::removeWorld);
return removeWorld(RemoveWorldOptions.world(world));
}
/**
@@ -508,9 +583,40 @@ public final class WorldManager {
*
* @param loadedWorld The multiverse world to remove.
* @return The result of the remove.
*
* @deprecated Use {@link #removeWorld(RemoveWorldOptions)} instead.
*/
@Deprecated(since = "5.2", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public Attempt<String, RemoveFailureReason> removeWorld(@NotNull LoadedMultiverseWorld loadedWorld) {
return unloadWorld(UnloadWorldOptions.world(loadedWorld))
return removeWorld(RemoveWorldOptions.world(loadedWorld));
}
/**
* Removes an existing multiverse world. It will be deleted from the worlds config and will no longer be known
* to Multiverse. World files will not be deleted.
*
* @param options The options for customizing the removal of a world.
* @return The result of the remove action.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public Attempt<String, RemoveFailureReason> removeWorld(@NotNull RemoveWorldOptions options) {
MultiverseWorld world = options.world();
return getLoadedWorld(world).fold(
() -> removeWorldFromConfig(world),
loadedWorld -> unloadBeforeRemoveWorld(loadedWorld, options)
);
}
private Attempt<String, RemoveFailureReason> unloadBeforeRemoveWorld(@NotNull LoadedMultiverseWorld loadedWorld,
@NotNull RemoveWorldOptions options) {
UnloadWorldOptions unloadWorldOptions = UnloadWorldOptions.world(loadedWorld)
.saveBukkitWorld(options.saveBukkitWorld())
.unloadBukkitWorld(options.unloadBukkitWorld());
return unloadWorld(unloadWorldOptions)
.transform(RemoveFailureReason.UNLOAD_FAILED)
.mapAttempt(this::removeWorldFromConfig);
}
@@ -541,12 +647,18 @@ public final class WorldManager {
*/
public Attempt<String, DeleteFailureReason> deleteWorld(@NotNull DeleteWorldOptions options) {
return getLoadedWorld(options.world()).fold(
() -> loadWorld(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.
*
@@ -564,7 +676,11 @@ public final class WorldManager {
? Attempt.failure(DeleteFailureReason.EVENT_CANCELLED)
: Attempt.success(null);
})
.mapAttempt(() -> removeWorld(world).transform(DeleteFailureReason.REMOVE_FAILED))
.mapAttempt(() -> removeWorld(RemoveWorldOptions
.world(world)
.unloadBukkitWorld(true)
.saveBukkitWorld(false)
).transform(DeleteFailureReason.REMOVE_FAILED))
.mapAttempt(() -> fileUtils.deleteFolder(worldFolder.get(), options.keepFiles()).fold(
exception -> worldActionResult(DeleteFailureReason.FAILED_TO_DELETE_FOLDER,
world.getName(), exception),
@@ -632,7 +748,7 @@ public final class WorldManager {
private Attempt<CloneWorldOptions, CloneFailureReason> cloneWorldCopyFolder(@NotNull CloneWorldOptions options) {
if (options.saveBukkitWorld()) {
Logging.finer("Saving bukkit world before cloning: " + options.world().getName());
options.world().getBukkitWorld().peek(World::save);
options.world().getBukkitWorld().peek(this::saveWorldWithFlush);
}
File worldFolder = options.world().getBukkitWorld().map(World::getWorldFolder).get();
File newWorldFolder = new File(Bukkit.getWorldContainer(), options.newWorldName());
@@ -642,6 +758,17 @@ public final class WorldManager {
success -> worldActionResult(options));
}
// This method is only available since 1.21
private final Method saveWithFlush = ReflectHelper.getMethod(World.class, "save", boolean.class);
private void saveWorldWithFlush(World world) {
if (saveWithFlush != null) {
Logging.fine("Using world save method with flush...");
ReflectHelper.invokeMethod(world, saveWithFlush, true);
} else {
world.save();
}
}
private void cloneWorldTransferData(@NotNull CloneWorldOptions options, @NotNull LoadedMultiverseWorld newWorld) {
DataTransfer<LoadedMultiverseWorld> dataTransfer = transferData(options, options.world());
dataTransfer.pasteAllTo(newWorld);
@@ -901,6 +1028,9 @@ public final class WorldManager {
}
private Option<MultiverseWorld> getUnloadedWorldByAlias(@Nullable String alias) {
if (alias == null || alias.isEmpty()) {
return Option.none();
}
String colourlessAlias = ChatTextFormatter.removeColor(alias);
return Option.ofOptional(worldsMap.values().stream()
.filter(world -> !world.isLoaded())
@@ -936,7 +1066,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable World world) {
return world == null ? Option.none() : Option.of((LoadedMultiverseWorld) loadedWorldsMap.get(world.getName()));
return world == null ? Option.none() : Option.of(loadedWorldsMap.get(world.getName()));
}
/**
@@ -946,7 +1076,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable MultiverseWorld world) {
return world == null ? Option.none() : Option.of((LoadedMultiverseWorld) loadedWorldsMap.get(world.getName()));
return world == null ? Option.none() : Option.of(loadedWorldsMap.get(world.getName()));
}
/**
@@ -956,7 +1086,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable String worldName) {
return Option.of((LoadedMultiverseWorld) loadedWorldsMap.get(worldName));
return Option.of(loadedWorldsMap.get(worldName));
}
/**
@@ -971,9 +1101,12 @@ public final class WorldManager {
}
private Option<LoadedMultiverseWorld> getLoadedWorldByAlias(@Nullable String alias) {
if (alias == null || alias.isEmpty()) {
return Option.none();
}
return Option.ofOptional(loadedWorldsMap.values().stream()
.filter(world -> world.getColourlessAlias().equalsIgnoreCase(ChatColor.stripColor(alias)))
.map(world -> (LoadedMultiverseWorld) world)
.filter(world -> world.getColourlessAlias()
.equalsIgnoreCase(ChatTextFormatter.removeColor(alias)))
.findFirst());
}
@@ -984,7 +1117,6 @@ public final class WorldManager {
*/
public Collection<LoadedMultiverseWorld> getLoadedWorlds() {
return loadedWorldsMap.values().stream()
.map(world -> (LoadedMultiverseWorld) world)
.toList();
}
@@ -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,10 +82,11 @@ 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", "")
return configData.replace("==: MVWorld", "w@: world")
.replace("==: MVSpawnSettings", "")
.replace("==: MVSpawnSubSettings", "")
.replace("==: MVEntryFee", "");
@@ -92,16 +94,26 @@ final class WorldsConfigManager {
.andThenTry(configData -> Files.writeString(worldConfigFile.toPath(), configData))
.andThenTry(() -> {
YamlConfiguration config = YamlConfiguration.loadConfiguration(worldConfigFile);
List<ConfigurationSection> worlds = config.getConfigurationSection("worlds")
.getKeys(false)
.stream()
.map(worldName -> config.getConfigurationSection("worlds." + worldName))
.toList();
ConfigurationSection worldsSection = config.getConfigurationSection("worlds");
if (worldsSection == null) {
worldsSection = config.createSection("worlds");
}
List<String> worldNames = getOldConfigWorldNames(worldsSection);
Map<String, ConfigurationSection> worldConfigMap = new HashMap<>();
for (String worldName : worldNames) {
ConfigurationSection worldSection = worldsSection.getConfigurationSection(worldName);
if (worldSection != null) {
worldConfigMap.put(worldName, worldSection);
}
}
config.set("worlds", null);
for (ConfigurationSection world : worlds) {
config.createSection(world.getName(), world.getValues(true));
for (Map.Entry<String, ConfigurationSection> entry : worldConfigMap.entrySet()) {
config.set(encodeWorldName(entry.getKey()), entry.getValue());
}
config.save(worldConfigFile);
})
@@ -115,6 +127,35 @@ final class WorldsConfigManager {
});
}
private @NotNull List<String> getOldConfigWorldNames(ConfigurationSection worldsSection) {
List<String> worldNames = new ArrayList<>();
recursiveGetOldConfigWorldNames(worldsSection, worldNames);
return worldNames;
}
private void recursiveGetOldConfigWorldNames(ConfigurationSection section, List<String> worldNames) {
Set<String> keys = section.getKeys(false);
if (keys.isEmpty()) {
// No keys in this section, nothing to do
return;
}
if (keys.contains("w@")) {
// this is the world data section already, get path without the "worlds." prefix
worldNames.add(section.getCurrentPath().substring(7));
return;
}
for (String key : keys) {
ConfigurationSection dataSection = section.getConfigurationSection(key);
if (dataSection == null) {
// Something is wrong with the config, skip this key
continue;
}
recursiveGetOldConfigWorldNames(dataSection, worldNames);
}
}
/**
* Parses the worlds.yml file and creates a WorldConfig for each world in the file if it doesn't already exist.
*
@@ -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() +
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.world.options;
import org.bukkit.World;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -24,6 +25,7 @@ public final class ImportWorldOptions {
private World.Environment environment = World.Environment.NORMAL;
private String generator = null;
private boolean useSpawnAdjust = true;
private boolean doFolderCheck = true;
ImportWorldOptions(String worldName) {
this.worldName = worldName;
@@ -119,4 +121,30 @@ public final class ImportWorldOptions {
public boolean useSpawnAdjust() {
return useSpawnAdjust;
}
/**
* Sets whether to ensure the world folder is a valid world before importing it.
*
* @param doFolderCheckInput Whether to do the folder check
* @return This {@link ImportWorldOptions} instance
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public @NotNull ImportWorldOptions doFolderCheck(boolean doFolderCheckInput) {
this.doFolderCheck = doFolderCheckInput;
return this;
}
/**
* Gets whether to ensure the world folder is a valid world before importing it.
*
* @return Whether to do the folder check
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public boolean doFolderCheck() {
return doFolderCheck;
}
}
@@ -0,0 +1,72 @@
package org.mvplugins.multiverse.core.world.options;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
/**
* Options for customizing the loading of a world.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public final class LoadWorldOptions {
/**
* Creates a new {@link LoadWorldOptions} instance with the given world.
*
* @param world The world to load.
* @return A new {@link LoadWorldOptions} instance.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public static LoadWorldOptions world(MultiverseWorld world) {
return new LoadWorldOptions(world);
}
private final MultiverseWorld world;
private boolean doFolderCheck = true;
LoadWorldOptions(MultiverseWorld world) {
this.world = world;
}
/**
* Gets the world to load.
*
* @return The world to load.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public MultiverseWorld world() {
return world;
}
/**
* Sets whether to check if the world folder is valid before loading the world.
* <br />
* This helps to prevent deleted world folders from being re-created.
*
* @return Whether to check if the world folder is valid before loading the world.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public boolean doFolderCheck() {
return doFolderCheck;
}
/**
* Sets whether to check if the world folder is valid before loading the world.
*
* @return This {@link LoadWorldOptions} instance.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public LoadWorldOptions doFolderCheck(boolean doFolderCheck) {
this.doFolderCheck = doFolderCheck;
return this;
}
}
@@ -0,0 +1,103 @@
package org.mvplugins.multiverse.core.world.options;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
/**
* Options for customizing the removal of a world.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public final class RemoveWorldOptions {
/**
* Creates a new {@link RemoveWorldOptions} instance with the given world. The world may be a loaded or unloaded.
*
* @param world The world to remove.
* @return A new {@link RemoveWorldOptions} instance.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public static RemoveWorldOptions world(MultiverseWorld world) {
return new RemoveWorldOptions(world);
}
private final MultiverseWorld world;
private boolean saveBukkitWorld = true;
private boolean unloadBukkitWorld = true;
private RemoveWorldOptions(MultiverseWorld world) {
this.world = world;
}
/**
* Gets the world to remove. The world may be a loaded or unloaded.
*
* @return The world to remove.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public MultiverseWorld world() {
return world;
}
/**
* Sets whether to save the Bukkit world before removing it.
* <br />
* This option only applies when {@link #unloadBukkitWorld()} is true.
*
* @param saveBukkitWorldInput Whether to save the Bukkit world.
* @return This {@link RemoveWorldOptions} instance.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public RemoveWorldOptions saveBukkitWorld(boolean saveBukkitWorldInput) {
this.saveBukkitWorld = saveBukkitWorldInput;
return this;
}
/**
* Gets whether to save the Bukkit world before removing it. By default, this is true.
* <br />
* This option only applies when {@link #unloadBukkitWorld()} is true.
*
* @return Whether to save the Bukkit world.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public boolean saveBukkitWorld() {
return saveBukkitWorld;
}
/**
* Sets whether to unload the Bukkit world before removing it. This option is usually only used if the world is
* managed by another plugin and you want to untrack it from Multiverse without unloading from the server.
*
* @param unloadBukkitWorldInput Whether to unload the Bukkit world.
* @return This {@link RemoveWorldOptions} instance.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public RemoveWorldOptions unloadBukkitWorld(boolean unloadBukkitWorldInput) {
this.unloadBukkitWorld = unloadBukkitWorldInput;
return this;
}
/**
* Gets whether to unload the Bukkit world before removing it. By default, this is true.
*
* @return Whether to unload the Bukkit world.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
public boolean unloadBukkitWorld() {
return unloadBukkitWorld;
}
}
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.world.reasons;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
@@ -30,6 +31,14 @@ public enum ImportFailureReason implements FailureReason {
*/
WORLD_EXIST_LOADED(MVCorei18n.IMPORTWORLD_WORLDEXISTLOADED),
/**
* The import environment input does not match the loaded Bukkit world's environment.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
BUKKIT_ENVIRONMENT_MISMATCH(MVCorei18n.IMPORTWORLD_BUKKITENVIRONMENTMISMATCH),
/**
* Bukkit API failed to create the world.
*/
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.world.reasons;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
@@ -25,11 +26,27 @@ public enum LoadFailureReason implements FailureReason {
*/
WORLD_EXIST_FOLDER(MVCorei18n.LOADWORLD_WORLDEXISTFOLDER),
/**
* The world folder is invalid.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
WORLD_FOLDER_INVALID(MVCorei18n.IMPORTWORLD_WORLDFOLDERINVALID),
/**
* The world is already loaded.
*/
WORLD_EXIST_LOADED(MVCorei18n.LOADWORLD_WORLDEXISTLOADED),
/**
* The mv world's environment does not match the loaded Bukkit world's environment.
*
* @since 5.2
*/
@ApiStatus.AvailableSince("5.2")
BUKKIT_ENVIRONMENT_MISMATCH(MVCorei18n.LOADWORLD_BUKKITENVIRONMENTMISMATCH),
/**
* Bukkit API failed to create the world.
*/
@@ -3,8 +3,10 @@ package org.mvplugins.multiverse.core.world.reasons;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.world.WorldManager;
/**
* Result of a world removal operation.
@@ -12,7 +14,11 @@ import org.mvplugins.multiverse.core.utils.result.FailureReason;
public enum RemoveFailureReason implements FailureReason {
/**
* The world does not exist.
*
* @deprecated No longer in use as {@link WorldManager#removeWorld(String)} is now deprecated.
*/
@Deprecated(forRemoval = true, since = "5.2")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
WORLD_NON_EXISTENT(MVCorei18n.REMOVEWORLD_WORLDNONEXISTENT),
/**
@@ -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,19 +281,21 @@ 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!
mv-core.importworld.worldexistunloaded=World '{world}' already exists, but it's not loaded!&f Type '&a/mv load {world}&f' if you wish to load it.
mv-core.importworld.worldexistloaded=World '{world}' already exists!
mv-core.importworld.worldfolderinvalid=World '{world}' folder contents does not seem to be a valid world!
mv-core.importworld.worldfolderinvalid=World '{world}' folder contents does not seem to be a valid world!\n&cIf the server software does something different with world folders, or you are very certain the world is valid, use '&3--skip-folder-check&c' flag.
mv-core.importworld.bukkitenvironmentmismatch=Environment mismatch detected!&f The world '{world}' is already loaded with environment '{bukkitEnvironment}', but Multiverse is trying to import it with environment '{mvEnvironment}'.
mv-core.loadworld.worldalreadyloading=World '{world}' is already loading! Please wait...
mv-core.loadworld.worldnonexistent=World '{world}' not found! Use '&a/mv create {world} <environment>&f' to create it.
mv-core.loadworld.worldexistfolder=World '{world}' exists in server folders, but it's not known to Multiverse!&f Type '&a/mv import {world} <environment>&f' if you wish to import it.
mv-core.loadworld.worldexistloaded=World '{world}' is already loaded!
mv-core.loadworld.bukkitenvironmentmismatch=Environment mismatch detected!&f The world '{world}' is already loaded with environment '{bukkitEnvironment}', but Multiverse is trying to load it with environment '{mvEnvironment}'.
mv-core.removeworld.worldnonexistent=World '{world}' not found!
@@ -325,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?
@@ -0,0 +1,58 @@
package org.mvplugins.multiverse.core.utils
import org.mvplugins.multiverse.core.utils.matcher.ExactStringMatcher
import org.mvplugins.multiverse.core.utils.matcher.RegexStringMatcher
import org.mvplugins.multiverse.core.utils.matcher.WildcardStringMatcher
import kotlin.test.Test
class StringMatcherTest {
@Test
fun `exact string matcher - single string`() {
val matcher = ExactStringMatcher("test")
assert(matcher.matches("test"))
assert(!matcher.matches("Test"))
assert(!matcher.matches("testing"))
}
@Test
fun `exact string matcher - multiple strings`() {
val matcher = ExactStringMatcher(setOf("test", "example", "sample"));
assert(matcher.matches("test"))
assert(matcher.matches("example"))
assert(matcher.matches("sample"))
assert(!matcher.matches("Test"))
assert(!matcher.matches("testing"))
}
@Test
fun `wildcard string matcher - single`() {
val matcher = WildcardStringMatcher("test*")
assert(matcher.matches("test"))
assert(matcher.matches("test123"))
assert(matcher.matches("testing"))
assert(!matcher.matches("TEST"))
assert(!matcher.matches("nottest123"))
}
@Test
fun `wildcard string matcher - multiple`() {
val matcher = WildcardStringMatcher("test-*_*-world")
assert(matcher.matches("test-123_pl-world"))
assert(matcher.matches("test-abc_xyz-world"))
assert(matcher.matches("test-hello_world-world"))
assert(!matcher.matches("test-hello-world"))
assert(!matcher.matches("test-hello_world-world!"))
assert(!matcher.matches("TEST-123_PL-WORLD"))
}
@Test
fun `regex string matcher`() {
val matcher = RegexStringMatcher("r=^test[0-9]+$")
assert(matcher.matches("test123"))
assert(matcher.matches("test456"))
assert(!matcher.matches("test"))
assert(!matcher.matches("TEST123"))
assert(!matcher.matches("testing"))
}
}
@@ -49,7 +49,7 @@ class WorldConfigMangerTest : TestWithMockBukkit() {
assertEquals(MVEconomist.VAULT_ECONOMY_MATERIAL, endWorldConfig.entryFeeCurrency)
assertEquals(0.0, endWorldConfig.entryFeeAmount)
val worldConfig = worldConfigManager.getWorldConfig("world").orNull
val worldConfig = worldConfigManager.getWorldConfig("world.a.b").orNull
assertNotNull(worldConfig)
assertEquals(-5176596003035866649, worldConfig.seed)
@@ -58,6 +58,9 @@ class WorldConfigMangerTest : TestWithMockBukkit() {
assertEquals(Material.DIRT, worldConfig.entryFeeCurrency)
assertEquals(5.0, worldConfig.entryFeeAmount)
val world2Config = worldConfigManager.getWorldConfig("world.a.c").orNull
assertNotNull(world2Config)
assertConfigEquals("/worlds/migrated_worlds.yml", "worlds.yml")
}
@@ -10,7 +10,9 @@ import org.mvplugins.multiverse.core.event.world.*
import org.mvplugins.multiverse.core.world.options.CloneWorldOptions
import org.mvplugins.multiverse.core.world.options.CreateWorldOptions
import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions
import org.mvplugins.multiverse.core.world.options.LoadWorldOptions
import org.mvplugins.multiverse.core.world.options.RegenWorldOptions
import org.mvplugins.multiverse.core.world.options.RemoveWorldOptions
import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions
import org.mvplugins.multiverse.core.world.reasons.CloneFailureReason
import org.mvplugins.multiverse.core.world.reasons.CreateFailureReason
@@ -102,7 +104,7 @@ class WorldManagerTest : TestWithMockBukkit() {
@Test
fun `Remove world`() {
assertTrue(worldManager.removeWorld(world).isSuccess)
assertTrue(worldManager.removeWorld(RemoveWorldOptions.world(world)).isSuccess)
assertFalse(worldManager.getWorld("world").isDefined)
assertFalse(worldManager.getLoadedWorld("world").isDefined)
assertFalse(worldManager.getUnloadedWorld("world").isDefined)
@@ -132,13 +134,23 @@ class WorldManagerTest : TestWithMockBukkit() {
assertTrue(worldManager.getWorld("world2").isDefined)
assertTrue(worldManager.getUnloadedWorld("world2").isDefined)
assertTrue(worldManager.loadWorld("world2").isSuccess)
assertTrue(worldManager.loadWorld(LoadWorldOptions.world(world2)).isSuccess)
assertTrue(world2.isLoaded)
assertTrue(worldManager.getLoadedWorld("world2").flatMap{ w -> w.bukkitWorld }.isDefined)
assertTrue(worldManager.getLoadedWorld("world2").isDefined)
assertFalse(worldManager.getUnloadedWorld("world2").isDefined)
}
@Test
fun `Load world failed - invalid world folder`() {
assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world2)).isSuccess)
File(Bukkit.getWorldContainer(), "world2/").deleteRecursively()
assertEquals(
LoadFailureReason.WORLD_FOLDER_INVALID,
worldManager.loadWorld(LoadWorldOptions.world(world2)).failureReason
)
}
@Test
fun `Load world failed - non-existent world`() {
assertEquals(
@@ -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
+50 -1
View File
@@ -46,7 +46,7 @@ world_the_end:
tick-rate: -1
world-blacklist: []
version: 1.2
world:
world[dot]a[dot]b:
adjust-spawn: true
alias: ''
allow-advancement-grant: true
@@ -95,3 +95,52 @@ world:
world-blacklist:
- test
version: 1.2
world[dot]a[dot]c:
adjust-spawn: true
alias: ''
allow-advancement-grant: true
allow-flight: false
allow-weather: true
anchor-respawn: true
auto-heal: true
auto-load: true
bed-respawn: true
difficulty: normal
entry-fee:
enabled: true
amount: 5.0
currency: DIRT
environment: normal
gamemode: survival
biome: ''
generator: ''
hidden: false
hunger: true
keep-spawn-in-memory: true
player-limit: -1
portal-form: all
pvp: true
respawn-world: ''
scale: 1.0
seed: -5176596003035866649
spawn-location:
==: MVSpawnLocation
x: -64.0
y: 64.0
z: 48.0
pitch: 0.0
yaw: 0.0
spawning:
animal:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
monster:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
world-blacklist:
- test
version: 1.2
+50 -1
View File
@@ -46,7 +46,7 @@ worlds:
generator: 'null'
playerLimit: '-1'
allowFlight: 'true'
world:
world.a.b:
==: MVWorld
hidden: 'false'
alias: ''
@@ -95,3 +95,52 @@ worlds:
generator: 'null'
playerLimit: '-1'
allowFlight: 'false'
world.a.c:
==: MVWorld
hidden: 'false'
alias: ''
color: WHITE
style: NORMAL
pvp: 'true'
scale: '1.0'
respawnWorld: ''
allowWeather: 'true'
difficulty: NORMAL
spawning:
==: MVSpawnSettings
animals:
==: MVSpawnSubSettings
spawn: 'true'
spawnrate: '-1'
exceptions: []
monsters:
==: MVSpawnSubSettings
spawn: 'true'
spawnrate: '-1'
exceptions: []
entryfee:
==: MVEntryFee
amount: '5.0'
currency: 'DIRT'
hunger: 'true'
autoHeal: 'true'
adjustSpawn: 'true'
portalForm: ALL
gameMode: SURVIVAL
keepSpawnInMemory: 'true'
spawnLocation:
==: MVSpawnLocation
x: -64.0
y: 64.0
z: 48.0
pitch: 0.0
yaw: 0.0
autoLoad: 'true'
bedRespawn: 'true'
worldBlacklist:
- test
environment: NORMAL
seed: '-5176596003035866649'
generator: 'null'
playerLimit: '-1'
allowFlight: 'false'