Implement support for property modification with ConfigModifyType and ListValueNode

This commit is contained in:
Ben Woo 2023-09-13 18:21:14 +08:00
parent 6f014838e9
commit 94b1e77a98
No known key found for this signature in database
GPG Key ID: FB2A3645536E12C8
17 changed files with 727 additions and 74 deletions

View File

@ -0,0 +1,73 @@
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.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.commandtools.context.WorldConfigValue;
import org.mvplugins.multiverse.core.configuration.handle.ConfigModifyType;
import org.mvplugins.multiverse.core.worldnew.MultiverseWorld;
import org.mvplugins.multiverse.core.worldnew.WorldManager;
@Service
@CommandAlias("mv")
class ModifyCommand extends MultiverseCommand {
private final WorldManager worldManager;
@Inject
ModifyCommand(@NotNull MVCommandManager commandManager, WorldManager worldManager) {
super(commandManager);
this.worldManager = worldManager;
}
/* /mv modify [world] <set|add|remove|reset> <property> <value> */
@Subcommand("modify")
@CommandPermission("multiverse.core.modify")
@CommandCompletion("@mvworlds:scope=both @configmodifytype @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("")
ConfigModifyType configModifyType,
@Syntax("<property>")
@Description("")
String propertyName,
@Syntax("[value]")
@Description("")
WorldConfigValue propertyValue) {
Logging.fine("ModifyCommand.onModifyCommand: world=%s, configModifyType=%s, propertyName=%s, propertyValue=%s",
world, configModifyType, propertyName, propertyValue);
world.modifyProperty(configModifyType, propertyName, propertyValue.getValue()).onSuccess(ignore -> {
issuer.sendMessage("Property " + propertyName + " set to " + world.getProperty(propertyName).getOrNull()
+ " for world " + world.getName() + ".");
worldManager.saveWorldsConfig();
}).onFailure(exception -> {
issuer.sendMessage("Failed to " + configModifyType.name().toLowerCase() + " property " + propertyName
+ " to " + propertyValue.getValue() + " for world " + world.getName() + ".");
issuer.sendMessage(exception.getMessage());
});
}
}

View File

@ -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;
@ -19,10 +20,12 @@ import com.google.common.collect.Sets;
import io.vavr.control.Try;
import jakarta.inject.Inject;
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.ConfigModifyType;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.ParsedDestination;
import org.mvplugins.multiverse.core.worldnew.LoadedMultiverseWorld;
@ -37,27 +40,32 @@ public class MVCommandCompletions extends PaperCommandCompletions {
private final DestinationsProvider destinationsProvider;
@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;
registerAsyncCompletion("commands", this::suggestCommands);
registerStaticCompletion("configmodifytype", suggestEnums(ConfigModifyType.class));
registerAsyncCompletion("destinations", this::suggestDestinations);
registerStaticCompletion("environments", suggestEnums(World.Environment.class));
registerAsyncCompletion("flags", this::suggestFlags);
registerStaticCompletion("gamerules", this::suggestGamerules);
registerStaticCompletion("mvconfigs", config.getNodes().getNames());
registerAsyncCompletion("mvworlds", this::suggestMVWorlds);
registerAsyncCompletion("mvworldpropsname", this::suggestMVWorldPropsName);
registerAsyncCompletion("mvworldpropsvalue", this::suggestMVWorldPropsValue);
setDefaultCompletion("configmodifytype", ConfigModifyType.class);
setDefaultCompletion("destinations", ParsedDestination.class);
setDefaultCompletion("flags", String[].class);
setDefaultCompletion("environments", World.Environment.class);
setDefaultCompletion("gamerules", GameRule.class);
setDefaultCompletion("mvworlds", MultiverseWorld.class);
setDefaultCompletion("mvworlds", LoadedMultiverseWorld.class);
}
@ -152,4 +160,41 @@ public class MVCommandCompletions extends PaperCommandCompletions {
Logging.severe("Invalid MVWorld scope: " + scope);
return Collections.emptyList();
}
private Collection<String> suggestMVWorldPropsName(BukkitCommandCompletionContext context) {
return Try.of(() -> {
MultiverseWorld mvWorld = context.getContextValue(MultiverseWorld.class);
ConfigModifyType modifyType = context.getContextValue(ConfigModifyType.class);
return mvWorld.getConfigurablePropertyNames(modifyType);
}).getOrElse(Collections.emptyList());
}
private Collection<String> suggestMVWorldPropsValue(BukkitCommandCompletionContext context) {
//noinspection unchecked
return Try.of(() -> {
MultiverseWorld mvWorld = context.getContextValue(MultiverseWorld.class);
String propertyName = context.getContextValue(String.class);
Class type = mvWorld.getPropertyType(propertyName).get();
if (type.isEnum()) {
return suggestEnums(type);
}
if (type == Boolean.class) {
return List.of("true", "false");
}
if (type == Integer.class) {
return List.of("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
}
if (type == Double.class) {
return List.of("0.0", "1.0", "2.0", "3.0", "4.0", "5.0");
}
return Collections.emptyList();
}).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();
}
}

View File

@ -1,6 +1,9 @@
package org.mvplugins.multiverse.core.commandtools;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import co.aikar.commands.BukkitCommandExecutionContext;
@ -8,8 +11,10 @@ import co.aikar.commands.BukkitCommandIssuer;
import co.aikar.commands.InvalidCommandArgument;
import co.aikar.commands.PaperCommandContexts;
import co.aikar.commands.contexts.ContextResolver;
import com.dumptruckman.minecraft.util.Logging;
import com.google.common.base.Strings;
import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.GameRule;
import org.bukkit.entity.Player;
@ -17,7 +22,9 @@ 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.commandtools.context.WorldConfigValue;
import org.mvplugins.multiverse.core.config.MVCoreConfig;
import org.mvplugins.multiverse.core.configuration.handle.ConfigModifyType;
import org.mvplugins.multiverse.core.configuration.node.Node;
import org.mvplugins.multiverse.core.configuration.node.ValueNode;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
@ -27,10 +34,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.worldnew.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.worldnew.MultiverseWorld;
import org.mvplugins.multiverse.core.worldnew.WorldManager;
@Service
public class MVCommandContexts extends PaperCommandContexts {
class MVCommandContexts extends PaperCommandContexts {
private final MVCommandManager mvCommandManager;
private final DestinationsProvider destinationsProvider;
@ -38,12 +46,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;
@ -57,10 +64,12 @@ public class MVCommandContexts extends PaperCommandContexts {
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(MultiverseWorld.class, this::parseWorld);
registerIssuerAwareContext(LoadedMultiverseWorld.class, this::parseLoadedWorld);
registerIssuerAwareContext(LoadedMultiverseWorld[].class, this::parseLoadedWorldArray);
registerIssuerAwareContext(Player.class, this::parsePlayer);
registerIssuerAwareContext(Player[].class, this::parsePlayerArray);
registerIssuerAwareContext(WorldConfigValue.class, this::parseWorldConfigValue);
}
private MVCommandIssuer parseMVCommandIssuer(BukkitCommandExecutionContext context) {
@ -162,13 +171,14 @@ public class MVCommandContexts extends PaperCommandContexts {
return new MVConfigValue(resolvedValue);
}
private LoadedMultiverseWorld parseMVWorld(BukkitCommandExecutionContext context) {
private MultiverseWorld parseWorld(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
// Get world based on sender only
if (resolve.equals("issuerOnly")) {
if (context.getIssuer().isPlayer()) {
return worldManager.getLoadedWorld(context.getIssuer().getPlayer().getWorld()).getOrNull();
return worldManager.getWorld(context.getIssuer().getPlayer().getWorld())
.getOrElseThrow(() -> new InvalidCommandArgument("Player is not in a Multiverse World."));
}
if (context.isOptional()) {
return null;
@ -177,7 +187,7 @@ public class MVCommandContexts extends PaperCommandContexts {
}
String worldName = context.getFirstArg();
LoadedMultiverseWorld world = worldManager.getLoadedWorld(worldName).getOrNull();
MultiverseWorld world = worldManager.getWorld(worldName).getOrNull();
// Get world based on input, fallback to sender if input is not a world
if (resolve.equals("issuerAware")) {
@ -186,7 +196,8 @@ public class MVCommandContexts extends PaperCommandContexts {
return world;
}
if (context.getIssuer().isPlayer()) {
return worldManager.getLoadedWorld(context.getIssuer().getPlayer().getWorld()).getOrNull();
return worldManager.getWorld(context.getIssuer().getPlayer().getWorld())
.getOrElseThrow(() -> new InvalidCommandArgument("Player is not in a Multiverse World."));
}
if (context.isOptional()) {
return null;
@ -205,7 +216,53 @@ public class MVCommandContexts extends PaperCommandContexts {
throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world.");
}
private LoadedMultiverseWorld[] parseMVWorldArray(BukkitCommandExecutionContext context) {
private LoadedMultiverseWorld parseLoadedWorld(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
// Get world based on sender only
if (resolve.equals("issuerOnly")) {
if (context.getIssuer().isPlayer()) {
return worldManager.getLoadedWorld(context.getIssuer().getPlayer().getWorld())
.getOrElseThrow(() -> new InvalidCommandArgument("Player is not in a Multiverse World."));
}
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();
LoadedMultiverseWorld world = worldManager.getLoadedWorld(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.getLoadedWorld(context.getIssuer().getPlayer().getWorld())
.getOrElseThrow(() -> new InvalidCommandArgument("Player is not in a Multiverse World."));
}
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 LoadedMultiverseWorld[] parseLoadedWorldArray(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
LoadedMultiverseWorld playerWorld = null;
@ -309,7 +366,6 @@ public class MVCommandContexts extends PaperCommandContexts {
throw new InvalidCommandArgument("Player " + playerIdentifier + " not found.");
}
private Player[] parsePlayerArray(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");
@ -353,4 +409,50 @@ public class MVCommandContexts extends PaperCommandContexts {
}
throw new InvalidCommandArgument("Player " + playerIdentifier + " not found.");
}
private WorldConfigValue parseWorldConfigValue(BukkitCommandExecutionContext context) {
MultiverseWorld mvWorld = (MultiverseWorld) context.getResolvedArg(MultiverseWorld.class);
ConfigModifyType modifyType = (ConfigModifyType) context.getResolvedArg(ConfigModifyType.class);
String propertyName = (String) context.getResolvedArg(String.class);
if (mvWorld == null || modifyType == null || propertyName == null) {
throw new InvalidCommandArgument("No world or property specified.");
}
if (modifyType == ConfigModifyType.RESET) {
if (context.popFirstArg() != null) {
throw new InvalidCommandArgument("No value should be specified for reset.");
}
return new WorldConfigValue(null);
}
Class<?> type = mvWorld.getPropertyType(propertyName)
.getOrElseThrow(() -> {
return new InvalidCommandArgument("The property " + propertyName + " is not valid for world "
+ mvWorld.getName() + ".");
});
return new WorldConfigValue(parseType(context, type));
}
private Object parseType(BukkitCommandExecutionContext context, Class<?> type) {
Object value = context.getFirstArg();
if (value == null) {
throw new InvalidCommandArgument("No value specified.");
}
// Special case for enums
if (type.isEnum()) {
return Try.of(() -> Enum.valueOf((Class<? extends Enum>) type, context.popFirstArg().toUpperCase()))
.getOrElseThrow(() -> new InvalidCommandArgument(("The value %s is not a valid %s. "
+ "Valid values are: %s").formatted(
value,
type.getSimpleName(),
Arrays.stream(((Class<? extends Enum>) type).getEnumConstants())
.map(enumValue -> enumValue.name().toLowerCase())
.reduce((a, b) -> a + ", " + b)
.orElse(""))));
}
ContextResolver<?, BukkitCommandExecutionContext> resolver = getResolver(type);
return resolver == null ? value : resolver.getContext(context);
}
}

View File

@ -12,7 +12,7 @@ import org.mvplugins.multiverse.core.commandtools.flags.CommandFlagsManager;
import org.mvplugins.multiverse.core.commandtools.flags.ParsedCommandFlags;
@Contract
public abstract class MultiverseCommand extends BaseCommand {
public abstract class MultiverseCommand extends BaseCommand {
protected final MVCommandManager commandManager;
private String flagGroupName;

View File

@ -0,0 +1,20 @@
package org.mvplugins.multiverse.core.commandtools.context;
public class WorldConfigValue {
private final Object value;
public WorldConfigValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
return "WorldConfigValue{"
+ "value=" + value
+ '}';
}
}

View File

@ -76,7 +76,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);
});
}
});

View File

@ -0,0 +1,8 @@
package org.mvplugins.multiverse.core.configuration.handle;
public enum ConfigModifyType {
SET,
ADD,
REMOVE,
RESET
}

View File

@ -1,7 +1,10 @@
package org.mvplugins.multiverse.core.configuration.handle;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.Plugin;
@ -10,6 +13,7 @@ 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;
@ -18,12 +22,12 @@ import org.mvplugins.multiverse.core.configuration.node.ValueNode;
*/
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,8 +71,36 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
});
}
public Collection<String> getNames() {
return nodes.getNames();
}
public Collection<String> getNamesThatSupports(ConfigModifyType configModifyType) {
return switch (configModifyType) {
case SET, RESET -> nodes.getNames();
case ADD, REMOVE -> nodes.stream()
.filter(node -> node instanceof ListValueNode)
.map(node -> ((ValueNode<?>) node).getName())
.filter(Option::isDefined)
.map(Option::get)
.toList();
};
}
public Try<Class> getTypeByName(@Nullable String name) {
return nodes.findNode(name, ValueNode.class)
.map(valueNode -> {
if (valueNode instanceof ListValueNode listValueNode) {
return listValueNode.getItemType();
}
return valueNode.getType();
})
.toTry(() -> new ConfigNodeNotFoundException(name));
}
/**
* 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.
*/
@ -78,17 +110,10 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
.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.
*
* @param node The node to get the value of.
* @return The value of the node.
*/
public <T> T get(@NotNull ValueNode<T> node) {
if (node.getSerializer() == null) {
return config.getObject(node.getPath(), node.getType(), node.getDefaultValue());
}
return node.getSerializer().deserialize(config.get(node.getPath(), node.getDefaultValue()), node.getType());
public Try<Void> modify(@NotNull ConfigModifyType type, @Nullable String name, @Nullable Object value) {
return nodes.findNode(name, ValueNode.class)
.toTry(() -> new ConfigNodeNotFoundException(name))
.flatMapTry(node -> modify(type, node, value));
}
/**
@ -99,9 +124,68 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
* @return True if the value was set, false otherwise.
*/
public Try<Void> set(@Nullable String name, Object value) {
//noinspection unchecked
return nodes.findNode(name, ValueNode.class)
.toTry(() -> new ConfigNodeNotFoundException(name))
.flatMap(node -> set(node, value));
.flatMapTry(node -> set(node, value));
}
public Try<Void> add(@Nullable String name, Object value) {
//noinspection unchecked
return nodes.findNode(name, ListValueNode.class)
.toTry(() -> new ConfigNodeNotFoundException(name))
.flatMapTry(node -> add(node, value));
}
public Try<Void> remove(@Nullable String name, Object value) {
//noinspection unchecked
return nodes.findNode(name, ListValueNode.class)
.toTry(() -> new ConfigNodeNotFoundException(name))
.flatMapTry(node -> remove(node, value));
}
public Try<Void> reset(@Nullable String name) {
return nodes.findNode(name, ValueNode.class)
.toTry(() -> new ConfigNodeNotFoundException(name))
.flatMapTry(this::reset);
}
/**
* Gets the value of a node, if the node has a default value, it will be returned if the node is not found.
*
* @param node The node to get the value of.
* @param <T> The type of the node value.
* @return The value of the node.
*/
public <T> T get(@NotNull ValueNode<T> node) {
if (node.getSerializer() == null) {
return config.getObject(node.getPath(), node.getType(), node.getDefaultValue());
}
return node.getSerializer().deserialize(config.get(node.getPath(), node.getDefaultValue()), node.getType());
}
public Try<Void> modify(@NotNull ConfigModifyType type, @NotNull ValueNode node, @Nullable Object value) {
return switch (type) {
case SET -> set(node, value);
case REMOVE -> {
if (!(node instanceof ListValueNode listNode)) {
yield Try.failure(new IllegalArgumentException("Node is not a list"));
}
yield remove(listNode, value);
}
case ADD -> {
if (!(node instanceof ListValueNode listNode)) {
yield Try.failure(new IllegalArgumentException("Node is not a list"));
}
yield add(listNode, value);
}
case RESET -> {
if (value != null) {
yield Try.failure(new IllegalArgumentException("Reset type cannot have a value"));
}
yield reset(node);
}
};
}
/**
@ -109,30 +193,62 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
*
* @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 -> {
T oldValue = get(node);
if (node.getSerializer() != null) {
var serialized = node.getSerializer().serialize(value, node.getType());
config.set(node.getPath(), serialized);
} else {
config.set(node.getPath(), value);
}
var serialized = node.getSerializer() != null
? node.getSerializer().serialize(value, node.getType())
: value;
config.set(node.getPath(), serialized);
node.onSetValue(oldValue, get(node));
return null;
});
}
public <I> Try<Void> add(@NotNull ListValueNode<I> node, I value) {
// TODO: Serialize value, Validate value
return Try.run(() -> {
var serialized = node.getItemSerializer() != null
? node.getItemSerializer().serialize(value, node.getItemType())
: value;
List list = get(node);
if (list == null) {
throw new IllegalArgumentException("List is null");
}
list.add(serialized);
config.set(node.getPath(), list);
node.onSetValue(list, get(node));
});
}
public <I> Try<Void> remove(@NotNull ListValueNode<I> node, I value) {
return Try.run(() -> {
var serialized = node.getItemSerializer() != null
? node.getItemSerializer().serialize(value, node.getItemType())
: value;
List list = get(node);
if (list == null) {
throw new IllegalArgumentException("List is null");
}
if (!list.remove(serialized)) {
throw new IllegalArgumentException("Value not found in list");
}
config.set(node.getPath(), list);
node.onSetItemValue(value, null);
});
}
/**
* Sets the default value of a node.
*
* @param node The node to set the default value of.
* @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()));
}
/**
@ -147,7 +263,8 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
protected @Nullable NodeGroup nodes;
protected @Nullable ConfigMigrator migrator;
protected Builder() {}
protected Builder() {
}
/**
* Sets the logger.

View File

@ -66,7 +66,7 @@ public class ConfigHeaderNode implements CommentedNode {
comment = "# " + comment;
}
comments.add(comment);
return (B) this;
return self();
}
/**
@ -77,5 +77,10 @@ public class ConfigHeaderNode implements CommentedNode {
public @NotNull ConfigHeaderNode build() {
return new ConfigHeaderNode(path, comments.toArray(new String[0]));
}
protected B self() {
//noinspection unchecked
return (B) this;
}
}
}

View File

@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core.configuration.node;
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;
@ -10,6 +11,7 @@ import org.jetbrains.annotations.Nullable;
/**
* A node that contains a value.
*
* @param <T> The type of the value.
*/
public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@ -19,19 +21,18 @@ 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 Supplier<T> defaultValueSupplier;
protected final @Nullable NodeSerializer<T> serializer;
protected final @Nullable Function<T, Try<Void>> validator;
protected final @Nullable BiConsumer<T, T> onSetValue;
@ -41,15 +42,14 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@NotNull String[] comments,
@Nullable String name,
@NotNull Class<T> type,
@Nullable T defaultValue,
@Nullable Supplier<T> defaultValueSupplier,
@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.defaultValueSupplier = defaultValueSupplier;
this.serializer = serializer;
this.validator = validator;
this.onSetValue = onSetValue;
@ -76,7 +76,7 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
*/
@Override
public @Nullable T getDefaultValue() {
return defaultValue;
return defaultValueSupplier != null ? defaultValueSupplier.get() : null;
}
public @Nullable NodeSerializer<T> getSerializer() {
@ -115,7 +115,7 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected @Nullable String name;
protected @NotNull final Class<T> type;
protected @Nullable T defaultValue;
protected @Nullable Supplier<T> defaultValueSupplier;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
@ -142,8 +142,19 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
* @return This builder.
*/
public @NotNull B defaultValue(@NotNull T defaultValue) {
this.defaultValue = defaultValue;
return (B) this;
this.defaultValueSupplier = () -> defaultValue;
return self();
}
/**
* Sets the default value for this node.
*
* @param defaultValueSupplier The supplier for the default value.
* @return This builder.
*/
public @NotNull B defaultValue(@NotNull Supplier<T> defaultValueSupplier) {
this.defaultValueSupplier = defaultValueSupplier;
return self();
}
/**
@ -154,17 +165,17 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
*/
public @NotNull B name(@Nullable String name) {
this.name = name;
return (B) this;
return self();
}
public @NotNull B serializer(@NotNull NodeSerializer<T> serializer) {
this.serializer = serializer;
return (B) this;
return self();
}
public @NotNull B validator(@NotNull Function<T, Try<Void>> validator) {
this.validator = validator;
return (B) this;
return self();
}
/**
@ -175,7 +186,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 +194,15 @@ 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,
defaultValueSupplier,
serializer,
validator,
onSetValue);
}
}
}

View File

@ -0,0 +1,190 @@
package org.mvplugins.multiverse.core.configuration.node;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import io.vavr.control.Try;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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 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 NodeSerializer<List<I>> serializer,
@Nullable Function<List<I>, Try<Void>> validator,
@Nullable BiConsumer<List<I>, List<I>> onSetValue,
@NotNull Class<I> itemType,
@Nullable NodeSerializer<I> itemSerializer,
@Nullable Function<I, Try<Void>> itemValidator,
@Nullable BiConsumer<I, I> onSetItemValue) {
super(path, comments, name, type, defaultValueSupplier, serializer, validator, onSetValue);
this.itemType = itemType;
this.itemSerializer = itemSerializer;
this.itemValidator = itemValidator;
this.onSetItemValue = onSetItemValue;
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Class<I> getItemType() {
return 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 NodeSerializer<I> itemSerializer;
protected Function<I, Try<Void>> itemValidator;
protected 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.defaultValueSupplier = () -> (List<I>) new ArrayList<Object>();
}
/**
* Sets the serializer for the node.
*
* @param serializer The serializer for the node.
* @return This builder.
*/
public @NotNull B itemSerializer(@Nullable NodeSerializer<I> serializer) {
this.itemSerializer = serializer;
return self();
}
/**
* Sets the validator for the node.
*
* @param validator The validator for the node.
* @return This builder.
*/
public @NotNull B itemValidator(@Nullable Function<I, Try<Void>> validator) {
this.itemValidator = validator;
if (validator == null) {
setDefaultValidator();
}
return self();
}
private void setDefaultValidator() {
this.validator = value -> {
if (value != null) {
return Try.sequence(value.stream().map(itemValidator).toList()).map(v -> null);
}
return Try.success(null);
};
}
/**
* Sets the onSetValue for the node.
*
* @param onSetValue The onSetValue for the node.
* @return This builder.
*/
public @NotNull B onSetItemValue(@Nullable BiConsumer<I, I> onSetValue) {
this.onSetItemValue = onSetValue;
if (onSetValue == null) {
setDefaultOnSetValue();
}
return self();
}
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 ListConfigNode<I> build() {
return new ListConfigNode<>(
path,
comments.toArray(new String[0]),
name,
type,
defaultValueSupplier,
serializer,
validator,
onSetValue,
itemType,
itemSerializer,
itemValidator,
onSetItemValue);
}
}
}

View File

@ -0,0 +1,40 @@
package org.mvplugins.multiverse.core.configuration.node;
import java.util.List;
import io.vavr.control.Try;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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();
/**
* 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);
}

View File

@ -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.
@ -49,10 +50,6 @@ public class NodeGroup implements Collection<Node> {
return nodesMap.keySet();
}
public Map<String, Node> getNodesMap() {
return nodesMap;
}
/**
* Gets the node with the given name.
*
@ -68,9 +65,10 @@ public class NodeGroup implements Collection<Node> {
*
* @param name The name of the node to get.
* @param type The type of the node to get.
* @param <T> The type of the 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);
}

View File

@ -13,6 +13,7 @@ import org.bukkit.Material;
import org.bukkit.World;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.handle.ConfigModifyType;
import org.mvplugins.multiverse.core.world.configuration.AllowedPortalType;
import org.mvplugins.multiverse.core.worldnew.config.WorldConfig;
@ -66,6 +67,14 @@ public class MultiverseWorld {
return worldConfig.getConfigurablePropertyNames();
}
public Collection<String> getConfigurablePropertyNames(ConfigModifyType configModifyType) {
return worldConfig.getConfigurablePropertyNames(configModifyType);
}
public Try<Class> getPropertyType(String name) {
return worldConfig.getPropertyType(name);
}
/**
* Gets a property on this world.
*
@ -87,6 +96,10 @@ public class MultiverseWorld {
return worldConfig.setProperty(name, value);
}
public Try<Void> modifyProperty(ConfigModifyType type, String name, Object value) {
return worldConfig.modifyProperty(type, name, value);
}
/**
* Gets whether or not Multiverse should auto-adjust the spawn for this world.
*

View File

@ -737,6 +737,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 world == null ? Option.none() : getWorld(world.getName());
}
/**
* 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.

View File

@ -14,6 +14,7 @@ import org.bukkit.configuration.ConfigurationSection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.handle.ConfigModifyType;
import org.mvplugins.multiverse.core.configuration.handle.ConfigurationSectionHandle;
import org.mvplugins.multiverse.core.configuration.migration.BooleanMigratorAction;
import org.mvplugins.multiverse.core.configuration.migration.ConfigMigrator;
@ -116,7 +117,15 @@ public final class WorldConfig {
}
public Collection<String> getConfigurablePropertyNames() {
return configNodes.getNodes().getNames();
return configHandle.getNames();
}
public Collection<String> getConfigurablePropertyNames(ConfigModifyType configModifyType) {
return configHandle.getNamesThatSupports(configModifyType);
}
public Try<Class> getPropertyType(String name) {
return configHandle.getTypeByName(name);
}
public Try<Object> getProperty(String name) {
@ -127,6 +136,10 @@ public final class WorldConfig {
return configHandle.set(name, value);
}
public Try<Void> modifyProperty(ConfigModifyType type, String name, Object value) {
return configHandle.modify(type, name, value);
}
public boolean getAdjustSpawn() {
return configHandle.get(configNodes.ADJUST_SPAWN);
}

View File

@ -11,6 +11,7 @@ import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
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.configuration.AllowedPortalType;
@ -212,9 +213,8 @@ public class WorldConfigNodes {
})
.build());
final ConfigNode<List> SPAWNING_ANIMALS_EXCEPTIONS = node(ConfigNode
.builder("spawning.animals.exceptions", List.class)
.defaultValue(new ArrayList<>())
final ListConfigNode<String> SPAWNING_ANIMALS_EXCEPTIONS = node(ListConfigNode
.listBuilder("spawning.animals.exceptions", String.class)
.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)
.defaultValue(new ArrayList<>())
final ListConfigNode<String> SPAWNING_MONSTERS_EXCEPTIONS = node(ListConfigNode
.listBuilder("spawning.monsters.exceptions", String.class)
.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)