Compare commits

...
28 Commits
Author SHA1 Message Date
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
24 changed files with 981 additions and 96 deletions

No files matched your search

+3 -3
View File
@@ -1,7 +1,7 @@
import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'org.mvplugins.multiverse-plugin' version '1.2.0'
id 'org.mvplugins.multiverse-plugin' version '1.2.1'
}
group = 'org.mvplugins.multiverse.core'
@@ -25,9 +25,9 @@ repositories {
}
configure(apiDependencies) {
serverApiVersion = '1.19.4-R0.1-SNAPSHOT'
serverApiVersion = '1.21.8-R0.1-SNAPSHOT'
mockBukkitServerApiVersion = '1.21'
mockBukkitVersion = '4.72.2'
mockBukkitVersion = '4.69.2'
}
dependencies {
@@ -87,7 +87,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);
@@ -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) {
@@ -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}
*/
@@ -137,6 +137,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 +512,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.")
@@ -19,8 +19,12 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.utils.position.PositionNumber;
import org.mvplugins.multiverse.core.utils.position.VectorPosition;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
@@ -62,7 +66,11 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
* @return A new {@link ExactDestinationInstance}
*/
public @NotNull ExactDestinationInstance fromLocation(@NotNull Location location) {
return new ExactDestinationInstance(this, new UnloadedWorldLocation(location));
return new ExactDestinationInstance(
this,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
);
}
/**
@@ -73,12 +81,16 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
@NotNull CommandSender sender,
@NotNull String destinationParams
) {
String[] items = REPatterns.COLON.split(destinationParams);
String[] items = REPatterns.COLON.split(destinationParams, 2);
if (items.length < 2) {
if (items[0].equals("@here")) {
return getLocationFromSender(sender)
.map(location -> Attempt.<ExactDestinationInstance, InstanceFailureReason>success(
new ExactDestinationInstance(this, new UnloadedWorldLocation(location))
new ExactDestinationInstance(
this,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
)
))
.getOrElse(() -> Attempt.failure(InstanceFailureReason.INVALID_COORDINATES_FORMAT)); // todo: specific failure reason for this case
}
@@ -86,41 +98,21 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
}
String worldName = items[0];
String coordinates = items[1];
String[] coordinatesParams = REPatterns.COMMA.split(coordinates);
if (coordinatesParams.length != 3) {
return Attempt.failure(InstanceFailureReason.INVALID_COORDINATES_FORMAT);
}
String positionStr = items[1];
World world = getLoadedMultiverseWorld(worldName).flatMap(LoadedMultiverseWorld::getBukkitWorld).getOrNull();
if (world == null) {
return Attempt.failure(InstanceFailureReason.WORLD_NOT_FOUND, Replace.WORLD.with(worldName));
}
UnloadedWorldLocation location;
EntityPosition position;
try {
location = new UnloadedWorldLocation(
world,
Double.parseDouble(coordinatesParams[0]),
Double.parseDouble(coordinatesParams[1]),
Double.parseDouble(coordinatesParams[2])
);
} catch (NumberFormatException e) {
position = EntityPosition.fromString(positionStr);
} catch (PositionParseException e) {
return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e));
}
if (items.length == 4) {
String pitch = items[2];
String yaw = items[3];
try {
location.setPitch(Float.parseFloat(pitch));
location.setYaw(Float.parseFloat(yaw));
} catch (NumberFormatException e) {
return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e));
}
}
return Attempt.success(new ExactDestinationInstance(this, location));
return Attempt.success(new ExactDestinationInstance(this, worldName, position));
}
//TODO: Extract to a world finder class
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.destination.core;
import io.vavr.control.Option;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
@@ -8,22 +9,30 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.utils.position.VectorPosition;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
/**
* Destination instance implementation for the {@link ExactDestination}.
*/
public final class ExactDestinationInstance extends DestinationInstance<ExactDestinationInstance, ExactDestination> {
private final UnloadedWorldLocation location;
private final String worldName;
private final EntityPosition position;
/**
* Constructor.
*
* @param location The location to teleport to.
* @param destination The parent destination.
* @param worldName The name of the world.
* @param position The position in the world.
*/
ExactDestinationInstance(@NotNull ExactDestination destination, @NotNull UnloadedWorldLocation location) {
ExactDestinationInstance(@NotNull ExactDestination destination,
@NotNull String worldName,
@NotNull EntityPosition position) {
super(destination);
this.location = location;
this.worldName = worldName;
this.position = position;
}
/**
@@ -31,10 +40,13 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
*/
@Override
public @NotNull Option<Location> getLocation(@NotNull Entity teleportee) {
if (location.getWorld() == null) {
World world = Bukkit.getWorld(worldName);
if (world == null) {
return Option.none();
}
return Option.of(location.toBukkitLocation());
Location destinationLocation = position.toBukkitLocation(teleportee.getLocation());
destinationLocation.setWorld(world);
return Option.of(destinationLocation);
}
/**
@@ -58,7 +70,7 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
*/
@Override
public @NotNull Option<String> getFinerPermissionSuffix() {
return Option.of(location.getWorld()).map(World::getName);
return Option.of(worldName);
}
/**
@@ -66,7 +78,6 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
*/
@Override
public @NotNull String serialise() {
return location.getWorldName() + ":" + location.getX() + "," + location.getY()
+ "," + location.getZ() + ":" + location.getPitch() + ":" + location.getYaw();
return worldName + ":" + position.toString();
}
}
@@ -0,0 +1,23 @@
package org.mvplugins.multiverse.core.exceptions.utils.position;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.locale.message.Message;
public class PositionParseException extends MultiverseException {
public PositionParseException(String message) {
super(message);
}
public PositionParseException(@Nullable Message message) {
super(message);
}
public PositionParseException(@Nullable String message, @Nullable Throwable cause) {
super(message, cause);
}
public PositionParseException(@Nullable Message message, @Nullable Throwable cause) {
super(message, cause);
}
}
@@ -346,6 +346,11 @@ public enum MVCorei18n implements MessageKeyProvider {
EXCEPTION_MULTIVERSEWORLD_UNLOADPLAYERSINWORLD,
EXCEPTION_MULTIVERSEWORLD_UNLOADERROR,
// multiverse position parse exception
EXCEPTION_POSITIONPARSE_INVALIDDIRECTION,
EXCEPTION_POSITIONPARSE_INVALIDCOORDINATES,
EXCEPTION_POSITIONPARSE_INVALIDNUMBER,
// generic
GENERIC_SUCCESS,
GENERIC_FAILURE,
@@ -77,7 +77,7 @@ public final class FileUtils {
*/
public @Nullable File getServerProperties() {
if (this.serverProperties == null) {
this.serverProperties = findFileFromServerDirectory("server.properties");
this.serverProperties = findFileFromServerDirectory(config.getServerPropertiesPath());
Logging.finer("server.properties: %s", this.serverProperties);
}
return this.serverProperties;
@@ -0,0 +1,150 @@
package org.mvplugins.multiverse.core.utils.position;
import org.bukkit.Location;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.utils.REPatterns;
/**
* Represents a position for an entity in 3D space, including both coordinates and facing direction.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public class EntityPosition {
/**
* Creates an EntityPosition with absolute coordinates and direction.
*
* @param x The absolute X coordinate.
* @param y The absolute Y coordinate.
* @param z The absolute Z coordinate.
* @param pitch The absolute pitch (vertical angle).
* @param yaw The absolute yaw (horizontal angle).
* @return A new EntityPosition instance with the specified absolute coordinates and direction.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static EntityPosition ofAbsolute(double x, double y, double z, double pitch, double yaw) {
return new EntityPosition(
VectorPosition.ofAbsolute(x, y, z),
FaceDirection.ofAbsolute(pitch, yaw)
);
}
/**
* Creates an EntityPosition from a Bukkit Location with absolute coordinates and direction.
*
* @param location The Bukkit Location to convert.
* @return A new EntityPosition instance representing the given location.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static EntityPosition ofLocation(Location location) {
return new EntityPosition(VectorPosition.ofLocation(location), FaceDirection.ofLocation(location));
}
/**
* Parses an EntityPosition from a string representation.
* The expected format is "&lt;x&gt;,&lt;y&gt;,&lt;z&gt;:&lt;pitch&gt;:&lt;yaw&gt;" for absolute coordinates and direction,
* or "&lt;x&gt;,&lt;y&gt;,&lt;z&gt;" for absolute coordinates with default direction (0 pitch, 0 yaw).
* <br>
* Relative coordinates and direction can be specified using the '~' prefix, e.g., "~10,~,~-10:0:90".
*
* @param positionStr The string representation of the position.
* @return A new EntityPosition instance parsed from the string.
* @throws PositionParseException If the string format is invalid.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static EntityPosition fromString(String positionStr) throws PositionParseException {
String[] parts = REPatterns.COLON.split(positionStr, 2);
return parts.length == 2
? new EntityPosition(VectorPosition.fromString(parts[0]), FaceDirection.fromString(parts[1]))
: new EntityPosition(VectorPosition.fromString(parts[0]), FaceDirection.ofAbsolute(0, 0));
}
private final VectorPosition vector;
private final FaceDirection direction;
/**
* Creates a new EntityPosition with the specified vector and direction.
*
* @param vector The vector position (coordinates).
* @param direction The facing direction (pitch and yaw).
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public EntityPosition(VectorPosition vector, FaceDirection direction) {
this.vector = vector;
this.direction = direction;
}
/**
* Gets the vector position (coordinates).
*
* @return The vector position.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public VectorPosition getVector() {
return vector;
}
/**
* Gets the facing direction (pitch and yaw).
*
* @return The facing direction.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public FaceDirection getDirection() {
return direction;
}
/**
* Augments a given Bukkit Location by applying the relative components of this EntityPosition.
* This modifies the provided Location in place.
*
* @param base The base Bukkit Location to augment and offset for relative positioning as required.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public void augmentBukkitLocation(Location base) {
vector.augmentBukkitLocation(base);
direction.augmentBukkitLocation(base);
}
/**
* Converts this EntityPosition to a new Bukkit Location based on a given base Location.
* This does not modify the base Location, but returns a new Location instance.
*
* @param base The base Bukkit Location to use as a reference for relative positioning as required.
* @return A new Bukkit Location representing this EntityPosition.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public Location toBukkitLocation(Location base) {
return new Location(
base.getWorld(),
vector.getX().getValue(base.getX()),
vector.getY().getValue(base.getY()),
vector.getZ().getValue(base.getZ()),
(float) direction.getYaw().getValue(base.getYaw()),
(float) direction.getPitch().getValue(base.getPitch())
);
}
@Override
public String toString() {
return vector + ":" + direction;
}
}
@@ -0,0 +1,138 @@
package org.mvplugins.multiverse.core.utils.position;
import org.bukkit.Location;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.REPatterns;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Represents a direction to face, defined by pitch and yaw.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public class FaceDirection {
/**
* Creates a FaceDirection with absolute pitch and yaw.
*
* @param pitch The absolute pitch (vertical angle).
* @param yaw The absolute yaw (horizontal angle).
* @return A new FaceDirection instance with the specified absolute pitch and yaw.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static FaceDirection ofAbsolute(double pitch, double yaw) {
return new FaceDirection(
PositionNumber.ofAbsolute(pitch),
PositionNumber.ofAbsolute(yaw)
);
}
/**
* Gets the pitch and yaw from a Bukkit Location to create an FaceDirection with absolute values.
*
* @param location The Bukkit Location to convert.
* @return A new FaceDirection instance representing the direction of the given location.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static FaceDirection ofLocation(org.bukkit.Location location) {
return new FaceDirection(
PositionNumber.ofAbsolute(location.getPitch()),
PositionNumber.ofAbsolute(location.getYaw())
);
}
/**
* Parses a FaceDirection from a string representation.
* The expected format is "&lt;pitch&gt;:&lt;yaw&gt;" for absolute pitch and yaw.
* <br>
* Relative pitch and yaw can be specified using the '~' prefix, e.g., "~:~5".
*
* @param directionStr The string representation of the direction.
* @return A new FaceDirection instance parsed from the string.
* @throws PositionParseException If the string format is invalid.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static FaceDirection fromString(String directionStr) throws PositionParseException {
String[] parts = REPatterns.COLON.split(directionStr, 2);
if (parts.length != 2) {
throw new PositionParseException(Message.of(MVCorei18n.EXCEPTION_POSITIONPARSE_INVALIDDIRECTION,
replace("{format}").with(directionStr)));
}
//TODO: Add support for compass directions (N, S, E, W, NE, NW, SE, SW) for yaw
PositionNumber pitch = PositionNumber.fromString(parts[0]);
PositionNumber yaw = PositionNumber.fromString(parts[1]);
return new FaceDirection(pitch, yaw);
}
private final PositionNumber pitch;
private final PositionNumber yaw;
/**
* Creates a new FaceDirection with the specified pitch and yaw.
*
* @param pitch The pitch (vertical angle).
* @param yaw The yaw (horizontal angle).
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public FaceDirection(PositionNumber pitch, PositionNumber yaw) {
this.pitch = pitch;
this.yaw = yaw;
}
/**
* Gets the pitch component of this FaceDirection.
*
* @return The pitch number representation.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getPitch() {
return pitch;
}
/**
* Gets the yaw component of this FaceDirection.
*
* @return The yaw number representation.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getYaw() {
return yaw;
}
/**
* Applies this FaceDirection to a given Bukkit Location, modifying its pitch and yaw.
* <br>
* Relative pitch and yaw will be offset based on the current values in the Location.
*
* @param base The Bukkit Location to modify.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public void augmentBukkitLocation(Location base) {
base.setPitch((float) pitch.getValue(base.getPitch()));
base.setYaw((float) yaw.getValue(base.getYaw()));
}
@Override
public String toString() {
return pitch + ":" + yaw;
}
}
@@ -0,0 +1,187 @@
package org.mvplugins.multiverse.core.utils.position;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Represents a number that can be either absolute or relative (prefixed with '~').
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public sealed interface PositionNumber permits PositionNumber.Relative, PositionNumber.Absolute {
/**
* Creates an absolute PositionNumber. Absolute numbers represent fixed coordinates that will not be adjusted
* based on any base value.
*
* @param value the absolute value
* @return a PositionNumber representing the absolute value
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
static PositionNumber ofAbsolute(double value) {
return new Absolute(value);
}
/**
* Creates a relative PositionNumber. Relative numbers are prefixed with '~' and represent an offset
* from a base value. For example, a relative PositionNumber of '~5' means 5 units more than the base value,
* while '~ -3' means 3 units less than the base value.
*
* @param value the relative offset value
* @return a PositionNumber representing the relative offset
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
static PositionNumber ofRelative(double value) {
return new Relative(value);
}
/**
* Parses a PositionNumber from a string. The string can represent either an absolute number (e.g., "10.5")
* or a relative number prefixed with '~' (e.g., "~5" or "~ -3"). If the string is "~" with no number,
* it is treated as a relative value of 0.
*
* @param string the string to parse
* @return a PositionNumber representing the parsed value.
* @throws PositionParseException If the string format is invalid.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
static PositionNumber fromString(String string) throws PositionParseException {
if (string.startsWith("~")) {
if (string.length() == 1) {
return new Relative(0);
}
return new Relative(tryParseDouble(string.substring(1)));
}
return new Absolute(tryParseDouble(string));
}
private static double tryParseDouble(String str) throws PositionParseException {
return Try.of(() -> Double.parseDouble(str))
.getOrElseThrow(throwable -> new PositionParseException(
Message.of(MVCorei18n.EXCEPTION_POSITIONPARSE_INVALIDNUMBER,
replace("{number}").with(str))));
}
/**
* Calculates the effective value based on the base value.
* For absolute PositionNumbers, this returns the stored value.
* For relative PositionNumbers, this returns base + stored value.
*
* @param base the base value to use for relative calculations
* @return the calculated effective value
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
double getValue(double base);
/**
* Checks if this PositionNumber is relative.
*
* @return true if this is a relative PositionNumber, false if absolute
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
boolean isRelative();
/**
* Checks if this PositionNumber is absolute.
*
* @return true if this is an absolute PositionNumber, false if relative
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
boolean isAbsolute();
/**
* Gets the raw stored value without any base adjustment.
*
* @return the raw value (for relative, this is the offset; for absolute, this is the fixed value)
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
double getRawValue();
final class Relative implements PositionNumber {
private final double value;
Relative(double value) {
this.value = value;
}
@Override
public double getValue(double base) {
return base + value;
}
@Override
public boolean isRelative() {
return true;
}
@Override
public boolean isAbsolute() {
return false;
}
@Override
public double getRawValue() {
return value;
}
@Override
public String toString() {
return "~" + value;
}
}
final class Absolute implements PositionNumber {
private final double value;
Absolute(double value) {
this.value = value;
}
@Override
public double getValue(double base) {
return value;
}
@Override
public boolean isRelative() {
return false;
}
@Override
public boolean isAbsolute() {
return true;
}
@Override
public double getRawValue() {
return value;
}
@Override
public String toString() {
return Double.toString(value);
}
}
}
@@ -0,0 +1,227 @@
package org.mvplugins.multiverse.core.utils.position;
import org.bukkit.Location;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.REPatterns;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Represents an x, y, z position in 3D space, with support for absolute and relative coordinates.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public class VectorPosition {
/**
* Creates a VectorPosition with absolute coordinates.
* @param x The absolute x coordinate.
* @param y The absolute y coordinate.
* @param z The absolute z coordinate.
* @return A new VectorPosition instance with the specified absolute coordinates.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static VectorPosition ofAbsolute(double x, double y, double z) {
return new VectorPosition(
PositionNumber.ofAbsolute(x),
PositionNumber.ofAbsolute(y),
PositionNumber.ofAbsolute(z)
);
}
/**
* Creates a VectorPosition from a Bukkit Vector, using absolute coordinates.
*
* @param vector The Bukkit Vector to convert.
* @return A new VectorPosition instance representing the given vector.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static VectorPosition ofVector(Vector vector) {
return new VectorPosition(
PositionNumber.ofAbsolute(vector.getX()),
PositionNumber.ofAbsolute(vector.getY()),
PositionNumber.ofAbsolute(vector.getZ())
);
}
/**
* Creates a VectorPosition from a Bukkit Location, using absolute coordinates.
*
* @param location The Bukkit Location to convert.
* @return A new VectorPosition instance representing the given location.
*
* @since 5.3
*/
public static VectorPosition ofLocation(Location location) {
return new VectorPosition(
PositionNumber.ofAbsolute(location.getX()),
PositionNumber.ofAbsolute(location.getY()),
PositionNumber.ofAbsolute(location.getZ())
);
}
/**
* Parses a VectorPosition from a string representation.
* The expected format is "&lt;x&gt;,&lt;y&gt;,&lt;z&gt;" for absolute or relative coordinates.
* <br>
* Relative coordinates can be specified using the '~' prefix, e.g., "~10,~,~-10".
*
* @param coordStr The string representation of the coordinates.
* @return A new VectorPosition instance parsed from the string.
* @throws PositionParseException If the string format is invalid.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public static VectorPosition fromString(String coordStr) throws PositionParseException {
String[] parts = REPatterns.COMMA.split(coordStr);
if (parts.length != 3) {
throw new PositionParseException(Message.of(MVCorei18n.EXCEPTION_POSITIONPARSE_INVALIDCOORDINATES,
replace("{format}").with(coordStr)));
}
return new VectorPosition(
PositionNumber.fromString(parts[0]),
PositionNumber.fromString(parts[1]),
PositionNumber.fromString(parts[2])
);
}
private final PositionNumber x;
private final PositionNumber y;
private final PositionNumber z;
/**
* Creates a new VectorPosition with the specified PositionNumbers for x, y, and z.
*
* @param x The PositionNumber for the x coordinate.
* @param y The PositionNumber for the y coordinate.
* @param z The PositionNumber for the z coordinate.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public VectorPosition(PositionNumber x, PositionNumber y, PositionNumber z) {
this.x = x;
this.y = y;
this.z = z;
}
/**
* Gets the PositionNumber for the x coordinate.
*
* @return The PositionNumber representing the x coordinate.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getX() {
return x;
}
/**
* Gets the PositionNumber for the y coordinate.
*
* @return The PositionNumber representing the y coordinate.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getY() {
return y;
}
/**
* Gets the PositionNumber for the z coordinate.
*
* @return The PositionNumber representing the z coordinate.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public PositionNumber getZ() {
return z;
}
/**
* Augments the given Bukkit Vector in place by applying this VectorPosition's coordinates.
* Relative coordinates will adjust the existing values, while absolute coordinates will set them directly.
*
* @param base The Bukkit Vector to augment.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public void augmentBukkitVector(Vector base) {
base.setX(x.getValue(base.getX()));
base.setY(y.getValue(base.getY()));
base.setZ(z.getValue(base.getZ()));
}
/**
* Augments the given Bukkit Location in place by applying this VectorPosition's coordinates.
* Relative coordinates will adjust the existing values, while absolute coordinates will set them directly.
*
* @param location The Bukkit Location to augment.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public void augmentBukkitLocation(Location location) {
location.setX(x.getValue(location.getX()));
location.setY(y.getValue(location.getY()));
location.setZ(z.getValue(location.getZ()));
}
/**
* Converts this VectorPosition to a new Bukkit Vector based on a given base Vector.
* This does not modify the base Vector, but returns a new Vector instance.
*
* @param base The base Bukkit Vector to use as a reference for relative positioning as required.
* @return A new Bukkit Vector representing this VectorPosition.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public Vector toBukkitVector(Vector base) {
return new Vector(
x.getValue(base.getX()),
y.getValue(base.getY()),
z.getValue(base.getZ())
);
}
/**
* Converts this VectorPosition to a new Bukkit Location based on a given base Location.
* This does not modify the base Location, but returns a new Location instance.
*
* @param base The base Bukkit Location to use as a reference for relative positioning as required.
* @return A new Bukkit Location representing this VectorPosition.
*
* @since 5.3
*/
@ApiStatus.AvailableSince("5.3")
public Location toBukkitLocation(Location base) {
return new Location(
base.getWorld(),
x.getValue(base.getX()),
y.getValue(base.getY()),
z.getValue(base.getZ()),
base.getYaw(),
base.getPitch()
);
}
@Override
public String toString() {
return x + "," + y + "," + z;
}
}
@@ -16,6 +16,7 @@ import org.bukkit.configuration.MemoryConfiguration;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
import org.mvplugins.multiverse.core.event.world.MVWorldPropertyChangedEvent;
import org.mvplugins.multiverse.core.config.node.ConfigNode;
@@ -37,11 +38,13 @@ final class WorldConfigNodes {
private final NodeGroup nodes = new NodeGroup();
private WorldManager worldManager;
private EnforcementHandler enforcementHandler;
private CoreConfig config;
private MultiverseWorld world = null;
WorldConfigNodes(@NotNull MultiverseCore multiverseCore) {
this.worldManager = multiverseCore.getServiceLocator().getService(WorldManager.class);
this.enforcementHandler = multiverseCore.getServiceLocator().getService(EnforcementHandler.class);
this.config = multiverseCore.getServiceLocator().getService(CoreConfig.class);
}
MultiverseWorld getWorld() {
@@ -244,7 +247,7 @@ final class WorldConfigNodes {
}));
final ConfigNode<EntitySpawnConfig> enititySpawnConfig = node(ConfigNode.builder("spawning", EntitySpawnConfig.class)
.defaultValue(() -> EntitySpawnConfig.fromSection(new MemoryConfiguration()))
.defaultValue(() -> EntitySpawnConfig.fromSection(config, new MemoryConfiguration()))
.hidden()
.serializer(new NodeSerializer<>() {
@Override
@@ -252,7 +255,7 @@ final class WorldConfigNodes {
ConfigurationSection spawnSection = (object instanceof ConfigurationSection section)
? section
: new MemoryConfiguration();
return EntitySpawnConfig.fromSection(spawnSection);
return EntitySpawnConfig.fromSection(config, spawnSection);
}
@Override
@@ -649,12 +649,18 @@ public final class WorldManager {
*/
public Attempt<String, DeleteFailureReason> deleteWorld(@NotNull DeleteWorldOptions options) {
return getLoadedWorld(options.world()).fold(
() -> loadWorld(LoadWorldOptions.world(options.world()))
.transform(DeleteFailureReason.LOAD_FAILED)
.mapAttempt(world -> doDeleteWorld(world, options)),
() -> loadThenDeleteWorld(options),
world -> doDeleteWorld(world, options));
}
private Attempt<String, DeleteFailureReason> loadThenDeleteWorld(@NotNull DeleteWorldOptions options) {
return loadWorld(LoadWorldOptions.world(options.world()))
.fold(
ignore -> worldActionResult(DeleteFailureReason.LOAD_FAILED, options.world().getName()),
loadedWorld -> doDeleteWorld(loadedWorld, options)
);
}
/**
* Deletes an existing multiverse world entirely. Warning: This will delete all world files.
*
@@ -19,6 +19,7 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
/**
* Manages the worlds.yml file.
@@ -81,8 +82,9 @@ final class WorldsConfigManager {
}
// Copy old config file to `worlds.yml.old`
Path oldWorldConfig = worldConfigFile.toPath().getParent().resolve(CONFIG_FILENAME + ".old");
Files.copy(worldConfigFile.toPath(), oldWorldConfig, COPY_ATTRIBUTES);
Path oldWorldConfig = worldConfigFile.toPath().getParent()
.resolve(CONFIG_FILENAME + ".old." + System.currentTimeMillis());
Files.copy(worldConfigFile.toPath(), oldWorldConfig, COPY_ATTRIBUTES, REPLACE_EXISTING);
return configData.replace("==: MVWorld", "w@: world")
.replace("==: MVSpawnSettings", "")
@@ -1,29 +1,35 @@
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.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) {
@@ -52,17 +58,17 @@ public final class EntitySpawnConfig {
}
@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));
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,19 @@ 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) != 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("This is NOT a bug in Multiverse. Do NOT report this to Multiverse support.");
t.printStackTrace();
// Assume it's a generator plugin since it tried to do something, most likely the id is wrong.
return true;
}
}
/**
@@ -206,6 +217,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;
@@ -22,13 +22,13 @@ import java.util.Objects;
* This is useful to store location with world that may not be loaded yet or have been unloaded at some point.
*/
@SerializableAs("UnloadedWorldLocation")
public final class UnloadedWorldLocation extends Location {
public class UnloadedWorldLocation extends Location {
public static UnloadedWorldLocation fromLocation(@NotNull Location location) {
return new UnloadedWorldLocation(location.getWorld(), location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
}
private String worldName;
private @Nullable String worldName;
public UnloadedWorldLocation(@Nullable String worldName, double x, double y, double z) {
super(null, x, y, z);
@@ -67,7 +67,7 @@ public final class UnloadedWorldLocation extends Location {
this.worldName = worldName;
}
public String getWorldName() {
public @Nullable String getWorldName() {
return worldName;
}
@@ -160,10 +160,10 @@ public final class UnloadedWorldLocation extends Location {
@Override
public int hashCode() {
int hash = 3;
hash = 19 * hash + worldName.hashCode();
hash = 19 * hash + (int) (Double.doubleToLongBits(this.getX()) ^ (Double.doubleToLongBits(this.getX()) >>> 32));
hash = 19 * hash + (int) (Double.doubleToLongBits(this.getY()) ^ (Double.doubleToLongBits(this.getY()) >>> 32));
hash = 19 * hash + (int) (Double.doubleToLongBits(this.getZ()) ^ (Double.doubleToLongBits(this.getZ()) >>> 32));
hash = 19 * hash + String.valueOf(worldName).hashCode();
hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.getX()));
hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.getY()));
hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.getZ()));
hash = 19 * hash + Float.floatToIntBits(this.getPitch());
hash = 19 * hash + Float.floatToIntBits(this.getYaw());
return hash;
@@ -171,7 +171,7 @@ public final class UnloadedWorldLocation extends Location {
@Override
public String toString() {
return "Location{" +
return "UnloadedWorldLocation{" +
"world=" + worldName +
",x=" + getX() +
",y=" + getY() +
@@ -255,7 +255,7 @@ mv-core.destination.cannon.failurereason.invalidformat=&cCannon destination form
mv-core.destination.exact.failurereason.invalidformat=&cExact destination format is: &6e:worldname:x,y,z:pitch:yaw
mv-core.destination.player.failurereason.playernotfound=&cPlayer '&6{player}&c' does not exist or is not online!
mv-core.destination.shared.failurereason.invalidcoordinatesformat=You must specify a valid coordinate in this format: &6{x},{y},{z}
mv-core.destination.shared.failurereason.invalidnumberformat=Value is not a valid number. {error}.
mv-core.destination.shared.failurereason.invalidnumberformat=&cInvalid destination input. {error}
mv-core.destination.shared.failurereason.worldnotfound=&cWorld '&6{world}&c' does not exist or is not loaded!
mv-core.destination.parse.failurereason.invaliddestinationid=Invalid destination id: {id}. Valid destination ids are: {ids}
@@ -281,8 +281,8 @@ mv-core.createworld.worldexistunloaded=World '{world}' already exists, but it's
mv-core.createworld.worldexistloaded=World '{world}' already exists!
mv-core.deleteworld.worldnonexistent=World '{world}' not found!
mv-core.deleteworld.loadfailed=Unable to load world '{world}', does the world folder exist?&f You can run '&a/mv remove {world}&f' to remove it from Multiverse, or attempt to load again with '&a/mv load {world}&f'.
mv-core.deleteworld.worldfoldernotfound=World '{world}' folder not found!
mv-core.deleteworld.loadfailed=Unable to load world '{world}', is the world folder deleted or corrupted?&f You can run '&a/mv remove {world}&f' to remove it from Multiverse, or attempt to load again with '&a/mv load {world}&f'.
mv-core.deleteworld.worldfoldernotfound=&cUnable to locate '{world}' folder! You may '&a/mv remove {world}&c' to remove it from Multiverse, and manually delete the folder if it exists.
mv-core.deleteworld.failedtodeletefolder=Failed to delete world folder '{world}': {error}\n&fSee console for more details.
mv-core.importworld.invalidworldname=World '{world}' contains invalid characters!
@@ -327,6 +327,11 @@ mv-core.exception.multiverseworld.unloaddefaultworld=&cYou can't unload the defa
mv-core.exception.multiverseworld.unloadplayersinworld=&cThere are still &6{count}&c player(s) in the world! Use '&6--remove-players&c' flag to your command to teleport all players out of the world.
mv-core.exception.multiverseworld.unloaderror=&cAn unknown error occurred while unloading world: &6{world}&c.\n&cSee console for more details.
# multiverse position parse exception
mv-core.exception.positionparse.invaliddirection=&cInvalid direction string format: {format}. Expected format: <pitch>:<yaw>
mv-core.exception.positionparse.invalidcoordinates=&cInvalid coordinates format: {format}. Expected format: <x>,<y>,<z>
mv-core.exception.positionparse.invalidnumber=&cInvalid number format: {number}. Expects a numeric value.
# generic
mv-core.generic.success=Success!
mv-core.generic.failure=Failed!
@@ -69,6 +69,22 @@ class DestinationTest : TestWithMockBukkit() {
assertEquals("e:world:1.2,2.0,3.0:10.5:9.5", destination.toString())
}
@Test
fun `Exact destination instance with relative position`() {
val destination = destinationsProvider.parseDestination("e:world:~1.5,2,~-3:10.5:~9.5").orNull
assertTrue(destination is ExactDestinationInstance)
val expectedLocation = Location(
world.bukkitWorld.orNull,
player.location.x + 1.5,
2.0,
player.location.z - 3.0,
player.location.yaw + 9.5F,
10.5F
)
assertLocationEquals(expectedLocation, destination.getLocation(player).orNull)
assertEquals("e:world:~1.5,2.0,~-3.0:10.5:~9.5", destination.toString())
}
@Test
fun `Exact destination instance from location`() {
val exactDestination = serviceLocator.getActiveService(ExactDestination::class.java).takeIf { it != null } ?: run {
@@ -123,6 +139,7 @@ class DestinationTest : TestWithMockBukkit() {
assertTrue(destinationsProvider.parseDestination("b:invalid-bed").isFailure)
assertTrue(destinationsProvider.parseDestination("ca:invalid-cannon").isFailure)
assertTrue(destinationsProvider.parseDestination("e:world:1,2,x").isFailure)
assertTrue(destinationsProvider.parseDestination("e:world:1,2,3:").isFailure)
assertTrue(destinationsProvider.parseDestination("pl:invalid-player").isFailure)
assertTrue(destinationsProvider.parseDestination("w:invalid-world").isFailure)
// todo: should we make invalid yaw for WorldDestination fail?
@@ -4,6 +4,8 @@ world:
enforce-access: false
enforce-gamemode: true
enforce-flight: true
apply-entity-spawn-rate: true
apply-entity-spawn-limit: true
auto-purge-entities: false
world-name-format:
nether: '%overworld%_nether'
@@ -52,6 +54,8 @@ event-priority:
misc:
bukkit-yml-path: bukkit.yml
server-properties-path: server.properties
auto-detect-generator-plugins: true
global-debug: 0
debug-permissions: false
silent-start: false