diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/ModifyCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/ModifyCommand.java new file mode 100644 index 00000000..87a58e0b --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/commands/ModifyCommand.java @@ -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] ") + @Description("") + void onModifyCommand( + MVCommandIssuer issuer, + + @Flags("resolve=issuerAware") + @Syntax("[world]") + @Description("") + MultiverseWorld world, + + @Syntax("") + @Description("") + PropertyModifyAction action, + + @Syntax("") + @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()); + }); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandCompletions.java b/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandCompletions.java index 2a7eda66..63b9f69e 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandCompletions.java +++ b/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandCompletions.java @@ -27,6 +27,7 @@ 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; @@ -60,9 +61,12 @@ class MVCommandCompletions extends PaperCommandCompletions { registerAsyncCompletion("flags", this::suggestFlags); registerStaticCompletion("gamemodes", suggestEnums(GameMode.class)); registerStaticCompletion("gamerules", this::suggestGamerules); - registerStaticCompletion("mvconfigs", config.getStringPropertyHandle().getPropertyNames()); + 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); @@ -124,7 +128,7 @@ class MVCommandCompletions extends PaperCommandCompletions { private Collection suggestMVConfigValues(BukkitCommandCompletionContext context) { return Try.of(() -> context.getContextValue(String.class)) .map(propertyName -> config.getStringPropertyHandle() - .getPropertySuggestedValues(propertyName, context.getInput())) + .getSuggestedPropertyValue(propertyName, context.getInput(), PropertyModifyAction.SET)) .getOrElse(Collections.emptyList()); } @@ -172,6 +176,23 @@ class MVCommandCompletions extends PaperCommandCompletions { return Collections.emptyList(); } + private Collection 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 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 > Collection suggestEnums(Class enumClass) { return EnumSet.allOf(enumClass).stream() .map(Enum::name) diff --git a/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandContexts.java b/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandContexts.java index 1807403e..e12c54b3 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandContexts.java +++ b/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandContexts.java @@ -252,7 +252,7 @@ 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()).getOrNull(); } if (context.isOptional()) { return null; diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/handle/CommentedYamlConfigHandle.java b/src/main/java/org/mvplugins/multiverse/core/configuration/handle/CommentedYamlConfigHandle.java index 9766108d..85b98c73 100644 --- a/src/main/java/org/mvplugins/multiverse/core/configuration/handle/CommentedYamlConfigHandle.java +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/handle/CommentedYamlConfigHandle.java @@ -77,7 +77,7 @@ public class CommentedYamlConfigHandle extends FileConfigHandle { Logging.warning("Failed to set node " + valueNode.getPath() + " to " + valueNode.getDefaultValue()); - setDefault(valueNode); + reset(valueNode); }); } }); diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/handle/GenericConfigHandle.java b/src/main/java/org/mvplugins/multiverse/core/configuration/handle/GenericConfigHandle.java index ae523dec..a1b621a8 100644 --- a/src/main/java/org/mvplugins/multiverse/core/configuration/handle/GenericConfigHandle.java +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/handle/GenericConfigHandle.java @@ -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,6 +10,7 @@ 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.ListValueNode; import org.mvplugins.multiverse.core.configuration.node.NodeGroup; import org.mvplugins.multiverse.core.configuration.node.ValueNode; @@ -105,12 +107,53 @@ public abstract class GenericConfigHandle { } /** - * Gets the configuration. Mainly used for {@link StringPropertyHandle}. + * Adds an item to a list node. * - * @return The configuration. + * @param node The list node to add the item to. + * @param itemValue The value of the item to add. + * @param The type of the list item. + * @return Empty try if the item was added, try containing an error otherwise. */ - @NotNull NodeGroup getNodes() { - return nodes; + public Try add(@NotNull ListValueNode 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 The type of the list item. + * @return Empty try if the item was removed, try containing an error otherwise. + */ + public Try remove(@NotNull ListValueNode 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; + }); } /** @@ -118,9 +161,19 @@ public abstract class GenericConfigHandle { * * @param node The node to set the default value of. * @param 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 Try reset(@NotNull ValueNode 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; } /** diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/handle/PropertyModifyAction.java b/src/main/java/org/mvplugins/multiverse/core/configuration/handle/PropertyModifyAction.java new file mode 100644 index 00000000..54a13b8e --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/handle/PropertyModifyAction.java @@ -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 +} diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/handle/StringPropertyHandle.java b/src/main/java/org/mvplugins/multiverse/core/configuration/handle/StringPropertyHandle.java index 72955a76..8ee6dcba 100644 --- a/src/main/java/org/mvplugins/multiverse/core/configuration/handle/StringPropertyHandle.java +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/handle/StringPropertyHandle.java @@ -3,11 +3,13 @@ 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; @@ -31,10 +33,31 @@ public class StringPropertyHandle { * * @return The names of all properties in this handle. */ - public Collection getPropertyNames() { + public Collection 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 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. * @@ -52,10 +75,26 @@ public class StringPropertyHandle { * @param input The current user input. * @return A collection of possible string values. */ - public Collection getPropertySuggestedValues(@Nullable String name, @Nullable String input) { - return findNode(name, ValueNode.class) - .map(node -> node.suggest(input)) - .getOrElse(Collections.emptyList()); + public Collection 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(); + }; } /** @@ -69,7 +108,7 @@ public class StringPropertyHandle { } /** - * Sets the value of a node, if the validator is not null, it will be tested first. + * Sets the value of a node. * * @param name The name of the node. * @param value The value to set. @@ -80,7 +119,58 @@ public class StringPropertyHandle { } /** - * Sets the string value of a node, if the validator is not null, it will be tested first. + * 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 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 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 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 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. @@ -92,6 +182,51 @@ public class StringPropertyHandle { .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 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 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 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 Try findNode(@Nullable String name, @NotNull Class type) { return handle.getNodes().findNode(name, type) .toTry(() -> new ConfigNodeNotFoundException(name)); diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/node/ConfigNode.java b/src/main/java/org/mvplugins/multiverse/core/configuration/node/ConfigNode.java index 3aed60b5..57a18454 100644 --- a/src/main/java/org/mvplugins/multiverse/core/configuration/node/ConfigNode.java +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/node/ConfigNode.java @@ -42,11 +42,11 @@ public class ConfigNode extends ConfigHeaderNode implements ValueNode { protected final @Nullable String name; protected final @NotNull Class type; protected final @Nullable Supplier defaultValue; - protected final @Nullable NodeSuggester suggester; - protected final @Nullable NodeStringParser stringParser; - protected final @Nullable NodeSerializer serializer; - protected final @Nullable Function> validator; - protected final @Nullable BiConsumer onSetValue; + protected @Nullable NodeSuggester suggester; + protected @Nullable NodeStringParser stringParser; + protected @Nullable NodeSerializer serializer; + protected @Nullable Function> validator; + protected @Nullable BiConsumer onSetValue; protected ConfigNode( @NotNull String path, diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/node/ListConfigNode.java b/src/main/java/org/mvplugins/multiverse/core/configuration/node/ListConfigNode.java new file mode 100644 index 00000000..3c513fad --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/node/ListConfigNode.java @@ -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 The type of the value. + */ +public class ListConfigNode extends ConfigNode> implements ListValueNode { + + /** + * Creates a new builder for a {@link ConfigNode}. + * + * @param path The path of the node. + * @param type The type of the value. + * @param The type of the value. + * @return The new builder. + */ + public static @NotNull Builder> listBuilder( + @NotNull String path, + @NotNull Class type) { + return new Builder<>(path, type); + } + + protected final Class itemType; + protected final NodeSuggester itemSuggester; + protected final NodeStringParser itemStringParser; + protected final NodeSerializer itemSerializer; + protected final Function> itemValidator; + protected final BiConsumer onSetItemValue; + + protected ListConfigNode( + @NotNull String path, + @NotNull String[] comments, + @Nullable String name, + @NotNull Class> type, + @Nullable Supplier> defaultValueSupplier, + @Nullable NodeSuggester suggester, + @Nullable NodeStringParser> stringParser, + @Nullable NodeSerializer> serializer, + @Nullable Function, Try> validator, + @Nullable BiConsumer, List> onSetValue, + @NotNull Class itemType, + @Nullable NodeSuggester itemSuggester, + @Nullable NodeStringParser itemStringParser, + @Nullable NodeSerializer itemSerializer, + @Nullable Function> itemValidator, + @Nullable BiConsumer 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 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 deserialize(Object object, Class> type) { + if (object instanceof List list) { + return list.stream().map(item -> itemSerializer.deserialize(item, itemType)).toList(); + } + return new ArrayList<>(); + } + + @Override + public Object serialize(List object, Class> 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 getItemType() { + return itemType; + } + + /** + * {@inheritDoc} + */ + @Override + public @NotNull Collection suggestItem(@Nullable String input) { + if (itemSuggester != null) { + return itemSuggester.suggest(input); + } + return Collections.emptyList(); + } + + /** + * {@inheritDoc} + */ + @Override + public @NotNull Try 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 getItemSerializer() { + return itemSerializer; + } + + /** + * {@inheritDoc} + */ + @Override + public Try 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> extends ConfigNode.Builder, B> { + + protected final @NotNull Class itemType; + protected @Nullable NodeSuggester itemSuggester; + protected @Nullable NodeStringParser itemStringParser; + protected @Nullable NodeSerializer itemSerializer; + protected @Nullable Function> itemValidator; + protected @Nullable BiConsumer 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 itemType) { + //noinspection unchecked + super(path, (Class>) (Object) List.class); + this.itemType = itemType; + this.defaultValue = () -> (List) 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 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 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> 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 onSetItemValue) { + this.onSetItemValue = onSetItemValue; + return self(); + } + + /** + * {@inheritDoc} + */ + @Override + public @NotNull ListConfigNode build() { + return new ListConfigNode<>( + path, + comments.toArray(new String[0]), + name, + type, + defaultValue, + suggester, + stringParser, + serializer, + validator, + onSetValue, + itemType, + itemSuggester, itemStringParser, itemSerializer, + itemValidator, + onSetItemValue); + } + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/node/ListValueNode.java b/src/main/java/org/mvplugins/multiverse/core/configuration/node/ListValueNode.java new file mode 100644 index 00000000..a16b9bce --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/node/ListValueNode.java @@ -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 The type of list item. + */ +public interface ListValueNode extends ValueNode> { + + /** + * Gets the class type of list item. + * + * @return The class type of list item. + */ + @NotNull Class getItemType(); + + /** + * Suggests possible string values for this node. + * + * @param input The input string. + * @return A collection of possible string values. + */ + @NotNull Collection 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 parseItemFromString(@Nullable String input); + + /** + * Gets the serializer for this node. + * + * @return The serializer for this node. + */ + @Nullable NodeSerializer getItemSerializer(); + + /** + * Validates the value of this node. + * + * @param value The value to validate. + * @return True if the value is valid, false otherwise. + */ + Try 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); +} diff --git a/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorld.java b/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorld.java index 75b9cd58..6e8b7ae7 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorld.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/MultiverseWorld.java @@ -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 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 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 setProperty(String name, Object value) { - return worldConfig.setProperty(name, value); + public StringPropertyHandle getStringPropertyHandle() { + return worldConfig.getStringPropertyHandle(); } /** diff --git a/src/main/java/org/mvplugins/multiverse/core/world/config/WorldConfig.java b/src/main/java/org/mvplugins/multiverse/core/world/config/WorldConfig.java index 827606f8..fd6870bd 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/config/WorldConfig.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/config/WorldConfig.java @@ -112,22 +112,14 @@ public final class WorldConfig { return configHandle.load(section); } + public StringPropertyHandle getStringPropertyHandle() { + return stringPropertyHandle; + } + public String getWorldName() { return worldName; } - public Collection getConfigurablePropertyNames() { - return configNodes.getNodes().getNames(); - } - - public Try getProperty(String name) { - return stringPropertyHandle.getProperty(name); - } - - public Try setProperty(String name, Object value) { - return stringPropertyHandle.setProperty(name, value); - } - public boolean getAdjustSpawn() { return configHandle.get(configNodes.ADJUST_SPAWN); } diff --git a/src/main/java/org/mvplugins/multiverse/core/world/config/WorldConfigNodes.java b/src/main/java/org/mvplugins/multiverse/core/world/config/WorldConfigNodes.java index 008d7e86..6e0fd5f0 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/config/WorldConfigNodes.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/config/WorldConfigNodes.java @@ -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; @@ -12,6 +11,7 @@ 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.LoadedMultiverseWorld; @@ -212,8 +212,8 @@ public class WorldConfigNodes { }) .build()); - final ConfigNode SPAWNING_ANIMALS_EXCEPTIONS = node(ConfigNode - .builder("spawning.animals.exceptions", List.class) + final ListConfigNode 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 SPAWNING_MONSTERS_EXCEPTIONS = node(ConfigNode - .builder("spawning.monsters.exceptions", List.class) + final ListConfigNode SPAWNING_MONSTERS_EXCEPTIONS = node(ListConfigNode + .listBuilder("spawning.monsters.exceptions", String.class) .defaultValue(new ArrayList<>()) .name("spawning-monsters-exceptions") .build()); - final ConfigNode WORLD_BLACKLIST = node(ConfigNode.builder("world-blacklist", List.class) - .defaultValue(new ArrayList<>()) + final ListConfigNode WORLD_BLACKLIST = node(ListConfigNode.listBuilder("world-blacklist", String.class) .build()); final ConfigNode VERSION = node(ConfigNode.builder("version", Double.class) diff --git a/src/main/java/org/mvplugins/multiverse/core/world/helpers/DataStore.java b/src/main/java/org/mvplugins/multiverse/core/world/helpers/DataStore.java index 08f6f994..c121946c 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/helpers/DataStore.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/helpers/DataStore.java @@ -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 { @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 { 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; } } diff --git a/src/test/java/org/mvplugins/multiverse/core/world/WorldConfigMangerTest.kt b/src/test/java/org/mvplugins/multiverse/core/world/WorldConfigMangerTest.kt index 9a23bd79..1648bf23 100644 --- a/src/test/java/org/mvplugins/multiverse/core/world/WorldConfigMangerTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/world/WorldConfigMangerTest.kt @@ -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) } diff --git a/src/test/java/org/mvplugins/multiverse/core/world/WorldConfigTest.kt b/src/test/java/org/mvplugins/multiverse/core/world/WorldConfigTest.kt index d4b76a62..bef9ad6b 100644 --- a/src/test/java/org/mvplugins/multiverse/core/world/WorldConfigTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/world/WorldConfigTest.kt @@ -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) } } diff --git a/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt b/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt index bfe857e2..52cdb3e2 100644 --- a/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt @@ -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