Compare commits

...
Author SHA1 Message Date
Ben Woo 278c51d056 Merge pull request #3056 from Multiverse/zax71MV5/versionCommand
Add `/mv version`
2024-03-12 10:38:12 +08:00
Zax71 caed081f82 Update Ben's name 2024-03-08 18:03:42 +00:00
Zax71 7276da7665 Implement i18n 2024-02-24 13:48:57 +00:00
Zax71 90b8729129 Add Ben and Zax71 to authors 2024-02-24 13:37:23 +00:00
Zax71 c24f198ed0 Add /mv version 2024-02-24 13:36:13 +00:00
Ben Woo 0c37c9e934 Merge pull request #3033 from Multiverse/ben/mv5/list-value-node
Implement ListValueNode and modify actions
2023-09-25 22:24:42 +08:00
Ben Woo 454bcd36e5 Refactor and fix checkstyles for some listeners 2023-09-25 14:09:09 +08:00
Ben Woo af7d5a8cb0 Remove useless MultiverseCoreListener 2023-09-25 14:08:51 +08:00
Ben Woo a44361f5e4 Merge pull request #3034 from Multiverse/zax71/MV5/gameruleCommandMerge
Zax71/mv5/gamerule command merge
2023-09-25 13:31:20 +08:00
Ben Woo 8d508358f7 Implement gamerule reset 2023-09-25 12:29:29 +08:00
Zax71 7c6d7d5883 Merge /mv gamerule and /mv gamerules 2023-09-24 10:34:11 +01:00
Ben Woo c78dbcd395 Implement ListValueNode and modify actions 2023-09-24 11:11:48 +08:00
Ben Woo b2c1aafb62 Make sure nodes is not null and fix some checkstyles 2023-09-23 10:14:10 +08:00
Ben Woo d08e6fae38 Merge pull request #3032 from Multiverse/zax71/MV5/GeneratorsCommand
Add `/mv generators` command
2023-09-23 09:52:17 +08:00
Ben Woo d6ccbd9eee Fix some checkstyles for generator command 2023-09-23 09:50:22 +08:00
Ben Woo d880ea95ed Merge pull request #3031 from Multiverse/ben/mv5/issuer-methods
Support more variety of message payload for MVCommandIssuer
2023-09-23 09:41:07 +08:00
Ben Woo 38aa2acf8e Merge pull request #3030 from Multiverse/zax71/MV5/InfoCommand
Add `/mv info`
2023-09-23 09:40:41 +08:00
Ben Woo 5845fdd541 Merge pull request #3027 from Multiverse/ben/mv5/config-suggest-parser
Implement autocomplete and string parsing for config handling
2023-09-23 09:39:15 +08:00
Ben Woo 01cfcc5684 Merge pull request #3019 from Multiverse/ben/mv5/teleporter
Ben/mv5/teleporter
2023-09-23 09:38:55 +08:00
Ben Woo 62a4e7e27d Merge pull request #3028 from Multiverse/ben/mv5/remove-unsafecallwrapper
Remove UnsafeCallWrapper as now we use varv Try
2023-09-23 09:38:39 +08:00
Zax71 ac174dcf66 Add /mv generators 2023-09-22 19:03:19 +01:00
Ben Woo 4095829eca Replace properties method in MVConfig to StringPropertyHandle 2023-09-22 23:09:31 +08:00
Ben Woo 13dc1bd0d0 Support more variety of message payload for MVCommandIssuer 2023-09-22 17:44:44 +08:00
Ben Woo 3a6c52337f Add more properties to info command and refactor 2023-09-22 17:20:41 +08:00
Ben Woo 61e3ffb7e5 Add null value formatting for MapContentProvider 2023-09-22 17:20:13 +08:00
Ben Woo 41f9989eef Fix more checkstyles and use LinkedHashMap for info command 2023-09-22 16:09:04 +08:00
Ben Woo 896524b57a Check some checkstyles for info command 2023-09-22 15:46:54 +08:00
Zax71 42ca91d05b Add /mv info 2023-09-21 18:54:49 +01:00
Ben Woo 5db5b948cc Fix some related checkstyles 2023-09-21 11:47:20 +08:00
Ben Woo 2e5679491c Remove UnsafeCallWrapper as now we use varv Try 2023-09-21 11:36:55 +08:00
Ben Woo 980d6784df Merge branch 'MV5' into ben/mv5/teleporter
# Conflicts:
#	src/main/java/org/mvplugins/multiverse/core/commands/RemoveCommand.java
2023-09-21 11:23:05 +08:00
Ben Woo a2f6e201d0 Add injection test for AsyncSafetyTeleporter 2023-09-21 11:20:23 +08:00
Ben Woo f2e193eda3 Improve node suggest javadocs 2023-09-20 23:39:04 +08:00
Ben Woo 4084cd4af1 Move property handling to a separate class 2023-09-20 23:35:10 +08:00
Ben Woo 6fe87fbc7b Implement autocomplete and string parsing for config handling 2023-09-20 16:32:25 +08:00
Ben Woo 6611ec4457 Make defaultValue a supplier and fix some checkstyle in ConfigNode 2023-09-20 13:45:14 +08:00
Ben Woo e4bff69372 Pass multiverse core instance directly into WorldConfigNodes 2023-09-20 12:39:59 +08:00
Ben Woo 5dba08dd1b Merge pull request #3025 from Multiverse/ben/mv5/fix-remove-locale
Fix remove success message key
2023-09-20 12:34:02 +08:00
Ben Woo 2e0ce517a8 Merge pull request #3026 from Multiverse/ben/mv5/enum-completion
Implement lowercased enum completion and fix some checkstyles
2023-09-20 12:33:47 +08:00
Ben Woo 7fcfa64538 Merge pull request #3024 from Multiverse/ben/mv5/mv-world-oncontext
Ben/mv5/mv world oncontext
2023-09-20 12:33:33 +08:00
Ben Woo cce041877f Cleanup NodeGroup with docs and annotation 2023-09-20 12:31:34 +08:00
Ben Woo 6ba51fa573 Implement lowercased enum completion and fix some checkstyles 2023-09-20 12:25:03 +08:00
Ben Woo bbfc1a05e8 Cleanup MultiverseCommand and add docs 2023-09-20 12:18:04 +08:00
Ben Woo 80fcf8fe19 Fix remove success message key 2023-09-20 12:10:02 +08:00
Ben Woo b99de26deb Use MultiverseWorld instead of worldname string for RemoveCommand 2023-09-20 12:06:08 +08:00
Ben Woo fb240a19fa Add context resolving for MultiverseWorld class 2023-09-20 12:02:29 +08:00
Ben Woo 180680f297 Add method to get MultiverseWorld from a bukkit world. 2023-09-20 12:01:26 +08:00
Ben Woo 91f9842b24 Merge pull request #3022 from Multiverse/dtm/mv5/teleporter_attempt
Add AsyncAttempt.
2023-09-14 22:34:34 +08:00
Ben Woo bf8244e587 Refactor some async attempt handling 2023-09-14 11:30:35 +08:00
Ben Woo 94bcca914f Add warning of player still in world while unloading 2023-09-14 11:26:07 +08:00
Ben Woo 23b429940c Fully implement AsyncResult 2023-09-14 11:03:45 +08:00
Jeremy Wood 3df46bc55c Add AsyncAttempt for example purposes. 2023-09-13 16:16:47 -04:00
Ben Woo 2f3306aa97 Add class level docs for AsyncResult 2023-09-14 00:37:33 +08:00
Ben Woo 82c84c2c7c Implement AsyncResult, a wrapper for CompletableFuture 2023-09-14 00:36:22 +08:00
Ben Woo 2642d940ba Clearer docs for tolerance and radius for getSafeLocation 2023-09-13 23:32:10 +08:00
Ben Woo 404ab738c0 Fix imports 2023-09-13 23:21:23 +08:00
Ben Woo bc1a2ff8a1 Merge branch 'MV5' into ben/mv5/teleporter
# Conflicts:
#	src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java
#	src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java
#	src/main/java/org/mvplugins/multiverse/core/commands/RemoveCommand.java
#	src/main/java/org/mvplugins/multiverse/core/commands/UnloadCommand.java
#	src/main/java/org/mvplugins/multiverse/core/destination/core/CannonDestination.java
#	src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestination.java
#	src/main/java/org/mvplugins/multiverse/core/destination/core/WorldDestination.java
#	src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java
#	src/main/java/org/mvplugins/multiverse/core/permissions/CorePermissionsChecker.java
#	src/main/java/org/mvplugins/multiverse/core/world/LoadedMultiverseWorld.java
#	src/main/java/org/mvplugins/multiverse/core/world/helpers/PlayerWorldTeleporter.java
2023-09-13 23:04:36 +08:00
Ben Woo ba93195671 Move logic of teleporting players out of world from world manager 2023-09-12 18:07:12 +08:00
Ben Woo 9ddfa59712 Merge branch 'MV5' into ben/mv5/teleporter
# Conflicts:
#	src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java
2023-09-12 17:25:50 +08:00
Ben Woo ef2b42d9a0 Check for destination's checkTeleportSafety 2023-09-12 16:36:43 +08:00
Ben Woo 245fac98e5 Implement new AsyncSafetyTeleporter 2023-09-12 16:33:43 +08:00
88 changed files with 3122 additions and 1499 deletions

No files matched your search

@@ -4,6 +4,7 @@ import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Minecart;
import org.bukkit.entity.Vehicle;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Contract;
/**
@@ -37,8 +38,35 @@ public interface BlockSafety {
*/
boolean playerCanSpawnHereSafely(Location l);
/**
* Finds a portal-block next to the specified {@link Location}.
*
* @param location The {@link Location}
* @return The next portal-block's {@link Location}.
*/
Location findPortalBlockNextTo(Location location);
/**
* Gets the next safe location around the given location.
*
* @param location A {@link Location}.
* @return A safe {@link Location}.
*/
@Nullable Location getSafeLocation(Location location);
/**
* Gets the next safe location around the given location.
*
* @param location A {@link Location}.
* @param tolerance The tolerance of how far up and down to search.
* @param radius The radius around given location to search.
* @return A safe {@link Location}.
*/
@Nullable Location getSafeLocation(Location location, int tolerance, int radius);
/**
* Gets a safe bed spawn location OR null if the bed is invalid.
*
* @param l The location of the bead head (block with the pillow on it).
* @return Safe location around the bed or null if no location was found.
*/
@@ -46,6 +74,7 @@ public interface BlockSafety {
/**
* Gets the location of the top block at the specified {@link Location}.
*
* @param l Any {@link Location}.
* @return The {@link Location} of the top-block.
*/
@@ -53,6 +82,7 @@ public interface BlockSafety {
/**
* Gets the location of the top block at the specified {@link Location}.
*
* @param l Any {@link Location}.
* @return The {@link Location} of the top-block.
*/
@@ -60,6 +90,7 @@ public interface BlockSafety {
/**
* Checks if an entity would be on track at the specified {@link Location}.
*
* @param l The {@link Location}.
* @return True if an entity would be on tracks at the specified {@link Location}.
*/
@@ -67,6 +98,7 @@ public interface BlockSafety {
/**
* Checks if the specified {@link Minecart} can spawn safely.
*
* @param cart The {@link Minecart}.
* @return True if the minecart can spawn safely.
*/
@@ -74,6 +106,7 @@ public interface BlockSafety {
/**
* Checks if the specified {@link Vehicle} can spawn safely.
*
* @param vehicle The {@link Vehicle}.
* @return True if the vehicle can spawn safely.
*/
@@ -45,14 +45,4 @@ public interface Destination<T extends DestinationInstance> {
* @return True if the SafeTeleporter will be used, false if not.
*/
boolean checkTeleportSafety();
/**
* Returns the teleporter to use for this destination.
*
* <p>By default, Multiverse will automatically use SafeTeleporter. If you want to use a different teleporter, you can
* override this method.</p>
*
* @return The custom teleporter to use for this destination. Return null to use the default teleporter.
*/
@Nullable Teleporter getTeleporter();
}
@@ -3,7 +3,7 @@ package org.mvplugins.multiverse.core.api;
import io.vavr.control.Try;
import org.jvnet.hk2.annotations.Contract;
import org.mvplugins.multiverse.core.configuration.node.NodeGroup;
import org.mvplugins.multiverse.core.configuration.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.placeholders.MultiverseCorePlaceholders;
@Contract
@@ -27,30 +27,11 @@ public interface MVConfig {
Try<Void> save();
/**
* Gets the nodes for the config.
* Gets the handler for managing config with string names and values.
*
* @return The nodes for the config.
* @return The config handle for string properties.
*/
NodeGroup getNodes();
/**
* Gets a property from the config.
*
* @param name The name of the property.
* @return A {@link Try} with the value of the property, otherwise a {@link Try.Failure} if there is no property by
* that name.
*/
Try<Object> getProperty(String name);
/**
* Sets a property in the config.
*
* @param name The name of the property.
* @param value The value of the property.
* @return An empty {@link Try} if the property was set successfully, otherwise a {@link Try.Failure} with the
* exception explaining why the property could not be set.
*/
Try<Void> setProperty(String name, Object value);
StringPropertyHandle getStringPropertyHandle();
/**
* Sets world access permissions should be enforced.
@@ -1,88 +0,0 @@
package org.mvplugins.multiverse.core.api;
import java.util.concurrent.CompletableFuture;
import co.aikar.commands.BukkitCommandIssuer;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.jvnet.hk2.annotations.Contract;
import org.mvplugins.multiverse.core.destination.ParsedDestination;
import org.mvplugins.multiverse.core.teleportation.TeleportResult;
/**
* Used to safely teleport people.
*/
@Contract
public interface SafeTTeleporter extends Teleporter {
/**
* Gets the next safe location around the given location.
* @param l A {@link Location}.
* @return A safe {@link Location}.
*/
Location getSafeLocation(Location l);
/**
* Gets the next safe location around the given location.
* @param l A {@link Location}.
* @param tolerance The tolerance.
* @param radius The radius.
* @return A safe {@link Location}.
*/
Location getSafeLocation(Location l, int tolerance, int radius);
/**
* Safely teleport the entity to the MVDestination. This will perform checks to see if the place is safe, and if
* it's not, will adjust the final destination accordingly.
*
* @param teleporter Person who performed the teleport command.
* @param teleportee Entity to teleport
* @param destination Destination to teleport them to
* @return true for success, false for failure
*/
@Deprecated
TeleportResult safelyTeleport(BukkitCommandIssuer teleporter, Entity teleportee, ParsedDestination<?> destination);
/**
* Safely teleport the entity to the MVDestination. This will perform checks to see if the place is safe, and if
* it's not, will adjust the final destination accordingly.
*
* @param teleporter Person who performed the teleport command.
* @param teleportee Entity to teleport
* @param destination Destination to teleport them to
* @return true for success, false for failure
*/
CompletableFuture<TeleportResult> safelyTeleportAsync(BukkitCommandIssuer teleporter, Entity teleportee, ParsedDestination<?> destination);
/**
* Safely teleport the entity to the Location. This may perform checks to
* see if the place is safe, and if
* it's not, will adjust the final destination accordingly.
*
* @param teleporter Person who issued the teleport command.
* @param teleportee Entity to teleport.
* @param location Location to teleport them to.
* @param safely Should the destination be checked for safety before teleport?
* @return true for success, false for failure.
*/
TeleportResult safelyTeleport(CommandSender teleporter, Entity teleportee, Location location,
boolean safely);
/**
* Returns a safe location for the entity to spawn at.
*
* @param entity The entity to spawn
* @param destination The MVDestination to take the entity to.
* @return A new location to spawn the entity at.
*/
Location getSafeLocation(Entity entity, DestinationInstance destination);
/**
* Finds a portal-block next to the specified {@link Location}.
* @param l The {@link Location}
* @return The next portal-block's {@link Location}.
*/
Location findPortalBlockNextTo(Location l);
}
@@ -1,34 +0,0 @@
package org.mvplugins.multiverse.core.api;
import java.util.concurrent.CompletableFuture;
import co.aikar.commands.BukkitCommandIssuer;
import org.bukkit.entity.Entity;
import org.jvnet.hk2.annotations.Contract;
import org.mvplugins.multiverse.core.destination.ParsedDestination;
import org.mvplugins.multiverse.core.teleportation.TeleportResult;
@Contract
public interface Teleporter {
/**
* Teleport the entity to the Multiverse Destination.
*
* @param teleporter Person who performed the teleport command.
* @param teleportee Entity to teleport
* @param destination Destination to teleport them to
* @return true for success, false for failure
*/
@Deprecated
TeleportResult teleport(BukkitCommandIssuer teleporter, Entity teleportee, ParsedDestination<?> destination);
/**
* Teleport the entity to the Multiverse Destination.
*
* @param teleporter Person who performed the teleport command.
* @param teleportee Entity to teleport
* @param destination Destination to teleport them to
* @return true for success, false for failure
*/
CompletableFuture<TeleportResult> teleportAsync(BukkitCommandIssuer teleporter, Entity teleportee, ParsedDestination<?> destination);
}
@@ -49,6 +49,6 @@ class CheckCommand extends MultiverseCommand {
"{player}", player.getName(),
"{destination}", destination.toString());
// TODO: More detailed output on permissions required.
this.destinationsProvider.checkTeleportPermissions(issuer, player, destination);
// this.destinationsProvider.checkTeleportPermissions(issuer, player, destination);
}
}
@@ -15,7 +15,6 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.commandtools.MVCommandIssuer;
import org.mvplugins.multiverse.core.commandtools.MVCommandManager;
import org.mvplugins.multiverse.core.commandtools.MultiverseCommand;
import org.mvplugins.multiverse.core.commandtools.context.MVConfigValue;
import org.mvplugins.multiverse.core.config.MVCoreConfig;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
@@ -33,35 +32,35 @@ class ConfigCommand extends MultiverseCommand {
@Subcommand("config")
@CommandPermission("multiverse.core.config")
@CommandCompletion("@mvconfigs")
@Syntax("<name> [new-value]")
@Description("") // TODO: Description
@CommandCompletion("@mvconfigs @mvconfigvalues")
@Syntax("<name> [value]")
@Description("Show or set a config value.")
void onConfigCommand(
MVCommandIssuer issuer,
@Syntax("<name>")
@Description("") // TODO: Description
@Description("The name of the config to set or show.")
String name,
@Optional
@Syntax("[new-value]")
@Description("") // TODO: Description
MVConfigValue value) {
@Syntax("[value]")
@Description("The value to set the config to. If not specified, the current value will be shown.")
String value) {
if (value == null) {
showConfigValue(issuer, name);
return;
}
updateConfigValue(issuer, name, value.getValue());
updateConfigValue(issuer, name, value);
}
private void showConfigValue(MVCommandIssuer issuer, String name) {
config.getProperty(name)
config.getStringPropertyHandle().getProperty(name)
.onSuccess(value -> issuer.sendMessage(name + "is currently set to " + value))
.onFailure(e -> issuer.sendMessage(e.getMessage()));
}
private void updateConfigValue(MVCommandIssuer issuer, String name, Object value) {
config.setProperty(name, value)
private void updateConfigValue(MVCommandIssuer issuer, String name, String value) {
config.getStringPropertyHandle().setPropertyString(name, value)
.onSuccess(ignore -> {
config.save();
issuer.sendMessage("Successfully set " + name + " to " + value);
@@ -71,7 +71,7 @@ class CreateCommand extends MultiverseCommand {
@Subcommand("create")
@CommandPermission("multiverse.core.create")
@CommandCompletion("@empty @flags:groupName=mvcreatecommand")
@CommandCompletion("@empty @environments @flags:groupName=mvcreatecommand")
@Syntax("<name> <environment> --seed [seed] --generator [generator[:id]] --world-type [worldtype] --adjust-spawn --no-structures")
@Description("{@@mv-core.create.description}")
void onCreateCommand(
@@ -1,11 +1,14 @@
package org.mvplugins.multiverse.core.commands;
import java.util.Collections;
import co.aikar.commands.MessageType;
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.Optional;
import co.aikar.commands.annotation.Single;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
@@ -17,48 +20,82 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.commandtools.MVCommandIssuer;
import org.mvplugins.multiverse.core.commandtools.MVCommandManager;
import org.mvplugins.multiverse.core.commandtools.MultiverseCommand;
import org.mvplugins.multiverse.core.commandtools.flags.CommandFlag;
import org.mvplugins.multiverse.core.commandtools.flags.ParsedCommandFlags;
import org.mvplugins.multiverse.core.commandtools.queue.QueuedCommand;
import org.mvplugins.multiverse.core.utils.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.Async;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
@Service
@CommandAlias("mv")
class DeleteCommand extends MultiverseCommand {
private final WorldManager worldManager;
private final PlayerWorldTeleporter playerWorldTeleporter;
private final CommandFlag REMOVE_PLAYERS_FLAG = flag(CommandFlag.builder("--remove-players")
.addAlias("-r")
.build());
@Inject
DeleteCommand(@NotNull MVCommandManager commandManager, @NotNull WorldManager worldManager) {
DeleteCommand(
@NotNull MVCommandManager commandManager,
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter) {
super(commandManager);
this.worldManager = worldManager;
this.playerWorldTeleporter = playerWorldTeleporter;
}
@Subcommand("delete")
@CommandPermission("multiverse.core.delete")
@CommandCompletion("@mvworlds:scope=both")
@CommandCompletion("@mvworlds:scope=loaded @flags:groupName=mvdeletecommand")
@Syntax("<world>")
@Description("{@@mv-core.delete.description}")
void onDeleteCommand(
MVCommandIssuer issuer,
@Single
@Conditions("worldname:scope=both")
@Conditions("worldname:scope=loaded")
@Syntax("<world>")
@Description("The world you want to delete.")
String worldName) {
LoadedMultiverseWorld world,
@Optional
@Syntax("[--remove-players]")
@Description("")
String[] flags) {
ParsedCommandFlags parsedFlags = parseFlags(flags);
this.commandManager.getCommandQueueManager().addToQueue(new QueuedCommand(
issuer.getIssuer(),
() -> {
issuer.sendInfo(MVCorei18n.DELETE_DELETING, "{world}", worldName);
worldManager.deleteWorld(worldName)
.onSuccess(deletedWorldName -> {
Logging.fine("World delete success: " + deletedWorldName);
issuer.sendInfo(MVCorei18n.DELETE_SUCCESS, "{world}", deletedWorldName);
}).onFailure(failure -> {
Logging.fine("World delete failure: " + failure);
issuer.sendError(failure.getFailureMessage());
});
}, this.commandManager.formatMessage(
issuer, MessageType.INFO, MVCorei18n.DELETE_PROMPT, "{world}", worldName)));
runDeleteCommand(issuer, world, parsedFlags);
}, this.commandManager.formatMessage(issuer, MessageType.INFO, MVCorei18n.DELETE_PROMPT,
"{world}", world.getName())));
}
private void runDeleteCommand(MVCommandIssuer issuer, LoadedMultiverseWorld world, ParsedCommandFlags parsedFlags) {
issuer.sendInfo(MVCorei18n.DELETE_DELETING, "{world}", world.getName());
var future = parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG)
? playerWorldTeleporter.removeFromWorld(world)
: Async.completedFuture(Collections.emptyList());
future.thenRun(() -> doWorldDeleting(issuer, world));
}
private void doWorldDeleting(MVCommandIssuer issuer, LoadedMultiverseWorld world) {
worldManager.deleteWorld(world)
.onSuccess(deletedWorldName -> {
Logging.fine("World delete success: " + deletedWorldName);
issuer.sendInfo(MVCorei18n.DELETE_SUCCESS, "{world}", deletedWorldName);
}).onFailure(failure -> {
Logging.fine("World delete failure: " + failure);
issuer.sendError(failure.getFailureMessage());
});
}
}
@@ -1,53 +1,96 @@
package org.mvplugins.multiverse.core.commands;
import co.aikar.commands.BukkitCommandIssuer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import co.aikar.commands.CommandIssuer;
import co.aikar.commands.InvalidCommandArgument;
import co.aikar.commands.MessageType;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Flags;
import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import jakarta.inject.Inject;
import org.bukkit.ChatColor;
import org.bukkit.GameRule;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.commandtools.MVCommandIssuer;
import org.mvplugins.multiverse.core.commandtools.MVCommandManager;
import org.mvplugins.multiverse.core.commandtools.MultiverseCommand;
import org.mvplugins.multiverse.core.commandtools.context.GameRuleValue;
import org.mvplugins.multiverse.core.commandtools.flags.CommandValueFlag;
import org.mvplugins.multiverse.core.commandtools.flags.ParsedCommandFlags;
import org.mvplugins.multiverse.core.display.ContentDisplay;
import org.mvplugins.multiverse.core.display.filters.ContentFilter;
import org.mvplugins.multiverse.core.display.filters.DefaultContentFilter;
import org.mvplugins.multiverse.core.display.filters.RegexContentFilter;
import org.mvplugins.multiverse.core.display.handlers.PagedSendHandler;
import org.mvplugins.multiverse.core.display.parsers.MapContentProvider;
import org.mvplugins.multiverse.core.utils.MVCorei18n;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
@Service
@CommandAlias("mv")
@Subcommand("gamerule|rule|gamerules|rules")
class GameruleCommand extends MultiverseCommand {
private final CommandValueFlag<Integer> PAGE_FLAG = flag(CommandValueFlag
.builder("--page", Integer.class)
.addAlias("-p")
.context(value -> {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new InvalidCommandArgument("Invalid page number: " + value);
}
})
.build());
private final CommandValueFlag<ContentFilter> FILTER_FLAG = flag(CommandValueFlag
.builder("--filter", ContentFilter.class)
.addAlias("-f")
.context(value -> {
try {
return RegexContentFilter.fromString(value);
} catch (IllegalArgumentException e) {
throw new InvalidCommandArgument("Invalid filter: " + value);
}
})
.build());
@Inject
GameruleCommand(@NotNull MVCommandManager commandManager) {
super(commandManager);
}
@Subcommand("gamerule")
@Subcommand("set")
@CommandPermission("multiverse.core.gamerule.set")
@CommandCompletion("@gamerules true|false|@range:1-10 @mvworlds:multiple|*")
@Syntax("<Gamerule> <Gamerule value> [World or *]")
@Description("{@@mv-core.gamerule.description}")
void onGameruleCommand(
BukkitCommandIssuer issuer,
@Description("{@@mv-core.gamerule.set.description}")
void onGameruleSetCommand(
MVCommandIssuer issuer,
@Syntax("<Gamerule>")
@Description("{@@mv-core.gamerule.gamerule.description}")
@Description("{@@mv-core.gamerule.set.gamerule.description}")
GameRule gamerule,
@Syntax("<Value>")
@Description("{@@mv-core.gamerule.value.description}")
@Description("{@@mv-core.gamerule.set.value.description}")
GameRuleValue gameRuleValue,
@Flags("resolve=issuerAware")
@Syntax("[World or *]")
@Description("{@@mv-core.gamerule.world.description}")
@Description("{@@mv-core.gamerule.set.world.description}")
LoadedMultiverseWorld[] worlds) {
Object value = gameRuleValue.getValue();
boolean success = true;
@@ -55,7 +98,7 @@ class GameruleCommand extends MultiverseCommand {
// Set gamerules and add false to list if it fails
World bukkitWorld = world.getBukkitWorld().getOrNull();
if (bukkitWorld == null || !bukkitWorld.setGameRule(gamerule, value)) {
issuer.sendError(MVCorei18n.GAMERULE_FAILED,
issuer.sendError(MVCorei18n.GAMERULE_SET_FAILED,
"{gamerule}", gamerule.getName(),
"{value}", value.toString(),
"{world}", world.getName(),
@@ -66,16 +109,115 @@ class GameruleCommand extends MultiverseCommand {
// Tell user if it was successful
if (success) {
if (worlds.length == 1) {
issuer.sendInfo(MVCorei18n.GAMERULE_SUCCESS_SINGLE,
issuer.sendInfo(MVCorei18n.GAMERULE_SET_SUCCESS_SINGLE,
"{gamerule}", gamerule.getName(),
"{value}", value.toString(),
"{world}", worlds[0].getName());
} else if (worlds.length > 1) {
issuer.sendInfo(MVCorei18n.GAMERULE_SUCCESS_MULTIPLE,
issuer.sendInfo(MVCorei18n.GAMERULE_SET_SUCCESS_MULTIPLE,
"{gamerule}", gamerule.getName(),
"{value}", value.toString(),
"{count}", String.valueOf(worlds.length));
}
}
}
@Subcommand("reset")
@CommandPermission("multiverse.core.gamerule.set")
@CommandCompletion("@gamerules @mvworlds:multiple|*")
@Syntax("<Gamerule> [World or *]")
@Description("{@@mv-core.gamerule.reset.description}")
void onGameruleSetCommand(
MVCommandIssuer issuer,
@Syntax("<Gamerule>")
@Description("{@@mv-core.gamerule.reset.gamerule.description}")
GameRule gamerule,
@Flags("resolve=issuerAware")
@Syntax("[World or *]")
@Description("{@@mv-core.gamerule.reset.world.description}")
LoadedMultiverseWorld[] worlds) {
AtomicBoolean success = new AtomicBoolean(true);
Arrays.stream(worlds).forEach(world -> world.getBukkitWorld().peek(bukkitWorld -> {
bukkitWorld.setGameRule(gamerule, bukkitWorld.getGameRuleDefault(gamerule));
}).onEmpty(() -> {
success.set(false);
issuer.sendError(MVCorei18n.GAMERULE_RESET_FAILED,
"{gamerule}", gamerule.getName(),
"{world}", world.getName());
}));
// Tell user if it was successful
if (success.get()) {
if (worlds.length == 1) {
issuer.sendInfo(MVCorei18n.GAMERULE_RESET_SUCCESS_SINGLE,
"{gamerule}", gamerule.getName(),
"{world}", worlds[0].getName());
} else if (worlds.length > 1) {
issuer.sendInfo(MVCorei18n.GAMERULE_RESET_SUCCESS_MULTIPLE,
"{gamerule}", gamerule.getName(),
"{count}", String.valueOf(worlds.length));
}
}
}
@Subcommand("list")
@CommandPermission("multiverse.core.gamerule.list")
@CommandCompletion("@mvworlds|@flags:groupName=mvgamerulecommand @flags:groupName=mvgamerulecommand")
@Syntax("[world] [--page <page>] [--filter <filter>]")
@Description("{@@mv-core.gamerule.list.description}")
void onGameruleListCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Syntax("<world>")
@Description("{@@mv-core.gamerule.list.description.world}")
LoadedMultiverseWorld world,
@Optional
@Syntax("[--page <page>] [--filter <filter>]")
@Description("{@@mv-core.gamerule.list.description.page}")
String[] flags) {
ParsedCommandFlags parsedFlags = parseFlags(flags);
ContentDisplay.create()
.addContent(MapContentProvider.forContent(getGameRuleMap(world.getBukkitWorld().getOrNull())) // TODO: Handle null
.withKeyColor(ChatColor.AQUA)
.withValueColor(ChatColor.WHITE))
.withSendHandler(PagedSendHandler.create()
.withHeader(this.getListTitle(issuer, world.getBukkitWorld().getOrNull()))
.doPagination(true)
.withTargetPage(parsedFlags.flagValue(PAGE_FLAG, 1))
.withFilter(parsedFlags.flagValue(FILTER_FLAG, DefaultContentFilter.get())))
.send(issuer);
}
/**
* Gets all the gamerules and their values for a given world.
*
* @param world The world to find gamerules for.
* @return A map of the gamerules and their values
*/
private Map<String, String> getGameRuleMap(World world) {
Map<String, String> gameRuleMap = new HashMap<>();
for (GameRule<?> gamerule : GameRule.values()) {
Object gameruleValue = world.getGameRuleValue(gamerule);
if (gameruleValue == null) {
gameRuleMap.put(gamerule.getName(), "null");
continue;
}
gameRuleMap.put(gamerule.getName(), gameruleValue.toString());
}
return gameRuleMap;
}
private String getListTitle(CommandIssuer issuer, World world) {
return this.commandManager.formatMessage(
issuer,
MessageType.INFO,
MVCorei18n.GAMERULE_LIST_TITLE,
"{world}", world.getName());
}
}
@@ -1,23 +1,18 @@
package org.mvplugins.multiverse.core.commands;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import co.aikar.commands.CommandIssuer;
import co.aikar.commands.InvalidCommandArgument;
import co.aikar.commands.MessageType;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Flags;
import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import jakarta.inject.Inject;
import org.bukkit.ChatColor;
import org.bukkit.GameRule;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
@@ -31,16 +26,18 @@ import org.mvplugins.multiverse.core.display.filters.ContentFilter;
import org.mvplugins.multiverse.core.display.filters.DefaultContentFilter;
import org.mvplugins.multiverse.core.display.filters.RegexContentFilter;
import org.mvplugins.multiverse.core.display.handlers.PagedSendHandler;
import org.mvplugins.multiverse.core.display.parsers.MapContentProvider;
import org.mvplugins.multiverse.core.display.parsers.ListContentProvider;
import org.mvplugins.multiverse.core.utils.MVCorei18n;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
/**
* List all gamerules in your current or specified world.
*/
@Service
@CommandAlias("mv")
class GamerulesCommand extends MultiverseCommand {
class GeneratorsCommand extends MultiverseCommand {
private final GeneratorProvider generatorProvider;
private final CommandValueFlag<Integer> PAGE_FLAG = flag(CommandValueFlag
.builder("--page", Integer.class)
@@ -67,66 +64,41 @@ class GamerulesCommand extends MultiverseCommand {
.build());
@Inject
GamerulesCommand(@NotNull MVCommandManager commandManager) {
GeneratorsCommand(@NotNull MVCommandManager commandManager, @NotNull GeneratorProvider generatorProvider) {
super(commandManager);
this.generatorProvider = generatorProvider;
}
@Subcommand("gamerules|rules")
@CommandPermission("multiverse.core.gamerule.list")
@CommandCompletion("@mvworlds|@flags:groupName=mvgamerulescommand @flags:groupName=mvgamerulescommand")
@Syntax("[world] [--page <page>] [--filter <filter>]")
@Description("{@@mv-core.gamerules.description}")
@Subcommand("generators|gens")
@CommandPermission("multiverse.core.generator")
@CommandCompletion("@flags:groupName=mvgeneratorscommand @flags:groupName=mvgeneratorscommand")
@Syntax("")
@Description("{@@mv-core.generators.description}")
void onGamerulesCommand(
@NotNull MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Syntax("<world>")
@Description("{@@mv-core.gamerules.description.world}")
LoadedMultiverseWorld world,
@Optional
@Syntax("[--page <page>] [--filter <filter>]")
@Description("{@@mv-core.gamerules.description.page}")
@Description("{@@mv-core.generators.description.flags}")
String[] flags) {
ParsedCommandFlags parsedFlags = parseFlags(flags);
// Get the generators loaded using the command suggestions
List<String> generators = (List<String>) generatorProvider.suggestGeneratorString("");
// Tell the user if we cannot find any generator plugins, then abort
if (generators.isEmpty()) {
issuer.sendMessage(commandManager.formatMessage(issuer, MessageType.INFO, MVCorei18n.GENERATORS_EMPTY));
return;
}
ContentDisplay.create()
.addContent(MapContentProvider.forContent(getGameRuleMap(world.getBukkitWorld().getOrNull())) // TODO: Handle null
.withKeyColor(ChatColor.AQUA)
.withValueColor(ChatColor.WHITE))
.addContent(ListContentProvider.forContent(generators))
.withSendHandler(PagedSendHandler.create()
.withHeader(this.getTitle(issuer, world.getBukkitWorld().getOrNull()))
.withHeader("%s====[ Multiverse Generator List ]====", ChatColor.AQUA)
.doPagination(true)
.withTargetPage(parsedFlags.flagValue(PAGE_FLAG, 1))
.withFilter(parsedFlags.flagValue(FILTER_FLAG, DefaultContentFilter.get())))
.send(issuer);
}
/**
* Gets all the gamerules and their values for a given world.
*
* @param world The world to find gamerules for.
* @return A map of the gamerules and their values
*/
private Map<String, String> getGameRuleMap(World world) {
Map<String, String> gameRuleMap = new HashMap<>();
for (GameRule<?> gamerule : GameRule.values()) {
Object gameruleValue = world.getGameRuleValue(gamerule);
if (gameruleValue == null) {
gameRuleMap.put(gamerule.getName(), "null");
continue;
}
gameRuleMap.put(gamerule.getName(), gameruleValue.toString());
}
return gameRuleMap;
}
private String getTitle(CommandIssuer issuer, World world) {
return this.commandManager.formatMessage(
issuer,
MessageType.INFO,
MVCorei18n.GAMERULES_TITLE,
"{world}", world.getName());
}
}
@@ -54,7 +54,7 @@ class ImportCommand extends MultiverseCommand {
@Subcommand("import")
@CommandPermission("multiverse.core.import")
@CommandCompletion("@mvworlds:scope=potential @flags:groupName=mvimportcommand")
@CommandCompletion("@mvworlds:scope=potential @environments @flags:groupName=mvimportcommand")
@Syntax("<name> <env> --generator [generator[:id]] --adjust-spawn")
@Description("{@@mv-core.import.description")
void onImportCommand(
@@ -0,0 +1,173 @@
package org.mvplugins.multiverse.core.commands;
import java.util.LinkedHashMap;
import java.util.Map;
import co.aikar.commands.InvalidCommandArgument;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Flags;
import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import jakarta.inject.Inject;
import org.bukkit.ChatColor;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.api.LocationManipulation;
import org.mvplugins.multiverse.core.commandtools.MVCommandIssuer;
import org.mvplugins.multiverse.core.commandtools.MVCommandManager;
import org.mvplugins.multiverse.core.commandtools.MultiverseCommand;
import org.mvplugins.multiverse.core.commandtools.flags.CommandValueFlag;
import org.mvplugins.multiverse.core.commandtools.flags.ParsedCommandFlags;
import org.mvplugins.multiverse.core.display.ContentDisplay;
import org.mvplugins.multiverse.core.display.filters.ContentFilter;
import org.mvplugins.multiverse.core.display.filters.DefaultContentFilter;
import org.mvplugins.multiverse.core.display.filters.RegexContentFilter;
import org.mvplugins.multiverse.core.display.handlers.PagedSendHandler;
import org.mvplugins.multiverse.core.display.parsers.MapContentProvider;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
@Service
@CommandAlias("mv")
class InfoCommand extends MultiverseCommand {
private final CommandValueFlag<Integer> PAGE_FLAG = flag(CommandValueFlag
.builder("--page", Integer.class)
.addAlias("-p")
.context(value -> {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new InvalidCommandArgument("Invalid page number: " + value);
}
})
.build());
private final CommandValueFlag<ContentFilter> FILTER_FLAG = flag(CommandValueFlag
.builder("--filter", ContentFilter.class)
.addAlias("-f")
.context(value -> {
try {
return RegexContentFilter.fromString(value);
} catch (IllegalArgumentException e) {
throw new InvalidCommandArgument("Invalid filter: " + value);
}
})
.build());
private final LocationManipulation locationManipulation;
private final MVEconomist economist;
@Inject
InfoCommand(
@NotNull MVCommandManager commandManager,
@NotNull LocationManipulation locationManipulation,
@NotNull MVEconomist economist) {
super(commandManager);
this.locationManipulation = locationManipulation;
this.economist = economist;
}
// TODO: support info for unloaded worlds
@Subcommand("info")
@CommandPermission("multiverse.core.info")
@CommandCompletion("@mvworlds:scope=both|@flags:groupName=mvinfocommand @flags:groupName=mvinfocommand")
@Syntax("[world] [--page <page>] [--filter <filter>]")
@Description("{@@mv-core.info.description")
public void onInfoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Syntax("<world>")
@Description("{@@mv-core.info.description.world}")
LoadedMultiverseWorld world,
@Optional
@Syntax("[--page <page>]")
@Description("{@@mv-core.info.description.page}")
String[] flags) {
ParsedCommandFlags parsedFlags = parseFlags(flags);
ContentDisplay.create()
.addContent(MapContentProvider.forContent(getInfo(world))
.withKeyColor(ChatColor.AQUA)
.withValueColor(ChatColor.WHITE))
.withSendHandler(PagedSendHandler.create()
.withHeader(getTitle(world))
.doPagination(true)
.withTargetPage(parsedFlags.flagValue(PAGE_FLAG, 1))
.withFilter(parsedFlags.flagValue(FILTER_FLAG, DefaultContentFilter.get())))
.send(issuer);
}
private String getTitle(MultiverseWorld world) {
return "&a&l---- World Info: &f&l%s&a&l ----".formatted(world.getName());
}
private Map<String, String> getInfo(LoadedMultiverseWorld world) {
Map<String, String> outMap = new LinkedHashMap<>();
outMap.put("World Name", world.getName());
outMap.put("World Alias", world.getAlias());
outMap.put("World UID", world.getUID().toString());
outMap.put("Game Mode: ", world.getGameMode().toString());
outMap.put("Difficulty", world.getDifficulty().toString());
outMap.put("Spawn Location", locationManipulation.strCoords(world.getSpawnLocation()));
outMap.put("Seed", String.valueOf(world.getSeed()));
getEntryFeeInfo(outMap, world); // Entry fee/reward
outMap.put("Respawn World", world.getRespawnWorldName());
outMap.put("World Type", world.getWorldType().get().toString());
outMap.put("Generator", world.getGenerator());
outMap.put("Generate Structures", world.canGenerateStructures().get().toString());
outMap.put("World Scale", String.valueOf(world.getScale()));
outMap.put("Weather Enabled", String.valueOf(world.getAllowWeather()));
outMap.put("Hunger Depletes", String.valueOf(world.getHunger()));
outMap.put("Keep Spawn In Memory", String.valueOf(world.getKeepSpawnInMemory()));
outMap.put("PVP Enabled", String.valueOf(world.getPvp()));
getAnimalSpawningInfo(outMap, world); // Animals that can spawn
getMonsterSpawningInfo(outMap, world); // Monsters that can spawn
return outMap;
}
private void getEntryFeeInfo(Map<String, String> outMap, LoadedMultiverseWorld world) {
double price = world.getPrice();
if (price == 0) {
outMap.put("Entry Fee", "FREE!");
} else if (price > 0) {
outMap.put("Entry Fee", economist.formatPrice(-price, world.getCurrency()));
} else if (price < 0) {
outMap.put("Entry Reward", economist.formatPrice(price, world.getCurrency()));
}
}
private void getAnimalSpawningInfo(Map<String, String> outMap, LoadedMultiverseWorld world) {
if (world.getSpawningAnimals()) {
outMap.put("Spawning Animals", "ALL");
} else {
if (!world.getSpawningAnimalsExceptions().isEmpty()) {
outMap.put("Spawning Animals", world.getSpawningAnimalsExceptions().toString());
} else {
outMap.put("Spawning Animals", "NONE");
}
}
}
private void getMonsterSpawningInfo(Map<String, String> outMap, LoadedMultiverseWorld world) {
if (world.getSpawningMonsters()) {
outMap.put("Spawning Monsters", "ALL");
} else {
if (!world.getSpawningMonstersExceptions().isEmpty()) {
outMap.put("Spawning Monsters", world.getSpawningMonstersExceptions().toString());
} else {
outMap.put("Spawning Monsters", "NONE");
}
}
}
}
@@ -0,0 +1,77 @@
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.Description;
import co.aikar.commands.annotation.Flags;
import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.commandtools.MVCommandIssuer;
import org.mvplugins.multiverse.core.commandtools.MVCommandManager;
import org.mvplugins.multiverse.core.commandtools.MultiverseCommand;
import org.mvplugins.multiverse.core.configuration.handle.PropertyModifyAction;
import org.mvplugins.multiverse.core.configuration.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@Service
@CommandAlias("mv")
class ModifyCommand extends MultiverseCommand {
private final WorldManager worldManager;
@Inject
ModifyCommand(@NotNull MVCommandManager commandManager, WorldManager worldManager) {
super(commandManager);
this.worldManager = worldManager;
}
@Subcommand("modify")
@CommandPermission("multiverse.core.modify")
@CommandCompletion("@mvworlds:scope=both @propsmodifyaction @mvworldpropsname @mvworldpropsvalue")
@Syntax("[world] <set|add|remove|reset> <property> <value>")
@Description("")
void onModifyCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Syntax("[world]")
@Description("")
MultiverseWorld world,
@Syntax("<set|add|remove|reset>")
@Description("")
PropertyModifyAction action,
@Syntax("<property>")
@Description("")
String propertyName,
@Optional
@Syntax("[value]")
@Description("")
String propertyValue) {
StringPropertyHandle worldPropertyHandle = world.getStringPropertyHandle();
worldPropertyHandle.modifyProperty(propertyName, propertyValue, action).onSuccess(ignore -> {
issuer.sendMessage("Property %s set to %s for world %s.".formatted(
propertyName,
worldPropertyHandle.getProperty(propertyName).getOrNull(),
world.getName()));
worldManager.saveWorldsConfig();
}).onFailure(exception -> {
issuer.sendMessage("Failed to %s property %s to %s for world %s.".formatted(
action.name().toLowerCase(),
propertyName,
propertyValue,
world.getName()));
issuer.sendMessage(exception.getMessage());
});
}
}
@@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.commands;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import co.aikar.commands.MessageType;
@@ -13,6 +14,7 @@ import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
@@ -24,8 +26,10 @@ import org.mvplugins.multiverse.core.commandtools.flags.CommandValueFlag;
import org.mvplugins.multiverse.core.commandtools.flags.ParsedCommandFlags;
import org.mvplugins.multiverse.core.commandtools.queue.QueuedCommand;
import org.mvplugins.multiverse.core.utils.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.Async;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
import org.mvplugins.multiverse.core.world.options.RegenWorldOptions;
@Service
@@ -33,6 +37,7 @@ import org.mvplugins.multiverse.core.world.options.RegenWorldOptions;
class RegenCommand extends MultiverseCommand {
private final WorldManager worldManager;
private final PlayerWorldTeleporter playerWorldTeleporter;
private final CommandValueFlag<String> SEED_FLAG = flag(CommandValueFlag.builder("--seed", String.class)
.addAlias("-s")
@@ -51,10 +56,18 @@ class RegenCommand extends MultiverseCommand {
.addAlias("-wb")
.build());
private final CommandFlag REMOVE_PLAYERS_FLAG = flag(CommandFlag.builder("--remove-players")
.addAlias("-r")
.build());
@Inject
RegenCommand(@NotNull MVCommandManager commandManager, @NotNull WorldManager worldManager) {
RegenCommand(
@NotNull MVCommandManager commandManager,
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter) {
super(commandManager);
this.worldManager = worldManager;
this.playerWorldTeleporter = playerWorldTeleporter;
}
@Subcommand("regen")
@@ -77,23 +90,43 @@ class RegenCommand extends MultiverseCommand {
this.commandManager.getCommandQueueManager().addToQueue(new QueuedCommand(
issuer.getIssuer(),
() -> {
issuer.sendInfo(MVCorei18n.REGEN_REGENERATING, "{world}", world.getName());
worldManager.regenWorld(RegenWorldOptions.world(world)
.randomSeed(parsedFlags.hasFlag(SEED_FLAG))
.seed(parsedFlags.flagValue(SEED_FLAG))
.keepWorldConfig(!parsedFlags.hasFlag(RESET_WORLD_CONFIG_FLAG))
.keepGameRule(!parsedFlags.hasFlag(RESET_GAMERULES_FLAG))
.keepWorldBorder(!parsedFlags.hasFlag(RESET_WORLD_BORDER_FLAG))
).onSuccess(newWorld -> {
Logging.fine("World regen success: " + newWorld);
issuer.sendInfo(MVCorei18n.REGEN_SUCCESS, "{world}", newWorld.getName());
}).onFailure(failure -> {
Logging.fine("World regen failure: " + failure);
issuer.sendError(failure.getFailureMessage());
});
},
() -> runRegenCommand(issuer, world, parsedFlags),
this.commandManager.formatMessage(
issuer, MessageType.INFO, MVCorei18n.REGEN_PROMPT, "{world}", world.getName())));
}
private void runRegenCommand(MVCommandIssuer issuer, LoadedMultiverseWorld world, ParsedCommandFlags parsedFlags) {
issuer.sendInfo(MVCorei18n.REGEN_REGENERATING, "{world}", world.getName());
List<Player> worldPlayers = world.getPlayers().getOrElse(Collections.emptyList());
var future = parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG)
? playerWorldTeleporter.removeFromWorld(world)
: Async.completedFuture(Collections.emptyList());
future.thenRun(() -> doWorldRegening(issuer, world, parsedFlags, worldPlayers));
}
private void doWorldRegening(
MVCommandIssuer issuer,
LoadedMultiverseWorld world,
ParsedCommandFlags parsedFlags,
List<Player> worldPlayers) {
RegenWorldOptions regenWorldOptions = RegenWorldOptions.world(world)
.randomSeed(parsedFlags.hasFlag(SEED_FLAG))
.seed(parsedFlags.flagValue(SEED_FLAG))
.keepWorldConfig(!parsedFlags.hasFlag(RESET_WORLD_CONFIG_FLAG))
.keepGameRule(!parsedFlags.hasFlag(RESET_GAMERULES_FLAG))
.keepWorldBorder(!parsedFlags.hasFlag(RESET_WORLD_BORDER_FLAG));
worldManager.regenWorld(regenWorldOptions).onSuccess(newWorld -> {
Logging.fine("World regen success: " + newWorld);
issuer.sendInfo(MVCorei18n.REGEN_SUCCESS, "{world}", newWorld.getName());
if (parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG)) {
playerWorldTeleporter.teleportPlayersToWorld(worldPlayers, newWorld);
}
}).onFailure(failure -> {
Logging.fine("World regen failure: " + failure);
issuer.sendError(failure.getFailureMessage());
});
}
}
@@ -1,10 +1,13 @@
package org.mvplugins.multiverse.core.commands;
import java.util.Collections;
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.Optional;
import co.aikar.commands.annotation.Single;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
@@ -16,19 +19,33 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.commandtools.MVCommandIssuer;
import org.mvplugins.multiverse.core.commandtools.MVCommandManager;
import org.mvplugins.multiverse.core.commandtools.MultiverseCommand;
import org.mvplugins.multiverse.core.commandtools.flags.CommandFlag;
import org.mvplugins.multiverse.core.commandtools.flags.ParsedCommandFlags;
import org.mvplugins.multiverse.core.utils.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.Async;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
@Service
@CommandAlias("mv")
class RemoveCommand extends MultiverseCommand {
private final WorldManager worldManager;
private final PlayerWorldTeleporter playerWorldTeleporter;
private final CommandFlag REMOVE_PLAYERS_FLAG = flag(CommandFlag.builder("--remove-players")
.addAlias("-r")
.build());
@Inject
RemoveCommand(@NotNull MVCommandManager commandManager, @NotNull WorldManager worldManager) {
RemoveCommand(
@NotNull MVCommandManager commandManager,
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter) {
super(commandManager);
this.worldManager = worldManager;
this.playerWorldTeleporter = playerWorldTeleporter;
}
@Subcommand("remove")
@@ -40,14 +57,31 @@ class RemoveCommand extends MultiverseCommand {
MVCommandIssuer issuer,
@Single
@Conditions("mvworlds:scope=both")
@Conditions("mvworlds:scope=both @flags:groupName=mvremovecommand")
@Syntax("<world>")
@Description("{@@mv-core.remove.world.description}")
String worldName) {
worldManager.removeWorld(worldName)
MultiverseWorld world,
@Optional
@Syntax("[--remove-players]")
@Description(/* TODO */"")
String[] flags) {
ParsedCommandFlags parsedFlags = parseFlags(flags);
var future = parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG)
? worldManager.getLoadedWorld(world)
.map(playerWorldTeleporter::removeFromWorld)
.getOrElse(Async.completedFuture(Collections.emptyList()))
: Async.completedFuture(Collections.emptyList());
future.thenRun(() -> doWorldRemoving(issuer, world));
}
private void doWorldRemoving(MVCommandIssuer issuer, MultiverseWorld world) {
worldManager.removeWorld(world)
.onSuccess(removedWorldName -> {
Logging.fine("World remove success: " + removedWorldName);
issuer.sendInfo(MVCorei18n.REMOVEWORLD_REMOVED, "{world}", removedWorldName);
issuer.sendInfo(MVCorei18n.REMOVE_SUCCESS, "{world}", removedWorldName);
}).onFailure(failure -> {
Logging.fine("World remove failure: " + failure);
issuer.sendError(failure.getFailureMessage());
@@ -1,9 +1,7 @@
package org.mvplugins.multiverse.core.commands;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.List;
import co.aikar.commands.BukkitCommandIssuer;
import co.aikar.commands.CommandIssuer;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
@@ -14,24 +12,32 @@ import co.aikar.commands.annotation.Syntax;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.commandtools.MVCommandIssuer;
import org.mvplugins.multiverse.core.commandtools.MVCommandManager;
import org.mvplugins.multiverse.core.commandtools.MultiverseCommand;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.ParsedDestination;
import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker;
import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter;
import org.mvplugins.multiverse.core.utils.MVCorei18n;
@Service
@CommandAlias("mv")
class TeleportCommand extends MultiverseCommand {
private final DestinationsProvider destinationsProvider;
private final CorePermissionsChecker permissionsChecker;
private final AsyncSafetyTeleporter safetyTeleporter;
@Inject
TeleportCommand(MVCommandManager commandManager, DestinationsProvider destinationsProvider) {
TeleportCommand(
@NotNull MVCommandManager commandManager,
@NotNull CorePermissionsChecker permissionsChecker,
@NotNull AsyncSafetyTeleporter safetyTeleporter) {
super(commandManager);
this.destinationsProvider = destinationsProvider;
this.permissionsChecker = permissionsChecker;
this.safetyTeleporter = safetyTeleporter;
}
@CommandAlias("mvtp")
@@ -40,7 +46,7 @@ class TeleportCommand extends MultiverseCommand {
@Syntax("[player] <destination>")
@Description("{@@mv-core.teleport.description}")
void onTeleportCommand(
BukkitCommandIssuer issuer,
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Syntax("[player]")
@@ -56,17 +62,25 @@ class TeleportCommand extends MultiverseCommand {
? issuer.getPlayer() == players[0] ? "you" : players[0].getName()
: players.length + " players";
// TODO: Multi player permission checking
if (!permissionsChecker.checkTeleportPermissions(issuer.getIssuer(), players[0], destination)) {
issuer.sendMessage("You do not have teleport permissions");
return;
}
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
"{player}", playerName, "{destination}", destination.toString());
CompletableFuture.allOf(Arrays.stream(players)
.map(player -> this.destinationsProvider.playerTeleportAsync(issuer, player, destination))
.toArray(CompletableFuture[]::new))
.thenRun(() -> Logging.finer("Async teleport completed."));
safetyTeleporter.teleportSafely(issuer.getIssuer(), List.of(players), destination)
.thenAccept(attempts -> Logging.fine("Async teleport completed: %s", attempts))
.exceptionally(throwable -> {
Logging.severe("Error while teleporting %s to %s: %s",
playerName, destination, throwable.getMessage());
});
}
@Override
public boolean hasPermission(CommandIssuer issuer) {
return this.destinationsProvider.hasAnyTeleportPermission(issuer);
return permissionsChecker.hasAnyTeleportPermission(issuer.getIssuer());
}
}
@@ -1,5 +1,7 @@
package org.mvplugins.multiverse.core.commands;
import java.util.Collections;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
@@ -18,8 +20,10 @@ import org.mvplugins.multiverse.core.commandtools.MultiverseCommand;
import org.mvplugins.multiverse.core.commandtools.flags.CommandFlag;
import org.mvplugins.multiverse.core.commandtools.flags.ParsedCommandFlags;
import org.mvplugins.multiverse.core.utils.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.Async;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions;
@Service
@@ -27,6 +31,7 @@ import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions;
class UnloadCommand extends MultiverseCommand {
private final WorldManager worldManager;
private final PlayerWorldTeleporter playerWorldTeleporter;
private final CommandFlag REMOVE_PLAYERS_FLAG = flag(CommandFlag.builder("--remove-players")
.addAlias("-r")
@@ -37,9 +42,13 @@ class UnloadCommand extends MultiverseCommand {
.build());
@Inject
UnloadCommand(@NotNull MVCommandManager commandManager, @NotNull WorldManager worldManager) {
UnloadCommand(
@NotNull MVCommandManager commandManager,
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter) {
super(commandManager);
this.worldManager = worldManager;
this.playerWorldTeleporter = playerWorldTeleporter;
}
@Subcommand("unload")
@@ -61,8 +70,16 @@ class UnloadCommand extends MultiverseCommand {
ParsedCommandFlags parsedFlags = parseFlags(flags);
issuer.sendInfo(MVCorei18n.UNLOAD_UNLOADING, "{world}", world.getAlias());
var future = parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG)
? playerWorldTeleporter.removeFromWorld(world)
: Async.completedFuture(Collections.emptyList());
future.thenRun(() -> doWorldUnloading(issuer, world, parsedFlags));
}
private void doWorldUnloading(MVCommandIssuer issuer, LoadedMultiverseWorld world, ParsedCommandFlags parsedFlags) {
UnloadWorldOptions unloadWorldOptions = UnloadWorldOptions.world(world)
.removePlayers(parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG))
.saveBukkitWorld(!parsedFlags.hasFlag(NO_SAVE_FLAG));
worldManager.unloadWorld(unloadWorldOptions)
.onSuccess(loadedWorld -> {
@@ -0,0 +1,37 @@
package org.mvplugins.multiverse.core.commands;
import co.aikar.commands.BukkitCommandIssuer;
import co.aikar.commands.MessageType;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Subcommand;
import jakarta.inject.Inject;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.commandtools.MVCommandManager;
import org.mvplugins.multiverse.core.commandtools.MultiverseCommand;
import org.mvplugins.multiverse.core.utils.MVCorei18n;
@Service
@CommandAlias("mv")
class VersionCommand extends MultiverseCommand {
private final MultiverseCore plugin;
@Inject
VersionCommand(@NotNull MVCommandManager commandManager, MultiverseCore plugin) {
super(commandManager);
this.plugin = plugin;
}
@Subcommand("version")
@CommandPermission("multiverse.core.version")
@Description("{@@mv-core.version.description}")
void versionCommand(BukkitCommandIssuer issuer) {
issuer.sendMessage(MessageType.INFO, MVCorei18n.VERSION_MV, "{version}", plugin.getDescription().getVersion());
issuer.sendMessage(MessageType.INFO, MVCorei18n.VERSION_AUTHORS, "{authors}", String.join(", ", plugin.getDescription().getAuthors()));
issuer.sendMessage(MessageType.INFO, MVCorei18n.VERSION_SECRETCODE); // An in joke I don't get...
}
}
@@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.commandtools;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -18,11 +19,15 @@ import com.dumptruckman.minecraft.util.Logging;
import com.google.common.collect.Sets;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.Difficulty;
import org.bukkit.GameMode;
import org.bukkit.GameRule;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.MVCoreConfig;
import org.mvplugins.multiverse.core.configuration.handle.PropertyModifyAction;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.ParsedDestination;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
@@ -30,33 +35,44 @@ import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@Service
public class MVCommandCompletions extends PaperCommandCompletions {
class MVCommandCompletions extends PaperCommandCompletions {
protected final MVCommandManager commandManager;
private final MVCommandManager commandManager;
private final WorldManager worldManager;
private final DestinationsProvider destinationsProvider;
private final MVCoreConfig config;
@Inject
public MVCommandCompletions(
MVCommandCompletions(
@NotNull MVCommandManager mvCommandManager,
@NotNull WorldManager worldManager,
@NotNull DestinationsProvider destinationsProvider,
@NotNull MVCoreConfig config
) {
@NotNull MVCoreConfig config) {
super(mvCommandManager);
this.commandManager = mvCommandManager;
this.worldManager = worldManager;
this.destinationsProvider = destinationsProvider;
this.config = config;
registerAsyncCompletion("commands", this::suggestCommands);
registerAsyncCompletion("destinations", this::suggestDestinations);
registerStaticCompletion("difficulties", suggestEnums(Difficulty.class));
registerStaticCompletion("environments", suggestEnums(World.Environment.class));
registerAsyncCompletion("flags", this::suggestFlags);
registerStaticCompletion("gamemodes", suggestEnums(GameMode.class));
registerStaticCompletion("gamerules", this::suggestGamerules);
registerStaticCompletion("mvconfigs", config.getNodes().getNames());
registerStaticCompletion("mvconfigs", config.getStringPropertyHandle().getAllPropertyNames());
registerAsyncCompletion("mvconfigvalues", this::suggestMVConfigValues);
registerAsyncCompletion("mvworlds", this::suggestMVWorlds);
registerAsyncCompletion("mvworldpropsname", this::suggestMVWorldPropsName);
registerAsyncCompletion("mvworldpropsvalue", this::suggestMVWorldPropsValue);
registerStaticCompletion("propsmodifyaction", suggestEnums(PropertyModifyAction.class));
setDefaultCompletion("destinations", ParsedDestination.class);
setDefaultCompletion("difficulties", Difficulty.class);
setDefaultCompletion("environments", World.Environment.class);
setDefaultCompletion("flags", String[].class);
setDefaultCompletion("gamemodes", GameMode.class);
setDefaultCompletion("gamerules", GameRule.class);
setDefaultCompletion("mvworlds", LoadedMultiverseWorld.class);
}
@@ -94,7 +110,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
}
return this.destinationsProvider
.suggestDestinations((BukkitCommandIssuer)context.getIssuer(), context.getInput());
.suggestDestinations((BukkitCommandIssuer) context.getIssuer(), context.getInput());
}
private Collection<String> suggestFlags(@NotNull BukkitCommandCompletionContext context) {
@@ -109,6 +125,13 @@ public class MVCommandCompletions extends PaperCommandCompletions {
return Arrays.stream(GameRule.values()).map(GameRule::getName).collect(Collectors.toList());
}
private Collection<String> suggestMVConfigValues(BukkitCommandCompletionContext context) {
return Try.of(() -> context.getContextValue(String.class))
.map(propertyName -> config.getStringPropertyHandle()
.getSuggestedPropertyValue(propertyName, context.getInput(), PropertyModifyAction.SET))
.getOrElse(Collections.emptyList());
}
private Collection<String> suggestMVWorlds(BukkitCommandCompletionContext context) {
if (context.hasConfig("playerOnly") && !context.getIssuer().isPlayer()) {
return Collections.emptyList();
@@ -152,4 +175,28 @@ public class MVCommandCompletions extends PaperCommandCompletions {
Logging.severe("Invalid MVWorld scope: " + scope);
return Collections.emptyList();
}
private Collection<String> suggestMVWorldPropsName(BukkitCommandCompletionContext context) {
return Try.of(() -> {
MultiverseWorld world = context.getContextValue(MultiverseWorld.class);
PropertyModifyAction action = context.getContextValue(PropertyModifyAction.class);
return world.getStringPropertyHandle().getModifiablePropertyNames(action);
}).getOrElse(Collections.emptyList());
}
private Collection<String> suggestMVWorldPropsValue(BukkitCommandCompletionContext context) {
return Try.of(() -> {
MultiverseWorld world = context.getContextValue(MultiverseWorld.class);
PropertyModifyAction action = context.getContextValue(PropertyModifyAction.class);
String propertyName = context.getContextValue(String.class);
return world.getStringPropertyHandle().getSuggestedPropertyValue(propertyName, context.getInput(), action);
}).getOrElse(Collections.emptyList());
}
private <T extends Enum<T>> Collection<String> suggestEnums(Class<T> enumClass) {
return EnumSet.allOf(enumClass).stream()
.map(Enum::name)
.map(String::toLowerCase)
.toList();
}
}
@@ -9,17 +9,13 @@ import co.aikar.commands.InvalidCommandArgument;
import co.aikar.commands.PaperCommandContexts;
import co.aikar.commands.contexts.ContextResolver;
import com.google.common.base.Strings;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.GameRule;
import org.bukkit.entity.Player;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.commandtools.context.GameRuleValue;
import org.mvplugins.multiverse.core.commandtools.context.MVConfigValue;
import org.mvplugins.multiverse.core.config.MVCoreConfig;
import org.mvplugins.multiverse.core.configuration.node.Node;
import org.mvplugins.multiverse.core.configuration.node.ValueNode;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.ParsedDestination;
import org.mvplugins.multiverse.core.display.filters.ContentFilter;
@@ -27,10 +23,11 @@ import org.mvplugins.multiverse.core.display.filters.DefaultContentFilter;
import org.mvplugins.multiverse.core.display.filters.RegexContentFilter;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@Service
public class MVCommandContexts extends PaperCommandContexts {
class MVCommandContexts extends PaperCommandContexts {
private final MVCommandManager mvCommandManager;
private final DestinationsProvider destinationsProvider;
@@ -38,12 +35,11 @@ public class MVCommandContexts extends PaperCommandContexts {
private final MVCoreConfig config;
@Inject
public MVCommandContexts(
MVCommandContexts(
MVCommandManager mvCommandManager,
DestinationsProvider destinationsProvider,
WorldManager worldManager,
MVCoreConfig config
) {
MVCoreConfig config) {
super(mvCommandManager);
this.mvCommandManager = mvCommandManager;
this.destinationsProvider = destinationsProvider;
@@ -56,9 +52,9 @@ public class MVCommandContexts extends PaperCommandContexts {
registerContext(ParsedDestination.class, this::parseDestination);
registerContext(GameRule.class, this::parseGameRule);
registerContext(GameRuleValue.class, this::parseGameRuleValue);
registerContext(MVConfigValue.class, this::parseMVConfigValue);
registerIssuerAwareContext(LoadedMultiverseWorld.class, this::parseMVWorld);
registerIssuerAwareContext(LoadedMultiverseWorld[].class, this::parseMVWorldArray);
registerIssuerAwareContext(LoadedMultiverseWorld.class, this::parseLoadedMultiverseWorld);
registerIssuerAwareContext(LoadedMultiverseWorld[].class, this::parseLoadedMultiverseWorldArray);
registerIssuerAwareContext(MultiverseWorld.class, this::parseMultiverseWorld);
registerIssuerAwareContext(Player.class, this::parsePlayer);
registerIssuerAwareContext(Player[].class, this::parsePlayerArray);
}
@@ -129,40 +125,7 @@ public class MVCommandContexts extends PaperCommandContexts {
return new GameRuleValue(resolvedValue);
}
private MVConfigValue parseMVConfigValue(BukkitCommandExecutionContext context) {
String configName = (String) context.getResolvedArg(String.class);
if (Strings.isNullOrEmpty(configName)) {
throw new InvalidCommandArgument("No config name specified.");
}
Option<Node> node = config.getNodes().findNode(configName);
if (node.isEmpty()) {
throw new InvalidCommandArgument("The config " + configName + " is not valid.");
}
String valueString = context.getFirstArg();
if (Strings.isNullOrEmpty(valueString)) {
throw new InvalidCommandArgument("No config value specified.");
}
if (!(node.get() instanceof ValueNode)) {
context.popFirstArg();
return new MVConfigValue(valueString);
}
ContextResolver<?, BukkitCommandExecutionContext> resolver = getResolver(((ValueNode<?>) node.get()).getType());
if (resolver == null) {
context.popFirstArg();
return new MVConfigValue(valueString);
}
Object resolvedValue = resolver.getContext(context);
if (resolvedValue == null) {
throw new InvalidCommandArgument("The config value " + valueString + " is not valid for config " + configName + ".");
}
return new MVConfigValue(resolvedValue);
}
private LoadedMultiverseWorld parseMVWorld(BukkitCommandExecutionContext context) {
private LoadedMultiverseWorld parseLoadedMultiverseWorld(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
// Get world based on sender only
@@ -205,7 +168,7 @@ public class MVCommandContexts extends PaperCommandContexts {
throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world.");
}
private LoadedMultiverseWorld[] parseMVWorldArray(BukkitCommandExecutionContext context) {
private LoadedMultiverseWorld[] parseLoadedMultiverseWorldArray(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
LoadedMultiverseWorld playerWorld = null;
@@ -265,6 +228,49 @@ public class MVCommandContexts extends PaperCommandContexts {
throw new InvalidCommandArgument("World " + worldStrings + " is not a loaded multiverse world.");
}
private MultiverseWorld parseMultiverseWorld(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
// Get world based on sender only
if (resolve.equals("issuerOnly")) {
if (context.getIssuer().isPlayer()) {
return worldManager.getWorld(context.getIssuer().getPlayer().getWorld()).getOrNull();
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("This command can only be used by a player in a Multiverse World.");
}
String worldName = context.getFirstArg();
MultiverseWorld world = worldManager.getWorld(worldName).getOrNull();
// Get world based on input, fallback to sender if input is not a world
if (resolve.equals("issuerAware")) {
if (world != null) {
context.popFirstArg();
return world;
}
if (context.getIssuer().isPlayer()) {
return worldManager.getWorld(context.getIssuer().getPlayer().getWorld()).getOrNull();
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("Player is not in a Multiverse World.");
}
// Get world based on input only
if (world != null) {
context.popFirstArg();
return world;
}
if (context.isOptional()) {
return null;
}
throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world.");
}
private Player parsePlayer(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
@@ -1,5 +1,6 @@
package org.mvplugins.multiverse.core.commandtools;
import co.aikar.commands.MessageKeys;
import co.aikar.commands.MessageType;
import co.aikar.commands.OpenBukkitCommandIssuer;
import co.aikar.locales.MessageKeyProvider;
@@ -7,6 +8,7 @@ import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.utils.message.Message;
import org.mvplugins.multiverse.core.utils.message.MessageReplacement;
public class MVCommandIssuer extends OpenBukkitCommandIssuer {
@@ -22,6 +24,58 @@ public class MVCommandIssuer extends OpenBukkitCommandIssuer {
return commandManager;
}
public void sendError(String message) {
sendMessage(MessageType.INFO, MessageKeys.INFO_MESSAGE, "{message}", message);
}
public void sendSyntax(String message) {
sendMessage(MessageType.SYNTAX, MessageKeys.INFO_MESSAGE, "{message}", message);
}
public void sendInfo(String message) {
sendMessage(MessageType.INFO, MessageKeys.INFO_MESSAGE, "{message}", message);
}
public void sendMessage(MessageType messageType, String message) {
var formatter = getManager().getFormat(messageType);
if (formatter != null) {
sendMessage(formatter.format(message));
} else {
sendMessage(message);
}
}
public void sendError(MessageKeyProvider key) {
sendMessage(MessageType.ERROR, key, new String[0]);
}
public void sendSyntax(MessageKeyProvider key) {
sendMessage(MessageType.SYNTAX, key, new String[0]);
}
public void sendInfo(MessageKeyProvider key) {
sendMessage(MessageType.INFO, key, new String[0]);
}
public void sendError(MessageKeyProvider key, MessageReplacement... replacements) {
sendMessage(MessageType.ERROR, key, replacements);
}
public void sendSyntax(MessageKeyProvider key, MessageReplacement... replacements) {
sendMessage(MessageType.SYNTAX, key, replacements);
}
public void sendInfo(MessageKeyProvider key, MessageReplacement... replacements) {
sendMessage(MessageType.INFO, key, replacements);
}
private void sendMessage(MessageType messageType, MessageKeyProvider key, MessageReplacement... replacements) {
sendMessage(messageType, Message.of(
key,
"{error_key: %s}".formatted(key.getMessageKey().getKey()),
replacements));
}
public void sendError(Message message) {
sendMessage(MessageType.ERROR, message);
}
@@ -34,7 +88,7 @@ public class MVCommandIssuer extends OpenBukkitCommandIssuer {
sendMessage(MessageType.INFO, message);
}
private void sendMessage(MessageType messageType, Message message) {
public void sendMessage(MessageType messageType, Message message) {
if (message instanceof MessageKeyProvider) {
sendMessage(messageType, (MessageKeyProvider) message,
message.getReplacements(getManager().getLocales(), this));
@@ -11,15 +11,26 @@ import org.mvplugins.multiverse.core.commandtools.flags.CommandFlagGroup;
import org.mvplugins.multiverse.core.commandtools.flags.CommandFlagsManager;
import org.mvplugins.multiverse.core.commandtools.flags.ParsedCommandFlags;
/**
* Base class for all Multiverse commands.
*/
@Contract
public abstract class MultiverseCommand extends BaseCommand {
public abstract class MultiverseCommand extends BaseCommand {
/**
* The command manager with multiverse specific methods.
*/
protected final MVCommandManager commandManager;
/**
* The flags manager for the above command manager.
*/
protected final CommandFlagsManager flagsManager;
private String flagGroupName;
private CommandFlagGroup.Builder flagGroupBuilder;
protected MultiverseCommand(@NotNull MVCommandManager commandManager) {
this.commandManager = commandManager;
this.flagsManager = commandManager.getFlagsManager();
}
@PostConstruct
@@ -30,10 +41,22 @@ public abstract class MultiverseCommand extends BaseCommand {
}
}
protected CommandFlagsManager getFlagsManager() {
return commandManager.getFlagsManager();
private void registerFlagGroup(@NotNull CommandFlagGroup flagGroup) {
if (flagGroupName != null) {
throw new IllegalStateException("Flag group already registered! (name: " + flagGroupName + ")");
}
flagsManager.registerFlagGroup(flagGroup);
flagGroupName = flagGroup.getName();
Logging.finest("Registered flag group: " + flagGroupName);
}
/**
* Add a new flag to the flag builder.
*
* @param flag The flag to add.
* @param <T> The type of the flag.
* @return The flag.
*/
protected <T extends CommandFlag> T flag(T flag) {
if (flagGroupBuilder == null) {
flagGroupBuilder = CommandFlagGroup.builder("mv" + getClass().getSimpleName().toLowerCase());
@@ -43,16 +66,13 @@ public abstract class MultiverseCommand extends BaseCommand {
return flag;
}
protected void registerFlagGroup(@NotNull CommandFlagGroup flagGroup) {
if (flagGroupName != null) {
throw new IllegalStateException("Flag group already registered! (name: " + flagGroupName + ")");
}
getFlagsManager().registerFlagGroup(flagGroup);
flagGroupName = flagGroup.getName();
Logging.fine("Registered flag group: " + flagGroupName);
}
/**
* Parses flags.
*
* @param flags The raw string array to parse into flags.
* @return The parsed flags.
*/
protected @NotNull ParsedCommandFlags parseFlags(@NotNull String[] flags) {
return getFlagsManager().parse(flagGroupName, flags);
return flagsManager.parse(flagGroupName, flags);
}
}
@@ -1,13 +0,0 @@
package org.mvplugins.multiverse.core.commandtools.context;
public class MVConfigValue {
private final Object value;
public MVConfigValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
@@ -16,13 +16,13 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.api.MVConfig;
import org.mvplugins.multiverse.core.configuration.handle.CommentedYamlConfigHandle;
import org.mvplugins.multiverse.core.configuration.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.configuration.migration.BooleanMigratorAction;
import org.mvplugins.multiverse.core.configuration.migration.ConfigMigrator;
import org.mvplugins.multiverse.core.configuration.migration.IntegerMigratorAction;
import org.mvplugins.multiverse.core.configuration.migration.InvertBoolMigratorAction;
import org.mvplugins.multiverse.core.configuration.migration.MoveMigratorAction;
import org.mvplugins.multiverse.core.configuration.migration.VersionMigrator;
import org.mvplugins.multiverse.core.configuration.node.NodeGroup;
@Service
public class MVCoreConfig implements MVConfig {
@@ -32,14 +32,14 @@ public class MVCoreConfig implements MVConfig {
private final Path configPath;
private final MVCoreConfigNodes configNodes;
private final CommentedYamlConfigHandle configHandle;
private final StringPropertyHandle stringPropertyHandle;
@Inject
MVCoreConfig(@NotNull MultiverseCore core, @NotNull PluginManager pluginManager) {
this.configPath = Path.of(core.getDataFolder().getPath(), CONFIG_FILENAME);
this.configNodes = new MVCoreConfigNodes(pluginManager);
this.configHandle = CommentedYamlConfigHandle.builder(configPath)
this.configHandle = CommentedYamlConfigHandle.builder(configPath, configNodes.getNodes())
.logger(Logging.getLogger())
.nodes(configNodes.getNodes())
.migrator(ConfigMigrator.builder(configNodes.VERSION)
.addVersionMigrator(VersionMigrator.builder(5.0)
.addAction(MoveMigratorAction.of("multiverse-configuration.enforceaccess", "world.enforce-access"))
@@ -70,7 +70,7 @@ public class MVCoreConfig implements MVConfig {
.build())
.build())
.build();
this.stringPropertyHandle = new StringPropertyHandle(configHandle);
load();
save();
}
@@ -108,18 +108,8 @@ public class MVCoreConfig implements MVConfig {
}
@Override
public NodeGroup getNodes() {
return configNodes.getNodes();
}
@Override
public Try<Object> getProperty(String name) {
return configHandle.get(name);
}
@Override
public Try<Void> setProperty(String name, Object value) {
return configHandle.set(name, value);
public StringPropertyHandle getStringPropertyHandle() {
return stringPropertyHandle;
}
@Override
@@ -0,0 +1,77 @@
package org.mvplugins.multiverse.core.configuration.functions;
import java.util.HashMap;
import java.util.Map;
import co.aikar.commands.ACFUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Provides default serializers for common types.
*/
public final class DefaultSerializerProvider {
private static final Map<Class<?>, NodeSerializer<?>> SERIALIZERS = new HashMap<>();
/**
* Adds a default serializer for the given type.
*
* @param type The type.
* @param serializer The serializer.
* @param <T> The type.
*/
public static <T> void addDefaultSerializer(@NotNull Class<T> type, @NotNull NodeSerializer<T> serializer) {
SERIALIZERS.put(type, serializer);
}
/**
* Gets the default serializer for the given type.
*
* @param type The type.
* @param <T> The type.
* @return The default serializer for the given type, or null if no default serializer exists.
*/
public static <T> @Nullable NodeSerializer<T> getDefaultSerializer(Class<T> type) {
if (type.isEnum()) {
// Special case for enums
return (NodeSerializer<T>) ENUM_SERIALIZER;
}
return (NodeSerializer<T>) SERIALIZERS.get(type);
}
private static final NodeSerializer<Enum> ENUM_SERIALIZER = new NodeSerializer<>() {
@Override
public Enum<?> deserialize(Object object, Class<Enum> type) {
return Enum.valueOf(type, String.valueOf(object).toUpperCase());
}
@Override
public Object serialize(Enum object, Class<Enum> type) {
return object.name();
}
};
private static final NodeSerializer<Boolean> BOOLEAN_SERIALIZER = new NodeSerializer<>() {
@Override
public Boolean deserialize(Object object, Class<Boolean> type) {
if (object instanceof Boolean) {
return (Boolean) object;
}
return ACFUtil.isTruthy(String.valueOf(object));
}
@Override
public Object serialize(Boolean object, Class<Boolean> type) {
return object;
}
};
static {
addDefaultSerializer(Boolean.class, BOOLEAN_SERIALIZER);
}
private DefaultSerializerProvider() {
// Prevent instantiation as this is a static utility class
}
}
@@ -0,0 +1,74 @@
package org.mvplugins.multiverse.core.configuration.functions;
import java.util.HashMap;
import java.util.Map;
import co.aikar.commands.ACFUtil;
import io.vavr.control.Try;
/**
* Provides default string parsers for common types.
*/
public final class DefaultStringParserProvider {
private static final Map<Class<?>, NodeStringParser<?>> PARSERS = new HashMap<>();
/**
* Adds a default string parser for the given type.
*
* @param clazz The type.
* @param parser The string parser.
*/
public static void addDefaultStringParser(Class<?> clazz, NodeStringParser<?> parser) {
PARSERS.put(clazz, parser);
}
/**
* Gets the default string parser for the given type.
*
* @param clazz The type.
* @param <T> The type.
* @return The default string parser for the given type, or null if no default string parser exists.
*/
public static <T> NodeStringParser<T> getDefaultStringParser(Class<T> clazz) {
if (clazz.isEnum()) {
// Special case for enums
return (NodeStringParser<T>) ENUM_STRING_PARSER;
}
return (NodeStringParser<T>) PARSERS.get(clazz);
}
private static final NodeStringParser<Enum> ENUM_STRING_PARSER = (input, type) -> Try.of(
() -> Enum.valueOf(type, input.toUpperCase()));
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(
() -> ACFUtil.isTruthy(String.valueOf(input).toLowerCase()));
private static final NodeStringParser<Integer> INTEGER_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseInt(input));
private static final NodeStringParser<Double> DOUBLE_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseDouble(input));
private static final NodeStringParser<Float> FLOAT_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseFloat(input));
private static final NodeStringParser<Long> LONG_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseLong(input));
static {
addDefaultStringParser(String.class, STRING_STRING_PARSER);
addDefaultStringParser(Boolean.class, BOOLEAN_STRING_PARSER);
addDefaultStringParser(Integer.class, INTEGER_STRING_PARSER);
addDefaultStringParser(Double.class, DOUBLE_STRING_PARSER);
addDefaultStringParser(Float.class, FLOAT_STRING_PARSER);
addDefaultStringParser(Long.class, LONG_STRING_PARSER);
}
private DefaultStringParserProvider() {
// Prevent instantiation as this is a static utility class
}
}
@@ -0,0 +1,65 @@
package org.mvplugins.multiverse.core.configuration.functions;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.jetbrains.annotations.Nullable;
/**
* Provides default suggestions for common types.
*/
public final class DefaultSuggesterProvider {
private static final Map<Class<?>, NodeSuggester> SUGGESTERS = new HashMap<>();
/**
* Adds a default suggester for the given type.
*
* @param clazz The type.
* @param suggester The suggester.
*/
public static void addDefaultSuggester(Class<?> clazz, NodeSuggester suggester) {
SUGGESTERS.put(clazz, suggester);
}
/**
* Gets the default suggester for the given type.
*
* @param clazz The type.
* @return The default suggester for the given type, or null if no default suggester exists.
*/
public static @Nullable NodeSuggester getDefaultSuggester(Class<?> clazz) {
if (clazz.isEnum()) {
// Special case for enums
return enumSuggester(clazz);
}
return SUGGESTERS.get(clazz);
}
private static NodeSuggester enumSuggester(Class<?> clazz) {
return input -> Arrays.stream(clazz.getEnumConstants())
.map(Object::toString)
.map(String::toLowerCase)
.collect(Collectors.toList());
}
private static final NodeSuggester BOOLEAN_SUGGESTER = input -> List.of("true", "false");
private static final NodeSuggester INTEGER_SUGGESTER = input -> IntStream.range(1, 10)
.boxed()
.map(String::valueOf)
.collect(Collectors.toList());
static {
addDefaultSuggester(Boolean.class, BOOLEAN_SUGGESTER);
addDefaultSuggester(Integer.class, INTEGER_SUGGESTER);
}
private DefaultSuggesterProvider() {
// Prevent instantiation as this is a static utility class
}
}
@@ -0,0 +1,26 @@
package org.mvplugins.multiverse.core.configuration.functions;
/**
* A function that serializes and deserializes objects to and from YAML.
*
* @param <T> The type of the object to serialize and deserialize.
*/
public interface NodeSerializer<T> {
/**
* Deserializes an object from YAML.
*
* @param object The object to deserialize.
* @param type The type of the object to deserialize.
* @return The deserialized typed value.
*/
T deserialize(Object object, Class<T> type);
/**
* Serializes an object to YAML.
*
* @param object The object to serialize.
* @param type The type of the object to serialize.
* @return The serialized object.
*/
Object serialize(T object, Class<T> type);
}
@@ -0,0 +1,22 @@
package org.mvplugins.multiverse.core.configuration.functions;
import io.vavr.control.Try;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A function that parses a string into a node value object of type {@link T}.
*
* @param <T> The type of the object to parse.
*/
@FunctionalInterface
public interface NodeStringParser<T> {
/**
* Parses a string into a node value object of type {@link T}.
*
* @param string The string to parse.
* @param type The type of the object to parse.
* @return The parsed object, or {@link Try.Failure} if the string could not be parsed.
*/
@NotNull Try<T> parse(@Nullable String string, @NotNull Class<T> type);
}
@@ -0,0 +1,20 @@
package org.mvplugins.multiverse.core.configuration.functions;
import java.util.Collection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A function that suggests possible values for a node value.
*/
@FunctionalInterface
public interface NodeSuggester {
/**
* Suggests possible values for a node value. Generated based on the current user input.
*
* @param input The current partial user input
* @return The possible values.
*/
@NotNull Collection<String> suggest(@Nullable String input);
}
@@ -0,0 +1 @@
package org.mvplugins.multiverse.core.configuration.functions;
@@ -24,16 +24,17 @@ public class CommentedYamlConfigHandle extends FileConfigHandle<CommentedConfigu
* Creates a new builder for a {@link CommentedYamlConfigHandle}.
*
* @param configPath The path to the config file.
* @param nodes The nodes.
* @return The builder.
*/
public static @NotNull Builder builder(@NotNull Path configPath) {
return new Builder(configPath);
public static @NotNull Builder builder(@NotNull Path configPath, @NotNull NodeGroup nodes) {
return new Builder(configPath, nodes);
}
protected CommentedYamlConfigHandle(
@NotNull Path configPath,
@Nullable Logger logger,
@Nullable NodeGroup nodes,
@NotNull NodeGroup nodes,
@Nullable ConfigMigrator migrator) {
super(configPath, logger, nodes, migrator);
}
@@ -76,7 +77,7 @@ public class CommentedYamlConfigHandle extends FileConfigHandle<CommentedConfigu
valueNode.getDefaultValue())).onFailure(e -> {
Logging.warning("Failed to set node " + valueNode.getPath()
+ " to " + valueNode.getDefaultValue());
setDefault(valueNode);
reset(valueNode);
});
}
});
@@ -96,8 +97,8 @@ public class CommentedYamlConfigHandle extends FileConfigHandle<CommentedConfigu
*/
public static class Builder extends FileConfigHandle.Builder<CommentedConfiguration, Builder> {
protected Builder(@NotNull Path configPath) {
super(configPath);
protected Builder(@NotNull Path configPath, @NotNull NodeGroup nodes) {
super(configPath, nodes);
}
/**
@@ -18,16 +18,19 @@ public class ConfigurationSectionHandle extends GenericConfigHandle<Configuratio
* Creates a new builder for a {@link ConfigurationSectionHandle}.
*
* @param configurationSection The configuration section.
* @param nodes The nodes.
* @return The builder.
*/
public static Builder<? extends Builder> builder(@NotNull ConfigurationSection configurationSection) {
return new Builder<>(configurationSection);
public static Builder<? extends Builder> builder(
@NotNull ConfigurationSection configurationSection, @NotNull NodeGroup nodes) {
return new Builder<>(configurationSection, nodes);
}
protected ConfigurationSectionHandle(@NotNull ConfigurationSection configurationSection,
@Nullable Logger logger,
@Nullable NodeGroup nodes,
@Nullable ConfigMigrator migrator) {
protected ConfigurationSectionHandle(
@NotNull ConfigurationSection configurationSection,
@Nullable Logger logger,
@NotNull NodeGroup nodes,
@Nullable ConfigMigrator migrator) {
super(logger, nodes, migrator);
this.config = configurationSection;
}
@@ -51,7 +54,8 @@ public class ConfigurationSectionHandle extends GenericConfigHandle<Configuratio
public static class Builder<B extends Builder<B>> extends GenericConfigHandle.Builder<ConfigurationSection, B> {
private final ConfigurationSection configurationSection;
protected Builder(@NotNull ConfigurationSection configurationSection) {
protected Builder(@NotNull ConfigurationSection configurationSection, @NotNull NodeGroup nodes) {
super(nodes);
this.configurationSection = configurationSection;
}
@@ -16,14 +16,19 @@ import org.mvplugins.multiverse.core.configuration.node.NodeGroup;
/**
* Generic configuration handle for file based configurations.
*
* @param <C> The configuration type.
*/
abstract class FileConfigHandle<C extends FileConfiguration> extends GenericConfigHandle<C> {
public abstract class FileConfigHandle<C extends FileConfiguration> extends GenericConfigHandle<C> {
protected final @NotNull Path configPath;
protected final @NotNull File configFile;
protected FileConfigHandle(@NotNull Path configPath, @Nullable Logger logger, @Nullable NodeGroup nodes, @Nullable ConfigMigrator migrator) {
protected FileConfigHandle(
@NotNull Path configPath,
@Nullable Logger logger,
@NotNull NodeGroup nodes,
@Nullable ConfigMigrator migrator) {
super(logger, nodes, migrator);
this.configPath = configPath;
this.configFile = configPath.toFile();
@@ -95,11 +100,13 @@ abstract class FileConfigHandle<C extends FileConfiguration> extends GenericConf
* @param <C> The configuration type.
* @param <B> The builder type.
*/
public static abstract class Builder<C extends FileConfiguration, B extends Builder<C, B>> extends GenericConfigHandle.Builder<C, B> {
public abstract static class Builder<C extends FileConfiguration, B extends Builder<C, B>>
extends GenericConfigHandle.Builder<C, B> {
protected @NotNull Path configPath;
protected Builder(@NotNull Path configPath) {
protected Builder(@NotNull Path configPath, @NotNull NodeGroup nodes) {
super(nodes);
this.configPath = configPath;
}
@@ -1,5 +1,6 @@
package org.mvplugins.multiverse.core.configuration.handle;
import java.util.List;
import java.util.logging.Logger;
import io.vavr.control.Try;
@@ -9,21 +10,24 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.migration.ConfigMigrator;
import org.mvplugins.multiverse.core.configuration.node.ConfigNodeNotFoundException;
import org.mvplugins.multiverse.core.configuration.node.ListValueNode;
import org.mvplugins.multiverse.core.configuration.node.NodeGroup;
import org.mvplugins.multiverse.core.configuration.node.ValueNode;
/**
* Generic configuration handle for all ConfigurationSection types.
*
* @param <C> The configuration type.
*/
public abstract class GenericConfigHandle<C extends ConfigurationSection> {
protected final @Nullable Logger logger;
protected final @Nullable NodeGroup nodes;
protected final @NotNull NodeGroup nodes;
protected final @Nullable ConfigMigrator migrator;
protected C config;
protected GenericConfigHandle(@Nullable Logger logger, @Nullable NodeGroup nodes, @Nullable ConfigMigrator migrator) {
protected GenericConfigHandle(@Nullable Logger logger, @NotNull NodeGroup nodes, @Nullable ConfigMigrator migrator) {
this.logger = logger;
this.nodes = nodes;
this.migrator = migrator;
@@ -67,17 +71,6 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
});
}
/**
* Gets the value of a node, if the node has a default value, it will be returned if the node is not found.
* @param name The name of the node.
* @return The value of the node.
*/
public Try<Object> get(@Nullable String name) {
return nodes.findNode(name, ValueNode.class)
.toTry(() -> new ConfigNodeNotFoundException(name))
.map(node -> get((ValueNode<Object>) node));
}
/**
* Gets the value of a node, if the node has a default value, it will be returned if the node is not found.
*
@@ -91,26 +84,13 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
return node.getSerializer().deserialize(config.get(node.getPath(), node.getDefaultValue()), node.getType());
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param name The name of the node.
* @param value The value to set.
* @return True if the value was set, false otherwise.
*/
public Try<Void> set(@Nullable String name, Object value) {
return nodes.findNode(name, ValueNode.class)
.toTry(() -> new ConfigNodeNotFoundException(name))
.flatMap(node -> set(node, value));
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param node The node to set the value of.
* @param value The value to set.
* @return True if the value was set, false otherwise.
* @param <T> The type of the node value.
* @return Empty try if the value was set, try containing an error otherwise.
*/
public <T> Try<Void> set(@NotNull ValueNode<T> node, T value) {
return node.validate(value).map(ignore -> {
@@ -126,13 +106,74 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
});
}
/**
* Adds an item to a list node.
*
* @param node The list node to add the item to.
* @param itemValue The value of the item to add.
* @param <I> The type of the list item.
* @return Empty try if the item was added, try containing an error otherwise.
*/
public <I> Try<Void> add(@NotNull ListValueNode<I> node, I itemValue) {
return node.validateItem(itemValue).map(ignore -> {
var serialized = node.getItemSerializer() != null
? node.getItemSerializer().serialize(itemValue, node.getItemType())
: itemValue;
List valueList = config.getList(node.getPath());
if (valueList == null) {
throw new IllegalArgumentException("Cannot add item to non-list node");
}
valueList.add(serialized);
config.set(node.getPath(), valueList);
node.onSetItemValue(null, itemValue);
return null;
});
}
/**
* Removes an item from a list node.
*
* @param node The list node to remove the item from.
* @param itemValue The value of the item to remove.
* @param <I> The type of the list item.
* @return Empty try if the item was removed, try containing an error otherwise.
*/
public <I> Try<Void> remove(@NotNull ListValueNode<I> node, I itemValue) {
return node.validateItem(itemValue).map(ignore -> {
var serialized = node.getItemSerializer() != null
? node.getItemSerializer().serialize(itemValue, node.getItemType())
: itemValue;
List valueList = config.getList(node.getPath());
if (valueList == null) {
throw new IllegalArgumentException("Cannot remove item from non-list node");
}
if (!valueList.remove(serialized)) {
throw new IllegalArgumentException("Cannot remove item from list node");
}
config.set(node.getPath(), valueList);
node.onSetItemValue(itemValue, null);
return null;
});
}
/**
* Sets the default value of a node.
*
* @param node The node to set the default value of.
* @param <T> The type of the node value.
* @return Empty try if the value was set, try containing an error otherwise.
*/
public void setDefault(@NotNull ValueNode node) {
config.set(node.getPath(), node.getDefaultValue());
public <T> Try<Void> reset(@NotNull ValueNode<T> node) {
return Try.run(() -> config.set(node.getPath(), node.getDefaultValue()));
}
/**
* Gets the configuration. Mainly used for {@link StringPropertyHandle}.
*
* @return The configuration.
*/
@NotNull NodeGroup getNodes() {
return nodes;
}
/**
@@ -141,13 +182,15 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
* @param <C> The configuration type.
* @param <B> The builder type.
*/
public static abstract class Builder<C extends ConfigurationSection, B extends GenericConfigHandle.Builder<C, B>> {
public abstract static class Builder<C extends ConfigurationSection, B extends GenericConfigHandle.Builder<C, B>> {
protected final @NotNull NodeGroup nodes;
protected @Nullable Logger logger;
protected @Nullable NodeGroup nodes;
protected @Nullable ConfigMigrator migrator;
protected Builder() {}
protected Builder(@NotNull NodeGroup nodes) {
this.nodes = nodes;
}
/**
* Sets the logger.
@@ -171,17 +214,6 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
return self();
}
/**
* Sets the nodes.
*
* @param nodes The nodes.
* @return The builder.
*/
public B nodes(@Nullable NodeGroup nodes) {
this.nodes = nodes;
return self();
}
/**
* Sets the migrator.
*
@@ -0,0 +1,11 @@
package org.mvplugins.multiverse.core.configuration.handle;
/**
* The type of modification to a config.
*/
public enum PropertyModifyAction {
SET,
ADD,
REMOVE,
RESET
}
@@ -0,0 +1,234 @@
package org.mvplugins.multiverse.core.configuration.handle;
import java.util.Collection;
import java.util.Collections;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.node.ConfigNodeNotFoundException;
import org.mvplugins.multiverse.core.configuration.node.ListValueNode;
import org.mvplugins.multiverse.core.configuration.node.Node;
import org.mvplugins.multiverse.core.configuration.node.ValueNode;
/**
* Handles setting of config with string names and values.
*/
public class StringPropertyHandle {
private final @NotNull GenericConfigHandle<?> handle;
/**
* Creates a new string property handle.
*
* @param handle The handle to wrap.
*/
public StringPropertyHandle(@NotNull GenericConfigHandle<?> handle) {
this.handle = handle;
}
/**
* Gets the names of all properties in this handle.
*
* @return The names of all properties in this handle.
*/
public Collection<String> getAllPropertyNames() {
return handle.getNodes().getNames();
}
/**
* Gets the names of all properties in this handle that can be modified by the given action.
*
* @param action The action to perform.
* @return The names of all properties in this handle that can be modified by the given action.
*/
public Collection<String> getModifiablePropertyNames(PropertyModifyAction action) {
return switch (action) {
case SET, RESET -> handle.getNodes().getNames();
case ADD, REMOVE -> handle.getNodes().stream()
.filter(node -> node instanceof ListValueNode)
.map(node -> ((ValueNode<?>) node).getName())
.filter(Option::isDefined)
.map(Option::get)
.toList();
default -> Collections.emptyList();
};
}
/**
* Gets the type of property.
*
* @param name The name of the property.
* @return The type of the property, or an error if the property was not found.
*/
public Try<Class<?>> getPropertyType(@Nullable String name) {
return findNode(name, ValueNode.class).map(ValueNode::getType);
}
/**
* Auto-complete suggestions for a property.
*
* @param name The name of the node.
* @param input The current user input.
* @return A collection of possible string values.
*/
public Collection<String> getSuggestedPropertyValue(
@Nullable String name, @Nullable String input, @NotNull PropertyModifyAction action) {
return switch (action) {
case SET -> findNode(name, ValueNode.class)
.map(node -> node.suggest(input))
.getOrElse(Collections.emptyList());
case ADD -> findNode(name, ListValueNode.class)
.map(node -> node.suggestItem(input))
.getOrElse(Collections.emptyList());
case REMOVE -> findNode(name, ListValueNode.class)
.map(node -> handle.get((ListValueNode<?>) node))
.map(valueList -> valueList.stream()
.map(String::valueOf)
.toList())
.getOrElse(Collections.emptyList());
default -> Collections.emptyList();
};
}
/**
* Gets the value of a node, if the node has a default value, it will be returned if the node is not found.
*
* @param name The name of the node.
* @return The value of the node, or an error if the node was not found.
*/
public Try<Object> getProperty(@Nullable String name) {
return findNode(name, ValueNode.class).map(node -> handle.get(node));
}
/**
* Sets the value of a node.
*
* @param name The name of the node.
* @param value The value to set.
* @return Empty try if the value was set, try containing an error otherwise.
*/
public Try<Void> setProperty(@Nullable String name, @Nullable Object value) {
return findNode(name, ValueNode.class).flatMap(node -> handle.set(node, value));
}
/**
* Adds a value to a list node.
*
* @param name The name of the node.
* @param value The value to add.
* @return Empty try if the value was added, try containing an error otherwise.
*/
public Try<Void> addProperty(@Nullable String name, @Nullable Object value) {
return findNode(name, ListValueNode.class).flatMap(node -> handle.add(node, value));
}
/**
* Removes a value from a list node.
*
* @param name The name of the node.
* @param value The value to remove.
* @return Empty try if the value was removed, try containing an error otherwise.
*/
public Try<Void> removeProperty(@Nullable String name, @Nullable Object value) {
return findNode(name, ListValueNode.class).flatMap(node -> handle.remove(node, value));
}
/**
* Resets the value of a node to its default value.
*
* @param name The name of the node.
* @return Empty try if the value was reset, try containing an error otherwise.
*/
public Try<Void> resetProperty(@Nullable String name) {
return findNode(name, ValueNode.class).flatMap(node -> handle.reset(node));
}
/**
* Modifies the value of a node based on the given action.
*
* @param name The name of the node.
* @param value The value to modify.
* @param action The action to perform.
* @return Empty try if the value was modified, try containing an error otherwise.
*/
public Try<Void> modifyProperty(
@Nullable String name, @Nullable Object value, @NotNull PropertyModifyAction action) {
return switch (action) {
case SET -> setProperty(name, value);
case ADD -> addProperty(name, value);
case REMOVE -> removeProperty(name, value);
case RESET -> resetProperty(name);
default -> Try.failure(new IllegalArgumentException("Unknown action: " + action));
};
}
/**
* Sets the string value of a node.
*
* @param name The name of the node.
* @param value The string value to set.
* @return Empty try if the value was set, try containing an error otherwise.
*/
public Try<Void> setPropertyString(@Nullable String name, @Nullable String value) {
return findNode(name, ValueNode.class)
.flatMap(node -> node.parseFromString(value)
.flatMap(parsedValue -> handle.set(node, parsedValue)));
}
/**
* Adds a string value to a list node.
*
* @param name The name of the node.
* @param value The string value to add.
* @return Empty try if the value was added, try containing an error otherwise.
*/
public Try<Void> addPropertyString(@Nullable String name, @Nullable String value) {
return findNode(name, ListValueNode.class)
.flatMap(node -> node.parseItemFromString(value)
.flatMap(parsedValue -> handle.add(node, parsedValue)));
}
/**
* Removes a string value from a list node.
*
* @param name The name of the node.
* @param value The string value to remove.
* @return Empty try if the value was removed, try containing an error otherwise.
*/
public Try<Void> removePropertyString(@Nullable String name, @Nullable String value) {
return findNode(name, ListValueNode.class)
.flatMap(node -> node.parseItemFromString(value)
.flatMap(parsedValue -> handle.remove(node, parsedValue)));
}
/**
* Modifies the value of a node based on the given action.
*
* @param name The name of the node.
* @param value The string value to modify.
* @param action The action to perform.
* @return Empty try if the value was modified, try containing an error otherwise.
*/
public Try<Void> modifyPropertyString(
@Nullable String name, @Nullable String value, @NotNull PropertyModifyAction action) {
return switch (action) {
case SET -> setPropertyString(name, value);
case ADD -> addPropertyString(name, value);
case REMOVE -> removePropertyString(name, value);
case RESET -> resetProperty(name);
default -> Try.failure(new IllegalArgumentException("Unknown action: " + action));
};
}
private <T extends Node> Try<T> findNode(@Nullable String name, @NotNull Class<T> type) {
return handle.getNodes().findNode(name, type)
.toTry(() -> new ConfigNodeNotFoundException(name));
}
}
@@ -22,13 +22,18 @@ public class YamlConfigHandle extends FileConfigHandle<YamlConfiguration> {
* Creates a new builder for {@link YamlConfigHandle}.
*
* @param configPath The path to the config file.
* @param nodes The nodes.
* @return The builder.
*/
public static @NotNull Builder<? extends Builder> builder(@NotNull Path configPath) {
return new Builder<>(configPath);
public static @NotNull Builder<? extends Builder> builder(@NotNull Path configPath, @NotNull NodeGroup nodes) {
return new Builder<>(configPath, nodes);
}
protected YamlConfigHandle(@NotNull Path configPath, @Nullable Logger logger, @Nullable NodeGroup nodes, @Nullable ConfigMigrator migrator) {
protected YamlConfigHandle(
@NotNull Path configPath,
@Nullable Logger logger,
@NotNull NodeGroup nodes,
@Nullable ConfigMigrator migrator) {
super(configPath, logger, nodes, migrator);
}
@@ -51,12 +56,13 @@ public class YamlConfigHandle extends FileConfigHandle<YamlConfiguration> {
/**
* Builder for {@link YamlConfigHandle}.
*
* @param <B> The type of the builder.
*/
public static class Builder<B extends Builder<B>> extends FileConfigHandle.Builder<YamlConfiguration, B> {
protected Builder(@NotNull Path configPath) {
super(configPath);
protected Builder(@NotNull Path configPath, @NotNull NodeGroup nodes) {
super(configPath, nodes);
}
/**
@@ -1,15 +1,26 @@
package org.mvplugins.multiverse.core.configuration.node;
import java.util.Collection;
import java.util.Collections;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.functions.DefaultSerializerProvider;
import org.mvplugins.multiverse.core.configuration.functions.DefaultStringParserProvider;
import org.mvplugins.multiverse.core.configuration.functions.DefaultSuggesterProvider;
import org.mvplugins.multiverse.core.configuration.functions.NodeSerializer;
import org.mvplugins.multiverse.core.configuration.functions.NodeStringParser;
import org.mvplugins.multiverse.core.configuration.functions.NodeSuggester;
/**
* A node that contains a value.
*
* @param <T> The type of the value.
*/
public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@@ -19,38 +30,48 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
*
* @param path The path of the node.
* @param type The type of the value.
* @return The new builder.
* @param <T> The type of the value.
* @return The new builder.
*/
public static @NotNull <T> ConfigNode.Builder<T, ? extends ConfigNode.Builder<T, ?>> builder(
@NotNull String path,
@NotNull Class<T> type
) {
@NotNull Class<T> type) {
return new ConfigNode.Builder<>(path, type);
}
protected final @Nullable String name;
protected final @NotNull Class<T> type;
protected final @Nullable T defaultValue;
protected final @Nullable NodeSerializer<T> serializer;
protected final @Nullable Function<T, Try<Void>> validator;
protected final @Nullable BiConsumer<T, T> onSetValue;
protected final @Nullable Supplier<T> defaultValue;
protected @Nullable NodeSuggester suggester;
protected @Nullable NodeStringParser<T> stringParser;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
protected ConfigNode(
@NotNull String path,
@NotNull String[] comments,
@Nullable String name,
@NotNull Class<T> type,
@Nullable T defaultValue,
@Nullable Supplier<T> defaultValue,
@Nullable NodeSuggester suggester,
@Nullable NodeStringParser<T> stringParser,
@Nullable NodeSerializer<T> serializer,
@Nullable Function<T, Try<Void>> validator,
@Nullable BiConsumer<T, T> onSetValue
) {
@Nullable BiConsumer<T, T> onSetValue) {
super(path, comments);
this.name = name;
this.type = type;
this.defaultValue = defaultValue;
this.serializer = serializer;
this.suggester = (suggester != null)
? suggester
: DefaultSuggesterProvider.getDefaultSuggester(type);
this.stringParser = (stringParser != null)
? stringParser
: DefaultStringParserProvider.getDefaultStringParser(type);
this.serializer = (serializer != null)
? serializer
: DefaultSerializerProvider.getDefaultSerializer(type);
this.validator = validator;
this.onSetValue = onSetValue;
}
@@ -76,9 +97,37 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
*/
@Override
public @Nullable T getDefaultValue() {
return defaultValue;
if (defaultValue != null) {
return defaultValue.get();
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Collection<String> suggest(@Nullable String input) {
if (suggester != null) {
return suggester.suggest(input);
}
return Collections.emptyList();
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Try<T> parseFromString(@Nullable String input) {
if (stringParser != null) {
return stringParser.parse(input, type);
}
return Try.failure(new UnsupportedOperationException("No string parser for type " + type.getName()));
}
/**
* {@inheritDoc}
*/
public @Nullable NodeSerializer<T> getSerializer() {
return serializer;
}
@@ -111,11 +160,12 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
* @param <B> The type of the builder.
*/
public static class Builder<T, B extends ConfigNode.Builder<T, B>> extends ConfigHeaderNode.Builder<B> {
private static final NodeSerializer<?> ENUM_NODE_SERIALIZER = new EnumNodeSerializer<>();
protected @Nullable String name;
protected @NotNull final Class<T> type;
protected @Nullable T defaultValue;
protected @Nullable Supplier<T> defaultValue;
protected @Nullable NodeSuggester suggester;
protected @Nullable NodeStringParser<T> stringParser;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
@@ -130,9 +180,6 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
super(path);
this.name = path;
this.type = type;
if (type.isEnum()) {
this.serializer = (NodeSerializer<T>) ENUM_NODE_SERIALIZER;
}
}
/**
@@ -142,8 +189,19 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
* @return This builder.
*/
public @NotNull B defaultValue(@NotNull T defaultValue) {
this.defaultValue = () -> defaultValue;
return self();
}
/**
* Sets the default value for this node.
*
* @param defaultValue The default value supplier.
* @return This builder.
*/
public @NotNull B defaultValue(@NotNull Supplier<T> defaultValue) {
this.defaultValue = defaultValue;
return (B) this;
return self();
}
/**
@@ -154,17 +212,51 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
*/
public @NotNull B name(@Nullable String name) {
this.name = name;
return (B) this;
return self();
}
/**
* Sets the suggester for this node.
*
* @param suggester The suggester for this node.
* @return This builder.
*/
public @NotNull B suggester(@NotNull NodeSuggester suggester) {
this.suggester = suggester;
return self();
}
/**
* Sets the string parser for this node.
*
* @param stringParser The string parser for this node.
* @return This builder.
*/
public @NotNull B stringParser(@NotNull NodeStringParser<T> stringParser) {
this.stringParser = stringParser;
return self();
}
/**
* Sets the serializer for this node.
*
* @param serializer The serializer for this node.
* @return This builder.
*/
public @NotNull B serializer(@NotNull NodeSerializer<T> serializer) {
this.serializer = serializer;
return (B) this;
return self();
}
/**
* Sets the validator for this node.
*
* @param validator The validator for this node.
* @return This builder.
*/
public @NotNull B validator(@NotNull Function<T, Try<Void>> validator) {
this.validator = validator;
return (B) this;
return self();
}
/**
@@ -175,7 +267,7 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
*/
public @NotNull B onSetValue(@NotNull BiConsumer<T, T> onSetValue) {
this.onSetValue = onSetValue;
return (B) this;
return self();
}
/**
@@ -183,7 +275,13 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
*/
@Override
public @NotNull ConfigNode<T> build() {
return new ConfigNode<>(path, comments.toArray(new String[0]), name, type, defaultValue, serializer, validator, onSetValue);
return new ConfigNode<>(path, comments.toArray(new String[0]),
name, type, defaultValue, suggester, stringParser, serializer, validator, onSetValue);
}
protected @NotNull B self() {
//noinspection unchecked
return (B) this;
}
}
}
@@ -1,14 +0,0 @@
package org.mvplugins.multiverse.core.configuration.node;
public class EnumNodeSerializer<T extends Enum<T>> implements NodeSerializer<T> {
@Override
public T deserialize(Object object, Class<T> type) {
return Enum.valueOf(type, object.toString().toUpperCase());
}
@Override
public Object serialize(T object, Class<T> type) {
return object.toString();
}
}
@@ -0,0 +1,329 @@
package org.mvplugins.multiverse.core.configuration.node;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import io.vavr.Value;
import io.vavr.control.Try;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.functions.DefaultStringParserProvider;
import org.mvplugins.multiverse.core.configuration.functions.DefaultSuggesterProvider;
import org.mvplugins.multiverse.core.configuration.functions.NodeSerializer;
import org.mvplugins.multiverse.core.configuration.functions.NodeStringParser;
import org.mvplugins.multiverse.core.configuration.functions.NodeSuggester;
/**
* A config node that contains a list of values.
*
* @param <I> The type of the value.
*/
public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueNode<I> {
/**
* Creates a new builder for a {@link ConfigNode}.
*
* @param path The path of the node.
* @param type The type of the value.
* @param <I> The type of the value.
* @return The new builder.
*/
public static @NotNull <I> Builder<I, ? extends Builder<I, ?>> listBuilder(
@NotNull String path,
@NotNull Class<I> type) {
return new Builder<>(path, type);
}
protected final Class<I> itemType;
protected final NodeSuggester itemSuggester;
protected final NodeStringParser<I> itemStringParser;
protected final NodeSerializer<I> itemSerializer;
protected final Function<I, Try<Void>> itemValidator;
protected final BiConsumer<I, I> onSetItemValue;
protected ListConfigNode(
@NotNull String path,
@NotNull String[] comments,
@Nullable String name,
@NotNull Class<List<I>> type,
@Nullable Supplier<List<I>> defaultValueSupplier,
@Nullable NodeSuggester suggester,
@Nullable NodeStringParser<List<I>> stringParser,
@Nullable NodeSerializer<List<I>> serializer,
@Nullable Function<List<I>, Try<Void>> validator,
@Nullable BiConsumer<List<I>, List<I>> onSetValue,
@NotNull Class<I> itemType,
@Nullable NodeSuggester itemSuggester,
@Nullable NodeStringParser<I> itemStringParser,
@Nullable NodeSerializer<I> itemSerializer,
@Nullable Function<I, Try<Void>> itemValidator,
@Nullable BiConsumer<I, I> onSetItemValue) {
super(path, comments, name, type, defaultValueSupplier, suggester, stringParser, serializer,
validator, onSetValue);
this.itemType = itemType;
this.itemSuggester = itemSuggester != null
? itemSuggester
: DefaultSuggesterProvider.getDefaultSuggester(itemType);
this.itemStringParser = itemStringParser != null
? itemStringParser
: DefaultStringParserProvider.getDefaultStringParser(itemType);
this.itemSerializer = itemSerializer;
this.itemValidator = itemValidator;
this.onSetItemValue = onSetItemValue;
setDefaults();
}
private void setDefaults() {
if (this.itemSuggester != null && this.suggester == null) {
setDefaultSuggester();
}
if (this.itemStringParser != null && this.stringParser == null) {
setDefaultStringParser();
}
if (this.itemValidator != null && this.validator == null) {
setDefaultValidator();
}
if (this.itemSerializer != null && this.serializer == null) {
setDefaultSerialiser();
}
if (this.onSetItemValue != null && this.onSetValue == null) {
setDefaultOnSetValue();
}
}
private void setDefaultSuggester() {
this.suggester = input -> {
int lastIndexOf = input == null ? -1 : input.lastIndexOf(',');
if (lastIndexOf == -1) {
return itemSuggester.suggest(input);
}
String lastInput = input.substring(lastIndexOf + 1);
String inputBeforeLast = input.substring(0, lastIndexOf + 1);
Set<String> inputs = Set.of(inputBeforeLast.split(","));
return itemSuggester.suggest(lastInput).stream()
.filter(item -> !inputs.contains(item))
.map(item -> inputBeforeLast + item)
.toList();
};
}
private void setDefaultStringParser() {
this.stringParser = (input, type) -> {
if (input == null) {
return Try.failure(new IllegalArgumentException("Input cannot be null"));
}
return Try.sequence(Arrays.stream(input.split(","))
.map(inputItem -> itemStringParser.parse(inputItem, itemType))
.toList()).map(Value::toJavaList);
};
}
private void setDefaultValidator() {
this.validator = value -> {
if (value != null) {
return Try.sequence(value.stream().map(itemValidator).toList()).map(v -> null);
}
return Try.success(null);
};
}
private void setDefaultSerialiser() {
this.serializer = new NodeSerializer<>() {
@Override
public List<I> deserialize(Object object, Class<List<I>> type) {
if (object instanceof List list) {
return list.stream().map(item -> itemSerializer.deserialize(item, itemType)).toList();
}
return new ArrayList<>();
}
@Override
public Object serialize(List<I> object, Class<List<I>> type) {
if (object != null) {
return object.stream().map(item -> itemSerializer.serialize(item, itemType)).toList();
}
return new ArrayList<>();
}
};
}
private void setDefaultOnSetValue() {
this.onSetValue = (oldValue, newValue) -> {
if (oldValue != null) {
oldValue.stream()
.filter(value -> !newValue.contains(value))
.forEach(item -> onSetItemValue.accept(item, null));
}
newValue.forEach(item -> onSetItemValue.accept(null, item));
};
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Class<I> getItemType() {
return itemType;
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Collection<String> suggestItem(@Nullable String input) {
if (itemSuggester != null) {
return itemSuggester.suggest(input);
}
return Collections.emptyList();
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Try<I> parseItemFromString(@Nullable String input) {
if (itemStringParser != null) {
return itemStringParser.parse(input, itemType);
}
return Try.failure(new UnsupportedOperationException("No item string parser for type " + itemType));
}
/**
* {@inheritDoc}
*/
@Override
public @Nullable NodeSerializer<I> getItemSerializer() {
return itemSerializer;
}
/**
* {@inheritDoc}
*/
@Override
public Try<Void> validateItem(@Nullable I value) {
if (itemValidator != null) {
return itemValidator.apply(value);
}
return Try.success(null);
}
/**
* {@inheritDoc}
*/
@Override
public void onSetItemValue(@Nullable I oldValue, @Nullable I newValue) {
if (onSetItemValue != null) {
onSetItemValue.accept(oldValue, newValue);
}
}
public static class Builder<I, B extends ListConfigNode.Builder<I, B>> extends ConfigNode.Builder<List<I>, B> {
protected final @NotNull Class<I> itemType;
protected @Nullable NodeSuggester itemSuggester;
protected @Nullable NodeStringParser<I> itemStringParser;
protected @Nullable NodeSerializer<I> itemSerializer;
protected @Nullable Function<I, Try<Void>> itemValidator;
protected @Nullable BiConsumer<I, I> onSetItemValue;
/**
* Creates a new builder.
*
* @param path The path of the node.
* @param itemType The type of the item value in list.
*/
protected Builder(@NotNull String path, @NotNull Class<I> itemType) {
//noinspection unchecked
super(path, (Class<List<I>>) (Object) List.class);
this.itemType = itemType;
this.defaultValue = () -> (List<I>) new ArrayList<>();
}
/**
* Sets the suggester for an individual item in the list.
*
* @param itemSuggester The suggester.
* @return This builder.
*/
public @NotNull B itemSuggester(@NotNull NodeSuggester itemSuggester) {
this.itemSuggester = itemSuggester;
return self();
}
/**
* Sets the string parser for an individual item in the list.
*
* @param itemStringParser The string parser.
* @return This builder.
*/
public @NotNull B itemStringParser(@NotNull NodeStringParser<I> itemStringParser) {
this.itemStringParser = itemStringParser;
return self();
}
/**
* Sets the serializer for an individual item in the list.
*
* @param serializer The serializer.
* @return This builder.
*/
public @NotNull B itemSerializer(@NotNull NodeSerializer<I> serializer) {
this.itemSerializer = serializer;
return self();
}
/**
* Sets the validator for an individual item in the list.
*
* @param itemValidator The validator.
* @return This builder.
*/
public @NotNull B itemValidator(@NotNull Function<I, Try<Void>> itemValidator) {
this.itemValidator = itemValidator;
return self();
}
/**
* Sets the onSetValue for an individual item in the list.
*
* @param onSetItemValue The onSetValue.
* @return This builder.
*/
public @NotNull B onSetItemValue(@Nullable BiConsumer<I, I> onSetItemValue) {
this.onSetItemValue = onSetItemValue;
return self();
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull ListConfigNode<I> build() {
return new ListConfigNode<>(
path,
comments.toArray(new String[0]),
name,
type,
defaultValue,
suggester,
stringParser,
serializer,
validator,
onSetValue,
itemType,
itemSuggester, itemStringParser, itemSerializer,
itemValidator,
onSetItemValue);
}
}
}
@@ -0,0 +1,64 @@
package org.mvplugins.multiverse.core.configuration.node;
import java.util.Collection;
import java.util.List;
import io.vavr.control.Try;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.functions.NodeSerializer;
/**
* A node that holds a list of values of a specific type.
*
* @param <I> The type of list item.
*/
public interface ListValueNode<I> extends ValueNode<List<I>> {
/**
* Gets the class type of list item.
*
* @return The class type of list item.
*/
@NotNull Class<I> getItemType();
/**
* Suggests possible string values for this node.
*
* @param input The input string.
* @return A collection of possible string values.
*/
@NotNull Collection<String> suggestItem(@Nullable String input);
/**
* Parses the given string into a value of type {@link I}. Used for property set by user input.
*
* @param input The string to parse.
* @return The parsed value, or given exception if parsing failed.
*/
@NotNull Try<I> parseItemFromString(@Nullable String input);
/**
* Gets the serializer for this node.
*
* @return The serializer for this node.
*/
@Nullable NodeSerializer<I> getItemSerializer();
/**
* Validates the value of this node.
*
* @param value The value to validate.
* @return True if the value is valid, false otherwise.
*/
Try<Void> validateItem(@Nullable I value);
/**
* Called when the value of this node is set.
*
* @param oldValue The old value.
* @param newValue The new value.
*/
void onSetItemValue(@Nullable I oldValue, @Nullable I newValue);
}
@@ -9,6 +9,7 @@ import java.util.Map;
import io.github.townyadvanced.commentedconfiguration.setting.CommentedNode;
import io.vavr.control.Option;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A collection of {@link CommentedNode}s, with mappings to nodes by name.
@@ -17,24 +18,32 @@ public class NodeGroup implements Collection<Node> {
private final Collection<Node> nodes;
private final Map<String, Node> nodesMap;
/**
* Creates a new empty node group.
*/
public NodeGroup() {
this.nodes = new ArrayList<>();
this.nodesMap = new HashMap<>();
}
public NodeGroup(Collection<Node> nodes) {
/**
* Creates a new node group with the given nodes.
*
* @param nodes The nodes to make up the group.
*/
public NodeGroup(@NotNull Collection<Node> nodes) {
this.nodes = nodes;
this.nodesMap = new HashMap<>(nodes.size());
nodes.forEach(this::addNodeIndex);
}
private void addNodeIndex(Node node) {
private void addNodeIndex(@NotNull Node node) {
if (node instanceof ValueNode) {
((ValueNode<?>) node).getName().peek(name -> nodesMap.put(name, node));
}
}
private void removeNodeIndex(Node node) {
private void removeNodeIndex(@NotNull Node node) {
if (node instanceof ValueNode) {
((ValueNode<?>) node).getName().peek(nodesMap::remove);
}
@@ -45,32 +54,29 @@ public class NodeGroup implements Collection<Node> {
*
* @return The names of all nodes in this group.
*/
public Collection<String> getNames() {
public @NotNull Collection<String> getNames() {
return nodesMap.keySet();
}
public Map<String, Node> getNodesMap() {
return nodesMap;
}
/**
* Gets the node with the given name.
*
* @param name The name of the node to get.
* @return The node with the given name, or {@link Option.None} if no node with the given name exists.
*/
public Option<Node> findNode(String name) {
public @NotNull Option<Node> findNode(@Nullable String name) {
return Option.of(nodesMap.get(name));
}
/**
* Gets the node with the given name.
*
* @param name The name of the node to get.
* @param type The type of the node to get.
* @param name The name of node to get.
* @param type The type of node to get.
* @param <T> The type of node.
* @return The node with the given name, or {@link Option.None} if no node with the given name exists.
*/
public <T extends Node> Option<T> findNode(String name, Class<T> type) {
public <T extends Node> @NotNull Option<T> findNode(@Nullable String name, @NotNull Class<T> type) {
return Option.of(nodesMap.get(name)).map(node -> type.isAssignableFrom(node.getClass()) ? (T) node : null);
}
@@ -1,6 +0,0 @@
package org.mvplugins.multiverse.core.configuration.node;
public interface NodeSerializer<T> {
T deserialize(Object object, Class<T> type);
Object serialize(T object, Class<T> type);
}
@@ -1,10 +1,14 @@
package org.mvplugins.multiverse.core.configuration.node;
import java.util.Collection;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.functions.NodeSerializer;
public interface ValueNode<T> extends Node {
/**
@@ -28,6 +32,22 @@ public interface ValueNode<T> extends Node {
*/
@Nullable T getDefaultValue();
/**
* Suggests possible string values for this node. Generated based on the current user input.
*
* @param input The current partial user input
* @return A collection of possible string values.
*/
@NotNull Collection<String> suggest(@Nullable String input);
/**
* Parses the given string into a value of type {@link T}. Used for property set by user input.
*
* @param input The string to parse.
* @return The parsed value, or given exception if parsing failed.
*/
@NotNull Try<T> parseFromString(@Nullable String input);
/**
* Gets the serializer for this node.
*
@@ -39,7 +59,8 @@ public interface ValueNode<T> extends Node {
* Validates the value of this node.
*
* @param value The value to validate.
* @return True if the value is valid, false otherwise.
* @return An empty {@link Try} if the value is valid, or a {@link Try} containing an exception if the value is
* invalid.
*/
Try<Void> validate(@Nullable T value);
@@ -3,14 +3,10 @@ package org.mvplugins.multiverse.core.destination;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import co.aikar.commands.BukkitCommandIssuer;
import co.aikar.commands.CommandIssuer;
import jakarta.inject.Inject;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.permissions.Permission;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.NotNull;
@@ -19,9 +15,6 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.api.Destination;
import org.mvplugins.multiverse.core.api.DestinationInstance;
import org.mvplugins.multiverse.core.api.SafeTTeleporter;
import org.mvplugins.multiverse.core.api.Teleporter;
import org.mvplugins.multiverse.core.teleportation.TeleportResult;
/**
* Provides destinations for teleportation.
@@ -32,13 +25,11 @@ public class DestinationsProvider {
private static final String PERMISSION_PREFIX = "multiverse.teleport.";
private final PluginManager pluginManager;
private final SafeTTeleporter safetyTeleporter;
private final Map<String, Destination<?>> destinationMap;
@Inject
DestinationsProvider(@NotNull PluginManager pluginManager, @NotNull SafeTTeleporter safetyTeleporter) {
DestinationsProvider(@NotNull PluginManager pluginManager) {
this.pluginManager = pluginManager;
this.safetyTeleporter = safetyTeleporter;
this.destinationMap = new HashMap<>();
}
@@ -123,93 +114,11 @@ public class DestinationsProvider {
}
/**
* Teleports the teleportee to the destination.
* Gets all registered destinations.
*
* @param teleporter The teleporter.
* @param teleportee The teleportee.
* @param destination The destination.
* @return The async teleport result.
* @return A collection of destinations.
*/
public CompletableFuture<TeleportResult> playerTeleportAsync(
@NotNull BukkitCommandIssuer teleporter,
@NotNull Player teleportee,
@NotNull ParsedDestination<?> destination) {
if (!checkTeleportPermissions(teleporter, teleportee, destination)) {
return CompletableFuture.completedFuture(TeleportResult.FAIL_PERMISSION);
}
return teleportAsync(teleporter, teleportee, destination);
}
/**
* Teleports the teleportee to the destination.
*
* @param teleporter The teleporter.
* @param teleportee The teleportee.
* @param destination The destination.
* @return The async teleport result.
*/
public CompletableFuture<TeleportResult> teleportAsync(
@NotNull BukkitCommandIssuer teleporter,
@NotNull Entity teleportee,
@NotNull ParsedDestination<?> destination) {
Teleporter teleportHandler = destination.getDestination().getTeleporter();
if (teleportHandler == null) {
teleportHandler = safetyTeleporter;
}
return teleportHandler.teleportAsync(teleporter, teleportee, destination);
}
/**
* Checks if the teleporter has permission to teleport the teleportee to the destination.
*
* @param teleporter The teleporter.
* @param teleportee The teleportee.
* @param destination The destination.
* @return True if the teleporter has permission, false otherwise.
*/
public boolean checkTeleportPermissions(
CommandIssuer teleporter, Entity teleportee, ParsedDestination<?> destination) {
// TODO: Move permission checking to a separate class
String permission = PERMISSION_PREFIX
+ (teleportee.equals(teleporter.getIssuer()) ? "self" : "other") + "."
+ destination.getDestination().getIdentifier();
if (!teleporter.hasPermission(permission)) {
teleporter.sendMessage("You don't have permission to teleport to this destination.");
return false;
}
// TODO: Config whether to use finer permission
String finerPermissionSuffix = destination.getDestinationInstance().getFinerPermissionSuffix();
if (finerPermissionSuffix == null || finerPermissionSuffix.isEmpty()) {
return true;
}
String finerPermission = permission + "." + finerPermissionSuffix;
if (!teleporter.hasPermission(finerPermission)) {
teleporter.sendMessage("You don't have permission to teleport to this destination.");
return false;
}
return true;
}
/**
* Checks if the issuer has permission to teleport to at least one destination.
*
* @param issuer The issuer.
* @return True if the issuer has permission, false otherwise.
*/
public boolean hasAnyTeleportPermission(CommandIssuer issuer) {
for (Destination<?> destination : this.destinationMap.values()) {
String permission = PERMISSION_PREFIX + "self." + destination.getIdentifier();
if (issuer.hasPermission(permission)) {
return true;
}
permission = PERMISSION_PREFIX + "other." + destination.getIdentifier();
if (issuer.hasPermission(permission)) {
return true;
}
}
return false;
public @NotNull Collection<Destination<?>> getDestinations() {
return this.destinationMap.values();
}
}
@@ -11,7 +11,6 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
import org.mvplugins.multiverse.core.api.Destination;
import org.mvplugins.multiverse.core.api.Teleporter;
/**
* {@link Destination} implementation for anchors.
@@ -61,12 +60,4 @@ public class AnchorDestination implements Destination<AnchorDestinationInstance>
public boolean checkTeleportSafety() {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public @Nullable Teleporter getTeleporter() {
return null;
}
}
@@ -12,7 +12,6 @@ import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.api.Destination;
import org.mvplugins.multiverse.core.api.Teleporter;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
/**
@@ -62,12 +61,4 @@ public class BedDestination implements Destination<BedDestinationInstance> {
public boolean checkTeleportSafety() {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public @Nullable Teleporter getTeleporter() {
return null;
}
}
@@ -12,7 +12,6 @@ import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.api.Destination;
import org.mvplugins.multiverse.core.api.Teleporter;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -97,12 +96,4 @@ public class CannonDestination implements Destination<CannonDestinationInstance>
public boolean checkTeleportSafety() {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public @Nullable Teleporter getTeleporter() {
return null;
}
}
@@ -12,7 +12,6 @@ import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.api.Destination;
import org.mvplugins.multiverse.core.api.Teleporter;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -100,12 +99,4 @@ public class ExactDestination implements Destination<ExactDestinationInstance> {
public boolean checkTeleportSafety() {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public @Nullable Teleporter getTeleporter() {
return null;
}
}
@@ -11,7 +11,6 @@ import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.api.Destination;
import org.mvplugins.multiverse.core.api.Teleporter;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
/**
@@ -60,12 +59,4 @@ public class PlayerDestination implements Destination<PlayerDestinationInstance>
public boolean checkTeleportSafety() {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public @Nullable Teleporter getTeleporter() {
return null;
}
}
@@ -11,7 +11,6 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.api.Destination;
import org.mvplugins.multiverse.core.api.LocationManipulation;
import org.mvplugins.multiverse.core.api.Teleporter;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -76,12 +75,4 @@ public class WorldDestination implements Destination<WorldDestinationInstance> {
public boolean checkTeleportSafety() {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public @Nullable Teleporter getTeleporter() {
return null;
}
}
@@ -2,9 +2,9 @@ package org.mvplugins.multiverse.core.display.parsers;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;
import co.aikar.commands.BukkitCommandIssuer;
import com.google.common.base.Strings;
import org.bukkit.ChatColor;
import org.jetbrains.annotations.NotNull;
@@ -45,8 +45,15 @@ public class MapContentProvider<K, V> implements ContentProvider {
@Override
public Collection<String> parse(@NotNull BukkitCommandIssuer issuer) {
return map.entrySet().stream()
.map(e -> String.format(format, keyColor, e.getKey(), separator, valueColor, e.getValue()))
.collect(Collectors.toList());
.map(e -> String.format(format, keyColor, e.getKey(), separator, valueColor, formatValue(e.getValue())))
.toList();
}
private String formatValue(V value) {
if (value instanceof String stringValue) {
return Strings.isNullOrEmpty(stringValue) ? "&7&onull" : stringValue;
}
return value == null ? "&7&onull" : String.valueOf(value);
}
/**
@@ -14,8 +14,8 @@ import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.mvplugins.multiverse.core.api.SafeTTeleporter;
import org.mvplugins.multiverse.core.destination.ParsedDestination;
import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter;
/**
* Event that gets called when a player use the /mvtp command.
@@ -89,8 +89,8 @@ public class MVTeleportEvent extends Event implements Cancellable {
}
/**
* Looks if this {@link MVTeleportEvent} is using the {@link SafeTTeleporter}.
* @return True if this {@link MVTeleportEvent} is using the {@link SafeTTeleporter}.
* Looks if this {@link MVTeleportEvent} is using the {@link AsyncSafetyTeleporter}.
* @return True if this {@link MVTeleportEvent} is using the {@link AsyncSafetyTeleporter}.
*/
public boolean isUsingSafeTTeleporter() {
return useSafeTeleport;
@@ -31,10 +31,11 @@ public class MVChatListener implements InjectableListener {
/**
* This handles a {@link AsyncPlayerChatEvent}.
*
* @param event The {@link AsyncPlayerChatEvent}.
*/
@EventHandler
public void playerChat(AsyncPlayerChatEvent event) {
public void asyncPlayerChat(AsyncPlayerChatEvent event) {
if (event.isCancelled()) {
return;
}
@@ -42,6 +42,7 @@ public class MVEntityListener implements InjectableListener {
/**
* This method is called when an entity's food level goes higher or lower.
*
* @param event The Event that was fired.
*/
@EventHandler
@@ -52,6 +53,7 @@ public class MVEntityListener implements InjectableListener {
if (!(event.getEntity() instanceof Player player)) {
return;
}
worldManager.getLoadedWorld(player.getWorld())
.peek(world -> {
if (!world.getHunger() && event.getFoodLevel() < player.getFoodLevel()) {
@@ -62,6 +64,7 @@ public class MVEntityListener implements InjectableListener {
/**
* This method is called when an entity's health goes up or down.
*
* @param event The Event that was fired.
*/
@EventHandler
@@ -69,6 +72,7 @@ public class MVEntityListener implements InjectableListener {
if (event.isCancelled() || event.getRegainReason() != RegainReason.REGEN) {
return;
}
worldManager.getLoadedWorld(event.getEntity().getWorld())
.peek(world -> {
if (!world.getAutoHeal()) {
@@ -79,6 +83,7 @@ public class MVEntityListener implements InjectableListener {
/**
* Handle Animal/Monster Spawn settings, seems like a more concrete method than using CraftBukkit.
*
* @param event The event.
*/
@EventHandler
@@ -31,8 +31,9 @@ import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.api.SafeTTeleporter;
import org.mvplugins.multiverse.core.api.BlockSafety;
import org.mvplugins.multiverse.core.commandtools.MVCommandManager;
import org.mvplugins.multiverse.core.config.MVCoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
@@ -40,6 +41,7 @@ import org.mvplugins.multiverse.core.destination.ParsedDestination;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.event.MVRespawnEvent;
import org.mvplugins.multiverse.core.inject.InjectableListener;
import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter;
import org.mvplugins.multiverse.core.teleportation.TeleportQueue;
import org.mvplugins.multiverse.core.utils.result.ResultChain;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
@@ -56,7 +58,8 @@ public class MVPlayerListener implements InjectableListener {
private final Plugin plugin;
private final MVCoreConfig config;
private final Provider<WorldManager> worldManagerProvider;
private final SafeTTeleporter safetyTeleporter;
private final BlockSafety blockSafety;
private final AsyncSafetyTeleporter safetyTeleporter;
private final Server server;
private final TeleportQueue teleportQueue;
private final MVEconomist economist;
@@ -72,7 +75,8 @@ public class MVPlayerListener implements InjectableListener {
MultiverseCore plugin,
MVCoreConfig config,
Provider<WorldManager> worldManagerProvider,
SafeTTeleporter safetyTeleporter,
BlockSafety blockSafety,
AsyncSafetyTeleporter safetyTeleporter,
Server server,
TeleportQueue teleportQueue,
MVEconomist economist,
@@ -83,6 +87,7 @@ public class MVPlayerListener implements InjectableListener {
this.plugin = plugin;
this.config = config;
this.worldManagerProvider = worldManagerProvider;
this.blockSafety = blockSafety;
this.safetyTeleporter = safetyTeleporter;
this.server = server;
this.teleportQueue = teleportQueue;
@@ -211,7 +216,7 @@ public class MVPlayerListener implements InjectableListener {
}
// Finally, teleport the player
safetyTeleporter.teleportAsync(getCommandManager().getCommandIssuer(player), player, joinDestination);
safetyTeleporter.teleportSafely(player, player, joinDestination);
}
/**
@@ -300,7 +305,7 @@ public class MVPlayerListener implements InjectableListener {
// REMEMBER! getTo MAY be NULL HERE!!!
// If the player was actually outside of the portal, adjust the from location
if (event.getFrom().getWorld().getBlockAt(event.getFrom()).getType() != Material.NETHER_PORTAL) {
Location newloc = this.safetyTeleporter.findPortalBlockNextTo(event.getFrom());
Location newloc = blockSafety.findPortalBlockNextTo(event.getFrom());
// TODO: Fix this. Currently, we only check for PORTAL blocks. I'll have to figure out what
// TODO: we want to do here.
if (newloc != null) {
@@ -352,7 +357,7 @@ public class MVPlayerListener implements InjectableListener {
new Runnable() {
@Override
public void run() {
safetyTeleporter.safelyTeleportAsync(getCommandManager().getCommandIssuer(player), player, parsedDestination);
safetyTeleporter.teleportSafely(player, player, parsedDestination);
}
}, 1L);
}
@@ -22,9 +22,10 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.MVCoreConfig;
import org.mvplugins.multiverse.core.inject.InjectableListener;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import static org.bukkit.PortalType.CUSTOM;
/**
* A custom listener for portal related events.
*/
@@ -42,49 +43,51 @@ public class MVPortalListener implements InjectableListener {
/**
* This is called when a portal is formed.
*
* @param event The event where a portal was created or formed due to a world link
*/
@EventHandler(ignoreCancelled = true)
public void portalForm(PortalCreateEvent event) {
Logging.fine("Attempting to create portal at '%s' with reason: %s", event.getWorld().getName(), event.getReason());
public void portalCreate(PortalCreateEvent event) {
Logging.fine("Attempting to create portal at '%s' with reason: %s",
event.getWorld().getName(), event.getReason());
LoadedMultiverseWorld world = this.worldManager.getLoadedWorld(event.getWorld()).getOrNull();
if (world == null) {
Logging.fine("World '%s' is not managed by Multiverse! Ignoring at PortalCreateEvent.", event.getWorld().getName());
return;
}
this.worldManager.getLoadedWorld(event.getWorld()).peek(world -> {
PortalType targetType = getPortalType(event);
if (targetType == PortalType.CUSTOM) {
return;
}
if (!world.getPortalForm().isPortalAllowed(targetType)) {
Logging.fine("Cancelling creation of %s portal because portalForm disallows.", targetType);
event.setCancelled(true);
}
}).onEmpty(() -> {
Logging.fine("World '%s' is not managed by Multiverse! Ignoring at PortalCreateEvent.",
event.getWorld().getName());
});
}
PortalType targetType;
switch (event.getReason()) {
case FIRE:
private PortalType getPortalType(PortalCreateEvent event) {
return switch (event.getReason()) {
case FIRE -> {
// Ensure portal by flint and steel actually creates nether
boolean isNether = false;
for (BlockState block : event.getBlocks()) {
if (block.getType() == Material.NETHER_PORTAL) {
isNether = true;
break;
yield PortalType.NETHER;
}
}
if (!isNether) {
return;
}
targetType = PortalType.NETHER;
break;
case NETHER_PAIR:
targetType = PortalType.NETHER;
break;
case END_PLATFORM:
targetType = PortalType.ENDER;
break;
default:
yield CUSTOM;
}
case NETHER_PAIR -> {
yield PortalType.NETHER;
}
case END_PLATFORM -> {
yield PortalType.ENDER;
}
default -> {
Logging.fine("Portal created is not NETHER or ENDER type. Ignoring...");
return;
}
if (!world.getPortalForm().isPortalAllowed(targetType)) {
Logging.fine("Cancelling creation of %s portal because portalForm disallows.", targetType);
event.setCancelled(true);
}
yield CUSTOM;
}
};
}
/**
@@ -93,28 +96,28 @@ public class MVPortalListener implements InjectableListener {
* @param event The player interact event.
*/
@EventHandler(ignoreCancelled = true)
public void portalForm(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) {
return;
}
if (event.getClickedBlock() == null || event.getClickedBlock().getType() != Material.END_PORTAL_FRAME) {
return;
}
if (event.getItem() == null || event.getItem().getType() != Material.ENDER_EYE) {
public void playerInteract(PlayerInteractEvent event) {
if (isCreateEndPortalInteraction(event)) {
return;
}
LoadedMultiverseWorld world = this.worldManager.getLoadedWorld(event.getPlayer().getWorld()).getOrNull();
if (world == null) {
this.worldManager.getLoadedWorld(event.getPlayer().getWorld()).peek(world -> {
if (!world.getPortalForm().isPortalAllowed(PortalType.ENDER)) {
Logging.fine("Cancelling creation of ENDER portal because portalForm disallows.");
event.setCancelled(true);
}
}).onEmpty(() -> {
Logging.fine("World '%s' is not managed by Multiverse! Ignoring at PlayerInteractEvent.",
event.getPlayer().getWorld().getName());
return;
}
});
}
if (!world.getPortalForm().isPortalAllowed(PortalType.ENDER)) {
Logging.fine("Cancelling creation of ENDER portal because portalForm disallows.");
event.setCancelled(true);
}
private boolean isCreateEndPortalInteraction(PlayerInteractEvent event) {
return event.getAction() != Action.RIGHT_CLICK_BLOCK
|| event.getClickedBlock() == null
|| event.getClickedBlock().getType() != Material.END_PORTAL_FRAME
|| event.getItem() == null
|| event.getItem().getType() != Material.ENDER_EYE;
}
/**
@@ -127,7 +130,7 @@ public class MVPortalListener implements InjectableListener {
if (event.isCancelled() || event.getTo() == null) {
return;
}
if (!config.isUsingCustomPortalSearch()) {
if (config.isUsingCustomPortalSearch()) {
event.setSearchRadius(config.getCustomPortalSearchRadius());
}
}
@@ -40,7 +40,7 @@ public class MVWorldListener implements InjectableListener {
* @param event The Event that was fired.
*/
@EventHandler(priority = EventPriority.MONITOR)
public void unloadWorld(WorldUnloadEvent event) {
public void worldUnload(WorldUnloadEvent event) {
if (event.isCancelled()) {
return;
}
@@ -58,7 +58,7 @@ public class MVWorldListener implements InjectableListener {
* @param event The Event that was fired.
*/
@EventHandler(priority = EventPriority.MONITOR)
public void loadWorld(WorldLoadEvent event) {
public void worldLoad(WorldLoadEvent event) {
worldManager.getUnloadedWorld(event.getWorld().getName())
.peek(world -> {
Logging.fine("Loading world: " + world.getName());
@@ -1,80 +0,0 @@
/******************************************************************************
* Multiverse 2 Copyright (c) the Multiverse Team 2011. *
* Multiverse 2 is licensed under the BSD License. *
* For more information please check the README.md file included *
* with this project. *
******************************************************************************/
package org.mvplugins.multiverse.core.listeners;
import org.bukkit.event.EventHandler;
import org.mvplugins.multiverse.core.event.MVConfigReloadEvent;
import org.mvplugins.multiverse.core.event.MVPlayerTouchedPortalEvent;
import org.mvplugins.multiverse.core.event.MVRespawnEvent;
import org.mvplugins.multiverse.core.event.MVTeleportEvent;
import org.mvplugins.multiverse.core.event.MVVersionEvent;
import org.mvplugins.multiverse.core.event.MVWorldDeleteEvent;
import org.mvplugins.multiverse.core.event.MVWorldPropertyChangeEvent;
import org.mvplugins.multiverse.core.inject.InjectableListener;
/**
* Subclasses of this listener can be used to conveniently listen to MultiverseCore-events.
*/
public abstract class MultiverseCoreListener implements InjectableListener {
/**
* Called when a {@link MVWorldPropertyChangeEvent} is fired.
* @param event The event.
*/
@EventHandler
public void worldPropertyChange(MVWorldPropertyChangeEvent event) {
}
/**
* Called when a {@link MVWorldDeleteEvent} is fired.
* @param event The event.
*/
@EventHandler
public void worldDelete(MVWorldDeleteEvent event) {
}
/**
* Called when a {@link MVVersionEvent} is fired.
* @param event The event.
*/
@EventHandler
public void versionRequest(MVVersionEvent event) {
}
/**
* Called when a {@link MVTeleportEvent} is fired.
* @param event The event.
*/
@EventHandler
public void playerTeleport(MVTeleportEvent event) {
}
/**
* Called when a {@link MVRespawnEvent} is fired.
* @param event The event.
*/
@EventHandler
public void playerRespawn(MVRespawnEvent event) {
}
/**
* Called when a {@link MVPlayerTouchedPortalEvent} is fired.
* @param event The event.
*/
@EventHandler
public void playerTouchedPortal(MVPlayerTouchedPortalEvent event) {
}
/**
* Called when a {@link MVConfigReloadEvent} is fired.
* @param event The event.
*/
@EventHandler
public void configReload(MVConfigReloadEvent event) {
}
}
@@ -1,8 +1,32 @@
package org.mvplugins.multiverse.core.permissions;
public class CorePermissions {
public static String WORLD_ACCESS = "multiverse.access";
public static String WORLD_EXEMPT = "multiverse.exempt";
public static String GAMEMODE_BYPASS = "mv.bypass.gamemode";
public static String PLAYERLIMIT_BYPASS = "mv.bypass.playerlimit";
final class CorePermissions {
/**
* Permission to access a world.
*/
static final String WORLD_ACCESS = "multiverse.access";
/**
* Permission to bypass the entry fee of a world.
*/
static final String WORLD_EXEMPT = "multiverse.exempt";
/**
* Permission to bypass the gamemode of a world.
*/
static final String GAMEMODE_BYPASS = "mv.bypass.gamemode";
/**
* Permission to bypass the player limit of a world.
*/
static final String PLAYERLIMIT_BYPASS = "mv.bypass.playerlimit";
/**
* Permission to teleport to a destination.
*/
static final String TELEPORT = "multiverse.teleport";
private CorePermissions() {
// Prevent instantiation as this is a static utility class
}
}
@@ -1,15 +1,28 @@
package org.mvplugins.multiverse.core.permissions;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.api.Destination;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.ParsedDestination;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
@Service
public class CorePermissionsChecker {
private DestinationsProvider destinationsProvider;
@Inject
CorePermissionsChecker(@NotNull DestinationsProvider destinationsProvider) {
this.destinationsProvider = destinationsProvider;
}
public boolean hasWorldAccessPermission(@NotNull CommandSender sender, @NotNull MultiverseWorld world) {
return hasPermission(sender, concatPermission(CorePermissions.WORLD_ACCESS, world.getName()));
}
@@ -26,7 +39,58 @@ public class CorePermissionsChecker {
return hasPermission(sender, concatPermission(CorePermissions.GAMEMODE_BYPASS, world.getName()));
}
private String concatPermission(String permission, String...child) {
/**
* Checks if the teleporter has permission to teleport the teleportee to the destination.
*
* @param teleporter The teleporter.
* @param teleportee The teleportee.
* @param destination The destination.
* @return True if the teleporter has permission, false otherwise.
*/
public boolean checkTeleportPermissions(
@NotNull CommandSender teleporter,
@NotNull Entity teleportee,
@NotNull ParsedDestination<?> destination) {
String permission = concatPermission(
CorePermissions.TELEPORT,
teleportee.equals(teleporter) ? "self" : "other",
destination.getDestination().getIdentifier());
if (!hasPermission(teleporter, permission)) {
return false;
}
// TODO: Config whether to use finer permission
String finerPermissionSuffix = destination.getDestinationInstance().getFinerPermissionSuffix();
if (finerPermissionSuffix == null || finerPermissionSuffix.isEmpty()) {
return true;
}
String finerPermission = concatPermission(permission, finerPermissionSuffix);
return hasPermission(teleporter, finerPermission);
}
/**
* Checks if the issuer has permission to teleport to at least one destination.
*
* @param sender The sender to check.
* @return True if the issuer has permission, false otherwise.
*/
public boolean hasAnyTeleportPermission(CommandSender sender) {
for (Destination<?> destination : destinationsProvider.getDestinations()) {
String permission = concatPermission(CorePermissions.TELEPORT, "self", destination.getIdentifier());
if (hasPermission(sender, permission)) {
return true;
}
permission = concatPermission(CorePermissions.TELEPORT, "other", destination.getIdentifier());
if (hasPermission(sender, permission)) {
return true;
}
}
return false;
}
private String concatPermission(String permission, String... child) {
return permission + "." + String.join(".", child);
}
@@ -0,0 +1,158 @@
package org.mvplugins.multiverse.core.teleportation;
import java.util.List;
import com.dumptruckman.minecraft.util.Logging;
import io.papermc.lib.PaperLib;
import jakarta.inject.Inject;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.api.BlockSafety;
import org.mvplugins.multiverse.core.destination.ParsedDestination;
import org.mvplugins.multiverse.core.utils.result.Async;
import org.mvplugins.multiverse.core.utils.result.AsyncAttempt;
import org.mvplugins.multiverse.core.utils.result.Attempt;
/**
* Teleports entities safely and asynchronously.
*/
@Service
public class AsyncSafetyTeleporter {
private final BlockSafety blockSafety;
private final TeleportQueue teleportQueue;
@Inject
AsyncSafetyTeleporter(
BlockSafety blockSafety,
TeleportQueue teleportQueue) {
this.blockSafety = blockSafety;
this.teleportQueue = teleportQueue;
}
public AsyncAttempt<Void, TeleportResult.Failure> teleportSafely(
@NotNull Entity teleportee,
@Nullable ParsedDestination<?> destination) {
return teleportSafely(null, teleportee, destination);
}
public <T extends Entity> Async<List<Attempt<Void, TeleportResult.Failure>>> teleportSafely(
@Nullable CommandSender teleporter,
@NotNull List<T> teleportees,
@Nullable ParsedDestination<?> destination) {
return AsyncAttempt.allOf(teleportees.stream()
.map(teleportee -> teleportSafely(teleporter, teleportee, destination))
.toList());
}
public AsyncAttempt<Void, TeleportResult.Failure> teleportSafely(
@Nullable CommandSender teleporter,
@NotNull Entity teleportee,
@Nullable ParsedDestination<?> destination) {
if (destination == null) {
return AsyncAttempt.failure(TeleportResult.Failure.NULL_DESTINATION);
}
return destination.getDestination().checkTeleportSafety()
? teleportSafely(teleporter, teleportee, destination.getLocation(teleportee))
: teleport(teleporter, teleportee, destination.getLocation(teleportee));
}
public AsyncAttempt<Void, TeleportResult.Failure> teleportSafely(
@NotNull Entity teleportee,
@Nullable Location location) {
return teleportSafely(null, teleportee, location);
}
public AsyncAttempt<Void, TeleportResult.Failure> teleportSafely(
@Nullable CommandSender teleporter,
@NotNull Entity teleportee,
@Nullable Location location) {
if (location == null) {
return AsyncAttempt.failure(TeleportResult.Failure.NULL_LOCATION);
}
Location safeLocation = blockSafety.getSafeLocation(location);
if (safeLocation == null) {
return AsyncAttempt.failure(TeleportResult.Failure.UNSAFE_LOCATION);
}
return teleport(teleporter, teleportee, safeLocation);
}
public <T extends Entity> Async<List<Attempt<Void, TeleportResult.Failure>>> teleport(
@NotNull List<T> teleportees,
@Nullable ParsedDestination<?> destination) {
return AsyncAttempt.allOf(teleportees.stream()
.map(teleportee -> teleport(teleportee, destination))
.toList());
}
public AsyncAttempt<Void, TeleportResult.Failure> teleport(
@NotNull Entity teleportee,
@Nullable ParsedDestination<?> destination) {
return teleport(null, teleportee, destination);
}
public AsyncAttempt<Void, TeleportResult.Failure> teleport(
@Nullable CommandSender teleporter,
@NotNull Entity teleportee,
@Nullable ParsedDestination<?> destination) {
if (destination == null) {
return AsyncAttempt.failure(TeleportResult.Failure.NULL_DESTINATION);
}
return teleport(teleporter, teleportee, destination.getLocation(teleportee));
}
public <T extends Entity> Async<List<Attempt<Void, TeleportResult.Failure>>> teleport(
@NotNull List<T> teleportees,
@Nullable Location location) {
return AsyncAttempt.allOf(teleportees.stream()
.map(teleportee -> teleport(teleportee, location))
.toList());
}
public AsyncAttempt<Void, TeleportResult.Failure> teleport(
@NotNull Entity teleportee,
@Nullable Location location) {
return teleport(null, teleportee, location);
}
public AsyncAttempt<Void, TeleportResult.Failure> teleport(
@Nullable CommandSender teleporter,
@NotNull Entity teleportee,
@Nullable Location location) {
if (location == null) {
return AsyncAttempt.failure(TeleportResult.Failure.NULL_LOCATION);
}
boolean shouldAddToQueue = teleporter != null && teleportee instanceof Player;
if (shouldAddToQueue) {
teleportQueue.addToQueue(teleporter.getName(), teleportee.getName());
}
return doAsyncTeleport(teleportee, location, shouldAddToQueue);
}
private AsyncAttempt<Void, TeleportResult.Failure> doAsyncTeleport(
@NotNull Entity teleportee,
@NotNull Location location,
boolean shouldAddToQueue) {
return AsyncAttempt.of(PaperLib.teleportAsync(teleportee, location), exception -> {
Logging.warning("Failed to teleport %s to %s: %s",
teleportee.getName(), location, exception.getMessage());
return Attempt.failure(TeleportResult.Failure.TELEPORT_FAILED_EXCEPTION);
}).mapAttempt(result -> {
Logging.finer("Teleported async %s to %s", teleportee.getName(), location);
if (result) {
if (shouldAddToQueue) {
teleportQueue.popFromQueue(teleportee.getName());
}
return Attempt.success(null);
}
return Attempt.failure(TeleportResult.Failure.TELEPORT_FAILED);
});
}
}
@@ -8,7 +8,6 @@
package org.mvplugins.multiverse.core.teleportation;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.Set;
import com.dumptruckman.minecraft.util.Logging;
@@ -16,11 +15,14 @@ import jakarta.inject.Inject;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Bed;
import org.bukkit.entity.Minecart;
import org.bukkit.entity.Vehicle;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.api.BlockSafety;
@@ -31,9 +33,13 @@ import org.mvplugins.multiverse.core.api.LocationManipulation;
*/
@Service
public class SimpleBlockSafety implements BlockSafety {
private final LocationManipulation locationManipulation;
private static final Vector DEFAULT_VECTOR = new Vector();
private static final int DEFAULT_TOLERANCE = 6;
private static final int DEFAULT_RADIUS = 9;
private static final Set<BlockFace> AROUND_BLOCK = EnumSet.noneOf(BlockFace.class);
private final LocationManipulation locationManipulation;
static {
AROUND_BLOCK.add(BlockFace.NORTH);
AROUND_BLOCK.add(BlockFace.NORTH_EAST);
@@ -46,7 +52,7 @@ public class SimpleBlockSafety implements BlockSafety {
}
@Inject
public SimpleBlockSafety(LocationManipulation locationManipulation) {
SimpleBlockSafety(LocationManipulation locationManipulation) {
this.locationManipulation = locationManipulation;
}
@@ -113,6 +119,197 @@ public class SimpleBlockSafety implements BlockSafety {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public Location findPortalBlockNextTo(Location location) {
Block b = location.getWorld().getBlockAt(location);
Location foundLocation = null;
if (b.getType() == Material.NETHER_PORTAL) {
return location;
}
if (b.getRelative(BlockFace.NORTH).getType() == Material.NETHER_PORTAL) {
foundLocation = getCloserBlock(location, b.getRelative(BlockFace.NORTH).getLocation(), foundLocation);
}
if (b.getRelative(BlockFace.SOUTH).getType() == Material.NETHER_PORTAL) {
foundLocation = getCloserBlock(location, b.getRelative(BlockFace.SOUTH).getLocation(), foundLocation);
}
if (b.getRelative(BlockFace.EAST).getType() == Material.NETHER_PORTAL) {
foundLocation = getCloserBlock(location, b.getRelative(BlockFace.EAST).getLocation(), foundLocation);
}
if (b.getRelative(BlockFace.WEST).getType() == Material.NETHER_PORTAL) {
foundLocation = getCloserBlock(location, b.getRelative(BlockFace.WEST).getLocation(), foundLocation);
}
return foundLocation;
}
private static Location getCloserBlock(Location source, Location blockA, Location blockB) {
// If B wasn't given, return a.
if (blockB == null) {
return blockA;
}
// Center our calculations
blockA.add(.5, 0, .5);
blockB.add(.5, 0, .5);
// Retrieve the distance to the normalized blocks
double testA = source.distance(blockA);
double testB = source.distance(blockB);
// Compare and return
if (testA <= testB) {
return blockA;
}
return blockB;
}
/**
* {@inheritDoc}
*/
@Override
public @Nullable Location getSafeLocation(Location location) {
return this.getSafeLocation(location, DEFAULT_TOLERANCE, DEFAULT_RADIUS);
}
/**
* {@inheritDoc}
*/
@Override
public @Nullable Location getSafeLocation(Location location, int tolerance, int radius) {
// Check around the player first in a configurable radius:
// TODO: Make this configurable
Location safe = checkAboveAndBelowLocation(location, tolerance, radius);
if (safe != null) {
safe.setX(safe.getBlockX() + .5); // SUPPRESS CHECKSTYLE: MagicNumberCheck
safe.setZ(safe.getBlockZ() + .5); // SUPPRESS CHECKSTYLE: MagicNumberCheck
Logging.fine("Hey! I found one: " + locationManipulation.strCoordsRaw(safe));
} else {
Logging.fine("Uh oh! No safe place found!");
}
return safe;
}
private Location checkAboveAndBelowLocation(Location l, int tolerance, int radius) {
// Tolerance must be an even number:
if (tolerance % 2 != 0) {
tolerance += 1;
}
// We want half of it, so we can go up and down
tolerance /= 2;
Logging.finer("Given Location of: " + locationManipulation.strCoordsRaw(l));
Logging.finer("Checking +-" + tolerance + " with a radius of " + radius);
// For now this will just do a straight up block.
Location locToCheck = l.clone();
// Check the main level
Location safe = this.checkAroundLocation(locToCheck, radius);
if (safe != null) {
return safe;
}
// We've already checked zero right above this.
int currentLevel = 1;
while (currentLevel <= tolerance) {
// Check above
locToCheck = l.clone();
locToCheck.add(0, currentLevel, 0);
safe = this.checkAroundLocation(locToCheck, radius);
if (safe != null) {
return safe;
}
// Check below
locToCheck = l.clone();
locToCheck.subtract(0, currentLevel, 0);
safe = this.checkAroundLocation(locToCheck, radius);
if (safe != null) {
return safe;
}
currentLevel++;
}
return null;
}
/*
* For my crappy algorithm, radius MUST be odd.
*/
private Location checkAroundLocation(Location l, int diameter) {
if (diameter % 2 == 0) {
diameter += 1;
}
Location checkLoc = l.clone();
// Start at 3, the min diameter around a block
int loopcounter = 3;
while (loopcounter <= diameter) {
boolean foundSafeArea = checkAroundSpecificDiameter(checkLoc, loopcounter);
// If a safe area was found:
if (foundSafeArea) {
// Return the checkLoc, it is the safe location.
return checkLoc;
}
// Otherwise, let's reset our location
checkLoc = l.clone();
// And increment the radius
loopcounter += 2;
}
return null;
}
private boolean checkAroundSpecificDiameter(Location checkLoc, int circle) {
// Adjust the circle to get how many blocks to step out.
// A radius of 3 makes the block step 1
// A radius of 5 makes the block step 2
// A radius of 7 makes the block step 3
// ...
int adjustedCircle = ((circle - 1) / 2);
checkLoc.add(adjustedCircle, 0, 0);
if (playerCanSpawnHereSafely(checkLoc)) {
return true;
}
// Now we go to the right that adjustedCircle many
for (int i = 0; i < adjustedCircle; i++) {
checkLoc.add(0, 0, 1);
if (playerCanSpawnHereSafely(checkLoc)) {
return true;
}
}
// Then down adjustedCircle *2
for (int i = 0; i < adjustedCircle * 2; i++) {
checkLoc.add(-1, 0, 0);
if (playerCanSpawnHereSafely(checkLoc)) {
return true;
}
}
// Then left adjustedCircle *2
for (int i = 0; i < adjustedCircle * 2; i++) {
checkLoc.add(0, 0, -1);
if (playerCanSpawnHereSafely(checkLoc)) {
return true;
}
}
// Then up Then left adjustedCircle *2
for (int i = 0; i < adjustedCircle * 2; i++) {
checkLoc.add(1, 0, 0);
if (playerCanSpawnHereSafely(checkLoc)) {
return true;
}
}
// Then finish up by doing adjustedCircle - 1
for (int i = 0; i < adjustedCircle - 1; i++) {
checkLoc.add(0, 0, 1);
if (playerCanSpawnHereSafely(checkLoc)) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*/
@@ -122,7 +319,7 @@ public class SimpleBlockSafety implements BlockSafety {
if (l == null) {
return null;
}
final Location trySpawn = this.getSafeSpawnAroundABlock(l);
final Location trySpawn = this.getSafeSpawnAroundBlock(l);
if (trySpawn != null) {
return trySpawn;
}
@@ -131,18 +328,17 @@ public class SimpleBlockSafety implements BlockSafety {
return null;
}
// Now we have 2 locations, check around each, if the type is bed, skip it.
return this.getSafeSpawnAroundABlock(otherBlock);
return this.getSafeSpawnAroundBlock(otherBlock);
}
/**
* Find a safe spawn around a location. (N,S,E,W,NE,NW,SE,SW)
*
* @param l Location to check around
* @return A safe location, or none if it wasn't found.
*/
private Location getSafeSpawnAroundABlock(Location l) {
Iterator<BlockFace> checkblock = AROUND_BLOCK.iterator();
while (checkblock.hasNext()) {
final BlockFace face = checkblock.next();
private Location getSafeSpawnAroundBlock(Location l) {
for (BlockFace face : AROUND_BLOCK) {
if (this.playerCanSpawnHereSafely(l.getBlock().getRelative(face).getLocation())) {
// Don't forget to center the player.
return l.getBlock().getRelative(face).getLocation().add(.5, 0, .5);
@@ -158,16 +354,15 @@ public class SimpleBlockSafety implements BlockSafety {
*/
private Location findOtherBedPiece(Location checkLoc) {
BlockData data = checkLoc.getBlock().getBlockData();
if (!(data instanceof Bed)) {
if (!(data instanceof Bed bed)) {
return null;
}
Bed b = (Bed) data;
if (b.getPart() == Bed.Part.HEAD) {
return checkLoc.getBlock().getRelative(b.getFacing().getOppositeFace()).getLocation();
if (bed.getPart() == Bed.Part.HEAD) {
return checkLoc.getBlock().getRelative(bed.getFacing().getOppositeFace()).getLocation();
}
// We shouldn't ever be looking at the foot, but here's the code for it.
return checkLoc.getBlock().getRelative(b.getFacing()).getLocation();
return checkLoc.getBlock().getRelative(bed.getFacing()).getLocation();
}
@@ -206,7 +401,7 @@ public class SimpleBlockSafety implements BlockSafety {
/*
* If someone has a better way of this... Please either tell us, or submit a pull request!
*/
public static boolean isSolidBlock(Material type) {
private static boolean isSolidBlock(Material type) {
return type.isSolid();
}
@@ -237,7 +432,7 @@ public class SimpleBlockSafety implements BlockSafety {
if (oneBelow.getBlock().getType() == Material.WATER) {
Location twoBelow = oneBelow.clone();
twoBelow.subtract(0, 1, 0);
return (oneBelow.getBlock().getType() == Material.WATER);
return oneBelow.getBlock().getType() == Material.WATER;
}
if (oneBelow.getBlock().getType() != Material.AIR) {
return false;
@@ -253,10 +448,7 @@ public class SimpleBlockSafety implements BlockSafety {
if (this.isBlockAboveAir(cart.getLocation())) {
return true;
}
if (this.isEntitiyOnTrack(locationManipulation.getNextBlock(cart))) {
return true;
}
return false;
return this.isEntitiyOnTrack(locationManipulation.getNextBlock(cart));
}
/**
@@ -264,10 +456,7 @@ public class SimpleBlockSafety implements BlockSafety {
*/
@Override
public boolean canSpawnVehicleSafely(Vehicle vehicle) {
if (this.isBlockAboveAir(vehicle.getLocation())) {
return true;
}
return false;
return this.isBlockAboveAir(vehicle.getLocation());
}
}
@@ -1,378 +0,0 @@
/******************************************************************************
* Multiverse 2 Copyright (c) the Multiverse Team 2011. *
* Multiverse 2 is licensed under the BSD License. *
* For more information please check the README.md file included *
* with this project. *
******************************************************************************/
package org.mvplugins.multiverse.core.teleportation;
import java.util.concurrent.CompletableFuture;
import co.aikar.commands.BukkitCommandIssuer;
import com.dumptruckman.minecraft.util.Logging;
import io.papermc.lib.PaperLib;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Minecart;
import org.bukkit.entity.Player;
import org.bukkit.entity.Vehicle;
import org.bukkit.util.Vector;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.api.BlockSafety;
import org.mvplugins.multiverse.core.api.DestinationInstance;
import org.mvplugins.multiverse.core.api.LocationManipulation;
import org.mvplugins.multiverse.core.api.SafeTTeleporter;
import org.mvplugins.multiverse.core.destination.ParsedDestination;
/**
* The default-implementation of {@link SafeTTeleporter}.
*/
@Service
public class SimpleSafeTTeleporter implements SafeTTeleporter {
private final MultiverseCore plugin;
private final LocationManipulation locationManipulation;
private final BlockSafety blockSafety;
private final TeleportQueue teleportQueue;
@Inject
public SimpleSafeTTeleporter(
MultiverseCore plugin,
LocationManipulation locationManipulation,
BlockSafety blockSafety,
TeleportQueue teleportQueue
) {
this.plugin = plugin;
this.locationManipulation = locationManipulation;
this.blockSafety = blockSafety;
this.teleportQueue = teleportQueue;
}
private static final Vector DEFAULT_VECTOR = new Vector();
private static final int DEFAULT_TOLERANCE = 6;
private static final int DEFAULT_RADIUS = 9;
/**
* {@inheritDoc}
*/
@Override
public Location getSafeLocation(Location l) {
return this.getSafeLocation(l, DEFAULT_TOLERANCE, DEFAULT_RADIUS);
}
/**
* {@inheritDoc}
*/
@Override
public Location getSafeLocation(Location l, int tolerance, int radius) {
// Check around the player first in a configurable radius:
// TODO: Make this configurable
Location safe = checkAboveAndBelowLocation(l, tolerance, radius);
if (safe != null) {
safe.setX(safe.getBlockX() + .5); // SUPPRESS CHECKSTYLE: MagicNumberCheck
safe.setZ(safe.getBlockZ() + .5); // SUPPRESS CHECKSTYLE: MagicNumberCheck
Logging.fine("Hey! I found one: " + locationManipulation.strCoordsRaw(safe));
} else {
Logging.fine("Uh oh! No safe place found!");
}
return safe;
}
private Location checkAboveAndBelowLocation(Location l, int tolerance, int radius) {
// Tolerance must be an even number:
if (tolerance % 2 != 0) {
tolerance += 1;
}
// We want half of it, so we can go up and down
tolerance /= 2;
Logging.finer("Given Location of: " + locationManipulation.strCoordsRaw(l));
Logging.finer("Checking +-" + tolerance + " with a radius of " + radius);
// For now this will just do a straight up block.
Location locToCheck = l.clone();
// Check the main level
Location safe = this.checkAroundLocation(locToCheck, radius);
if (safe != null) {
return safe;
}
// We've already checked zero right above this.
int currentLevel = 1;
while (currentLevel <= tolerance) {
// Check above
locToCheck = l.clone();
locToCheck.add(0, currentLevel, 0);
safe = this.checkAroundLocation(locToCheck, radius);
if (safe != null) {
return safe;
}
// Check below
locToCheck = l.clone();
locToCheck.subtract(0, currentLevel, 0);
safe = this.checkAroundLocation(locToCheck, radius);
if (safe != null) {
return safe;
}
currentLevel++;
}
return null;
}
/*
* For my crappy algorithm, radius MUST be odd.
*/
private Location checkAroundLocation(Location l, int diameter) {
if (diameter % 2 == 0) {
diameter += 1;
}
Location checkLoc = l.clone();
// Start at 3, the min diameter around a block
int loopcounter = 3;
while (loopcounter <= diameter) {
boolean foundSafeArea = checkAroundSpecificDiameter(checkLoc, loopcounter);
// If a safe area was found:
if (foundSafeArea) {
// Return the checkLoc, it is the safe location.
return checkLoc;
}
// Otherwise, let's reset our location
checkLoc = l.clone();
// And increment the radius
loopcounter += 2;
}
return null;
}
private boolean checkAroundSpecificDiameter(Location checkLoc, int circle) {
// Adjust the circle to get how many blocks to step out.
// A radius of 3 makes the block step 1
// A radius of 5 makes the block step 2
// A radius of 7 makes the block step 3
// ...
int adjustedCircle = ((circle - 1) / 2);
checkLoc.add(adjustedCircle, 0, 0);
if (blockSafety.playerCanSpawnHereSafely(checkLoc)) {
return true;
}
// Now we go to the right that adjustedCircle many
for (int i = 0; i < adjustedCircle; i++) {
checkLoc.add(0, 0, 1);
if (blockSafety.playerCanSpawnHereSafely(checkLoc)) {
return true;
}
}
// Then down adjustedCircle *2
for (int i = 0; i < adjustedCircle * 2; i++) {
checkLoc.add(-1, 0, 0);
if (blockSafety.playerCanSpawnHereSafely(checkLoc)) {
return true;
}
}
// Then left adjustedCircle *2
for (int i = 0; i < adjustedCircle * 2; i++) {
checkLoc.add(0, 0, -1);
if (blockSafety.playerCanSpawnHereSafely(checkLoc)) {
return true;
}
}
// Then up Then left adjustedCircle *2
for (int i = 0; i < adjustedCircle * 2; i++) {
checkLoc.add(1, 0, 0);
if (blockSafety.playerCanSpawnHereSafely(checkLoc)) {
return true;
}
}
// Then finish up by doing adjustedCircle - 1
for (int i = 0; i < adjustedCircle - 1; i++) {
checkLoc.add(0, 0, 1);
if (blockSafety.playerCanSpawnHereSafely(checkLoc)) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public TeleportResult safelyTeleport(BukkitCommandIssuer teleporter, Entity teleportee, ParsedDestination<?> destination) {
return safelyTeleportAsync(teleporter, teleportee, destination).join();
}
@Override
public CompletableFuture<TeleportResult> safelyTeleportAsync(BukkitCommandIssuer teleporter, Entity teleportee, ParsedDestination<?> destination) {
if (destination == null) {
Logging.finer("Entity tried to teleport to an invalid destination");
return CompletableFuture.completedFuture(TeleportResult.FAIL_INVALID);
}
Player teleporteePlayer = null;
if (teleportee instanceof Player) {
teleporteePlayer = ((Player) teleportee);
} else if (teleportee.getPassenger() instanceof Player) {
teleporteePlayer = ((Player) teleportee.getPassenger());
}
if (teleporteePlayer == null) {
return CompletableFuture.completedFuture(TeleportResult.FAIL_INVALID);
}
teleportQueue.addToQueue(teleporter.getIssuer().getName(), teleporteePlayer.getName());
Location safeLoc = destination.getLocation(teleportee);
if (destination.getDestination().checkTeleportSafety()) {
safeLoc = this.getSafeLocation(teleportee, destination.getDestinationInstance());
}
if (safeLoc == null) {
return CompletableFuture.completedFuture(TeleportResult.FAIL_UNSAFE);
}
CompletableFuture<TeleportResult> future = new CompletableFuture<>();
PaperLib.teleportAsync(teleportee, safeLoc).thenAccept(result -> {
if (!result) {
future.complete(TeleportResult.FAIL_OTHER);
return;
}
Vector v = destination.getDestinationInstance().getVelocity(teleportee);
if (v != null && !DEFAULT_VECTOR.equals(v)) {
Bukkit.getScheduler().runTaskLater(this.plugin, () -> {
teleportee.setVelocity(v);
}, 1);
}
future.complete(TeleportResult.SUCCESS);
});
return future;
}
/**
* {@inheritDoc}
*/
@Override
public TeleportResult safelyTeleport(CommandSender teleporter, Entity teleportee, Location location, boolean safely) {
if (safely) {
location = this.getSafeLocation(location);
}
if (location != null) {
if (teleportee.teleport(location)) {
return TeleportResult.SUCCESS;
}
return TeleportResult.FAIL_OTHER;
}
return TeleportResult.FAIL_UNSAFE;
}
@Override
public Location getSafeLocation(Entity entity, DestinationInstance destination) {
Location l = destination.getLocation(entity);
if (blockSafety.playerCanSpawnHereSafely(l)) {
Logging.fine("The first location you gave me was safe.");
return l;
}
if (entity instanceof Minecart) {
Minecart m = (Minecart) entity;
if (!blockSafety.canSpawnCartSafely(m)) {
return null;
}
} else if (entity instanceof Vehicle) {
Vehicle v = (Vehicle) entity;
if (!blockSafety.canSpawnVehicleSafely(v)) {
return null;
}
}
Location safeLocation = this.getSafeLocation(l);
if (safeLocation != null) {
// Add offset to account for a vehicle on dry land!
if (entity instanceof Minecart && !blockSafety.isEntitiyOnTrack(safeLocation)) {
safeLocation.setY(safeLocation.getBlockY() + .5);
Logging.finer("Player was inside a minecart. Offsetting Y location.");
}
Logging.finer("Had to look for a bit, but I found a safe place for ya!");
return safeLocation;
}
if (entity instanceof Player) {
Player p = (Player) entity;
p.sendMessage("No safe locations found!");
Logging.finer("No safe location found for " + p.getName());
} else if (entity.getPassenger() instanceof Player) {
Player p = (Player) entity.getPassenger();
p.sendMessage("No safe locations found!");
Logging.finer("No safe location found for " + p.getName());
}
Logging.fine("Sorry champ, you're basically trying to teleport into a minefield. I should just kill you now.");
return null;
}
/**
* {@inheritDoc}
*/
@Override
public Location findPortalBlockNextTo(Location l) {
Block b = l.getWorld().getBlockAt(l);
Location foundLocation = null;
if (b.getType() == Material.NETHER_PORTAL) {
return l;
}
if (b.getRelative(BlockFace.NORTH).getType() == Material.NETHER_PORTAL) {
foundLocation = getCloserBlock(l, b.getRelative(BlockFace.NORTH).getLocation(), foundLocation);
}
if (b.getRelative(BlockFace.SOUTH).getType() == Material.NETHER_PORTAL) {
foundLocation = getCloserBlock(l, b.getRelative(BlockFace.SOUTH).getLocation(), foundLocation);
}
if (b.getRelative(BlockFace.EAST).getType() == Material.NETHER_PORTAL) {
foundLocation = getCloserBlock(l, b.getRelative(BlockFace.EAST).getLocation(), foundLocation);
}
if (b.getRelative(BlockFace.WEST).getType() == Material.NETHER_PORTAL) {
foundLocation = getCloserBlock(l, b.getRelative(BlockFace.WEST).getLocation(), foundLocation);
}
return foundLocation;
}
private static Location getCloserBlock(Location source, Location blockA, Location blockB) {
// If B wasn't given, return a.
if (blockB == null) {
return blockA;
}
// Center our calculations
blockA.add(.5, 0, .5);
blockB.add(.5, 0, .5);
// Retrieve the distance to the normalized blocks
double testA = source.distance(blockA);
double testB = source.distance(blockB);
// Compare and return
if (testA <= testB) {
return blockA;
}
return blockB;
}
@Override
public TeleportResult teleport(BukkitCommandIssuer teleporter, Entity teleportee, ParsedDestination<?> destination) {
return safelyTeleport(teleporter, teleportee, destination);
}
@Override
public CompletableFuture<TeleportResult> teleportAsync(BukkitCommandIssuer teleporter, Entity teleportee, ParsedDestination<?> destination) {
return safelyTeleportAsync(teleporter, teleportee, destination);
}
}
@@ -12,7 +12,7 @@ public class TeleportQueue {
private final Map<String, String> teleportQueue;
public TeleportQueue() {
TeleportQueue() {
teleportQueue = new HashMap<>();
}
@@ -1,38 +1,19 @@
/******************************************************************************
* Multiverse 2 Copyright (c) the Multiverse Team 2011. *
* Multiverse 2 is licensed under the BSD License. *
* For more information please check the README.md file included *
* with this project. *
******************************************************************************/
package org.mvplugins.multiverse.core.teleportation;
/**
* An enum containing possible teleport-results.
*/
public enum TeleportResult {
/**
* Insufficient permissions.
*/
FAIL_PERMISSION,
/**
* The teleport was unsafe.
*/
FAIL_UNSAFE,
/**
* The player was to poor.
*/
FAIL_TOO_POOR,
/**
* The teleport was invalid.
*/
FAIL_INVALID,
/**
* Unknown reason.
*/
FAIL_OTHER,
/**
* The player was successfully teleported.
*/
SUCCESS
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.utils.result.SuccessReason;
public class TeleportResult {
public enum Success implements SuccessReason {
SUCCESS
}
public enum Failure implements FailureReason {
NULL_DESTINATION,
NULL_LOCATION,
UNSAFE_LOCATION,
TELEPORT_FAILED,
TELEPORT_FAILED_EXCEPTION,
PLAYER_OFFLINE,
}
}
@@ -43,21 +43,32 @@ public enum MVCorei18n implements MessageKeyProvider {
// delete command
DELETE_DELETING,
DELETE_PROMPT,
DELETE_SUCCESS,
// Dumps command
DUMPS_DESCRIPTION,
DUMPS_URL_LIST,
// gamerule command
GAMERULE_FAILED,
GAMERULE_SUCCESS_SINGLE,
GAMERULE_SUCCESS_MULTIPLE,
// gamerule set command
GAMERULE_SET_FAILED,
GAMERULE_SET_SUCCESS_SINGLE,
GAMERULE_SET_SUCCESS_MULTIPLE,
// Gamerules command
GAMERULES_DESCRIPTION,
GAMERULES_DESCRIPTION_PAGE,
GAMERULES_DESCRIPTION_WORLD,
GAMERULES_TITLE,
// gamerule reset command
GAMERULE_RESET_FAILED,
GAMERULE_RESET_SUCCESS_SINGLE,
GAMERULE_RESET_SUCCESS_MULTIPLE,
// gamerule list command
GAMERULE_LIST_DESCRIPTION,
GAMERULE_LIST_DESCRIPTION_PAGE,
GAMERULE_LIST_DESCRIPTION_WORLD,
GAMERULE_LIST_TITLE,
// Generators command
GENERATORS_DESCRIPTION,
GENERATORS_DESCRIPTION_FLAGS,
GENERATORS_EMPTY,
// import command
IMPORT_IMPORTING,
@@ -76,6 +87,9 @@ public enum MVCorei18n implements MessageKeyProvider {
RELOAD_RELOADING,
RELOAD_SUCCESS,
// remove command
REMOVE_SUCCESS,
// root MV command
ROOT_TITLE,
ROOT_HELP,
@@ -87,6 +101,11 @@ public enum MVCorei18n implements MessageKeyProvider {
UNLOAD_UNLOADING,
UNLOAD_SUCCESS,
// version command
VERSION_MV,
VERSION_AUTHORS,
VERSION_SECRETCODE,
// debug command
DEBUG_INFO_OFF,
DEBUG_INFO_ON,
@@ -115,7 +134,6 @@ public enum MVCorei18n implements MessageKeyProvider {
CREATEWORLD_WORLDEXISTLOADED,
CREATEWORLD_BUKKITCREATIONFAILED,
DELETE_SUCCESS,
DELETEWORLD_WORLDNONEXISTENT,
DELETEWORLD_LOADFAILED,
DELETEWORLD_WORLDFOLDERNOTFOUND,
@@ -133,7 +151,6 @@ public enum MVCorei18n implements MessageKeyProvider {
LOADWORLD_WORLDEXISTLOADED,
LOADWORLD_BUKKITCREATIONFAILED,
REMOVEWORLD_REMOVED,
REMOVEWORLD_WORLDNONEXISTENT,
UNLOADWORLD_WORLDALREADYUNLOADING,
@@ -1,49 +0,0 @@
package org.mvplugins.multiverse.core.utils;
import java.util.concurrent.Callable;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.MVCoreConfig;
/**
* Wraps calls that could result in exceptions that are not Multiverse's fault.
*/
@Service
public class UnsafeCallWrapper {
private final MVCoreConfig config;
@Inject
public UnsafeCallWrapper(MVCoreConfig configProvider) {
this.config = configProvider;
}
/**
* Wraps calls that could result in exceptions that are not Multiverse's fault.
*
* @param callable The potentially unsafe call.
* @param plugin The plugin that's probably the culprit.
* @param action What MV was attempting to do (error message, format string).
* @param formatArgs The formatting arguments for the error message.
* The exception that was thrown will be appended to these objects.
* @param <T> The type of the return value.
* @return The return value or null if the call failed.
*/
public <T> T wrap(Callable<T> callable, String plugin, String action, Object... formatArgs) {
try {
// We're ready to catch you! JUMP!
return callable.call();
} catch (Throwable t) {
Object[] actualFormatArgs = new Object[formatArgs.length + 1];
System.arraycopy(formatArgs, 0, actualFormatArgs, 0, formatArgs.length);
actualFormatArgs[formatArgs.length] = t;
Logging.warning(action, actualFormatArgs);
Logging.warning("This is a bug in %s, NOT a bug in Multiverse!", plugin);
if (config.getGlobalDebug() >= 1)
t.printStackTrace();
return null;
}
}
}
@@ -0,0 +1,163 @@
package org.mvplugins.multiverse.core.utils.result;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* Represents the result of an asynchronous operation that wraps a {@link CompletableFuture}.
*
* @param <T> The type of the value.
*/
public final class Async<T> {
/**
* Returns a new AsyncResult that is completed when all of the given AsyncResult complete.
*
* @param results The results to wait for.
* @return A new AsyncResult that is completed when all of the given AsyncResult complete.
*/
public static Async<Void> allOf(Async<?>... results) {
return new Async<>(CompletableFuture.allOf(Arrays.stream(results)
.map(result -> result.future)
.toArray(CompletableFuture[]::new)));
}
/**
* Returns a new AsyncResult that is completed when all of the given AsyncResult with the same type complete.
*
* @param results The results to wait for.
* @param <T> The type of the AsyncResult.
* @return A new AsyncResult that is completed when all of the given AsyncResult complete.
*/
public static <T> Async<List<T>> allOf(List<Async<T>> results) {
return new Async<>(CompletableFuture.allOf(results.stream()
.map(result -> result.future)
.toArray(CompletableFuture[]::new))
.thenApply(v -> results.stream()
.map(result -> result.future.join())
.toList()));
}
/**
* Wraps a CompletableFuture in an AsyncResult.
*
* @param future The future to wrap.
* @param <T> The type of the future.
* @return A new AsyncResult that is completed when the given future completes.
*/
public static <T> Async<T> of(CompletableFuture<T> future) {
return new Async<>(future);
}
/**
* Returns a new AsyncResult that is already completed with the given value.
*
* @param value The value to complete the AsyncResult with.
* @param <T> The type of the value.
* @return The completed AsyncResult.
*/
public static <T> Async<T> completedFuture(T value) {
return new Async<>(CompletableFuture.completedFuture(value));
}
/**
* Returns a new CompletableFuture that is already completed exceptionally with the given exception.
*
* @param throwable The exception to complete the AsyncResult with.
* @param <T> The type of the value.
* @return The completed AsyncResult.
*/
public static <T> Async<T> failedFuture(Throwable throwable) {
return new Async<>(CompletableFuture.failedFuture(throwable));
}
private final CompletableFuture<T> future;
/**
* Creates a new AsyncResult.
*/
public Async() {
this(new CompletableFuture<>());
}
private Async(CompletableFuture<T> future) {
this.future = future;
}
/**
* If not already completed, sets the value returned by related methods to the given value.
*
* @param value The value to complete the AsyncResult with.
* @return true if this invocation caused this AsyncResult to transition to a completed state, else false.
*/
public boolean complete(T value) {
return future.complete(value);
}
/**
* If not already completed, causes invocations of related methods to throw the given exception.
*
* @param throwable The exception to complete the AsyncResult with.
* @return true if this invocation caused this AsyncResult to transition to a completed state, else false.
*/
public boolean completeExceptionally(Throwable throwable) {
return future.completeExceptionally(throwable);
}
/**
* Executes the given action when this AsyncResult completes.
*
* @param consumer The action to perform.
* @return This AsyncResult.
*/
public Async<Void> thenAccept(Consumer<T> consumer) {
return new Async<>(future.thenAccept(consumer));
}
/**
* Executes the given action when this AsyncResult completes.
*
* @param runnable The action to perform.
* @return This AsyncResult.
*/
public Async<Void> thenRun(Runnable runnable) {
return new Async<>(future.thenRun(runnable));
}
/**
* Executes the given action when this AsyncResult completes and returns a new AsyncResult with the new value.
*
* @param function The action to perform.
* @param <U> The type of the new value.
* @return A new AsyncResult with the new value.
*/
public <U> Async<U> thenApply(Function<T, U> function) {
return new Async<>(future.thenApply(function));
}
/**
* Executes the given action when this AsyncResult completes with an exception.
*
* @param consumer The action to perform.
* @return This AsyncResult.
*/
public Async<T> exceptionally(Consumer<Throwable> consumer) {
return new Async<>(future.exceptionally(throwable -> {
consumer.accept(throwable);
return null;
}));
}
/**
* Executes the given action when this AsyncResult completes with an exception and returns a new AsyncResult with the new value.
*
* @param function The action to perform.
* @return A new AsyncResult with the new value.
*/
public Async<T> exceptionally(Function<Throwable, T> function) {
return new Async<>(future.exceptionally(function));
}
}
@@ -0,0 +1,73 @@
package org.mvplugins.multiverse.core.utils.result;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.mvplugins.multiverse.core.utils.message.MessageReplacement;
public final class AsyncAttempt<T, F extends FailureReason> {
public static <T, F extends FailureReason> Async<List<Attempt<T, F>>> allOf(List<AsyncAttempt<T, F>> attempts) {
return Async.of(CompletableFuture.allOf(attempts.stream()
.map(attempt -> attempt.future)
.toArray(CompletableFuture[]::new))
.thenApply(v -> attempts.stream()
.map(attempt -> attempt.future.join())
.toList()));
}
public static <T, F extends FailureReason> AsyncAttempt<T, F> of(
CompletableFuture<T> future,
BiFunction<T, Throwable, Attempt<T, F>> completionHandler) {
return new AsyncAttempt<>(future.handle(completionHandler));
}
public static <T, F extends FailureReason> AsyncAttempt<T, F> of(
CompletableFuture<T> future,
Function<Throwable, Attempt<T, F>> exceptionHandler) {
BiFunction<T, Throwable, Attempt<T, F>> completionHandler = (result, exception) -> exception != null
? exceptionHandler.apply(exception)
: Attempt.success(result);
return of(future, completionHandler);
}
public static <F extends FailureReason> AsyncAttempt<Void, F> success() {
return new AsyncAttempt<>(CompletableFuture.completedFuture(null));
}
public static <F extends FailureReason> AsyncAttempt<Void, F> failure(
F failureReason,
MessageReplacement... messageReplacements) {
return new AsyncAttempt<>(CompletableFuture.completedFuture(
Attempt.failure(failureReason, messageReplacements)));
}
private final CompletableFuture<Attempt<T, F>> future;
private AsyncAttempt(CompletableFuture<Attempt<T, F>> future) {
this.future = future;
}
public <U> AsyncAttempt<U, F> map(Function<? super T, ? extends U> mapper) {
return new AsyncAttempt<>(future.thenApply(attempt -> attempt.map(mapper)));
}
public <U> AsyncAttempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper) {
return new AsyncAttempt<>(future.thenApply(attempt -> attempt.mapAttempt(mapper)));
}
public <U> AsyncAttempt<U, F> mapAsyncAttempt(Function<? super T, AsyncAttempt<U, F>> mapper) {
return new AsyncAttempt<>(future.thenApplyAsync(
attempt -> attempt.mapAttempt(rasult -> mapper.apply(rasult).toAttempt())));
}
public AsyncAttempt<T, F> onSuccess(Runnable runnable) {
return new AsyncAttempt<>(future.thenApply(attempt -> attempt.onSuccess(runnable)));
}
public Attempt<T, F> toAttempt() {
return future.join();
}
}
@@ -14,7 +14,6 @@ import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.api.BlockSafety;
import org.mvplugins.multiverse.core.api.LocationManipulation;
import org.mvplugins.multiverse.core.api.SafeTTeleporter;
import org.mvplugins.multiverse.core.world.config.NullLocation;
import org.mvplugins.multiverse.core.world.config.SpawnLocation;
import org.mvplugins.multiverse.core.world.config.WorldConfig;
@@ -29,19 +28,16 @@ public class LoadedMultiverseWorld extends MultiverseWorld {
private final UUID worldUid;
private final BlockSafety blockSafety;
private final SafeTTeleporter safetyTeleporter;
private final LocationManipulation locationManipulation;
LoadedMultiverseWorld(
@NotNull World world,
@NotNull WorldConfig worldConfig,
@NotNull BlockSafety blockSafety,
@NotNull SafeTTeleporter safetyTeleporter,
@NotNull LocationManipulation locationManipulation) {
super(world.getName(), worldConfig);
this.worldUid = world.getUID();
this.blockSafety = blockSafety;
this.safetyTeleporter = safetyTeleporter;
this.locationManipulation = locationManipulation;
setupWorldConfig(world);
@@ -82,7 +78,7 @@ public class LoadedMultiverseWorld extends MultiverseWorld {
// The location is not safe, so we need to find a better one.
Logging.warning("Spawn location from world.dat file was unsafe. Adjusting...");
Logging.warning("Original Location: " + locationManipulation.strCoordsRaw(location));
Location newSpawn = safetyTeleporter.getSafeLocation(location,
Location newSpawn = blockSafety.getSafeLocation(location,
SPAWN_LOCATION_SEARCH_TOLERANCE, SPAWN_LOCATION_SEARCH_RADIUS);
// I think we could also do this, as I think this is what Notch does.
// Not sure how it will work in the nether...
@@ -107,6 +103,15 @@ public class LoadedMultiverseWorld extends MultiverseWorld {
return location;
}
/**
* Gets the Unique ID of this world.
*
* @return Unique ID of this world.
*/
public UUID getUID() {
return worldUid;
}
/**
* Gets the Bukkit world object that this world describes.
*
@@ -1,6 +1,5 @@
package org.mvplugins.multiverse.core.world;
import java.util.Collection;
import java.util.List;
import com.google.common.base.Strings;
@@ -13,6 +12,7 @@ import org.bukkit.Material;
import org.bukkit.World;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.world.config.AllowedPortalType;
import org.mvplugins.multiverse.core.world.config.WorldConfig;
@@ -57,34 +57,12 @@ public class MultiverseWorld {
}
/**
* Gets the properties that can be configured on this world. Can be used for {@link #getProperty(String)} and
* {@link #setProperty(String, Object)}.
* Gets the properties handler of this world.
*
* @return A collection of property names.
* @return The properties handler of this world.
*/
public Collection<String> getConfigurablePropertyNames() {
return worldConfig.getConfigurablePropertyNames();
}
/**
* Gets a property on this world.
*
* @param name The name of the property.
* @return The value of the property.
*/
public Try<Object> getProperty(String name) {
return worldConfig.getProperty(name);
}
/**
* Sets a property on this world.
*
* @param name The name of the property.
* @param value The value of the property.
* @return Result of setting property.
*/
public Try<Void> setProperty(String name, Object value) {
return worldConfig.setProperty(name, value);
public StringPropertyHandle getStringPropertyHandle() {
return worldConfig.getStringPropertyHandle();
}
/**
@@ -27,7 +27,6 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.api.BlockSafety;
import org.mvplugins.multiverse.core.api.LocationManipulation;
import org.mvplugins.multiverse.core.api.SafeTTeleporter;
import org.mvplugins.multiverse.core.utils.message.MessageReplacement;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
@@ -75,7 +74,6 @@ public class WorldManager {
private final PlayerWorldTeleporter playerWorldActions;
private final FilesManipulator filesManipulator;
private final BlockSafety blockSafety;
private final SafeTTeleporter safetyTeleporter;
private final LocationManipulation locationManipulation;
@Inject
@@ -86,7 +84,6 @@ public class WorldManager {
@NotNull PlayerWorldTeleporter playerWorldActions,
@NotNull FilesManipulator filesManipulator,
@NotNull BlockSafety blockSafety,
@NotNull SafeTTeleporter safetyTeleporter,
@NotNull LocationManipulation locationManipulation) {
this.worldsMap = new HashMap<>();
this.loadedWorldsMap = new HashMap<>();
@@ -99,7 +96,6 @@ public class WorldManager {
this.playerWorldActions = playerWorldActions;
this.filesManipulator = filesManipulator;
this.blockSafety = blockSafety;
this.safetyTeleporter = safetyTeleporter;
this.locationManipulation = locationManipulation;
}
@@ -297,7 +293,6 @@ public class WorldManager {
world,
worldConfig,
blockSafety,
safetyTeleporter,
locationManipulation);
loadedWorldsMap.put(loadedWorld.getName(), loadedWorld);
saveWorldsConfig();
@@ -354,7 +349,6 @@ public class WorldManager {
world,
worldConfig,
blockSafety,
safetyTeleporter,
locationManipulation);
loadedWorldsMap.put(loadedWorld.getName(), loadedWorld);
saveWorldsConfig();
@@ -377,10 +371,6 @@ public class WorldManager {
return worldActionResult(UnloadFailureReason.WORLD_ALREADY_UNLOADING, world.getName());
}
if (options.removePlayers()) {
playerWorldActions.removeFromWorld(world);
}
return unloadBukkitWorld(world.getBukkitWorld().getOrNull(), options.saveBukkitWorld()).fold(
exception -> worldActionResult(UnloadFailureReason.BUKKIT_UNLOAD_FAILED,
world.getName(), exception),
@@ -432,7 +422,7 @@ public class WorldManager {
*/
public Attempt<String, RemoveFailureReason> removeWorld(@NotNull LoadedMultiverseWorld loadedWorld) {
// TODO: Config option on removePlayers
return unloadWorld(UnloadWorldOptions.world(loadedWorld).removePlayers(true))
return unloadWorld(UnloadWorldOptions.world(loadedWorld))
.transform(RemoveFailureReason.UNLOAD_FAILED)
.mapAttempt(this::removeWorldFromConfig);
}
@@ -594,7 +584,6 @@ public class WorldManager {
*/
public Attempt<LoadedMultiverseWorld, RegenFailureReason> regenWorld(@NotNull RegenWorldOptions options) {
LoadedMultiverseWorld world = options.world();
List<Player> playersInWorld = world.getPlayers().getOrElse(Collections.emptyList());
DataTransfer<LoadedMultiverseWorld> dataTransfer = transferData(options, world);
boolean shouldKeepSpawnLocation = options.keepWorldConfig() && options.seed() == world.getSeed();
Location spawnLocation = world.getSpawnLocation();
@@ -617,7 +606,6 @@ public class WorldManager {
// different seed.
newWorld.setSpawnLocation(spawnLocation);
}
playerWorldActions.teleportPlayersToWorld(playersInWorld, newWorld);
saveWorldsConfig();
});
}
@@ -681,12 +669,13 @@ public class WorldManager {
unloadTracker.add(world.getName());
if (!Bukkit.unloadWorld(world, save)) {
// TODO: Localize this, maybe with MultiverseException
if (!world.getPlayers().isEmpty()) {
throw new Exception("There are still players in the world! Please use --remove-players flag to "
+ "your command if wish to teleport all players out of the world.");
}
throw new Exception("Is this the default world? You can't unload the default world!");
}
Logging.fine("Bukkit unloaded world: " + world.getName());
}).onFailure(exception -> {
Logging.severe("Failed to unload bukkit world: " + world.getName());
exception.printStackTrace();
}).andFinally(() -> unloadTracker.remove(world.getName()));
}
@@ -737,6 +726,17 @@ public class WorldManager {
return !isLoadedWorld(worldName) && isWorld(worldName);
}
/**
* Get a world that may or may not be loaded. It will an {@link LoadedMultiverseWorld} if the world is loaded,
* otherwise returns an {@link MultiverseWorld} instance.
*
* @param world The bukkit world to get.
* @return The world if it exists.
*/
public Option<MultiverseWorld> getWorld(@Nullable World world) {
return Option.of(world).flatMap(this::getWorld);
}
/**
* Get a world that may or may not be loaded. It will an {@link LoadedMultiverseWorld} if the world is loaded,
* otherwise returns an {@link MultiverseWorld} instance.
@@ -14,7 +14,9 @@ import org.bukkit.configuration.ConfigurationSection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.configuration.handle.ConfigurationSectionHandle;
import org.mvplugins.multiverse.core.configuration.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.configuration.migration.BooleanMigratorAction;
import org.mvplugins.multiverse.core.configuration.migration.ConfigMigrator;
import org.mvplugins.multiverse.core.configuration.migration.IntegerMigratorAction;
@@ -23,7 +25,6 @@ import org.mvplugins.multiverse.core.configuration.migration.MoveMigratorAction;
import org.mvplugins.multiverse.core.configuration.migration.NullStringMigratorAction;
import org.mvplugins.multiverse.core.configuration.migration.VersionMigrator;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.helpers.EnforcementHandler;
/**
* Represents a world configuration.
@@ -33,20 +34,21 @@ public final class WorldConfig {
private final String worldName;
private final WorldConfigNodes configNodes;
private final ConfigurationSectionHandle configHandle;
private final StringPropertyHandle stringPropertyHandle;
WorldConfig(
@NotNull String worldName,
@NotNull ConfigurationSection configSection,
@NotNull EnforcementHandler enforcementHandler) {
@NotNull MultiverseCore multiverseCore) {
this.worldName = worldName;
this.configNodes = new WorldConfigNodes(enforcementHandler);
this.configHandle = ConfigurationSectionHandle.builder(configSection)
this.configNodes = new WorldConfigNodes(multiverseCore);
this.configHandle = ConfigurationSectionHandle.builder(configSection, configNodes.getNodes())
.logger(Logging.getLogger())
.nodes(configNodes.getNodes())
.migrator(ConfigMigrator.builder(configNodes.VERSION)
.addVersionMigrator(initialVersionMigrator())
.build())
.build();
this.stringPropertyHandle = new StringPropertyHandle(configHandle);
load();
}
@@ -110,22 +112,14 @@ public final class WorldConfig {
return configHandle.load(section);
}
public StringPropertyHandle getStringPropertyHandle() {
return stringPropertyHandle;
}
public String getWorldName() {
return worldName;
}
public Collection<String> getConfigurablePropertyNames() {
return configNodes.getNodes().getNames();
}
public Try<Object> getProperty(String name) {
return configHandle.get(name);
}
public Try<Void> setProperty(String name, Object value) {
return configHandle.set(name, value);
}
public boolean getAdjustSpawn() {
return configHandle.get(configNodes.ADJUST_SPAWN);
}
@@ -1,7 +1,6 @@
package org.mvplugins.multiverse.core.world.config;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Difficulty;
import org.bukkit.GameMode;
@@ -10,10 +9,11 @@ import org.bukkit.Material;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.configuration.node.ConfigNode;
import org.mvplugins.multiverse.core.configuration.node.ListConfigNode;
import org.mvplugins.multiverse.core.configuration.node.Node;
import org.mvplugins.multiverse.core.configuration.node.NodeGroup;
import org.mvplugins.multiverse.core.world.config.AllowedPortalType;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.helpers.EnforcementHandler;
@@ -27,8 +27,8 @@ public class WorldConfigNodes {
private EnforcementHandler enforcementHandler;
private LoadedMultiverseWorld world = null;
WorldConfigNodes(@NotNull EnforcementHandler enforcementHandler) {
this.enforcementHandler = enforcementHandler;
WorldConfigNodes(@NotNull MultiverseCore multiverseCore) {
this.enforcementHandler = multiverseCore.getService(EnforcementHandler.class);
}
LoadedMultiverseWorld getWorld() {
@@ -212,8 +212,8 @@ public class WorldConfigNodes {
})
.build());
final ConfigNode<List> SPAWNING_ANIMALS_EXCEPTIONS = node(ConfigNode
.builder("spawning.animals.exceptions", List.class)
final ListConfigNode<String> SPAWNING_ANIMALS_EXCEPTIONS = node(ListConfigNode
.listBuilder("spawning.animals.exceptions", String.class)
.defaultValue(new ArrayList<>())
.name("spawning-animals-exceptions")
.build());
@@ -238,14 +238,13 @@ public class WorldConfigNodes {
})
.build());
final ConfigNode<List> SPAWNING_MONSTERS_EXCEPTIONS = node(ConfigNode
.builder("spawning.monsters.exceptions", List.class)
final ListConfigNode<String> SPAWNING_MONSTERS_EXCEPTIONS = node(ListConfigNode
.listBuilder("spawning.monsters.exceptions", String.class)
.defaultValue(new ArrayList<>())
.name("spawning-monsters-exceptions")
.build());
final ConfigNode<List> WORLD_BLACKLIST = node(ConfigNode.builder("world-blacklist", List.class)
.defaultValue(new ArrayList<>())
final ListConfigNode<String> WORLD_BLACKLIST = node(ListConfigNode.listBuilder("world-blacklist", String.class)
.build());
final ConfigNode<Double> VERSION = node(ConfigNode.builder("version", Double.class)
@@ -21,7 +21,6 @@ import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.world.helpers.EnforcementHandler;
/**
* Manages the worlds.yml file.
@@ -35,14 +34,14 @@ public final class WorldsConfigManager {
private final File worldConfigFile;
private YamlConfiguration worldsConfig;
private final EnforcementHandler enforcementHandler;
private final MultiverseCore multiverseCore;
@Inject
WorldsConfigManager(@NotNull MultiverseCore core, @NotNull EnforcementHandler enforcementHandler) {
WorldsConfigManager(@NotNull MultiverseCore core, @NotNull MultiverseCore multiverseCore) {
worldConfigMap = new HashMap<>();
worldConfigFile = core.getDataFolder().toPath().resolve(CONFIG_FILENAME).toFile();
this.enforcementHandler = enforcementHandler;
this.multiverseCore = multiverseCore;
}
/**
@@ -128,7 +127,7 @@ public final class WorldsConfigManager {
WorldConfig newWorldConfig = new WorldConfig(
worldName,
getWorldConfigSection(worldName),
enforcementHandler);
multiverseCore);
worldConfigMap.put(worldName, newWorldConfig);
newWorldsAdded.add(newWorldConfig);
});
@@ -183,7 +182,7 @@ public final class WorldsConfigManager {
if (worldConfigMap.containsKey(worldName)) {
throw new IllegalArgumentException("WorldConfig for world " + worldName + " already exists.");
}
WorldConfig worldConfig = new WorldConfig(worldName, getWorldConfigSection(worldName), enforcementHandler);
WorldConfig worldConfig = new WorldConfig(worldName, getWorldConfigSection(worldName), multiverseCore);
worldConfigMap.put(worldName, worldConfig);
return worldConfig;
}
@@ -10,6 +10,7 @@ import org.bukkit.GameRule;
import org.bukkit.World;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.configuration.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
/**
@@ -90,7 +91,8 @@ public interface DataStore<T> {
@Override
public WorldConfigStore copyFrom(LoadedMultiverseWorld world) {
this.configMap = new HashMap<>();
world.getConfigurablePropertyNames().forEach(name -> world.getProperty(name)
StringPropertyHandle worldPropertyHandler = world.getStringPropertyHandle();
worldPropertyHandler.getAllPropertyNames().forEach(name -> worldPropertyHandler.getProperty(name)
.peek(value -> configMap.put(name, value)).onFailure(e -> {
Logging.warning("Failed to get property " + name + " from world "
+ world.getName() + ": " + e.getMessage());
@@ -106,12 +108,11 @@ public interface DataStore<T> {
if (configMap == null) {
return this;
}
configMap.forEach((name, value) -> {
world.setProperty(name, value).onFailure(e -> {
Logging.warning("Failed to set property %s to %s for world %s: %s",
name, value, world.getName(), e.getMessage());
});
});
StringPropertyHandle worldPropertyHandler = world.getStringPropertyHandle();
configMap.forEach((name, value) -> worldPropertyHandler.setProperty(name, value).onFailure(e -> {
Logging.warning("Failed to set property %s to %s for world %s: %s",
name, value, world.getName(), e.getMessage());
}));
return this;
}
}
@@ -2,7 +2,6 @@ package org.mvplugins.multiverse.core.world.helpers;
import java.util.List;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
import org.bukkit.Location;
@@ -11,7 +10,10 @@ import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.api.SafeTTeleporter;
import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter;
import org.mvplugins.multiverse.core.teleportation.TeleportResult;
import org.mvplugins.multiverse.core.utils.result.Async;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
/**
@@ -19,10 +21,10 @@ import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
*/
@Service
public class PlayerWorldTeleporter {
private final SafeTTeleporter safetyTeleporter;
private final AsyncSafetyTeleporter safetyTeleporter;
@Inject
PlayerWorldTeleporter(@NotNull SafeTTeleporter safetyTeleporter) {
PlayerWorldTeleporter(@NotNull AsyncSafetyTeleporter safetyTeleporter) {
this.safetyTeleporter = safetyTeleporter;
}
@@ -30,11 +32,12 @@ public class PlayerWorldTeleporter {
* Removes all players from the given world.
*
* @param world The world to remove all players from.
* @return A list of async futures that represent the teleportation result of each player.
*/
public void removeFromWorld(@NotNull LoadedMultiverseWorld world) {
public Async<List<Attempt<Void, TeleportResult.Failure>>> removeFromWorld(@NotNull LoadedMultiverseWorld world) {
// TODO: Better handling of fallback world
World toWorld = Bukkit.getWorlds().get(0);
transferFromWorldTo(world, toWorld);
return transferFromWorldTo(world, toWorld);
}
/**
@@ -42,9 +45,12 @@ public class PlayerWorldTeleporter {
*
* @param from The world to transfer players from.
* @param to The location to transfer players to.
* @return A list of async futures that represent the teleportation result of each player.
*/
public void transferFromWorldTo(@NotNull LoadedMultiverseWorld from, @NotNull LoadedMultiverseWorld to) {
transferAllFromWorldToLocation(from, to.getSpawnLocation());
public Async<List<Attempt<Void, TeleportResult.Failure>>> transferFromWorldTo(
@NotNull LoadedMultiverseWorld from,
@NotNull LoadedMultiverseWorld to) {
return transferAllFromWorldToLocation(from, to.getSpawnLocation());
}
/**
@@ -52,24 +58,28 @@ public class PlayerWorldTeleporter {
*
* @param from The world to transfer players from.
* @param to The world to transfer players to.
* @return A list of async futures that represent the teleportation result of each player.
*/
public void transferFromWorldTo(@NotNull LoadedMultiverseWorld from, @NotNull World to) {
transferAllFromWorldToLocation(from, to.getSpawnLocation());
public Async<List<Attempt<Void, TeleportResult.Failure>>> transferFromWorldTo(
@NotNull LoadedMultiverseWorld from,
@NotNull World to) {
return transferAllFromWorldToLocation(from, to.getSpawnLocation());
}
/**
* Transfers all players from the given world to the given location.
*
* @param world The world to transfer players from.
* @param location The location to transfer players to.
* @param world The world to transfer players from.
* @param location The location to transfer players to.
* @return A list of async futures that represent the teleportation result of each player.
*/
public void transferAllFromWorldToLocation(@NotNull LoadedMultiverseWorld world, @NotNull Location location) {
world.getPlayers().peek(players -> players.forEach(player -> {
if (player.isOnline()) {
Logging.fine("Teleporting player '%s' to world spawn: %s", player.getName(), location);
safetyTeleporter.safelyTeleport(null, player, location, true);
}
}));
public Async<List<Attempt<Void, TeleportResult.Failure>>> transferAllFromWorldToLocation(
@NotNull LoadedMultiverseWorld world,
@NotNull Location location) {
return world.getPlayers()
.map(players -> safetyTeleporter.teleport(players, location))
.getOrElse(() -> Async.failedFuture(
new IllegalStateException("Unable to get players from world" + world.getName())));
}
/**
@@ -77,13 +87,12 @@ public class PlayerWorldTeleporter {
*
* @param players The players to teleport.
* @param world The world to teleport players to.
* @return A list of async futures that represent the teleportation result of each player.
*/
public void teleportPlayersToWorld(@NotNull List<Player> players, @NotNull LoadedMultiverseWorld world) {
players.forEach(player -> {
Location spawnLocation = world.getSpawnLocation();
if (player.isOnline()) {
safetyTeleporter.safelyTeleport(null, player, spawnLocation, true);
}
});
public Async<List<Attempt<Void, TeleportResult.Failure>>> teleportPlayersToWorld(
@NotNull List<Player> players,
@NotNull LoadedMultiverseWorld world) {
Location spawnLocation = world.getSpawnLocation();
return safetyTeleporter.teleport(players, spawnLocation);
}
}
@@ -18,7 +18,6 @@ public class UnloadWorldOptions {
}
private final LoadedMultiverseWorld world;
private boolean removePlayers = false;
private boolean saveBukkitWorld = true;
UnloadWorldOptions(LoadedMultiverseWorld world) {
@@ -34,26 +33,6 @@ public class UnloadWorldOptions {
return world;
}
/**
* Sets whether to teleport the players out from the world before unloading.
*
* @param removePlayersInput Whether to remove players from the world before unloading.
* @return This {@link UnloadWorldOptions} instance.
*/
public UnloadWorldOptions removePlayers(boolean removePlayersInput) {
this.removePlayers = removePlayersInput;
return this;
}
/**
* Gets whether to teleport the players out from the world before unloading.
*
* @return Whether to remove players from the world before unloading.
*/
public boolean removePlayers() {
return removePlayers;
}
/**
* Sets whether to save the bukkit world before unloading.
*
@@ -60,20 +60,33 @@ mv-core.delete.success=&aWorld '{world}' deleted!
mv-core.dumps.description=Dumps version info to the console or paste services
mv-core.dumps.url.list={service} : {link}
# /mv gamerule
mv-core.gamerule.description=Changes a gamerule in one or more worlds
mv-core.gamerule.gamerule.description=Gamerule to set
mv-core.gamerule.value.description=Value of gamerule
mv-core.gamerule.world.description=World to apply gamerule to, current world by default
mv-core.gamerule.failed=Failed to set gamerule {gamerule} to {value} in {world}. &fIt should be a {type}.
mv-core.gamerule.success.single=&aSuccessfully set {gamerule} to {value} in {world}.
mv-core.gamerule.success.multiple=&aSuccessfully set {gamerule} to {value} in {count} worlds.
# /mv gamerule set
mv-core.gamerule.set.description=Changes a gamerule in one or more worlds.
mv-core.gamerule.set.gamerule.description=Gamerule to set.
mv-core.gamerule.set.value.description=Value of gamerule.
mv-core.gamerule.set.world.description=World to apply gamerule to, current player's world by default.
mv-core.gamerule.set.failed=Failed to set gamerule {gamerule} to {value} in {world}. &fIt should be a {type}.
mv-core.gamerule.set.success.single=&aSuccessfully set {gamerule} to {value} in {world}.
mv-core.gamerule.set.success.multiple=&aSuccessfully set {gamerule} to {value} in {count} worlds.
# /mv gamerules
mv-core.gamerules.description=Lists gamerules for the specified world
mv-core.gamerules.description.page=The page to view
mv-core.gamerules.description.world=The world to list gamerules in
mv-core.gamerules.title= --- Gamerules for {world} ---
# /mv gamerule reset
mv-core.gamerule.reset.description=Resets a gamerule in one or more worlds to it's default value.
mv-core.gamerule.reset.gamerule.description=Gamerule to reset.
mv-core.gamerule.reset.world.description=World to reset gamerule of, current player's world by default.
mv-core.gamerule.reset.failed=Failed to reset gamerule {gamerule} in {world}.
mv-core.gamerule.reset.success.single=&aSuccessfully reset {gamerule} in {world}.
mv-core.gamerule.reset.success.multiple=&aSuccessfully reset {gamerule} in {count} worlds.
# /mv gamerule list
mv-core.gamerule.list.description=Lists gamerules for the specified world.
mv-core.gamerule.list.description.page=The page to view.
mv-core.gamerule.list.description.world=The world to list gamerules in.
mv-core.gamerule.list.title= --- Gamerules for {world} ---
# /mv generators
mv-core.generators.description=Lists generators known to Multiverse
mv-core.generators.description.flags=Filter - only shows entries matching this. Page - the page to show
mv-core.generators.empty=&cNo Generator Plugins found.
# /mv import
mv-core.import.description=Imports an existing world folder.
@@ -126,6 +139,12 @@ mv-core.unload.success=&aWorld '{world}' unloaded!
# /mv usage
mv-core.usage.description=Show Multiverse-Core command usage.
# /mv version
mv-core.version.description=Displays version and authors
mv-core.version.mv=Multiverse Core Version &fv{version}
mv-core.version.authors=Multiverse Core Authors &f{authors}
mv-core.version.secretcode=Special Code: &fFRN002
# commands error
mv-core.commands.error.playersonly=&cThis command can only be used by players
mv-core.commands.error.multiverseworldonly=&cThis can only be used in multiverse worlds
+1 -1
View File
@@ -1,6 +1,6 @@
name: Multiverse-Core
main: org.mvplugins.multiverse.core.MultiverseCore
authors: ['dumptruckman', 'Rigby', 'fernferret', 'lithium3141', 'main--']
authors: ['dumptruckman', 'Rigby', 'fernferret', 'lithium3141', 'main--', 'benwoo1110', 'Zax71']
website: 'https://dev.bukkit.org/projects/multiverse-core'
softdepend: ['Vault', 'PlaceholderAPI']
api-version: 1.13
@@ -52,14 +52,14 @@ class ConfigTest : TestWithMockBukkit() {
@Test
fun `Getting existing config property with getProperty returns expected value`() {
assertEquals(false, config.getProperty("enforce-access").get())
assertEquals("world", config.getProperty("first-spawn-location").get())
assertEquals(false, config.stringPropertyHandle.getProperty("enforce-access").get())
assertEquals("world", config.stringPropertyHandle.getProperty("first-spawn-location").get())
}
@Test
fun `Getting non-existing config property with getProperty returns null`() {
assertTrue(config.getProperty("invalid-property").isFailure)
assertTrue(config.getProperty("version").isFailure)
assertTrue(config.stringPropertyHandle.getProperty("invalid-property").isFailure)
assertTrue(config.stringPropertyHandle.getProperty("version").isFailure)
}
@Test
@@ -70,19 +70,31 @@ class ConfigTest : TestWithMockBukkit() {
@Test
fun `Updating an existing config property with setProperty reflects the changes in getProperty`() {
assertTrue(config.setProperty("enforce-access", true).isSuccess)
assertEquals(true, config.getProperty("enforce-access").get())
assertTrue(config.stringPropertyHandle.setProperty("enforce-access", true).isSuccess)
assertEquals(true, config.stringPropertyHandle.getProperty("enforce-access").get())
assertTrue(config.setProperty("first-spawn-location", "world2").isSuccess)
assertEquals("world2", config.getProperty("first-spawn-location").get())
assertTrue(config.stringPropertyHandle.setProperty("first-spawn-location", "world2").isSuccess)
assertEquals("world2", config.stringPropertyHandle.getProperty("first-spawn-location").get())
assertTrue(config.setProperty("global-debug", 1).isSuccess)
assertEquals(1, config.getProperty("global-debug").get())
assertTrue(config.stringPropertyHandle.setProperty("global-debug", 1).isSuccess)
assertEquals(1, config.stringPropertyHandle.getProperty("global-debug").get())
}
@Test
fun `Updating an existing config property with setPropertyString reflects the changes in getProperty`() {
assertTrue(config.stringPropertyHandle.setPropertyString("enforce-access", "true").isSuccess)
assertEquals(true, config.stringPropertyHandle.getProperty("enforce-access").get())
assertTrue(config.stringPropertyHandle.setPropertyString("first-spawn-location", "world2").isSuccess)
assertEquals("world2", config.stringPropertyHandle.getProperty("first-spawn-location").get())
assertTrue(config.stringPropertyHandle.setPropertyString("global-debug", "1").isSuccess)
assertEquals(1, config.stringPropertyHandle.getProperty("global-debug").get())
}
@Test
fun `Updating a non-existing property with setProperty returns false`() {
assertTrue(config.setProperty("invalid-property", false).isFailure)
assertTrue(config.setProperty("version", 1.1).isFailure)
assertTrue(config.stringPropertyHandle.setProperty("invalid-property", false).isFailure)
assertTrue(config.stringPropertyHandle.setProperty("version", 1.1).isFailure)
}
}
@@ -5,18 +5,16 @@ import org.mvplugins.multiverse.core.anchor.AnchorManager
import org.mvplugins.multiverse.core.api.BlockSafety
import org.mvplugins.multiverse.core.api.Destination
import org.mvplugins.multiverse.core.api.LocationManipulation
import org.mvplugins.multiverse.core.api.SafeTTeleporter
import org.mvplugins.multiverse.core.commandtools.MVCommandManager
import org.mvplugins.multiverse.core.commandtools.MultiverseCommand
import org.mvplugins.multiverse.core.commandtools.PluginLocales
import org.mvplugins.multiverse.core.config.MVCoreConfig
import org.mvplugins.multiverse.core.economy.MVEconomist
import org.mvplugins.multiverse.core.listeners.*
import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter
import org.mvplugins.multiverse.core.teleportation.SimpleBlockSafety
import org.mvplugins.multiverse.core.teleportation.SimpleLocationManipulation
import org.mvplugins.multiverse.core.teleportation.SimpleSafeTTeleporter
import org.mvplugins.multiverse.core.teleportation.TeleportQueue
import org.mvplugins.multiverse.core.utils.UnsafeCallWrapper
import org.mvplugins.multiverse.core.utils.metrics.MetricsConfigurator
import org.mvplugins.multiverse.core.world.WorldManager
import kotlin.test.*
@@ -28,6 +26,11 @@ class InjectionTest : TestWithMockBukkit() {
assertNotNull(multiverseCore.getService(AnchorManager::class.java))
}
@Test
fun `AsyncSafetyTeleporter is available as a service`() {
assertNotNull(multiverseCore.getService(AsyncSafetyTeleporter::class.java))
}
@Test
fun `BlockSafety is available as a service`() {
assertNotNull(multiverseCore.getService(BlockSafety::class.java))
@@ -50,23 +53,11 @@ class InjectionTest : TestWithMockBukkit() {
assertNotNull(multiverseCore.getService(SimpleLocationManipulation::class.java))
}
@Test
fun `SafeTTeleporter is available as a service`() {
assertNotNull(multiverseCore.getService(SafeTTeleporter::class.java))
assertNotNull(multiverseCore.getService(SimpleSafeTTeleporter::class.java))
}
@Test
fun `TeleportQueue is available as a service`() {
assertNotNull(multiverseCore.getService(TeleportQueue::class.java))
}
@Test
@Ignore
fun `UnsafeCallWrapper is available as a service`() {
assertNotNull(multiverseCore.getService(UnsafeCallWrapper::class.java))
}
@Test
fun `MVWorldManager is available as a service`() {
assertNotNull(multiverseCore.getService(WorldManager::class.java))
@@ -68,9 +68,9 @@ class WorldConfigMangerTest : TestWithMockBukkit() {
val worldConfig = worldConfigManager.getWorldConfig("world").orNull
assertNotNull(worldConfig)
worldConfig.setProperty("adjust-spawn", true)
worldConfig.setProperty("alias", "newalias")
worldConfig.setProperty("spawn-location", SpawnLocation(-64.0, 64.0, 48.0))
worldConfig.stringPropertyHandle.setProperty("adjust-spawn", true)
worldConfig.stringPropertyHandle.setProperty("alias", "newalias")
worldConfig.stringPropertyHandle.setProperty("spawn-location", SpawnLocation(-64.0, 64.0, 48.0))
assertTrue(worldConfigManager.save().isSuccess)
}
@@ -30,14 +30,14 @@ class WorldConfigTest : TestWithMockBukkit() {
@Test
fun `Getting existing world property with getProperty returns expected value`() {
assertEquals("my world", worldConfig.getProperty("alias").get())
assertEquals(false, worldConfig.getProperty("hidden").get())
assertEquals("my world", worldConfig.stringPropertyHandle.getProperty("alias").get())
assertEquals(false, worldConfig.stringPropertyHandle.getProperty("hidden").get())
}
@Test
fun `Getting non-existing world property with getProperty returns null`() {
assertTrue(worldConfig.getProperty("invalid-property").isFailure)
assertTrue(worldConfig.getProperty("version").isFailure)
assertTrue(worldConfig.stringPropertyHandle.getProperty("invalid-property").isFailure)
assertTrue(worldConfig.stringPropertyHandle.getProperty("version").isFailure)
}
@Test
@@ -48,23 +48,23 @@ class WorldConfigTest : TestWithMockBukkit() {
@Test
fun `Updating an existing world property with setProperty reflects the changes in getProperty`() {
assertTrue(worldConfig.setProperty("adjust-spawn", true).isSuccess)
assertEquals(true, worldConfig.getProperty("adjust-spawn").get())
assertTrue(worldConfig.stringPropertyHandle.setProperty("adjust-spawn", true).isSuccess)
assertEquals(true, worldConfig.stringPropertyHandle.getProperty("adjust-spawn").get())
assertTrue(worldConfig.setProperty("alias", "abc").isSuccess)
assertEquals("abc", worldConfig.getProperty("alias").get())
assertTrue(worldConfig.stringPropertyHandle.setProperty("alias", "abc").isSuccess)
assertEquals("abc", worldConfig.stringPropertyHandle.getProperty("alias").get())
assertTrue(worldConfig.setProperty("scale", 2.0).isSuccess)
assertEquals(2.0, worldConfig.getProperty("scale").get())
assertTrue(worldConfig.stringPropertyHandle.setProperty("scale", 2.0).isSuccess)
assertEquals(2.0, worldConfig.stringPropertyHandle.getProperty("scale").get())
val blacklists = listOf("a", "b", "c")
assertTrue(worldConfig.setProperty("world-blacklist", blacklists).isSuccess)
assertEquals(blacklists, worldConfig.getProperty("world-blacklist").get())
assertTrue(worldConfig.stringPropertyHandle.setProperty("world-blacklist", blacklists).isSuccess)
assertEquals(blacklists, worldConfig.stringPropertyHandle.getProperty("world-blacklist").get())
}
@Test
fun `Updating a non-existing property with setProperty returns false`() {
assertTrue(worldConfig.setProperty("invalid-property", false).isFailure)
assertTrue(worldConfig.setProperty("version", 1.1).isFailure)
assertTrue(worldConfig.stringPropertyHandle.setProperty("invalid-property", false).isFailure)
assertTrue(worldConfig.stringPropertyHandle.setProperty("version", 1.1).isFailure)
}
}
@@ -34,9 +34,9 @@ class WorldManagerTest : TestWithMockBukkit() {
val world = worldManager.getLoadedWorld("world_nether").get()
assertNotNull(world)
assertEquals("world_nether", world.name)
assertEquals(World.Environment.NETHER, world.getProperty("environment").get())
assertEquals("", world.getProperty("generator").get())
assertEquals(1234L, world.getProperty("seed").get())
assertEquals(World.Environment.NETHER, world.environment)
assertEquals("", world.generator)
assertEquals(1234L, world.seed)
}
@Test